1
0
Fork 0

Dropping balls MVP

This commit is contained in:
Pabloader 2025-12-16 14:38:43 +00:00
parent 33fde737ba
commit 99a306c10b
6 changed files with 391 additions and 3 deletions

View File

@ -0,0 +1,16 @@
#ifndef VEC2_H
#define VEC2_H
typedef struct vec2_t {
float x, y;
} vec2;
vec2 vec2_add(vec2 a, vec2 b);
vec2 vec2_sub(vec2 a, vec2 b);
float vec2_dot(vec2 a, vec2 b);
vec2 vec2_mul(vec2 x, float f);
vec2 vec2_div(vec2 x, float f);
float vec2_mag(vec2 x);
vec2 vec2_normalize(vec2 x);
#endif

43
build/assets/lib/vec2.c Normal file
View File

@ -0,0 +1,43 @@
#include "vec2.h"
#include <math.h>
vec2 vec2_add(vec2 a, vec2 b) {
return (vec2){
.x = a.x + b.x,
.y = a.y + b.y,
};
}
vec2 vec2_sub(vec2 a, vec2 b) {
return (vec2){
.x = a.x - b.x,
.y = a.y - b.y,
};
}
float vec2_dot(vec2 a, vec2 b) {
return a.x * b.x + a.y * b.y;
}
vec2 vec2_mul(vec2 x, float f) {
return (vec2){
.x = x.x * f,
.y = x.y * f,
};
}
vec2 vec2_div(vec2 x, float f) {
return (vec2){
.x = x.x / f,
.y = x.y / f,
};
}
float vec2_mag(vec2 x) {
return sqrtf(x.x * x.x + x.y * x.y);
}
vec2 vec2_normalize(vec2 x) {
float len = vec2_mag(x);
return vec2_div(x, len);
}

198
src/common/physics/engine.c Normal file
View File

@ -0,0 +1,198 @@
#include <js.h>
#include <math.h>
#include <memory.h>
#include <stdint.h>
#include <stdlib.h>
#include <vec2.h>
#define TYPE_CIRCLE 1
#define TYPE_PLANE 2
#define MAX_BODIES 1000
////////// Structs
typedef struct rigid_body_t {
// Public
uint8_t type;
uint8_t reserved[3];
vec2 pos;
vec2 vel;
float mass;
union {
struct {
float radius;
} circle;
struct {
vec2 normal;
} plane;
};
// Private
vec2 force;
struct rigid_body_t* prev;
struct rigid_body_t* next;
} rigid_body;
////////// Prototypes
void rigid_body_resolve_collision(rigid_body* rb);
int rigid_body_collide(rigid_body* a, rigid_body* b);
void rigid_body_handle_collision(rigid_body* a, rigid_body* b);
////////// Globals
static rigid_body* rigid_body_head = NULL;
static rigid_body* rigid_body_tail = NULL;
////////// Functions
rigid_body* rigid_body_new(float x, float y, float vx, float vy, float mass) {
rigid_body* rb = malloc(sizeof(rigid_body));
memset(rb, 0, sizeof(rigid_body));
rb->pos.x = x;
rb->pos.y = y;
rb->vel.x = vx;
rb->vel.y = vy;
rb->force.x = 0;
rb->force.y = 0;
rb->mass = mass;
rb->prev = NULL;
rb->next = NULL;
if (!rigid_body_head) {
rigid_body_head = rb;
rigid_body_tail = rb;
} else {
rigid_body_tail->next = rb;
rb->prev = rigid_body_tail;
rigid_body_tail = rb;
}
return rb;
}
EXPORT(rigid_body_new_circle) rigid_body* rigid_body_new_circle(float x, float y, float vx, float vy, float mass, float radius) {
rigid_body* rb = rigid_body_new(x, y, vx, vy, mass);
rb->type = TYPE_CIRCLE;
rb->circle.radius = radius;
return rb;
}
EXPORT(rigid_body_free) void rigid_body_free(rigid_body* rb) {
if (rb == rigid_body_head) {
rigid_body_head = rb->next;
}
if (rb == rigid_body_tail) {
rigid_body_tail = rb->prev;
}
if (rb->prev) {
rb->prev->next = rb->next;
}
if (rb->next) {
rb->next->prev = rb->prev;
}
free(rb);
}
EXPORT(rigid_body_update) void rigid_body_update(rigid_body* rb, float dt) {
if (!isinf(rb->mass)) {
rigid_body_resolve_collision(rb);
rb->vel.x += rb->force.x * dt / rb->mass;
rb->vel.y += rb->force.y * dt / rb->mass;
rb->pos.x += rb->vel.x * dt;
rb->pos.y += rb->vel.y * dt;
}
rb->force.x = 0;
rb->force.y = 0;
}
EXPORT(rigid_body_update_all) void rigid_body_update_all(float dt) {
rigid_body* current = rigid_body_head;
while (current) {
rigid_body_update(current, dt);
current = current->next;
}
}
EXPORT(rigid_body_add_force) void rigid_body_add_force(rigid_body* rb, float fx, float fy) {
rb->force.x += fx;
rb->force.y += fy;
}
void rigid_body_resolve_collision(rigid_body* rb) {
rigid_body* current = rigid_body_head;
while (current) {
if (current != rb && rigid_body_collide(rb, current)) {
rigid_body_handle_collision(rb, current);
}
current = current->next;
}
}
int rigid_body_collide(rigid_body* rb1, rigid_body* rb2) {
if (rb1->type == TYPE_CIRCLE && rb2->type == TYPE_CIRCLE) {
float dx = rb2->pos.x - rb1->pos.x;
float dy = rb2->pos.y - rb1->pos.y;
float distance = sqrtf(dx * dx + dy * dy);
return distance < rb1->circle.radius + rb2->circle.radius;
}
return 0; // Handle other types later
}
void rigid_body_handle_collision(rigid_body* rb1, rigid_body* rb2) {
if (rb1->type == TYPE_CIRCLE && rb2->type == TYPE_CIRCLE) {
vec2 d = vec2_sub(rb2->pos, rb1->pos);
vec2 n = vec2_normalize(d);
float distance = vec2_mag(d);
float overlap = rb1->circle.radius + rb2->circle.radius - distance;
vec2 n_overlap = vec2_mul(n, overlap / 2);
if (!isinf(rb1->mass)) {
rb1->pos = vec2_sub(rb1->pos, n_overlap);
}
if (!isinf(rb2->mass)) {
rb2->pos = vec2_add(rb2->pos, n_overlap);
}
float v1n = vec2_dot(rb1->vel, n);
float v2n = vec2_dot(rb2->vel, n);
float v1n_new = v1n;
float v2n_new = v2n;
if (!isinf(rb1->mass) && !isinf(rb2->mass)) {
v1n_new = v1n - 2.0f * (v1n - v2n) * rb1->mass / (rb1->mass + rb2->mass);
v2n_new = v2n + 2.0f * (v1n - v2n) * rb2->mass / (rb1->mass + rb2->mass);
} else if (isinf(rb1->mass)) {
v2n_new = -v2n;
} else if (isinf(rb2->mass)) {
v1n_new = -v1n;
} else {
return;
}
vec2 v1t = vec2_sub(rb1->vel, vec2_mul(n, v1n));
vec2 v2t = vec2_sub(rb2->vel, vec2_mul(n, v2n));
if (!isinf(rb1->mass)) {
rb1->vel = vec2_add(v1t, vec2_mul(n, v1n_new));
}
if (!isinf(rb2->mass)) {
rb2->vel = vec2_add(v2t, vec2_mul(n, v2n_new));
}
}
}

View File

@ -0,0 +1,60 @@
import E from './engine.c';
namespace Physics {
const TYPE_CIRCLE = 1;
const TYPE_PLANE = 2;
const bodies = new Set<number>();
export function newCircle(x: number, y: number, radius: number, mass: number = 1.0): number {
const circlePtr = E.rigid_body_new_circle(x, y, 0, 0, mass, radius);
bodies.add(circlePtr);
return circlePtr;
}
export function deleteBody(body: number) {
bodies.delete(body);
E.rigid_body_free(body);
}
export function addForce(body: number, fx: number, fy: number) {
E.rigid_body_add_force(body, fx, fy);
}
export function addGlobalForce(fx: number, fy: number) {
for (const body of bodies) {
E.rigid_body_add_force(body, fx, fy);
}
}
export function update(dt: number) {
E.rigid_body_update_all(dt);
}
export function getBody(body: number) {
const type = E.data.getUint8(body);
const x = E.data.getFloat32(body + 4, true);
const y = E.data.getFloat32(body + 8, true);
const vx = E.data.getFloat32(body + 12, true);
const vy = E.data.getFloat32(body + 16, true);
const mass = E.data.getFloat32(body + 20, true);
if (type === TYPE_CIRCLE) {
const radius = E.data.getFloat32(body + 24, true);
return { id: body, x, y, vx, vy, mass, radius };
} else if (type === TYPE_PLANE) {
const nx = E.data.getFloat32(body + 24, true);
const ny = E.data.getFloat32(body + 28, true);
return { id: body, x, y, vx, vy, mass, nx, ny };
}
return { id: body, x, y, vx, vy, mass };
}
export function getBodies() {
return Array.from(bodies).map(getBody);
}
}
export default Physics;

View File

@ -99,7 +99,7 @@ export async function commandInt16(char: BluetoothRemoteGATTCharacteristic, comm
await char.writeValue(formatInt16(command, data));
}
async function startPrint(char: BluetoothRemoteGATTCharacteristic, energy = 0xFF00) {
async function startPrint(char: BluetoothRemoteGATTCharacteristic) {
await commandInt16(char, Command.SetEnergy, 0xFF00);
await commandInt8(char, Command.SetQuality, 50);
await commandInt16(char, Command.SetSpeed, 32);
@ -113,14 +113,15 @@ async function endPrint(char: BluetoothRemoteGATTCharacteristic) {
await commandInt8(char, Command.GetState, 0);
}
export async function printBitmap(char: BluetoothRemoteGATTCharacteristic, bitmap: number[] | Uint8Array, energy = 0xFF00) {
await startPrint(char, energy);
export async function printBitmap(char: BluetoothRemoteGATTCharacteristic, bitmap: number[] | Uint8Array) {
await startPrint(char);
for (let i = 0; i < bitmap.length; i += MTU) {
const chunk = bitmap.slice(i, i + MTU);
await command(char, Command.DrawBitmap, chunk);
}
await endPrint(char);
// Wait for print to finish
const notify = await char.service.getCharacteristic(NOTIFY_CHARACTERISTIC);
await notify.startNotifications();
@ -137,4 +138,19 @@ export async function printBitmap(char: BluetoothRemoteGATTCharacteristic, bitma
await promise;
clearInterval(interval);
notify.removeEventListener('characteristicvaluechanged', handler);
}
export async function testPrint() {
const device = await navigator.bluetooth.requestDevice({
filters: [{ namePrefix: "MX" }],
optionalServices: [PRINTER_SERVICE],
});
if (!device?.gatt) return;
await device.gatt.connect();
const srv = await device.gatt.getPrimaryService(PRINTER_SERVICE);
const char = await srv.getCharacteristic(PRINT_CHARACTERISTIC);
await printBitmap(char, Array(PRINTER_WIDTH * 16).fill(0x55));
}

View File

@ -0,0 +1,55 @@
import { createCanvas } from "@common/display/canvas";
import { getRealPoint } from "@common/dom";
import { gameLoop } from "@common/game";
import Physics from "@common/physics";
const setup = () => {
const canvas = createCanvas(640, 480);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to get canvas context');
}
canvas.addEventListener('contextmenu', e => {
e.stopPropagation();
e.preventDefault();
})
canvas.addEventListener('mousedown', (event) => {
event.stopPropagation();
event.preventDefault();
return false;
});
canvas.addEventListener('mouseup', (event) => {
event.stopPropagation();
event.preventDefault();
const { x, y } = getRealPoint(canvas, event);
console.log(event.button);
const mass = event.button === 0 ? 1 : Number.POSITIVE_INFINITY;
const r = event.button === 0 ? 16 : 4;
Physics.newCircle(x, y, r, mass);
return false;
});
return { canvas, ctx };
}
const frame = (dt: number, state: ReturnType<typeof setup>) => {
Physics.addGlobalForce(0, 98);
Physics.update(dt);
const { ctx, canvas } = state;
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const { id, x, y, radius } of Physics.getBodies()) {
if (y >= canvas.height) {
Physics.deleteBody(id);
}
ctx.fillStyle = `hsl(${id % 360}, 100%, 50%)`;
if (radius) {
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
}
}
export default gameLoop(setup, frame);