diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 4fc11c52..f3017930 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -51,6 +51,9 @@ jobs: - name: Build Canvas run: core build web -dir canvas/cmd/canvas -o static/canvas + - name: Build Marbles + run: core build web -dir marbles -o static/marbles + - name: Setup Pages id: pages uses: actions/configure-pages@v3 diff --git a/go.mod b/go.mod index 3fc0985f..d521ab95 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22 require ( cogentcore.org/core v0.3.3-0.20240902213628-48df10901467 + github.com/Knetic/govaluate v3.0.0+incompatible github.com/aandrew-me/tgpt/v2 v2.7.2 github.com/alecthomas/chroma/v2 v2.13.0 github.com/bogdanfinn/fhttp v0.5.27 diff --git a/go.sum b/go.sum index 1b6cbe58..73174d86 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ cogentcore.org/core v0.3.3-0.20240902213628-48df10901467/go.mod h1:dg3uRsPcd8S1Z github.com/Bios-Marcel/wastebasket v0.0.4-0.20240213135800-f26f1ae0a7c4 h1:6lx9xzJAhdjq0LvVfbITeC3IH9Fzvo1aBahyPu2FuG8= github.com/Bios-Marcel/wastebasket v0.0.4-0.20240213135800-f26f1ae0a7c4/go.mod h1:FChzXi1izqzdPb6BiNZmcZLGyTYiT61iGx9Rxx9GNeI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f6s9Tn1Tt7/WTEg= +github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE= github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8= github.com/aandrew-me/tgpt/v2 v2.7.2 h1:xhcglzpC+A16t40GvEPAwPATFeRY5h4lYpabN6DBK9k= diff --git a/marbles/README.md b/marbles/README.md new file mode 100644 index 00000000..851c9fe1 --- /dev/null +++ b/marbles/README.md @@ -0,0 +1,13 @@ +# Cogent Marbles + +Graph equations and run marbles on them. Inspired by [desmos.com](https://desmos.com). Uses [Cogent Core](https://github.com/cogentcore/core) for graphics, and [Knetic/govaluate](https://github.com/Knetic/govaluate) for evaluating equations. Formerly located at [kkoreilly/marbles](https://github.com/kkoreilly/marbles/). + +## Install + +You can run Cogent Marbles on the web at https://cogentcore.org/cogent/marbles. You can also run it locally: + +```sh +go run cogentcore.org/cogent/marbles@main +``` + +See the [Cogent Core installation instructions](https://www.cogentcore.org/core/setup/install) for more information. diff --git a/marbles/app.go b/marbles/app.go new file mode 100644 index 00000000..e67e4530 --- /dev/null +++ b/marbles/app.go @@ -0,0 +1,99 @@ +package main + +import ( + "cogentcore.org/core/core" + "cogentcore.org/core/events" + "cogentcore.org/core/icons" + "cogentcore.org/core/keymap" + "cogentcore.org/core/math32" + "cogentcore.org/core/styles" + "cogentcore.org/core/tree" +) + +func (gr *Graph) MakeToolbar(p *tree.Plan) { + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(gr.Graph).SetIcon(icons.ShowChart) + }) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(gr.Run).SetIcon(icons.PlayArrow) + }) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(gr.Stop).SetIcon(icons.Stop) + }) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(gr.Step).SetIcon(icons.Step) + }) + + tree.Add(p, func(w *core.Separator) {}) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(gr.AddLine).SetIcon(icons.Add) + }) + + tree.Add(p, func(w *core.Separator) {}) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(gr.SelectNextMarble).SetText("Next marble").SetIcon(icons.ArrowForward) + }) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(gr.StopSelecting).SetText("Unselect").SetIcon(icons.Close) + }) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(gr.TrackSelectedMarble).SetText("Track").SetIcon(icons.PinDrop) + }) + + tree.Add(p, func(w *core.Separator) {}) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(gr.OpenJSON).SetText("Open").SetIcon(icons.Open).SetKey(keymap.Open) + w.Args[0].SetTag(`extension:".json"`) + }) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(gr.SaveLast).SetText("Save").SetIcon(icons.Save).SetKey(keymap.Save) + w.Updater(func() { + w.SetEnabled(gr.State.File != "") + }) + }) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(gr.SaveJSON).SetText("Save as").SetIcon(icons.SaveAs).SetKey(keymap.SaveAs) + w.Args[0].SetTag(`extension:".json"`) + }) + tree.Add(p, func(w *core.FuncButton) { + w.SetFunc(gr.Reset).SetIcon(icons.Reset) + }) +} + +func (gr *Graph) MakeBasicElements(b *core.Body) { + sp := core.NewSplits(b).SetTiles(core.TileSecondLong) + sp.Styler(func(s *styles.Style) { + if sp.SizeClass() == core.SizeExpanded { + s.Direction = styles.Column + } else { + s.Direction = styles.Row + } + }) + + gr.Objects.LinesTable = core.NewTable(sp).SetSlice(&gr.Lines) + gr.Objects.LinesTable.OnChange(func(e events.Event) { + gr.Graph() + }) + + gr.Objects.ParamsForm = core.NewForm(sp).SetStruct(&gr.Params) + gr.Objects.ParamsForm.OnChange(func(e events.Event) { + gr.Graph() + }) + + gr.Objects.Graph = core.NewCanvas(sp).SetDraw(gr.draw) + + gr.Vectors.Min = math32.Vector2{X: -GraphViewBoxSize, Y: -GraphViewBoxSize} + gr.Vectors.Max = math32.Vector2{X: GraphViewBoxSize, Y: GraphViewBoxSize} + gr.Vectors.Size = gr.Vectors.Max.Sub(gr.Vectors.Min) + var n float32 = 1.0 / float32(TheSettings.GraphInc) + gr.Vectors.Inc = math32.Vector2{X: n, Y: n} + + statusText := core.NewText(b) + statusText.Updater(func() { + if gr.State.File == "" { + statusText.SetText("Welcome to Cogent Marbles!") + } else { + statusText.SetText("" + string(gr.State.File) + "") + } + }) +} diff --git a/marbles/core.toml b/marbles/core.toml new file mode 100644 index 00000000..0291d402 --- /dev/null +++ b/marbles/core.toml @@ -0,0 +1 @@ +About = "Cogent Marbles allows you to enter equations, which are graphed, and then marbles are dropped down on the resulting lines, and bounce around in very entertaining ways!" diff --git a/marbles/draw.go b/marbles/draw.go new file mode 100644 index 00000000..5cb5124a --- /dev/null +++ b/marbles/draw.go @@ -0,0 +1,123 @@ +package main + +import ( + "cogentcore.org/core/colors" + "cogentcore.org/core/math32" + "cogentcore.org/core/paint" +) + +// draw renders the graph. +func (gr *Graph) draw(pc *paint.Context) { + TheGraph.EvalMu.Lock() + defer TheGraph.EvalMu.Unlock() + gr.updateCoords() + gr.drawAxes(pc) + gr.drawTrackingLines(pc) + gr.drawLines(pc) + gr.drawMarbles(pc) +} + +func (gr *Graph) updateCoords() { + if !gr.State.Running || gr.Params.CenterX.Changes || gr.Params.CenterY.Changes { + sizeFromCenter := math32.Vector2{X: GraphViewBoxSize, Y: GraphViewBoxSize} + center := math32.Vector2{X: float32(gr.Params.CenterX.Eval(0, 0)), Y: float32(gr.Params.CenterY.Eval(0, 0))} + gr.Vectors.Min = center.Sub(sizeFromCenter) + gr.Vectors.Max = center.Add(sizeFromCenter) + gr.Vectors.Size = sizeFromCenter.MulScalar(2) + } +} + +// canvasCoord converts the given graph coordinate to a normalized 0-1 canvas coordinate. +func (gr *Graph) canvasCoord(v math32.Vector2) math32.Vector2 { + res := math32.Vector2{} + res.X = (v.X - gr.Vectors.Min.X) / (gr.Vectors.Max.X - gr.Vectors.Min.X) + res.Y = (gr.Vectors.Max.Y - v.Y) / (gr.Vectors.Max.Y - gr.Vectors.Min.Y) + return res +} + +func (gr *Graph) drawAxes(pc *paint.Context) { + pc.StrokeStyle.Color = colors.Scheme.OutlineVariant + + start := gr.canvasCoord(math32.Vec2(gr.Vectors.Min.X, 0)) + end := gr.canvasCoord(math32.Vec2(gr.Vectors.Max.X, 0)) + pc.MoveTo(start.X, start.Y) + pc.LineTo(end.X, end.Y) + pc.Stroke() + + start = gr.canvasCoord(math32.Vec2(0, gr.Vectors.Min.Y)) + end = gr.canvasCoord(math32.Vec2(0, gr.Vectors.Max.Y)) + pc.MoveTo(start.X, start.Y) + pc.LineTo(end.X, end.Y) + pc.Stroke() +} + +func (gr *Graph) drawTrackingLines(pc *paint.Context) { + for _, m := range gr.Marbles { + if !m.TrackingInfo.Track { + continue + } + for j, pos := range m.TrackingInfo.History { + cpos := gr.canvasCoord(pos) + if j == 0 { + pc.MoveTo(cpos.X, cpos.Y) + } else { + pc.LineTo(cpos.X, cpos.Y) + } + } + pc.StrokeStyle.Color = colors.Uniform(m.Color) + pc.Stroke() + } +} + +func (gr *Graph) drawLines(pc *paint.Context) { + for _, ln := range gr.Lines { + // TODO: this logic doesn't work + // If the line doesn't change over time then we don't need to keep graphing it while running marbles + // if !ln.Changes && gr.State.Running && !gr.Params.CenterX.Changes && !gr.Params.CenterY.Changes { + // continue + // } + ln.draw(gr, pc) + } +} + +func (ln *Line) draw(gr *Graph, pc *paint.Context) { + start := true + skipped := false + for x := TheGraph.Vectors.Min.X; x < TheGraph.Vectors.Max.X; x += TheGraph.Vectors.Inc.X { + if TheGraph.State.Error != nil { + return + } + fx := float64(x) + y := ln.Expr.Eval(fx, TheGraph.State.Time, ln.TimesHit) + GraphIf := ln.GraphIf.EvalBool(fx, y, TheGraph.State.Time, ln.TimesHit) + if GraphIf && TheGraph.Vectors.Min.Y < float32(y) && TheGraph.Vectors.Max.Y > float32(y) { + coord := gr.canvasCoord(math32.Vec2(x, float32(y))) + if start || skipped { + pc.MoveTo(coord.X, coord.Y) + start, skipped = false, false + } else { + pc.LineTo(coord.X, coord.Y) + } + } else { + skipped = true + } + } + pc.StrokeStyle.Color = colors.Uniform(ln.Colors.Color) + pc.StrokeStyle.Width.Dp(4) + pc.ToDots() + pc.Stroke() +} + +func (gr *Graph) drawMarbles(pc *paint.Context) { + for i, m := range gr.Marbles { + pos := gr.canvasCoord(m.Pos) + if i == gr.State.SelectedMarble { + pc.DrawCircle(pos.X, pos.Y, 0.02) + pc.FillStyle.Color = colors.Scheme.Warn.Container + pc.Fill() + } + pc.DrawCircle(pos.X, pos.Y, 0.005) + pc.FillStyle.Color = colors.Uniform(m.Color) + pc.Fill() + } +} diff --git a/marbles/eqchange.go b/marbles/eqchange.go new file mode 100644 index 00000000..41d6ca68 --- /dev/null +++ b/marbles/eqchange.go @@ -0,0 +1,151 @@ +package main + +import ( + "cmp" + "fmt" + "slices" + "strconv" + "strings" + + "github.com/Knetic/govaluate" +) + +// EquationChange type has the string that needs to be replaced and what to replace it with +type EquationChange struct { + Old string + New string +} + +// UnreadableChangeSlice is all of the strings that should change before compiling, but the user shouldn't see +var UnreadableChangeSlice = []EquationChange{ + {"^", "**"}, + {"√", "sqrt"}, + {"∞", "inf()"}, + {"∫", "int"}, + {"∏", "psum"}, + {"Σ", "sum"}, + {")(", ")*("}, +} + +// EquationChangeSlice is all of the strings that should be changed +var EquationChangeSlice = []EquationChange{ + {"''", `"`}, + {"**", "^"}, + {"sqrt", "√"}, + {"pi", "π"}, + {"inf", "∞"}, + {"int", "∫"}, + {"psum", "∏"}, + {"sum", "Σ"}, + {`\`, ""}, +} + +// ZeroArgFunctions are the functions that do not take any arguments. +var ZeroArgFunctions = []string{"rand", "nmarbles", "inf"} + +// PrepareExpr prepares an expression by looping both equation change slices +func (ex *Expr) PrepareExpr(functionsArg map[string]govaluate.ExpressionFunction) (string, map[string]govaluate.ExpressionFunction) { + functions := make(map[string]govaluate.ExpressionFunction) + for name, function := range functionsArg { + functions[name] = function + } + ex.LoopEquationChangeSlice() + params := []string{"π", "e", "x", "a", "t", "h", "y", "n"} + symbols := []string{"+", "-", "*", "/", "^", ">", "<", "=", "(", ")"} + expr := LoopUnreadableChangeSlice(ex.Expr) + expr = strings.ReplaceAll(expr, "true", "(0==0)") // prevent true and false from being interpreted as functions + expr = strings.ReplaceAll(expr, "false", "(0!=0)") + for _, s := range symbols { + expr = strings.ReplaceAll(expr, s+"-", s+" -") + expr = strings.ReplaceAll(expr, s+".", s+"0.") + } + for _, p := range params { + expr = strings.ReplaceAll(expr, strings.ToUpper(p), p) + } + i := 0 + functionsToDelete := []string{} + functionsToAdd := make(map[string]govaluate.ExpressionFunction) + // sort functions by length so that functions that contain other functions don't cause problems + functionKeys := []string{} + for k := range functions { + functionKeys = append(functionKeys, k) + } + slices.SortFunc(functionKeys, func(a, b string) int { + return cmp.Compare(len(b), len(a)) + }) + isZeroArg := map[string]bool{} + for _, name := range functionKeys { // to prevent issues with the equation, all functions are turned into zfunctionindexz. z is just a letter that isn't used in anything else. + function := functions[name] + newName := fmt.Sprintf("z%vz", i) + expr = strings.ReplaceAll(expr, name, newName) + functionsToAdd[newName] = function + functionsToDelete = append(functionsToDelete, name) + isZeroArg[newName] = slices.Contains(ZeroArgFunctions, name) + i++ + } + for name, function := range functionsToAdd { + functions[name] = function + } + for _, name := range functionsToDelete { + delete(functions, name) + } + for fname := range functions { // if there is a function name and no parentheses after, put parentheses around the next character, or directly after it if it is a zero-arg function + if isZeroArg[fname] { + expr = strings.ReplaceAll(expr, fname, fname+"()") + expr = strings.ReplaceAll(expr, fname+"()()", fname+"()") + } + for _, pname := range params { + expr = strings.ReplaceAll(expr, fname+pname, fname+"("+pname+")") + } + for n := 0; n < 10; n++ { + ns := strconv.Itoa(n) + expr = strings.ReplaceAll(expr, fname+ns, fname+"("+ns+")") + } + } + for n := 0; n < 10; n++ { // if the expression contains a number and then a parameter or a function right after, then change it to multiply the number and the parameter/function. Also ()number changes to ()*number + ns := strconv.Itoa(n) + for _, pname := range params { + expr = strings.ReplaceAll(expr, ns+pname, ns+"*"+pname) + expr = strings.ReplaceAll(expr, ns+"("+pname, ns+"*("+pname) + } + for fname := range functions { + expr = strings.ReplaceAll(expr, ns+fname, ns+"*"+fname) + expr = strings.ReplaceAll(expr, ns+"("+fname, ns+"*("+fname) + } + expr = strings.ReplaceAll(expr, ")"+ns, ")*"+ns) + } + for _, pname := range params { // if the expression contains a parameter before another parameter or a function, make it multiply. Also ()parameter changes to ()*parameter + for _, pname1 := range params { + for strings.Contains(expr, pname+pname1) || strings.Contains(expr, pname+"("+pname1) { + expr = strings.ReplaceAll(expr, pname+pname1, pname+"*"+pname1) + expr = strings.ReplaceAll(expr, pname+"("+pname1, pname+"*("+pname1) + } + } + for fname := range functions { + expr = strings.ReplaceAll(expr, pname+fname, pname+"*"+fname) + expr = strings.ReplaceAll(expr, pname+"("+fname, pname+"*("+fname) + } + expr = strings.ReplaceAll(expr, ")"+pname, ")*"+pname) + expr = strings.ReplaceAll(expr, pname+"(", pname+"*(") + } + for fname := range functions { // replace ()fname() with ()*fname() + expr = strings.ReplaceAll(expr, ")"+fname, ")*"+fname) + } + + return expr, functions +} + +// LoopEquationChangeSlice loops over the Equation Change slice and makes the replacements +func (ex *Expr) LoopEquationChangeSlice() { + for _, d := range EquationChangeSlice { + ex.Expr = strings.ReplaceAll(ex.Expr, d.Old, d.New) + } +} + +// LoopUnreadableChangeSlice loops over the unreadable Change slice and makes the replacements +func LoopUnreadableChangeSlice(expr string) string { + for _, d := range UnreadableChangeSlice { + expr = strings.ReplaceAll(expr, d.Old, d.New) + } + return expr +} diff --git a/marbles/expr.go b/marbles/expr.go new file mode 100644 index 00000000..07f560f7 --- /dev/null +++ b/marbles/expr.go @@ -0,0 +1,114 @@ +package main + +import ( + "fmt" + "math" + + "github.com/Knetic/govaluate" + "gonum.org/v1/gonum/integrate" +) + +// Expr is an expression +type Expr struct { + // Equation: use x for the x value, t for the time passed since the marbles were ran (incremented by TimeStep), and a for 10*sin(t) (swinging back and forth version of t) + Expr string `width:"30" label:""` + + Val *govaluate.EvaluableExpression `display:"-" json:"-"` + + Params map[string]any `display:"-" json:"-"` +} + +// Integrate returns the integral of an expression +func (ex *Expr) Integrate(min, max float64, h int) float64 { + var vals []float64 + sign := float64(1) + diff := max - min + if diff == 0 { + return 0 + } + if diff < 0 { + diff = -diff + sign = -1 + min, max = max, min + } + accuracy := 16 + dx := diff / float64(accuracy) + for x := min; x <= max; x += dx { + vals = append(vals, ex.Eval(x, TheGraph.State.Time, h)) + } + if len(vals) != accuracy+1 { + vals = append(vals, ex.Eval(max, TheGraph.State.Time, h)) + } + val := integrate.Romberg(vals, dx) + return sign * val +} + +// Compile gets an expression ready for evaluation. +func (ex *Expr) Compile() error { + expr, functions := ex.PrepareExpr(TheGraph.Functions) + var err error + ex.Val, err = govaluate.NewEvaluableExpressionWithFunctions(expr, functions) + if HandleError(err) { + ex.Val = nil + return err + } + if ex.Params == nil { + ex.Params = make(map[string]any, 2) + } + ex.Params["π"] = math.Pi + ex.Params["e"] = math.E + return err +} + +// Eval corees the y value of the function for given x, t and h value +func (ex *Expr) Eval(x, t float64, h int) float64 { + if ex.Expr == "" { + return 0 + } + ex.Params["x"] = x + ex.Params["t"] = t + ex.Params["a"] = 10 * math.Sin(t) + ex.Params["h"] = h + yi, err := ex.Val.Evaluate(ex.Params) + if HandleError(err) { + return 0 + } + switch y := yi.(type) { + case float64: + return y + default: + TheGraph.Stop() + HandleError(fmt.Errorf("expression %v is invalid, it is a %T value, should be a float64 value", ex.Expr, yi)) + return 0 + } +} + +// EvalWithY calls eval but with a y value set +func (ex *Expr) EvalWithY(x, t float64, h int, y float64) float64 { + ex.Params["y"] = y + return ex.Eval(x, t, h) +} + +// EvalBool checks if a statement is true based on the x, y, t and h values +func (ex *Expr) EvalBool(x, y, t float64, h int) bool { + if ex.Expr == "" { + return true + } + ex.Params["x"] = x + ex.Params["t"] = t + ex.Params["a"] = 10 * math.Sin(t) + ex.Params["h"] = h + ex.Params["y"] = y + ri, err := ex.Val.Evaluate(ex.Params) + if HandleError(err) { + return true + } + switch r := ri.(type) { + case bool: + return r + default: + TheGraph.Stop() + HandleError(fmt.Errorf("expression %v is invalid, it is a %T value, should be a bool value", ex.Expr, ri)) + return false + } +} diff --git a/marbles/functions.go b/marbles/functions.go new file mode 100644 index 00000000..a33dd70b --- /dev/null +++ b/marbles/functions.go @@ -0,0 +1,339 @@ +package main + +import ( + "fmt" + "math" + "math/rand" + "strings" + + "github.com/Knetic/govaluate" + "gonum.org/v1/gonum/diff/fd" +) + +// Functions are a map of named expression functions +type Functions map[string]govaluate.ExpressionFunction + +// NewFuncV makes a function that can be used in expressions from a function that takes a variadic input and returns a single value. +func NewFuncV[I, O any](f func(...I) O) govaluate.ExpressionFunction { + return func(args ...any) (any, error) { + newArgs := []I{} + for i, arg := range args { + a, ok := arg.(I) + if !ok { + return nil, fmt.Errorf("evaluation error: function of type %T does not accept input type %T for argument %v", f, arg, i) + } + newArgs = append(newArgs, a) + } + res := f(newArgs...) + return res, nil + } +} + +// NewFunc0 makes a function that can be used in expressions from a function that takes no arguments and returns a single value. +// IMPORTANT: zero arg functions must be added to [ZeroArgFunctions]. +func NewFunc0[O any](f func() O) govaluate.ExpressionFunction { + return func(args ...any) (any, error) { + if len(args) != 0 { + return nil, fmt.Errorf("evaluation error: function of type %T wants 0 arguments, not %v arguments", f, len(args)) + } + res := f() + return res, nil + } +} + +// NewFunc1 makes a function that can be used in expressions from a function that takes a single argument and returns a single value. +func NewFunc1[I, O any](f func(I) O) govaluate.ExpressionFunction { + return func(args ...any) (any, error) { + if len(args) != 1 { + return nil, fmt.Errorf("evaluation error: function of type %T wants 1 argument, not %v arguments", f, len(args)) + } + arg0, ok := args[0].(I) + if !ok { + return nil, fmt.Errorf("evaluation error: function of type %T does not accept input type %T", f, args[0]) + } + res := f(arg0) + return res, nil + } +} + +// NewFunc2 makes a function that can be used in expressions from a function that takes two arguments and returns a single value. +func NewFunc2[I1, I2, O any](f func(I1, I2) O) govaluate.ExpressionFunction { + return func(args ...any) (any, error) { + if len(args) != 2 { + return nil, fmt.Errorf("evaluation error: function of type %T wants 2 arguments, not %v arguments", f, len(args)) + } + arg0, ok := args[0].(I1) + if !ok { + return nil, fmt.Errorf("evaluation error: function of type %T does not accept input type %T for argument 0", f, args[0]) + } + arg1, ok := args[1].(I2) + if !ok { + return nil, fmt.Errorf("evaluation error: function of type %T does not accept input type %T for argument 1", f, args[1]) + } + res := f(arg0, arg1) + return res, nil + } +} + +// NewFunc3 makes a function that can be used in expressions from a function that takes three arguments and returns a single value. +func NewFunc3[I1, I2, I3, O any](f func(I1, I2, I3) O) govaluate.ExpressionFunction { + return func(args ...any) (any, error) { + if len(args) != 3 { + return nil, fmt.Errorf("evaluation error: function of type %T wants 3 arguments, not %v arguments", f, len(args)) + } + arg0, ok := args[0].(I1) + if !ok { + return nil, fmt.Errorf("evaluation error: function of type %T does not accept input type %T for argument 0", f, args[0]) + } + arg1, ok := args[1].(I2) + if !ok { + return nil, fmt.Errorf("evaluation error: function of type %T does not accept input type %T for argument 1", f, args[1]) + } + arg2, ok := args[2].(I3) + if !ok { + return nil, fmt.Errorf("evaluation error: function of type %T does not accept input type %T for argument 2", f, args[2]) + } + res := f(arg0, arg1, arg2) + return res, nil + } +} + +// DefaultFunctions are the default functions that can be used in expressions +var DefaultFunctions = Functions{ + "sin": NewFunc1(math.Sin), + "cos": NewFunc1(math.Cos), + "tan": NewFunc1(math.Tan), + "sec": NewFunc1(func(x float64) float64 { + return 1 / math.Cos(x) + }), + "csc": NewFunc1(func(x float64) float64 { + return 1 / math.Sin(x) + }), + "cot": NewFunc1(func(x float64) float64 { + return 1 / math.Tan(x) + }), + "arcsin": NewFunc1(math.Asin), + "arccos": NewFunc1(math.Acos), + "arctan": NewFunc1(math.Atan), + "arcsec": NewFunc1(func(x float64) float64 { + return math.Acos(1 / x) + }), + "arccsc": NewFunc1(func(x float64) float64 { + return math.Asin(1 / x) + }), + "arccot": NewFunc1(func(x float64) float64 { + y := math.Atan(1 / x) + if x < 0 { + y += math.Pi + } + return y + }), + "sinh": NewFunc1(math.Sinh), + "cosh": NewFunc1(math.Cosh), + "tanh": NewFunc1(math.Tanh), + "sech": NewFunc1(func(x float64) float64 { + return 1 / math.Cosh(x) + }), + "csch": NewFunc1(func(x float64) float64 { + return 1 / math.Sinh(x) + }), + "coth": NewFunc1(func(x float64) float64 { + return 1 / math.Tanh(x) + }), + "arcsinh": NewFunc1(math.Asinh), + "arccosh": NewFunc1(math.Acosh), + "arctanh": NewFunc1(math.Atanh), + "arcsech": NewFunc1(func(x float64) float64 { + return math.Acosh(1 / x) + }), + "arccsch": NewFunc1(func(x float64) float64 { + return math.Asinh(1 / x) + }), + "arccoth": NewFunc1(func(x float64) float64 { + return math.Atanh(1 / x) + }), + "ln": NewFunc1(math.Log), + "log": NewFunc2(func(x, base float64) float64 { + return math.Log(x) / math.Log(base) + }), + "abs": NewFunc1(math.Abs), + "pow": NewFunc2(math.Pow), + "exp": NewFunc1(math.Exp), + "mod": NewFunc2(math.Mod), + "fact": NewFunc1(func(x float64) float64 { + return math.Gamma(x + 1) + }), + "floor": NewFunc1(math.Floor), + "ceil": NewFunc1(math.Ceil), + "round": NewFunc1(math.Round), + "sqrt": NewFunc1(math.Sqrt), + "cbrt": NewFunc1(math.Cbrt), + "min": NewFuncV(func(v ...float64) any { + if len(v) == 0 { + return 0 + } + min := v[0] + for i := 1; i < len(v); i++ { + x := v[i] + if x < min { + min = x + } + } + return min + }), + "max": NewFuncV(func(v ...float64) any { + if len(v) == 0 { + return 0 + } + max := v[0] + for i := 1; i < len(v); i++ { + x := v[i] + if x > max { + max = x + } + } + return max + }), + "avg": NewFuncV(func(v ...float64) any { + if len(v) == 0 { + return 0 + } + var total float64 + for _, x := range v { + total += x + } + return total / float64(len(v)) + }), + "if": NewFunc3(func(condition bool, val1, val2 any) any { + if condition { + return val1 + } + return val2 + }), + // IMPORTANT: zero arg functions must be added to [ZeroArgFunctions]. + "rand": NewFunc0(rand.Float64), + "nmarbles": NewFunc0(func() float64 { + return float64(TheGraph.Params.NMarbles) + }), + "inf": NewFunc0(func() float64 { + return math.Inf(1) + }), +} + +// CheckArgs checks if a function is passed the right number of arguments, and the right type of arguments. +func CheckArgs(name string, have []any, want ...string) error { + if len(have) != len(want) { + return fmt.Errorf("function %v needs %v arguments, not %v arguments", name, len(want), len(have)) + } + for i, d := range want { + if d != fmt.Sprintf("%T", have[i]) { + return fmt.Errorf("function %v needs %v. %v does not work", name, want, have) + } + } + return nil +} + +// SetFunctionsTo sets the functions of the graph to another set of functions +func (gr *Graph) SetFunctionsTo(functions Functions) { + gr.Functions = make(Functions) + for k, d := range functions { + gr.Functions[k] = d + } +} + +// AddLineFunctions adds all of the line functions +func (gr *Graph) AddLineFunctions() { + for k, ln := range gr.Lines { + ln.SetFunctionName(k) + } +} + +// SetFunctionName sets the function name for a line and adds the function to the functions +func (ln *Line) SetFunctionName(k int) { + if k >= len(FunctionNames) { + // ln.FuncName = "unassigned" + return + } + functionName := FunctionNames[k] + // ln.FuncName = functionName + "(x)=" + TheGraph.Functions[functionName] = func(args ...any) (any, error) { + err := CheckArgs(functionName, args, "float64") + if err != nil { + return 0, err + } + val := float64(ln.Expr.Eval(args[0].(float64), TheGraph.State.Time, ln.TimesHit)) + return val, nil + } + TheGraph.Functions[functionName+"'"] = func(args ...any) (any, error) { + err := CheckArgs(functionName+"'", args, "float64") + if err != nil { + return 0, err + } + val := fd.Derivative(func(x float64) float64 { + return ln.Expr.Eval(x, TheGraph.State.Time, ln.TimesHit) + }, args[0].(float64), &fd.Settings{ + Formula: fd.Central, + }) + return val, nil + } + TheGraph.Functions[functionName+`"`] = func(args ...any) (any, error) { + err := CheckArgs(functionName+`"`, args, "float64") + if err != nil { + return 0, err + } + val := fd.Derivative(func(x float64) float64 { + return ln.Expr.Eval(x, TheGraph.State.Time, ln.TimesHit) + }, args[0].(float64), &fd.Settings{ + Formula: fd.Central2nd, + }) + return val, nil + } + capitalName := strings.ToUpper(functionName) + TheGraph.Functions[capitalName] = func(args ...any) (any, error) { + err := CheckArgs(capitalName, args, "float64") + if err != nil { + return 0, err + } + val := ln.Expr.Integrate(0, args[0].(float64), ln.TimesHit) + return val, nil + } + TheGraph.Functions[functionName+"int"] = func(args ...any) (any, error) { + err := CheckArgs(functionName+"int", args, "float64", "float64") + if err != nil { + return 0, err + } + min := args[0].(float64) + max := args[1].(float64) + val := ln.Expr.Integrate(min, max, ln.TimesHit) + return val, nil + } + TheGraph.Functions[functionName+"h"] = func(args ...any) (any, error) { + err := CheckArgs(functionName+"h", args, "float64") + if err != nil { + return 0, err + } + return float64(ln.TimesHit) * args[0].(float64), nil + } + TheGraph.Functions[functionName+"sum"] = func(args ...any) (any, error) { + err := CheckArgs(functionName+"sum", args, "float64", "float64") + if err != nil { + return 0, err + } + total := 0.0 + for i := args[0].(float64); i <= args[1].(float64); i++ { + total += (ln.Expr.Eval(i, TheGraph.State.Time, ln.TimesHit)) + } + return total, nil + } + TheGraph.Functions[functionName+"psum"] = func(args ...any) (any, error) { + err := CheckArgs(functionName+"psum", args, "float64", "float64") + if err != nil { + return 0, err + } + total := 1.0 + for i := args[0].(float64); i <= args[1].(float64); i++ { + total *= (ln.Expr.Eval(i, TheGraph.State.Time, ln.TimesHit)) + } + return total, nil + } +} diff --git a/marbles/graph.go b/marbles/graph.go new file mode 100644 index 00000000..d0239ab9 --- /dev/null +++ b/marbles/graph.go @@ -0,0 +1,486 @@ +// Copyright (c) 2020, Kai O'Reilly. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "errors" + "image/color" + "sort" + "strings" + "sync" + "unicode" + + "cogentcore.org/core/colors" + "cogentcore.org/core/core" + "cogentcore.org/core/math32" + "cogentcore.org/core/parse/complete" +) + +// Graph contains the lines and parameters of a graph +type Graph struct { //types:add + + // the parameters for updating the marbles + Params Params + + // the lines of the graph -- can have any number + Lines Lines + + Marbles []*Marble `json:"-"` + + State State `json:"-"` + + Functions Functions `json:"-"` + + Vectors Vectors `json:"-"` + + Objects Objects `json:"-"` + + EvalMu sync.Mutex `json:"-"` +} + +// State has the state of the graph +type State struct { + Running bool + Time float64 + PrevTime float64 + Step int + Error error + SelectedMarble int + File core.Filename +} + +// Line represents one line with an equation etc +type Line struct { + + // Equation: use x for the x value, t for the time passed since the marbles were ran (incremented by TimeStep), and a for 10*sin(t) (swinging back and forth version of t) + Expr Expr + + // Graph this line if this condition is true. Ex: x>3 + GraphIf Expr + + // how bouncy the line is -- 1 = perfectly bouncy, 0 = no bounce at all + Bounce Expr `min:"0" max:"2" step:".05"` + + // Line color and colorswitch + Colors LineColors + + TimesHit int `display:"-" json:"-"` + + Changes bool `display:"-" json:"-"` +} + +// Params are the parameters of the graph +type Params struct { //types:add + + // Number of marbles + NMarbles int `min:"1" max:"10000" step:"10" label:"Number of marbles"` + + // Marble horizontal start position + MarbleStartX Expr + + // Marble vertical start position + MarbleStartY Expr + + // Starting horizontal velocity of the marbles + StartVelocityY Param `view:"inline" label:"Starting velocity y"` + + // Starting vertical velocity of the marbles + StartVelocityX Param `view:"inline" label:"Starting velocity x"` + + // how fast to move along velocity vector -- lower = smoother, more slow-mo + UpdateRate Param `view:"inline"` + + // how fast time increases + TimeStep Param `view:"inline"` + + // how fast it accelerates down + YForce Param `view:"inline" label:"Y force (Gravity)"` + + // how fast the marbles move side to side without collisions, set to 0 for no movement + XForce Param `view:"inline" label:"X force (Wind)"` + + // the center point of the graph, x + CenterX Param `view:"inline" label:"Graph center x"` + + // the center point of the graph, y + CenterY Param `view:"inline" label:"Graph center y"` + + TrackingSettings TrackingSettings +} + +// Param is the type of certain parameters that can change over time and x +type Param struct { + Expr Expr `label:""` + + Changes bool `display:"-" json:"-"` + + BaseVal float64 `display:"-" json:"-"` +} + +// LineColors contains the color and colorswitch for a line +type LineColors struct { + + // color to draw the line in + Color color.RGBA + + // Switch the color of the marble that hits this line + ColorSwitch color.RGBA +} + +// Vectors contains the size and increment of the graph +type Vectors struct { + Min math32.Vector2 + Max math32.Vector2 + Size math32.Vector2 + Inc math32.Vector2 +} + +// Objects contains the svg graph and the svg groups, plus the axes +type Objects struct { + Body *core.Body + Graph *core.Canvas + + LinesTable *core.Table + ParamsForm *core.Form +} + +// Lines is a collection of lines +type Lines []*Line + +const GraphViewBoxSize = 10 + +var BasicFunctionList = []string{} + +var CompleteWords = []string{} + +// FunctionNames has all of the supported function names, in order +var FunctionNames = []string{"f", "g", "b", "c", "j", "k", "l", "m", "o", "p", "q", "r", "s", "u", "v", "w"} + +// TheGraph is current graph +var TheGraph Graph + +// Init sets up the graph for the given body. It should only be called once. +func (gr *Graph) Init(b *core.Body) { + gr.Objects.Body = b + gr.Defaults() + gr.MakeBasicElements(b) + gr.SetFunctionsTo(DefaultFunctions) + gr.CompileExprs() + gr.ResetMarbles() +} + +// Defaults sets the default parameters and lines for the graph, specified in settings +func (gr *Graph) Defaults() { + gr.Params.Defaults() + gr.Lines.Defaults() +} + +// Graph updates graph for current equations, and resets marbles too +func (gr *Graph) Graph() { //types:add + defer gr.Objects.Graph.NeedsRender() + + if gr.State.Running { + gr.Stop() + } + gr.State.Error = nil + gr.SetFunctionsTo(DefaultFunctions) + gr.AddLineFunctions() + gr.CompileExprs() + if gr.State.Error != nil { + return + } + gr.ResetMarbles() + gr.State.Time = 0 + if gr.State.Error != nil { + return + } + SetCompleteWords(TheGraph.Functions) + // if gr.State.Error == nil { + // errorText.SetText("Graphed successfully") + // } +} + +func (gr *Graph) graphAndUpdate() { + gr.Graph() + gr.Objects.Body.Scene.Update() +} + +// Run runs the marbles for NSteps +func (gr *Graph) Run() { //types:add + gr.AutoSave() + go gr.RunMarbles() +} + +// Stop stops the marbles +func (gr *Graph) Stop() { //types:add + gr.State.Running = false +} + +// Step does one step update of marbles +func (gr *Graph) Step() { //types:add + if gr.State.Running { + return + } + gr.UpdateMarbles() + gr.State.Time += gr.Params.TimeStep.Eval(0, 0) +} + +// StopSelecting stops selecting current marble +func (gr *Graph) StopSelecting() { //types:add + gr.State.SelectedMarble = -1 + if !gr.State.Running { + gr.Objects.Graph.NeedsRender() + } +} + +// TrackSelectedMarble toggles track for the currently selected marble +func (gr *Graph) TrackSelectedMarble() { //types:add + if gr.State.SelectedMarble == -1 { + return + } + gr.Marbles[gr.State.SelectedMarble].ToggleTrack(gr.State.SelectedMarble) +} + +// AddLine adds a new blank line +func (gr *Graph) AddLine() { //types:add + var color color.RGBA + if TheSettings.LineDefaults.LineColors.Color == colors.White { + color = colors.Spaced(len(gr.Lines) - 1) + } else { + color = TheSettings.LineDefaults.LineColors.Color + } + newLine := &Line{Colors: LineColors{color, TheSettings.LineDefaults.LineColors.ColorSwitch}} + gr.Lines = append(gr.Lines, newLine) + gr.Objects.LinesTable.Update() +} + +// Reset resets the graph to its starting position (one default line and default params) +func (gr *Graph) Reset() { //types:add + gr.State.File = "" + gr.Lines = nil + gr.Lines.Defaults() + gr.Params.Defaults() + gr.graphAndUpdate() +} + +// CompileExprs gets the lines of the graph ready for graphing +func (gr *Graph) CompileExprs() { + for k, ln := range gr.Lines { + ln.Changes = false + if ln.Expr.Expr == "" { + ln.Expr.Expr = TheSettings.LineDefaults.Expr + } + if colors.IsNil(ln.Colors.Color) { + if TheSettings.LineDefaults.LineColors.Color == colors.White { + ln.Colors.Color = colors.Spaced(k) + } else { + ln.Colors.Color = TheSettings.LineDefaults.LineColors.Color + } + } + if colors.IsNil(ln.Colors.ColorSwitch) { + ln.Colors.ColorSwitch = TheSettings.LineDefaults.LineColors.ColorSwitch + } + if ln.Bounce.Expr == "" { + ln.Bounce.Expr = TheSettings.LineDefaults.Bounce + } + if ln.GraphIf.Expr == "" { + ln.GraphIf.Expr = TheSettings.LineDefaults.GraphIf + } + if CheckCircular(ln.Expr.Expr, k) { + HandleError(errors.New("circular logic detected")) + return + } + if CheckIfChanges(ln.Expr.Expr) || CheckIfChanges(ln.GraphIf.Expr) || CheckIfChanges(ln.Bounce.Expr) { + ln.Changes = true + } + ln.TimesHit = 0 + ln.Compile() + } + gr.CompileParams() +} + +// CompileParams compiles all of the graph parameter expressions +func (gr *Graph) CompileParams() { + gr.Params.StartVelocityY.Compile() + gr.Params.StartVelocityX.Compile() + gr.Params.UpdateRate.Compile() + gr.Params.YForce.Compile() + gr.Params.XForce.Compile() + gr.Params.TimeStep.Compile() + gr.Params.CenterX.Compile() + gr.Params.CenterY.Compile() +} + +// CheckCircular checks if an expr references itself +func CheckCircular(expr string, k int) bool { + if CheckIfReferences(expr, k) { + return true + } + for i := range FunctionNames { + if CheckIfReferences(expr, i) { + return CheckCircular(TheGraph.Lines[i].Expr.Expr, k) + } + } + return false +} + +// CheckIfReferences checks if an expr references a given function +func CheckIfReferences(expr string, k int) bool { + sort.Slice(BasicFunctionList, func(i, j int) bool { + return len(BasicFunctionList[i]) > len(BasicFunctionList[j]) + }) + for _, d := range BasicFunctionList { + expr = strings.ReplaceAll(expr, d, "") + } + if k >= len(FunctionNames) || k >= len(TheGraph.Lines) { + return false + } + funcName := FunctionNames[k] + if strings.Contains(expr, funcName) || strings.Contains(expr, strings.ToUpper(funcName)) { + return true + } + return false +} + +// CheckIfChanges checks if an equation changes over time +func CheckIfChanges(expr string) bool { + for _, d := range BasicFunctionList { + expr = strings.ReplaceAll(expr, d, "") + } + if strings.Contains(expr, "a") || strings.Contains(expr, "h") || strings.Contains(expr, "t") { + return true + } + for k := range FunctionNames { + if CheckIfReferences(expr, k) { + return CheckIfChanges(TheGraph.Lines[k].Expr.Expr) + } + } + return false +} + +// InitBasicFunctionList adds all of the basic functions to a list +func InitBasicFunctionList() { + for k := range DefaultFunctions { + BasicFunctionList = append(BasicFunctionList, k) + } + BasicFunctionList = append(BasicFunctionList, "true", "false") +} + +// Compile compiles all of the expressions in a line +func (ln *Line) Compile() { + ln.Expr.Compile() + ln.Bounce.Compile() + ln.GraphIf.Compile() +} + +// Defaults sets the line to the defaults specified in settings +func (ln *Line) Defaults(lidx int) { + ln.Expr.Expr = TheSettings.LineDefaults.Expr + if TheSettings.LineDefaults.LineColors.Color == colors.White { + ln.Colors.Color = colors.Spaced(lidx) + } else { + ln.Colors.Color = TheSettings.LineDefaults.LineColors.Color + } + ln.Bounce.Expr = TheSettings.LineDefaults.Bounce + ln.GraphIf.Expr = TheSettings.LineDefaults.GraphIf + ln.Colors.ColorSwitch = TheSettings.LineDefaults.LineColors.ColorSwitch +} + +// Defaults makes the lines and then defaults them +func (ls *Lines) Defaults() { + *ls = make(Lines, 1, 10) + ln := Line{} + (*ls)[0] = &ln + ln.Defaults(0) + +} + +// Defaults sets the graph parameters to the default settings +func (pr *Params) Defaults() { + pr.NMarbles = TheSettings.GraphDefaults.NMarbles + pr.MarbleStartX = TheSettings.GraphDefaults.MarbleStartX + pr.MarbleStartY = TheSettings.GraphDefaults.MarbleStartY + pr.StartVelocityY = TheSettings.GraphDefaults.StartVelocityY + pr.StartVelocityX = TheSettings.GraphDefaults.StartVelocityX + pr.UpdateRate = TheSettings.GraphDefaults.UpdateRate + pr.YForce = TheSettings.GraphDefaults.YForce + pr.XForce = TheSettings.GraphDefaults.XForce + pr.TimeStep = TheSettings.GraphDefaults.TimeStep + pr.CenterX = TheSettings.GraphDefaults.CenterX + pr.CenterY = TheSettings.GraphDefaults.CenterY + pr.TrackingSettings = TheSettings.GraphDefaults.TrackingSettings +} + +// BasicDefaults sets the default defaults for the graph parameters +func (pr *Params) BasicDefaults() { + pr.NMarbles = 100 + pr.MarbleStartX.Expr = "10(rand-0.5)" + pr.MarbleStartY.Expr = "10-2n/nmarbles" + pr.StartVelocityY.Expr.Expr = "0" + pr.StartVelocityX.Expr.Expr = "0" + pr.UpdateRate.Expr.Expr = ".02" + pr.TimeStep.Expr.Expr = "0.01" + pr.YForce.Expr.Expr = "-0.1" + pr.XForce.Expr.Expr = "0" + pr.CenterX.Expr.Expr = "0" + pr.CenterY.Expr.Expr = "0" + pr.TrackingSettings.Defaults() +} + +// Eval evaluates a parameter +func (pr *Param) Eval(x, y float64) float64 { + if !pr.Changes { + return pr.BaseVal + } + return pr.Expr.EvalWithY(x, TheGraph.State.Time, 0, y) +} + +// Compile compiles evalexpr and sets changes +func (pr *Param) Compile() { + pr.Expr.Compile() + expr := pr.Expr.Expr + for _, d := range BasicFunctionList { + expr = strings.ReplaceAll(expr, d, "") + } + if CheckIfChanges(expr) || strings.Contains(expr, "x") || strings.Contains(expr, "y") { + pr.Changes = true + } else { + pr.BaseVal = pr.Expr.Eval(0, 0, 0) + } +} + +// ExprComplete finds the possible completions for the expr in text field +func ExprComplete(data any, text string, posLn, posCh int) (md complete.Matches) { + seedStart := 0 + for i := len(text) - 1; i >= 0; i-- { + r := rune(text[i]) + if !unicode.IsLetter(r) || r == []rune("x")[0] || r == []rune("X")[0] { + seedStart = i + 1 + break + } + } + md.Seed = text[seedStart:] + possibles := complete.MatchSeedString(CompleteWords, md.Seed) + for _, p := range possibles { + m := complete.Completion{Text: p, Icon: ""} + md.Matches = append(md.Matches, m) + } + return md +} + +// ExprCompleteEdit is the editing function called when using complete +func ExprCompleteEdit(data any, text string, cursorPos int, completion complete.Completion, seed string) (ed complete.Edit) { + ed = complete.EditWord(text, cursorPos, completion.Text, seed) + return ed +} + +// SetCompleteWords sets the words used for complete in the expressions +func SetCompleteWords(functions Functions) { + CompleteWords = []string{} + for k := range functions { + CompleteWords = append(CompleteWords, k) + } + CompleteWords = append(CompleteWords, "true", "false", "pi", "a", "t") +} diff --git a/marbles/graphs/center.json b/marbles/graphs/center.json new file mode 100644 index 00000000..757f5ac9 --- /dev/null +++ b/marbles/graphs/center.json @@ -0,0 +1 @@ +{"Params":{"NMarbles":100,"MarbleStartX":{"Expr":"20(rand-0.5)"},"MarbleStartY":{"Expr":"if(rand\u003c0.5, rand+9, rand-10)"},"StartVelocityY":{"Expr":{"Expr":"0"}},"StartVelocityX":{"Expr":{"Expr":"0"}},"UpdateRate":{"Expr":{"Expr":".02"}},"TimeStep":{"Expr":{"Expr":"0.01"}},"YForce":{"Expr":{"Expr":"0.1if(y\u003e0, -rand, rand)"}},"XForce":{"Expr":{"Expr":"0.1if(x\u003e0, -rand, rand)"}},"CenterX":{"Expr":{"Expr":"0"}},"CenterY":{"Expr":{"Expr":"0"}},"TrackingSettings":{"TrackByDefault":false,"NTrackingFrames":300,"Accuracy":20,"LineColor":{"R":255,"G":255,"B":255,"A":255}}},"Lines":[{"Expr":{"Expr":"√(7^2-x^2)"},"GraphIf":{"Expr":"abs(x-a)\u003e1"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":255,"G":113,"B":100,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"-√(7^2-x^2)"},"GraphIf":{"Expr":"abs(x-a)\u003e1"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":255,"G":113,"B":100,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"√(5^2-x^2)"},"GraphIf":{"Expr":"abs(x+a)\u003e1"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":0,"G":135,"B":228,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"-√(5^2-x^2)"},"GraphIf":{"Expr":"abs(x+a)\u003e1"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":0,"G":135,"B":228,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"2"},"GraphIf":{"Expr":"abs(x)\u003c4"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":238,"G":130,"B":238,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"-2"},"GraphIf":{"Expr":"abs(x)\u003c4"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":238,"G":130,"B":238,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"-10(x-4)"},"GraphIf":{"Expr":"abs(y)\u003c1.5"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":238,"G":130,"B":238,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"10(x+4)"},"GraphIf":{"Expr":"abs(y)\u003c1.5"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":238,"G":130,"B":238,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"sin(2x)"},"GraphIf":{"Expr":"abs(x)\u003c3-h/40"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":255,"G":0,"B":0,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"-sin(2x)"},"GraphIf":{"Expr":"abs(x)\u003c3-h/40"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":255,"G":0,"B":0,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}}]} diff --git a/marbles/graphs/maze.json b/marbles/graphs/maze.json new file mode 100644 index 00000000..8d8afe6b --- /dev/null +++ b/marbles/graphs/maze.json @@ -0,0 +1 @@ +{"Params":{"NMarbles":100,"MarbleStartX":{"Expr":"10(rand-0.5)"},"MarbleStartY":{"Expr":"10-2n/nmarbles"},"StartVelocityY":{"Expr":{"Expr":"0"}},"StartVelocityX":{"Expr":{"Expr":"0"}},"UpdateRate":{"Expr":{"Expr":".02"}},"TimeStep":{"Expr":{"Expr":"0.01"}},"YForce":{"Expr":{"Expr":"-0.1"}},"XForce":{"Expr":{"Expr":"0"}},"CenterX":{"Expr":{"Expr":"0"}},"CenterY":{"Expr":{"Expr":"0"}},"TrackingSettings":{"TrackByDefault":false,"NTrackingFrames":300,"Accuracy":20,"LineColor":{"R":255,"G":255,"B":255,"A":255}}},"Lines":[{"Expr":{"Expr":"-9(x+9)"},"GraphIf":{"Expr":"true"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":255,"G":113,"B":100,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"9(x-9)"},"GraphIf":{"Expr":"true"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":255,"G":113,"B":100,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"-ax/100+7"},"GraphIf":{"Expr":"x\u003c8"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":0,"G":135,"B":228,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"sinx+4"},"GraphIf":{"Expr":"abs(x+a)%2\u003e0.5"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":0,"G":182,"B":77,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"(x-a)^2/30"},"GraphIf":{"Expr":"y\u003c2.5"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":188,"G":174,"B":0,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"-1"},"GraphIf":{"Expr":"x\u003ca+10"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":255,"G":90,"B":226,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"-(x^2)/20-2"},"GraphIf":{"Expr":"abs(x)\u003c8"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":0,"G":174,"B":193,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"-absx/2-3"},"GraphIf":{"Expr":"abs(x)\u003e0.5"},"Bounce":{"Expr":"1.05"},"Colors":{"Color":{"R":253,"G":143,"B":0,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"x^2/2-9"},"GraphIf":{"Expr":"abs(x)\u003c1"},"Bounce":{"Expr":"0.05"},"Colors":{"Color":{"R":0,"G":128,"B":0,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}}]} diff --git a/marbles/graphs/platform.json b/marbles/graphs/platform.json new file mode 100644 index 00000000..ef75bf4b --- /dev/null +++ b/marbles/graphs/platform.json @@ -0,0 +1 @@ +{"Params":{"NMarbles":100,"MarbleStartX":{"Expr":"10(rand-0.5)"},"MarbleStartY":{"Expr":"10-2n/nmarbles"},"StartVelocityY":{"Expr":{"Expr":"0"}},"StartVelocityX":{"Expr":{"Expr":"0"}},"UpdateRate":{"Expr":{"Expr":".02"}},"TimeStep":{"Expr":{"Expr":"0.01"}},"YForce":{"Expr":{"Expr":"-0.1"}},"XForce":{"Expr":{"Expr":"-x/50"}},"CenterX":{"Expr":{"Expr":"0"}},"CenterY":{"Expr":{"Expr":"0"}},"TrackingSettings":{"TrackByDefault":false,"NTrackingFrames":300,"Accuracy":20,"LineColor":{"R":255,"G":255,"B":255,"A":255}}},"Lines":[{"Expr":{"Expr":"7"},"GraphIf":{"Expr":"x\u003c0\u0026\u0026t\u003c1"},"Bounce":{"Expr":"-0.95"},"Colors":{"Color":{"R":255,"G":0,"B":0,"A":255},"ColorSwitch":{"R":255,"G":0,"B":0,"A":255}}},{"Expr":{"Expr":"7"},"GraphIf":{"Expr":"x\u003e=0\u0026\u0026t\u003c1"},"Bounce":{"Expr":"-0.95"},"Colors":{"Color":{"R":0,"G":0,"B":255,"A":255},"ColorSwitch":{"R":0,"G":0,"B":255,"A":255}}},{"Expr":{"Expr":"x^2/-40+2"},"GraphIf":{"Expr":"absx\u003c7"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":0,"G":128,"B":0,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}}]} diff --git a/marbles/graphs/tug-of-war.json b/marbles/graphs/tug-of-war.json new file mode 100644 index 00000000..7024c021 --- /dev/null +++ b/marbles/graphs/tug-of-war.json @@ -0,0 +1 @@ +{"Params":{"NMarbles":100,"MarbleStartX":{"Expr":"if(n%2==0, 5(rand+0.1), 5(rand-1.1))"},"MarbleStartY":{"Expr":"10-2n/nmarbles"},"StartVelocityY":{"Expr":{"Expr":"0"}},"StartVelocityX":{"Expr":{"Expr":"0"}},"UpdateRate":{"Expr":{"Expr":".02"}},"TimeStep":{"Expr":{"Expr":"0.01"}},"YForce":{"Expr":{"Expr":"-0.1"}},"XForce":{"Expr":{"Expr":"0"}},"CenterX":{"Expr":{"Expr":"0"}},"CenterY":{"Expr":{"Expr":"0"}},"TrackingSettings":{"TrackByDefault":false,"NTrackingFrames":300,"Accuracy":20,"LineColor":{"R":255,"G":255,"B":255,"A":255}}},"Lines":[{"Expr":{"Expr":"x^2/3"},"GraphIf":{"Expr":"false"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":0,"G":182,"B":77,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"f(x+5)-h/100"},"GraphIf":{"Expr":"x\u003c-0.01"},"Bounce":{"Expr":"0.3rand+0.8"},"Colors":{"Color":{"R":255,"G":0,"B":0,"A":255},"ColorSwitch":{"R":255,"G":0,"B":0,"A":255}}},{"Expr":{"Expr":"f(x-5)-h/100"},"GraphIf":{"Expr":"x\u003e0"},"Bounce":{"Expr":"0.3rand+0.8"},"Colors":{"Color":{"R":0,"G":0,"B":255,"A":255},"ColorSwitch":{"R":0,"G":0,"B":255,"A":255}}},{"Expr":{"Expr":"gx-bx-f(x+5)+f(x-5)"},"GraphIf":{"Expr":"false"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":188,"G":174,"B":0,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"9"},"GraphIf":{"Expr":"t\u003e1\u0026\u0026cx\u003e=0\u0026\u0026abs(x-cx)\u003c1"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":0,"G":0,"B":255,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}},{"Expr":{"Expr":"9"},"GraphIf":{"Expr":"t\u003e1\u0026\u0026cx\u003c0\u0026\u0026abs(x-cx)\u003c1"},"Bounce":{"Expr":"0.95"},"Colors":{"Color":{"R":255,"G":0,"B":0,"A":255},"ColorSwitch":{"R":255,"G":255,"B":255,"A":255}}}]} diff --git a/marbles/io.go b/marbles/io.go new file mode 100644 index 00000000..cd3febb9 --- /dev/null +++ b/marbles/io.go @@ -0,0 +1,75 @@ +package main + +import ( + "errors" + "fmt" + "io/fs" + "path/filepath" + + "cogentcore.org/core/base/iox/jsonx" + "cogentcore.org/core/core" +) + +// SaveLast saves to the last opened or saved file +func (gr *Graph) SaveLast() { //types:add + if gr.State.File != "" { + TheGraph.SaveJSON(gr.State.File) + } else { + HandleError(fmt.Errorf("Graph.SaveLast: no last file")) + } +} + +// OpenJSON opens a graph from a JSON file +func (gr *Graph) OpenJSON(filename core.Filename) error { //types:add + err := jsonx.Open(gr, string(filename)) + if HandleError(err) { + return err + } + gr.State.File = filename + gr.graphAndUpdate() + return nil +} + +// OpenAutoSave opens the last graphed graph, stays between sessions of the app +func (gr *Graph) OpenAutoSave() error { + err := jsonx.Open(gr, filepath.Join(core.TheApp.AppDataDir(), "autosave.json")) + if errors.Is(err, fs.ErrNotExist) { + return nil + } + if HandleError(err) { + return err + } + gr.graphAndUpdate() + return nil +} + +// SaveJSON saves a graph to a JSON file +func (gr *Graph) SaveJSON(filename core.Filename) error { //types:add + var err error + if TheSettings.PrettyJSON { + err = jsonx.SaveIndent(gr, string(filename)) + } else { + err = jsonx.Save(gr, string(filename)) + } + if HandleError(err) { + return err + } + gr.State.File = filename + gr.graphAndUpdate() + return nil +} + +// AutoSave saves the graph to autosave.json, called automatically +func (gr *Graph) AutoSave() error { + filename := filepath.Join(core.TheApp.AppDataDir(), "autosave.json") + var err error + if TheSettings.PrettyJSON { + err = jsonx.SaveIndent(gr, filename) + } else { + err = jsonx.Save(gr, filename) + } + if HandleError(err) { + return err + } + return nil +} diff --git a/marbles/main.go b/marbles/main.go new file mode 100644 index 00000000..40f5edca --- /dev/null +++ b/marbles/main.go @@ -0,0 +1,31 @@ +// Copyright (c) 2020, Kai O'Reilly. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +//go:generate core generate + +import ( + "cogentcore.org/core/base/errors" + "cogentcore.org/core/core" +) + +func main() { + TheSettings.Defaults() + + b := core.NewBody("Cogent Marbles") + b.AddTopBar(func(bar *core.Frame) { + core.NewToolbar(bar).Maker(TheGraph.MakeToolbar) + }) + + TheGraph.Init(b) + TheGraph.OpenAutoSave() + + b.RunMainWindow() +} + +// TODO(kai/marbles): better error handling +func HandleError(err error) bool { + return errors.Log(err) != nil +} diff --git a/marbles/marbles.go b/marbles/marbles.go new file mode 100644 index 00000000..76fe6827 --- /dev/null +++ b/marbles/marbles.go @@ -0,0 +1,290 @@ +package main + +import ( + "image/color" + "math" + "slices" + "time" + + "cogentcore.org/core/base/errors" + "cogentcore.org/core/colors" + "cogentcore.org/core/math32" +) + +// Marble contains the information of a marble +type Marble struct { + Pos math32.Vector2 + Velocity math32.Vector2 + PrevPos math32.Vector2 + Color color.RGBA + TrackingInfo TrackingInfo +} + +// TrackingInfo contains all of the tracking info for a marble. +type TrackingInfo struct { + Track bool + // History contains all of the previous positions, + // with the most recent last. + History []math32.Vector2 + FramesSinceLastUpdate int + StartedTrackingAt int +} + +// GraphMarblesInit initializes the graph drawing of the marbles +func (gr *Graph) GraphMarblesInit() { + for i, m := range gr.Marbles { + if TheSettings.MarbleSettings.MarbleColor == "default" { + m.Color = colors.Spaced(i) + } else { + m.Color = errors.Log1(colors.FromName(TheSettings.MarbleSettings.MarbleColor)) + } + m.TrackingInfo.History = []math32.Vector2{m.Pos} + m.TrackingInfo.StartedTrackingAt = 0 + } +} + +// Init makes a marble +func (m *Marble) Init(n int) { + if TheGraph.Params.MarbleStartX.Compile() != nil { + return + } + TheGraph.Params.MarbleStartX.Params["n"] = n + xPos := TheGraph.Params.MarbleStartX.Eval(0, 0, 0) + + if TheGraph.Params.MarbleStartY.Compile() != nil { + return + } + TheGraph.Params.MarbleStartY.Params["n"] = n + yPos := TheGraph.Params.MarbleStartY.Eval(xPos, 0, 0) + + m.Pos = math32.Vector2{X: float32(xPos), Y: float32(yPos)} + // fmt.Printf("mb.Pos: %v \n", mb.Pos) + startY := TheGraph.Params.StartVelocityY.Eval(float64(m.Pos.X), float64(m.Pos.Y)) + startX := TheGraph.Params.StartVelocityX.Eval(float64(m.Pos.X), float64(m.Pos.Y)) + m.Velocity = math32.Vector2{X: float32(startX), Y: float32(startY)} + m.PrevPos = m.Pos + tls := TheGraph.Params.TrackingSettings + m.TrackingInfo.Track = tls.TrackByDefault +} + +// InitMarbles creates the marbles and puts them at their initial positions +func (gr *Graph) InitMarbles() { + gr.Marbles = make([]*Marble, 0) + for n := 0; n < gr.Params.NMarbles; n++ { + m := Marble{} + m.Init(n) + gr.Marbles = append(gr.Marbles, &m) + } + gr.State.SelectedMarble = -1 +} + +// ResetMarbles just calls InitMarbles and GraphMarblesInit +func (gr *Graph) ResetMarbles() { + gr.InitMarbles() + gr.GraphMarblesInit() +} + +// UpdateMarbles updates the marbles graph and marbles data +func (gr *Graph) UpdateMarbles() bool { + gr.Objects.Graph.NeedsRender() + gr.UpdateMarblesData() + return false +} + +// UpdateTracking updates the tracking info for the marble. +func (m *Marble) UpdateTracking() { + if !m.TrackingInfo.Track { + return + } + tls := TheGraph.Params.TrackingSettings + fslu := m.TrackingInfo.FramesSinceLastUpdate + if fslu <= 100/tls.Accuracy { + m.TrackingInfo.FramesSinceLastUpdate++ + return + } + m.TrackingInfo.History = append(m.TrackingInfo.History, m.Pos) + m.TrackingInfo.FramesSinceLastUpdate = 0 + if TheGraph.State.Step-m.TrackingInfo.StartedTrackingAt >= tls.NTrackingFrames { + m.TrackingInfo.History = slices.Delete(m.TrackingInfo.History, 0, 1) + } +} + +// UpdateMarblesData updates marbles data +func (gr *Graph) UpdateMarblesData() { + gr.EvalMu.Lock() + defer gr.EvalMu.Unlock() + + for _, m := range gr.Marbles { + + m.Velocity.Y += float32(gr.Params.YForce.Eval(float64(m.Pos.X), float64(m.Pos.Y))) * ((gr.Vectors.Size.Y * gr.Vectors.Size.X) / 400) + m.Velocity.X += float32(gr.Params.XForce.Eval(float64(m.Pos.X), float64(m.Pos.Y))) * ((gr.Vectors.Size.Y * gr.Vectors.Size.X) / 400) + updtrate := float32(gr.Params.UpdateRate.Eval(float64(m.Pos.X), float64(m.Pos.Y))) + npos := m.Pos.Add(m.Velocity.MulScalar(updtrate)) + ppos := m.Pos + setColor := colors.White + for _, ln := range gr.Lines { + if ln.Expr.Val == nil { + continue + } + + // previous line y (with old time) + yp := ln.Expr.Eval(float64(m.Pos.X), gr.State.PrevTime, ln.TimesHit) + // new line y with old time + yno := ln.Expr.Eval(float64(npos.X), gr.State.PrevTime, ln.TimesHit) + // new line y + yn := ln.Expr.Eval(float64(npos.X), gr.State.Time, ln.TimesHit) + + if m.Collided(ln, npos, yp, yn) { + ln.TimesHit++ + setColor = ln.Colors.ColorSwitch + m.Pos, m.Velocity = m.CalcCollide(ln, npos, yp, yn, yno) + break + } + } + + m.PrevPos = ppos + m.Pos = m.Pos.Add(m.Velocity.MulScalar(float32(gr.Params.UpdateRate.Eval(float64(m.Pos.X), float64(m.Pos.Y))))) + if setColor != colors.White { + m.Color = setColor + } + m.UpdateTracking() + } +} + +// Collided returns true if the marble has collided with the line, and false if the marble has not. +func (m *Marble) Collided(ln *Line, npos math32.Vector2, yp, yn float64) bool { + graphIf := ln.GraphIf.EvalBool(float64(npos.X), yn, TheGraph.State.Time, ln.TimesHit) + inBounds := TheGraph.InBounds(npos) + collided := (float64(npos.Y) < yn && float64(m.Pos.Y) >= yp) || (float64(npos.Y) > yn && float64(m.Pos.Y) <= yp) + if collided && graphIf && inBounds { + return true + } + return false +} + +// CalcCollide calculates the new position and velocity of a marble after a collision with the given +// line, given the previous line y, new line y, and new line y with old time +func (m *Marble) CalcCollide(ln *Line, npos math32.Vector2, yp, yn, yno float64) (math32.Vector2, math32.Vector2) { + dly := yn - yp // change in the lines y + dx := npos.X - m.Pos.X + + var yi, xi float32 + + if dx == 0 { + + xi = npos.X + yi = float32(yn) + + } else { + + ml := float32(dly) / dx + dmy := npos.Y - m.Pos.Y + mm := dmy / dx + + xi = (npos.X*(ml-mm) + npos.Y - float32(yn)) / (ml - mm) + yi = float32(ln.Expr.Eval(float64(xi), TheGraph.State.Time, ln.TimesHit)) + // fmt.Printf("xi: %v, yi: %v \n", xi, yi) + } + + yl := ln.Expr.Eval(float64(xi)-.01, TheGraph.State.Time, ln.TimesHit) // point to the left of x + yr := ln.Expr.Eval(float64(xi)+.01, TheGraph.State.Time, ln.TimesHit) // point to the right of x + + //slp := (yr - yl) / .02 + angLn := math32.Atan2(float32(yr-yl), 0.02) + angN := angLn + math.Pi/2 // + 90 deg + + angI := math32.Atan2(m.Velocity.Y, m.Velocity.X) + angII := angI + math.Pi + + angNII := angN - angII + angR := math.Pi + 2*angNII + + Bounce := ln.Bounce.EvalWithY(float64(npos.X), TheGraph.State.Time, ln.TimesHit, float64(yi)) + + nvx := float32(Bounce) * (m.Velocity.X*math32.Cos(angR) - m.Velocity.Y*math32.Sin(angR)) + nvy := float32(Bounce) * (m.Velocity.X*math32.Sin(angR) + m.Velocity.Y*math32.Cos(angR)) + + vel := math32.Vector2{X: nvx, Y: nvy} + pos := math32.Vector2{X: xi, Y: yi + float32(yn-yno)} // adding change from prev time to current time in same pos fixes collisions with moving lines + + return pos, vel +} + +// InBounds checks whether a point is in the bounds of the graph +func (gr *Graph) InBounds(pos math32.Vector2) bool { + if pos.Y > gr.Vectors.Min.Y && pos.Y < gr.Vectors.Max.Y && pos.X > gr.Vectors.Min.X && pos.X < gr.Vectors.Max.X { + return true + } + return false +} + +// RunMarbles runs the marbles for NSteps +func (gr *Graph) RunMarbles() { + if gr.State.Running { + return + } + gr.State.Running = true + gr.State.Step = 0 + startFrames := 0 + start := time.Now() + ticker := time.NewTicker(time.Second / 60) + for range ticker.C { + if !gr.State.Running { + ticker.Stop() + return + } + gr.State.Step++ + if gr.State.Error != nil { + gr.State.Running = false + } + for j := 0; j < TheSettings.NFramesPer-1; j++ { + gr.UpdateMarblesData() + gr.State.PrevTime = gr.State.Time + gr.State.Time += gr.Params.TimeStep.Eval(0, 0) + } + gr.Objects.Graph.AsyncLock() + ok := gr.UpdateMarbles() + gr.Objects.Graph.AsyncUnlock() + if ok { + gr.State.Step-- + continue + } + if time.Since(start).Milliseconds() >= 3000 { + _ = startFrames + // fpsText.SetText(fmt.Sprintf("FPS: %v", (gr.State.Step-startFrames)/3)) + start = time.Now() + startFrames = gr.State.Step + } + gr.State.PrevTime = gr.State.Time + gr.State.Time += gr.Params.TimeStep.Eval(0, 0) + } +} + +// ToggleTrack toogles tracking setting for a certain marble +func (m *Marble) ToggleTrack(idx int) { + m.TrackingInfo.Track = !m.TrackingInfo.Track + m.TrackingInfo.FramesSinceLastUpdate = 0 + m.TrackingInfo.History = []math32.Vector2{m.Pos} + m.TrackingInfo.StartedTrackingAt = TheGraph.State.Step +} + +// SelectNextMarble selects the next marble in the viewbox +func (gr *Graph) SelectNextMarble() { //types:add + if !gr.State.Running { + defer gr.Objects.Graph.NeedsRender() + } + gr.State.SelectedMarble++ + if gr.State.SelectedMarble >= len(gr.Marbles) { + gr.State.SelectedMarble = 0 + } + newMarble := gr.Marbles[gr.State.SelectedMarble] + if !gr.InBounds(newMarble.Pos) { // If the marble isn't in bounds, don't select it + for _, m := range gr.Marbles { // If all marbles aren't in bounds, do nothing + if gr.InBounds(m.Pos) { + gr.SelectNextMarble() + return + } + } + return + } +} diff --git a/marbles/settings.go b/marbles/settings.go new file mode 100644 index 00000000..75c69d49 --- /dev/null +++ b/marbles/settings.go @@ -0,0 +1,155 @@ +package main + +import ( + "image/color" + + "cogentcore.org/core/colors" +) + +// TODO: update settings to use Cogent Core settings structure + +// Settings are the settings the app has +type Settings struct { + LineDefaults LineDefaults `view:"no-inline" label:"Line Defaults"` + + GraphDefaults Params `view:"no-inline" label:"Graph Param Defaults"` + + MarbleSettings MarbleSettings `view:"inline" label:"Marble Settings"` + + GraphSize int `label:"Graph Size" min:"100" max:"800"` + + GraphInc int `label:"Line quality in n per x" min:"1" max:"100"` + + NFramesPer int `label:"Number of times to update the marbles each render" min:"1" max:"100"` + LineFontSize int `label:"Line Font Size"` + ConfirmQuit bool `label:"Confirm App Close"` + PrettyJSON bool `label:"Save formatted JSON"` +} + +// MarbleSettings are the settings for the marbles in the app +type MarbleSettings struct { + MarbleColor string + MarbleSize float64 +} + +// LineDefaults are the settings for the default line +type LineDefaults struct { + Expr string + GraphIf string + Bounce string + LineColors LineColors +} + +// TrackingSettings contains the tracking line settings +type TrackingSettings struct { + TrackByDefault bool + + NTrackingFrames int `min:"0" step:"10"` + + Accuracy int `min:"1" max:"100" step:"5"` + LineColor color.RGBA +} + +// TheSettings is the instance of settings +var TheSettings Settings + +/* +// SettingProps is the toolbar for settings +var SettingProps = ki.Props{ + "ToolBar": ki.PropSlice{ + {Name: "Reset", Value: ki.Props{ + "label": "Reset Settings", + }, + }, + }} + +// KiTTheSettings is for the toolbar +var KiTTheSettings = kit.Types.AddType(&TheSettings, SettingProps) + +// Reset resets the settings to defaults +func (se *Settings) Reset() { + se.Defaults() + se.Save() +} + +// Get gets the settings from localdata/settings.json +func (se *Settings) Get() { + b, err := os.ReadFile(filepath.Join(GetMarblesFolder(), "localData/settings.json")) + if err != nil { + se.Defaults() + se.Save() + return + } + err = json.Unmarshal(b, se) + if err != nil { + se.Defaults() + se.Save() + return + } + if se.LineDefaults.Expr == "" { + se.LineDefaults.BasicDefaults() + se.Save() + } + if se.MarbleSettings.MarbleColor == "" { + se.MarbleSettings.Defaults() + se.Save() + } + if se.ColorSettings.BackgroundColor == gist.NilColor { + se.ColorSettings.Defaults() + se.Save() + } + +} + +// Save saves the settings to localData/settings.json +func (se *Settings) Save() { + var b []byte + var err error + if TheSettings.PrettyJSON { + b, err = json.MarshalIndent(se, "", " ") + } else { + b, err = json.Marshal(se) + } + if HandleError(err) { + return + } + err = os.WriteFile(filepath.Join(GetMarblesFolder(), "localData/settings.json"), b, os.ModePerm) + HandleError(err) +} +*/ + +// Defaults defaults the settings +func (se *Settings) Defaults() { + se.LineDefaults.BasicDefaults() + se.GraphDefaults.BasicDefaults() + se.MarbleSettings.Defaults() + se.GraphSize = 700 + se.GraphInc = 40 + se.NFramesPer = 1 + se.LineFontSize = 24 + se.ConfirmQuit = true + se.PrettyJSON = false +} + +// Defaults sets the default settings for the tracking lines. +func (ts *TrackingSettings) Defaults() { + ts.TrackByDefault = false + ts.NTrackingFrames = 300 + ts.Accuracy = 20 + ts.LineColor = colors.White +} + +// BasicDefaults sets the line defaults to their defaults +func (ln *LineDefaults) BasicDefaults() { + ln.Expr = "sinx" + ln.LineColors.Color = colors.White + ln.Bounce = "0.95" + ln.GraphIf = "true" + ln.LineColors.ColorSwitch = colors.White +} + +// Defaults sets the marble settings to their defaults +func (ms *MarbleSettings) Defaults() { + ms.MarbleColor = "default" + ms.MarbleSize = 0.1 +} diff --git a/marbles/typegen.go b/marbles/typegen.go new file mode 100644 index 00000000..4f6c7502 --- /dev/null +++ b/marbles/typegen.go @@ -0,0 +1,11 @@ +// Code generated by "core generate"; DO NOT EDIT. + +package main + +import ( + "cogentcore.org/core/types" +) + +var _ = types.AddType(&types.Type{Name: "main.Graph", IDName: "graph", Doc: "Graph contains the lines and parameters of a graph", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Methods: []types.Method{{Name: "Graph", Doc: "Graph updates graph for current equations, and resets marbles too", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Run", Doc: "Run runs the marbles for NSteps", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Stop", Doc: "Stop stops the marbles", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Step", Doc: "Step does one step update of marbles", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "StopSelecting", Doc: "StopSelecting stops selecting current marble", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "TrackSelectedMarble", Doc: "TrackSelectedMarble toggles track for the currently selected marble", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "AddLine", Doc: "AddLine adds a new blank line", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "Reset", Doc: "Reset resets the graph to its starting position (one default line and default params)", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "SaveLast", Doc: "SaveLast saves to the last opened or saved file", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}, {Name: "OpenJSON", Doc: "OpenJSON opens a graph from a JSON file", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename"}, Returns: []string{"error"}}, {Name: "SaveJSON", Doc: "SaveJSON saves a graph to a JSON file", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Args: []string{"filename"}, Returns: []string{"error"}}, {Name: "SelectNextMarble", Doc: "SelectNextMarble selects the next marble in the viewbox", Directives: []types.Directive{{Tool: "types", Directive: "add"}}}}, Fields: []types.Field{{Name: "Params", Doc: "the parameters for updating the marbles"}, {Name: "Lines", Doc: "the lines of the graph -- can have any number"}, {Name: "Marbles"}, {Name: "State"}, {Name: "Functions"}, {Name: "Vectors"}, {Name: "Objects"}, {Name: "EvalMu"}}}) + +var _ = types.AddType(&types.Type{Name: "main.Params", IDName: "params", Doc: "Params are the parameters of the graph", Directives: []types.Directive{{Tool: "types", Directive: "add"}}, Fields: []types.Field{{Name: "NMarbles", Doc: "Number of marbles"}, {Name: "MarbleStartX", Doc: "Marble horizontal start position"}, {Name: "MarbleStartY", Doc: "Marble vertical start position"}, {Name: "StartVelocityY", Doc: "Starting horizontal velocity of the marbles"}, {Name: "StartVelocityX", Doc: "Starting vertical velocity of the marbles"}, {Name: "UpdateRate", Doc: "how fast to move along velocity vector -- lower = smoother, more slow-mo"}, {Name: "TimeStep", Doc: "how fast time increases"}, {Name: "YForce", Doc: "how fast it accelerates down"}, {Name: "XForce", Doc: "how fast the marbles move side to side without collisions, set to 0 for no movement"}, {Name: "CenterX", Doc: "the center point of the graph, x"}, {Name: "CenterY", Doc: "the center point of the graph, y"}, {Name: "TrackingSettings"}}})