From 571f9b73e79abc7116f29e557a38fc68ec045d3e Mon Sep 17 00:00:00 2001 From: Aleksey Veresov Date: Fri, 22 Apr 2022 20:25:47 +0300 Subject: Splits logic and graphics. --- cirsim/component.go | 166 ++------------------------ cirsim/models.go | 28 ++--- cirsim/node.go | 99 ---------------- cirsim/simulation.go | 290 +++++++++++++--------------------------------- cirsim_fyne/component.go | 178 ++++++++++++++++++++++++++++ cirsim_fyne/node.go | 94 +++++++++++++++ cirsim_fyne/simulation.go | 223 +++++++++++++++++++++++++++++++++++ main.go | 4 +- 8 files changed, 597 insertions(+), 485 deletions(-) delete mode 100644 cirsim/node.go create mode 100644 cirsim_fyne/component.go create mode 100644 cirsim_fyne/node.go create mode 100644 cirsim_fyne/simulation.go diff --git a/cirsim/component.go b/cirsim/component.go index 358eee3..99d8b54 100644 --- a/cirsim/component.go +++ b/cirsim/component.go @@ -1,173 +1,23 @@ package cirsim -import ( - "fmt" - "io" - "log" - - "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 ComponentSettings interface { + Nodes() [2]int + ModelName() string +} type component struct { - widget.BaseWidget - modeler - pos fyne.Position + Modeler currentOverTime []float64 nodes [2]int - currentRange chart.Range - chart *canvas.Image - labels []*widget.Label - entries []*widget.Entry } -func newComponent(settings io.Reader, r chart.Range, update func()) *component { +func newComponent(settings ComponentSettings) *component { var c component - var t string - var a int - var b int - _, err := fmt.Fscanf(settings, "%f %f %s %d %d\n", - &c.pos.X, &c.pos.Y, &t, &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.setupModeler(t, update) - c.currentRange = r + c.nodes = settings.Nodes() + c.Modeler = newModeler(settings.ModelName()) c.currentOverTime = make([]float64, iterations) for i := range c.currentOverTime { c.currentOverTime[i] = 0 } - c.renderChart() return &c } - -func (c *component) setupModeler(name string, update func()) { - c.modeler = newModeler(name) - c.entries = make([]*widget.Entry, 0) - c.labels = make([]*widget.Label, 0) - for k, v := range c.parameters() { - 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", c.parameters()[k])) - } else { - c.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() { - 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(iterations)-1), - YValues: c.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) { - 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 { - 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() { - c.renderChart() - 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/models.go b/cirsim/models.go index 70710d7..56531bf 100644 --- a/cirsim/models.go +++ b/cirsim/models.go @@ -5,14 +5,14 @@ import ( "math" ) -type modeler interface { +type Modeler interface { conductance(time, delta, voltage, current float64) float64 current(time, delta, voltage, current float64) float64 - parameters() map[string]float64 - updateParameter(name string, value float64) + Parameters() map[string]float64 + UpdateParameter(name string, value float64) } -func newModeler(name string) modeler { +func newModeler(name string) Modeler { switch name { case "resistor": return newResistor() @@ -45,10 +45,10 @@ func (m *capacitor) conductance(time, delta, voltage, current float64) float64 { func (m *capacitor) current(time, delta, voltage, current float64) float64 { return -current - voltage*2*m.capacitance/delta } -func (m *capacitor) parameters() map[string]float64 { +func (m *capacitor) Parameters() map[string]float64 { return map[string]float64{"Capacitance": m.capacitance} } -func (m *capacitor) updateParameter(name string, value float64) { +func (m *capacitor) UpdateParameter(name string, value float64) { if name == "Capacitance" { m.capacitance = value } @@ -68,10 +68,10 @@ func (m *resistor) conductance(time, delta, voltage, current float64) float64 { func (m *resistor) current(time, delta, voltage, current float64) float64 { return 0 } -func (m *resistor) parameters() map[string]float64 { +func (m *resistor) Parameters() map[string]float64 { return map[string]float64{"Resistance": m.resistance} } -func (m *resistor) updateParameter(name string, value float64) { +func (m *resistor) UpdateParameter(name string, value float64) { if name == "Resistance" { m.resistance = value } @@ -91,10 +91,10 @@ func (m *inductor) conductance(time, delta, voltage, current float64) float64 { func (m *inductor) current(time, delta, voltage, current float64) float64 { return current + voltage*delta/(2*m.inductance) } -func (m *inductor) parameters() map[string]float64 { +func (m *inductor) Parameters() map[string]float64 { return map[string]float64{"Inductance": m.inductance} } -func (m *inductor) updateParameter(name string, value float64) { +func (m *inductor) UpdateParameter(name string, value float64) { if name == "Inductance" { m.inductance = value } @@ -112,10 +112,10 @@ func (m *diode) conductance(time, delta, voltage, current float64) float64 { func (m *diode) current(time, delta, voltage, current float64) float64 { return 0 } -func (m *diode) parameters() map[string]float64 { +func (m *diode) Parameters() map[string]float64 { return map[string]float64{} } -func (m *diode) updateParameter(name string, value float64) {} +func (m *diode) UpdateParameter(name string, value float64) {} type power struct { maxCurrent float64 @@ -132,10 +132,10 @@ func (m *power) conductance(time, delta, voltage, current float64) float64 { func (m *power) current(time, delta, voltage, current float64) float64 { return m.maxCurrent * math.Sin(time*m.frequency*2*math.Pi) } -func (m *power) parameters() map[string]float64 { +func (m *power) Parameters() map[string]float64 { return map[string]float64{"Current": m.maxCurrent, "Frequency": m.frequency} } -func (m *power) updateParameter(name string, value float64) { +func (m *power) UpdateParameter(name string, value float64) { if name == "Current" { m.maxCurrent = value } else if name == "Frequency" { diff --git a/cirsim/node.go b/cirsim/node.go deleted file mode 100644 index d26d78f..0000000 --- a/cirsim/node.go +++ /dev/null @@ -1,99 +0,0 @@ -package cirsim - -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 - n.voltageOverTime = make([]float64, iterations) - for i := range n.voltageOverTime { - n.voltageOverTime[i] = 0 - } - n.renderChart() - return &n -} - -func (n *node) renderChart() { - 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(iterations)-1), - YValues: n.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() { n.renderChart() } -func (n *node) Destroy() {} -func (n *node) Objects() []fyne.CanvasObject { - return []fyne.CanvasObject{n.chart} -} diff --git a/cirsim/simulation.go b/cirsim/simulation.go index 9b7234c..fa3a8bd 100644 --- a/cirsim/simulation.go +++ b/cirsim/simulation.go @@ -1,163 +1,73 @@ package cirsim -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" - "github.com/wcharczuk/go-chart/v2" - "gonum.org/v1/gonum/mat" -) +import "gonum.org/v1/gonum/mat" const ( - defaultPeriod float64 = 0.01 - iterations int = 1000 - chartWidth = 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 + defaultPeriod float64 = 0.01 + iterations int = 1000 ) +type Simulator interface { + Period() float64 + SetPeriod(float64) + VoltageRange() (float64, float64) + CurrentRange() (float64, float64) + VoltagesOfNode(i int) []float64 + CurrentsOfComponent(i int) []float64 + ModelerOfComponent(i int) Modeler + Simulate() +} + type simulation struct { period float64 - size fyne.Size - nodes []*node + voltageMax float64 + voltageMin float64 + currentMax float64 + currentMin float64 + nodeVoltages [][]float64 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) - } +func New(nodesCount int, components []ComponentSettings) Simulator { var sim simulation sim.period = defaultPeriod - 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() - sim.update() - return cont + sim.nodeVoltages = make([][]float64, nodesCount) + for i := range sim.nodeVoltages { + sim.nodeVoltages[i] = make([]float64, iterations) + } + sim.components = make([]*component, 0) + for _, c := range components { + sim.components = append(sim.components, newComponent(c)) + } + sim.Simulate() + return &sim } -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.SetPlaceHolder(fmt.Sprintf("default: %fs", defaultPeriod)) - sim.periodEntry.OnSubmitted = sim.updatePeriod - return container.NewHBox( - sim.voltageLabel, - sim.currentLabel, - layout.NewSpacer(), - periodLabel, - container.New(&entryLayout{}, sim.periodEntry), - ) +func (sim *simulation) Period() float64 { + return sim.period } - -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) SetPeriod(period float64) { + sim.period = period } - -func (sim *simulation) addComponents(cont *fyne.Container, settings io.Reader) { - c := newComponent(settings, &sim.currentRange, sim.update) - for c != nil { - sim.components = append(sim.components, c) - cont.Add(c) - c = newComponent(settings, &sim.currentRange, sim.update) - } +func (sim *simulation) VoltageRange() (float64, float64) { + return sim.voltageMin, sim.voltageMax } - -func (sim *simulation) update() { - sim.simulate() - sim.voltageRange.Max = 0 - sim.voltageRange.Min = 0 - for _, n := range sim.nodes { - for _, v := range n.voltageOverTime { - if v > sim.voltageRange.Max { - sim.voltageRange.Max = v - } else if v < sim.voltageRange.Min { - sim.voltageRange.Min = v - } - } - } - sim.currentRange.Max = sim.components[0].currentOverTime[0] - sim.currentRange.Min = sim.components[0].currentOverTime[0] - for _, comp := range sim.components { - for _, c := range comp.currentOverTime { - if c > sim.currentRange.Max { - sim.currentRange.Max = c - } else if c < sim.currentRange.Min { - sim.currentRange.Min = c - } - } - } - 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 _, n := range sim.nodes { - n.Refresh() - } - for _, c := range sim.components { - c.Refresh() - } +func (sim *simulation) CurrentRange() (float64, float64) { + return sim.currentMin, sim.currentMax +} +func (sim *simulation) VoltagesOfNode(i int) []float64 { + return sim.nodeVoltages[i] +} +func (sim *simulation) CurrentsOfComponent(i int) []float64 { + return sim.components[i].currentOverTime +} +func (sim *simulation) ModelerOfComponent(i int) Modeler { + return sim.components[i] } -func (sim *simulation) simulate() { +func (sim *simulation) Simulate() { sim.nullify() - N := len(sim.nodes) + N := len(sim.nodeVoltages) delta := sim.period / float64(iterations) for i := 0; i != iterations; i++ { // fill conductances and currents: @@ -165,8 +75,8 @@ func (sim *simulation) simulate() { conductances := mat.NewDense(N+1, N, nil) currents := mat.NewVecDense(N+1, nil) for _, c := range sim.components { - voltage := sim.nodes[c.nodes[1]].voltageOverTime[i] - - sim.nodes[c.nodes[0]].voltageOverTime[i] + voltage := sim.nodeVoltages[c.nodes[1]][i] - + sim.nodeVoltages[c.nodes[0]][i] current := c.current(time, delta, voltage, c.currentOverTime[i]) cond := c.conductance(time, delta, voltage, c.currentOverTime[i]) currents.SetVec(c.nodes[1], currents.AtVec(c.nodes[1])-current) @@ -192,8 +102,8 @@ func (sim *simulation) simulate() { voltages := mat.NewVecDense(N, nil) voltages.SolveVec(conductances, currents) // save results: - for j, n := range sim.nodes { - n.voltageOverTime[i] = voltages.AtVec(j) + for j, n := range sim.nodeVoltages { + n[i] = voltages.AtVec(j) } for _, c := range sim.components { voltage := voltages.AtVec(c.nodes[1]) - voltages.AtVec(c.nodes[0]) @@ -202,12 +112,13 @@ func (sim *simulation) simulate() { c.currentOverTime[i] = voltage*cond + current } } + sim.updateRanges() } func (sim *simulation) nullify() { - for _, n := range sim.nodes { - for i := range n.voltageOverTime { - n.voltageOverTime[i] = 0 + for _, n := range sim.nodeVoltages { + for i := range n { + n[i] = 0 } } for _, c := range sim.components { @@ -217,72 +128,27 @@ func (sim *simulation) nullify() { } } -func (sim *simulation) updatePeriod(period string) { - _, err := fmt.Sscanf(period+"\n", "%f\n", &sim.period) - if err != nil { - sim.periodEntry.SetText(fmt.Sprintf("%f", sim.period)) - } else { - 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) - 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) +func (sim *simulation) updateRanges() { + sim.voltageMax = 0 + sim.voltageMin = 0 + for _, n := range sim.nodeVoltages { + for _, v := range n { + if v > sim.voltageMax { + sim.voltageMax = v + } else if v < sim.voltageMin { + sim.voltageMin = v + } + } } -} - -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)) + sim.currentMax = sim.components[0].currentOverTime[0] + sim.currentMin = sim.components[0].currentOverTime[0] + for _, comp := range sim.components { + for _, c := range comp.currentOverTime { + if c > sim.currentMax { + sim.currentMax = c + } else if c < sim.currentMin { + sim.currentMin = c + } + } } } 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)) + } +} diff --git a/main.go b/main.go index 23474fa..b925e09 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/theme" - "git.veresov.xyz/aversey/cirsim/cirsim" + "git.veresov.xyz/aversey/cirsim/cirsim_fyne" ) func main() { @@ -13,6 +13,6 @@ func main() { w := a.NewWindow("Circuit Simulator") w.Resize(fyne.NewSize(1280, 720)) w.SetIcon(theme.SettingsIcon()) - w.SetContent(cirsim.New()) + w.SetContent(cirsim_fyne.New()) w.ShowAndRun() } -- cgit v1.2.3