├── .github ├── renovate.json5 └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── api.go ├── docs ├── demo.gif ├── demo_android.jpg └── demo_browser.png ├── drawsdf.go ├── ebiten.go ├── entrypoint.go ├── examples ├── cylinder_head │ └── main.go └── spiral │ ├── cmr10.ttf │ └── main.go ├── fsnotifysupported.go ├── fsnotifyunsupported.go ├── go.mod ├── go.sum ├── impl2.go ├── impl2_test.go ├── impl3.go ├── impl3_test.go ├── impl3mesh.go ├── implcommon.go ├── implremote.go ├── interaction.go ├── internal ├── reflect.go ├── reflect2.go ├── reflect3.go ├── reflect_test.go ├── remote.go └── state.go ├── option.go ├── run.go ├── runsignals_linux.go ├── runsignals_other.go └── util.go /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":disableDependencyDashboard" 6 | ], 7 | "automerge": true, 8 | "automergeType": "branch", 9 | "schedule": [ 10 | "before 3am on Saturday" 11 | ], 12 | "lockFileMaintenance": { 13 | "enabled": true, 14 | "schedule": [ 15 | "before 3am on Saturday" 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go test 2 | 3 | on: 4 | push: 5 | branches: [main, renovate/**] 6 | pull_request: 7 | branches: [main, renovate/**] 8 | 9 | jobs: 10 | 11 | build-and-test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: 1.23 20 | 21 | - name: Install Ebiten dependencies 22 | run: sudo apt-get update && sudo apt-get install -y libx11-dev libgl1-mesa-dev libasound2-dev libgles2-mesa-dev libalut-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev libxxf86vm-dev xvfb 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Go test (with virtual display) 28 | run: xvfb-run go test -v ./... 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Custom 2 | /.idea 3 | 4 | ### Go template 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Yeicor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SDFX-UI 2 | 3 | ![GitHub go.mod Go version of a Go module](https://img.shields.io/github/go-mod/go-version/Yeicor/sdfx-ui.svg) 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/Yeicor/sdfx-ui.svg)](https://pkg.go.dev/github.com/Yeicor/sdfx-ui) 5 | [![Go](https://github.com/Yeicor/sdfx-ui/actions/workflows/go.yml/badge.svg)](https://github.com/Yeicor/sdfx-ui/actions/workflows/go.yml) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/Yeicor/sdfx-ui)](https://goreportcard.com/report/github.com/Yeicor/sdfx-ui) 7 | [![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://mit-license.org/) 8 | 9 | > :warning: **This project is superseded by the [SDF Viewer](https://github.com/Yeicor/sdf-viewer) App with 10 | the [SDF Viewer Go](https://github.com/Yeicor/sdf-viewer-go) integration. The advantages of the new project are mainly 11 | the real-time rendering speed (GPU-accelerated), flexibility (easy to integrate with other languages and libraries), and 12 | even more features (parameter customization, custom procedural textures...).** 13 | 14 | SDFX-UI is a SDF (2D and 3D) renderer intended for fast development iterations that renders directly to a window. It 15 | integrates with your code as a library, meaning that you define your surface and then call the method that starts this 16 | UI. 17 | 18 | The UI will listen for code updates (you can use your preferred code editor) and displays the new surfaces automatically 19 | with the same state (e.g., camera position). 20 | 21 | Pull requests are welcome! The priority is to improve the user experience (making a proper UI or adding new 22 | capabilities) 23 | without losing performance. 24 | 25 | ## Demo 26 | 27 | Configuring the Renderer is as simple as [creating your signed distance function](https://github.com/deadsy/sdfx) and 28 | calling `ui.NewRenderer(anySDF).Run()`. 29 | 30 | ```go 31 | package main 32 | 33 | import ( 34 | "github.com/Yeicor/sdfx-ui" 35 | "github.com/deadsy/sdfx/sdf" 36 | "github.com/deadsy/sdfx/vec/v3" 37 | ) 38 | 39 | func main() { 40 | anySDF, _ := sdf.Box3D(v3.Vec{X: 1, Y: 1, Z: 1}, 0.2) 41 | _ = ui.NewRenderer(anySDF).Run() // Error handling ignored 42 | } 43 | ``` 44 | 45 | This is a demo of the workflow for designing your own SDF using this UI (code at examples/spiral/main.go): 46 | 47 | ![Demo video](docs/demo.gif) 48 | 49 | ## How does it work? 50 | 51 | The first time you run the code, it starts the renderer process. It also starts listening for code changes. When a code 52 | change is detected, the app is recompiled by the renderer (taking advantage of Go's fast compilation times) and quickly 53 | renders the new surface to the same window (with the same camera position and other settings). 54 | 55 | The SDF2 renderer shows the value of the SDF on each pixel using a grayscale: where bright pixels indicate outside the 56 | object and darker pixels are inside. The camera can be moved and scaled (using the mouse), rendering only the 57 | interesting part of the SDF. 58 | 59 | SDF3s are raycasted from a perspective arc-ball camera that can be rotated around a pivot point, move its pivot and move 60 | closer or farther away from the pivot (using Blender-like mouse controls). Note that only the shown surface is actually 61 | rendered thanks to raycasting from the camera. This also means that the resulting surface can be much more detailed 62 | (depending on chosen resolution) than the triangle meshes generated by standard mesh generators. 63 | 64 | There is an alternative SDF3 renderer (enabled via `ui.Opt3Mesh(...)`) which generates a triangle mesh using an 65 | algorithm from SDFX and then renders that triangle mesh to the screen thanks 66 | to [FauxGL](https://github.com/fogleman/fauxgl). It is a software renderer that is still very fast for our purposes 67 | (faster than the required mesh generation to use it). The main advantage over the raycast renderer is very fast camera 68 | movements and parameter updates, with the disadvantages of low (limited) detail and slower updates (as the initial mesh 69 | generation is slow). 70 | 71 | SDFX-UI uses [Ebiten](https://github.com/hajimehoshi/ebiten) for window management and rendering. Ebiten is 72 | cross-platform, so it could also be used to showcase a surface (without automatic updates) creating an application for 73 | desktop, web, mobile or Nintendo Switch™. 74 | 75 | ### Browser and mobile 76 | 77 | They use the same code as the demo, see compilation instructions at examples/spiral/main.go. 78 | 79 | ![Screenshot_20220107_234547](docs/demo_browser.png) 80 | ![Screenshot_20220107-234815220](docs/demo_android.jpg) 81 | 82 | Note that mobile only works with mouse and keyboard for now. 83 | 84 | ### Other demos 85 | 86 | Show bounding boxes: 87 | 88 | ![Screenshot_20220226_205856](https://user-images.githubusercontent.com/4929005/155857780-89552844-6021-4cfb-aff7-9ce53461c34f.png) 89 | 90 | ## Related projects 91 | 92 | - [sdf3ui](https://github.com/soypat/sdf3ui): browser based 3D model visualizer for use 93 | with [a similar SDF library](https://github.com/soypat/sdf). 94 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/Yeicor/sdfx-ui/internal" 5 | "github.com/deadsy/sdfx/sdf" 6 | "math" 7 | ) 8 | 9 | func (r *Renderer) newRendererState() *internal.RendererState { 10 | r.implLock.RLock() 11 | defer r.implLock.RUnlock() 12 | s := &internal.RendererState{ 13 | ResInv: 4, 14 | Bb: toBox2(r.impl.BoundingBox()), // 100% zoom (will fix aspect ratio later) 15 | } 16 | resetCam3(s, r) 17 | return s 18 | } 19 | 20 | func cam3MatrixNoTranslation(s *internal.RendererState) sdf.M44 { 21 | return sdf.RotateZ(s.CamYaw).Mul(sdf.RotateX(s.CamPitch)) 22 | } 23 | 24 | func resetCam3(s *internal.RendererState, r *Renderer) { 25 | s.CamCenter = r.impl.BoundingBox().Center() 26 | s.CamDist = r.impl.BoundingBox().Size().Length() / 2 27 | s.CamPitch = -math.Pi / 4 // Look from 45º up 28 | s.CamYaw = -math.Pi / 4 // Look from 45º right 29 | } 30 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeicor/sdfx-ui/1d6bf29fab655ef18ca6d2b71b36073e18c92fb0/docs/demo.gif -------------------------------------------------------------------------------- /docs/demo_android.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeicor/sdfx-ui/1d6bf29fab655ef18ca6d2b71b36073e18c92fb0/docs/demo_android.jpg -------------------------------------------------------------------------------- /docs/demo_browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeicor/sdfx-ui/1d6bf29fab655ef18ca6d2b71b36073e18c92fb0/docs/demo_browser.png -------------------------------------------------------------------------------- /drawsdf.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "context" 5 | "github.com/Yeicor/sdfx-ui/internal" 6 | "github.com/deadsy/sdfx/sdf" 7 | v2 "github.com/deadsy/sdfx/vec/v2" 8 | "github.com/deadsy/sdfx/vec/v2i" 9 | "github.com/hajimehoshi/ebiten" 10 | "image" 11 | "image/color" 12 | "log" 13 | "math" 14 | "runtime/debug" 15 | "time" 16 | ) 17 | 18 | func (r *Renderer) drawSDF(screen *ebiten.Image) { 19 | // Draw latest SDF render (and overlay the latest partial render) 20 | r.implStateLock.RLock() 21 | defer r.implStateLock.RUnlock() 22 | r.cachedRenderLock.RLock() 23 | defer r.cachedRenderLock.RUnlock() 24 | drawOpts := &ebiten.DrawImageOptions{} 25 | var tr v2.Vec 26 | if r.translateFrom.X != math.MaxInt && !r.smoothCamera { // Preview translations without rendering (until mouse release) 27 | cx, cy := getCursor() 28 | if r.translateFromStop.X != math.MaxInt { 29 | cx, cy = r.translateFromStop.X, r.translateFromStop.Y 30 | } 31 | tr = v2.Vec{X: float64(cx), Y: float64(cy)}.Sub(v2.Vec{X: float64(r.translateFrom.X), Y: float64(r.translateFrom.Y)}).DivScalar(float64(r.implState.ResInv)) 32 | // TODO: Place SDF2 render at the right location during special renders (zooming, changing resolution) 33 | } 34 | drawOpts.GeoM.Translate(tr.X, tr.Y) 35 | cachedRenderWidth, cachedRenderHeight := r.cachedRender.Size() 36 | drawOpts.GeoM.Scale(float64(r.screenSize.X)/float64(cachedRenderWidth), float64(r.screenSize.Y)/float64(cachedRenderHeight)) 37 | err := screen.DrawImage(r.cachedRender, drawOpts) 38 | if err != nil { 39 | panic(err) // Can this happen? 40 | } 41 | drawOpts.GeoM.Reset() 42 | cachedPartialRenderWidth, cachedPartialRenderHeight := r.cachedPartialRender.Size() 43 | drawOpts.GeoM.Scale(float64(r.screenSize.X)/float64(cachedPartialRenderWidth), float64(r.screenSize.Y)/float64(cachedPartialRenderHeight)) 44 | err = screen.DrawImage(r.cachedPartialRender, drawOpts) 45 | if err != nil { 46 | panic(err) // Can this happen? 47 | } 48 | } 49 | 50 | // rerender will discard any current rendering and start a new render (use it when something changes). 51 | // It does not lock execution (queues a new render in background). 52 | func (r *Renderer) rerender(callbacks ...func(err error)) { 53 | r.rerenderOpt(true, callbacks...) 54 | } 55 | 56 | func (r *Renderer) rerenderOpt(forceCancel bool, callbacks ...func(err error)) { 57 | r.cachedRenderLock.RLock() 58 | if r.cachedRender == nil { 59 | log.Println("Trying to render too soon (before first Update())") 60 | debug.PrintStack() 61 | } 62 | r.cachedRenderLock.RUnlock() 63 | go func(callbacks ...func(err error)) { 64 | var err error 65 | defer func() { 66 | for _, callback := range callbacks { 67 | callback(err) 68 | } 69 | }() 70 | //lint:ignore SA1012 package API allows this 71 | if !r.renderingLock.TryLock(nil) { 72 | if !forceCancel { 73 | return 74 | } 75 | r.implStateLock.RLock() // Avoid race condition with creating a new context 76 | r.renderingCtxCancel() 77 | r.implStateLock.RUnlock() 78 | r.renderingLock.Lock() // Wait for previous render to finish (should be very fast) 79 | } 80 | defer r.renderingLock.Unlock() 81 | var renderCtx context.Context 82 | r.implStateLock.Lock() 83 | renderCtx, r.renderingCtxCancel = context.WithCancel(context.Background()) 84 | renderSize := v2i.Vec{X: int(float64(r.screenSize.X) / float64(r.implState.ResInv)), Y: int(float64(r.screenSize.Y) / float64(r.implState.ResInv))} 85 | r.implStateLock.Unlock() 86 | partialRenders := make(chan *image.RGBA) 87 | r.goPartialRendersHandler(partialRenders, renderSize) 88 | renderStartTime := time.Now() 89 | r.implStateLock.RLock() 90 | sameSize := r.cachedRenderCPU != nil && (v2i.Vec{r.cachedRenderCPU.Rect.Max.X, r.cachedRenderCPU.Rect.Max.Y} == renderSize) 91 | if !sameSize { 92 | r.cachedRenderCPU = image.NewRGBA(image.Rect(0, 0, renderSize.X, renderSize.Y)) 93 | } 94 | implState := r.implState 95 | if r.smoothCamera { 96 | if r.translateFrom.X != math.MaxInt { 97 | cx, cy := getCursor() 98 | switch r.implDimCache { 99 | case 2: 100 | implState = r.apply2DCameraMoveTo(cx, cy) 101 | case 3: 102 | implState = r.apply3DCameraMoveTo(cx, cy) 103 | } 104 | } 105 | } 106 | r.implStateLock.RUnlock() 107 | r.implLock.RLock() 108 | err = r.impl.Render(&internal.RenderArgs{ 109 | Ctx: renderCtx, 110 | State: implState, 111 | StateLock: r.implStateLock, 112 | CachedRenderLock: r.cachedRenderLock, 113 | PartialRenders: partialRenders, 114 | FullRender: r.cachedRenderCPU, 115 | }) 116 | r.implLock.RUnlock() 117 | if err != nil { 118 | if err != context.Canceled { 119 | log.Println("[DevRenderer] Error rendering:", err) 120 | } 121 | return 122 | } 123 | renderGPUStartTime := time.Now() 124 | renderGpuImg, err := ebiten.NewImageFromImage(r.cachedRenderCPU, ebiten.FilterDefault) 125 | if err != nil { 126 | log.Println("Error sending image to GPU:", err) 127 | return 128 | } 129 | log.Println("[DevRenderer] CPU Render took:", renderGPUStartTime.Sub(renderStartTime), "- Sending to GPU took:", time.Since(renderGPUStartTime)) 130 | r.implLock.RLock() 131 | r.implStateLock.Lock() // WARNING: Locking order (to avoid deadlocks) 132 | r.implDimCache = r.impl.Dimensions() // Only updated here 133 | if r.implDimCache == 2 { 134 | r.cachedRenderBb2 = r.implState.Bb 135 | } else { 136 | r.cachedRenderBb2 = sdf.Box2{} 137 | } 138 | r.implStateLock.Unlock() 139 | r.implLock.RUnlock() 140 | r.cachedRenderLock.Lock() 141 | // Need to resize the rendering result: overwrite 142 | r.cachedRender = renderGpuImg 143 | // TODO: reuse the previous render for the parts that did not change (SDF2 only) 144 | r.cachedRenderLock.Unlock() 145 | }(callbacks...) 146 | } 147 | 148 | func (r *Renderer) goPartialRendersHandler(partialRenders chan *image.RGBA, renderSize v2i.Vec) { 149 | go func(renderSize v2i.Vec) { 150 | partialRenderCopy := image.NewRGBA(image.Rect(0, 0, renderSize.X, renderSize.Y)) 151 | lastPartialRender := time.Now() 152 | for partialRender := range partialRenders { 153 | if time.Since(lastPartialRender) < r.partialRenderEvery { 154 | continue // Skip this partial render (throttled) as it slows down significantly the full render 155 | } 156 | lastPartialRender = time.Now() 157 | r.cachedRenderLock.RLock() 158 | copy(partialRenderCopy.Pix, partialRender.Pix) 159 | r.cachedRenderLock.RUnlock() 160 | // WARNING: This blocks the main rendering thread: call sparingly 161 | gpuImg, err := ebiten.NewImageFromImage(partialRenderCopy, ebiten.FilterDefault) 162 | if err != nil { 163 | log.Println("Error sending image to GPU:", err) 164 | continue 165 | } 166 | r.cachedRenderLock.Lock() 167 | r.cachedPartialRender = gpuImg 168 | r.cachedRenderLock.Unlock() 169 | } 170 | r.cachedRenderLock.Lock() // Use the cached render as the partial one (to make sure it is complete) 171 | err := r.cachedPartialRender.Fill(color.Transparent) 172 | if err != nil { 173 | log.Println("cachedPartialRender.Fill(color.Transparent) error:", err) 174 | } 175 | r.cachedRenderLock.Unlock() 176 | }(renderSize) 177 | } 178 | -------------------------------------------------------------------------------- /ebiten.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/deadsy/sdfx/vec/v2i" 5 | "github.com/hajimehoshi/ebiten" 6 | ) 7 | 8 | // rendererEbitenGame hides the private ebiten implementation while behaving like a *Renderer internally 9 | type rendererEbitenGame struct { 10 | *Renderer 11 | } 12 | 13 | func (r rendererEbitenGame) Update(_ *ebiten.Image) error { 14 | var err error 15 | r.cachedRenderLock.RLock() 16 | firstFrame := r.cachedRender == nil 17 | if firstFrame { // This always runs before the first frame 18 | r.cachedRender, err = ebiten.NewImage(1, 1, ebiten.FilterDefault) 19 | if err != nil { 20 | return err 21 | } 22 | r.cachedPartialRender = r.cachedRender 23 | } 24 | r.cachedRenderLock.RUnlock() 25 | r.onUpdateInputs() 26 | return nil 27 | } 28 | 29 | func (r rendererEbitenGame) Draw(screen *ebiten.Image) { 30 | r.drawSDF(screen) 31 | r.drawUI(screen) 32 | } 33 | 34 | func (r rendererEbitenGame) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { 35 | r.cachedRenderLock.RLock() 36 | firstFrame := r.cachedRender == nil 37 | r.cachedRenderLock.RUnlock() 38 | r.implStateLock.RLock() 39 | defer r.implStateLock.RUnlock() 40 | if !firstFrame { // Layout is called before Update(), but don't render in this case 41 | newScreenSize := v2i.Vec{X: outsideWidth, Y: outsideHeight} 42 | if r.screenSize != newScreenSize { 43 | r.screenSize = newScreenSize 44 | r.rerender() 45 | } 46 | } 47 | return outsideWidth, outsideHeight // Use all available pixels, no re-scaling (unless ResInv is modified) 48 | } 49 | -------------------------------------------------------------------------------- /entrypoint.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/Yeicor/sdfx-ui/internal" 5 | "github.com/cenkalti/backoff/v4" 6 | "github.com/deadsy/sdfx/sdf" 7 | "github.com/deadsy/sdfx/vec/v2i" 8 | "github.com/hajimehoshi/ebiten" 9 | "github.com/subchen/go-trylock/v2" 10 | "image" 11 | "math" 12 | "os" 13 | "os/exec" 14 | "sync" 15 | "time" 16 | ) 17 | 18 | // Renderer is a SDF2/SDF3 renderer intended for fast development iterations that renders directly to a window. 19 | // The first time, it starts the renderer process. It also starts listening for code changes. 20 | // When a code change is detected, the app is recompiled (taking advantage of go's fast compilation times) 21 | // by the renderer and communicates directly to the renderer, providing the new surface data to the previous window. 22 | // 23 | // It allows very fast SDF updates (saving camera position) whenever the code changes, speeding up the modelling process. 24 | // The renderer is mainly CPU-based (with a resolution parameter to control speed vs quality), as sdfx is also CPU-based. 25 | // The scene is only rendered when something changes, as rendering SDFs with good quality is not real-time. 26 | // 27 | // The SDF2 renderer is based on the PNG renderer, showing the image directly on screen (without creating the PNG file). 28 | // The camera can be moved and scaled (using the mouse), rendering only the interesting part of the SDF. 29 | // 30 | // SDF3s are raycasted from a perspective arc-ball camera that can be rotated around a pivot point, move its pivot and 31 | // move closer or farther away from the pivot (using Blender-like mouse controls). 32 | // Note that only the shown surface is actually rendered thanks to raycasting from the camera. 33 | // This also means that the resulting surface can be much more detailed (depending on chosen resolution) 34 | // than the triangle meshes generated by standard renderers. 35 | // 36 | // TODO: Once merged, use max-resolution runtime-computed VoxelSdf2 and VoxelSdf3 to accelerate camera movements 37 | // 38 | // It uses [ebiten](https://github.com/hajimehoshi/ebiten) for rendering, which is cross-platform, so it could also 39 | // be used to showcase a surface (without automatic updates) creating an application for desktop, web or mobile. 40 | type Renderer struct { 41 | impl internal.DevRendererImpl // the implementation to use SDF2/SDF3/remote process. 42 | implDimCache int // the number of dimensions of impl (cached to avoid remote calls every frame) 43 | implLock *sync.RWMutex // the implementation lock 44 | implState *internal.RendererState // the renderer's state, so impl can be swapped while keeping the state. 45 | implStateLock *sync.RWMutex // the renderer's state lock 46 | cachedRender *ebiten.Image // the latest cached render (to avoid rendering every frame, or frame parts even if nothing changed) 47 | cachedRenderCPU *image.RGBA // the latest cached render (to avoid rendering every frame, or frame parts even if nothing changed) 48 | cachedRenderBb2 sdf.Box2 // what part of the SDF2 the latest cached render represents (not implemented, and no equivalent optimization available for SDF3s) 49 | cachedPartialRender *ebiten.Image // the latest partial render (to display render progress visually) 50 | cachedRenderLock *sync.RWMutex // the lock over tha partial render 51 | screenSize v2i.Vec // the screen ResInv 52 | renderingCtxCancel func() // non-nil if we are currently rendering 53 | renderingLock trylock.TryLocker // locked when we are rendering, use renderingCtx to cancel the previous render 54 | translateFrom v2i.Vec // Translate/rotate (for 3D) screen space start 55 | translateFromStop v2i.Vec // Translate/rotate (for 3D) screen space end (recorded while processing the new frame) 56 | // Static configuration 57 | runCmd func() *exec.Cmd // generates a new command to compile and run the code for the new SDF 58 | watchFiles []string // the files to watch for recompilation of new code 59 | backOff backoff.BackOff // the backoff to connect to the new process after recompilation 60 | partialRenderEvery time.Duration // how much time to wait between partial render updates to screen 61 | zoomFactor float64 // how much to scale the SDF2/SDF3 on each zoom operation (> 1) 62 | smoothCamera bool // whether to render while moving the camera (for 2D and 3D) 63 | } 64 | 65 | // NewRenderer see Renderer 66 | func NewRenderer(anySDF interface{}, opts ...Option) *Renderer { 67 | r := &Renderer{ 68 | implLock: &sync.RWMutex{}, 69 | implStateLock: &sync.RWMutex{}, 70 | cachedRenderLock: &sync.RWMutex{}, 71 | renderingLock: trylock.New(), 72 | translateFrom: v2i.Vec{math.MaxInt, math.MaxInt}, 73 | translateFromStop: v2i.Vec{math.MaxInt, math.MaxInt}, 74 | // Configuration 75 | runCmd: func() *exec.Cmd { 76 | return exec.Command("go", "run", "-v", ".") 77 | }, 78 | watchFiles: []string{"."}, 79 | backOff: backoff.NewExponentialBackOff(), 80 | partialRenderEvery: time.Second, 81 | zoomFactor: 1.25, 82 | } 83 | r.backOff.(*backoff.ExponentialBackOff).InitialInterval = 10 * time.Millisecond 84 | switch s := anySDF.(type) { 85 | case sdf.SDF2: 86 | r.impl = newDevRenderer2(s) 87 | r.cachedRenderBb2 = s.BoundingBox() 88 | case sdf.SDF3: 89 | r.impl = newDevRenderer3(s) 90 | default: 91 | panic("anySDF must be either a SDF2 or a SDF3") 92 | } 93 | r.implDimCache = r.impl.Dimensions() 94 | r.implState = r.newRendererState() 95 | r.implState.ReflectTree = r.impl.ReflectTree() // Compute the reflection-based tree once on load and cache it 96 | // Apply all configuration options 97 | for _, opt := range opts { 98 | opt(r) 99 | } 100 | return r 101 | } 102 | 103 | const requestedAddressEnvKey = "SDFX_DEV_RENDERER_CHILD" 104 | 105 | // Run starts the UI or connects to a previous UI renderer and provides the new surface automatically 106 | func (r *Renderer) Run() error { 107 | requestedAddress := os.Getenv(requestedAddressEnvKey) 108 | if requestedAddress != "" { // Found a parent renderer (environment variable) 109 | return r.runChild(requestedAddress) 110 | } // Otherwise, listen for code changes to spawn a child renderer and create the local renderer 111 | return r.runRenderer(r.runCmd, r.watchFiles) 112 | } 113 | -------------------------------------------------------------------------------- /examples/cylinder_head/main.go: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | /* 3 | 4 | SOURCE: https://github.com/deadsy/sdfx/tree/master/examples/cylinder_head 5 | 6 | Wallaby Cylinder Head 7 | 8 | No draft version for 3d printing and lost-PLA investment casting. 9 | 10 | */ 11 | //----------------------------------------------------------------------------- 12 | 13 | package main 14 | 15 | import ( 16 | ui "github.com/Yeicor/sdfx-ui" 17 | "github.com/deadsy/sdfx/render" 18 | "github.com/deadsy/sdfx/sdf" 19 | v2 "github.com/deadsy/sdfx/vec/v2" 20 | v3 "github.com/deadsy/sdfx/vec/v3" 21 | "github.com/hajimehoshi/ebiten" 22 | "math" 23 | ) 24 | 25 | //----------------------------------------------------------------------------- 26 | 27 | //----------------------------------------------------------------------------- 28 | // scaling 29 | 30 | const desiredScale = 1.25 31 | const alShrink = 1.0 / 0.99 // ~1% 32 | const plaShrink = 1.0 / 0.998 //~0.2% 33 | 34 | // dimension scaling 35 | func dim(x float64) float64 { 36 | return x * desiredScale * sdf.MillimetresPerInch * alShrink * plaShrink 37 | } 38 | 39 | var generalRound = dim(0.1) 40 | 41 | //----------------------------------------------------------------------------- 42 | // exhaust bosses 43 | 44 | var ebSideRadius = dim(5.0 / 32.0) 45 | var ebMainRadius = dim(5.0 / 16.0) 46 | var ebHoleRadius = dim(3.0 / 16.0) 47 | var ebC2cDistance = dim(13.0 / 16.0) 48 | var ebDistance = ebC2cDistance / 2.0 49 | 50 | var ebXOffset = 0.5*(headLength+ebHeight) - ebHeight0 51 | var ebYOffset = (headWidth / 2.0) - ebDistance - ebSideRadius 52 | var ebZOffset = dim(1.0 / 16.0) 53 | 54 | var ebHeight0 = dim(1.0 / 16.0) 55 | var ebHeight1 = dim(1.0 / 8.0) 56 | var ebHeight = ebHeight0 + ebHeight1 57 | 58 | func exhaustBoss(mode string, xOfs float64) sdf.SDF3 { 59 | 60 | var s0 sdf.SDF2 61 | 62 | if mode == "body" { 63 | s0 = sdf.NewFlange1(ebDistance, ebMainRadius, ebSideRadius) 64 | } else if mode == "hole" { 65 | s0, _ = sdf.Circle2D(ebHoleRadius) 66 | } else { 67 | panic("bad mode") 68 | } 69 | 70 | s1 := sdf.Extrude3D(s0, ebHeight) 71 | m := sdf.RotateZ(sdf.DtoR(90)) 72 | m = sdf.RotateY(sdf.DtoR(90)).Mul(m) 73 | m = sdf.Translate3d(v3.Vec{X: xOfs, Y: ebYOffset, Z: ebZOffset}).Mul(m) 74 | s1 = sdf.Transform3D(s1, m) 75 | return s1 76 | } 77 | 78 | func exhaustBosses(mode string) sdf.SDF3 { 79 | return sdf.Union3D(exhaustBoss(mode, ebXOffset), exhaustBoss(mode, -ebXOffset)) 80 | } 81 | 82 | //----------------------------------------------------------------------------- 83 | // spark plug bosses 84 | 85 | var sp2spDistance = dim(1.0 + (5.0 / 8.0)) 86 | var spTheta = sdf.DtoR(30) 87 | 88 | var spBossR1 = dim(21.0 / 64.0) 89 | var spBossR2 = dim(15.0 / 32.0) 90 | var spBossH1 = dim(0.79) 91 | var spBossH2 = dim(0.94) 92 | var spBossH3 = dim(2) 93 | 94 | var spHoleD = dim(21.0 / 64.0) 95 | var spHoleR = spHoleD / 2.0 96 | var spHoleH = dim(1.0) 97 | 98 | var spCbH1 = dim(1.0) 99 | var spCbH2 = dim(2.0) 100 | var spCbR = dim(5.0 / 16.0) 101 | 102 | var spHyp = spHoleH + spCbR*math.Tan(spTheta) 103 | var spYOfs = spHyp*math.Cos(spTheta) - headWidth/2 104 | var spZOfs = -spHyp * math.Sin(spTheta) 105 | 106 | func sparkplug(mode string, xOfs float64) sdf.SDF3 { 107 | var vlist []v2.Vec 108 | if mode == "boss" { 109 | boss := sdf.NewPolygon() 110 | boss.Add(0, 0) 111 | boss.Add(spBossR1, 0) 112 | boss.Add(spBossR1, spBossH1).Smooth(spBossR1*0.3, 3) 113 | boss.Add(spBossR2, spBossH2).Smooth(spBossR2*0.3, 3) 114 | boss.Add(spBossR2, spBossH3) 115 | boss.Add(0, spBossH3) 116 | vlist = boss.Vertices() 117 | } else if mode == "hole" { 118 | vlist = []v2.Vec{ 119 | {0, 0}, 120 | {spHoleR, 0}, 121 | {spHoleR, spHoleH}, 122 | {0, spHoleH}, 123 | } 124 | } else if mode == "counterbore" { 125 | p := sdf.NewPolygon() 126 | p.Add(0, spCbH1) 127 | p.Add(spCbR, spCbH1).Smooth(spCbR/6.0, 3) 128 | p.Add(spCbR, spCbH2) 129 | p.Add(0, spCbH2) 130 | vlist = p.Vertices() 131 | } else { 132 | panic("bad mode") 133 | } 134 | s0, _ := sdf.Polygon2D(vlist) 135 | s, _ := sdf.Revolve3D(s0) 136 | m := sdf.RotateX(sdf.Pi/2 - spTheta) 137 | m = sdf.Translate3d(v3.Vec{X: xOfs, Y: spYOfs, Z: spZOfs}).Mul(m) 138 | s = sdf.Transform3D(s, m) 139 | return s 140 | } 141 | 142 | func sparkplugs(mode string) sdf.SDF3 { 143 | xOfs := 0.5 * sp2spDistance 144 | return sdf.Union3D(sparkplug(mode, xOfs), sparkplug(mode, -xOfs)) 145 | } 146 | 147 | //----------------------------------------------------------------------------- 148 | // valve bosses 149 | 150 | var valveDiameter = dim(1.0 / 4.0) 151 | var valveRadius = valveDiameter / 2.0 152 | var valveYOffset = dim(1.0 / 8.0) 153 | var valveWall = dim(5.0 / 32.0) 154 | var v2vDistance = dim(1.0 / 2.0) 155 | var valveDraft = sdf.DtoR(5) 156 | 157 | func valve(d float64, mode string) sdf.SDF3 { 158 | 159 | var s sdf.SDF3 160 | h := headHeight - cylinderHeight 161 | 162 | if mode == "boss" { 163 | delta := h * math.Tan(valveDraft) 164 | r1 := valveRadius + valveWall 165 | r0 := r1 + delta 166 | s, _ = sdf.Cone3D(h, r0, r1, 0) 167 | } else if mode == "hole" { 168 | s, _ = sdf.Cylinder3D(h, valveRadius, 0) 169 | } else { 170 | panic("bad mode") 171 | } 172 | 173 | zOfs := cylinderHeight / 2 174 | return sdf.Transform3D(s, sdf.Translate3d(v3.Vec{X: d, Y: valveYOffset, Z: zOfs})) 175 | } 176 | 177 | func valveSet(d float64, mode string) sdf.SDF3 { 178 | delta := v2vDistance / 2 179 | s := sdf.Union3D(valve(-delta, mode), valve(delta, mode)) 180 | s.(*sdf.UnionSDF3).SetMin(sdf.PolyMin(generalRound)) 181 | return sdf.Transform3D(s, sdf.Translate3d(v3.Vec{X: d})) 182 | } 183 | 184 | func valveSets(mode string) sdf.SDF3 { 185 | delta := c2cDistance / 2 186 | return sdf.Union3D(valveSet(-delta, mode), valveSet(delta, mode)) 187 | } 188 | 189 | //----------------------------------------------------------------------------- 190 | // cylinder domes (or full base) 191 | 192 | var cylinderHeight = dim(3.0 / 16.0) 193 | var cylinderDiameter = dim(1.0 + (1.0 / 8.0)) 194 | var cylinderWall = dim(1.0 / 4.0) 195 | var cylinderRadius = cylinderDiameter / 2.0 196 | 197 | var domeRadius = cylinderWall + cylinderRadius 198 | var domeHeight = cylinderWall + cylinderHeight 199 | 200 | var c2cDistance = dim(1.0 + (3.0 / 8.0)) 201 | 202 | func cylinderHead(d float64, mode string) sdf.SDF3 { 203 | var s sdf.SDF3 204 | 205 | if mode == "dome" { 206 | zOfs := (headHeight - domeHeight) / 2 207 | extraZ := generalRound * 2 208 | s, _ = sdf.Cylinder3D(domeHeight+extraZ, domeRadius, generalRound) 209 | s = sdf.Transform3D(s, sdf.Translate3d(v3.Vec{X: d, Z: -zOfs - extraZ})) 210 | } else if mode == "chamber" { 211 | zOfs := (headHeight - cylinderHeight) / 2 212 | s, _ = sdf.Cylinder3D(cylinderHeight, cylinderRadius, 0) 213 | s = sdf.Transform3D(s, sdf.Translate3d(v3.Vec{X: d, Z: -zOfs})) 214 | } else { 215 | panic("bad mode") 216 | } 217 | return s 218 | } 219 | 220 | func cylinderHeads(mode string) sdf.SDF3 { 221 | xOfs := c2cDistance / 2 222 | s := sdf.Union3D(cylinderHead(-xOfs, mode), cylinderHead(xOfs, mode)) 223 | if mode == "dome" { 224 | s.(*sdf.UnionSDF3).SetMin(sdf.PolyMin(generalRound)) 225 | } 226 | return s 227 | } 228 | 229 | //----------------------------------------------------------------------------- 230 | // cylinder studs: location, bosses and holes 231 | 232 | var studBossRadius = dim(3.0 / 16.0) 233 | var studHoleDy = dim(11.0 / 16.0) 234 | var studHoleDx0 = dim(7.0 / 16.0) 235 | var studHoleDx1 = dim(1.066) 236 | 237 | var studLocations = []v2.Vec{ 238 | {studHoleDx0 + studHoleDx1, 0}, 239 | {studHoleDx0 + studHoleDx1, studHoleDy}, 240 | {studHoleDx0 + studHoleDx1, -studHoleDy}, 241 | {studHoleDx0, studHoleDy}, 242 | {studHoleDx0, -studHoleDy}, 243 | {-studHoleDx0 - studHoleDx1, 0}, 244 | {-studHoleDx0 - studHoleDx1, studHoleDy}, 245 | {-studHoleDx0 - studHoleDx1, -studHoleDy}, 246 | {-studHoleDx0, studHoleDy}, 247 | {-studHoleDx0, -studHoleDy}, 248 | } 249 | 250 | //----------------------------------------------------------------------------- 251 | // head walls 252 | 253 | var headLength = dim(4.30 / 1.25) 254 | var headWidth = dim(2.33 / 1.25) 255 | var headHeight = dim(7.0 / 8.0) 256 | var headCornerRound = dim((5.0 / 32.0) / 1.25) 257 | var headWallThickness = dim(0.154) 258 | 259 | func headWallOuter2d() sdf.SDF2 { 260 | return sdf.Box2D(v2.Vec{X: headLength, Y: headWidth}, headCornerRound) 261 | } 262 | 263 | func headWallInner2d() sdf.SDF2 { 264 | l := headLength - (2 * headWallThickness) 265 | w := headWidth - (2 * headWallThickness) 266 | s0 := sdf.Box2D(v2.Vec{X: l, Y: w}, 0) 267 | c, _ := sdf.Circle2D(studBossRadius) 268 | s1 := sdf.Multi2D(c, studLocations) 269 | s := sdf.Difference2D(s0, s1) 270 | s.(*sdf.DifferenceSDF2).SetMax(sdf.PolyMax(generalRound)) 271 | return s 272 | } 273 | 274 | func headEnvelope() sdf.SDF3 { 275 | s0 := sdf.Box2D(v2.Vec{X: headLength + 2*ebHeight1, Y: headWidth}, 0) 276 | return sdf.Extrude3D(s0, headHeight) 277 | } 278 | 279 | func headWall() sdf.SDF3 { 280 | s := headWallOuter2d() 281 | s = sdf.Difference2D(s, headWallInner2d()) 282 | return sdf.Extrude3D(s, headHeight) 283 | } 284 | 285 | //----------------------------------------------------------------------------- 286 | // manifolds 287 | 288 | var manifoldRadius = dim(4.5 / 16.0) 289 | var manifoldHoleRadius = dim(1.0 / 8.0) 290 | var inletTheta = 30.2564 291 | var exhaustTheta = 270.0 + 13.9736 292 | var exhaustXOffset = (c2cDistance / 2) + (v2vDistance / 2) 293 | var inletXOffset = (c2cDistance / 2) - (v2vDistance / 2) 294 | 295 | func manifoldSet(r float64) sdf.SDF3 { 296 | 297 | h := dim(2) 298 | 299 | sEx, _ := sdf.Cylinder3D(h, r, 0) 300 | m := sdf.Translate3d(v3.Vec{Z: h / 2}) 301 | m = sdf.RotateX(sdf.DtoR(-90)).Mul(m) 302 | m = sdf.RotateZ(sdf.DtoR(exhaustTheta)).Mul(m) 303 | m = sdf.Translate3d(v3.Vec{X: exhaustXOffset, Y: valveYOffset, Z: ebZOffset}).Mul(m) 304 | sEx = sdf.Transform3D(sEx, m) 305 | 306 | sIn, _ := sdf.Cylinder3D(h, r, 0) 307 | m = sdf.Translate3d(v3.Vec{Z: h / 2}) 308 | m = sdf.RotateX(sdf.DtoR(-90)).Mul(m) 309 | m = sdf.RotateZ(sdf.DtoR(inletTheta)).Mul(m) 310 | m = sdf.Translate3d(v3.Vec{X: inletXOffset, Y: valveYOffset, Z: ebZOffset}).Mul(m) 311 | sIn = sdf.Transform3D(sIn, m) 312 | 313 | return sdf.Union3D(sEx, sIn) 314 | } 315 | 316 | func manifolds(mode string) sdf.SDF3 { 317 | var r float64 318 | if mode == "body" { 319 | r = manifoldRadius 320 | } else if mode == "hole" { 321 | r = manifoldHoleRadius 322 | } else { 323 | panic("bad mode") 324 | } 325 | s0 := manifoldSet(r) 326 | s1 := sdf.Transform3D(s0, sdf.MirrorYZ()) 327 | s := sdf.Union3D(s0, s1) 328 | if mode == "body" { 329 | s.(*sdf.UnionSDF3).SetMin(sdf.PolyMin(generalRound)) 330 | } 331 | return s 332 | } 333 | 334 | //----------------------------------------------------------------------------- 335 | 336 | func allowances(_ sdf.SDF3) sdf.SDF3 { 337 | //eb0_2d := Slice2D(s, V3{eb_x_offset, 0, 0}, V3{1, 0, 0}) 338 | //return Extrude3D(eb0_2d, 10.0) 339 | return nil 340 | } 341 | 342 | //----------------------------------------------------------------------------- 343 | 344 | func additive() sdf.SDF3 { 345 | s := sdf.Union3D( 346 | headWall(), 347 | //head_base(), 348 | cylinderHeads("dome"), 349 | valveSets("boss"), 350 | sparkplugs("boss"), 351 | manifolds("body"), 352 | exhaustBosses("body"), 353 | ) 354 | s.(*sdf.UnionSDF3).SetMin(sdf.PolyMin(generalRound)) 355 | 356 | s = sdf.Difference3D(s, sparkplugs("counterbore")) 357 | 358 | // cleanup the blending artifacts on the outside 359 | s = sdf.Intersect3D(s, headEnvelope()) 360 | 361 | //if casting == true { 362 | s = sdf.Union3D(s, allowances(s)) 363 | //} 364 | 365 | return s 366 | } 367 | 368 | //----------------------------------------------------------------------------- 369 | 370 | func subtractive() sdf.SDF3 { 371 | var s sdf.SDF3 372 | //if casting == false { 373 | // s = sdf.Union3D(cylinder_heads("chamber"), 374 | // head_stud_holes(), 375 | // valve_sets("hole"), 376 | // sparkplugs("hole"), 377 | // manifolds("hole"), 378 | // exhaust_bosses("hole"), 379 | // ) 380 | //} 381 | return s 382 | } 383 | 384 | //----------------------------------------------------------------------------- 385 | 386 | func main() { 387 | s := sdf.Difference3D(additive(), subtractive()) 388 | 389 | ebiten.SetWindowTitle("SDFX-UI cylinder head demo") 390 | ebiten.SetRunnableOnUnfocused(true) 391 | ebiten.SetWindowResizable(true) 392 | 393 | // Actual rendering loop 394 | err := ui.NewRenderer(s, 395 | ui.OptMWatchFiles([]string{"main.go"}), // Default of "." also works, but it triggers too often if generating a profile 396 | ui.Opt3Mesh(render.NewMarchingCubesUniform(100), math.Pi/3), 397 | ui.OptMSmoothCamera(true), 398 | ).Run() 399 | if err != nil { 400 | panic(err) 401 | } 402 | } 403 | 404 | //----------------------------------------------------------------------------- 405 | -------------------------------------------------------------------------------- /examples/spiral/cmr10.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeicor/sdfx-ui/1d6bf29fab655ef18ca6d2b71b36073e18c92fb0/examples/spiral/cmr10.ttf -------------------------------------------------------------------------------- /examples/spiral/main.go: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | /* 3 | 4 | SOURCE: https://github.com/deadsy/sdfx/blob/master/examples/spiral/main.go 5 | 6 | Spirals 7 | 8 | */ 9 | //----------------------------------------------------------------------------- 10 | package main 11 | 12 | import ( 13 | "github.com/Yeicor/sdfx-ui" 14 | "github.com/deadsy/sdfx/sdf" 15 | "github.com/hajimehoshi/ebiten" 16 | "log" 17 | ) 18 | 19 | func spiralSdf() (s interface{}, err error) { 20 | s, err = sdf.ArcSpiral2D(1.0, 20.0, 0.25*sdf.Pi, 21 | 8*sdf.Tau, 1.0) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | //c, err := sdf.Circle2D(22.) 27 | //if err != nil { 28 | // return nil, err 29 | //} 30 | //s = sdf.Union2D(s.(sdf.SDF2), c) 31 | 32 | //c2, err := sdf.Circle2D(20.) 33 | //if err != nil { 34 | // return nil, err 35 | //} 36 | //c2 = sdf.Transform2D(c2, sdf.Translate2d(v2.Vec{X: 0})) 37 | //s = sdf.Difference2D(s.(sdf.SDF2), c2) 38 | 39 | ////WARNING: Text is slow to render (especially with -race flag) 40 | //f, err := sdf.LoadFont("cmr10.ttf") 41 | //if err != nil { 42 | // log.Fatalf("can't read font file %s\n", err) 43 | //} 44 | //t, err := sdf.TextSDF2(f, sdf.NewText("Spiral"), 10) 45 | //if err != nil { 46 | // return nil, err 47 | //} 48 | //s = sdf.Difference2D(s.(sdf.SDF2), t) 49 | 50 | s = sdf.Extrude3D(s.(sdf.SDF2), 4) 51 | //s, _ = sdf.ExtrudeRounded3D(s.(sdf.SDF2), 4, 0.25) 52 | //s, _ = sdf.RevolveTheta3D(s.(sdf.SDF2), math.Pi/2) 53 | 54 | //box3, _ := sdf.Box3D(v3.Vec{X: 40, Y: 10, Z: 15}, 0.2) 55 | //box3 = sdf.Transform3D(box3, sdf.Translate3d(v3.Vec{Y: 30, Z: -5})) 56 | //s = sdf.Union3D(s.(sdf.SDF3), box3) 57 | 58 | return s, err 59 | } 60 | 61 | // DESKTOP: go run . 62 | // BROWSER: $(go env GOPATH)/bin/wasmserve . 63 | // MOBILE: $(go env GOPATH)/bin/gomobile build -v -target=android . 64 | func main() { 65 | s, err := spiralSdf() 66 | if err != nil { 67 | log.Fatalf("error: %s\n", err) 68 | } 69 | 70 | // Rendering configuration boilerplate 71 | ebiten.SetWindowTitle("SDFX-UI spiral demo") 72 | ebiten.SetRunnableOnUnfocused(true) 73 | ebiten.SetWindowResizable(true) 74 | //ebiten.SetWindowPosition(2600, 0) 75 | //ebiten.SetWindowSize(1920, 1040) 76 | 77 | //// Profiling boilerplate 78 | //defer func() { 79 | // //cmd := exec.Command("go", "tool", "pprof", "cpu.pprof") 80 | // cmd := exec.Command("go", "tool", "trace", "trace.out") 81 | // cmd.Stdin = os.Stdin 82 | // cmd.Stdout = os.Stdout 83 | // cmd.Stderr = os.Stderr 84 | // err = cmd.Run() 85 | // if err != nil { 86 | // panic(err) 87 | // } 88 | //}() 89 | ////defer profile.Start(profile.ProfilePath(".")).Stop() 90 | //defer profile.Start(profile.TraceProfile, profile.ProfilePath(".")).Stop() 91 | 92 | // Actual rendering loop 93 | err = ui.NewRenderer(s, 94 | ui.OptMWatchFiles([]string{"main.go"}), // Default of "." also works, but it triggers too often if generating a profile 95 | //ui.Opt3Mesh(&render.MarchingCubesUniform{}, 100, math.Pi/3), 96 | ui.OptMSmoothCamera(true), 97 | ).Run() 98 | if err != nil { 99 | panic(err) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /fsnotifysupported.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd || openbsd || netbsd || dragonfly || darwin || windows || linux || solaris 2 | // +build freebsd openbsd netbsd dragonfly darwin windows linux solaris 3 | 4 | package ui 5 | 6 | import "github.com/fsnotify/fsnotify" 7 | 8 | func newFsWatcher() (*fsnotify.Watcher, error) { 9 | return fsnotify.NewWatcher() 10 | } 11 | -------------------------------------------------------------------------------- /fsnotifyunsupported.go: -------------------------------------------------------------------------------- 1 | //go:build plan9 || solaris || js 2 | // +build plan9 solaris js 3 | 4 | package ui 5 | 6 | import ( 7 | "errors" 8 | ) 9 | 10 | // fsEvent represents a single file system notification. 11 | type fsEvent struct { 12 | Name string // Relative path to the file or directory. 13 | Op fsOp // File operation that triggered the event. 14 | } 15 | 16 | // fsOp describes a set of file operations. 17 | type fsOp uint32 18 | 19 | var errFsNotifyNotSupported = errors.New("fsnotify is not supported for this platform") 20 | 21 | // Watcher watches a set of files, delivering events to a channel. 22 | type Watcher struct { 23 | Events chan fsEvent 24 | Errors chan error 25 | } 26 | 27 | // NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. 28 | func newFsWatcher() (*Watcher, error) { 29 | return nil, errFsNotifyNotSupported 30 | } 31 | 32 | // Close removes all watches and closes the events channel. 33 | func (w *Watcher) Close() error { 34 | return nil 35 | } 36 | 37 | // Add starts watching the named file or directory (non-recursively). 38 | func (w *Watcher) Add(name string) error { 39 | return nil 40 | } 41 | 42 | // Remove stops watching the the named file or directory (non-recursively). 43 | func (w *Watcher) Remove(name string) error { 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Yeicor/sdfx-ui 2 | 3 | go 1.23 4 | 5 | toolchain go1.24.0 6 | 7 | require ( 8 | github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df 9 | github.com/cenkalti/backoff/v4 v4.3.0 10 | github.com/cenkalti/backoff/v5 v5.0.2 11 | github.com/deadsy/sdfx v0.0.0-20230215171614-2d4e9502ec6f 12 | github.com/fogleman/fauxgl v0.0.0-20250110135958-abf826acbbbd 13 | github.com/fsnotify/fsnotify v1.8.0 14 | github.com/hajimehoshi/ebiten v1.12.12 15 | github.com/hajimehoshi/ebiten/v2 v2.8.6 16 | github.com/mitchellh/reflectwalk v1.0.2 17 | github.com/subchen/go-trylock/v2 v2.0.0 18 | golang.org/x/image v0.24.0 19 | ) 20 | 21 | require ( 22 | github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect 23 | github.com/fogleman/simplify v0.0.0-20170216171241-d32f302d5046 // indirect 24 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 25 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 26 | github.com/hpinc/go3mf v0.24.2 // indirect 27 | github.com/llgcode/draw2d v0.0.0-20210904075650-80aa0a2a901d // indirect 28 | github.com/qmuntal/opc v0.7.11 // indirect 29 | github.com/yofu/dxf v0.0.0-20190710012328-5a6d1e83f16c // indirect 30 | golang.org/x/exp/shiny v0.0.0-20230223210539-50820d90acfd // indirect 31 | golang.org/x/mobile v0.0.0-20221110043201-43a038452099 // indirect 32 | golang.org/x/sys v0.25.0 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 2 | gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= 3 | git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 6 | github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= 7 | github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= 8 | github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= 9 | github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= 10 | github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= 11 | github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= 12 | github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= 13 | github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 14 | github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 15 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 16 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 17 | github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 18 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/deadsy/sdfx v0.0.0-20230215171614-2d4e9502ec6f h1:ByByP1FcB34R2ECYlNyGCfnsp8W09V5qGkZMRePzMeg= 21 | github.com/deadsy/sdfx v0.0.0-20230215171614-2d4e9502ec6f/go.mod h1:yqeU6qWfzYX7lxp9/60IuMWropG47IWaq+hPXZndVwQ= 22 | github.com/dhconnelly/rtreego v1.1.0/go.mod h1:SDozu0Fjy17XH1svEXJgdYq8Tah6Zjfa/4Q33Z80+KM= 23 | github.com/fogleman/fauxgl v0.0.0-20200818143847-27cddc103802 h1:5vdq0jOnV15v1NdZbAcU+dIJ22rFgwaieiFewPvnKCA= 24 | github.com/fogleman/fauxgl v0.0.0-20200818143847-27cddc103802/go.mod h1:7f7F8EvO8MWvDx9sIoloOfZBCKzlWuZV/h3TjpXOO3k= 25 | github.com/fogleman/fauxgl v0.0.0-20250110135958-abf826acbbbd h1:8bZGm26jDoW+JQ1ZPugRU0ADy5k45DRb42sOxEeufNo= 26 | github.com/fogleman/fauxgl v0.0.0-20250110135958-abf826acbbbd/go.mod h1:7f7F8EvO8MWvDx9sIoloOfZBCKzlWuZV/h3TjpXOO3k= 27 | github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 28 | github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 29 | github.com/fogleman/simplify v0.0.0-20170216171241-d32f302d5046 h1:n3RPbpwXSFT0G8FYslzMUBDO09Ix8/dlqzvUkcJm4Jk= 30 | github.com/fogleman/simplify v0.0.0-20170216171241-d32f302d5046/go.mod h1:KDwyDqFmVUxUmo7tmqXtyaaJMdGon06y8BD2jmh84CQ= 31 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 32 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 33 | github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= 34 | github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= 35 | github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= 36 | github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= 37 | github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= 38 | github.com/go-gl/gl v0.0.0-20180407155706-68e253793080/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= 39 | github.com/go-gl/glfw v0.0.0-20180426074136-46a8d530c326/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 40 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 41 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200707082815-5321531c36a2/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 42 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU= 43 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 44 | github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= 45 | github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= 46 | github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= 47 | github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= 48 | github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= 49 | github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= 50 | github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY= 51 | github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 52 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 53 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 54 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 55 | github.com/hajimehoshi/bitmapfont v1.3.0 h1:h6+HJQ+2MKT3lEVEArjVC4/h0qcFXlVsMTGuRijEnVA= 56 | github.com/hajimehoshi/bitmapfont v1.3.0/go.mod h1:/Qb7yVjHYNUV4JdqNkPs6BSZwLjKqkZOMIp6jZD0KgE= 57 | github.com/hajimehoshi/ebiten v1.12.12 h1:JvmF1bXRa+t+/CcLWxrJCRsdjs2GyBYBSiFAfIqDFlI= 58 | github.com/hajimehoshi/ebiten v1.12.12/go.mod h1:1XI25ImVCDPJiXox4h9yK/CvN5sjDYnbF4oZcFzPXHw= 59 | github.com/hajimehoshi/ebiten/v2 v2.8.6/go.mod h1:cCQ3np7rdmaJa1ZnvslraVlpxNb3wCjEnAP1LHNyXNA= 60 | github.com/hajimehoshi/file2byteslice v0.0.0-20200812174855-0e5e8a80490e/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE= 61 | github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= 62 | github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= 63 | github.com/hajimehoshi/oto v0.6.8/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= 64 | github.com/hpinc/go3mf v0.24.1/go.mod h1:QtHqY8cmfyaSuT4J+fi6UTIQPzqxBdvlSwWzcLlmeQI= 65 | github.com/hpinc/go3mf v0.24.2 h1:BPBC+w9qobnvP5IWnFS3/21h6JYorijnGLAGK/rRFwI= 66 | github.com/hpinc/go3mf v0.24.2/go.mod h1:QtHqY8cmfyaSuT4J+fi6UTIQPzqxBdvlSwWzcLlmeQI= 67 | github.com/hschendel/stl v1.0.4/go.mod h1:XQFFLKrq9YTaBpmouDui4JSaxMyAYkpD7elGSSj/y3M= 68 | github.com/jakecoffman/cp v1.0.0/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg= 69 | github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk= 70 | github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= 71 | github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 72 | github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 73 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 74 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 75 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 76 | github.com/llgcode/draw2d v0.0.0-20210904075650-80aa0a2a901d h1:4/ycg+VrwjGurTqiHv2xM/h6Qm81qSra+KbfT4FH2FA= 77 | github.com/llgcode/draw2d v0.0.0-20210904075650-80aa0a2a901d/go.mod h1:mVa0dA29Db2S4LVqDYLlsePDzRJLDfdhVZiI15uY0FA= 78 | github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb h1:61ndUreYSlWFeCY44JxDDkngVoI7/1MVhEl98Nm0KOk= 79 | github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb/go.mod h1:1l8ky+Ew27CMX29uG+a2hNOKpeNYEQjjtiALiBlFQbY= 80 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 81 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 82 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 83 | github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= 84 | github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= 85 | github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= 86 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= 87 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 88 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 89 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 90 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 91 | github.com/qmuntal/opc v0.7.10/go.mod h1:jYcgCT/xuTl5oMYeS/nSrfRe9gNrrsAPPeOFE2oRVaQ= 92 | github.com/qmuntal/opc v0.7.11 h1:OxAgE5sf4uAH4Nh8fjE/TAfDUlNN2Dh0zJI9Qw9xOiI= 93 | github.com/qmuntal/opc v0.7.11/go.mod h1:jYcgCT/xuTl5oMYeS/nSrfRe9gNrrsAPPeOFE2oRVaQ= 94 | github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= 95 | github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= 96 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 97 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 98 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 99 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 100 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 101 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 102 | github.com/subchen/go-trylock/v2 v2.0.0 h1:XAZYp/ZvkBFuvSPAeGM0TjbMby/mHoWnnLBAv2FidUw= 103 | github.com/subchen/go-trylock/v2 v2.0.0/go.mod h1:jjSakPS+IvBCtFw5Fao9rQqdiCnF0ZrkzVkauvkZzLY= 104 | github.com/yofu/dxf v0.0.0-20190710012328-5a6d1e83f16c h1:qgsxLgTXCVH8Dxar36HI5af2ZfinVz5vF8erPpyzM+A= 105 | github.com/yofu/dxf v0.0.0-20190710012328-5a6d1e83f16c/go.mod h1:gnT4GQzgKW8+TLI0xheUgdmNV4dsAN0WJUVnztRZkfI= 106 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 107 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 108 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 109 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 110 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 111 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 112 | golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 113 | golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 114 | golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 115 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 116 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= 117 | golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= 118 | golang.org/x/exp/shiny v0.0.0-20230223210539-50820d90acfd h1:NhaxQbivfgmJiv/W6gdum6cKl9yydOhdKsUnUBBbcqM= 119 | golang.org/x/exp/shiny v0.0.0-20230223210539-50820d90acfd/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0= 120 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 121 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 122 | golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 123 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 124 | golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 125 | golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 126 | golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 127 | golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 128 | golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 129 | golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 130 | golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 131 | golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 132 | golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 133 | golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 134 | golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 135 | golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= 136 | golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= 137 | golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= 138 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 139 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 140 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 141 | golang.org/x/mobile v0.0.0-20210208171126-f462b3930c8f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= 142 | golang.org/x/mobile v0.0.0-20221110043201-43a038452099 h1:aIu0lKmfdgtn2uTj7JI2oN4TUrQvgB+wzTPO23bCKt8= 143 | golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= 144 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 145 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 146 | golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 147 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 148 | golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 149 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 150 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 151 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 152 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 153 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 154 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 155 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 156 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 157 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 158 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 159 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 160 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 161 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 162 | golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 163 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 164 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 165 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 166 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 167 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 168 | golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 169 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 170 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 171 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 172 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 173 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 174 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 175 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 176 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 177 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 178 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 179 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 180 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 181 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 182 | golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 183 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 184 | golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 185 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 186 | golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 187 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 188 | golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 189 | golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 190 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 191 | golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= 192 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 193 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 194 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 195 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 196 | gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= 197 | gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= 198 | gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= 199 | gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= 200 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= 201 | gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= 202 | gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= 203 | gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= 204 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 205 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 206 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 207 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 208 | honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= 209 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 210 | -------------------------------------------------------------------------------- /impl2.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/Yeicor/sdfx-ui/internal" 5 | "github.com/deadsy/sdfx/sdf" 6 | v2 "github.com/deadsy/sdfx/vec/v2" 7 | "github.com/deadsy/sdfx/vec/v2i" 8 | v3 "github.com/deadsy/sdfx/vec/v3" 9 | "image" 10 | "image/color" 11 | "image/color/palette" 12 | "math" 13 | ) 14 | 15 | //----------------------------------------------------------------------------- 16 | // CONFIGURATION 17 | //----------------------------------------------------------------------------- 18 | 19 | // Opt2Cam sets the default camera for SDF2 (may grow to follow the aspect ratio of the screen). 20 | // WARNING: Need to run again the main renderer to apply a change of this option. 21 | func Opt2Cam(bb sdf.Box2) Option { 22 | return func(r *Renderer) { 23 | r.implState.Bb = bb 24 | } 25 | } 26 | 27 | // Opt2EvalRange skips the initial scan of the SDF2 to find the minimum and maximum value, and can also be used to 28 | // make the surface easier to see by setting them to a value close to 0. 29 | func Opt2EvalRange(min, max float64) Option { 30 | return func(r *Renderer) { 31 | if r2, ok := r.impl.(*renderer2); ok { 32 | r2.evalMin = min 33 | r2.evalMax = max 34 | } 35 | } 36 | } 37 | 38 | // Opt2EvalScanCells configures the initial scan of the SDF2 to find minimum and maximum values (defaults to 128x128 cells). 39 | func Opt2EvalScanCells(cells v2i.Vec) Option { 40 | return func(r *Renderer) { 41 | if r2, ok := r.impl.(*renderer2); ok { 42 | r2.evalScanCells = cells 43 | } 44 | } 45 | } 46 | 47 | // Opt2BBColor sets the bounding box colors for the different objects. 48 | func Opt2BBColor(getColor func(idx int) color.Color) Option { 49 | return func(r *Renderer) { 50 | if r2, ok := r.impl.(*renderer2); ok { 51 | r2.getBBColor = getColor 52 | } 53 | } 54 | } 55 | 56 | //----------------------------------------------------------------------------- 57 | // RENDERER 58 | //----------------------------------------------------------------------------- 59 | 60 | type renderer2 struct { 61 | s sdf.SDF2 // The SDF to render 62 | pixelsRand []int // Cached set of pixels in random order to avoid shuffling (reset on recompilation and resolution changes) 63 | evalMin, evalMax float64 // The pre-computed minimum and maximum of the whole surface (for stable colors and speed) 64 | evalScanCells v2i.Vec 65 | getBBColor func(idx int) color.Color 66 | } 67 | 68 | func newDevRenderer2(s sdf.SDF2) internal.DevRendererImpl { 69 | r := &renderer2{ 70 | s: s, 71 | evalScanCells: v2i.Vec{128, 128}, 72 | getBBColor: func(idx int) color.Color { 73 | return palette.WebSafe[((idx + 1) % len(palette.WebSafe))] 74 | }, 75 | } 76 | return r 77 | } 78 | 79 | func (r *renderer2) Dimensions() int { 80 | return 2 81 | } 82 | 83 | func (r *renderer2) BoundingBox() sdf.Box3 { 84 | bb := r.s.BoundingBox() 85 | return sdf.Box3{Min: v3.Vec{X: bb.Min.X, Y: bb.Max.Y, Z: 0.}, Max: v3.Vec{X: bb.Max.X, Y: bb.Max.Y, Z: 0.}} 86 | } 87 | 88 | func (r *renderer2) ReflectTree() *internal.ReflectTree { 89 | return internal.NewReflectionSDF(r.s).GetReflectSDFTree2() 90 | } 91 | 92 | func (r *renderer2) ColorModes() int { 93 | // 0: Gradient (useful for debugging sides) 94 | // 1: Black/white (clearer surface boundary) 95 | return 2 96 | } 97 | 98 | func (r *renderer2) Render(args *internal.RenderArgs) error { 99 | if r.evalMin == 0 && r.evalMax == 0 { // First render (ignoring external cache) 100 | // Compute minimum and maximum evaluate values for a shared color scale for all blocks 101 | r.evalMin, r.evalMax = utilSdf2MinMax(r.s, r.s.BoundingBox(), r.evalScanCells) 102 | //log.Println("MIN:", r.evalMin, "MAX:", r.evalMax) 103 | } 104 | 105 | // Maintain Bb aspect ratio on ResInv change, increasing the sizeCorner as needed 106 | args.StateLock.Lock() 107 | fullRenderSize := args.FullRender.Bounds().Size() 108 | bbAspectRatio := args.State.Bb.Size().X / args.State.Bb.Size().Y 109 | screenAspectRatio := float64(fullRenderSize.X) / float64(fullRenderSize.Y) 110 | if math.Abs(bbAspectRatio-screenAspectRatio) > 1e-12 { 111 | if bbAspectRatio > screenAspectRatio { 112 | scaleYBy := bbAspectRatio / screenAspectRatio 113 | args.State.Bb = sdf.NewBox2(args.State.Bb.Center(), args.State.Bb.Size().Mul(v2.Vec{X: 1, Y: scaleYBy})) 114 | } else { 115 | scaleXBy := screenAspectRatio / bbAspectRatio 116 | args.State.Bb = sdf.NewBox2(args.State.Bb.Center(), args.State.Bb.Size().Mul(v2.Vec{X: scaleXBy, Y: 1})) 117 | } 118 | } 119 | args.StateLock.Unlock() 120 | 121 | // Apply color mode 122 | evalMin, evalMax := r.evalMin, r.evalMax 123 | if args.State.ColorMode == 1 { // Force black and white to see the surface better 124 | evalMin, evalMax = -1e-12, 1e-12 125 | } 126 | 127 | // Perform the actual render 128 | err := implCommonRender(func(pixel v2i.Vec, pixel01 v2.Vec) interface{} { return nil }, 129 | func(pixel v2i.Vec, pixel01 v2.Vec, job interface{}) *jobResult { 130 | pixel01.Y = 1 - pixel01.Y // Inverted Y 131 | args.StateLock.RLock() 132 | pos := args.State.Bb.Min.Add(pixel01.Mul(args.State.Bb.Size())) 133 | args.StateLock.RUnlock() 134 | grayVal := imageColor2(r.s.Evaluate(pos), evalMin, evalMax) 135 | return &jobResult{ 136 | pixel: pixel, 137 | color: color.RGBA{R: uint8(grayVal * 255), G: uint8(grayVal * 255), B: uint8(grayVal * 255), A: 255}, 138 | } 139 | }, args, &r.pixelsRand) 140 | 141 | if err == nil && args.State.DrawBbs { 142 | // Draw bounding boxes over the image 143 | boxes2 := args.State.ReflectTree.GetBoundingBoxes2() 144 | for i, bb := range boxes2 { 145 | //log.Println("Draw", bb) 146 | pixel01Min := bb.Min.Sub(args.State.Bb.Min).Div(args.State.Bb.Size()) 147 | pixel01Max := bb.Max.Sub(args.State.Bb.Min).Div(args.State.Bb.Size()) 148 | fullRenderSizeV2 := v2.Vec{X: float64(fullRenderSize.X), Y: float64(fullRenderSize.Y)} 149 | posMin := pixel01Min.Mul(fullRenderSizeV2) 150 | posMax := pixel01Max.Mul(fullRenderSizeV2) 151 | drawRect(args.FullRender, int(posMin.X), fullRenderSize.Y-int(posMax.Y), 152 | int(posMax.X), fullRenderSize.Y-int(posMin.Y), r.getBBColor(i)) 153 | } 154 | } 155 | 156 | return err 157 | } 158 | 159 | // imageColor2 returns the grayscale color for the returned SDF2.Evaluate value, given the reference minimum and maximum 160 | // SDF2.Evaluate values. The returned value is in the range [0, 1]. 161 | func imageColor2(dist, dmin, dmax float64) float64 { 162 | // Clamp due to possibly forced min and max 163 | var val float64 164 | // NOTE: This condition forces the surface to be close to 255/2 gray value, otherwise dmax >>> dmin or viceversa 165 | // could cause the surface to be visually displaced 166 | if dist >= 0 { 167 | val = math.Max(0.5, math.Min(1, 0.5+0.5*((dist)/(dmax)))) 168 | } else { // Force lower scale for inside surface 169 | val = math.Max(0, math.Min(0.5, 0.5*((dist-dmin)/(-dmin)))) 170 | } 171 | return val 172 | } 173 | 174 | // drawHLine draws a horizontal line 175 | func drawHLine(img *image.RGBA, x1, y, x2 int, col color.Color) { 176 | for ; x1 <= x2; x1++ { 177 | if x1 >= 0 && x1 < img.Bounds().Dx() && y >= 0 && y < img.Bounds().Dy() { 178 | img.Set(x1, y, col) 179 | } 180 | } 181 | } 182 | 183 | // drawVLine draws a veritcal line 184 | func drawVLine(img *image.RGBA, x, y1, y2 int, col color.Color) { 185 | for ; y1 <= y2; y1++ { 186 | if x >= 0 && x < img.Bounds().Dx() && y1 >= 0 && y1 < img.Bounds().Dy() { 187 | img.Set(x, y1, col) 188 | } 189 | } 190 | } 191 | 192 | // drawRect draws a rectangle utilizing drawHLine() and drawVLine() 193 | func drawRect(img *image.RGBA, x1, y1, x2, y2 int, col color.Color) { 194 | drawHLine(img, x1, y1, x2, col) 195 | drawHLine(img, x1, y2, x2, col) 196 | drawVLine(img, x1, y1, y2, col) 197 | drawVLine(img, x2, y1, y2, col) 198 | } 199 | -------------------------------------------------------------------------------- /impl2_test.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "context" 5 | "github.com/Yeicor/sdfx-ui/internal" 6 | "github.com/deadsy/sdfx/sdf" 7 | "image" 8 | "sync" 9 | "testing" 10 | ) 11 | 12 | func BenchmarkDevRenderer2_Render(b *testing.B) { 13 | s, _ := sdf.ArcSpiral2D(1.0, 20.0, 0.25*sdf.Pi, 8*sdf.Tau, 1.0) 14 | impl := newDevRenderer2(s) 15 | b.ReportAllocs() 16 | state := internal.RendererState{ 17 | ResInv: 8, 18 | Bb: s.BoundingBox(), 19 | } 20 | fullRender := image.NewRGBA(image.Rect(0, 0, 1920/state.ResInv, 1080/state.ResInv)) 21 | lock1 := &sync.RWMutex{} 22 | lock2 := &sync.RWMutex{} 23 | b.ResetTimer() 24 | for n := 0; n < b.N; n++ { 25 | err := impl.Render(&internal.RenderArgs{Ctx: context.Background(), State: &state, StateLock: lock1, CachedRenderLock: lock2, FullRender: fullRender}) 26 | if err != nil { 27 | b.Fatal(err) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /impl3.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/Yeicor/sdfx-ui/internal" 5 | "github.com/deadsy/sdfx/sdf" 6 | "github.com/deadsy/sdfx/vec/v2" 7 | "github.com/deadsy/sdfx/vec/v2i" 8 | "github.com/deadsy/sdfx/vec/v3" 9 | "image" 10 | "image/color" 11 | "image/color/palette" 12 | "math" 13 | ) 14 | 15 | //----------------------------------------------------------------------------- 16 | // CONFIGURATION 17 | //----------------------------------------------------------------------------- 18 | 19 | // Opt3SwapYAndZ sets the UP direction to Y+ instead of Z+ (or swaps it back). 20 | func Opt3SwapYAndZ() Option { 21 | return func(r *Renderer) { 22 | if r3, ok := r.impl.(*renderer3); ok { 23 | r3.s = &swapYZ{r3.s} 24 | } 25 | } 26 | } 27 | 28 | // Opt3Cam sets the default transform for the camera (pivot center, angles and distance). 29 | // WARNING: Need to run again the main renderer to apply a change of this option. 30 | func Opt3Cam(camCenter v3.Vec, pitch, yaw, dist float64) Option { 31 | return func(r *Renderer) { 32 | r.implState.CamCenter = camCenter 33 | r.implState.CamPitch = pitch 34 | r.implState.CamYaw = yaw 35 | r.implState.CamDist = dist 36 | } 37 | } 38 | 39 | // Opt3CamFov sets the default Field Of View for the camera (default 90º, in radians). 40 | func Opt3CamFov(fov float64) Option { 41 | return func(r *Renderer) { 42 | if r3, ok := r.impl.(*renderer3); ok { 43 | r3.camFOV = fov 44 | } 45 | } 46 | } 47 | 48 | // Opt3RayConfig sets the configuration for the raycast (balancing performance and quality). 49 | // Rendering a pink pixel means that the ray reached maxSteps without hitting the surface or reaching the limit 50 | // (consider increasing maxSteps (reduce performance), increasing epsilon or increasing stepScale (both reduce quality)). 51 | func Opt3RayConfig(scaleAndSigmoid, stepScale, epsilon float64, maxSteps int) Option { 52 | return func(r *Renderer) { 53 | if r3, ok := r.impl.(*renderer3); ok { 54 | r3.rayScaleAndSigmoid = scaleAndSigmoid 55 | r3.rayStepScale = stepScale 56 | r3.rayEpsilon = epsilon 57 | r3.rayMaxSteps = maxSteps 58 | } 59 | } 60 | } 61 | 62 | // Opt3Colors changes rendering colors. 63 | func Opt3Colors(surface, background, error color.RGBA) Option { 64 | return func(r *Renderer) { 65 | if r3, ok := r.impl.(*renderer3); ok { 66 | r3.surfaceColor = surface 67 | r3.backgroundColor = background 68 | r3.errorColor = error 69 | } 70 | } 71 | } 72 | 73 | // Opt3NormalEps sets the distance between samples used to compute the normals. 74 | func Opt3NormalEps(normalEps float64) Option { 75 | return func(r *Renderer) { 76 | if r3, ok := r.impl.(*renderer3); ok { 77 | r3.normalEps = normalEps / 2 78 | } 79 | } 80 | } 81 | 82 | // Opt3LightDir sets the light direction for basic lighting simulation. 83 | // Actually, two lights are simulated (the given one and the opposite one), as part of the surface would be hard to see otherwise 84 | func Opt3LightDir(lightDir v3.Vec) Option { 85 | return func(r *Renderer) { 86 | if r3, ok := r.impl.(*renderer3); ok { 87 | r3.lightDir = lightDir.Normalize() 88 | } 89 | } 90 | } 91 | 92 | // Opt3BBColor sets the bounding box colors for the different objects. 93 | func Opt3BBColor(getColor func(idx int) color.Color) Option { 94 | return func(r *Renderer) { 95 | if r3, ok := r.impl.(*renderer3); ok { 96 | r3.getBBColor = getColor 97 | } 98 | } 99 | } 100 | 101 | //----------------------------------------------------------------------------- 102 | // RENDERER 103 | //----------------------------------------------------------------------------- 104 | 105 | type renderer3 struct { 106 | s sdf.SDF3 // The SDF to render 107 | pixelsRand []int // Cached set of pixels in random order to avoid shuffling (reset on recompilation and resolution changes) 108 | camFOV float64 // The Field Of View (X axis) for the camera 109 | surfaceColor, backgroundColor, errorColor color.RGBA 110 | normalEps float64 111 | lightDir v3.Vec // The light's direction for ColorMode: true (simple simulation based on normals) 112 | depthBuffer []float64 113 | getBBColor func(idx int) color.Color 114 | 115 | // Raycast configuration 116 | rayScaleAndSigmoid, rayStepScale, rayEpsilon float64 117 | rayMaxSteps int 118 | 119 | meshRenderer *renderer3mesh // Alternative renderer 120 | } 121 | 122 | func newDevRenderer3(s sdf.SDF3) internal.DevRendererImpl { 123 | r := &renderer3{ 124 | s: &invertZ{s}, // TODO: fix rendering to use Z+ (instead of Z-) as UP instead of this hack. 125 | camFOV: math.Pi / 2, // 90º FOV-X 126 | surfaceColor: color.RGBA{R: 255 - 20, G: 255 - 40, B: 255 - 80, A: 255}, 127 | backgroundColor: color.RGBA{R: 50, G: 100, B: 150, A: 255}, 128 | errorColor: color.RGBA{R: 255, B: 255, A: 255}, 129 | normalEps: 1e-6, 130 | lightDir: v3.Vec{X: -1, Y: 1, Z: 1}.Normalize(), // Same as default camera TODO: Follow camera mode? 131 | rayScaleAndSigmoid: 0, 132 | rayStepScale: 1, 133 | rayEpsilon: 1e-2, 134 | rayMaxSteps: 100, 135 | meshRenderer: &renderer3mesh{}, 136 | getBBColor: func(idx int) color.Color { 137 | return palette.WebSafe[((idx + 1) % len(palette.WebSafe))] 138 | }, 139 | } 140 | return r 141 | } 142 | 143 | func (r *renderer3) Dimensions() int { 144 | return 3 145 | } 146 | 147 | func (r *renderer3) BoundingBox() sdf.Box3 { 148 | return r.s.BoundingBox() 149 | } 150 | 151 | func (r *renderer3) ReflectTree() *internal.ReflectTree { 152 | return internal.NewReflectionSDF(r.s).GetReflectSDFTree3() 153 | } 154 | 155 | func (r *renderer3) ColorModes() int { 156 | // Use alternative renderer instead if configured to do so 157 | if r.meshRenderer != nil { 158 | return r.meshRenderer.ColorModes() 159 | } 160 | // 0: Constant color with basic shading (2 lights and no projected shadows) 161 | // 1: Normal XYZ as RGB 162 | return 2 163 | } 164 | 165 | func (r *renderer3) Render(args *internal.RenderArgs) error { 166 | // Use alternative renderer instead if configured to do so 167 | if r.meshRenderer != nil && r.meshRenderer.mesh != nil { 168 | err := r.meshRenderer.Render(r, args) 169 | return err 170 | } 171 | 172 | // Compute camera matrix and more (once per render) 173 | args.StateLock.RLock() 174 | colorModeCopy := args.State.ColorMode 175 | bounds := args.FullRender.Bounds() 176 | boundsSize := v2i.Vec{bounds.Size().X, bounds.Size().Y} 177 | //aspectRatio := float64(boundsSize[0]) / float64(boundsSize.Y) 178 | camViewMatrix := cam3MatrixNoTranslation(args.State) 179 | camPos := args.State.CamCenter.Add(camViewMatrix.MulPosition(v3.Vec{Y: -args.State.CamDist})) 180 | camDir := args.State.CamCenter.Sub(camPos).Normalize() 181 | camFovX := r.camFOV 182 | camFovY := 2 * math.Atan(math.Tan(camFovX/2) /**aspectRatio*/) 183 | // Approximate max ray length for the whole camera (it could be improved... or maybe a fixed value is better) 184 | sBb := r.BoundingBox() 185 | maxRay := math.Abs(collideRayBb(camPos, camDir, sBb)) 186 | // If we do not hit the box (in a straight line, set a default -- box size, as following condition will be true) 187 | if !sBb.Contains(camPos) { // If we hit from the outside of the box, add the whole size of the box 188 | maxRay += sBb.Size().Length() 189 | } 190 | maxRay *= 4 // Rays thrown from the camera at different angles may need a little more maxRay 191 | 192 | if args.State.DrawBbs { 193 | // Reset internal depth buffer 194 | expectedLen := boundsSize.X * boundsSize.Y 195 | if len(r.depthBuffer) != expectedLen { 196 | r.depthBuffer = make([]float64, expectedLen) 197 | } 198 | for i := 0; i < len(r.depthBuffer); i++ { 199 | r.depthBuffer[i] = math.MaxFloat64 200 | } 201 | } else { 202 | r.depthBuffer = nil 203 | } 204 | args.StateLock.RUnlock() 205 | 206 | // Perform the actual render 207 | camHalfFov := v2.Vec{X: camFovX, Y: camFovY}.DivScalar(2) 208 | err := implCommonRender(func(pixel v2i.Vec, pixel01 v2.Vec) interface{} { 209 | return &pixelRender{ 210 | pixel: pixel, 211 | bounds: boundsSize, 212 | camPos: camPos, 213 | camDir: camDir, 214 | camViewMatrix: camViewMatrix, 215 | camHalfFov: camHalfFov, 216 | maxRay: maxRay, 217 | color: colorModeCopy, 218 | rendered: color.RGBA{}, 219 | } 220 | }, func(pixel v2i.Vec, pixel01 v2.Vec, job interface{}) *jobResult { 221 | return &jobResult{ 222 | pixel: pixel, 223 | color: r.samplePixel(pixel01, job.(*pixelRender)), 224 | } 225 | }, args, &r.pixelsRand) 226 | 227 | if err == nil && args.State.DrawBbs { 228 | // FIXME: Assumes perfectly matching cameras (between both 3D renderers), 229 | // but they differ (in aspect ratio <--> FoV, matching on square windows) 230 | r.renderBbs(args, r.depthBuffer) 231 | } 232 | 233 | return err 234 | } 235 | 236 | func (r *renderer3) renderBbs(args *internal.RenderArgs, depthBuffer []float64) { 237 | // Needed to render boxes 238 | backgroundColorOld := r.backgroundColor 239 | r.backgroundColor = color.RGBA{A: 0} 240 | camMatrix, _ := r.meshRenderer.reset(r, args) 241 | r.backgroundColor = backgroundColorOld 242 | // Draw bounding boxes over the image 243 | var boxesRender *image.NRGBA 244 | for i, bb := range args.State.ReflectTree.GetBoundingBoxes3() { 245 | boxesRender = r.meshRenderer.renderBoundingBox(bb, camMatrix, r.getBBColor(i)) 246 | } 247 | if boxesRender != nil && len(depthBuffer) > 0 { 248 | // Now merge both renders by depth! 249 | size := args.FullRender.Bounds().Size() 250 | boxesDepth := r.meshRenderer.depthBuffer() 251 | i := 0 252 | for y := 0; y < size.Y; y++ { 253 | for x := 0; x < size.X; x++ { 254 | overlay := boxesRender.NRGBAAt(x, y) 255 | if overlay.A > 0 { 256 | boxesRenderDepth := boxesDepth[i] 257 | renderDepth := depthBuffer[i] 258 | //if renderDepth < math.MaxFloat64/2 { 259 | // fmt.Println(boxesRenderDepth, renderDepth) 260 | //} 261 | if boxesRenderDepth < renderDepth { 262 | //prevColor := args.FullRender.RGBAAt(x, y) 263 | args.FullRender.Set(x, y, overlay) 264 | } 265 | } 266 | i++ 267 | } 268 | } 269 | } 270 | } 271 | 272 | type pixelRender struct { 273 | // CAMERA RELATED 274 | pixel, bounds v2i.Vec // Pixel and bounds for pixel 275 | camPos, camDir v3.Vec // Camera parameters 276 | camViewMatrix sdf.M44 // The world to camera matrix 277 | camHalfFov v2.Vec // Camera's field of view 278 | maxRay float64 // The maximum distance of a ray (camPos, camDir) before getting out of bounds 279 | // MISC 280 | color int 281 | // OUTPUT 282 | rendered color.RGBA 283 | } 284 | 285 | func (r *renderer3) samplePixel(pixel01 v2.Vec, job *pixelRender) color.RGBA { 286 | depthBufferIndex := -1 287 | if len(r.depthBuffer) > 0 { 288 | depthBufferIndex = job.pixel.Y*job.bounds.X + job.pixel.X 289 | } 290 | // Generate the ray for this pixel using the given camera parameters 291 | rayFrom := job.camPos 292 | // Get pixel inside of ([-1, 1], [-1, 1]) 293 | rayDirXZBase := pixel01.MulScalar(2).SubScalar(1) 294 | rayDirXZBase.Y = -rayDirXZBase.Y 295 | rayDirXZBase.X *= float64(job.bounds.X) / float64(job.bounds.Y) // Apply aspect ratio (again) 296 | // Convert to the projection over a displacement of 1 297 | rayDirXZBase = rayDirXZBase.Mul(v2.Vec{X: math.Tan(job.camHalfFov.X), Y: math.Tan(job.camHalfFov.Y)}) 298 | rayDir := v3.Vec{X: rayDirXZBase.X, Y: 1, Z: rayDirXZBase.Y} // Z is UP (and this default camera is X-right Y-up) 299 | // Apply the camera matrix to the default ray 300 | rayDir = job.camViewMatrix.MulPosition(rayDir) // .Normalize() (done in Raycast already) 301 | // TODO: Orthogonal camera mode? 302 | 303 | // Query the surface with the given ray 304 | hit, t, steps := sdf.Raycast3(r.s, rayFrom, rayDir, r.rayScaleAndSigmoid, r.rayStepScale, r.rayEpsilon, job.maxRay, r.rayMaxSteps) 305 | // Convert the possible hit to a color 306 | if t >= 0 { // Hit the surface 307 | if len(r.depthBuffer) > 0 { // HACK: Depth function similar to fauxgl (but not the same) 308 | r.depthBuffer[depthBufferIndex] = 1 / (1 + math.Exp(-t/10)) 309 | } 310 | normal := sdf.Normal3(r.s, hit, r.normalEps) 311 | if job.color == 0 { // Basic lighting + constant color 312 | lightIntensity := math.Abs(normal.Dot(r.lightDir)) // Actually also simulating the opposite light 313 | // If this was a performant ray-tracer, we could bounce the light 314 | return color.RGBA{ 315 | R: uint8(float64(r.surfaceColor.R) * lightIntensity), 316 | G: uint8(float64(r.surfaceColor.G) * lightIntensity), 317 | B: uint8(float64(r.surfaceColor.B) * lightIntensity), 318 | A: r.surfaceColor.A, 319 | } 320 | } // Otherwise, Color == abs(normal) 321 | return color.RGBA{ 322 | R: uint8(math.Abs(normal.X) * 255), 323 | G: uint8(math.Abs(normal.Y) * 255), 324 | B: uint8(math.Abs(normal.Z) * 255), 325 | A: 255, 326 | } 327 | } else // Otherwise, missed the surface (or run out of steps) 328 | if len(r.depthBuffer) > 0 { 329 | r.depthBuffer[depthBufferIndex] = math.MaxFloat64 330 | } 331 | if steps == r.rayMaxSteps { 332 | // Reached the maximum amount of steps (should change parameters) 333 | return r.errorColor 334 | } 335 | // The void 336 | return r.backgroundColor 337 | } 338 | 339 | type invertZ struct { 340 | impl sdf.SDF3 341 | } 342 | 343 | func (i *invertZ) Evaluate(p v3.Vec) float64 { 344 | return i.impl.Evaluate(p.Mul(v3.Vec{X: 1, Y: 1, Z: -1})) 345 | } 346 | 347 | func (i *invertZ) BoundingBox() sdf.Box3 { 348 | box := i.impl.BoundingBox() 349 | box.Min.Z = -box.Min.Z 350 | box.Max.Z = -box.Max.Z 351 | if box.Max.Z < box.Min.Z { 352 | box.Max.Z, box.Min.Z = box.Min.Z, box.Max.Z 353 | } 354 | return box 355 | } 356 | 357 | // collideRayBb https://gamedev.stackexchange.com/a/18459. 358 | // Returns the length traversed through the array to reach the box, which may be negative (hit backwards). 359 | // In case of no hit it returns a guess of where it would hit 360 | func collideRayBb(origin v3.Vec, dir v3.Vec, bb sdf.Box3) float64 { 361 | dirFrac := v3.Vec{X: 1 / dir.X, Y: 1 / dir.Y, Z: 1 / dir.Z} // Assumes normalized dir 362 | t135 := bb.Min.Sub(origin).Mul(dirFrac) 363 | t246 := bb.Max.Sub(origin).Mul(dirFrac) 364 | tmin := math.Max(math.Max(math.Min(t135.X, t246.X), math.Min(t135.Y, t246.Y)), math.Min(t135.Z, t246.Z)) 365 | tmax := math.Min(math.Min(math.Max(t135.X, t246.X), math.Max(t135.Y, t246.Y)), math.Max(t135.Z, t246.Z)) 366 | //if tmin > tmax { // if tmin > tmax, ray doesn't intersect AABB 367 | // return inf 368 | //} 369 | if tmax < 0 { // if tmax < 0, ray (line) is intersecting AABB, but the whole AABB is behind us 370 | return tmax 371 | } 372 | if bb.Contains(origin) { // This is triggered if inside 373 | return tmax 374 | } 375 | return tmin 376 | } 377 | 378 | type swapYZ struct { 379 | impl sdf.SDF3 380 | } 381 | 382 | func (s *swapYZ) Evaluate(p v3.Vec) float64 { 383 | return s.impl.Evaluate(v3.Vec{X: p.X, Y: p.Z, Z: p.Y}) 384 | } 385 | 386 | func (s *swapYZ) BoundingBox() sdf.Box3 { 387 | box := s.impl.BoundingBox() 388 | box.Min.Z, box.Min.Y = box.Min.Y, box.Min.Z 389 | box.Max.Z, box.Max.Y = box.Max.Y, box.Max.Z 390 | return box 391 | } 392 | -------------------------------------------------------------------------------- /impl3_test.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "context" 5 | "github.com/Yeicor/sdfx-ui/internal" 6 | "github.com/deadsy/sdfx/sdf" 7 | v3 "github.com/deadsy/sdfx/vec/v3" 8 | "image" 9 | "math" 10 | "sync" 11 | "testing" 12 | ) 13 | 14 | func BenchmarkDevRenderer3_Render(b *testing.B) { 15 | s, _ := sdf.ArcSpiral2D(1.0, 20.0, 0.25*sdf.Pi, 8*sdf.Tau, 1.0) 16 | s3, _ := sdf.ExtrudeRounded3D(s, 4, 1) 17 | impl := newDevRenderer3(s3) 18 | b.ReportAllocs() 19 | state := internal.RendererState{ 20 | ResInv: 8, 21 | Bb: s.BoundingBox(), 22 | } 23 | fullRender := image.NewRGBA(image.Rect(0, 0, 1920/state.ResInv, 1080/state.ResInv)) 24 | lock1 := &sync.RWMutex{} 25 | lock2 := &sync.RWMutex{} 26 | b.ResetTimer() 27 | for n := 0; n < b.N; n++ { 28 | err := impl.Render(&internal.RenderArgs{Ctx: context.Background(), State: &state, StateLock: lock1, CachedRenderLock: lock2, FullRender: fullRender}) 29 | if err != nil { 30 | b.Fatal(err) 31 | } 32 | } 33 | } 34 | 35 | func Test_collideRayBb(t *testing.T) { 36 | type args struct { 37 | origin v3.Vec 38 | dir v3.Vec 39 | bb sdf.Box3 40 | } 41 | tests := []struct { 42 | name string 43 | args args 44 | want float64 45 | }{ 46 | { 47 | name: "Basic", 48 | args: args{ 49 | origin: v3.Vec{Z: -2}, 50 | dir: v3.Vec{Z: 1}, 51 | bb: sdf.Box3{ 52 | Min: v3.Vec{X: -1, Y: -1, Z: -1}, 53 | Max: v3.Vec{X: 1, Y: 1, Z: 1}, 54 | }, 55 | }, 56 | want: 1, 57 | }, 58 | { 59 | name: "Sideways", 60 | args: args{ 61 | origin: v3.Vec{X: -2, Y: -2, Z: -2}, 62 | dir: v3.Vec{X: 1, Y: 1, Z: 1}.Normalize(), 63 | bb: sdf.Box3{ 64 | Min: v3.Vec{X: -1, Y: -1, Z: -1}, 65 | Max: v3.Vec{X: 1, Y: 1, Z: 1}, 66 | }, 67 | }, 68 | want: v3.Vec{X: 1, Y: 1, Z: 1}.Length(), 69 | }, 70 | { 71 | name: "Backwards", 72 | args: args{ 73 | origin: v3.Vec{X: 2, Y: 2, Z: 2}, 74 | dir: v3.Vec{X: 1, Y: 1, Z: 1}.Normalize(), 75 | bb: sdf.Box3{ 76 | Min: v3.Vec{X: -1, Y: -1, Z: -1}, 77 | Max: v3.Vec{X: 1, Y: 1, Z: 1}, 78 | }, 79 | }, 80 | want: -v3.Vec{X: 1, Y: 1, Z: 1}.Length(), 81 | }, 82 | { 83 | name: "Inside", 84 | args: args{ 85 | origin: v3.Vec{X: 0.1, Y: 0.1, Z: 0.1}, 86 | dir: v3.Vec{X: 1, Y: 1, Z: 1}.Normalize(), 87 | bb: sdf.Box3{ 88 | Min: v3.Vec{X: -1, Y: -1, Z: -1}, 89 | Max: v3.Vec{X: 1, Y: 1, Z: 1}, 90 | }, 91 | }, 92 | want: v3.Vec{X: 0.9, Y: 0.9, Z: 0.9}.Length(), 93 | }, 94 | { 95 | name: "Inside2", 96 | args: args{ 97 | origin: v3.Vec{X: 0.1, Y: 0.1, Z: 0.1}, 98 | dir: v3.Vec{X: -1, Y: -1, Z: -1}.Normalize(), 99 | bb: sdf.Box3{ 100 | Min: v3.Vec{X: -1, Y: -1, Z: -1}, 101 | Max: v3.Vec{X: 1, Y: 1, Z: 1}, 102 | }, 103 | }, 104 | want: v3.Vec{X: -1.1, Y: -1.1, Z: -1.1}.Length(), 105 | }, 106 | { 107 | name: "No hit", 108 | args: args{ 109 | origin: v3.Vec{X: 10, Y: 0, Z: 0}, 110 | dir: v3.Vec{X: 1, Y: 1, Z: 1}.Normalize(), 111 | bb: sdf.Box3{ 112 | Min: v3.Vec{X: -1, Y: -1, Z: -1}, 113 | Max: v3.Vec{X: 1, Y: 1, Z: 1}, 114 | }, 115 | }, 116 | want: -15.588457268119893, 117 | }, 118 | } 119 | for _, tt := range tests { 120 | t.Run(tt.name, func(t *testing.T) { 121 | if got := collideRayBb(tt.args.origin, tt.args.dir, tt.args.bb); math.Abs(got-tt.want) > 1e-12 { 122 | t.Errorf("collideRayBb() = %v, want %v", got, tt.want) 123 | } 124 | }) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /impl3mesh.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/Yeicor/sdfx-ui/internal" 5 | "github.com/deadsy/sdfx/render" 6 | "github.com/deadsy/sdfx/sdf" 7 | "github.com/deadsy/sdfx/vec/v2i" 8 | v3 "github.com/deadsy/sdfx/vec/v3" 9 | "github.com/fogleman/fauxgl" 10 | "image" 11 | "image/color" 12 | "log" 13 | "math" 14 | ) 15 | 16 | //----------------------------------------------------------------------------- 17 | // CONFIGURATION 18 | //----------------------------------------------------------------------------- 19 | 20 | // Opt3Mesh enables and configures the 3D mesh renderer instead of the default raycast based renderer 21 | // WARNING: Should be the last option applied (as some other options might modify the SDF3). 22 | func Opt3Mesh(meshGenerator render.Render3, smoothNormalsRadians float64) Option { 23 | return func(r *Renderer) { 24 | if r3, ok := r.impl.(*renderer3); ok { 25 | log.Println("[DevRenderer] Rendering 3D mesh...") // only performed once per compilation 26 | var triangles []*fauxgl.Triangle 27 | triChan := make(chan []*render.Triangle3) 28 | go func() { 29 | meshGenerator.Render(r3.s, triChan) 30 | close(triChan) 31 | }() 32 | for tris := range triChan { 33 | for _, tri := range tris { 34 | triangles = append(triangles, r3mConvertTriangle(tri)) 35 | } 36 | } 37 | mesh := fauxgl.NewTriangleMesh(triangles) 38 | // smooth the normals 39 | mesh.SmoothNormalsThreshold(smoothNormalsRadians) 40 | r3.meshRenderer = &renderer3mesh{mesh: mesh, lastContext: nil} 41 | log.Println("[DevRenderer] Mesh is ready") 42 | } 43 | } 44 | } 45 | 46 | //----------------------------------------------------------------------------- 47 | // RENDERER 48 | //----------------------------------------------------------------------------- 49 | 50 | // renderer3mesh is an extension to renderer3 that is set when the trimesh renderer is enabled 51 | type renderer3mesh struct { 52 | mesh *fauxgl.Mesh // the pre-compiled mesh to render 53 | lastContext *fauxgl.Context 54 | } 55 | 56 | func (rm *renderer3mesh) ColorModes() int { 57 | // 0: Constant color with basic shading (1 light and no projected shadows) 58 | // 1: Normal XYZ as RGB 59 | // 2: 1 but in wireframe mode 60 | return 3 61 | } 62 | 63 | func (rm *renderer3mesh) Render(r *renderer3, args *internal.RenderArgs) error { 64 | camFauxglMatrix, camPos := rm.reset(r, args) 65 | 66 | // Configure the shader (based on ColorMode) 67 | if args.State.ColorMode == 0 { 68 | // use builtin phong shader 69 | shader := fauxgl.NewPhongShader(camFauxglMatrix, r3mToFauxglVector(r.lightDir), r3mToFauxglVector(camPos)) 70 | shader.ObjectColor = fauxgl.MakeColor(r.surfaceColor) 71 | rm.lastContext.Shader = shader 72 | rm.lastContext.Wireframe = false 73 | } else { 74 | // use normal based shader 75 | rm.lastContext.Shader = &r3mNormalShader{camFauxglMatrix} 76 | rm.lastContext.Wireframe = args.State.ColorMode == 2 // set to wireframe mode 77 | } 78 | // Perform the actual render 79 | rm.lastContext.DrawMesh(rm.mesh) // This is already multithread, no need to parallelize anymore 80 | img := rm.lastContext.Image() 81 | 82 | // Copy output full render (no partial renders supported) 83 | args.CachedRenderLock.Lock() 84 | copy(args.FullRender.Pix[args.FullRender.PixOffset(0, 0):], img.(*image.NRGBA).Pix[img.(*image.NRGBA).PixOffset(0, 0):]) 85 | args.CachedRenderLock.Unlock() 86 | 87 | if args.State.DrawBbs { 88 | // Draw bounding boxes over the image 89 | depthBufferClone := make([]float64, len(rm.lastContext.DepthBuffer)) 90 | copy(depthBufferClone, rm.lastContext.DepthBuffer) 91 | r.renderBbs(args, depthBufferClone) 92 | } 93 | 94 | if args.PartialRenders != nil { 95 | close(args.PartialRenders) 96 | } 97 | 98 | return nil 99 | } 100 | 101 | func (rm *renderer3mesh) reset(r *renderer3, args *internal.RenderArgs) (fauxgl.Matrix, v3.Vec) { 102 | args.StateLock.Lock() 103 | bounds := args.FullRender.Bounds() 104 | boundsSize := v2i.Vec{bounds.Size().X, bounds.Size().Y} 105 | if rm.lastContext == nil || rm.lastContext.Width != boundsSize.X || rm.lastContext.Height != boundsSize.Y { 106 | // Rebuild rendering context only when needed 107 | rm.lastContext = fauxgl.NewContext(boundsSize.X, boundsSize.Y) 108 | } else { 109 | rm.lastContext.ClearDepthBuffer() 110 | } 111 | rm.lastContext.ClearColorBufferWith(fauxgl.MakeColor(r.backgroundColor)) 112 | 113 | // Compute camera matrix and more (once per render) 114 | //args.state.CamYaw += math.Pi // HACK 115 | //args.state.CamCenter.X = -args.state.CamCenter.X 116 | //args.state.CamCenter.Y = -args.state.CamCenter.Y 117 | aspectRatio := float64(boundsSize.X) / float64(boundsSize.Y) 118 | camViewMatrix := cam3MatrixNoTranslation(args.State) 119 | camPos := args.State.CamCenter.Add(camViewMatrix.MulPosition(v3.Vec{Y: -args.State.CamDist / 1.12 /* Adjust to other implementation*/})) 120 | camDir := args.State.CamCenter.Sub(camPos).Normalize() 121 | camFovX := r.camFOV 122 | camFovY := 2 * math.Atan(math.Tan(camFovX/2)*aspectRatio) 123 | // Approximate max ray length for the whole camera (it could be improved... or maybe a fixed value is better) 124 | sBb := r.BoundingBox() 125 | maxRay := math.Abs(collideRayBb(camPos, camDir, sBb)) 126 | // If we do not hit the box (in a straight line, set a default -- box size, as following condition will be true) 127 | if !sBb.Contains(camPos) { // If we hit from the outside of the box, add the whole size of the box 128 | maxRay += sBb.Size().Length() 129 | } 130 | maxRay *= 4 // Rays thrown from the camera at different angles may need a little more maxRay 131 | camFauxglMatrix := fauxgl.LookAt(r3mToFauxglVector(camPos), r3mToFauxglVector(args.State.CamCenter), fauxgl.Vector{Z: 1}). 132 | Perspective(camFovY*180/math.Pi, aspectRatio, 1e-6, maxRay) 133 | //args.state.CamYaw -= math.Pi // HACK (restore) 134 | //args.state.CamCenter.X = -args.state.CamCenter.X 135 | //args.state.CamCenter.Y = -args.state.CamCenter.Y 136 | args.StateLock.Unlock() 137 | return camFauxglMatrix, camPos 138 | } 139 | 140 | func (rm *renderer3mesh) depthBuffer() []float64 { 141 | return rm.lastContext.DepthBuffer 142 | } 143 | 144 | func (rm *renderer3mesh) renderBoundingBox(bb sdf.Box3, camFauxglMatrix fauxgl.Matrix, color color.Color) *image.NRGBA { 145 | mesh := fauxgl.NewCubeOutlineForBox(fauxgl.Box{ 146 | Min: fauxgl.Vector{X: bb.Min.X, Y: bb.Min.Y, Z: -bb.Min.Z}, // FIXME: Swap back Z when camera is fixed 147 | Max: fauxgl.Vector{X: bb.Max.X, Y: bb.Max.Y, Z: -bb.Max.Z}, 148 | }) 149 | 150 | // Render the cube as a wireframe 151 | shader := fauxgl.NewSolidColorShader(camFauxglMatrix, fauxgl.MakeColor(color)) 152 | rm.lastContext.Shader = shader 153 | rm.lastContext.Wireframe = true 154 | rm.lastContext.DrawMesh(mesh) 155 | 156 | return rm.lastContext.Image().(*image.NRGBA) 157 | } 158 | 159 | func r3mConvertTriangle(tri *render.Triangle3) *fauxgl.Triangle { 160 | normal := tri.Normal() 161 | normalV := r3mToFauxglVector(normal) 162 | return &fauxgl.Triangle{ 163 | V1: fauxgl.Vertex{Position: r3mToFauxglVector(tri.V[0]), Normal: normalV, Color: fauxgl.Gray(1)}, 164 | V2: fauxgl.Vertex{Position: r3mToFauxglVector(tri.V[1]), Normal: normalV, Color: fauxgl.Gray(1)}, 165 | V3: fauxgl.Vertex{Position: r3mToFauxglVector(tri.V[2]), Normal: normalV, Color: fauxgl.Gray(1)}, 166 | } 167 | } 168 | 169 | func r3mToFauxglVector(normal v3.Vec) fauxgl.Vector { 170 | return fauxgl.Vector{X: normal.X, Y: normal.Y, Z: normal.Z} 171 | } 172 | 173 | // r3mNormalShader 174 | type r3mNormalShader struct { 175 | Matrix fauxgl.Matrix 176 | } 177 | 178 | func (shader *r3mNormalShader) Vertex(v fauxgl.Vertex) fauxgl.Vertex { 179 | v.Output = shader.Matrix.MulPositionW(v.Position) 180 | return v 181 | } 182 | 183 | func (shader *r3mNormalShader) Fragment(v fauxgl.Vertex) fauxgl.Color { 184 | return fauxgl.MakeColor(color.RGBA{ 185 | R: uint8(math.Abs(v.Normal.X) * 255), 186 | G: uint8(math.Abs(v.Normal.Y) * 255), 187 | B: uint8(math.Abs(v.Normal.Z) * 255), 188 | A: 255, 189 | }) 190 | } 191 | -------------------------------------------------------------------------------- /implcommon.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/Yeicor/sdfx-ui/internal" 5 | "github.com/deadsy/sdfx/vec/v2" 6 | "github.com/deadsy/sdfx/vec/v2i" 7 | "image/color" 8 | "math/rand" 9 | "runtime" 10 | "sync" 11 | ) 12 | 13 | type jobInternal struct { 14 | pixel v2i.Vec 15 | pixel01 v2.Vec 16 | data interface{} 17 | } 18 | 19 | type jobResult struct { 20 | pixel v2i.Vec 21 | color color.RGBA 22 | } 23 | 24 | func implCommonRender(genJob func(pixel v2i.Vec, pixel01 v2.Vec) interface{}, 25 | processJob func(pixel v2i.Vec, pixel01 v2.Vec, job interface{}) *jobResult, 26 | args *internal.RenderArgs, pixelsRand *[]int) error { 27 | 28 | // Set all pixels to transparent initially (for partial renderings to work) 29 | args.CachedRenderLock.Lock() 30 | for i := 3; i < len(args.FullRender.Pix); i += 4 { 31 | args.FullRender.Pix[i] = 255 32 | } 33 | args.CachedRenderLock.Unlock() 34 | 35 | // Update random pixels if needed 36 | bounds := args.FullRender.Bounds() 37 | boundsSize := v2i.Vec{bounds.Size().X, bounds.Size().Y} 38 | pixelCount := boundsSize.X * boundsSize.Y 39 | if pixelCount != len(*pixelsRand) { 40 | // Random seed shouldn't matter, just make pixel coloring seem random for partial renders 41 | *pixelsRand = rand.Perm(pixelCount) 42 | } 43 | 44 | // Spawn the workers that will render 1 pixel at a time 45 | jobs := make(chan *jobInternal) 46 | jobResults := make(chan *jobResult) 47 | workerWg := &sync.WaitGroup{} 48 | for i := 0; i < runtime.NumCPU(); i++ { 49 | workerWg.Add(1) 50 | go func() { 51 | defer workerWg.Done() 52 | loop: 53 | for { 54 | job, ok := <-jobs 55 | if !ok { // Cancelled or finished render (stopped generating jobs) 56 | break loop 57 | } 58 | jobResults <- processJob(job.pixel, job.pixel01, job.data) 59 | } 60 | }() 61 | } 62 | go func() { // Make sure job results are closed after all jobs are processed 63 | workerWg.Wait() 64 | close(jobResults) 65 | }() 66 | 67 | // Spawn the work generator 68 | go func() { 69 | loop: // Sample each pixel on the image separately (and in random order to see the image faster) 70 | for _, randPixelIndex := range *pixelsRand { 71 | // Sample a random pixel in the image 72 | sampledPixel := v2i.Vec{X: randPixelIndex % boundsSize.X, Y: randPixelIndex / boundsSize.X} 73 | sampledPixel01 := v2.Vec{X: float64(sampledPixel.X) / float64(boundsSize.X), Y: float64(sampledPixel.Y) / float64(boundsSize.Y)} 74 | // Queue the job for parallel processing 75 | select { 76 | case <-args.Ctx.Done(): 77 | break loop 78 | case jobs <- &jobInternal{ 79 | pixel: sampledPixel, 80 | pixel01: sampledPixel01, 81 | data: genJob(sampledPixel, sampledPixel01), 82 | }: 83 | } 84 | } 85 | close(jobs) // Close the jobs channel to mark the end 86 | }() 87 | 88 | // Listen for all job results and update the image, freeing locks and sending a partial image update every batch of pixels 89 | const pixelBatch = 1000 // Configurable? Shouldn't matter much as you can already configure time between partial renders. 90 | pixelNum := 0 91 | args.CachedRenderLock.Lock() 92 | var err error 93 | pixelLoop: 94 | for renderedPixel := range jobResults { 95 | args.FullRender.SetRGBA(renderedPixel.pixel.X, renderedPixel.pixel.Y, renderedPixel.color) 96 | pixelNum++ 97 | if pixelNum%pixelBatch == 0 { 98 | args.CachedRenderLock.Unlock() 99 | runtime.Gosched() // Breathe (let renderer do something, best-effort) 100 | select { // Check if this render is cancelled (could also check every pixel...) 101 | case <-args.Ctx.Done(): 102 | err = args.Ctx.Err() 103 | break pixelLoop 104 | default: 105 | } 106 | if args.PartialRenders != nil { // Send the partial render update 107 | // Use a shader to fill transparent pixel with nearest neighbors to make it look better while rendering (losing previous background render)? 108 | args.PartialRenders <- args.FullRender 109 | } 110 | args.CachedRenderLock.Lock() 111 | } 112 | } 113 | if err == nil { 114 | args.CachedRenderLock.Unlock() 115 | } 116 | if args.PartialRenders != nil { 117 | close(args.PartialRenders) 118 | } 119 | return err 120 | } 121 | -------------------------------------------------------------------------------- /implremote.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/Yeicor/sdfx-ui/internal" 5 | "github.com/barkimedes/go-deepcopy" 6 | "github.com/deadsy/sdfx/sdf" 7 | "github.com/deadsy/sdfx/vec/v2i" 8 | "log" 9 | "net/rpc" 10 | "time" 11 | ) 12 | 13 | // rendererClient implements DevRendererImpl by calling a remote implementation (using Go's net/rpc) 14 | type rendererClient struct { 15 | cl *rpc.Client 16 | cachedReflectTree *internal.ReflectTree // Avoids sending the whole metadata tree over the network more than once 17 | } 18 | 19 | // newDevRendererClient see rendererClient 20 | func newDevRendererClient(client *rpc.Client) internal.DevRendererImpl { 21 | return &rendererClient{cl: client} 22 | } 23 | 24 | func (d *rendererClient) Dimensions() int { 25 | var out int 26 | err := d.cl.Call("RendererService.Dimensions", &out, &out) 27 | if err != nil { 28 | log.Println("[DevRenderer] Error on remote call (RendererService.Dimensions):", err) 29 | } 30 | return out 31 | } 32 | 33 | func (d *rendererClient) BoundingBox() sdf.Box3 { 34 | var out sdf.Box3 35 | err := d.cl.Call("RendererService.BoundingBox", &out, &out) 36 | if err != nil { 37 | log.Println("[DevRenderer] Error on remote call (RendererService.BoundingBox):", err) 38 | } 39 | return out 40 | } 41 | 42 | func (d *rendererClient) ReflectTree() *internal.ReflectTree { 43 | if d.cachedReflectTree != nil { 44 | return d.cachedReflectTree 45 | } 46 | var in sdf.Box3 47 | var out internal.ReflectTree 48 | err := d.cl.Call("RendererService.BoundingBox", &in, &out) 49 | if err != nil { 50 | log.Println("[DevRenderer] Error on remote call (RendererService.BoundingBox):", err) 51 | } 52 | return &out 53 | } 54 | 55 | func (d *rendererClient) ColorModes() int { 56 | var out int 57 | err := d.cl.Call("RendererService.ColorModes", &out, &out) 58 | if err != nil { 59 | log.Println("[DevRenderer] Error on remote call (RendererService.ColorModes):", err) 60 | } 61 | return out 62 | } 63 | 64 | func (d *rendererClient) Render(args *internal.RenderArgs) error { 65 | fullRenderSize := args.FullRender.Bounds().Size() 66 | args.StateLock.RLock() // Clone the state to avoid locking while the rendering is happening 67 | argsRemote := &internal.RemoteRenderArgs{ 68 | RenderSize: v2i.Vec{X: fullRenderSize.X, Y: fullRenderSize.Y}, 69 | State: deepcopy.MustAnything(args.State).(*internal.RendererState), 70 | } 71 | argsRemote.State.ReflectTree = nil // HACK: Avoids sending the whole metadata tree over the network more than once 72 | args.StateLock.RUnlock() 73 | var ignoreMe int 74 | err := d.cl.Call("RendererService.RenderStart", argsRemote, &ignoreMe) 75 | if err != nil { 76 | return err 77 | } 78 | for { 79 | var res internal.RemoteRenderResults 80 | err = d.cl.Call("RendererService.RenderGet", ignoreMe, &res) 81 | if err != nil { 82 | return err 83 | } 84 | select { 85 | case <-args.Ctx.Done(): // Cancel remote renderer also 86 | err = d.cl.Call("RendererService.RenderCancel", ignoreMe, &ignoreMe) 87 | if err != nil { 88 | log.Println("[DevRenderer] Error on remote call (RendererService.RenderCancel):", err) 89 | } 90 | return args.Ctx.Err() 91 | default: 92 | } 93 | if res.NewState != nil { 94 | args.StateLock.Lock() // Clone back the new state to avoid locking while the rendering is happening 95 | *args.State = *res.NewState 96 | args.StateLock.Unlock() 97 | } 98 | if res.IsPartial { 99 | if args.PartialRenders != nil { 100 | args.PartialRenders <- res.RenderedImg 101 | } 102 | } else { // Final render 103 | if args.PartialRenders != nil { 104 | close(args.PartialRenders) 105 | } 106 | args.CachedRenderLock.Lock() 107 | *args.FullRender = *res.RenderedImg 108 | args.CachedRenderLock.Unlock() 109 | break 110 | } 111 | } 112 | return err 113 | } 114 | 115 | func (d *rendererClient) Shutdown(timeout time.Duration) error { 116 | var out int 117 | return d.cl.Call("RendererService.Shutdown", &timeout, &out) 118 | } 119 | -------------------------------------------------------------------------------- /interaction.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/Yeicor/sdfx-ui/internal" 7 | "github.com/barkimedes/go-deepcopy" 8 | "github.com/deadsy/sdfx/vec/conv" 9 | v2 "github.com/deadsy/sdfx/vec/v2" 10 | "github.com/deadsy/sdfx/vec/v2i" 11 | v3 "github.com/deadsy/sdfx/vec/v3" 12 | "github.com/hajimehoshi/ebiten" 13 | "github.com/hajimehoshi/ebiten/inpututil" 14 | "github.com/hajimehoshi/ebiten/text" 15 | "image/color" 16 | "math" 17 | "strconv" 18 | "time" 19 | ) 20 | 21 | // onUpdateInputs handles inputs 22 | func (r *Renderer) onUpdateInputs() { 23 | r.implLock.RLock() 24 | defer r.implLock.RUnlock() 25 | r.onUpdateInputsCommon() 26 | // SDF2/SDF3-SPECIFIC CONTROLS 27 | r.implStateLock.RLock() 28 | implDimCache := r.implDimCache 29 | r.implStateLock.RUnlock() 30 | switch implDimCache { 31 | case 2: 32 | r.onUpdateInputsSDF2() 33 | case 3: 34 | r.onUpdateInputsSDF3() 35 | default: 36 | panic("RendererState.onUpdateInputs not implemented for " + strconv.Itoa(r.implDimCache) + " dimensions") 37 | } 38 | } 39 | 40 | func (r *Renderer) onUpdateInputsCommon() { 41 | // SHARED CONTROLS 42 | if inpututil.IsKeyJustPressed(ebiten.KeyKPAdd) || inpututil.IsKeyJustPressed(ebiten.KeyEqual) { 43 | r.implStateLock.Lock() 44 | r.implState.ResInv /= 2 45 | if r.implState.ResInv < 1 { 46 | r.implState.ResInv = 1 47 | } 48 | r.implStateLock.Unlock() 49 | r.rerender() 50 | } 51 | if inpututil.IsKeyJustPressed(ebiten.KeyKPSubtract) || inpututil.IsKeyJustPressed(ebiten.KeyMinus) { 52 | r.implStateLock.Lock() 53 | r.implState.ResInv *= 2 54 | if r.implState.ResInv > 64 { 55 | r.implState.ResInv = 64 56 | } 57 | r.implStateLock.Unlock() 58 | r.rerender() 59 | } 60 | if inpututil.IsKeyJustPressed(ebiten.KeyB) { 61 | r.implStateLock.Lock() 62 | r.implState.DrawBbs = !r.implState.DrawBbs 63 | r.implStateLock.Unlock() 64 | r.rerender() 65 | } 66 | // Color 67 | if inpututil.IsKeyJustPressed(ebiten.KeyC) { 68 | r.implStateLock.Lock() 69 | r.implState.ColorMode = (r.implState.ColorMode + 1) % r.impl.ColorModes() 70 | r.implStateLock.Unlock() 71 | r.rerender() 72 | } 73 | if r.smoothCamera { 74 | r.implStateLock.RLock() 75 | if r.translateFrom.X != math.MaxInt { 76 | r.rerenderOpt(false) // Trigger mid-movement rerender 77 | } 78 | r.implStateLock.RUnlock() 79 | } 80 | } 81 | 82 | func (r *Renderer) onUpdateInputsSDF2() { 83 | // Zooming 84 | _, wheelUpDown := ebiten.Wheel() 85 | if wheelUpDown != 0 { 86 | r.implStateLock.Lock() 87 | scale := 1 - wheelUpDown*r.implState.Bb.Size().Length2()*0.02 // Scale depending on current scale 88 | scale = math.Max(1/r.zoomFactor, math.Min(r.zoomFactor, scale)) // Apply zoom limits 89 | r.implState.Bb = r.implState.Bb.ScaleAboutCenter(scale) 90 | r.implStateLock.Unlock() 91 | r.rerender() 92 | } 93 | // Translation 94 | if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonMiddle) || len(inpututil.JustPressedTouchIDs()) > 0 { 95 | // Save the cursor's position for previsualization and applying the final translation 96 | cx, cy := getCursor() 97 | r.implStateLock.Lock() 98 | if r.translateFrom.X == math.MaxInt { // Only if not already moving... 99 | r.translateFrom = v2i.Vec{X: cx, Y: cy} 100 | } 101 | r.implStateLock.Unlock() 102 | } 103 | if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonMiddle) || inpututil.IsTouchJustReleased(0) { 104 | // Actually apply the translation and force a rerender 105 | cx, cy := getCursor() 106 | r.implStateLock.Lock() 107 | if r.translateFrom.X != math.MaxInt { // Only if already moving... 108 | r.implState = r.apply2DCameraMoveTo(cx, cy) 109 | // Keep displacement until rerender is complete (avoid jump) using callback below + extra variable set here 110 | r.translateFromStop = v2i.Vec{X: cx, Y: cy} 111 | } 112 | if r.smoothCamera { 113 | r.translateFrom = v2i.Vec{X: math.MaxInt, Y: math.MaxInt} 114 | } 115 | r.implStateLock.Unlock() 116 | r.rerender(func(err error) { 117 | r.implStateLock.Lock() 118 | if !r.smoothCamera { 119 | r.translateFrom = v2i.Vec{X: math.MaxInt, Y: math.MaxInt} 120 | } 121 | r.translateFromStop = v2i.Vec{X: math.MaxInt, Y: math.MaxInt} 122 | r.implStateLock.Unlock() 123 | }) 124 | } 125 | // Reset camera transform (100% of surface) 126 | if inpututil.IsKeyJustPressed(ebiten.KeyR) { 127 | r.implStateLock.Lock() 128 | r.implState.Bb = toBox2(r.impl.BoundingBox()) // 100% zoom (impl2 will fix aspect ratio) 129 | r.implStateLock.Unlock() 130 | r.rerender() 131 | } 132 | } 133 | 134 | func (r *Renderer) apply2DCameraMoveTo(cx int, cy int) *internal.RendererState { 135 | newVal := deepcopy.MustAnything(r.implState).(*internal.RendererState) 136 | newVal.Bb = r.implState.Bb.Translate( 137 | conv.V2iToV2(r.translateFrom).Sub(conv.V2iToV2(v2i.Vec{X: cx, Y: cy})).Mul(v2.Vec{X: 1, Y: -1}). // Invert Y 138 | Div(conv.V2iToV2(r.screenSize)).Mul(r.implState.Bb.Size())) 139 | return newVal 140 | } 141 | 142 | func getCursor() (int, int) { 143 | cx, cy := ebiten.CursorPosition() 144 | if tX, tY := ebiten.TouchPosition(0); tX != 0 && tY != 0 { // Override cursor with touch if available 145 | cx, cy = tX, tY // FIXME: Probably 0 does not exist anymore 146 | } 147 | return cx, cy 148 | } 149 | 150 | func (r *Renderer) onUpdateInputsSDF3() { 151 | // Zooming 152 | _, wheelUpDown := ebiten.Wheel() 153 | if wheelUpDown != 0 { 154 | r.implStateLock.Lock() 155 | scale := 1 - wheelUpDown*100 156 | scale = math.Max(1/r.zoomFactor, math.Min(r.zoomFactor, scale)) // Apply zoom limits 157 | r.implState.CamDist *= scale 158 | r.implStateLock.Unlock() 159 | r.rerender() 160 | } 161 | r.onUpdateInputsSDF3RotTrans() 162 | // Reset camera transform 163 | if inpututil.IsKeyJustPressed(ebiten.KeyR) { 164 | r.implStateLock.Lock() 165 | resetCam3(r.implState, r) 166 | r.implStateLock.Unlock() 167 | r.rerender() 168 | } 169 | } 170 | 171 | func (r *Renderer) onUpdateInputsSDF3RotTrans() { 172 | // Rotation + Translation 173 | if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonMiddle) || len(inpututil.JustPressedTouchIDs()) > 0 { 174 | // Save the cursor's position for previsualization and applying the final translation 175 | cx, cy := getCursor() 176 | r.implStateLock.Lock() 177 | if r.translateFrom.X == math.MaxInt { // Only if not already moving... 178 | r.translateFrom = v2i.Vec{X: cx, Y: cy} 179 | } 180 | r.implStateLock.Unlock() 181 | } 182 | if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonMiddle) || inpututil.IsTouchJustReleased(0) { 183 | // Actually apply the translation and force a rerender 184 | cx, cy := getCursor() 185 | r.implStateLock.Lock() 186 | if r.translateFrom.X != math.MaxInt { // Only if already moving... 187 | r.implState = r.apply3DCameraMoveTo(cx, cy) 188 | // Keep displacement until rerender is complete (avoid jump) using callback below + extra variable set here 189 | r.translateFromStop = v2i.Vec{X: cx, Y: cy} 190 | } 191 | if r.smoothCamera { 192 | r.translateFrom = v2i.Vec{X: math.MaxInt, Y: math.MaxInt} 193 | } 194 | r.implStateLock.Unlock() 195 | r.rerender(func(err error) { 196 | r.implStateLock.Lock() 197 | if !r.smoothCamera { 198 | r.translateFrom = v2i.Vec{X: math.MaxInt, Y: math.MaxInt} 199 | } 200 | r.translateFromStop = v2i.Vec{X: math.MaxInt, Y: math.MaxInt} 201 | r.implStateLock.Unlock() 202 | }) 203 | } 204 | } 205 | 206 | func (r *Renderer) apply3DCameraMoveTo(cx int, cy int) *internal.RendererState { 207 | newVal := deepcopy.MustAnything(r.implState).(*internal.RendererState) 208 | delta := conv.V2iToV2(v2i.Vec{X: cx, Y: cy}).Sub(conv.V2iToV2(r.translateFrom)) 209 | if ebiten.IsKeyPressed(ebiten.KeyShift) { // Translation 210 | // Move on the plane perpendicular to the camera's direction 211 | camViewMatrix := cam3MatrixNoTranslation(r.implState) 212 | camPos := r.implState.CamCenter.Add(camViewMatrix.MulPosition(v3.Vec{Y: -r.implState.CamDist})) 213 | camDir := r.implState.CamCenter.Sub(camPos).Normalize() 214 | planeZero := r.implState.CamCenter 215 | planeRight := v3.Vec{Z: 1}.Cross(camDir).Normalize() 216 | planeUp := camDir.Cross(planeRight).Normalize() 217 | newPos := planeZero. // TODO: Proper projection on plane delta computation 218 | Add(planeRight.MulScalar(delta.X * r.implState.CamDist / 200)). 219 | Add(planeUp.MulScalar(delta.Y * r.implState.CamDist / 200)) 220 | newVal.CamCenter = newPos 221 | //log.Println("New camera pivot (center", r.implState.CamCenter, ")") 222 | } else { // Rotation 223 | newVal.CamYaw -= delta.X / 100 // TODO: Proper delta computation 224 | if newVal.CamYaw < -math.Pi { 225 | newVal.CamYaw += 2 * math.Pi // Limits (wrap around) 226 | } else if newVal.CamYaw > math.Pi { 227 | newVal.CamYaw -= 2 * math.Pi // Limits (wrap around) 228 | } 229 | newVal.CamPitch -= delta.Y / 100 230 | newVal.CamPitch = math.Max(-(math.Pi/2 - 1e-5), math.Min(math.Pi/2-1e-5, newVal.CamPitch)) 231 | //log.Println("New camera rotation (pitch", r.implState.CamPitch, "yaw", r.implState.CamYaw, ")") 232 | } 233 | return newVal 234 | } 235 | 236 | // ControlsText returns the help text 237 | func (r *Renderer) drawUI(screen *ebiten.Image) { 238 | // Notify when rendering 239 | ctx, cancelFunc := context.WithTimeout(context.Background(), time.Millisecond) 240 | defer cancelFunc() 241 | if r.renderingLock.RTryLock(ctx) { 242 | r.renderingLock.RUnlock() 243 | } else { 244 | drawDefaultTextWithShadow(screen, "Rendering...", 5, 5+12, color.RGBA{R: 255, A: 255}) 245 | } 246 | 247 | // Draw current state and controls 248 | r.implStateLock.RLock() 249 | defer r.implStateLock.RUnlock() 250 | msgFmt := "TPS: %0.2f/%d\nResolution: %.2f [+ or = / -]\nColor: %d [C]\nBoxes: %t [B]\nReset camera [R]" 251 | msgValues := []interface{}{ebiten.CurrentTPS(), ebiten.MaxTPS(), 1 / float64(r.implState.ResInv), r.implState.ColorMode, r.implState.DrawBbs} 252 | switch r.implDimCache { 253 | case 2: 254 | msgFmt = "SDF2 Renderer\n=============\n" + msgFmt + "\nTranslate cam [MiddleMouse]\nZoom cam [MouseWheel]" 255 | case 3: 256 | msgFmt = "SDF3 Renderer\n=============\n" + msgFmt + "\nRotate cam [MiddleMouse]\nTranslate cam [Shift+MiddleMouse]\nZoom cam [MouseWheel]" 257 | } 258 | msg := fmt.Sprintf(msgFmt, msgValues...) 259 | boundString := text.BoundString(defaultFont, msg) 260 | drawDefaultTextWithShadow(screen, msg, 5, r.screenSize.Y-boundString.Size().Y+10, color.RGBA{G: 255, A: 255}) 261 | } 262 | -------------------------------------------------------------------------------- /internal/reflect.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "github.com/deadsy/sdfx/sdf" 7 | "github.com/deadsy/sdfx/vec/v2" 8 | "github.com/deadsy/sdfx/vec/v3" 9 | "github.com/mitchellh/reflectwalk" 10 | "reflect" 11 | "unsafe" 12 | ) 13 | 14 | // ReflectionSDF provides reflect-based metadata about the SDF hierarchy with the provided root: bounding boxes, etc. 15 | // Remember that reflect is relatively slow and results should be cached. 16 | type ReflectionSDF struct { 17 | sdf interface{} 18 | } 19 | 20 | // NewReflectionSDF is internal: do not use outside this project 21 | func NewReflectionSDF(sdf interface{}) *ReflectionSDF { 22 | return &ReflectionSDF{sdf: sdf} 23 | } 24 | 25 | // ReflectTree is internal: do not use outside this project 26 | type ReflectTree struct { 27 | Info *SDFNodeMeta 28 | Children []*ReflectTree 29 | } 30 | 31 | // GetReflectTree is internal: do not use outside this project 32 | func (r *ReflectionSDF) GetReflectTree(targetTypes ...reflect.Type) *ReflectTree { 33 | return r.getReflectTree(r.sdf, targetTypes...) 34 | } 35 | 36 | func (r *ReflectionSDF) getReflectTree(rootSdf interface{}, targetSdfTypes ...reflect.Type) *ReflectTree { 37 | var res *ReflectTree 38 | parentToSubRes := map[interface{}]*ReflectTree{} 39 | uniqueID := 0 40 | err := reflectwalk.Walk([]interface{}{rootSdf}, /* <-- Wrapper for root to work */ 41 | newTreeSDF2WalkerFunc(func(parents []*SDFNodeMeta, value *SDFNodeMeta) error { 42 | // Process the full hierarchy 43 | value.ID = uniqueID 44 | uniqueID++ 45 | subTree := &ReflectTree{Info: value, Children: []*ReflectTree{}} 46 | if len(parents) == 0 { // Single root node 47 | res = subTree 48 | } else { // Some child 49 | parentToRegisterTo := parents[len(parents)-1].SDF 50 | appendTo := parentToSubRes[parentToRegisterTo] 51 | appendTo.Children = append(appendTo.Children, subTree) 52 | } 53 | parentToSubRes[value.SDF] = subTree 54 | return nil 55 | }, targetSdfTypes...)) 56 | if err != nil { 57 | panic(err) // Shouldn't happen 58 | } 59 | return res 60 | } 61 | 62 | // SDFNodeMeta is internal: do not use outside this project 63 | type SDFNodeMeta struct { 64 | ID int // An unique ID for this node (unique for the current tree) 65 | Level int // The fake level (it is not consistent across different branches) 66 | Bb sdf.Box3 // The cached bounding box (as it can be sent through the network) 67 | // The following are only available in main renderer mode (can't be sent through the network and needs a code restart to use) 68 | SDF interface{} // The SDF (2D/3D) 69 | Value reflect.Value // The Value (can be modified!) 70 | } 71 | 72 | func init() { 73 | gob.Register(sdf.Box3{}) 74 | } 75 | 76 | // GobEncode is internal: do not use outside this project 77 | func (s *SDFNodeMeta) GobEncode() ([]byte, error) { 78 | buf := &bytes.Buffer{} 79 | err := gob.NewEncoder(buf).Encode([]interface{}{s.ID, s.Level, s.Bb}) 80 | return buf.Bytes(), err 81 | } 82 | 83 | // GobDecode is internal: do not use outside this project 84 | func (s *SDFNodeMeta) GobDecode(bs []byte) error { 85 | var tmp []interface{} 86 | err := gob.NewDecoder(bytes.NewReader(bs)).Decode(&tmp) 87 | if err != nil { 88 | return nil 89 | } 90 | s.ID = tmp[0].(int) 91 | s.SDF = tmp[1] 92 | s.Bb = tmp[2].(sdf.Box3) 93 | return err 94 | } 95 | 96 | type treeSDFWalkerFunc struct { 97 | impl func(parents []*SDFNodeMeta, value *SDFNodeMeta) error 98 | targetSdfTypes []reflect.Type 99 | curParents []*SDFNodeMeta 100 | lastFound *SDFNodeMeta 101 | curLevel, minLevelSinceLast int 102 | } 103 | 104 | func newTreeSDF2WalkerFunc(impl func(parents []*SDFNodeMeta, value *SDFNodeMeta) error, targetSdfTypes ...reflect.Type) *treeSDFWalkerFunc { 105 | return &treeSDFWalkerFunc{impl: impl, targetSdfTypes: targetSdfTypes} 106 | } 107 | 108 | func (i *treeSDFWalkerFunc) Enter(_ reflectwalk.Location) error { 109 | i.curLevel++ 110 | return nil 111 | } 112 | 113 | func (i *treeSDFWalkerFunc) Exit(_ reflectwalk.Location) error { 114 | i.curLevel-- 115 | if i.curLevel < i.minLevelSinceLast { 116 | i.minLevelSinceLast = i.curLevel 117 | } 118 | return nil 119 | } 120 | 121 | func (i *treeSDFWalkerFunc) Interface(value reflect.Value) error { 122 | // FIXME: Assumes all SDF2 are always saved as interfaces (but they could be concrete types: pointers, functions, maps, etc.) 123 | if !value.CanInterface() { 124 | // HACK: Read-only access to unexported value (Interface() is not allowed due to possible write operations?) 125 | value = getUnexportedField(value, unsafe.Pointer(value.UnsafeAddr())) 126 | } 127 | value = value.Elem() // The internal element of the interface 128 | for _, tp := range i.targetSdfTypes { 129 | if value.Type().Implements(tp) { 130 | return i.handleSDF(value, value.Interface()) 131 | } 132 | } 133 | return nil 134 | } 135 | 136 | func (i *treeSDFWalkerFunc) handleSDF(value reflect.Value, s interface{}) error { 137 | // FIXME: Detect and avoid infinite loops (self-references in hierarchy) 138 | //log.Println("handleSDF:", value.Type(), i.curLevel) 139 | 140 | // Find out which SDF to assign to this SDF2 141 | if i.lastFound != nil && i.curLevel > i.lastFound.Level && i.minLevelSinceLast > i.lastFound.Level { // Below in hierarchy 142 | i.curParents = append(i.curParents, i.lastFound) 143 | } else { 144 | isAbove := false 145 | for len(i.curParents) > 0 && i.curLevel <= i.curParents[len(i.curParents)-1].Level { // Above in hierarchy 146 | isAbove = true 147 | i.curParents = i.curParents[:len(i.curParents)-1] 148 | } 149 | if !isAbove { // At the same Level 150 | // Nothing to change 151 | } 152 | } 153 | 154 | // Extra cached values 155 | var bb sdf.Box3 156 | switch tmp := s.(type) { 157 | case sdf.SDF2: 158 | toV3 := func(v2 v2.Vec, z float64) v3.Vec { return v3.Vec{X: v2.X, Y: v2.Y, Z: z} } 159 | bb = sdf.Box3{Min: toV3(tmp.BoundingBox().Min, -1e-3), Max: toV3(tmp.BoundingBox().Max, 1e-3)} 160 | case sdf.SDF3: 161 | bb = tmp.BoundingBox() 162 | } 163 | 164 | // Record the last found Level and reset minLevelSinceLast 165 | i.lastFound = &SDFNodeMeta{ 166 | ID: -1, 167 | Level: i.curLevel, 168 | Bb: bb, 169 | SDF: s, 170 | Value: value, 171 | } 172 | i.minLevelSinceLast = i.curLevel + 1 // Will be reset on next iteration to curLevel if going back up the tree 173 | 174 | // Call the implementation 175 | err := i.impl(i.curParents, i.lastFound) 176 | 177 | return err 178 | } 179 | 180 | func getUnexportedField(field reflect.Value, unsafeAddr unsafe.Pointer) reflect.Value { 181 | return reflect.NewAt(field.Type(), unsafeAddr).Elem() 182 | } 183 | -------------------------------------------------------------------------------- /internal/reflect2.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/deadsy/sdfx/sdf" 5 | "github.com/deadsy/sdfx/vec/v2" 6 | "reflect" 7 | ) 8 | 9 | var sdf2Type = reflect.TypeOf((*sdf.SDF2)(nil)).Elem() 10 | 11 | // GetReflectSDFTree2 is internal: do not use outside this project 12 | func (r *ReflectionSDF) GetReflectSDFTree2() *ReflectTree { 13 | return r.GetReflectTree(sdf2Type) 14 | } 15 | 16 | // GetBoundingBoxes2 is internal: do not use outside this project 17 | func (r *ReflectTree) GetBoundingBoxes2() []sdf.Box2 { 18 | return r.getBoundingBoxes2(r) 19 | } 20 | 21 | // getBoundingBoxes2 flattens the tree if only bounds are wanted 22 | func (r *ReflectTree) getBoundingBoxes2(tree *ReflectTree) []sdf.Box2 { 23 | var res []sdf.Box2 24 | // HACK: Skip condition: to make results cleaner 25 | skipParent := false 26 | // HACK: Stop condition (apart from finishing the tree): to make results cleaner 27 | skipChildren := false 28 | if !tree.Info.Value.IsNil() { 29 | tpName := tree.Info.Value.Type().String() 30 | switch tpName { 31 | //case "*ui.swapYZ": 32 | // fallthrough 33 | //case "*ui.invertZ": 34 | // skipParent = true 35 | case "*sdf.TransformSDF2": 36 | fallthrough 37 | case "*sdf.ScaleUniformSDF2": 38 | skipChildren = true 39 | default: 40 | } 41 | } 42 | if //goland:noinspection GoBoolExpressions 43 | !skipParent { 44 | res = append(res, sdf.Box2{ 45 | Min: v2.Vec{X: tree.Info.Bb.Min.X, Y: tree.Info.Bb.Min.Y}, 46 | Max: v2.Vec{X: tree.Info.Bb.Max.X, Y: tree.Info.Bb.Max.Y}, 47 | }) 48 | } 49 | if !skipChildren { 50 | for _, subTree := range tree.Children { 51 | res = append(res, r.getBoundingBoxes2(subTree)...) 52 | } 53 | } 54 | return res 55 | } 56 | -------------------------------------------------------------------------------- /internal/reflect3.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/deadsy/sdfx/sdf" 5 | "reflect" 6 | ) 7 | 8 | var sdf3Type = reflect.TypeOf((*sdf.SDF3)(nil)).Elem() 9 | 10 | // GetReflectSDFTree3 is internal: do not use outside this project 11 | func (r *ReflectionSDF) GetReflectSDFTree3() *ReflectTree { 12 | // NOTE: The SDF3 hierarchy may also contain SDF2 (most likely for initial 2D design that is later extruded) 13 | // TODO: Include SDF2 boxes? Works, but results in ugly renderings 14 | return r.GetReflectTree( /*sdf2Type, */ sdf3Type) 15 | } 16 | 17 | // GetBoundingBoxes3 is internal: do not use outside this project 18 | func (r *ReflectTree) GetBoundingBoxes3() []sdf.Box3 { 19 | return r.getBoundingBoxes3(r) 20 | } 21 | 22 | // getBoundingBoxes3 flattens the tree if only bounds are wanted 23 | func (r *ReflectTree) getBoundingBoxes3(tree *ReflectTree) []sdf.Box3 { 24 | var res []sdf.Box3 25 | // HACK: Skip condition: to make results cleaner 26 | skipParent := false 27 | // HACK: Stop condition (apart from finishing the tree): to make results cleaner 28 | skipChildren := false 29 | if tree.Info.Value.Kind() != reflect.Invalid && !tree.Info.Value.IsNil() { 30 | tpName := tree.Info.Value.Type().String() 31 | switch tpName { 32 | case "*ui.swapYZ": 33 | fallthrough 34 | case "*ui.invertZ": 35 | skipParent = true 36 | case "*sdf.TransformSDF3": 37 | fallthrough 38 | case "*sdf.ScaleUniformSDF3": 39 | skipChildren = true 40 | default: 41 | } 42 | } 43 | if !skipParent { 44 | res = append(res, tree.Info.Bb) 45 | } 46 | if !skipChildren { 47 | for _, subTree := range tree.Children { 48 | res = append(res, r.getBoundingBoxes3(subTree)...) 49 | } 50 | } 51 | return res 52 | } 53 | -------------------------------------------------------------------------------- /internal/reflect_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "github.com/deadsy/sdfx/sdf" 6 | v2 "github.com/deadsy/sdfx/vec/v2" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func preOrderNumSDFs(tree *ReflectTree, pre []int, level int) []int { 12 | pre = append(pre, len(tree.Children)) 13 | fmt.Printf("%s> %#v\n", strings.Repeat(" ", level), tree.Info.SDF) 14 | for _, ch := range tree.Children { 15 | pre = preOrderNumSDFs(ch, pre, level+1) 16 | } 17 | return pre 18 | } 19 | 20 | func testTree2Common(t *testing.T, d sdf.SDF2, expectedPreOrderNumSDFs []int) { 21 | tree := NewReflectionSDF(d).GetReflectSDFTree2() 22 | gotPreOrderNumSDFs := preOrderNumSDFs(tree, []int{}, 0) 23 | if len(expectedPreOrderNumSDFs) != len(gotPreOrderNumSDFs) { 24 | t.Fatalf("expected a total of %d nodes in the reflection tree, but got %d", 25 | len(expectedPreOrderNumSDFs), len(gotPreOrderNumSDFs)) // Count the root node 26 | } 27 | for step, expected := range expectedPreOrderNumSDFs { 28 | got := gotPreOrderNumSDFs[step] 29 | if expected != got { 30 | t.Fatalf("expected %d children SDFs at pre-order step %d, but got %d", expected, step, got) 31 | } 32 | } 33 | } 34 | 35 | func TestReflectTree2Single(t *testing.T) { 36 | s := sdf.Box2D(v2.Vec{X: 1, Y: 1}, 0.25) 37 | testTree2Common(t, s, []int{0}) 38 | } 39 | 40 | func TestReflectTree2Union(t *testing.T) { 41 | var s sdf.SDF2 42 | s = sdf.Box2D(v2.Vec{X: 1, Y: 1}, 0.25) 43 | s2 := sdf.Box2D(v2.Vec{X: 2, Y: 1}, 0.25) 44 | s = sdf.Union2D(s, s2) 45 | testTree2Common(t, s, []int{2, 0, 0}) 46 | } 47 | 48 | func TestReflectTree2MultiLevel(t *testing.T) { 49 | var s sdf.SDF2 50 | s = sdf.Box2D(v2.Vec{X: 1, Y: 1}, 0.25) 51 | s2 := sdf.Box2D(v2.Vec{X: 2, Y: 1}, 0.25) 52 | s = sdf.Union2D(s, s2) 53 | s2 = sdf.Box2D(v2.Vec{X: 1, Y: 2}, 0.25) 54 | s = sdf.Difference2D(s, s2) 55 | testTree2Common(t, s, []int{2, 2, 0, 0, 0}) 56 | } 57 | -------------------------------------------------------------------------------- /internal/remote.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/barkimedes/go-deepcopy" 7 | "github.com/deadsy/sdfx/sdf" 8 | "github.com/deadsy/sdfx/vec/v2i" 9 | "image" 10 | "log" 11 | "net/rpc" 12 | "os" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | // RendererService is an internal struct that has to be exported for RPC. 18 | // is the server counterpart to rendererClient. 19 | // It provides remote access to a devRendererImpl. 20 | type RendererService struct { 21 | impl DevRendererImpl 22 | prevRenderCancel func() 23 | renderCtx context.Context 24 | stateLock, cachedRenderLock *sync.RWMutex 25 | renders chan *RemoteRenderResults 26 | done chan os.Signal 27 | reflectTree *ReflectTree 28 | } 29 | 30 | // NewDevRendererService see RendererService 31 | func NewDevRendererService(impl DevRendererImpl, done chan os.Signal) *rpc.Server { 32 | server := rpc.NewServer() 33 | srv := RendererService{ 34 | impl: impl, 35 | prevRenderCancel: func() {}, 36 | renderCtx: context.Background(), 37 | renders: make(chan *RemoteRenderResults), 38 | done: done, 39 | } 40 | close(srv.renders) // Mark the previous render as finished 41 | err := server.Register(&srv) 42 | if err != nil { 43 | panic(err) // Shouldn't happen (only on bad implementation) 44 | } 45 | return server 46 | } 47 | 48 | // Dimensions is an internal method that has to be exported for RPC. 49 | func (d *RendererService) Dimensions(_ int, out *int) error { 50 | *out = d.impl.Dimensions() 51 | return nil 52 | } 53 | 54 | // BoundingBox is an internal method that has to be exported for RPC. 55 | func (d *RendererService) BoundingBox(_ sdf.Box3, out *sdf.Box3) error { 56 | *out = d.impl.BoundingBox() 57 | return nil 58 | } 59 | 60 | // ReflectTree is an internal method that has to be exported for RPC. 61 | func (d *RendererService) ReflectTree(_ sdf.Box3, out *ReflectTree) error { 62 | out = d.impl.ReflectTree() 63 | d.reflectTree = out 64 | return nil 65 | } 66 | 67 | // ColorModes is an internal method that has to be exported for RPC. 68 | func (d *RendererService) ColorModes(_ int, out *int) error { 69 | *out = d.impl.ColorModes() 70 | return nil 71 | } 72 | 73 | // RemoteRenderArgs is an internal struct that has to be exported for RPC. 74 | // 75 | //goland:noinspection GoDeprecation 76 | type RemoteRenderArgs struct { 77 | RenderSize v2i.Vec 78 | State *RendererState 79 | } 80 | 81 | // RemoteRenderResults is an internal struct that has to be exported for RPC. 82 | // 83 | //goland:noinspection GoDeprecation 84 | type RemoteRenderResults struct { 85 | IsPartial bool 86 | RenderedImg *image.RGBA 87 | NewState *RendererState 88 | } 89 | 90 | // RenderStart is an internal method that has to be exported for RPC. 91 | // RenderStart starts a new render (cancelling the previous one) 92 | func (d *RendererService) RenderStart(args RemoteRenderArgs, _ *int) error { 93 | d.prevRenderCancel() // Cancel previous render always (no concurrent renderings, although each rendering is parallel by itself) 94 | var newCtx context.Context 95 | newCtx, d.prevRenderCancel = context.WithCancel(context.Background()) 96 | loop: // Wait for previous renders to be properly completed/cancelled before continuing 97 | for { 98 | select { 99 | case <-newCtx.Done(): // End before started 100 | return newCtx.Err() 101 | case _, ok := <-d.renders: 102 | if !ok { 103 | break loop 104 | } 105 | } 106 | } 107 | d.stateLock = &sync.RWMutex{} 108 | d.cachedRenderLock = &sync.RWMutex{} 109 | d.cachedRenderLock.Lock() 110 | d.renderCtx = newCtx 111 | d.renders = make(chan *RemoteRenderResults) 112 | if d.reflectTree == nil { 113 | _ = d.ReflectTree(sdf.Box3{}, d.reflectTree) 114 | } 115 | args.State.ReflectTree = d.reflectTree // HACK: Avoid sending the reflection-based tree over the network (use local cached version) 116 | d.cachedRenderLock.Unlock() 117 | partialRenders := make(chan *image.RGBA) 118 | partialRendersFinish := make(chan struct{}) 119 | go func() { // Start processing partial renders as requested (will silently drop it if not requested) 120 | loop: 121 | for partialRender := range partialRenders { 122 | select { 123 | case <-d.renderCtx.Done(): 124 | log.Println("[DevRenderer] partialRender cancel") 125 | break loop 126 | case d.renders <- &RemoteRenderResults{ 127 | IsPartial: true, 128 | RenderedImg: partialRender, 129 | NewState: args.State, 130 | }: 131 | default: 132 | } 133 | } 134 | close(partialRendersFinish) 135 | }() 136 | go func() { // spawn the blocking render in a different goroutine 137 | fullRender := image.NewRGBA(image.Rect(0, 0, args.RenderSize.X, args.RenderSize.Y)) 138 | err := d.impl.Render(&RenderArgs{ 139 | Ctx: d.renderCtx, 140 | State: args.State, 141 | StateLock: d.stateLock, 142 | CachedRenderLock: d.cachedRenderLock, 143 | PartialRenders: partialRenders, 144 | FullRender: fullRender, 145 | }) 146 | if err != nil { 147 | log.Println("[DevRenderer] RendererService.Render error:", err) 148 | } 149 | <-partialRendersFinish // Make sure all partial renders are sent before the full render 150 | if err == nil { // Now we can send the full render 151 | select { 152 | case d.renders <- &RemoteRenderResults{ 153 | IsPartial: false, 154 | RenderedImg: fullRender, 155 | NewState: args.State, 156 | }: 157 | case <-d.renderCtx.Done(): 158 | } 159 | } 160 | close(d.renders) 161 | }() 162 | return nil 163 | } 164 | 165 | var errNoRenderRunning = errors.New("no render currently running") 166 | 167 | // RenderGet is an internal struct that has to be exported for RPC. 168 | // RenderGet gets the next partial or full render available (partial renders might be lost if not called, but not the full render). 169 | // It will return an error if no render is running (or it was cancelled before returning the next result) 170 | func (d *RendererService) RenderGet(_ int, out *RemoteRenderResults) error { 171 | //d.renderMu.Lock() 172 | //defer d.renderMu.Unlock() 173 | select { 174 | case read, ok := <-d.renders: 175 | if !ok { 176 | return errNoRenderRunning 177 | } 178 | out.IsPartial = read.IsPartial 179 | d.cachedRenderLock.RLock() // Need to perform a copy of the image to avoid races with the encoder task 180 | out.RenderedImg = image.NewRGBA(read.RenderedImg.Rect) 181 | copy(out.RenderedImg.Pix, read.RenderedImg.Pix) 182 | d.cachedRenderLock.RUnlock() 183 | d.stateLock.RLock() 184 | out.NewState = deepcopy.MustAnything(read.NewState).(*RendererState) 185 | d.stateLock.RUnlock() 186 | return nil 187 | case <-d.renderCtx.Done(): 188 | return errNoRenderRunning // It was cancelled after get was called 189 | } 190 | } 191 | 192 | // RenderCancel is an internal struct that has to be exported for RPC. 193 | // RenderCancel cancels the current rendering. It will always succeed with no error. 194 | func (d *RendererService) RenderCancel(_ int, _ *int) error { 195 | //d.renderMu.Lock() 196 | //defer d.renderMu.Unlock() 197 | d.prevRenderCancel() // Cancel previous render 198 | return nil 199 | } 200 | 201 | // Shutdown is an internal struct that has to be exported for RPC. 202 | // Shutdown sends a signal on the configured channel (with a timeout) 203 | func (d *RendererService) Shutdown(t time.Duration, _ *int) error { 204 | select { 205 | case d.done <- os.Kill: 206 | return nil 207 | case <-time.After(t): 208 | return errors.New("shutdown timeout") 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /internal/state.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "github.com/deadsy/sdfx/sdf" 6 | "github.com/deadsy/sdfx/vec/v3" 7 | "image" 8 | "sync" 9 | ) 10 | 11 | // DevRendererImpl is the interface implemented by the SDF2 and SDF3 renderers. 12 | // Note that the implementation is independent of the graphics backend used and renders CPU images. 13 | type DevRendererImpl interface { 14 | // Dimensions are 2 for SDF2 and 3 for SDF3 15 | Dimensions() int 16 | // BoundingBox returns the full bounding box of the surface (Z is ignored for SDF2) 17 | BoundingBox() sdf.Box3 18 | // ReflectTree returns the main reflection-based metadata structure for handling the SDF hierarchy 19 | ReflectTree() *ReflectTree 20 | // ColorModes returns the number of color modes supported 21 | ColorModes() int 22 | // Render performs a full render, given the screen size (it may be cancelled using the given context). 23 | // Returns partially rendered images as progress is made through PartialRenders (if non-nil, channel closed). 24 | Render(args *RenderArgs) error 25 | // TODO: Map clicks to source code? (using reflection on the SDF and profiling/code generation?) 26 | } 27 | 28 | // RendererState is an internal struct that has to be exported for RPC. 29 | type RendererState struct { 30 | // SHARED 31 | ResInv int // How detailed is the image: number screen pixels for each pixel rendered (SDF2: use a power of two) 32 | DrawBbs bool // Whether to show all bounding boxes (useful for debugging subtraction/intersection of SDFs) 33 | ColorMode int // The color mode (each render may support multiple modes) 34 | ReflectTree *ReflectTree // Cached read-only reflection metadata to have some insight into the SDF hierarchy 35 | // SDF2 36 | Bb sdf.Box2 // Controls the scale and displacement 37 | // SDF3 38 | CamCenter v3.Vec // Arc-Ball camera center (the point we are looking at) 39 | CamYaw, CamPitch, CamDist float64 // Arc-Ball rotation angles (around CamCenter) and distance from CamCenter 40 | } 41 | 42 | // RenderArgs is internal: do not use outside this project 43 | type RenderArgs struct { 44 | Ctx context.Context 45 | State *RendererState 46 | StateLock, CachedRenderLock *sync.RWMutex 47 | PartialRenders chan<- *image.RGBA 48 | FullRender *image.RGBA 49 | } 50 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/cenkalti/backoff/v4" 5 | "os/exec" 6 | "time" 7 | ) 8 | 9 | // Option configures a Renderer to statically change its default behaviour. 10 | type Option = func(r *Renderer) 11 | 12 | // NOTE: There are more options defined in impl*.go, all starting with Opt 13 | 14 | // OptMRunCommand replaces the default run command (go run -v .) with any other command generator. 15 | // WARNING: Need to run again the main renderer to apply a change of this option. 16 | func OptMRunCommand(runCmd func() *exec.Cmd) Option { 17 | return func(r *Renderer) { 18 | r.runCmd = runCmd 19 | } 20 | } 21 | 22 | // OptMWatchFiles replaces the default set of files to watch for changes (["."]). 23 | // WARNING: Need to run again the main renderer to apply a change of this option. 24 | func OptMWatchFiles(filePaths []string) Option { 25 | return func(r *Renderer) { 26 | r.watchFiles = filePaths 27 | } 28 | } 29 | 30 | // OptMBackoff changes the default backoff algorithm used when trying to connect to the new code. 31 | // WARNING: Need to run again the main renderer to apply a change of this option. 32 | func OptMBackoff(backOff backoff.BackOff) Option { 33 | return func(r *Renderer) { 34 | r.backOff = backOff 35 | } 36 | } 37 | 38 | // OptMPartialRenderEvery changes the default duration between partial renders (loading a partial render takes a little 39 | // time and slows down the full render if too frequent). 40 | // WARNING: Need to run again the main renderer to apply a change of this option. 41 | func OptMPartialRenderEvery(duration time.Duration) Option { 42 | return func(r *Renderer) { 43 | r.partialRenderEvery = duration 44 | } 45 | } 46 | 47 | // OptMZoom changes the default scaling factor (> 1) 48 | // WARNING: Need to run again the main renderer to apply a change of this option. 49 | func OptMZoom(zoom float64) Option { 50 | return func(r *Renderer) { 51 | r.zoomFactor = zoom 52 | } 53 | } 54 | 55 | // OptMResInv changes the default image pixels per rendererd pixel 56 | // WARNING: Need to run again the main renderer to apply a change of this option. 57 | func OptMResInv(resInv int) Option { 58 | return func(r *Renderer) { 59 | r.implState.ResInv = resInv 60 | } 61 | } 62 | 63 | // OptMColorMode changes the default color mode of the renderer 64 | // WARNING: Need to run again the main renderer to apply a change of this option. 65 | func OptMColorMode(colorMode int) Option { 66 | return func(r *Renderer) { 67 | r.implState.ColorMode = colorMode % r.impl.ColorModes() 68 | } 69 | } 70 | 71 | // OptMSmoothCamera renders camera frames while dragging the mouse if enabled (2D/3D). Disabled by default. 72 | // WARNING: Need to run again the main renderer to apply a change of this option. 73 | func OptMSmoothCamera(smoothCamera bool) Option { 74 | return func(r *Renderer) { 75 | r.smoothCamera = smoothCamera 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /run.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Yeicor/sdfx-ui/internal" 6 | "github.com/cenkalti/backoff/v4" 7 | "github.com/hajimehoshi/ebiten" 8 | "io" 9 | "log" 10 | "net" 11 | "net/http" 12 | "net/rpc" 13 | "os" 14 | "os/exec" 15 | "os/signal" 16 | "strconv" 17 | "syscall" 18 | "time" 19 | ) 20 | 21 | const changeEventThrottle = 100 * time.Millisecond 22 | 23 | func (r *Renderer) runRenderer(runCmdF func() *exec.Cmd, watchFiles []string) error { 24 | if len(watchFiles) > 0 { 25 | watcher, err := newFsWatcher() 26 | if err != nil { 27 | log.Println("Error watching files (won't update on changes):", err) 28 | } else { 29 | defer func(watcher io.Closer) { 30 | err := watcher.Close() 31 | if err != nil { 32 | log.Println("[DevRenderer] File watcher close error:", err) 33 | } 34 | }(watcher) 35 | 36 | go func() { 37 | var runCmd *exec.Cmd 38 | lastEvent := time.Now() 39 | for { 40 | select { 41 | case event, ok := <-watcher.Events: 42 | if !ok { 43 | return 44 | } 45 | if time.Since(lastEvent) < changeEventThrottle { 46 | log.Println("[DevRenderer] Change detected (but throttled)!", event) 47 | continue // Events tend to be generated in bulk if using an IDE, skip them if too close together 48 | } 49 | log.Println("[DevRenderer] Change detected!", event) 50 | runCmd = r.rendererSwapChild(runCmd, runCmdF) 51 | lastEvent = time.Now() 52 | case err, ok := <-watcher.Errors: 53 | if !ok { 54 | return 55 | } 56 | log.Println("[DevRenderer] File watcher error:", err) 57 | } 58 | } 59 | }() 60 | 61 | for _, matchedFile := range watchFiles { 62 | err = watcher.Add(matchedFile) 63 | if err != nil { 64 | log.Println("Error watching file", matchedFile, "-", err) 65 | } 66 | } 67 | } 68 | } 69 | 70 | return ebiten.RunGame(rendererEbitenGame{r}) // blocks until the window is closed 71 | } 72 | 73 | func (r *Renderer) runChild(requestedAddress string) error { 74 | // Listen for signals 75 | done := make(chan os.Signal, 1) 76 | signal.Notify(done, signals()...) 77 | // Set up a remote service that the parent renderer will connect to view the new SDF 78 | service := internal.NewDevRendererService(r.impl, done) 79 | service.HandleHTTP(rpc.DefaultRPCPath, rpc.DefaultDebugPath) 80 | // TODO: Use service.ServeConn() on a pipe to the parent, avoiding using ports (must be as cross-platform as possible) 81 | listener, err := net.Listen("tcp", requestedAddress) // Close() called on srv.Close() 82 | if err != nil { 83 | return err 84 | } 85 | srv := &http.Server{Addr: listener.Addr().String(), Handler: http.DefaultServeMux} 86 | defer func(srv *http.Server) { 87 | err := srv.Close() 88 | if err != nil { 89 | log.Println("[DevRenderer] srv.Close error:", err) 90 | } 91 | }(srv) 92 | go func() { 93 | err := srv.Serve(listener) 94 | if err != nil { 95 | log.Println("[DevRenderer] srv.Serve error:", err) 96 | } 97 | done <- syscall.SIGKILL 98 | }() 99 | log.Println("[DevRenderer] Child service ready...") 100 | <-done // Will block until interrupt is received or the server crashes 101 | log.Println("[DevRenderer] Child service finished successfully...") 102 | return nil 103 | } 104 | 105 | func (r *Renderer) rendererSwapChild(runCmd *exec.Cmd, runCmdF func() *exec.Cmd) *exec.Cmd { 106 | r.implLock.Lock() // No more renders until we swapped the implementation 107 | defer r.implLock.Unlock() 108 | //log.Println("[DevRenderer] r.implLock acquired!") 109 | // 1. Gracefully close the previous command 110 | if runCmd != nil { 111 | log.Println("[DevRenderer] Closing previous child process") 112 | if rend, ok := r.impl.(*rendererClient); ok { 113 | err := rend.Shutdown(5 * time.Second) 114 | if err != nil { 115 | log.Println("[DevRenderer] Closing previous child process ERROR:", err, "(the child will probably keep running in background)") 116 | } 117 | } 118 | } 119 | log.Println("[DevRenderer] Compiling and running new code") 120 | // 2. Get a random free port to ask the child to listen on (it might not be free when the process starts, but ¯\_(ツ)_/¯) 121 | tmpL, err := net.Listen("tcp", ":0") 122 | if err != nil { 123 | log.Println("[DevRenderer] net.Listen error:", err) 124 | return nil 125 | } 126 | requestedFreeAddr := tmpL.Addr().String() 127 | err = tmpL.Close() 128 | if err != nil { 129 | log.Println("[DevRenderer] tmpL.Close error:", err) 130 | return nil 131 | } 132 | // 3. Configure the process and start it in the background 133 | runCmd = runCmdF() 134 | runCmd.Env = append(os.Environ(), requestedAddressEnvKey+"="+requestedFreeAddr) 135 | runCmd.Stdout = os.Stdout // Merge stdout 136 | runCmd.Stderr = os.Stderr // Merge stderr 137 | err = runCmd.Start() 138 | if err != nil { 139 | log.Println("[DevRenderer] runCmd.Start error:", err) 140 | return nil 141 | } 142 | // Note that in case of "go run ...", a new process is forked after successful compilation and the runCmd PID will die. 143 | startupFinished := make(chan *os.ProcessState) // true if success 144 | go func() { 145 | ps, err2 := runCmd.Process.Wait() 146 | if err2 != nil { 147 | log.Println("[DevRenderer] runCmd error:", err2) 148 | } 149 | select { 150 | case startupFinished <- ps: 151 | case <-time.After(time.Minute): // Some timeout for detecting failed build/run to let this goroutine die 152 | } 153 | close(startupFinished) 154 | }() 155 | // 4. Connect to it as fast as possible, with exponential backoff to relax on errors. 156 | log.Println("[DevRenderer] Trying to connect to new code with exponential backoff...") 157 | r.backOff.Reset() 158 | err = backoff.RetryNotify(func() error { 159 | dialHTTP, err := rpc.DialHTTP("tcp", requestedFreeAddr) 160 | if err != nil { 161 | select { 162 | case ps, ok := <-startupFinished: 163 | if ok && !ps.Success() { 164 | err2 := backoff.Permanent(fmt.Errorf("new code crashed (pid " + strconv.Itoa(runCmd.Process.Pid) + 165 | "), fix errors: " + ps.String())) 166 | return err2 167 | } 168 | default: // Do not block checking if process success 169 | } 170 | return err 171 | } 172 | remoteRenderer := newDevRendererClient(dialHTTP) 173 | // 4.1. Swap the renderer on success 174 | r.impl = remoteRenderer 175 | r.implState.ColorMode = r.implState.ColorMode % r.impl.ColorModes() // Use a valid color mode always 176 | r.rerender() // Render the new SDF!!! 177 | return nil 178 | }, r.backOff, func(err error, duration time.Duration) { 179 | log.Println("[DevRenderer] connection error:", err, "- retrying in:", duration) 180 | }) 181 | if err != nil { 182 | log.Println("[DevRenderer] backoff.RetryNotify gave up on connecting, with error:", err) 183 | return runCmd 184 | } 185 | return runCmd 186 | } 187 | -------------------------------------------------------------------------------- /runsignals_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package ui 5 | 6 | import ( 7 | "os" 8 | "syscall" 9 | ) 10 | 11 | func signals() []os.Signal { 12 | return []os.Signal{os.Interrupt, syscall.SIGTERM} 13 | } 14 | -------------------------------------------------------------------------------- /runsignals_other.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package ui 5 | 6 | import ( 7 | "os" 8 | ) 9 | 10 | func signals() []os.Signal { 11 | return []os.Signal{os.Interrupt} 12 | } 13 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/deadsy/sdfx/sdf" 5 | "github.com/deadsy/sdfx/vec/conv" 6 | v2 "github.com/deadsy/sdfx/vec/v2" 7 | "github.com/deadsy/sdfx/vec/v2i" 8 | "github.com/hajimehoshi/ebiten" 9 | "github.com/hajimehoshi/ebiten/text" 10 | "golang.org/x/image/font/inconsolata" 11 | "image/color" 12 | "math" 13 | "runtime" 14 | ) 15 | 16 | func utilSdf2MinMax(s sdf.SDF2, bb sdf.Box2, cells v2i.Vec) (dmin, dmax float64) { 17 | cellSize := bb.Size().Div(conv.V2iToV2(cells)) 18 | for x := 0; x < cells.X; x++ { 19 | for y := 0; y < cells.Y; y++ { 20 | // TODO: Reverse raycast (without limiting to a single direction) to find extreme values instead of 0s 21 | // (should lower sample count for same results) 22 | pos := bb.Min.Add((v2.Vec{X: float64(x), Y: float64(y)}).Mul(cellSize)) 23 | d := s.Evaluate(pos) 24 | dmax = math.Max(dmax, d) 25 | dmin = math.Min(dmin, d) 26 | } 27 | } 28 | return 29 | } 30 | 31 | var defaultFont = inconsolata.Regular8x16 // Just a simple embedded font (to avoid problems with some platforms) 32 | 33 | func drawDefaultTextWithShadow(screen *ebiten.Image, msg string, x, y int, c color.Color) { 34 | if runtime.GOOS != "js" { // Rendering text is slow on JS 35 | for dx := -1; dx <= 1; dx++ { 36 | for dy := -1; dy <= 1; dy++ { 37 | text.Draw(screen, msg, defaultFont, x+dx, y+dy, color.RGBA{R: 0, G: 0, B: 0, A: 50}) // Shadow first (background) 38 | } 39 | } 40 | } 41 | text.Draw(screen, msg, defaultFont, x, y, c) 42 | } 43 | 44 | func toBox2(box3 sdf.Box3) sdf.Box2 { 45 | return sdf.Box2{ 46 | Min: v2.Vec{X: box3.Min.X, Y: box3.Min.Y}, 47 | Max: v2.Vec{X: box3.Max.X, Y: box3.Max.Y}, 48 | } 49 | } 50 | --------------------------------------------------------------------------------