summaryrefslogtreecommitdiff
path: root/cirsim_fyne
diff options
context:
space:
mode:
authorAleksey Veresov <aleksey@veresov.pro>2022-04-22 20:25:47 +0300
committerAleksey Veresov <aleksey@veresov.pro>2022-04-22 20:25:47 +0300
commit571f9b73e79abc7116f29e557a38fc68ec045d3e (patch)
treeb833ba89dc23f03932669f37e86fae0087b89820 /cirsim_fyne
parent6d289b952d1b5c8b8e151ded0195e4658f5d6d0b (diff)
downloadcirsim-571f9b73e79abc7116f29e557a38fc68ec045d3e.tar
cirsim-571f9b73e79abc7116f29e557a38fc68ec045d3e.tar.xz
cirsim-571f9b73e79abc7116f29e557a38fc68ec045d3e.zip
Splits logic and graphics.
Diffstat (limited to 'cirsim_fyne')
-rw-r--r--cirsim_fyne/component.go178
-rw-r--r--cirsim_fyne/node.go94
-rw-r--r--cirsim_fyne/simulation.go223
3 files changed, 495 insertions, 0 deletions
diff --git a/cirsim_fyne/component.go b/cirsim_fyne/component.go
new file mode 100644
index 0000000..299b2bb
--- /dev/null
+++ b/cirsim_fyne/component.go
@@ -0,0 +1,178 @@
+package cirsim_fyne
+
+import (
+ "fmt"
+ "io"
+ "log"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/canvas"
+ "fyne.io/fyne/v2/widget"
+ "git.veresov.xyz/aversey/cirsim/cirsim"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+type component struct {
+ widget.BaseWidget
+ modeler cirsim.Modeler
+ pos fyne.Position
+ modelName string
+ nodes [2]int
+ currentRange chart.Range
+ chart *canvas.Image
+ labels []*widget.Label
+ entries []*widget.Entry
+}
+
+func (c *component) ModelName() string {
+ return c.modelName
+}
+func (c *component) Nodes() [2]int {
+ return c.nodes
+}
+
+func newComponent(settings io.Reader, r chart.Range) *component {
+ var c component
+ var a int
+ var b int
+ _, err := fmt.Fscanf(settings, "%f %f %s %d %d\n",
+ &c.pos.X, &c.pos.Y, &c.modelName, &a, &b,
+ )
+ if err != nil {
+ return nil
+ }
+ if a <= 0 || b <= 0 {
+ log.Fatal("unconnected components in the circuit")
+ }
+ c.nodes[0] = a - 1
+ c.nodes[1] = b - 1
+ c.currentRange = r
+ return &c
+}
+
+func (c *component) setupModeler(modeler cirsim.Modeler, update func()) {
+ c.modeler = modeler
+ c.entries = make([]*widget.Entry, 0)
+ c.labels = make([]*widget.Label, 0)
+ params := c.modeler.Parameters()
+ for k, v := range params {
+ e := widget.NewEntry()
+ e.TextStyle.Monospace = true
+ e.SetPlaceHolder(k)
+ e.SetText(fmt.Sprintf("%f", v))
+ e.OnSubmitted = func(s string) {
+ var v float64
+ _, err := fmt.Sscanf(s+"\n", "%f\n", &v)
+ if err != nil {
+ e.SetText(fmt.Sprintf("%f", params[k]))
+ } else {
+ c.modeler.UpdateParameter(k, v)
+ update()
+ }
+ }
+ c.entries = append(c.entries, e)
+ l := widget.NewLabel(k)
+ l.TextStyle.Monospace = true
+ c.labels = append(c.labels, l)
+ }
+}
+
+func (c *component) renderChart(currentOverTime []float64) {
+ graph := chart.Chart{
+ Width: chartWidth,
+ Height: chartHeight,
+ ColorPalette: &componentColorPalette{},
+ XAxis: chart.HideXAxis(),
+ YAxis: chart.YAxis{
+ Style: chart.Hidden(),
+ Range: c.currentRange,
+ },
+ Series: []chart.Series{
+ chart.ContinuousSeries{
+ XValues: chart.LinearRange(0, float64(len(currentOverTime))-1),
+ YValues: currentOverTime,
+ },
+ },
+ }
+ writer := &chart.ImageWriter{}
+ graph.Render(chart.PNG, writer)
+ img, _ := writer.Image()
+ c.chart = canvas.NewImageFromImage(img)
+}
+
+type componentColorPalette struct{}
+
+func (*componentColorPalette) BackgroundColor() drawing.Color {
+ return drawing.ColorTransparent
+}
+func (*componentColorPalette) BackgroundStrokeColor() drawing.Color {
+ return drawing.ColorTransparent
+}
+func (*componentColorPalette) CanvasColor() drawing.Color {
+ return drawing.Color{
+ R: currentCanvasR,
+ G: currentCanvasG,
+ B: currentCanvasB,
+ A: currentCanvasA,
+ }
+}
+func (*componentColorPalette) CanvasStrokeColor() drawing.Color {
+ return drawing.ColorTransparent
+}
+func (*componentColorPalette) AxisStrokeColor() drawing.Color {
+ return drawing.ColorTransparent
+}
+func (*componentColorPalette) TextColor() drawing.Color {
+ return drawing.ColorTransparent
+}
+func (*componentColorPalette) GetSeriesColor(index int) drawing.Color {
+ return drawing.Color{R: currentR, G: currentG, B: currentB, A: currentA}
+}
+
+func (c *component) CreateRenderer() fyne.WidgetRenderer { return c }
+func (c *component) Layout(s fyne.Size) {
+ const chartWidth = float32(chartWidth)
+ const chartHeight = float32(chartHeight)
+ pos := fyne.NewPos(0, 0)
+ for i, e := range c.entries {
+ c.labels[i].Resize(fyne.NewSize(chartWidth, c.labels[i].MinSize().Height))
+ c.labels[i].Move(pos)
+ pos.Y += c.labels[i].MinSize().Height
+ e.Resize(fyne.NewSize(chartWidth, e.MinSize().Height))
+ e.Move(pos)
+ pos.Y += e.MinSize().Height
+ }
+ c.chart.Resize(fyne.NewSize(chartWidth, chartHeight))
+ c.chart.Move(pos)
+}
+func (c *component) MinSize() fyne.Size {
+ const chartWidth = float32(chartWidth)
+ const chartHeight = float32(chartHeight)
+ res := fyne.NewSize(chartWidth, chartHeight)
+ for _, e := range c.entries {
+ res.Height += e.MinSize().Height
+ }
+ for _, l := range c.labels {
+ res.Height += l.MinSize().Height
+ }
+ return res
+}
+func (c *component) Refresh() {
+ for _, e := range c.entries {
+ e.Refresh()
+ }
+ for _, l := range c.labels {
+ l.Refresh()
+ }
+}
+func (c *component) Destroy() {}
+func (c *component) Objects() []fyne.CanvasObject {
+ res := []fyne.CanvasObject{}
+ for i, e := range c.entries {
+ res = append(res, c.labels[i])
+ res = append(res, e)
+ }
+ res = append(res, c.chart)
+ return res
+}
diff --git a/cirsim_fyne/node.go b/cirsim_fyne/node.go
new file mode 100644
index 0000000..84a15eb
--- /dev/null
+++ b/cirsim_fyne/node.go
@@ -0,0 +1,94 @@
+package cirsim_fyne
+
+import (
+ "fmt"
+ "io"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/canvas"
+ "fyne.io/fyne/v2/widget"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+type node struct {
+ widget.BaseWidget
+ pos fyne.Position
+ voltageOverTime []float64
+ voltageRange chart.Range
+ chart *canvas.Image
+}
+
+func newNode(settings io.Reader, r chart.Range) *node {
+ var n node
+ _, err := fmt.Fscanf(settings, "%f %f\n", &n.pos.X, &n.pos.Y)
+ if err != nil {
+ return nil
+ }
+ n.voltageRange = r
+ return &n
+}
+
+func (n *node) renderChart(voltageOverTime []float64) {
+ graph := chart.Chart{
+ Width: chartWidth,
+ Height: chartHeight,
+ ColorPalette: &nodeColorPalette{},
+ XAxis: chart.HideXAxis(),
+ YAxis: chart.YAxis{
+ Style: chart.Hidden(),
+ Range: n.voltageRange,
+ },
+ Series: []chart.Series{
+ chart.ContinuousSeries{
+ XValues: chart.LinearRange(0, float64(len(voltageOverTime))-1),
+ YValues: voltageOverTime,
+ },
+ },
+ }
+ writer := &chart.ImageWriter{}
+ graph.Render(chart.PNG, writer)
+ img, _ := writer.Image()
+ n.chart = canvas.NewImageFromImage(img)
+}
+
+type nodeColorPalette struct{}
+
+func (*nodeColorPalette) BackgroundColor() drawing.Color {
+ return drawing.ColorTransparent
+}
+func (*nodeColorPalette) BackgroundStrokeColor() drawing.Color {
+ return drawing.ColorTransparent
+}
+func (*nodeColorPalette) CanvasColor() drawing.Color {
+ return drawing.Color{
+ R: voltageCanvasR,
+ G: voltageCanvasG,
+ B: voltageCanvasB,
+ A: voltageCanvasA,
+ }
+}
+func (*nodeColorPalette) CanvasStrokeColor() drawing.Color {
+ return drawing.ColorTransparent
+}
+func (*nodeColorPalette) AxisStrokeColor() drawing.Color {
+ return drawing.ColorTransparent
+}
+func (*nodeColorPalette) TextColor() drawing.Color {
+ return drawing.ColorTransparent
+}
+func (*nodeColorPalette) GetSeriesColor(index int) drawing.Color {
+ return drawing.Color{R: voltageR, G: voltageG, B: voltageB, A: voltageA}
+}
+
+func (n *node) CreateRenderer() fyne.WidgetRenderer { return n }
+func (n *node) Layout(s fyne.Size) {
+ n.chart.Resize(s)
+ n.chart.Move(fyne.NewPos(0, 0))
+}
+func (n *node) MinSize() fyne.Size { return n.chart.MinSize() }
+func (n *node) Refresh() {}
+func (n *node) Destroy() {}
+func (n *node) Objects() []fyne.CanvasObject {
+ return []fyne.CanvasObject{n.chart}
+}
diff --git a/cirsim_fyne/simulation.go b/cirsim_fyne/simulation.go
new file mode 100644
index 0000000..aa53ec8
--- /dev/null
+++ b/cirsim_fyne/simulation.go
@@ -0,0 +1,223 @@
+package cirsim_fyne
+
+import (
+ "fmt"
+ "image/color"
+ "io"
+ "log"
+ "os"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/canvas"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/layout"
+ "fyne.io/fyne/v2/widget"
+ "git.veresov.xyz/aversey/cirsim/cirsim"
+ "github.com/wcharczuk/go-chart/v2"
+)
+
+const (
+ chartWidth int = 140
+ chartHeight = 60
+ currentCanvasR uint8 = 191
+ currentCanvasG = 254
+ currentCanvasB = 247
+ currentCanvasA = 128
+ currentR = 3
+ currentG = 247
+ currentB = 198
+ currentA = 255
+ voltageCanvasR = 249
+ voltageCanvasG = 254
+ voltageCanvasB = 172
+ voltageCanvasA = 128
+ voltageR = 247
+ voltageG = 239
+ voltageB = 3
+ voltageA = 255
+)
+
+type simulation struct {
+ sim cirsim.Simulator
+ size fyne.Size
+ nodes []*node
+ components []*component
+ voltageRange chart.ContinuousRange
+ currentRange chart.ContinuousRange
+ periodEntry *widget.Entry
+ voltageLabel *canvas.Text
+ currentLabel *canvas.Text
+}
+
+func New() fyne.CanvasObject {
+ background := canvas.NewRectangle(color.White)
+ circuit := canvas.NewImageFromFile("circuit.svg")
+ settings, err := os.Open("circuit")
+ if err != nil {
+ log.Fatal(err)
+ }
+ var sim simulation
+ sim.voltageRange = chart.ContinuousRange{Min: 0, Max: 0}
+ sim.currentRange = chart.ContinuousRange{Min: 0, Max: 0}
+ fmt.Fscanf(settings, "%f %f\n\n", &sim.size.Width, &sim.size.Height)
+ cont := container.New(&sim, sim.newPanel(settings), background, circuit)
+ sim.addNodes(cont, settings)
+ sim.addComponents(cont, settings)
+ settings.Close()
+ components := make([]cirsim.ComponentSettings, len(sim.components))
+ for i := range components {
+ components[i] = sim.components[i]
+ }
+ sim.sim = cirsim.New(len(sim.nodes), components)
+ sim.periodEntry.SetPlaceHolder(
+ fmt.Sprintf("default: %fs", sim.sim.Period()))
+ sim.setupComponentModelers()
+ sim.update()
+ return cont
+}
+
+func (sim *simulation) newPanel(settings io.Reader) *fyne.Container {
+ sim.voltageLabel = canvas.NewText(
+ fmt.Sprintf(" %e < voltage < %e ",
+ sim.voltageRange.Min, sim.voltageRange.Max),
+ color.RGBA{R: voltageR, G: voltageG, B: voltageB, A: voltageA},
+ )
+ sim.voltageLabel.TextStyle.Monospace = true
+ sim.currentLabel = canvas.NewText(
+ fmt.Sprintf(" %e < current < %e ",
+ sim.currentRange.Min, sim.currentRange.Max),
+ color.RGBA{R: currentR, G: currentG, B: currentB, A: currentA},
+ )
+ sim.currentLabel.TextStyle.Monospace = true
+ periodLabel := widget.NewLabel("Period")
+ periodLabel.TextStyle.Monospace = true
+ sim.periodEntry = widget.NewEntry()
+ sim.periodEntry.TextStyle.Monospace = true
+ sim.periodEntry.OnSubmitted = sim.updatePeriod
+ return container.NewHBox(
+ sim.voltageLabel,
+ sim.currentLabel,
+ layout.NewSpacer(),
+ periodLabel,
+ container.New(&entryLayout{}, sim.periodEntry),
+ )
+}
+
+func (sim *simulation) addNodes(cont *fyne.Container, settings io.Reader) {
+ n := newNode(settings, &sim.voltageRange)
+ for n != nil {
+ sim.nodes = append(sim.nodes, n)
+ cont.Add(n)
+ n = newNode(settings, &sim.voltageRange)
+ }
+}
+
+func (sim *simulation) addComponents(cont *fyne.Container, settings io.Reader) {
+ c := newComponent(settings, &sim.currentRange)
+ for c != nil {
+ sim.components = append(sim.components, c)
+ cont.Add(c)
+ c = newComponent(settings, &sim.currentRange)
+ }
+}
+
+func (sim *simulation) setupComponentModelers() {
+ for i := range sim.components {
+ sim.components[i].setupModeler(
+ sim.sim.ModelerOfComponent(i), sim.update)
+ }
+}
+
+func (sim *simulation) update() {
+ sim.sim.Simulate()
+ sim.voltageRange.Min, sim.voltageRange.Max = sim.sim.VoltageRange()
+ sim.currentRange.Min, sim.currentRange.Max = sim.sim.CurrentRange()
+ sim.voltageLabel.Text = fmt.Sprintf(" %e < voltage < %e ",
+ sim.voltageRange.Min, sim.voltageRange.Max)
+ sim.currentLabel.Text = fmt.Sprintf(" %e < current < %e ",
+ sim.currentRange.Min, sim.currentRange.Max)
+ sim.voltageLabel.Refresh()
+ sim.currentLabel.Refresh()
+ for i, n := range sim.nodes {
+ n.renderChart(sim.sim.VoltagesOfNode(i))
+ n.Refresh()
+ }
+ for i, c := range sim.components {
+ c.renderChart(sim.sim.CurrentsOfComponent(i))
+ c.Refresh()
+ }
+}
+
+func (sim *simulation) updatePeriod(period string) {
+ var periodVal float64
+ _, err := fmt.Sscanf(period+"\n", "%f\n", periodVal)
+ if err != nil {
+ sim.periodEntry.SetText(fmt.Sprintf("%f", sim.sim.Period()))
+ } else {
+ sim.sim.SetPeriod(periodVal)
+ sim.update()
+ }
+}
+
+func (l *simulation) MinSize(objects []fyne.CanvasObject) fyne.Size {
+ return objects[2].MinSize()
+}
+func (l *simulation) Layout(obs []fyne.CanvasObject, size fyne.Size) {
+ var p fyne.Position
+ var s fyne.Size
+ panelHeight := obs[0].MinSize().Height
+ H := size.Height - panelHeight
+ W := size.Width
+ h := l.size.Height
+ w := l.size.Width
+ var scale float32
+ if H/W > h/w {
+ p = fyne.NewPos(0, (H-W*h/w)/2+panelHeight)
+ s = fyne.NewSize(W, W*h/w)
+ scale = W / w
+ } else {
+ p = fyne.NewPos((W-H*w/h)/2, panelHeight)
+ s = fyne.NewSize(H*w/h, H)
+ scale = H / h
+ }
+ obs[0].Resize(fyne.NewSize(W, panelHeight))
+ obs[0].Move(fyne.NewPos(0, 0))
+ obs[1].Resize(size)
+ obs[1].Move(fyne.NewPos(0, panelHeight))
+ obs[2].Resize(s)
+ obs[2].Move(p)
+ const chartWidth = float32(chartWidth)
+ const chartHeight = float32(chartHeight)
+ for i, n := range l.nodes {
+ obs[3+i].Resize(fyne.NewSize(chartWidth, chartHeight))
+ shift := fyne.NewPos(
+ p.X+n.pos.X*scale-chartWidth/2.0,
+ p.Y+n.pos.Y*scale-chartHeight/2.0,
+ )
+ obs[3+i].Move(shift)
+ }
+ for i, c := range l.components {
+ ms := obs[3+len(l.nodes)+i].MinSize()
+ obs[3+len(l.nodes)+i].Resize(ms)
+ shift := fyne.NewPos(
+ p.X+c.pos.X*scale-chartWidth/2.0,
+ p.Y+c.pos.Y*scale-ms.Height+chartHeight/2.0,
+ )
+ obs[3+len(l.nodes)+i].Move(shift)
+ }
+}
+
+type entryLayout struct{}
+
+func (*entryLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
+ return fyne.NewSize(
+ objects[0].MinSize().Width*6,
+ objects[0].MinSize().Height,
+ )
+}
+func (*entryLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
+ for _, o := range objects {
+ o.Resize(size)
+ o.Move(fyne.NewPos(0, 0))
+ }
+}