mirror of
https://github.com/BoredDevNL/BoredOS.git
synced 2026-05-15 02:46:08 +00:00
1770 lines
66 KiB
C
1770 lines
66 KiB
C
// Copyright (c) 2023-2026 Chris (boreddevnl)
|
|
// This software is released under the GNU General Public License v3.0. See LICENSE file for details.
|
|
// This header needs to maintain in any file it is present in, as per the GPL license terms.
|
|
|
|
// BOREDOS_APP_DESC: 3/2D Graphing and plotting utility.
|
|
// BOREDOS_APP_ICONS: /Library/images/icons/colloid/se.sjoerd.Graphs.png;/Library/images/icons/colloid/app-icon-preview.png
|
|
#include "syscall.h"
|
|
#include "libui.h"
|
|
#include "../../wm/libwidget.h"
|
|
#include <stdbool.h>
|
|
#include "stdlib.h"
|
|
#include "math.h"
|
|
|
|
// External syscalls from libc
|
|
extern void sys_parallel_run(void (*fn)(void*), void **args, int count);
|
|
|
|
// =========
|
|
// Constants
|
|
// =========
|
|
// Adjust the below variables to suit your system specification and preferences.
|
|
// --- User Configuration ---
|
|
#define ROTATE 1 // Set to 0 to disable auto-rotation in 3D mode.
|
|
#define GRID_3D 41 // Grid resolution. Adjust on how much you want your PC to die (lmao)
|
|
// --------------------------
|
|
#define TOOLBAR_H 30
|
|
#define STATUSBAR_H 30
|
|
#define GRAPH_Y TOOLBAR_H
|
|
#define CLIENT_H (win_h - 20)
|
|
|
|
static int win_w = 750;
|
|
static int win_h = 600;
|
|
static int graph_w = 750;
|
|
static int graph_h = 520;
|
|
static int fb_capacity = 0;
|
|
static uint32_t *graph_fb = NULL;
|
|
static int32_t *graph_zb = NULL;
|
|
static uint64_t *graph_czb = NULL;
|
|
|
|
#define COLOR_BG 0xFF1A1A2E
|
|
#define COLOR_GRID 0xFF2A2A4A
|
|
#define COLOR_AXIS 0xFF6A6A8A
|
|
#define COLOR_CURVE 0xFF00FF88
|
|
#define COLOR_TEXT 0xFFE0E0E0
|
|
#define COLOR_TOOLBAR_BG 0xFF2D2D2D
|
|
#define COLOR_STATUS_BG 0xFF252535
|
|
#define COLOR_DARK_BG 0xFF1E1E1E
|
|
#define COLOR_DARK_PANEL 0xFF2D2D2D
|
|
#define COLOR_DARK_TEXT 0xFFF0F0F0
|
|
#define MAX_TOKENS 256
|
|
#define MAX_NODES 128
|
|
|
|
#define MODE_2D 0
|
|
#define MODE_3D 1
|
|
|
|
|
|
// =================
|
|
// Expression Parser
|
|
// =================
|
|
enum {
|
|
TOK_NUM, TOK_VAR_X, TOK_VAR_Y, TOK_VAR_Z,
|
|
TOK_PLUS, TOK_MINUS, TOK_STAR, TOK_SLASH, TOK_CARET,
|
|
TOK_LPAREN, TOK_RPAREN, TOK_EQUALS,
|
|
TOK_FN_SIN, TOK_FN_COS, TOK_FN_TAN, TOK_FN_SQRT, TOK_FN_ABS, TOK_FN_LOG,
|
|
TOK_END
|
|
};
|
|
|
|
enum {
|
|
NODE_NUM, NODE_VAR,
|
|
NODE_ADD, NODE_SUB, NODE_MUL, NODE_DIV, NODE_POW,
|
|
NODE_NEG,
|
|
NODE_SIN, NODE_COS, NODE_TAN, NODE_SQRT, NODE_ABS, NODE_LOG
|
|
};
|
|
|
|
typedef struct { int type; double value; } Token;
|
|
typedef struct { int type; double value; int var_idx; int left, right; } ASTNode;
|
|
|
|
enum {
|
|
OP_PUSH_NUM, OP_PUSH_VAR,
|
|
OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_POW,
|
|
OP_NEG,
|
|
OP_SIN, OP_COS, OP_TAN, OP_SQRT, OP_ABS, OP_LOG
|
|
};
|
|
|
|
typedef struct {
|
|
int op;
|
|
double val;
|
|
int var_idx;
|
|
} Instruction;
|
|
|
|
#define MAX_BC_SIZE 256
|
|
static Instruction lhs_bc[MAX_BC_SIZE], rhs_bc[MAX_BC_SIZE];
|
|
static int lhs_bc_len = 0, rhs_bc_len = 0;
|
|
|
|
|
|
|
|
static bool is_alpha(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); }
|
|
static bool is_digit(char c) { return c >= '0' && c <= '9'; }
|
|
static bool is_value_tok(int t) {
|
|
return t == TOK_NUM || t == TOK_VAR_X || t == TOK_VAR_Y || t == TOK_VAR_Z || t == TOK_RPAREN;
|
|
}
|
|
|
|
static int tokenize(const char *input, Token *tokens) {
|
|
int count = 0;
|
|
const char *p = input;
|
|
|
|
while (*p && count < MAX_TOKENS - 1) {
|
|
while (*p == ' ' || *p == '\t') p++;
|
|
if (!*p) break;
|
|
|
|
if (count > 0 && is_value_tok(tokens[count-1].type)) {
|
|
if (is_digit(*p) || *p == '.' || is_alpha(*p) || *p == '(') {
|
|
tokens[count].type = TOK_STAR; tokens[count].value = 0; count++;
|
|
}
|
|
}
|
|
|
|
if (is_digit(*p) || (*p == '.' && is_digit(p[1]))) {
|
|
double val = 0;
|
|
while (is_digit(*p)) { val = val * 10 + (*p - '0'); p++; }
|
|
if (*p == '.') {
|
|
p++; double frac = 0.1;
|
|
while (is_digit(*p)) { val += (*p - '0') * frac; frac *= 0.1; p++; }
|
|
}
|
|
tokens[count].type = TOK_NUM; tokens[count].value = val; count++;
|
|
} else if (is_alpha(*p)) {
|
|
char buf[32]; int len = 0;
|
|
while (is_alpha(*p) && len < 31) buf[len++] = *p++;
|
|
buf[len] = 0;
|
|
|
|
if (len == 1 && (buf[0] == 'x' || buf[0] == 'X')) tokens[count].type = TOK_VAR_X;
|
|
else if (len == 1 && (buf[0] == 'y' || buf[0] == 'Y')) tokens[count].type = TOK_VAR_Y;
|
|
else if (len == 1 && (buf[0] == 'z' || buf[0] == 'Z')) tokens[count].type = TOK_VAR_Z;
|
|
else if (strcmp(buf, "sin") == 0) tokens[count].type = TOK_FN_SIN;
|
|
else if (strcmp(buf, "cos") == 0) tokens[count].type = TOK_FN_COS;
|
|
else if (strcmp(buf, "tan") == 0) tokens[count].type = TOK_FN_TAN;
|
|
else if (strcmp(buf, "sqrt") == 0) tokens[count].type = TOK_FN_SQRT;
|
|
else if (strcmp(buf, "abs") == 0) tokens[count].type = TOK_FN_ABS;
|
|
else if (strcmp(buf, "log") == 0) tokens[count].type = TOK_FN_LOG;
|
|
else if (strcmp(buf, "pi") == 0 || strcmp(buf, "PI") == 0) {
|
|
tokens[count].type = TOK_NUM; tokens[count].value = M_PI;
|
|
} else { tokens[count].type = TOK_NUM; tokens[count].value = 0; }
|
|
count++;
|
|
} else {
|
|
switch (*p) {
|
|
case '+': tokens[count].type = TOK_PLUS; break;
|
|
case '-': tokens[count].type = TOK_MINUS; break;
|
|
case '*': tokens[count].type = TOK_STAR; break;
|
|
case '/': tokens[count].type = TOK_SLASH; break;
|
|
case '^': tokens[count].type = TOK_CARET; break;
|
|
case '(': {
|
|
if (count > 0 && is_value_tok(tokens[count-1].type)) {
|
|
tokens[count].type = TOK_STAR; count++;
|
|
}
|
|
tokens[count].type = TOK_LPAREN; break;
|
|
}
|
|
case ')': tokens[count].type = TOK_RPAREN; break;
|
|
case '=': tokens[count].type = TOK_EQUALS; break;
|
|
default: p++; continue;
|
|
}
|
|
p++; count++;
|
|
}
|
|
}
|
|
tokens[count].type = TOK_END;
|
|
return count;
|
|
}
|
|
|
|
static int parse_expr(Token *t, int *p, ASTNode *n, int *nc);
|
|
|
|
static int parse_atom(Token *t, int *p, ASTNode *n, int *nc) {
|
|
if (t[*p].type == TOK_NUM) {
|
|
int i = (*nc)++;
|
|
n[i].type = NODE_NUM; n[i].value = t[*p].value; n[i].left = n[i].right = -1;
|
|
(*p)++; return i;
|
|
}
|
|
if (t[*p].type >= TOK_VAR_X && t[*p].type <= TOK_VAR_Z) {
|
|
int i = (*nc)++;
|
|
n[i].type = NODE_VAR; n[i].var_idx = t[*p].type - TOK_VAR_X;
|
|
n[i].left = n[i].right = -1; (*p)++; return i;
|
|
}
|
|
if (t[*p].type == TOK_LPAREN) {
|
|
(*p)++;
|
|
int inner = parse_expr(t, p, n, nc);
|
|
if (t[*p].type == TOK_RPAREN) (*p)++;
|
|
return inner;
|
|
}
|
|
int i = (*nc)++;
|
|
n[i].type = NODE_NUM; n[i].value = 0; n[i].left = n[i].right = -1;
|
|
return i;
|
|
}
|
|
|
|
static int parse_unary(Token *t, int *p, ASTNode *n, int *nc) {
|
|
if (t[*p].type == TOK_MINUS) {
|
|
(*p)++;
|
|
int op = parse_unary(t, p, n, nc);
|
|
int i = (*nc)++; n[i].type = NODE_NEG; n[i].left = op; n[i].right = -1; return i;
|
|
}
|
|
if (t[*p].type >= TOK_FN_SIN && t[*p].type <= TOK_FN_LOG) {
|
|
int fn_type = t[*p].type; (*p)++;
|
|
int arg;
|
|
if (t[*p].type == TOK_LPAREN) {
|
|
(*p)++;
|
|
arg = parse_expr(t, p, n, nc);
|
|
if (t[*p].type == TOK_RPAREN) (*p)++;
|
|
} else {
|
|
arg = parse_unary(t, p, n, nc);
|
|
}
|
|
int node_type = NODE_SIN;
|
|
if (fn_type == TOK_FN_COS) node_type = NODE_COS;
|
|
else if (fn_type == TOK_FN_TAN) node_type = NODE_TAN;
|
|
else if (fn_type == TOK_FN_SQRT) node_type = NODE_SQRT;
|
|
else if (fn_type == TOK_FN_ABS) node_type = NODE_ABS;
|
|
else if (fn_type == TOK_FN_LOG) node_type = NODE_LOG;
|
|
int i = (*nc)++; n[i].type = node_type; n[i].left = arg; n[i].right = -1; return i;
|
|
}
|
|
return parse_atom(t, p, n, nc);
|
|
}
|
|
|
|
static int parse_power(Token *t, int *p, ASTNode *n, int *nc) {
|
|
int left = parse_unary(t, p, n, nc);
|
|
if (t[*p].type == TOK_CARET) {
|
|
(*p)++;
|
|
int right = parse_power(t, p, n, nc);
|
|
int i = (*nc)++; n[i].type = NODE_POW; n[i].left = left; n[i].right = right; return i;
|
|
}
|
|
return left;
|
|
}
|
|
|
|
static int parse_term(Token *t, int *p, ASTNode *n, int *nc) {
|
|
int left = parse_power(t, p, n, nc);
|
|
while (t[*p].type == TOK_STAR || t[*p].type == TOK_SLASH) {
|
|
int op = (t[*p].type == TOK_STAR) ? NODE_MUL : NODE_DIV; (*p)++;
|
|
int right = parse_power(t, p, n, nc);
|
|
int i = (*nc)++; n[i].type = op; n[i].left = left; n[i].right = right; left = i;
|
|
}
|
|
return left;
|
|
}
|
|
|
|
static int parse_expr(Token *t, int *p, ASTNode *n, int *nc) {
|
|
int left = parse_term(t, p, n, nc);
|
|
while (t[*p].type == TOK_PLUS || t[*p].type == TOK_MINUS) {
|
|
int op = (t[*p].type == TOK_PLUS) ? NODE_ADD : NODE_SUB; (*p)++;
|
|
int right = parse_term(t, p, n, nc);
|
|
int i = (*nc)++; n[i].type = op; n[i].left = left; n[i].right = right; left = i;
|
|
}
|
|
return left;
|
|
}
|
|
|
|
static double eval_ast(ASTNode *n, int idx, double x, double y, double z) {
|
|
if (idx < 0) return 0;
|
|
ASTNode *nd = &n[idx];
|
|
switch (nd->type) {
|
|
case NODE_NUM: return nd->value;
|
|
case NODE_VAR: return nd->var_idx == 0 ? x : nd->var_idx == 1 ? y : z;
|
|
case NODE_ADD: return eval_ast(n, nd->left, x, y, z) + eval_ast(n, nd->right, x, y, z);
|
|
case NODE_SUB: return eval_ast(n, nd->left, x, y, z) - eval_ast(n, nd->right, x, y, z);
|
|
case NODE_MUL: return eval_ast(n, nd->left, x, y, z) * eval_ast(n, nd->right, x, y, z);
|
|
case NODE_DIV: {
|
|
double d = eval_ast(n, nd->right, x, y, z);
|
|
return fabs(d) < 1e-15 ? 1e15 : eval_ast(n, nd->left, x, y, z) / d;
|
|
}
|
|
case NODE_POW: {
|
|
double b = eval_ast(n, nd->left, x, y, z);
|
|
double e = eval_ast(n, nd->right, x, y, z);
|
|
if (e == 2.0) return b * b;
|
|
if (e == 3.0) return b * b * b;
|
|
return pow(b, e);
|
|
}
|
|
case NODE_NEG: return -eval_ast(n, nd->left, x, y, z);
|
|
case NODE_SIN: return sin(eval_ast(n, nd->left, x, y, z));
|
|
case NODE_COS: return cos(eval_ast(n, nd->left, x, y, z));
|
|
case NODE_TAN: return tan(eval_ast(n, nd->left, x, y, z));
|
|
case NODE_SQRT: return sqrt(eval_ast(n, nd->left, x, y, z));
|
|
case NODE_ABS: return fabs(eval_ast(n, nd->left, x, y, z));
|
|
case NODE_LOG: return log(eval_ast(n, nd->left, x, y, z));
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// =================
|
|
// Bytecode Compiler
|
|
// =================
|
|
static int compile_ast(ASTNode *nodes, int idx, Instruction *bc, int *len) {
|
|
if (idx < 0 || *len >= MAX_BC_SIZE) return 0;
|
|
ASTNode *n = &nodes[idx];
|
|
|
|
compile_ast(nodes, n->left, bc, len);
|
|
compile_ast(nodes, n->right, bc, len);
|
|
|
|
Instruction *inst = &bc[(*len)++];
|
|
switch (n->type) {
|
|
case NODE_NUM: inst->op = OP_PUSH_NUM; inst->val = n->value; break;
|
|
case NODE_VAR: inst->op = OP_PUSH_VAR; inst->var_idx = n->var_idx; break;
|
|
case NODE_ADD: inst->op = OP_ADD; break;
|
|
case NODE_SUB: inst->op = OP_SUB; break;
|
|
case NODE_MUL: inst->op = OP_MUL; break;
|
|
case NODE_DIV: inst->op = OP_DIV; break;
|
|
case NODE_POW: inst->op = OP_POW; break;
|
|
case NODE_NEG: inst->op = OP_NEG; break;
|
|
case NODE_SIN: inst->op = OP_SIN; break;
|
|
case NODE_COS: inst->op = OP_COS; break;
|
|
case NODE_TAN: inst->op = OP_TAN; break;
|
|
case NODE_SQRT: inst->op = OP_SQRT; break;
|
|
case NODE_ABS: inst->op = OP_ABS; break;
|
|
case NODE_LOG: inst->op = OP_LOG; break;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static double run_bc(Instruction *bc, int len, double x, double y, double z) {
|
|
if (len == 0) return 0;
|
|
double stack[32];
|
|
int sp = 0;
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
Instruction *inst = &bc[i];
|
|
switch (inst->op) {
|
|
case OP_PUSH_NUM: stack[sp++] = inst->val; break;
|
|
case OP_PUSH_VAR: stack[sp++] = (inst->var_idx == 0 ? x : inst->var_idx == 1 ? y : z); break;
|
|
case OP_ADD: { double b = stack[--sp]; double a = stack[--sp]; stack[sp++] = a + b; break; }
|
|
case OP_SUB: { double b = stack[--sp]; double a = stack[--sp]; stack[sp++] = a - b; break; }
|
|
case OP_MUL: { double b = stack[--sp]; double a = stack[--sp]; stack[sp++] = a * b; break; }
|
|
case OP_DIV: {
|
|
double b = stack[--sp]; double a = stack[--sp];
|
|
stack[sp++] = (fabs(b) < 1e-15) ? 1e15 : a / b; break;
|
|
}
|
|
case OP_POW: {
|
|
double b = stack[--sp]; double a = stack[--sp];
|
|
if (b == 2.0) stack[sp++] = a * a;
|
|
else if (b == 3.0) stack[sp++] = a * a * a;
|
|
else stack[sp++] = pow(a, b);
|
|
break;
|
|
}
|
|
case OP_NEG: stack[sp-1] = -stack[sp-1]; break;
|
|
case OP_SIN: stack[sp-1] = sin(stack[sp-1]); break;
|
|
case OP_COS: stack[sp-1] = cos(stack[sp-1]); break;
|
|
case OP_TAN: stack[sp-1] = tan(stack[sp-1]); break;
|
|
case OP_SQRT: stack[sp-1] = sqrt(stack[sp-1]); break;
|
|
case OP_ABS: stack[sp-1] = fabs(stack[sp-1]); break;
|
|
case OP_LOG: stack[sp-1] = log(stack[sp-1]); break;
|
|
}
|
|
}
|
|
return sp > 0 ? stack[sp-1] : 0;
|
|
}
|
|
|
|
// Check which variables an AST subtree uses
|
|
static void ast_find_vars(ASTNode *n, int idx, bool *has_x, bool *has_y, bool *has_z) {
|
|
if (idx < 0) return;
|
|
if (n[idx].type == NODE_VAR) {
|
|
if (n[idx].var_idx == 0) *has_x = true;
|
|
if (n[idx].var_idx == 1) *has_y = true;
|
|
if (n[idx].var_idx == 2) *has_z = true;
|
|
}
|
|
ast_find_vars(n, n[idx].left, has_x, has_y, has_z);
|
|
ast_find_vars(n, n[idx].right, has_x, has_y, has_z);
|
|
}
|
|
|
|
// =================
|
|
// Application State
|
|
// =================
|
|
static ui_window_t win_graph;
|
|
|
|
static char eq_buffer[256];
|
|
static int eq_len = 0;
|
|
|
|
// Parsed equation
|
|
static ASTNode lhs_nodes[MAX_NODES], rhs_nodes[MAX_NODES];
|
|
static int lhs_root = -1, rhs_root = -1;
|
|
static int lhs_nc = 0, rhs_nc = 0;
|
|
static bool eq_valid = false;
|
|
static int graph_mode = MODE_2D;
|
|
|
|
// 2D view
|
|
static double view_x_min = -10, view_x_max = 10;
|
|
static double view_y_min = -6.4, view_y_max = 6.4;
|
|
|
|
// 3D view
|
|
static double rot_x = 0.5, rot_y = 0.6;
|
|
static double range_3d = 5.0;
|
|
static double zoom_3d = 1.0;
|
|
static bool filled_mode = false;
|
|
static bool is_explicit_3d = false;
|
|
static bool is_explicit_2d = false;
|
|
|
|
static bool right_dragging = false;
|
|
static int drag_last_x = 0, drag_last_y = 0;
|
|
|
|
#define MAX_Z_PER_POINT 4
|
|
typedef struct {
|
|
double z[MAX_Z_PER_POINT];
|
|
float nx[MAX_Z_PER_POINT], ny[MAX_Z_PER_POINT], nz[MAX_Z_PER_POINT];
|
|
int sx[MAX_Z_PER_POINT], sy[MAX_Z_PER_POINT], dz[MAX_Z_PER_POINT];
|
|
int count;
|
|
} surf_point_t;
|
|
|
|
static double surf_x[GRID_3D][GRID_3D], surf_y_3d[GRID_3D][GRID_3D];
|
|
static surf_point_t surf[GRID_3D][GRID_3D];
|
|
|
|
static bool surface_needs_eval = true;
|
|
static double rot_cx, rot_sx, rot_cy, rot_sy;
|
|
|
|
static widget_textbox_t tb_equation;
|
|
static widget_button_t btn_plot;
|
|
static widget_button_t btn_mode;
|
|
static widget_button_t btn_range_minus;
|
|
static widget_button_t btn_range_plus;
|
|
|
|
static const char *preset_labels[] = {
|
|
"y = sin(x)", "y = x^2", "y = cos(x)*x",
|
|
"z = sin(x)*cos(y)", "z = x^2 - y^2", "x^2+y^2+z^2=25", "x^2+y^2=16"
|
|
};
|
|
#define NUM_PRESETS 7
|
|
|
|
static bool presets_open = false;
|
|
|
|
// Widget context
|
|
static void gfx_draw_rect(void *ud, int x, int y, int w, int h, uint32_t c) {
|
|
ui_draw_rect((ui_window_t)ud, x, y, w, h, c);
|
|
}
|
|
static void gfx_draw_rr(void *ud, int x, int y, int w, int h, int r, uint32_t c) {
|
|
ui_draw_rounded_rect_filled((ui_window_t)ud, x, y, w, h, r, c);
|
|
}
|
|
static void gfx_draw_str(void *ud, int x, int y, const char *s, uint32_t c) {
|
|
ui_draw_string((ui_window_t)ud, x, y, s, c);
|
|
}
|
|
static widget_context_t wctx = { 0, gfx_draw_rect, gfx_draw_rr, gfx_draw_str, NULL, NULL, false };
|
|
|
|
// ================
|
|
// Graphics helpers
|
|
// ================
|
|
static void gfb_clear(uint32_t c) {
|
|
int total = graph_w * graph_h;
|
|
uint64_t clear_val = ((uint64_t)1000000LL << 32) | c;
|
|
for (int i = 0; i < total; i++) {
|
|
graph_fb[i] = c;
|
|
graph_zb[i] = 1000000;
|
|
if (graph_czb) graph_czb[i] = clear_val;
|
|
}
|
|
}
|
|
|
|
static void gfb_pixel(int x, int y, uint32_t c) {
|
|
if (x >= 0 && x < graph_w && y >= 0 && y < graph_h)
|
|
graph_fb[y * graph_w + x] = c;
|
|
}
|
|
|
|
static void gfb_pixel_z(int x, int y, int z, uint32_t c) {
|
|
if (x < 0 || x >= graph_w || y < 0 || y >= graph_h) return;
|
|
int idx = y * graph_w + x;
|
|
|
|
if (graph_czb) {
|
|
uint64_t new_val = ((uint64_t)z << 32) | c;
|
|
uint64_t old_val;
|
|
while (z < (int32_t)((old_val = __atomic_load_n(&graph_czb[idx], __ATOMIC_RELAXED)) >> 32)) {
|
|
if (__atomic_compare_exchange_n(&graph_czb[idx], &old_val, new_val, false, __ATOMIC_SEQ_CST, __ATOMIC_RELAXED))
|
|
break;
|
|
}
|
|
} else {
|
|
int32_t old_z;
|
|
while (z < (old_z = __atomic_load_n(&graph_zb[idx], __ATOMIC_RELAXED))) {
|
|
if (__atomic_compare_exchange_n(&graph_zb[idx], &old_z, z, false, __ATOMIC_SEQ_CST, __ATOMIC_RELAXED)) {
|
|
graph_fb[idx] = c;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
static void gfb_line(int x0, int y0, int x1, int y1, uint32_t c) {
|
|
int dx = fabs((double)(x1 - x0)), dy = fabs((double)(y1 - y0));
|
|
int sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
|
|
int err = dx - dy;
|
|
for (int i = 0; i < 2000; i++) {
|
|
gfb_pixel(x0, y0, c);
|
|
if (x0 == x1 && y0 == y1) break;
|
|
int e2 = 2 * err;
|
|
if (e2 > -dy) { err -= dy; x0 += sx; }
|
|
if (e2 < dx) { err += dx; y0 += sy; }
|
|
}
|
|
}
|
|
|
|
static void gfb_line_z(int x0, int y0, int z0, int x1, int y1, int z1, uint32_t c) {
|
|
int dx = fabs((double)(x1 - x0)), dy = fabs((double)(y1 - y0));
|
|
int sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
|
|
int err = dx - dy;
|
|
int steps = (dx > dy ? dx : dy);
|
|
if (steps == 0) { gfb_pixel_z(x0, y0, z0, c); return; }
|
|
|
|
for (int i = 0; i <= steps && i < 2000; i++) {
|
|
int cz = z0 + (int)((long)(z1 - z0) * i / steps);
|
|
gfb_pixel_z(x0, y0, cz, c);
|
|
if (x0 == x1 && y0 == y1) break;
|
|
int e2 = 2 * err;
|
|
if (e2 > -dy) { err -= dy; x0 += sx; }
|
|
if (e2 < dx) { err += dx; y0 += sy; }
|
|
}
|
|
}
|
|
|
|
static void gfb_triangle_z(int x0, int y0, int z0, int x1, int y1, int z1, int x2, int y2, int z2, uint32_t c) {
|
|
if (y1 < y0) { int t; t=x0; x0=x1; x1=t; t=y0; y0=y1; y1=t; t=z0; z0=z1; z1=t; }
|
|
if (y2 < y0) { int t; t=x0; x0=x2; x2=t; t=y0; y0=y2; y2=t; t=z0; z0=z2; z2=t; }
|
|
if (y2 < y1) { int t; t=x1; x1=x2; x2=t; t=y1; y1=y2; y2=t; t=z1; z1=z2; z2=t; }
|
|
|
|
if (y0 == y2) return;
|
|
|
|
for (int y = y0; y <= y2; y++) {
|
|
if (y < 0 || y >= graph_h) continue;
|
|
bool second_half = y > y1 || y1 == y0;
|
|
int h1 = second_half ? (y2 - y1) : (y1 - y0);
|
|
int h2 = y2 - y0;
|
|
if (h1 == 0) h1 = 1;
|
|
if (h2 == 0) h2 = 1;
|
|
|
|
float alpha = (float)(y - y0) / h2;
|
|
float beta = (float)(y - (second_half ? y1 : y0)) / h1;
|
|
|
|
int ax = x0 + (int)((x2 - x0) * alpha);
|
|
int bx = second_half ? x1 + (int)((x2 - x1) * beta) : x0 + (int)((x1 - x0) * beta);
|
|
int az = z0 + (int)((z2 - z0) * alpha);
|
|
int bz = second_half ? z1 + (int)((z2 - z1) * beta) : z0 + (int)((z1 - z0) * beta);
|
|
|
|
if (ax > bx) { int t; t=ax; ax=bx; bx=t; t=az; az=bz; bz=t; }
|
|
int span = bx - ax;
|
|
int x_start = ax < 0 ? 0 : ax;
|
|
int x_end = bx >= graph_w ? graph_w - 1 : bx;
|
|
for (int x = x_start; x <= x_end; x++) {
|
|
float phi = (span == 0) ? 0.5f : (float)(x - ax) / span;
|
|
int cz = az + (int)((bz - az) * phi);
|
|
gfb_pixel_z(x, y, cz, c);
|
|
}
|
|
}
|
|
}
|
|
|
|
static uint32_t color_by_height(double z, double zmin, double zmax) {
|
|
if (zmax <= zmin) return COLOR_CURVE;
|
|
double t = (z - zmin) / (zmax - zmin);
|
|
if (t < 0) t = 0;
|
|
if (t > 1) t = 1;
|
|
int r, g, b;
|
|
if (t < 0.25) { double s = t/0.25; r=0; g=(int)(s*255); b=255; }
|
|
else if (t < 0.5) { double s=(t-0.25)/0.25; r=0; g=255; b=(int)((1-s)*255); }
|
|
else if (t < 0.75) { double s=(t-0.5)/0.25; r=(int)(s*255); g=255; b=0; }
|
|
else { double s=(t-0.75)/0.25; r=255; g=(int)((1-s)*255); b=0; }
|
|
return 0xFF000000 | (r<<16) | (g<<8) | b;
|
|
}
|
|
|
|
static uint32_t apply_shading(uint32_t color, double nx, double ny, double nz) {
|
|
double len = sqrt(nx*nx + ny*ny + nz*nz);
|
|
if (len > 1e-9) { nx /= len; ny /= len; nz /= len; }
|
|
else { nx = 0; ny = 1; nz = 0; }
|
|
|
|
double lx = 0.577, ly = 0.707, lz = 0.408;
|
|
double dot = nx * lx + ny * ly + nz * lz;
|
|
if (dot < 0) dot = -dot * 0.2;
|
|
|
|
double intensity = 0.3 + 0.7 * dot;
|
|
if (intensity > 1.0) intensity = 1.0;
|
|
|
|
int r = (color >> 16) & 0xFF;
|
|
int g = (color >> 8) & 0xFF;
|
|
int b = color & 0xFF;
|
|
|
|
r = (int)(r * intensity);
|
|
g = (int)(g * intensity);
|
|
b = (int)(b * intensity);
|
|
if (r > 255) r = 255; if (g > 255) g = 255; if (b > 255) b = 255;
|
|
|
|
return 0xFF000000 | (r << 16) | (g << 8) | b;
|
|
}
|
|
|
|
// ========================
|
|
// 2D coordinate transforms
|
|
// ========================
|
|
static int screen_x_2d(double wx) {
|
|
return (int)((wx - view_x_min) / (view_x_max - view_x_min) * graph_w);
|
|
}
|
|
static int screen_y_2d(double wy) {
|
|
return (int)((view_y_max - wy) / (view_y_max - view_y_min) * graph_h);
|
|
}
|
|
static double world_x_2d(int px) {
|
|
return view_x_min + (px / (double)graph_w) * (view_x_max - view_x_min);
|
|
}
|
|
|
|
// =============
|
|
// 3D projection
|
|
// =============
|
|
static void project_3d(double px, double py, double pz, int *sx, int *sy, int *sz) {
|
|
double nx = px * rot_cy + pz * rot_sy, nz = -px * rot_sy + pz * rot_cy;
|
|
px = nx; pz = nz;
|
|
double ny = py * rot_cx - pz * rot_sx;
|
|
nz = py * rot_sx + pz * rot_cx;
|
|
py = ny; pz = nz;
|
|
|
|
double view_dim = graph_h < graph_w ? graph_h : graph_w;
|
|
double base_scale = view_dim * 0.35 / range_3d * zoom_3d;
|
|
|
|
double d = range_3d * 5;
|
|
double zd = pz + d;
|
|
if (zd < d * 0.1) zd = d * 0.1;
|
|
double persp = d / zd;
|
|
*sx = (int)(px * base_scale * persp) + graph_w / 2;
|
|
*sy = (int)(-py * base_scale * persp) + graph_h / 2;
|
|
*sz = (int)(pz * 10000); // Fixed-point depth
|
|
}
|
|
// istg math is scary
|
|
// ====================================================
|
|
// Evaluate the implicit function: f(x,y,z) = LHS - RHS
|
|
// ====================================================
|
|
static double eval_implicit(double x, double y, double z) {
|
|
return run_bc(lhs_bc, lhs_bc_len, x, y, z) - run_bc(rhs_bc, rhs_bc_len, x, y, z);
|
|
}
|
|
|
|
static double eval_rhs_only(double x, double y, double z) {
|
|
return run_bc(rhs_bc, rhs_bc_len, x, y, z);
|
|
}
|
|
|
|
// ===========================
|
|
// Parse and classify equation
|
|
// ===========================
|
|
static void parse_equation(void) {
|
|
surface_needs_eval = true;
|
|
eq_valid = false;
|
|
lhs_nc = 0; rhs_nc = 0;
|
|
lhs_root = -1; rhs_root = -1;
|
|
|
|
// Find '='
|
|
int eq_pos = -1;
|
|
for (int i = 0; eq_buffer[i]; i++) {
|
|
if (eq_buffer[i] == '=') { eq_pos = i; break; }
|
|
}
|
|
|
|
if (eq_pos >= 0) {
|
|
// Split into LHS and RHS
|
|
char lhs_str[256], rhs_str[256];
|
|
memcpy(lhs_str, eq_buffer, eq_pos); lhs_str[eq_pos] = 0;
|
|
strcpy(rhs_str, eq_buffer + eq_pos + 1);
|
|
|
|
Token lt[MAX_TOKENS], rt[MAX_TOKENS];
|
|
tokenize(lhs_str, lt); tokenize(rhs_str, rt);
|
|
int lp = 0, rp = 0;
|
|
lhs_root = parse_expr(lt, &lp, lhs_nodes, &lhs_nc);
|
|
rhs_root = parse_expr(rt, &rp, rhs_nodes, &rhs_nc);
|
|
} else {
|
|
Token tt[MAX_TOKENS];
|
|
tokenize(eq_buffer, tt);
|
|
int tp = 0;
|
|
rhs_root = parse_expr(tt, &tp, rhs_nodes, &rhs_nc);
|
|
// LHS = y (for display as y=f(x) if no y/z in expr)
|
|
bool hx=false, hy=false, hz=false;
|
|
ast_find_vars(rhs_nodes, rhs_root, &hx, &hy, &hz);
|
|
if (!hy && !hz) {
|
|
lhs_nc = 1;
|
|
lhs_nodes[0].type = NODE_VAR; lhs_nodes[0].var_idx = 1;
|
|
lhs_nodes[0].left = lhs_nodes[0].right = -1;
|
|
lhs_root = 0;
|
|
} else {
|
|
lhs_root = rhs_root;
|
|
memcpy(lhs_nodes, rhs_nodes, sizeof(ASTNode) * rhs_nc);
|
|
lhs_nc = rhs_nc;
|
|
rhs_nc = 1;
|
|
rhs_nodes[0].type = NODE_NUM; rhs_nodes[0].value = 0;
|
|
rhs_nodes[0].left = rhs_nodes[0].right = -1;
|
|
rhs_root = 0;
|
|
}
|
|
}
|
|
|
|
// Determine mode
|
|
bool lhx=false, lhy=false, lhz=false, rhx=false, rhy=false, rhz=false;
|
|
ast_find_vars(lhs_nodes, lhs_root, &lhx, &lhy, &lhz);
|
|
ast_find_vars(rhs_nodes, rhs_root, &rhx, &rhy, &rhz);
|
|
|
|
bool has_z = lhz || rhz;
|
|
is_explicit_2d = false;
|
|
is_explicit_3d = false;
|
|
|
|
if (has_z) {
|
|
graph_mode = MODE_3D;
|
|
if (lhs_nc >= 1 && lhs_nodes[lhs_root].type == NODE_VAR &&
|
|
lhs_nodes[lhs_root].var_idx == 2 && !rhz) {
|
|
is_explicit_3d = true;
|
|
}
|
|
} else {
|
|
graph_mode = MODE_2D;
|
|
if (lhs_nc >= 1 && lhs_nodes[lhs_root].type == NODE_VAR &&
|
|
lhs_nodes[lhs_root].var_idx == 1 && !rhy) {
|
|
is_explicit_2d = true;
|
|
}
|
|
}
|
|
|
|
eq_valid = true;
|
|
|
|
// Compile to bytecode
|
|
lhs_bc_len = 0; rhs_bc_len = 0;
|
|
if (lhs_root >= 0) compile_ast(lhs_nodes, lhs_root, lhs_bc, &lhs_bc_len);
|
|
if (rhs_root >= 0) compile_ast(rhs_nodes, rhs_root, rhs_bc, &rhs_bc_len);
|
|
}
|
|
|
|
// =========
|
|
// Rendering
|
|
// =========
|
|
static double get_nice_step(double range, int target_divisions) {
|
|
if (range <= 0) return 1.0;
|
|
double approx = range / target_divisions;
|
|
|
|
// Find magnitude (10^n)
|
|
double mag = 1.0;
|
|
if (approx >= 1.0) {
|
|
while (approx >= 10.0) { approx /= 10.0; mag *= 10.0; }
|
|
} else {
|
|
while (approx < 1.0) { approx *= 10.0; mag /= 10.0; }
|
|
}
|
|
|
|
// Pick nice residual
|
|
double res;
|
|
if (approx < 1.5) res = 1.0;
|
|
else if (approx < 3.0) res = 2.0;
|
|
else if (approx < 7.0) res = 5.0;
|
|
else res = 10.0;
|
|
|
|
return res * mag;
|
|
}
|
|
|
|
static void apply_aspect_ratio(void) {
|
|
if (graph_w <= 0 || graph_h <= 0) return;
|
|
double cy = (view_y_min + view_y_max) / 2.0;
|
|
double x_range = view_x_max - view_x_min;
|
|
double target_y_range = x_range * (double)graph_h / (double)graph_w;
|
|
view_y_min = cy - target_y_range / 2.0;
|
|
view_y_max = cy + target_y_range / 2.0;
|
|
}
|
|
|
|
static void autofit_2d_view(void) {
|
|
if (!is_explicit_2d) {
|
|
apply_aspect_ratio();
|
|
return;
|
|
}
|
|
double y_min_data = 1e30, y_max_data = -1e30;
|
|
bool found = false;
|
|
for (int px = 0; px < graph_w; px += 2) {
|
|
double wx = world_x_2d(px);
|
|
double wy = eval_rhs_only(wx, 0, 0);
|
|
if (wy == wy && fabs(wy) < 1e10) {
|
|
if (wy < y_min_data) y_min_data = wy;
|
|
if (wy > y_max_data) y_max_data = wy;
|
|
found = true;
|
|
}
|
|
}
|
|
if (found) {
|
|
if (y_min_data * y_max_data <= 0 || fabs(y_min_data) < (y_max_data - y_min_data) * 0.5) {
|
|
double max_abs = fabs(y_min_data);
|
|
if (fabs(y_max_data) > max_abs) max_abs = fabs(y_max_data);
|
|
double pad = max_abs * 0.15;
|
|
if (pad < 0.5) pad = 0.5;
|
|
view_y_min = -(max_abs + pad);
|
|
view_y_max = (max_abs + pad);
|
|
} else {
|
|
double pad = (y_max_data - y_min_data) * 0.15;
|
|
if (pad < 0.5) pad = 0.5;
|
|
view_y_min = y_min_data - pad;
|
|
view_y_max = y_max_data + pad;
|
|
}
|
|
|
|
double x_range = view_x_max - view_x_min;
|
|
double target_y_range = x_range * (double)graph_h / (double)graph_w;
|
|
double current_y_range = view_y_max - view_y_min;
|
|
|
|
if (current_y_range < target_y_range) {
|
|
double cy = (view_y_min + view_y_max) / 2.0;
|
|
if (fabs(cy) < current_y_range * 0.1) cy = 0;
|
|
view_y_min = cy - target_y_range / 2.0;
|
|
view_y_max = cy + target_y_range / 2.0;
|
|
} else {
|
|
double target_x_range = current_y_range * (double)graph_w / (double)graph_h;
|
|
double cx = (view_x_min + view_x_max) / 2.0;
|
|
if (fabs(cx) < x_range * 0.1) cx = 0;
|
|
view_x_min = cx - target_x_range / 2.0;
|
|
view_x_max = cx + target_x_range / 2.0;
|
|
}
|
|
} else {
|
|
apply_aspect_ratio();
|
|
}
|
|
}
|
|
|
|
static void render_2d_grid(void) {
|
|
// Grid intervals
|
|
double x_step = get_nice_step(view_x_max - view_x_min, 8);
|
|
double y_step = get_nice_step(view_y_max - view_y_min, 6);
|
|
|
|
// X grid lines
|
|
double x_start = (int)(view_x_min / x_step) * x_step;
|
|
int safety = 0;
|
|
for (double wx = x_start - x_step; wx <= view_x_max + x_step && safety < 200; wx += x_step, safety++) {
|
|
int sx = screen_x_2d(wx);
|
|
if (sx >= 0 && sx < graph_w) {
|
|
for (int y = 0; y < graph_h; y++) gfb_pixel(sx, y, COLOR_GRID);
|
|
}
|
|
}
|
|
|
|
// Y grid lines
|
|
double y_start = (int)(view_y_min / y_step) * y_step;
|
|
safety = 0;
|
|
for (double wy = y_start - y_step; wy <= view_y_max + y_step && safety < 200; wy += y_step, safety++) {
|
|
int sy = screen_y_2d(wy);
|
|
if (sy >= 0 && sy < graph_h) {
|
|
for (int x = 0; x < graph_w; x++) gfb_pixel(x, sy, COLOR_GRID);
|
|
}
|
|
}
|
|
|
|
// Axes
|
|
int ax = screen_x_2d(0), ay = screen_y_2d(0);
|
|
if (ax >= 0 && ax < graph_w)
|
|
for (int y = 0; y < graph_h; y++) gfb_pixel(ax, y, COLOR_AXIS);
|
|
if (ay >= 0 && ay < graph_h)
|
|
for (int x = 0; x < graph_w; x++) gfb_pixel(x, ay, COLOR_AXIS);
|
|
}
|
|
|
|
static void render_2d_explicit(void) {
|
|
int prev_sx = -1, prev_sy = -1;
|
|
bool prev_valid = false;
|
|
|
|
for (int px = 0; px < graph_w; px++) {
|
|
double wx = world_x_2d(px);
|
|
double wy = eval_rhs_only(wx, 0, 0);
|
|
if (wy != wy || fabs(wy) > 1e10) { prev_valid = false; continue; }
|
|
int sy = screen_y_2d(wy);
|
|
if (prev_valid && fabs((double)(sy - prev_sy)) < graph_h) {
|
|
gfb_line(prev_sx, prev_sy, px, sy, COLOR_CURVE);
|
|
}
|
|
prev_sx = px; prev_sy = sy; prev_valid = true;
|
|
}
|
|
}
|
|
|
|
static void render_2d_implicit(void) {
|
|
// Marching squares for f(x,y) = 0
|
|
int grid_x = 200, grid_y = 130;
|
|
double dx = (view_x_max - view_x_min) / grid_x;
|
|
double dy = (view_y_max - view_y_min) / grid_y;
|
|
|
|
for (int gy = 0; gy < grid_y; gy++) {
|
|
for (int gx = 0; gx < grid_x; gx++) {
|
|
double x0 = view_x_min + gx * dx;
|
|
double y0 = view_y_max - gy * dy;
|
|
double x1 = x0 + dx, y1 = y0 - dy;
|
|
|
|
double f00 = eval_implicit(x0, y0, 0);
|
|
double f10 = eval_implicit(x1, y0, 0);
|
|
double f01 = eval_implicit(x0, y1, 0);
|
|
double f11 = eval_implicit(x1, y1, 0);
|
|
|
|
// Check edges for sign changes
|
|
int sx0 = screen_x_2d(x0), sx1 = screen_x_2d(x1);
|
|
int sy0 = screen_y_2d(y0), sy1 = screen_y_2d(y1);
|
|
|
|
if ((f00 > 0) != (f10 > 0)) {
|
|
double t = f00 / (f00 - f10);
|
|
int mx = sx0 + (int)(t * (sx1 - sx0));
|
|
gfb_pixel(mx, sy0, COLOR_CURVE);
|
|
gfb_pixel(mx+1, sy0, COLOR_CURVE);
|
|
}
|
|
if ((f00 > 0) != (f01 > 0)) {
|
|
double t = f00 / (f00 - f01);
|
|
int my = sy0 + (int)(t * (sy1 - sy0));
|
|
gfb_pixel(sx0, my, COLOR_CURVE);
|
|
gfb_pixel(sx0, my+1, COLOR_CURVE);
|
|
}
|
|
if ((f10 > 0) != (f11 > 0)) {
|
|
double t = f10 / (f10 - f11);
|
|
int my = sy0 + (int)(t * (sy1 - sy0));
|
|
gfb_pixel(sx1, my, COLOR_CURVE);
|
|
}
|
|
if ((f01 > 0) != (f11 > 0)) {
|
|
double t = f01 / (f01 - f11);
|
|
int mx = sx0 + (int)(t * (sx1 - sx0));
|
|
gfb_pixel(mx, sy1, COLOR_CURVE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void render_3d_axes(void) {
|
|
int ox, oy, oz;
|
|
project_3d(0, 0, 0, &ox, &oy, &oz);
|
|
|
|
int ax, ay, az;
|
|
project_3d(range_3d, 0, 0, &ax, &ay, &az); gfb_line_z(ox, oy, oz, ax, ay, az, 0xFFFF4444);
|
|
project_3d(0, range_3d, 0, &ax, &ay, &az); gfb_line_z(ox, oy, oz, ax, ay, az, 0xFF44FF44);
|
|
project_3d(0, 0, range_3d, &ax, &ay, &az); gfb_line_z(ox, oy, oz, ax, ay, az, 0xFF4444FF);
|
|
}
|
|
|
|
// =======================
|
|
// Parallel Evaluation Job
|
|
// =======================
|
|
|
|
typedef struct {
|
|
float c[MAX_Z_PER_POINT];
|
|
float nx[MAX_Z_PER_POINT], ny[MAX_Z_PER_POINT], nz[MAX_Z_PER_POINT];
|
|
int count;
|
|
} eval_cache_entry_t;
|
|
|
|
static eval_cache_entry_t eval_cache[3][GRID_3D][GRID_3D];
|
|
|
|
typedef struct {
|
|
int start_j, end_j;
|
|
double range;
|
|
double step;
|
|
double z_scale;
|
|
int march_axis;
|
|
} eval_job_t;
|
|
|
|
static void eval_3d_explicit_job(void *arg) {
|
|
eval_job_t *job = (eval_job_t *)arg;
|
|
for (int j = job->start_j; j < job->end_j; j++) {
|
|
for (int i = 0; i < GRID_3D; i++) {
|
|
double wx = -job->range + i * job->step;
|
|
double wy = -job->range + j * job->step;
|
|
double wz = eval_rhs_only(wx, wy, 0);
|
|
surf_x[j][i] = wx;
|
|
surf_y_3d[j][i] = wy;
|
|
surf[j][i].count = 0;
|
|
|
|
if (fabs(wz) > 1e10 || wz != wz) {
|
|
continue;
|
|
}
|
|
|
|
surf[j][i].z[0] = wz;
|
|
|
|
double eps = 0.001;
|
|
double dfx = 0.5 * (eval_rhs_only(wx+eps, wy, 0) - eval_rhs_only(wx-eps, wy, 0)) / eps;
|
|
double dfy = 0.5 * (eval_rhs_only(wx, wy+eps, 0) - eval_rhs_only(wx, wy-eps, 0)) / eps;
|
|
|
|
double nx = -dfx;
|
|
double ny = -dfy;
|
|
double nz = 1.0;
|
|
double len = sqrt(nx*nx + ny*ny + nz*nz);
|
|
if (len > 1e-9) { nx /= len; ny /= len; nz /= len; }
|
|
else { nx = 0; ny = 0; nz = 1; }
|
|
|
|
surf[j][i].nx[0] = (float)nx;
|
|
surf[j][i].ny[0] = (float)ny;
|
|
surf[j][i].nz[0] = (float)nz;
|
|
surf[j][i].count = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void eval_3d_implicit_job(void *arg) {
|
|
eval_job_t *job = (eval_job_t *)arg;
|
|
const int axis = job->march_axis;
|
|
const int march_steps = 170;
|
|
const double mstep = job->range * 2.0 / march_steps;
|
|
|
|
for (int j = job->start_j; j < job->end_j; j++) {
|
|
for (int i = 0; i < GRID_3D; i++) {
|
|
double a = -job->range + i * job->step;
|
|
double b = -job->range + j * job->step;
|
|
|
|
eval_cache_entry_t *ce = &eval_cache[axis][j][i];
|
|
ce->count = 0;
|
|
|
|
double prev_f;
|
|
switch (axis) {
|
|
case 1: prev_f = eval_implicit(-job->range, a, b); break;
|
|
case 2: prev_f = eval_implicit(a, -job->range, b); break;
|
|
default: prev_f = eval_implicit(a, b, -job->range); break;
|
|
}
|
|
|
|
double found[MAX_Z_PER_POINT]; int nf = 0;
|
|
for (int k = 1; k <= march_steps && nf < MAX_Z_PER_POINT; k++) {
|
|
double c = -job->range + k * mstep;
|
|
double cur_f;
|
|
switch (axis) {
|
|
case 1: cur_f = eval_implicit(c, a, b); break;
|
|
case 2: cur_f = eval_implicit(a, c, b); break;
|
|
default: cur_f = eval_implicit(a, b, c); break;
|
|
}
|
|
if ((prev_f > 0) != (cur_f > 0) && fabs(prev_f) < 1e10 && fabs(cur_f) < 1e10) {
|
|
double ca = c - mstep, cb = c, fa = prev_f, fb = cur_f; (void)fb;
|
|
for (int bi = 0; bi < 15; bi++) {
|
|
double cm = (ca+cb)*0.5, fm;
|
|
switch (axis) {
|
|
case 1: fm = eval_implicit(cm, a, b); break;
|
|
case 2: fm = eval_implicit(a, cm, b); break;
|
|
default: fm = eval_implicit(a, b, cm); break;
|
|
}
|
|
if ((fa>0)!=(fm>0)) { cb=cm; fb=fm; } else { ca=cm; fa=fm; }
|
|
}
|
|
found[nf++] = (ca+cb)*0.5;
|
|
}
|
|
prev_f = cur_f;
|
|
}
|
|
for (int r = 0; r < nf-1; r++)
|
|
for (int s = r+1; s < nf; s++)
|
|
if (found[r] > found[s]) { double t=found[r]; found[r]=found[s]; found[s]=t; }
|
|
|
|
double eps = 0.001;
|
|
for (int r = 0; r < nf; r++) {
|
|
double c = found[r];
|
|
double nx, ny, nz;
|
|
switch (axis) {
|
|
case 1:
|
|
nx = eval_implicit(c+eps,a,b)-eval_implicit(c-eps,a,b);
|
|
ny = eval_implicit(c,a+eps,b)-eval_implicit(c,a-eps,b);
|
|
nz = eval_implicit(c,a,b+eps)-eval_implicit(c,a,b-eps);
|
|
break;
|
|
case 2:
|
|
nx = eval_implicit(a+eps,c,b)-eval_implicit(a-eps,c,b);
|
|
ny = eval_implicit(a,c+eps,b)-eval_implicit(a,c-eps,b);
|
|
nz = eval_implicit(a,c,b+eps)-eval_implicit(a,c,b-eps);
|
|
break;
|
|
default:
|
|
nx = eval_implicit(a+eps,b,c)-eval_implicit(a-eps,b,c);
|
|
ny = eval_implicit(a,b+eps,c)-eval_implicit(a,b-eps,c);
|
|
nz = eval_implicit(a,b,c+eps)-eval_implicit(a,b,c-eps);
|
|
break;
|
|
}
|
|
double d = sqrt(nx*nx+ny*ny+nz*nz);
|
|
if (d > 1e-12) { nx/=d; ny/=d; nz/=d; } else { nx=0; ny=1; nz=0; }
|
|
ce->c[r] = (float)c;
|
|
ce->nx[r] = (float)nx;
|
|
ce->ny[r] = (float)ny;
|
|
ce->nz[r] = (float)nz;
|
|
}
|
|
ce->count = nf;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void eval_3d_project_job(void *arg) {
|
|
eval_job_t *job = (eval_job_t *)arg;
|
|
const int axis = job->march_axis;
|
|
for (int j = job->start_j; j < job->end_j; j++) {
|
|
for (int i = 0; i < GRID_3D; i++) {
|
|
eval_cache_entry_t *ce = &eval_cache[axis][j][i];
|
|
double a = -job->range + i * job->step;
|
|
double b = -job->range + j * job->step;
|
|
|
|
surf_x[j][i] = a;
|
|
surf_y_3d[j][i] = b;
|
|
surf[j][i].count = ce->count;
|
|
for (int s = 0; s < ce->count; s++) {
|
|
double c = ce->c[s];
|
|
surf[j][i].z[s] = (float)c;
|
|
surf[j][i].nx[s] = ce->nx[s];
|
|
surf[j][i].ny[s] = ce->ny[s];
|
|
surf[j][i].nz[s] = ce->nz[s];
|
|
switch (axis) {
|
|
case 1: project_3d(c, b, a, &surf[j][i].sx[s], &surf[j][i].sy[s], &surf[j][i].dz[s]); break;
|
|
case 2: project_3d(a, b, c, &surf[j][i].sx[s], &surf[j][i].sy[s], &surf[j][i].dz[s]); break;
|
|
default:project_3d(a, c, b, &surf[j][i].sx[s], &surf[j][i].sy[s], &surf[j][i].dz[s]); break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void eval_3d_explicit_project_job(void *arg) {
|
|
eval_job_t *job = (eval_job_t *)arg;
|
|
for (int j = job->start_j; j < job->end_j; j++) {
|
|
for (int i = 0; i < GRID_3D; i++) {
|
|
double a = -job->range + i * job->step;
|
|
double b = -job->range + j * job->step;
|
|
|
|
for (int s = 0; s < surf[j][i].count; s++) {
|
|
double c = surf[j][i].z[s];
|
|
project_3d(a, c * job->z_scale, b, &surf[j][i].sx[s], &surf[j][i].sy[s], &surf[j][i].dz[s]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
typedef struct {
|
|
int start_j, end_j;
|
|
double zmin, zmax;
|
|
int march_axis;
|
|
int normal_axis;
|
|
} draw_job_t;
|
|
|
|
static void render_3d_draw_job(void *arg) {
|
|
draw_job_t *job = (draw_job_t *)arg;
|
|
for (int j = job->start_j; j < job->end_j; j++) {
|
|
for (int i = 0; i < GRID_3D; i++) {
|
|
if (surf[j][i].count == 0) continue;
|
|
|
|
for (int s = 0; s < surf[j][i].count; s++) {
|
|
int sx0 = surf[j][i].sx[s], sy0 = surf[j][i].sy[s], dz0 = surf[j][i].dz[s];
|
|
|
|
double height_val = (job->march_axis == 0) ? surf[j][i].z[s] : surf_y_3d[j][i];
|
|
uint32_t col = color_by_height(height_val, job->zmin, job->zmax);
|
|
if (filled_mode && job->normal_axis >= 0) {
|
|
float anx = (float)fabs(surf[j][i].nx[s]);
|
|
float any = (float)fabs(surf[j][i].ny[s]);
|
|
float anz = (float)fabs(surf[j][i].nz[s]);
|
|
bool dominant;
|
|
switch (job->normal_axis) {
|
|
case 0: dominant = (anz >= anx - 0.05f) && (anz >= any - 0.05f); break;
|
|
case 1: dominant = (anx >= any - 0.05f) && (anx >= anz - 0.05f); break;
|
|
case 2: dominant = (any >= anx - 0.05f) && (any >= anz - 0.05f); break;
|
|
default: dominant = true; break;
|
|
}
|
|
if (!dominant) continue;
|
|
|
|
// Depth bias to prevent Z-fighting between passes
|
|
int bias = (job->normal_axis == 0) ? 0 : (job->normal_axis == 1) ? 2 : 4;
|
|
dz0 += bias;
|
|
}
|
|
|
|
// Refined neighbor selection: only connect if points are world-space neighbors
|
|
float world_step = (float)(job->zmax - job->zmin) / (GRID_3D - 1); // rough scaling
|
|
if (world_step < 0.1f) world_step = 0.5f;
|
|
float thresh = world_step * 2.5f;
|
|
|
|
int s_tr = -1;
|
|
if (i+1 < GRID_3D) {
|
|
float mind = 1e30f;
|
|
for (int n=0; n < surf[j][i+1].count; n++) {
|
|
float d = (float)fabs(surf[j][i+1].z[n] - surf[j][i].z[s]);
|
|
if (d < mind) { mind = d; s_tr = n; }
|
|
}
|
|
if (mind > thresh) s_tr = -1;
|
|
}
|
|
int s_bl = -1;
|
|
if (j+1 < GRID_3D) {
|
|
float mind = 1e30f;
|
|
for (int n=0; n < surf[j+1][i].count; n++) {
|
|
float d = (float)fabs(surf[j+1][i].z[n] - surf[j][i].z[s]);
|
|
if (d < mind) { mind = d; s_bl = n; }
|
|
}
|
|
if (mind > thresh) s_bl = -1;
|
|
}
|
|
int s_br = -1;
|
|
if (i+1 < GRID_3D && j+1 < GRID_3D) {
|
|
float mind = 1e30f;
|
|
for (int n=0; n < surf[j+1][i+1].count; n++) {
|
|
float d = (float)fabs(surf[j+1][i+1].z[n] - surf[j][i].z[s]);
|
|
if (d < mind) { mind = d; s_br = n; }
|
|
}
|
|
if (mind > thresh) s_br = -1;
|
|
}
|
|
|
|
if (filled_mode) {
|
|
bool v_tr = (s_tr >= 0);
|
|
bool v_bl = (s_bl >= 0);
|
|
bool v_br = (s_br >= 0);
|
|
|
|
if (v_tr && v_bl && v_br) {
|
|
int bias = (job->normal_axis == 0) ? 0 : (job->normal_axis == 1) ? 2 : 4;
|
|
int sx_tr = surf[j][i+1].sx[s_tr], sy_tr = surf[j][i+1].sy[s_tr], dz_tr = surf[j][i+1].dz[s_tr] + bias;
|
|
int sx_bl = surf[j+1][i].sx[s_bl], sy_bl = surf[j+1][i].sy[s_bl], dz_bl = surf[j+1][i].dz[s_bl] + bias;
|
|
int sx_br = surf[j+1][i+1].sx[s_br], sy_br = surf[j+1][i+1].sy[s_br], dz_br = surf[j+1][i+1].dz[s_br] + bias;
|
|
|
|
float avg_nx = surf[j][i].nx[s] + surf[j][i+1].nx[s_tr] + surf[j+1][i].nx[s_bl] + surf[j+1][i+1].nx[s_br];
|
|
float avg_ny = surf[j][i].ny[s] + surf[j][i+1].ny[s_tr] + surf[j+1][i].ny[s_bl] + surf[j+1][i+1].ny[s_br];
|
|
float avg_nz = surf[j][i].nz[s] + surf[j][i+1].nz[s_tr] + surf[j+1][i].nz[s_bl] + surf[j+1][i+1].nz[s_br];
|
|
avg_nx *= 0.25f; avg_ny *= 0.25f; avg_nz *= 0.25f;
|
|
|
|
float nlen = (float)sqrt(avg_nx*avg_nx + avg_ny*avg_ny + avg_nz*avg_nz);
|
|
if (nlen > 1e-9) { avg_nx /= nlen; avg_ny /= nlen; avg_nz /= nlen; }
|
|
else { avg_nx = 0; avg_ny = 0; avg_nz = 1; }
|
|
|
|
uint32_t scol = apply_shading(col, avg_nx, avg_ny, avg_nz);
|
|
gfb_triangle_z(sx0, sy0, dz0, sx_tr, sy_tr, dz_tr, sx_bl, sy_bl, dz_bl, scol);
|
|
gfb_triangle_z(sx_tr, sy_tr, dz_tr, sx_br, sy_br, dz_br, sx_bl, sy_bl, dz_bl, scol);
|
|
} else if (v_tr && v_bl) {
|
|
uint32_t scol = apply_shading(col, surf[j][i].nx[s], surf[j][i].ny[s], surf[j][i].nz[s]);
|
|
gfb_triangle_z(sx0, sy0, dz0,
|
|
surf[j][i+1].sx[s_tr], surf[j][i+1].sy[s_tr], surf[j][i+1].dz[s_tr],
|
|
surf[j+1][i].sx[s_bl], surf[j+1][i].sy[s_bl], surf[j+1][i].dz[s_bl], scol);
|
|
} else if (v_tr && v_br) {
|
|
uint32_t scol = apply_shading(col, surf[j][i].nx[s], surf[j][i].ny[s], surf[j][i].nz[s]);
|
|
gfb_triangle_z(sx0, sy0, dz0,
|
|
surf[j][i+1].sx[s_tr], surf[j][i+1].sy[s_tr], surf[j][i+1].dz[s_tr],
|
|
surf[j+1][i+1].sx[s_br], surf[j+1][i+1].sy[s_br], surf[j+1][i+1].dz[s_br], scol);
|
|
} else if (v_bl && v_br) {
|
|
uint32_t scol = apply_shading(col, surf[j][i].nx[s], surf[j][i].ny[s], surf[j][i].nz[s]);
|
|
gfb_triangle_z(sx0, sy0, dz0,
|
|
surf[j+1][i].sx[s_bl], surf[j+1][i].sy[s_bl], surf[j+1][i].dz[s_bl],
|
|
surf[j+1][i+1].sx[s_br], surf[j+1][i+1].sy[s_br], surf[j+1][i+1].dz[s_br], scol);
|
|
}
|
|
} else {
|
|
if (i + 1 < GRID_3D && s_tr >= 0) {
|
|
gfb_line_z(sx0, sy0, dz0, surf[j][i+1].sx[s_tr], surf[j][i+1].sy[s_tr], surf[j][i+1].dz[s_tr], col);
|
|
}
|
|
if (j + 1 < GRID_3D && s_bl >= 0) {
|
|
gfb_line_z(sx0, sy0, dz0, surf[j+1][i].sx[s_bl], surf[j+1][i].sy[s_bl], surf[j+1][i].dz[s_bl], col);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
static void render_3d_explicit(void) {
|
|
double step = range_3d * 2.0 * 1.05 / (GRID_3D - 1);
|
|
double zmin = 1e30, zmax = -1e30;
|
|
|
|
if (surface_needs_eval) {
|
|
for (int j = 0; j < GRID_3D; j++) {
|
|
for (int i = 0; i < GRID_3D; i++) {
|
|
surf[j][i].count = 0;
|
|
}
|
|
}
|
|
|
|
int num_chunks = 4;
|
|
eval_job_t jobs[4];
|
|
void *job_args[4];
|
|
int rows_per_chunk = GRID_3D / num_chunks;
|
|
|
|
for (int c = 0; c < num_chunks; c++) {
|
|
jobs[c].start_j = c * rows_per_chunk;
|
|
jobs[c].end_j = (c == num_chunks - 1) ? GRID_3D : (c + 1) * rows_per_chunk;
|
|
jobs[c].range = range_3d;
|
|
jobs[c].step = step;
|
|
jobs[c].march_axis = 0;
|
|
job_args[c] = &jobs[c];
|
|
}
|
|
|
|
sys_parallel_run(eval_3d_explicit_job, job_args, num_chunks);
|
|
}
|
|
|
|
// Compute min/max for coloring
|
|
for (int j = 0; j < GRID_3D; j++) {
|
|
for (int i = 0; i < GRID_3D; i++) {
|
|
for (int s = 0; s < surf[j][i].count; s++) {
|
|
if (surf[j][i].z[s] < zmin) zmin = surf[j][i].z[s];
|
|
if (surf[j][i].z[s] > zmax) zmax = surf[j][i].z[s];
|
|
}
|
|
}
|
|
}
|
|
|
|
double z_scale = 1.0;
|
|
if (zmax > zmin && (zmax - zmin) > 0.001) {
|
|
z_scale = (range_3d * 2.0) / (zmax - zmin);
|
|
}
|
|
|
|
{
|
|
int num_chunks = 4;
|
|
eval_job_t jobs[4];
|
|
void *job_args[4];
|
|
int rows_per_chunk = GRID_3D / num_chunks;
|
|
for (int c = 0; c < num_chunks; c++) {
|
|
jobs[c].start_j = c * rows_per_chunk;
|
|
jobs[c].end_j = (c == num_chunks - 1) ? GRID_3D : (c + 1) * rows_per_chunk;
|
|
jobs[c].range = range_3d;
|
|
jobs[c].step = step;
|
|
jobs[c].z_scale = z_scale;
|
|
jobs[c].march_axis = 0;
|
|
job_args[c] = &jobs[c];
|
|
}
|
|
sys_parallel_run(eval_3d_explicit_project_job, job_args, num_chunks);
|
|
}
|
|
|
|
{
|
|
int num_chunks = 4;
|
|
draw_job_t jobs[4];
|
|
void *job_args[4];
|
|
int rows_per_chunk = GRID_3D / num_chunks;
|
|
for (int c = 0; c < num_chunks; c++) {
|
|
jobs[c].start_j = c * rows_per_chunk;
|
|
jobs[c].end_j = (c == num_chunks - 1) ? GRID_3D : (c + 1) * rows_per_chunk;
|
|
jobs[c].zmin = zmin;
|
|
jobs[c].zmax = zmax;
|
|
jobs[c].march_axis = 0;
|
|
jobs[c].normal_axis = -1;
|
|
job_args[c] = &jobs[c];
|
|
}
|
|
sys_parallel_run(render_3d_draw_job, job_args, num_chunks);
|
|
}
|
|
}
|
|
|
|
static void render_3d_implicit(void) {
|
|
double step = range_3d * 2.0 * 1.05 / (GRID_3D - 1);
|
|
|
|
if (surface_needs_eval) {
|
|
int num_chunks = 4, rows_per_chunk = GRID_3D / num_chunks;
|
|
eval_job_t jobs[4]; void *job_args[4];
|
|
for (int axis = 0; axis < 3; axis++) {
|
|
for (int c = 0; c < num_chunks; c++) {
|
|
jobs[c].start_j = c * rows_per_chunk;
|
|
jobs[c].end_j = (c == num_chunks-1) ? GRID_3D : (c+1)*rows_per_chunk;
|
|
jobs[c].range = range_3d;
|
|
jobs[c].step = step;
|
|
jobs[c].z_scale = 1.0;
|
|
jobs[c].march_axis = axis;
|
|
job_args[c] = &jobs[c];
|
|
}
|
|
sys_parallel_run(eval_3d_implicit_job, job_args, num_chunks);
|
|
}
|
|
surface_needs_eval = false;
|
|
}
|
|
|
|
double zmin = 1e30, zmax = -1e30;
|
|
for (int j = 0; j < GRID_3D; j++)
|
|
for (int i = 0; i < GRID_3D; i++) {
|
|
int cnt = eval_cache[0][j][i].count;
|
|
for (int s = 0; s < cnt; s++) {
|
|
float z = eval_cache[0][j][i].c[s];
|
|
if (z < zmin) zmin = z;
|
|
if (z > zmax) zmax = z;
|
|
}
|
|
}
|
|
if (zmin > zmax) { zmin = -(float)range_3d; zmax = (float)range_3d; }
|
|
|
|
static const int normal_axes[3] = {0, 1, 2};
|
|
int num_chunks = 4, rows_per_chunk = GRID_3D / num_chunks;
|
|
|
|
for (int axis = 0; axis < 3; axis++) {
|
|
{
|
|
eval_job_t jobs[4]; void *job_args[4];
|
|
for (int c = 0; c < num_chunks; c++) {
|
|
jobs[c].start_j = c * rows_per_chunk;
|
|
jobs[c].end_j = (c == num_chunks-1) ? GRID_3D : (c+1)*rows_per_chunk;
|
|
jobs[c].range = range_3d;
|
|
jobs[c].step = step;
|
|
jobs[c].z_scale = 1.0;
|
|
jobs[c].march_axis = axis;
|
|
job_args[c] = &jobs[c];
|
|
}
|
|
sys_parallel_run(eval_3d_project_job, job_args, num_chunks);
|
|
}
|
|
{
|
|
draw_job_t jobs[4]; void *job_args[4];
|
|
for (int c = 0; c < num_chunks; c++) {
|
|
jobs[c].start_j = c * rows_per_chunk;
|
|
jobs[c].end_j = (c == num_chunks-1) ? GRID_3D : (c+1)*rows_per_chunk;
|
|
jobs[c].zmin = zmin;
|
|
jobs[c].zmax = zmax;
|
|
jobs[c].march_axis = axis;
|
|
jobs[c].normal_axis = filled_mode ? normal_axes[axis] : -1;
|
|
job_args[c] = &jobs[c];
|
|
}
|
|
sys_parallel_run(render_3d_draw_job, job_args, num_chunks);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void double_to_str(double val, char *buf) {
|
|
if (val != val) { strcpy(buf, "NaN"); return; }
|
|
if (val > 1e15) { strcpy(buf, "inf"); return; }
|
|
if (val < -1e15) { strcpy(buf, "-inf"); return; }
|
|
|
|
if (val > 2e9) {
|
|
strcpy(buf, ">2G");
|
|
return;
|
|
}
|
|
if (val < -2e9) {
|
|
strcpy(buf, "<-2G");
|
|
return;
|
|
}
|
|
|
|
if (val < 0) { *buf++ = '-'; val = -val; }
|
|
|
|
int ipart = (int)val;
|
|
itoa(ipart, buf);
|
|
while (*buf) buf++;
|
|
double frac = val - (double)ipart;
|
|
if (frac > 0.005) {
|
|
*buf++ = '.';
|
|
int d1 = (int)(frac * 10) % 10;
|
|
int d2 = (int)(frac * 100) % 10;
|
|
*buf++ = '0' + d1;
|
|
if (d2) *buf++ = '0' + d2;
|
|
}
|
|
*buf = 0;
|
|
}
|
|
|
|
static void render_graph(void) {
|
|
gfb_clear(COLOR_BG);
|
|
|
|
if (!eq_valid) {
|
|
if (graph_mode == MODE_2D) render_2d_grid();
|
|
ui_draw_image(win_graph, 0, GRAPH_Y, graph_w, graph_h, graph_fb);
|
|
return;
|
|
}
|
|
|
|
if (graph_mode == MODE_2D) {
|
|
if (is_explicit_2d) render_2d_explicit();
|
|
render_2d_grid();
|
|
if (is_explicit_2d) {
|
|
int prev_sx = -1, prev_sy = -1;
|
|
bool prev_valid = false;
|
|
for (int px = 0; px < graph_w; px++) {
|
|
double wx = world_x_2d(px);
|
|
double wy = eval_rhs_only(wx, 0, 0);
|
|
if (wy != wy || fabs(wy) > 1e10) { prev_valid = false; continue; }
|
|
int sy = screen_y_2d(wy);
|
|
if (prev_valid && fabs((double)(sy - prev_sy)) < graph_h)
|
|
gfb_line(prev_sx, prev_sy, px, sy, COLOR_CURVE);
|
|
prev_sx = px; prev_sy = sy; prev_valid = true;
|
|
}
|
|
} else render_2d_implicit();
|
|
} else {
|
|
render_3d_axes();
|
|
if (is_explicit_3d) render_3d_explicit();
|
|
else render_3d_implicit();
|
|
}
|
|
|
|
if (graph_mode == MODE_3D && graph_czb) {
|
|
int total = graph_w * graph_h;
|
|
for (int i = 0; i < total; i++) {
|
|
graph_fb[i] = (uint32_t)(graph_czb[i] & 0xFFFFFFFF);
|
|
}
|
|
}
|
|
|
|
ui_draw_image(win_graph, 0, GRAPH_Y, graph_w, graph_h, graph_fb);
|
|
|
|
if (graph_mode == MODE_2D) {
|
|
double x_step = get_nice_step(view_x_max - view_x_min, 8);
|
|
double y_step = get_nice_step(view_y_max - view_y_min, 6);
|
|
|
|
int axis_y = screen_y_2d(0);
|
|
if (axis_y < 10) axis_y = 10;
|
|
if (axis_y > graph_h - 20) axis_y = graph_h - 20;
|
|
axis_y += GRAPH_Y;
|
|
|
|
double x_start = (int)(view_x_min / x_step) * x_step;
|
|
int safety = 0;
|
|
for (double wx = x_start - x_step; wx <= view_x_max + x_step && safety < 100; wx += x_step, safety++) {
|
|
if (fabs(wx) < x_step * 0.1) continue;
|
|
int sx = screen_x_2d(wx);
|
|
if (sx > 20 && sx < graph_w - 40) {
|
|
char buf[32]; double_to_str(wx, buf);
|
|
ui_draw_string(win_graph, sx - 10, axis_y + 4, buf, COLOR_TEXT);
|
|
}
|
|
}
|
|
|
|
int axis_x = screen_x_2d(0);
|
|
if (axis_x < 5) axis_x = 5;
|
|
if (axis_x > graph_w - 40) axis_x = graph_w - 40;
|
|
|
|
double y_start = (int)(view_y_min / y_step) * y_step;
|
|
safety = 0;
|
|
for (double wy = y_start - y_step; wy <= view_y_max + y_step && safety < 100; wy += y_step, safety++) {
|
|
if (fabs(wy) < y_step * 0.1) continue;
|
|
int sy = screen_y_2d(wy);
|
|
if (sy > 20 && sy < graph_h - 20) {
|
|
char buf[32]; double_to_str(wy, buf);
|
|
ui_draw_string(win_graph, axis_x + 6, sy + GRAPH_Y - 5, buf, COLOR_TEXT);
|
|
}
|
|
}
|
|
|
|
int zx = screen_x_2d(0), zy = screen_y_2d(0);
|
|
if (zx >= 0 && zx < graph_w - 10 && zy >= 0 && zy < graph_h - 10) {
|
|
ui_draw_string(win_graph, zx + 4, zy + GRAPH_Y + 4, "0", COLOR_TEXT);
|
|
}
|
|
}
|
|
}
|
|
|
|
// =====
|
|
// Paint
|
|
// =====
|
|
static void paint_all(void) {
|
|
rot_cx = cos(rot_x); rot_sx = sin(rot_x);
|
|
rot_cy = cos(rot_y); rot_sy = sin(rot_y);
|
|
|
|
ui_draw_rect(win_graph, 0, 0, win_w, TOOLBAR_H, COLOR_TOOLBAR_BG);
|
|
widget_textbox_draw(&wctx, &tb_equation);
|
|
widget_button_draw(&wctx, &btn_plot);
|
|
widget_button_draw(&wctx, &btn_mode);
|
|
ui_draw_rounded_rect_filled(win_graph, win_w - 80, 4, 70, 22, 4, 0xFF3A3A5A);
|
|
ui_draw_string(win_graph, win_w - 72, 8, "Presets", COLOR_DARK_TEXT);
|
|
render_graph();
|
|
|
|
// Status bar
|
|
int sty = GRAPH_Y + graph_h;
|
|
ui_draw_rect(win_graph, 0, sty, win_w, STATUSBAR_H, COLOR_STATUS_BG);
|
|
ui_draw_string(win_graph, 10, sty + 8,
|
|
graph_mode == MODE_3D ? "3D | CPU INTENSIVE!!" :
|
|
"2D | Scroll=Zoom", COLOR_TEXT);
|
|
|
|
char range_buf[64];
|
|
if (graph_mode == MODE_2D) {
|
|
strcpy(range_buf, "x:[");
|
|
char tmp[16]; double_to_str(view_x_min, tmp); strcat(range_buf, tmp);
|
|
strcat(range_buf, ",");
|
|
double_to_str(view_x_max, tmp); strcat(range_buf, tmp);
|
|
strcat(range_buf, "]");
|
|
ui_draw_string(win_graph, win_w - 150, sty + 8, range_buf, COLOR_TEXT);
|
|
} else {
|
|
strcpy(range_buf, "Range:");
|
|
char tmp[16]; double_to_str(range_3d, tmp); strcat(range_buf, tmp);
|
|
ui_draw_string(win_graph, win_w - 200, sty + 8, range_buf, COLOR_TEXT);
|
|
widget_button_draw(&wctx, &btn_range_minus);
|
|
widget_button_draw(&wctx, &btn_range_plus);
|
|
}
|
|
|
|
if (presets_open) {
|
|
int px = win_w - 150, py = TOOLBAR_H;
|
|
ui_draw_rounded_rect_filled(win_graph, px, py, 140, NUM_PRESETS * 20 + 4, 4, COLOR_DARK_PANEL);
|
|
for (int i = 0; i < NUM_PRESETS; i++) {
|
|
ui_draw_string(win_graph, px + 8, py + 4 + i * 20, preset_labels[i], COLOR_DARK_TEXT);
|
|
}
|
|
}
|
|
|
|
ui_mark_dirty(win_graph, 0, 0, win_w, CLIENT_H);
|
|
surface_needs_eval = false;
|
|
}
|
|
|
|
// ====
|
|
// Zoom
|
|
// ====
|
|
static void reset_view(void) {
|
|
if (graph_mode == MODE_2D) {
|
|
view_x_min = -10.0; view_x_max = 10.0;
|
|
view_y_min = -6.4; view_y_max = 6.4;
|
|
apply_aspect_ratio();
|
|
} else {
|
|
zoom_3d = 1.0;
|
|
rot_x = -0.5;
|
|
rot_y = 0.5;
|
|
range_3d = 10.0;
|
|
}
|
|
}
|
|
|
|
static void zoom_2d(double factor) {
|
|
if (factor <= 0) return;
|
|
|
|
double cx = (view_x_min + view_x_max) / 2.0;
|
|
double cy = (view_y_min + view_y_max) / 2.0;
|
|
|
|
double half_x = (view_x_max - view_x_min) / 2.0;
|
|
double half_y = (view_y_max - view_y_min) / 2.0;
|
|
|
|
half_x *= factor;
|
|
half_y *= factor;
|
|
|
|
// Safety caps for zoom range
|
|
if (half_x < 1e-12) half_x = 1e-12;
|
|
if (half_x > 1e12) half_x = 1e12;
|
|
if (half_y < 1e-12) half_y = 1e-12;
|
|
if (half_y > 1e12) half_y = 1e12;
|
|
|
|
view_x_min = cx - half_x;
|
|
view_x_max = cx + half_x;
|
|
view_y_min = cy - half_y;
|
|
view_y_max = cy + half_y;
|
|
|
|
apply_aspect_ratio();
|
|
}
|
|
|
|
static void handle_scroll(int dz) {
|
|
if (graph_mode == MODE_2D) {
|
|
surface_needs_eval = true;
|
|
if (dz > 0) zoom_2d(0.85);
|
|
else zoom_2d(1.18);
|
|
} else {
|
|
if (dz > 0) zoom_3d *= 1.15;
|
|
else zoom_3d *= 0.87;
|
|
}
|
|
}
|
|
|
|
static void update_widget_layout(void) {
|
|
tb_equation.w = win_w - 220;
|
|
btn_plot.x = win_w - 200;
|
|
widget_button_init(&btn_mode, win_w - 145, 4, 60, 22, filled_mode ? "Wire" : "Filled");
|
|
|
|
int sty = win_h - STATUSBAR_H - 20;
|
|
widget_button_init(&btn_range_minus, win_w - 95, sty + 4, 30, 22, "-");
|
|
widget_button_init(&btn_range_plus, win_w - 55, sty + 4, 30, 22, "+");
|
|
}
|
|
|
|
// ====
|
|
// Main
|
|
// ====
|
|
int main(void) {
|
|
win_graph = ui_window_create("Grapher", 80, 60, win_w, win_h);
|
|
ui_window_set_resizable(win_graph, true);
|
|
wctx.user_data = (void *)win_graph;
|
|
|
|
fb_capacity = graph_w * graph_h;
|
|
graph_fb = (uint32_t *)malloc(fb_capacity * sizeof(uint32_t));
|
|
graph_zb = (int32_t *)malloc(fb_capacity * sizeof(int32_t));
|
|
graph_czb = (uint64_t *)malloc(fb_capacity * sizeof(uint64_t));
|
|
if (!graph_fb || !graph_zb || !graph_czb) return 1;
|
|
|
|
memset(eq_buffer, 0, sizeof(eq_buffer));
|
|
strcpy(eq_buffer, "y = sin(x)");
|
|
eq_len = strlen(eq_buffer);
|
|
|
|
widget_textbox_init(&tb_equation, 10, 4, 340, 22, eq_buffer, 255);
|
|
tb_equation.cursor_pos = eq_len;
|
|
widget_button_init(&btn_plot, 360, 4, 50, 22, "Plot");
|
|
update_widget_layout();
|
|
|
|
// Parse initial equation
|
|
parse_equation();
|
|
|
|
paint_all();
|
|
|
|
gui_event_t ev;
|
|
bool needs_repaint = false;
|
|
while (1) {
|
|
bool got_event = false;
|
|
while (ui_get_event(win_graph, &ev)) {
|
|
got_event = true;
|
|
if (ev.type == GUI_EVENT_CLOSE) {
|
|
sys_exit(0);
|
|
} else if (ev.type == GUI_EVENT_PAINT) {
|
|
needs_repaint = true;
|
|
} else if (ev.type == GUI_EVENT_KEY) {
|
|
char c = (char)ev.arg1;
|
|
if (tb_equation.focused) {
|
|
if (c == '\n') {
|
|
eq_len = strlen(eq_buffer);
|
|
parse_equation();
|
|
if (graph_mode == MODE_2D) autofit_2d_view();
|
|
needs_repaint = true;
|
|
} else {
|
|
widget_textbox_handle_key(&tb_equation, c, NULL);
|
|
eq_len = strlen(eq_buffer);
|
|
needs_repaint = true;
|
|
}
|
|
} else if ((c == 'r' || c == 'R') && ev.arg3 == 1) {
|
|
reset_view();
|
|
surface_needs_eval = true;
|
|
needs_repaint = true;
|
|
} else if ((c == 'f' || c == 'F')) {
|
|
filled_mode = !filled_mode;
|
|
update_widget_layout();
|
|
needs_repaint = true;
|
|
}
|
|
} else if (ev.type == GUI_EVENT_RESIZE) {
|
|
win_w = ev.arg1;
|
|
win_h = ev.arg2;
|
|
graph_w = win_w;
|
|
graph_h = (win_h - 20) - TOOLBAR_H - STATUSBAR_H;
|
|
if (graph_h < 50) graph_h = 50;
|
|
|
|
int req_cap = graph_w * graph_h;
|
|
if (req_cap > fb_capacity) {
|
|
if (graph_fb) free(graph_fb);
|
|
if (graph_zb) free(graph_zb);
|
|
if (graph_czb) free(graph_czb);
|
|
fb_capacity = (int)(req_cap * 1.5);
|
|
graph_fb = (uint32_t *)malloc(fb_capacity * sizeof(uint32_t));
|
|
graph_zb = (int32_t *)malloc(fb_capacity * sizeof(int32_t));
|
|
graph_czb = (uint64_t *)malloc(fb_capacity * sizeof(uint64_t));
|
|
}
|
|
|
|
update_widget_layout();
|
|
|
|
if (graph_mode == MODE_2D) {
|
|
apply_aspect_ratio();
|
|
}
|
|
surface_needs_eval = true;
|
|
needs_repaint = true;
|
|
} else if (ev.type == GUI_EVENT_CLICK) {
|
|
int mx = ev.arg1, my = ev.arg2;
|
|
|
|
if (presets_open) {
|
|
int px = win_w - 150, py = TOOLBAR_H;
|
|
if (mx >= px && mx < px + 140 && my >= py && my < py + NUM_PRESETS * 20 + 4) {
|
|
int idx = (my - py - 2) / 20;
|
|
if (idx >= 0 && idx < NUM_PRESETS) {
|
|
strcpy(eq_buffer, preset_labels[idx]);
|
|
eq_len = strlen(eq_buffer);
|
|
tb_equation.cursor_pos = eq_len;
|
|
parse_equation();
|
|
if (graph_mode == MODE_2D) autofit_2d_view();
|
|
}
|
|
}
|
|
presets_open = false;
|
|
needs_repaint = true;
|
|
continue;
|
|
}
|
|
|
|
if (mx >= win_w - 80 && mx < win_w - 10 && my >= 4 && my < 26) {
|
|
presets_open = !presets_open;
|
|
needs_repaint = true;
|
|
continue;
|
|
}
|
|
|
|
if (widget_button_handle_mouse(&btn_plot, mx, my, false, true, NULL)) {
|
|
parse_equation();
|
|
if (graph_mode == MODE_2D) autofit_2d_view();
|
|
needs_repaint = true;
|
|
continue;
|
|
}
|
|
|
|
if (widget_button_handle_mouse(&btn_mode, mx, my, false, true, NULL)) {
|
|
filled_mode = !filled_mode;
|
|
update_widget_layout();
|
|
needs_repaint = true;
|
|
continue;
|
|
}
|
|
|
|
if (graph_mode == MODE_3D) {
|
|
if (widget_button_handle_mouse(&btn_range_plus, mx, my, false, true, NULL)) {
|
|
range_3d *= 1.25;
|
|
surface_needs_eval = true;
|
|
needs_repaint = true;
|
|
continue;
|
|
}
|
|
if (widget_button_handle_mouse(&btn_range_minus, mx, my, false, true, NULL)) {
|
|
range_3d *= 0.8;
|
|
surface_needs_eval = true;
|
|
needs_repaint = true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
widget_textbox_handle_mouse(&tb_equation, mx, my, true, NULL);
|
|
needs_repaint = true;
|
|
|
|
} else if (ev.type == GUI_EVENT_MOUSE_DOWN) {
|
|
int mx = ev.arg1, my = ev.arg2;
|
|
widget_button_handle_mouse(&btn_plot, mx, my, true, false, NULL);
|
|
widget_button_handle_mouse(&btn_mode, mx, my, true, false, NULL);
|
|
if (graph_mode == MODE_3D) {
|
|
widget_button_handle_mouse(&btn_range_plus, mx, my, true, false, NULL);
|
|
widget_button_handle_mouse(&btn_range_minus, mx, my, true, false, NULL);
|
|
}
|
|
needs_repaint = true;
|
|
|
|
} else if (ev.type == GUI_EVENT_MOUSE_UP) {
|
|
int mx = ev.arg1, my = ev.arg2;
|
|
widget_button_handle_mouse(&btn_plot, mx, my, false, false, NULL);
|
|
widget_button_handle_mouse(&btn_mode, mx, my, false, false, NULL);
|
|
if (graph_mode == MODE_3D) {
|
|
widget_button_handle_mouse(&btn_range_plus, mx, my, false, false, NULL);
|
|
widget_button_handle_mouse(&btn_range_minus, mx, my, false, false, NULL);
|
|
}
|
|
needs_repaint = true;
|
|
|
|
} else if (ev.type == GUI_EVENT_RIGHT_CLICK) {
|
|
if (graph_mode == MODE_3D) {
|
|
right_dragging = true;
|
|
drag_last_x = ev.arg1;
|
|
drag_last_y = ev.arg2;
|
|
}
|
|
} else if (ev.type == GUI_EVENT_MOUSE_MOVE) {
|
|
int mx = ev.arg1, my = ev.arg2;
|
|
int buttons = ev.arg3;
|
|
|
|
if (graph_mode == MODE_3D && (buttons & 2)) {
|
|
double dx = mx - drag_last_x;
|
|
double dy = my - drag_last_y;
|
|
rot_y += dx * 0.01;
|
|
rot_x += dy * 0.01;
|
|
drag_last_x = mx;
|
|
drag_last_y = my;
|
|
needs_repaint = true;
|
|
} else {
|
|
right_dragging = false;
|
|
}
|
|
} else if (ev.type == 9) {
|
|
handle_scroll(ev.arg1);
|
|
if (eq_valid) needs_repaint = true;
|
|
}
|
|
}
|
|
|
|
if (graph_mode == MODE_3D && ROTATE == 1) {
|
|
rot_y += 0.01;
|
|
needs_repaint = true;
|
|
}
|
|
|
|
if (needs_repaint) {
|
|
paint_all();
|
|
needs_repaint = false;
|
|
}
|
|
|
|
if (!got_event) {
|
|
sleep(16);
|
|
}
|
|
}
|
|
|
|
free(graph_fb);
|
|
sys_exit(0);
|
|
return 0;
|
|
}
|