From c014f29ce7dd5745674adeecc82c9e9c954b7538 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sun, 25 May 2025 01:58:10 -0500 Subject: [PATCH] fill out box2d methods --- examples/box2d_demo/config.js | 8 + examples/box2d_demo/main.js | 343 +++++ source/jsffi.c | 2 + source/qjs_box2d.c | 2416 +++++++++++++++++++++++++++++++-- tests/box2d.js | 95 +- 5 files changed, 2732 insertions(+), 132 deletions(-) create mode 100644 examples/box2d_demo/config.js create mode 100644 examples/box2d_demo/main.js diff --git a/examples/box2d_demo/config.js b/examples/box2d_demo/config.js new file mode 100644 index 00000000..ea5603bd --- /dev/null +++ b/examples/box2d_demo/config.js @@ -0,0 +1,8 @@ +var config = { + title: "Box2D Physics Demo", + width: 1280, + height: 720, + fullscreen: false +} + +return config \ No newline at end of file diff --git a/examples/box2d_demo/main.js b/examples/box2d_demo/main.js new file mode 100644 index 00000000..7ddd3170 --- /dev/null +++ b/examples/box2d_demo/main.js @@ -0,0 +1,343 @@ +var box2d = use('box2d') +var moth = use('moth', $_.delay) +moth.initialize() +var draw2d = use('draw2d') + +// Physics world setup +var world = new box2d.World({ + gravity: {x: 0, y: -10} +}) + +// Ground body (static) +var ground = world.createBody({ + type: 'static', + position: {x: 0, y: -10} +}) + +var groundShape = ground.createBoxShape({ + width: 50, + height: 0.5, + density: 0, + friction: 0.7 +}) + +// Walls +var leftWall = world.createBody({ + type: 'static', + position: {x: -25, y: 0} +}) +leftWall.createBoxShape({ + width: 0.5, + height: 30, + density: 0 +}) + +var rightWall = world.createBody({ + type: 'static', + position: {x: 25, y: 0} +}) +rightWall.createBoxShape({ + width: 0.5, + height: 30, + density: 0 +}) + +// Dynamic bodies array +var boxes = [] +var circles = [] + +// Create some dynamic boxes +for (var i = 0; i < 5; i++) { + var box = world.createBody({ + type: 'dynamic', + position: {x: -10 + i * 4, y: 5 + i * 3}, + angle: Math.random() * Math.PI + }) + + box.createBoxShape({ + width: 2, + height: 2, + density: 1.0, + friction: 0.3, + restitution: 0.5 + }) + + boxes.push(box) +} + +// Create some circles +for (var i = 0; i < 3; i++) { + var circle = world.createBody({ + type: 'dynamic', + position: {x: 5 + i * 3, y: 10 + i * 2} + }) + + circle.createCircleShape({ + radius: 1, + density: 0.5, + friction: 0.2, + restitution: 0.8 + }) + + circles.push(circle) +} + +// Connected bodies with distance joint +var bodyA = world.createBody({ + type: 'dynamic', + position: {x: -5, y: 15} +}) +bodyA.createCircleShape({ + radius: 0.5, + density: 1.0 +}) + +var bodyB = world.createBody({ + type: 'dynamic', + position: {x: 0, y: 15} +}) +bodyB.createCircleShape({ + radius: 0.5, + density: 1.0 +}) + +var joint = world.createDistanceJoint({ + bodyA: bodyA, + bodyB: bodyB, + localAnchorA: {x: 0, y: 0}, + localAnchorB: {x: 0, y: 0}, + length: 5 +}) + +// Mouse interaction +var mouseBody = null +var mousePressed = false + +// Game state +var camera = {x: 0, y: 0, zoom: 10} + +// Input state +var keys = {} +var mouse = { + pos: {x: 0, y: 0}, + buttons: [false, false, false] +} + +// Main update function +prosperon.on('update', function(dt) { + // Step physics simulation + world.step(dt, 4) + + // Camera controls + if (keys[4]) camera.x -= 20 * dt // A + if (keys[7]) camera.x += 20 * dt // D + if (keys[26]) camera.y += 20 * dt // W + if (keys[22]) camera.y -= 20 * dt // S + if (keys[20]) camera.zoom *= 1 + dt // Q + if (keys[8]) camera.zoom *= 1 - dt // E + + // Mouse interaction + var mouseWorld = screenToWorld(mouse.pos) + // Raycast to find body under mouse + var result = world.rayCast(mouseWorld, {x: 0, y: -1}, 0.1) + + if (!result.hit) { + // Create new body at mouse position + if (keys[225]) { // LSHIFT + // Create circle + var newCircle = world.createBody({ + type: 'dynamic', + position: mouseWorld + }) + newCircle.createCircleShape({ + radius: 0.5 + Math.random(), + density: 1.0, + restitution: 0.3 + Math.random() * 0.5 + }) + circles.push(newCircle) + } else { + // Create box + var newBox = world.createBody({ + type: 'dynamic', + position: mouseWorld, + angle: Math.random() * Math.PI * 2 + }) + newBox.createBoxShape({ + width: 1 + Math.random() * 2, + height: 1 + Math.random() * 2, + density: 1.0, + restitution: 0.2 + Math.random() * 0.3 + }) + boxes.push(newBox) + } + } + + boxes.forEach(function(box) { + var dir = { + x: box.position.x - mouseWorld.x, + y: box.position.y - mouseWorld.y + } + var dist = Math.sqrt(dir.x * dir.x + dir.y * dir.y) + if (dist < 10 && dist > 0.1) { + dir.x /= dist + dir.y /= dist + box.applyLinearImpulse({x: dir.x * 50, y: dir.y * 50}) + } + }) + + + // Reset with R + if (keys[21] && !keys[21 + '_prev']) { // R key pressed + // Reset all dynamic bodies + boxes.forEach(function(box) { + box.position = {x: Math.random() * 20 - 10, y: 10 + Math.random() * 10} + box.angle = Math.random() * Math.PI * 2 + box.linearVelocity = {x: 0, y: 0} + box.angularVelocity = 0 + }) + + circles.forEach(function(circle) { + circle.position = {x: Math.random() * 20 - 10, y: 10 + Math.random() * 10} + circle.linearVelocity = {x: 0, y: 0} + }) + } + + // Update previous key states + for (var k in keys) { + if (k.indexOf('_prev') === -1) { + keys[k + '_prev'] = keys[k] + } + } +}) + +// Event handlers +prosperon.on('key_down', function(e) { + keys[e.scancode] = true +}) + +prosperon.on('key_up', function(e) { + keys[e.scancode] = false +}) + +prosperon.on('mouse_button_down', function(e) { + mouse.buttons[e.which] = true + + if (e.which === 0 && !mousePressed) { + mousePressed = true + var mouseWorld = screenToWorld(mouse.pos) + + // Raycast to find body under mouse + var result = world.rayCast(mouseWorld, {x: 0, y: -1}, 0.1) + + if (!result.hit) { + // Create new body at mouse position + if (keys[225]) { // LSHIFT + // Create circle + var newCircle = world.createBody({ + type: 'dynamic', + position: mouseWorld + }) + newCircle.createCircleShape({ + radius: 0.5 + Math.random(), + density: 1.0, + restitution: 0.3 + Math.random() * 0.5 + }) + circles.push(newCircle) + } else { + // Create box + var newBox = world.createBody({ + type: 'dynamic', + position: mouseWorld, + angle: Math.random() * Math.PI * 2 + }) + newBox.createBoxShape({ + width: 1 + Math.random() * 2, + height: 1 + Math.random() * 2, + density: 1.0, + restitution: 0.2 + Math.random() * 0.3 + }) + boxes.push(newBox) + } + } + } + + if (e.which === 1) { + // Apply impulse with right click + var mouseWorld = screenToWorld(mouse.pos) + boxes.forEach(function(box) { + var dir = { + x: box.position.x - mouseWorld.x, + y: box.position.y - mouseWorld.y + } + var dist = Math.sqrt(dir.x * dir.x + dir.y * dir.y) + if (dist < 10 && dist > 0.1) { + dir.x /= dist + dir.y /= dist + box.applyLinearImpulse({x: dir.x * 50, y: dir.y * 50}) + } + }) + } +}) + +prosperon.on('mouse_button_up', function(e) { + mouse.buttons[e.which] = false + if (e.which === 0) { + mousePressed = false + } +}) + +prosperon.on('mouse_motion', function(e) { + mouse.pos = e.pos +}) + +// Rendering +prosperon.on('draw', function() { + // Clear background + + // Draw ground + drawBox(ground.position, 50, 0.5, ground.angle, {r: 0.5, g: 0.5, b: 0.5, a: 1}) + + // Draw walls + drawBox(leftWall.position, 0.5, 30, 0, {r: 0.5, g: 0.5, b: 0.5, a: 1}) + drawBox(rightWall.position, 0.5, 30, 0, {r: 0.5, g: 0.5, b: 0.5, a: 1}) + + // Draw boxes + boxes.forEach(function(box) { + drawBox(box.position, 2, 2, box.angle, {r: 0.8, g: 0.3, b: 0.3, a: 1}) + }) + + // Draw circles + circles.forEach(function(circle) { + draw2d.circle(circle.position, 1, {r: 0.3, g: 0.8, b: 0.3, a: 1}) + }) + + // Draw connected bodies + draw2d.circle(bodyA.position, 0.5, {r: 0.8, g: 0.8, b: 0.3, a: 1}) + draw2d.circle(bodyB.position, 0.5, {r: 0.8, g: 0.8, b: 0.3, a: 1}) + draw2d.line([bodyA.position, bodyB.position], {r: 1, g: 1, b: 0, a: 0.5}) + + // Draw UI + draw2d.text("Box2D Demo", {x: 10, y: 10}, 20, {r: 1, g: 1, b: 1, a: 1}) + draw2d.text("Controls:", {x: 10, y: 40}, 16, {r: 1, g: 1, b: 1, a: 1}) + draw2d.text("- WASD: Move camera", {x: 10, y: 60}, 14, {r: 1, g: 1, b: 1, a: 1}) + draw2d.text("- Q/E: Zoom in/out", {x: 10, y: 80}, 14, {r: 1, g: 1, b: 1, a: 1}) + draw2d.text("- Left click: Create box", {x: 10, y: 100}, 14, {r: 1, g: 1, b: 1, a: 1}) + draw2d.text("- Shift + Left click: Create circle", {x: 10, y: 120}, 14, {r: 1, g: 1, b: 1, a: 1}) + draw2d.text("- Right click: Apply impulse", {x: 10, y: 140}, 14, {r: 1, g: 1, b: 1, a: 1}) + draw2d.text("- R: Reset bodies", {x: 10, y: 160}, 14, {r: 1, g: 1, b: 1, a: 1}) + + // Show physics stats + draw2d.text("Bodies: " + (boxes.length + circles.length + 4), {x: 10, y: 200}, 14, {r: 1, g: 1, b: 1, a: 1}) +}) + +// Helper functions +function drawBox(pos, width, height, angle, color) { + draw2d.rectangle({x:pos.x,y:pos.y,width, height}) +} + +function screenToWorld(screenPos) { + return { + x: (screenPos.x - prosperon.x * 0.5) / camera.zoom + camera.x, + y: (screenPos.y - prosperon.y * 0.5) / camera.zoom + camera.y + } +} \ No newline at end of file diff --git a/source/jsffi.c b/source/jsffi.c index f96dcd9f..2e01791e 100644 --- a/source/jsffi.c +++ b/source/jsffi.c @@ -32,6 +32,7 @@ #include "qjs_dmon.h" #include "qjs_nota.h" #include "qjs_wota.h" +#include "qjs_box2d.h" #include "qjs_enet.h" #include "qjs_soloud.h" #include "qjs_qr.h" @@ -2896,6 +2897,7 @@ void ffi_load(JSContext *js) arrput(rt->module_registry, MISTLINE(enet)); arrput(rt->module_registry, MISTLINE(qr)); arrput(rt->module_registry, MISTLINE(wota)); + arrput(rt->module_registry, MISTLINE(box2d)); arrput(rt->module_registry, MISTLINE(crypto)); arrput(rt->module_registry, MISTLINE(blob)); arrput(rt->module_registry, MISTLINE(http)); diff --git a/source/qjs_box2d.c b/source/qjs_box2d.c index 5f8e2668..28d27471 100644 --- a/source/qjs_box2d.c +++ b/source/qjs_box2d.c @@ -1,160 +1,2329 @@ #include "qjs_box2d.h" +#include "qjs_macros.h" +#include "jsffi.h" #include "box2d/box2d.h" +#include "prosperon.h" #include +#include -#define countof(a) (sizeof(a)/sizeof(*(a))) +// Forward declarations +typedef struct box2d_world box2d_world; +typedef struct box2d_body box2d_body; +typedef struct box2d_shape box2d_shape; +typedef struct box2d_joint box2d_joint; -static JSClassID box2d_world_id; -static JSClassID box2d_body_id; - -static void box2d_world_finalizer(JSRuntime *rt, JSValue val) -{ - b2WorldId *id = JS_GetOpaque(val, box2d_world_id); - if (!id) return; - b2DestroyWorld(*id); -} - -static void box2d_body_finalizer(JSRuntime *rt, JSValue val) -{ - b2BodyId *id = JS_GetOpaque(val, box2d_body_id); - if (!id) return; - if (b2Body_IsValid(*id)) - b2DestroyBody(*id); - free(id); -} - -static JSClassDef box2d_world = { - "Box2d World", - .finalizer = box2d_world_finalizer +// World wrapper +struct box2d_world { + b2WorldId id; + JSContext *js; + JSValue contact_listener; + JSValue filter_callback; + JSValue pre_solve_callback; }; -static JSClassDef box2d_body = { - "Box2d Body", - .finalizer = box2d_body_finalizer +// Body wrapper +struct box2d_body { + b2BodyId id; + box2d_world *world; }; -static JSValue js_box2d_body_get_position(JSContext *js, JSValueConst self) { - b2BodyId *bid = JS_GetOpaque(self, box2d_body_id); - if (!b2Body_IsValid(*bid)) return JS_ThrowReferenceError(js, "Body is invalid"); - b2Vec2 pos = b2Body_GetPosition(*bid); - JSValue obj = JS_NewObject(js); - JS_SetPropertyStr(js, obj, "x", JS_NewFloat64(js, pos.x)); - JS_SetPropertyStr(js, obj, "y", JS_NewFloat64(js, pos.y)); - return obj; -} - -static JSValue js_box2d_body_get_angle(JSContext *js, JSValueConst self) { - b2BodyId *bid = JS_GetOpaque(self, box2d_body_id); - if (!b2Body_IsValid(*bid)) return JS_ThrowReferenceError(js, "Body is invalid"); - return JS_NewFloat64(js, b2Rot_GetAngle(b2Body_GetRotation(*bid))); -} - -static const JSCFunctionListEntry js_box2d_body_funcs[] = { - JS_CFUNC_DEF("getPosition", 0, js_box2d_body_get_position), - JS_CFUNC_DEF("getAngle", 0, js_box2d_body_get_angle) +// Shape wrapper +struct box2d_shape { + b2ShapeId id; + box2d_body *body; }; -JSValue js_box2d_world_body(JSContext *js, JSValueConst self, int argc, JSValueConst *argv) +// Joint wrapper +struct box2d_joint { + b2JointId id; + box2d_world *world; +}; + +// Free functions +void box2d_world_free(JSRuntime *rt, box2d_world *world) { - b2WorldId *wid = JS_GetOpaque(self, box2d_world_id); - if (!b2World_IsValid(*wid)) JS_ThrowReferenceError(js, "Cannot create body. World is invalid."); - b2BodyDef bdf = b2DefaultBodyDef(); - b2BodyId *bid = malloc(sizeof(*bid)); - *bid = b2CreateBody(*wid, &bdf); - JSValue obj = JS_NewObjectClass(js, box2d_body_id); - JS_SetOpaque(obj, bid); - return obj; + if (b2World_IsValid(world->id)) { + b2DestroyWorld(world->id); + } + if (!JS_IsUndefined(world->contact_listener)) + JS_FreeValueRT(rt, world->contact_listener); + if (!JS_IsUndefined(world->filter_callback)) + JS_FreeValueRT(rt, world->filter_callback); + if (!JS_IsUndefined(world->pre_solve_callback)) + JS_FreeValueRT(rt, world->pre_solve_callback); + free(world); } -JSValue js_box2d_world_step(JSContext *js, JSValueConst self, int argc, JSValueConst *argv) +void box2d_body_free(JSRuntime *rt, box2d_body *body) { - b2WorldId *wid = JS_GetOpaque(self, box2d_world_id); - if (!b2World_IsValid(*wid)) JS_ThrowReferenceError(js, "World is invalid."); - - float timestep; - int substepcount; - JS_ToFloat64(js, ×tep, argv[0]); - JS_ToInt32(js, &substepcount, argv[1]); - b2World_Step(*wid, timestep, substepcount); - return JS_UNDEFINED; + if (b2Body_IsValid(body->id)) { + b2DestroyBody(body->id); + } + free(body); } -JSValue js_box2d_world_get_gravity(JSContext *js, JSValueConst self) +void box2d_shape_free(JSRuntime *rt, box2d_shape *shape) { - b2WorldId *wid = JS_GetOpaque(self, box2d_world_id); - if (!b2World_IsValid(*wid)) return JS_ThrowReferenceError(js, "World is invalid"); - b2Vec2 grav = b2World_GetGravity(*wid); - JSValue obj = JS_NewObject(js); - JS_SetPropertyStr(js, obj, "x", JS_NewFloat64(js, grav.x)); - JS_SetPropertyStr(js, obj, "y", JS_NewFloat64(js, grav.y)); - return obj; + if (b2Shape_IsValid(shape->id)) { + b2DestroyShape(shape->id); + } + free(shape); } -JSValue js_box2d_world_set_gravity(JSContext *js, JSValueConst self, JSValue v) +void box2d_joint_free(JSRuntime *rt, box2d_joint *joint) { - b2WorldId *wid = JS_GetOpaque(self, box2d_world_id); - if (!b2World_IsValid(*wid)) return JS_ThrowReferenceError(js, "World is invalid"); - - JSValue x_val = JS_GetPropertyStr(js, v, "x"); - JSValue y_val = JS_GetPropertyStr(js, v, "y"); - - double x, y; - if (JS_ToFloat64(js, &x, x_val) || JS_ToFloat64(js, &y, y_val)) { + if (b2Joint_IsValid(joint->id)) { + b2DestroyJoint(joint->id); + } + free(joint); +} + +// Define classes using QJSCLASS macro +QJSCLASS(box2d_world,) +QJSCLASS(box2d_body,) +QJSCLASS(box2d_shape,) +QJSCLASS(box2d_joint,) + +// Vector conversion functions - renamed to avoid conflicts with existing js2vec2/vec22js +static b2Vec2 js2b2vec2(JSContext *js, JSValue v) +{ + b2Vec2 vec = {0, 0}; + if (JS_IsObject(v)) { + JSValue x_val = JS_GetPropertyStr(js, v, "x"); + JSValue y_val = JS_GetPropertyStr(js, v, "y"); + double x, y; + if (!JS_ToFloat64(js, &x, x_val)) vec.x = (float)x; + if (!JS_ToFloat64(js, &y, y_val)) vec.y = (float)y; JS_FreeValue(js, x_val); JS_FreeValue(js, y_val); - printf("FAILED\n"); - return JS_ThrowTypeError(js, "Gravity must have numeric x and y properties"); + } else if (JS_IsArray(js, v)) { + JSValue x_val = JS_GetPropertyUint32(js, v, 0); + JSValue y_val = JS_GetPropertyUint32(js, v, 1); + double x, y; + if (!JS_ToFloat64(js, &x, x_val)) vec.x = (float)x; + if (!JS_ToFloat64(js, &y, y_val)) vec.y = (float)y; + JS_FreeValue(js, x_val); + JS_FreeValue(js, y_val); + } + return vec; +} + +static JSValue b2vec22js(JSContext *js, b2Vec2 vec) +{ + JSValue obj = JS_NewObject(js); + JS_SetPropertyStr(js, obj, "x", JS_NewFloat64(js, vec.x)); + JS_SetPropertyStr(js, obj, "y", JS_NewFloat64(js, vec.y)); + return obj; +} + +// Transform conversion +static b2Transform js2transform(JSContext *js, JSValue v) +{ + b2Transform t; + t.p = (b2Vec2){0, 0}; + t.q = b2Rot_identity; + + if (JS_IsObject(v)) { + JSValue pos_val = JS_GetPropertyStr(js, v, "position"); + JSValue angle_val = JS_GetPropertyStr(js, v, "angle"); + + if (!JS_IsUndefined(pos_val)) { + t.p = js2b2vec2(js, pos_val); + } + + if (!JS_IsUndefined(angle_val)) { + double angle; + if (!JS_ToFloat64(js, &angle, angle_val)) { + t.q = b2MakeRot((float)angle); + } + } + + JS_FreeValue(js, pos_val); + JS_FreeValue(js, angle_val); } - JS_FreeValue(js, x_val); - JS_FreeValue(js, y_val); + return t; +} + +static JSValue transform2js(JSContext *js, b2Transform t) +{ + JSValue obj = JS_NewObject(js); + JS_SetPropertyStr(js, obj, "position", b2vec22js(js, t.p)); + JS_SetPropertyStr(js, obj, "angle", JS_NewFloat64(js, b2Rot_GetAngle(t.q))); + return obj; +} + +// Body type conversion +static b2BodyType js2bodytype(JSContext *js, JSValue v) +{ + if (JS_IsString(v)) { + const char *str = JS_ToCString(js, v); + b2BodyType type = b2_staticBody; + if (strcmp(str, "static") == 0) { + type = b2_staticBody; + } else if (strcmp(str, "kinematic") == 0) { + type = b2_kinematicBody; + } else if (strcmp(str, "dynamic") == 0) { + type = b2_dynamicBody; + } + JS_FreeCString(js, str); + return type; + } else if (JS_IsNumber(v)) { + int type = 0; + JS_ToInt32(js, &type, v); + return (b2BodyType)type; + } + return b2_staticBody; +} + +// ===== WORLD ===== + +// Constructor +JSC_CCALL(box2d_world_constructor, + b2WorldDef def = b2DefaultWorldDef(); - b2World_SetGravity(*wid, (b2Vec2){(float)x, (float)y}); + // Parse options if provided + if (argc > 0 && JS_IsObject(argv[0])) { + JSValue gravity_val = JS_GetPropertyStr(js, argv[0], "gravity"); + if (!JS_IsUndefined(gravity_val)) { + def.gravity = js2b2vec2(js, gravity_val); + JS_FreeValue(js, gravity_val); + } + + JSValue sleep_val = JS_GetPropertyStr(js, argv[0], "enableSleep"); + if (!JS_IsUndefined(sleep_val)) { + def.enableSleep = JS_ToBool(js, sleep_val); + JS_FreeValue(js, sleep_val); + } + } + + box2d_world *world = malloc(sizeof(*world)); + world->id = b2CreateWorld(&def); + world->js = js; + world->contact_listener = JS_UNDEFINED; + world->filter_callback = JS_UNDEFINED; + world->pre_solve_callback = JS_UNDEFINED; + + return box2d_world2js(js, world); +) + +// World.prototype.createBody() +JSC_CCALL(box2d_world_createBody, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + b2BodyDef def = b2DefaultBodyDef(); + + // Parse body definition + if (argc > 0 && JS_IsObject(argv[0])) { + JSValue type_val = JS_GetPropertyStr(js, argv[0], "type"); + if (!JS_IsUndefined(type_val)) { + def.type = js2bodytype(js, type_val); + JS_FreeValue(js, type_val); + } + + JSValue pos_val = JS_GetPropertyStr(js, argv[0], "position"); + if (!JS_IsUndefined(pos_val)) { + def.position = js2b2vec2(js, pos_val); + JS_FreeValue(js, pos_val); + } + + JSValue angle_val = JS_GetPropertyStr(js, argv[0], "angle"); + if (!JS_IsUndefined(angle_val)) { + double angle; + if (!JS_ToFloat64(js, &angle, angle_val)) { + def.rotation = b2MakeRot((float)angle); + } + JS_FreeValue(js, angle_val); + } + + JSValue vel_val = JS_GetPropertyStr(js, argv[0], "linearVelocity"); + if (!JS_IsUndefined(vel_val)) { + def.linearVelocity = js2b2vec2(js, vel_val); + JS_FreeValue(js, vel_val); + } + + JSValue angvel_val = JS_GetPropertyStr(js, argv[0], "angularVelocity"); + if (!JS_IsUndefined(angvel_val)) { + double av; + if (!JS_ToFloat64(js, &av, angvel_val)) { + def.angularVelocity = (float)av; + } + JS_FreeValue(js, angvel_val); + } + + JSValue awake_val = JS_GetPropertyStr(js, argv[0], "isAwake"); + if (!JS_IsUndefined(awake_val)) { + def.isAwake = JS_ToBool(js, awake_val); + JS_FreeValue(js, awake_val); + } + + JSValue enabled_val = JS_GetPropertyStr(js, argv[0], "isEnabled"); + if (!JS_IsUndefined(enabled_val)) { + def.isEnabled = JS_ToBool(js, enabled_val); + JS_FreeValue(js, enabled_val); + } + } + + box2d_body *body = malloc(sizeof(*body)); + body->id = b2CreateBody(world->id, &def); + body->world = world; + + return box2d_body2js(js, body); +) + +// World.prototype.step() +JSC_CCALL(box2d_world_step, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + double timestep = 1.0/60.0; + int substeps = 4; + + if (argc > 0) JS_ToFloat64(js, ×tep, argv[0]); + if (argc > 1) JS_ToInt32(js, &substeps, argv[1]); + + b2World_Step(world->id, (float)timestep, substeps); +) + +// World.prototype.rayCast() +JSC_CCALL(box2d_world_rayCast, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + if (argc < 2) return JS_ThrowTypeError(js, "rayCast requires origin and direction"); + + b2Vec2 origin = js2b2vec2(js, argv[0]); + b2Vec2 direction = js2b2vec2(js, argv[1]); + float maxFraction = 1.0f; + + if (argc > 2) { + double mf; + JS_ToFloat64(js, &mf, argv[2]); + maxFraction = (float)mf; + } + + b2QueryFilter filter = b2DefaultQueryFilter(); + b2RayResult result = b2World_CastRayClosest(world->id, origin, direction, filter); + + ret = JS_NewObject(js); + JS_SetPropertyStr(js, ret, "hit", JS_NewBool(js, result.hit)); + + if (result.hit) { + JS_SetPropertyStr(js, ret, "point", b2vec22js(js, result.point)); + JS_SetPropertyStr(js, ret, "normal", b2vec22js(js, result.normal)); + JS_SetPropertyStr(js, ret, "fraction", JS_NewFloat64(js, result.fraction)); + } + + return ret; +) + +// World.prototype.gravity getter +JSValue js_box2d_world_get_gravity(JSContext *js, JSValueConst self) +{ + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + b2Vec2 gravity = b2World_GetGravity(world->id); + return b2vec22js(js, gravity); +} + +// World.prototype.gravity setter +JSValue js_box2d_world_set_gravity(JSContext *js, JSValueConst self, JSValue val) +{ + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + b2Vec2 gravity = js2b2vec2(js, val); + b2World_SetGravity(world->id, gravity); return JS_UNDEFINED; } +// ===== BODY ===== + +// Body.prototype.createCircleShape() +JSC_CCALL(box2d_body_createCircleShape, + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + b2ShapeDef shape_def = b2DefaultShapeDef(); + b2Circle circle = {0}; + circle.center = (b2Vec2){0, 0}; + circle.radius = 0.5f; + + if (argc > 0 && JS_IsObject(argv[0])) { + JSValue radius_val = JS_GetPropertyStr(js, argv[0], "radius"); + if (!JS_IsUndefined(radius_val)) { + double r; + if (!JS_ToFloat64(js, &r, radius_val)) { + circle.radius = (float)r; + } + JS_FreeValue(js, radius_val); + } + + JSValue center_val = JS_GetPropertyStr(js, argv[0], "center"); + if (!JS_IsUndefined(center_val)) { + circle.center = js2b2vec2(js, center_val); + JS_FreeValue(js, center_val); + } + + JSValue density_val = JS_GetPropertyStr(js, argv[0], "density"); + if (!JS_IsUndefined(density_val)) { + double d; + if (!JS_ToFloat64(js, &d, density_val)) { + shape_def.density = (float)d; + } + JS_FreeValue(js, density_val); + } + + JSValue friction_val = JS_GetPropertyStr(js, argv[0], "friction"); + if (!JS_IsUndefined(friction_val)) { + double f; + if (!JS_ToFloat64(js, &f, friction_val)) { + shape_def.friction = (float)f; + } + JS_FreeValue(js, friction_val); + } + + JSValue restitution_val = JS_GetPropertyStr(js, argv[0], "restitution"); + if (!JS_IsUndefined(restitution_val)) { + double r; + if (!JS_ToFloat64(js, &r, restitution_val)) { + shape_def.restitution = (float)r; + } + JS_FreeValue(js, restitution_val); + } + + JSValue sensor_val = JS_GetPropertyStr(js, argv[0], "isSensor"); + if (!JS_IsUndefined(sensor_val)) { + shape_def.isSensor = JS_ToBool(js, sensor_val); + JS_FreeValue(js, sensor_val); + } + } + + box2d_shape *shape = malloc(sizeof(*shape)); + shape->id = b2CreateCircleShape(body->id, &shape_def, &circle); + shape->body = body; + + return box2d_shape2js(js, shape); +) + +// Body.prototype.createBoxShape() +JSC_CCALL(box2d_body_createBoxShape, + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + b2ShapeDef shape_def = b2DefaultShapeDef(); + float width = 1.0f, height = 1.0f; + b2Vec2 center = {0, 0}; + float angle = 0; + + if (argc > 0 && JS_IsObject(argv[0])) { + JSValue width_val = JS_GetPropertyStr(js, argv[0], "width"); + if (!JS_IsUndefined(width_val)) { + double w; + if (!JS_ToFloat64(js, &w, width_val)) { + width = (float)w; + } + JS_FreeValue(js, width_val); + } + + JSValue height_val = JS_GetPropertyStr(js, argv[0], "height"); + if (!JS_IsUndefined(height_val)) { + double h; + if (!JS_ToFloat64(js, &h, height_val)) { + height = (float)h; + } + JS_FreeValue(js, height_val); + } + + JSValue center_val = JS_GetPropertyStr(js, argv[0], "center"); + if (!JS_IsUndefined(center_val)) { + center = js2b2vec2(js, center_val); + JS_FreeValue(js, center_val); + } + + JSValue angle_val = JS_GetPropertyStr(js, argv[0], "angle"); + if (!JS_IsUndefined(angle_val)) { + double a; + if (!JS_ToFloat64(js, &a, angle_val)) { + angle = (float)a; + } + JS_FreeValue(js, angle_val); + } + + // Common shape properties + JSValue density_val = JS_GetPropertyStr(js, argv[0], "density"); + if (!JS_IsUndefined(density_val)) { + double d; + if (!JS_ToFloat64(js, &d, density_val)) { + shape_def.density = (float)d; + } + JS_FreeValue(js, density_val); + } + + JSValue friction_val = JS_GetPropertyStr(js, argv[0], "friction"); + if (!JS_IsUndefined(friction_val)) { + double f; + if (!JS_ToFloat64(js, &f, friction_val)) { + shape_def.friction = (float)f; + } + JS_FreeValue(js, friction_val); + } + + JSValue restitution_val = JS_GetPropertyStr(js, argv[0], "restitution"); + if (!JS_IsUndefined(restitution_val)) { + double r; + if (!JS_ToFloat64(js, &r, restitution_val)) { + shape_def.restitution = (float)r; + } + JS_FreeValue(js, restitution_val); + } + } + + b2Polygon polygon = b2MakeOffsetBox(width * 0.5f, height * 0.5f, center, angle); + + box2d_shape *shape = malloc(sizeof(*shape)); + shape->id = b2CreatePolygonShape(body->id, &shape_def, &polygon); + shape->body = body; + + return box2d_shape2js(js, shape); +) + +// Body.prototype.createSegmentShape() +JSC_CCALL(box2d_body_createSegmentShape, + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + b2ShapeDef shape_def = b2DefaultShapeDef(); + b2Segment segment; + + if (argc < 2) { + return JS_ThrowTypeError(js, "Segment shape requires two points"); + } + + segment.point1 = js2b2vec2(js, argv[0]); + segment.point2 = js2b2vec2(js, argv[1]); + + if (argc > 2 && JS_IsObject(argv[2])) { + JSValue density_val = JS_GetPropertyStr(js, argv[2], "density"); + if (!JS_IsUndefined(density_val)) { + double d; + if (!JS_ToFloat64(js, &d, density_val)) { + shape_def.density = (float)d; + } + JS_FreeValue(js, density_val); + } + + JSValue friction_val = JS_GetPropertyStr(js, argv[2], "friction"); + if (!JS_IsUndefined(friction_val)) { + double f; + if (!JS_ToFloat64(js, &f, friction_val)) { + shape_def.friction = (float)f; + } + JS_FreeValue(js, friction_val); + } + + JSValue restitution_val = JS_GetPropertyStr(js, argv[2], "restitution"); + if (!JS_IsUndefined(restitution_val)) { + double r; + if (!JS_ToFloat64(js, &r, restitution_val)) { + shape_def.restitution = (float)r; + } + JS_FreeValue(js, restitution_val); + } + } + + box2d_shape *shape = malloc(sizeof(*shape)); + shape->id = b2CreateSegmentShape(body->id, &shape_def, &segment); + shape->body = body; + + return box2d_shape2js(js, shape); +) + +// Body.prototype.createCapsuleShape() +JSC_CCALL(box2d_body_createCapsuleShape, + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + b2ShapeDef shape_def = b2DefaultShapeDef(); + b2Capsule capsule; + + if (argc > 0 && JS_IsObject(argv[0])) { + JSValue center1_val = JS_GetPropertyStr(js, argv[0], "center1"); + if (!JS_IsUndefined(center1_val)) { + capsule.center1 = js2b2vec2(js, center1_val); + JS_FreeValue(js, center1_val); + } else { + capsule.center1 = (b2Vec2){0, -0.5f}; + } + + JSValue center2_val = JS_GetPropertyStr(js, argv[0], "center2"); + if (!JS_IsUndefined(center2_val)) { + capsule.center2 = js2b2vec2(js, center2_val); + JS_FreeValue(js, center2_val); + } else { + capsule.center2 = (b2Vec2){0, 0.5f}; + } + + JSValue radius_val = JS_GetPropertyStr(js, argv[0], "radius"); + if (!JS_IsUndefined(radius_val)) { + double r; + if (!JS_ToFloat64(js, &r, radius_val)) { + capsule.radius = (float)r; + } + JS_FreeValue(js, radius_val); + } else { + capsule.radius = 0.5f; + } + + // Common shape properties + JSValue density_val = JS_GetPropertyStr(js, argv[0], "density"); + if (!JS_IsUndefined(density_val)) { + double d; + if (!JS_ToFloat64(js, &d, density_val)) { + shape_def.density = (float)d; + } + JS_FreeValue(js, density_val); + } + + JSValue friction_val = JS_GetPropertyStr(js, argv[0], "friction"); + if (!JS_IsUndefined(friction_val)) { + double f; + if (!JS_ToFloat64(js, &f, friction_val)) { + shape_def.friction = (float)f; + } + JS_FreeValue(js, friction_val); + } + + JSValue restitution_val = JS_GetPropertyStr(js, argv[0], "restitution"); + if (!JS_IsUndefined(restitution_val)) { + double r; + if (!JS_ToFloat64(js, &r, restitution_val)) { + shape_def.restitution = (float)r; + } + JS_FreeValue(js, restitution_val); + } + } else { + // Default capsule + capsule.center1 = (b2Vec2){0, -0.5f}; + capsule.center2 = (b2Vec2){0, 0.5f}; + capsule.radius = 0.5f; + } + + box2d_shape *shape = malloc(sizeof(*shape)); + shape->id = b2CreateCapsuleShape(body->id, &shape_def, &capsule); + shape->body = body; + + return box2d_shape2js(js, shape); +) + +// Body.prototype.createPolygonShape() +JSC_CCALL(box2d_body_createPolygonShape, + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + b2ShapeDef shape_def = b2DefaultShapeDef(); + b2Polygon polygon; + + if (argc < 1 || !JS_IsArray(js, argv[0])) { + return JS_ThrowTypeError(js, "Polygon shape requires an array of points"); + } + + uint32_t length; + JSValue len_val = JS_GetPropertyStr(js, argv[0], "length"); + if (JS_ToUint32(js, &length, len_val)) { + JS_FreeValue(js, len_val); + return JS_EXCEPTION; + } + JS_FreeValue(js, len_val); + + if (length < 3 || length > b2_maxPolygonVertices) { + return JS_ThrowRangeError(js, "Polygon must have between 3 and %d vertices", b2_maxPolygonVertices); + } + + b2Vec2 points[b2_maxPolygonVertices]; + for (uint32_t i = 0; i < length; i++) { + JSValue point_val = JS_GetPropertyUint32(js, argv[0], i); + points[i] = js2b2vec2(js, point_val); + JS_FreeValue(js, point_val); + } + + polygon = b2MakePolygon(points, (int)length); + + if (argc > 1 && JS_IsObject(argv[1])) { + JSValue density_val = JS_GetPropertyStr(js, argv[1], "density"); + if (!JS_IsUndefined(density_val)) { + double d; + if (!JS_ToFloat64(js, &d, density_val)) { + shape_def.density = (float)d; + } + JS_FreeValue(js, density_val); + } + + JSValue friction_val = JS_GetPropertyStr(js, argv[1], "friction"); + if (!JS_IsUndefined(friction_val)) { + double f; + if (!JS_ToFloat64(js, &f, friction_val)) { + shape_def.friction = (float)f; + } + JS_FreeValue(js, friction_val); + } + + JSValue restitution_val = JS_GetPropertyStr(js, argv[1], "restitution"); + if (!JS_IsUndefined(restitution_val)) { + double r; + if (!JS_ToFloat64(js, &r, restitution_val)) { + shape_def.restitution = (float)r; + } + JS_FreeValue(js, restitution_val); + } + } + + box2d_shape *shape = malloc(sizeof(*shape)); + shape->id = b2CreatePolygonShape(body->id, &shape_def, &polygon); + shape->body = body; + + return box2d_shape2js(js, shape); +) + +// Body.prototype.position getter +JSValue js_box2d_body_get_position(JSContext *js, JSValueConst self) +{ + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + b2Vec2 pos = b2Body_GetPosition(body->id); + return b2vec22js(js, pos); +} + +// Body.prototype.position setter +JSValue js_box2d_body_set_position(JSContext *js, JSValueConst self, JSValue val) +{ + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + b2Vec2 pos = js2b2vec2(js, val); + b2Rot rot = b2Body_GetRotation(body->id); + b2Body_SetTransform(body->id, pos, rot); + return JS_UNDEFINED; +} + +// Body.prototype.angle getter +JSValue js_box2d_body_get_angle(JSContext *js, JSValueConst self) +{ + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + b2Rot rot = b2Body_GetRotation(body->id); + return JS_NewFloat64(js, b2Rot_GetAngle(rot)); +} + +// Body.prototype.angle setter +JSValue js_box2d_body_set_angle(JSContext *js, JSValueConst self, JSValue val) +{ + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + double angle; + if (JS_ToFloat64(js, &angle, val)) return JS_EXCEPTION; + + b2Vec2 pos = b2Body_GetPosition(body->id); + b2Body_SetTransform(body->id, pos, b2MakeRot((float)angle)); + return JS_UNDEFINED; +} + +// Body.prototype.linearVelocity getter +JSValue js_box2d_body_get_linearVelocity(JSContext *js, JSValueConst self) +{ + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + b2Vec2 vel = b2Body_GetLinearVelocity(body->id); + return b2vec22js(js, vel); +} + +// Body.prototype.linearVelocity setter +JSValue js_box2d_body_set_linearVelocity(JSContext *js, JSValueConst self, JSValue val) +{ + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + b2Vec2 vel = js2b2vec2(js, val); + b2Body_SetLinearVelocity(body->id, vel); + return JS_UNDEFINED; +} + +// Body.prototype.angularVelocity getter +JSValue js_box2d_body_get_angularVelocity(JSContext *js, JSValueConst self) +{ + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + return JS_NewFloat64(js, b2Body_GetAngularVelocity(body->id)); +} + +// Body.prototype.angularVelocity setter +JSValue js_box2d_body_set_angularVelocity(JSContext *js, JSValueConst self, JSValue val) +{ + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + double av; + if (JS_ToFloat64(js, &av, val)) return JS_EXCEPTION; + + b2Body_SetAngularVelocity(body->id, (float)av); + return JS_UNDEFINED; +} + +// Body.prototype.type getter +JSValue js_box2d_body_get_type(JSContext *js, JSValueConst self) +{ + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + return JS_NewInt32(js, b2Body_GetType(body->id)); +} + +// Body.prototype.type setter +JSValue js_box2d_body_set_type(JSContext *js, JSValueConst self, JSValue val) +{ + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + b2Body_SetType(body->id, js2bodytype(js, val)); + return JS_UNDEFINED; +} + +// Body.prototype.getMass() +JSC_CCALL(box2d_body_getMass, + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + return JS_NewFloat64(js, b2Body_GetMass(body->id)); +) + +// Body.prototype.applyForce() +JSC_CCALL(box2d_body_applyForce, + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + b2Vec2 force = js2b2vec2(js, argv[0]); + b2Vec2 point = b2Body_GetWorldCenterOfMass(body->id); + + if (argc > 1) { + point = js2b2vec2(js, argv[1]); + } + + b2Body_ApplyForce(body->id, force, point, true); +) + +// Body.prototype.applyImpulse() +JSC_CCALL(box2d_body_applyImpulse, + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + b2Vec2 impulse = js2b2vec2(js, argv[0]); + b2Vec2 point = b2Body_GetWorldCenterOfMass(body->id); + + if (argc > 1) { + point = js2b2vec2(js, argv[1]); + } + + b2Body_ApplyLinearImpulse(body->id, impulse, point, true); +) + +// Body.prototype.applyLinearImpulse() - alias for applyImpulse +JSC_CCALL(box2d_body_applyLinearImpulse, + box2d_body *body = js2box2d_body(js, self); + if (!b2Body_IsValid(body->id)) return JS_ThrowReferenceError(js, "Body is invalid"); + + b2Vec2 impulse = js2b2vec2(js, argv[0]); + b2Vec2 point = b2Body_GetWorldCenterOfMass(body->id); + + if (argc > 1) { + point = js2b2vec2(js, argv[1]); + } + + b2Body_ApplyLinearImpulse(body->id, impulse, point, true); +) + +// ===== JOINT ===== + +// World.prototype.createDistanceJoint() +JSC_CCALL(box2d_world_createDistanceJoint, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + if (argc < 1 || !JS_IsObject(argv[0])) { + return JS_ThrowTypeError(js, "Joint definition object required"); + } + + b2DistanceJointDef def = b2DefaultDistanceJointDef(); + + JSValue bodyA_val = JS_GetPropertyStr(js, argv[0], "bodyA"); + JSValue bodyB_val = JS_GetPropertyStr(js, argv[0], "bodyB"); + + if (!JS_IsObject(bodyA_val) || !JS_IsObject(bodyB_val)) { + JS_FreeValue(js, bodyA_val); + JS_FreeValue(js, bodyB_val); + return JS_ThrowTypeError(js, "bodyA and bodyB are required"); + } + + box2d_body *bodyA = js2box2d_body(js, bodyA_val); + box2d_body *bodyB = js2box2d_body(js, bodyB_val); + JS_FreeValue(js, bodyA_val); + JS_FreeValue(js, bodyB_val); + + if (!bodyA || !bodyB) { + return JS_ThrowTypeError(js, "Invalid body objects"); + } + + def.bodyIdA = bodyA->id; + def.bodyIdB = bodyB->id; + + // Anchor points + JSValue anchorA_val = JS_GetPropertyStr(js, argv[0], "localAnchorA"); + if (!JS_IsUndefined(anchorA_val)) { + def.localAnchorA = js2b2vec2(js, anchorA_val); + JS_FreeValue(js, anchorA_val); + } + + JSValue anchorB_val = JS_GetPropertyStr(js, argv[0], "localAnchorB"); + if (!JS_IsUndefined(anchorB_val)) { + def.localAnchorB = js2b2vec2(js, anchorB_val); + JS_FreeValue(js, anchorB_val); + } + + // Length + JSValue length_val = JS_GetPropertyStr(js, argv[0], "length"); + if (!JS_IsUndefined(length_val)) { + double len; + if (!JS_ToFloat64(js, &len, length_val)) { + def.length = (float)len; + } + JS_FreeValue(js, length_val); + } + + // Spring settings + JSValue hertz_val = JS_GetPropertyStr(js, argv[0], "hertz"); + if (!JS_IsUndefined(hertz_val)) { + double hz; + if (!JS_ToFloat64(js, &hz, hertz_val)) { + def.hertz = (float)hz; + } + JS_FreeValue(js, hertz_val); + } + + JSValue damping_val = JS_GetPropertyStr(js, argv[0], "dampingRatio"); + if (!JS_IsUndefined(damping_val)) { + double d; + if (!JS_ToFloat64(js, &d, damping_val)) { + def.dampingRatio = (float)d; + } + JS_FreeValue(js, damping_val); + } + + box2d_joint *joint = malloc(sizeof(*joint)); + joint->id = b2CreateDistanceJoint(world->id, &def); + joint->world = world; + + return box2d_joint2js(js, joint); +) + +// World.prototype.createRevoluteJoint() +JSC_CCALL(box2d_world_createRevoluteJoint, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + if (argc < 1 || !JS_IsObject(argv[0])) { + return JS_ThrowTypeError(js, "Joint definition object required"); + } + + b2RevoluteJointDef def = b2DefaultRevoluteJointDef(); + + JSValue bodyA_val = JS_GetPropertyStr(js, argv[0], "bodyA"); + JSValue bodyB_val = JS_GetPropertyStr(js, argv[0], "bodyB"); + + if (!JS_IsObject(bodyA_val) || !JS_IsObject(bodyB_val)) { + JS_FreeValue(js, bodyA_val); + JS_FreeValue(js, bodyB_val); + return JS_ThrowTypeError(js, "bodyA and bodyB are required"); + } + + box2d_body *bodyA = js2box2d_body(js, bodyA_val); + box2d_body *bodyB = js2box2d_body(js, bodyB_val); + JS_FreeValue(js, bodyA_val); + JS_FreeValue(js, bodyB_val); + + if (!bodyA || !bodyB) { + return JS_ThrowTypeError(js, "Invalid body objects"); + } + + def.bodyIdA = bodyA->id; + def.bodyIdB = bodyB->id; + + // Anchor points + JSValue anchorA_val = JS_GetPropertyStr(js, argv[0], "localAnchorA"); + if (!JS_IsUndefined(anchorA_val)) { + def.localAnchorA = js2b2vec2(js, anchorA_val); + JS_FreeValue(js, anchorA_val); + } + + JSValue anchorB_val = JS_GetPropertyStr(js, argv[0], "localAnchorB"); + if (!JS_IsUndefined(anchorB_val)) { + def.localAnchorB = js2b2vec2(js, anchorB_val); + JS_FreeValue(js, anchorB_val); + } + + // Limits + JSValue enableLimit_val = JS_GetPropertyStr(js, argv[0], "enableLimit"); + if (!JS_IsUndefined(enableLimit_val)) { + def.enableLimit = JS_ToBool(js, enableLimit_val); + JS_FreeValue(js, enableLimit_val); + } + + JSValue lowerAngle_val = JS_GetPropertyStr(js, argv[0], "lowerAngle"); + if (!JS_IsUndefined(lowerAngle_val)) { + double angle; + if (!JS_ToFloat64(js, &angle, lowerAngle_val)) { + def.lowerAngle = (float)angle; + } + JS_FreeValue(js, lowerAngle_val); + } + + JSValue upperAngle_val = JS_GetPropertyStr(js, argv[0], "upperAngle"); + if (!JS_IsUndefined(upperAngle_val)) { + double angle; + if (!JS_ToFloat64(js, &angle, upperAngle_val)) { + def.upperAngle = (float)angle; + } + JS_FreeValue(js, upperAngle_val); + } + + // Motor + JSValue enableMotor_val = JS_GetPropertyStr(js, argv[0], "enableMotor"); + if (!JS_IsUndefined(enableMotor_val)) { + def.enableMotor = JS_ToBool(js, enableMotor_val); + JS_FreeValue(js, enableMotor_val); + } + + JSValue motorSpeed_val = JS_GetPropertyStr(js, argv[0], "motorSpeed"); + if (!JS_IsUndefined(motorSpeed_val)) { + double speed; + if (!JS_ToFloat64(js, &speed, motorSpeed_val)) { + def.motorSpeed = (float)speed; + } + JS_FreeValue(js, motorSpeed_val); + } + + JSValue maxMotorTorque_val = JS_GetPropertyStr(js, argv[0], "maxMotorTorque"); + if (!JS_IsUndefined(maxMotorTorque_val)) { + double torque; + if (!JS_ToFloat64(js, &torque, maxMotorTorque_val)) { + def.maxMotorTorque = (float)torque; + } + JS_FreeValue(js, maxMotorTorque_val); + } + + box2d_joint *joint = malloc(sizeof(*joint)); + joint->id = b2CreateRevoluteJoint(world->id, &def); + joint->world = world; + + return box2d_joint2js(js, joint); +) + +// World.prototype.createMotorJoint() +JSC_CCALL(box2d_world_createMotorJoint, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + if (argc < 1 || !JS_IsObject(argv[0])) { + return JS_ThrowTypeError(js, "Joint definition object required"); + } + + b2MotorJointDef def = b2DefaultMotorJointDef(); + + JSValue bodyA_val = JS_GetPropertyStr(js, argv[0], "bodyA"); + JSValue bodyB_val = JS_GetPropertyStr(js, argv[0], "bodyB"); + + if (!JS_IsObject(bodyA_val) || !JS_IsObject(bodyB_val)) { + JS_FreeValue(js, bodyA_val); + JS_FreeValue(js, bodyB_val); + return JS_ThrowTypeError(js, "bodyA and bodyB are required"); + } + + box2d_body *bodyA = js2box2d_body(js, bodyA_val); + box2d_body *bodyB = js2box2d_body(js, bodyB_val); + JS_FreeValue(js, bodyA_val); + JS_FreeValue(js, bodyB_val); + + if (!bodyA || !bodyB) { + return JS_ThrowTypeError(js, "Invalid body objects"); + } + + def.bodyIdA = bodyA->id; + def.bodyIdB = bodyB->id; + + JSValue linearOffset_val = JS_GetPropertyStr(js, argv[0], "linearOffset"); + if (!JS_IsUndefined(linearOffset_val)) { + def.linearOffset = js2b2vec2(js, linearOffset_val); + JS_FreeValue(js, linearOffset_val); + } + + JSValue angularOffset_val = JS_GetPropertyStr(js, argv[0], "angularOffset"); + if (!JS_IsUndefined(angularOffset_val)) { + double angle; + if (!JS_ToFloat64(js, &angle, angularOffset_val)) { + def.angularOffset = (float)angle; + } + JS_FreeValue(js, angularOffset_val); + } + + JSValue maxForce_val = JS_GetPropertyStr(js, argv[0], "maxForce"); + if (!JS_IsUndefined(maxForce_val)) { + double force; + if (!JS_ToFloat64(js, &force, maxForce_val)) { + def.maxForce = (float)force; + } + JS_FreeValue(js, maxForce_val); + } + + JSValue maxTorque_val = JS_GetPropertyStr(js, argv[0], "maxTorque"); + if (!JS_IsUndefined(maxTorque_val)) { + double torque; + if (!JS_ToFloat64(js, &torque, maxTorque_val)) { + def.maxTorque = (float)torque; + } + JS_FreeValue(js, maxTorque_val); + } + + JSValue correctionFactor_val = JS_GetPropertyStr(js, argv[0], "correctionFactor"); + if (!JS_IsUndefined(correctionFactor_val)) { + double factor; + if (!JS_ToFloat64(js, &factor, correctionFactor_val)) { + def.correctionFactor = (float)factor; + } + JS_FreeValue(js, correctionFactor_val); + } + + box2d_joint *joint = malloc(sizeof(*joint)); + joint->id = b2CreateMotorJoint(world->id, &def); + joint->world = world; + + return box2d_joint2js(js, joint); +) + +// World.prototype.createMouseJoint() +JSC_CCALL(box2d_world_createMouseJoint, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + if (argc < 1 || !JS_IsObject(argv[0])) { + return JS_ThrowTypeError(js, "Joint definition object required"); + } + + b2MouseJointDef def = b2DefaultMouseJointDef(); + + JSValue bodyA_val = JS_GetPropertyStr(js, argv[0], "bodyA"); + JSValue bodyB_val = JS_GetPropertyStr(js, argv[0], "bodyB"); + + if (!JS_IsObject(bodyA_val) || !JS_IsObject(bodyB_val)) { + JS_FreeValue(js, bodyA_val); + JS_FreeValue(js, bodyB_val); + return JS_ThrowTypeError(js, "bodyA and bodyB are required"); + } + + box2d_body *bodyA = js2box2d_body(js, bodyA_val); + box2d_body *bodyB = js2box2d_body(js, bodyB_val); + JS_FreeValue(js, bodyA_val); + JS_FreeValue(js, bodyB_val); + + if (!bodyA || !bodyB) { + return JS_ThrowTypeError(js, "Invalid body objects"); + } + + def.bodyIdA = bodyA->id; + def.bodyIdB = bodyB->id; + + JSValue target_val = JS_GetPropertyStr(js, argv[0], "target"); + if (!JS_IsUndefined(target_val)) { + def.target = js2b2vec2(js, target_val); + JS_FreeValue(js, target_val); + } + + JSValue hertz_val = JS_GetPropertyStr(js, argv[0], "hertz"); + if (!JS_IsUndefined(hertz_val)) { + double hz; + if (!JS_ToFloat64(js, &hz, hertz_val)) { + def.hertz = (float)hz; + } + JS_FreeValue(js, hertz_val); + } + + JSValue dampingRatio_val = JS_GetPropertyStr(js, argv[0], "dampingRatio"); + if (!JS_IsUndefined(dampingRatio_val)) { + double d; + if (!JS_ToFloat64(js, &d, dampingRatio_val)) { + def.dampingRatio = (float)d; + } + JS_FreeValue(js, dampingRatio_val); + } + + JSValue maxForce_val = JS_GetPropertyStr(js, argv[0], "maxForce"); + if (!JS_IsUndefined(maxForce_val)) { + double force; + if (!JS_ToFloat64(js, &force, maxForce_val)) { + def.maxForce = (float)force; + } + JS_FreeValue(js, maxForce_val); + } + + box2d_joint *joint = malloc(sizeof(*joint)); + joint->id = b2CreateMouseJoint(world->id, &def); + joint->world = world; + + return box2d_joint2js(js, joint); +) + +// World.prototype.createPrismaticJoint() +JSC_CCALL(box2d_world_createPrismaticJoint, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + if (argc < 1 || !JS_IsObject(argv[0])) { + return JS_ThrowTypeError(js, "Joint definition object required"); + } + + b2PrismaticJointDef def = b2DefaultPrismaticJointDef(); + + JSValue bodyA_val = JS_GetPropertyStr(js, argv[0], "bodyA"); + JSValue bodyB_val = JS_GetPropertyStr(js, argv[0], "bodyB"); + + if (!JS_IsObject(bodyA_val) || !JS_IsObject(bodyB_val)) { + JS_FreeValue(js, bodyA_val); + JS_FreeValue(js, bodyB_val); + return JS_ThrowTypeError(js, "bodyA and bodyB are required"); + } + + box2d_body *bodyA = js2box2d_body(js, bodyA_val); + box2d_body *bodyB = js2box2d_body(js, bodyB_val); + JS_FreeValue(js, bodyA_val); + JS_FreeValue(js, bodyB_val); + + if (!bodyA || !bodyB) { + return JS_ThrowTypeError(js, "Invalid body objects"); + } + + def.bodyIdA = bodyA->id; + def.bodyIdB = bodyB->id; + + JSValue localAnchorA_val = JS_GetPropertyStr(js, argv[0], "localAnchorA"); + if (!JS_IsUndefined(localAnchorA_val)) { + def.localAnchorA = js2b2vec2(js, localAnchorA_val); + JS_FreeValue(js, localAnchorA_val); + } + + JSValue localAnchorB_val = JS_GetPropertyStr(js, argv[0], "localAnchorB"); + if (!JS_IsUndefined(localAnchorB_val)) { + def.localAnchorB = js2b2vec2(js, localAnchorB_val); + JS_FreeValue(js, localAnchorB_val); + } + + JSValue localAxisA_val = JS_GetPropertyStr(js, argv[0], "localAxisA"); + if (!JS_IsUndefined(localAxisA_val)) { + def.localAxisA = js2b2vec2(js, localAxisA_val); + JS_FreeValue(js, localAxisA_val); + } + + JSValue enableLimit_val = JS_GetPropertyStr(js, argv[0], "enableLimit"); + if (!JS_IsUndefined(enableLimit_val)) { + def.enableLimit = JS_ToBool(js, enableLimit_val); + JS_FreeValue(js, enableLimit_val); + } + + JSValue lowerTranslation_val = JS_GetPropertyStr(js, argv[0], "lowerTranslation"); + if (!JS_IsUndefined(lowerTranslation_val)) { + double trans; + if (!JS_ToFloat64(js, &trans, lowerTranslation_val)) { + def.lowerTranslation = (float)trans; + } + JS_FreeValue(js, lowerTranslation_val); + } + + JSValue upperTranslation_val = JS_GetPropertyStr(js, argv[0], "upperTranslation"); + if (!JS_IsUndefined(upperTranslation_val)) { + double trans; + if (!JS_ToFloat64(js, &trans, upperTranslation_val)) { + def.upperTranslation = (float)trans; + } + JS_FreeValue(js, upperTranslation_val); + } + + JSValue enableMotor_val = JS_GetPropertyStr(js, argv[0], "enableMotor"); + if (!JS_IsUndefined(enableMotor_val)) { + def.enableMotor = JS_ToBool(js, enableMotor_val); + JS_FreeValue(js, enableMotor_val); + } + + JSValue motorSpeed_val = JS_GetPropertyStr(js, argv[0], "motorSpeed"); + if (!JS_IsUndefined(motorSpeed_val)) { + double speed; + if (!JS_ToFloat64(js, &speed, motorSpeed_val)) { + def.motorSpeed = (float)speed; + } + JS_FreeValue(js, motorSpeed_val); + } + + JSValue maxMotorForce_val = JS_GetPropertyStr(js, argv[0], "maxMotorForce"); + if (!JS_IsUndefined(maxMotorForce_val)) { + double force; + if (!JS_ToFloat64(js, &force, maxMotorForce_val)) { + def.maxMotorForce = (float)force; + } + JS_FreeValue(js, maxMotorForce_val); + } + + box2d_joint *joint = malloc(sizeof(*joint)); + joint->id = b2CreatePrismaticJoint(world->id, &def); + joint->world = world; + + return box2d_joint2js(js, joint); +) + +// World.prototype.createWeldJoint() +JSC_CCALL(box2d_world_createWeldJoint, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + if (argc < 1 || !JS_IsObject(argv[0])) { + return JS_ThrowTypeError(js, "Joint definition object required"); + } + + b2WeldJointDef def = b2DefaultWeldJointDef(); + + JSValue bodyA_val = JS_GetPropertyStr(js, argv[0], "bodyA"); + JSValue bodyB_val = JS_GetPropertyStr(js, argv[0], "bodyB"); + + if (!JS_IsObject(bodyA_val) || !JS_IsObject(bodyB_val)) { + JS_FreeValue(js, bodyA_val); + JS_FreeValue(js, bodyB_val); + return JS_ThrowTypeError(js, "bodyA and bodyB are required"); + } + + box2d_body *bodyA = js2box2d_body(js, bodyA_val); + box2d_body *bodyB = js2box2d_body(js, bodyB_val); + JS_FreeValue(js, bodyA_val); + JS_FreeValue(js, bodyB_val); + + if (!bodyA || !bodyB) { + return JS_ThrowTypeError(js, "Invalid body objects"); + } + + def.bodyIdA = bodyA->id; + def.bodyIdB = bodyB->id; + + JSValue localAnchorA_val = JS_GetPropertyStr(js, argv[0], "localAnchorA"); + if (!JS_IsUndefined(localAnchorA_val)) { + def.localAnchorA = js2b2vec2(js, localAnchorA_val); + JS_FreeValue(js, localAnchorA_val); + } + + JSValue localAnchorB_val = JS_GetPropertyStr(js, argv[0], "localAnchorB"); + if (!JS_IsUndefined(localAnchorB_val)) { + def.localAnchorB = js2b2vec2(js, localAnchorB_val); + JS_FreeValue(js, localAnchorB_val); + } + + JSValue referenceAngle_val = JS_GetPropertyStr(js, argv[0], "referenceAngle"); + if (!JS_IsUndefined(referenceAngle_val)) { + double angle; + if (!JS_ToFloat64(js, &angle, referenceAngle_val)) { + def.referenceAngle = (float)angle; + } + JS_FreeValue(js, referenceAngle_val); + } + + JSValue linearHertz_val = JS_GetPropertyStr(js, argv[0], "linearHertz"); + if (!JS_IsUndefined(linearHertz_val)) { + double hz; + if (!JS_ToFloat64(js, &hz, linearHertz_val)) { + def.linearHertz = (float)hz; + } + JS_FreeValue(js, linearHertz_val); + } + + JSValue angularHertz_val = JS_GetPropertyStr(js, argv[0], "angularHertz"); + if (!JS_IsUndefined(angularHertz_val)) { + double hz; + if (!JS_ToFloat64(js, &hz, angularHertz_val)) { + def.angularHertz = (float)hz; + } + JS_FreeValue(js, angularHertz_val); + } + + JSValue linearDampingRatio_val = JS_GetPropertyStr(js, argv[0], "linearDampingRatio"); + if (!JS_IsUndefined(linearDampingRatio_val)) { + double d; + if (!JS_ToFloat64(js, &d, linearDampingRatio_val)) { + def.linearDampingRatio = (float)d; + } + JS_FreeValue(js, linearDampingRatio_val); + } + + JSValue angularDampingRatio_val = JS_GetPropertyStr(js, argv[0], "angularDampingRatio"); + if (!JS_IsUndefined(angularDampingRatio_val)) { + double d; + if (!JS_ToFloat64(js, &d, angularDampingRatio_val)) { + def.angularDampingRatio = (float)d; + } + JS_FreeValue(js, angularDampingRatio_val); + } + + box2d_joint *joint = malloc(sizeof(*joint)); + joint->id = b2CreateWeldJoint(world->id, &def); + joint->world = world; + + return box2d_joint2js(js, joint); +) + +// World.prototype.createWheelJoint() +JSC_CCALL(box2d_world_createWheelJoint, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + if (argc < 1 || !JS_IsObject(argv[0])) { + return JS_ThrowTypeError(js, "Joint definition object required"); + } + + b2WheelJointDef def = b2DefaultWheelJointDef(); + + JSValue bodyA_val = JS_GetPropertyStr(js, argv[0], "bodyA"); + JSValue bodyB_val = JS_GetPropertyStr(js, argv[0], "bodyB"); + + if (!JS_IsObject(bodyA_val) || !JS_IsObject(bodyB_val)) { + JS_FreeValue(js, bodyA_val); + JS_FreeValue(js, bodyB_val); + return JS_ThrowTypeError(js, "bodyA and bodyB are required"); + } + + box2d_body *bodyA = js2box2d_body(js, bodyA_val); + box2d_body *bodyB = js2box2d_body(js, bodyB_val); + JS_FreeValue(js, bodyA_val); + JS_FreeValue(js, bodyB_val); + + if (!bodyA || !bodyB) { + return JS_ThrowTypeError(js, "Invalid body objects"); + } + + def.bodyIdA = bodyA->id; + def.bodyIdB = bodyB->id; + + JSValue localAnchorA_val = JS_GetPropertyStr(js, argv[0], "localAnchorA"); + if (!JS_IsUndefined(localAnchorA_val)) { + def.localAnchorA = js2b2vec2(js, localAnchorA_val); + JS_FreeValue(js, localAnchorA_val); + } + + JSValue localAnchorB_val = JS_GetPropertyStr(js, argv[0], "localAnchorB"); + if (!JS_IsUndefined(localAnchorB_val)) { + def.localAnchorB = js2b2vec2(js, localAnchorB_val); + JS_FreeValue(js, localAnchorB_val); + } + + JSValue localAxisA_val = JS_GetPropertyStr(js, argv[0], "localAxisA"); + if (!JS_IsUndefined(localAxisA_val)) { + def.localAxisA = js2b2vec2(js, localAxisA_val); + JS_FreeValue(js, localAxisA_val); + } + + JSValue enableLimit_val = JS_GetPropertyStr(js, argv[0], "enableLimit"); + if (!JS_IsUndefined(enableLimit_val)) { + def.enableLimit = JS_ToBool(js, enableLimit_val); + JS_FreeValue(js, enableLimit_val); + } + + JSValue lowerTranslation_val = JS_GetPropertyStr(js, argv[0], "lowerTranslation"); + if (!JS_IsUndefined(lowerTranslation_val)) { + double trans; + if (!JS_ToFloat64(js, &trans, lowerTranslation_val)) { + def.lowerTranslation = (float)trans; + } + JS_FreeValue(js, lowerTranslation_val); + } + + JSValue upperTranslation_val = JS_GetPropertyStr(js, argv[0], "upperTranslation"); + if (!JS_IsUndefined(upperTranslation_val)) { + double trans; + if (!JS_ToFloat64(js, &trans, upperTranslation_val)) { + def.upperTranslation = (float)trans; + } + JS_FreeValue(js, upperTranslation_val); + } + + JSValue enableMotor_val = JS_GetPropertyStr(js, argv[0], "enableMotor"); + if (!JS_IsUndefined(enableMotor_val)) { + def.enableMotor = JS_ToBool(js, enableMotor_val); + JS_FreeValue(js, enableMotor_val); + } + + JSValue motorSpeed_val = JS_GetPropertyStr(js, argv[0], "motorSpeed"); + if (!JS_IsUndefined(motorSpeed_val)) { + double speed; + if (!JS_ToFloat64(js, &speed, motorSpeed_val)) { + def.motorSpeed = (float)speed; + } + JS_FreeValue(js, motorSpeed_val); + } + + JSValue maxMotorTorque_val = JS_GetPropertyStr(js, argv[0], "maxMotorTorque"); + if (!JS_IsUndefined(maxMotorTorque_val)) { + double torque; + if (!JS_ToFloat64(js, &torque, maxMotorTorque_val)) { + def.maxMotorTorque = (float)torque; + } + JS_FreeValue(js, maxMotorTorque_val); + } + + JSValue hertz_val = JS_GetPropertyStr(js, argv[0], "hertz"); + if (!JS_IsUndefined(hertz_val)) { + double hz; + if (!JS_ToFloat64(js, &hz, hertz_val)) { + def.hertz = (float)hz; + } + JS_FreeValue(js, hertz_val); + } + + JSValue dampingRatio_val = JS_GetPropertyStr(js, argv[0], "dampingRatio"); + if (!JS_IsUndefined(dampingRatio_val)) { + double d; + if (!JS_ToFloat64(js, &d, dampingRatio_val)) { + def.dampingRatio = (float)d; + } + JS_FreeValue(js, dampingRatio_val); + } + + box2d_joint *joint = malloc(sizeof(*joint)); + joint->id = b2CreateWheelJoint(world->id, &def); + joint->world = world; + + return box2d_joint2js(js, joint); +) + +// World.prototype.setContactCallback() +JSC_CCALL(box2d_world_setContactCallback, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + if (argc > 0 && JS_IsFunction(js, argv[0])) { + // Store the callback + JS_FreeValue(js, world->contact_listener); + world->contact_listener = JS_DupValue(js, argv[0]); + } else { + // Clear the callback + JS_FreeValue(js, world->contact_listener); + world->contact_listener = JS_NULL; + } + + return JS_UNDEFINED; +) + +// World.prototype.setFilterCallback() +JSC_CCALL(box2d_world_setFilterCallback, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + if (argc > 0 && JS_IsFunction(js, argv[0])) { + // Store the callback + JS_FreeValue(js, world->filter_callback); + world->filter_callback = JS_DupValue(js, argv[0]); + } else { + // Clear the callback + JS_FreeValue(js, world->filter_callback); + world->filter_callback = JS_NULL; + } + + return JS_UNDEFINED; +) + +// World.prototype.setPreSolveCallback() +JSC_CCALL(box2d_world_setPreSolveCallback, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + if (argc > 0 && JS_IsFunction(js, argv[0])) { + // Store the callback + JS_FreeValue(js, world->pre_solve_callback); + world->pre_solve_callback = JS_DupValue(js, argv[0]); + } else { + // Clear the callback + JS_FreeValue(js, world->pre_solve_callback); + world->pre_solve_callback = JS_NULL; + } + + return JS_UNDEFINED; +) + +// World.prototype.setRestitutionThreshold() +JSC_CCALL(box2d_world_setRestitutionThreshold, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + if (argc < 1) return JS_ThrowTypeError(js, "Restitution threshold value required"); + + double threshold; + if (JS_ToFloat64(js, &threshold, argv[0])) return JS_EXCEPTION; + + b2World_SetRestitutionThreshold(world->id, (float)threshold); + return JS_UNDEFINED; +) + +// World.prototype.setHitEventThreshold() +JSC_CCALL(box2d_world_setHitEventThreshold, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + if (argc < 1) return JS_ThrowTypeError(js, "Hit event threshold value required"); + + double threshold; + if (JS_ToFloat64(js, &threshold, argv[0])) return JS_EXCEPTION; + + b2World_SetHitEventThreshold(world->id, (float)threshold); + return JS_UNDEFINED; +) + +// World.prototype.setContactTuning() +JSC_CCALL(box2d_world_setContactTuning, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + if (argc < 3) return JS_ThrowTypeError(js, "Contact tuning requires hertz, damping ratio, and push velocity"); + + double hertz, dampingRatio, pushVelocity; + if (JS_ToFloat64(js, &hertz, argv[0])) return JS_EXCEPTION; + if (JS_ToFloat64(js, &dampingRatio, argv[1])) return JS_EXCEPTION; + if (JS_ToFloat64(js, &pushVelocity, argv[2])) return JS_EXCEPTION; + + b2World_SetContactTuning(world->id, (float)hertz, (float)dampingRatio, (float)pushVelocity); + return JS_UNDEFINED; +) + +// World.prototype.getProfile() +JSC_CCALL(box2d_world_getProfile, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + b2Profile profile = b2World_GetProfile(world->id); + + JSValue profile_obj = JS_NewObject(js); + JS_SetPropertyStr(js, profile_obj, "step", JS_NewFloat64(js, profile.step)); + JS_SetPropertyStr(js, profile_obj, "pairs", JS_NewFloat64(js, profile.pairs)); + JS_SetPropertyStr(js, profile_obj, "collide", JS_NewFloat64(js, profile.collide)); + JS_SetPropertyStr(js, profile_obj, "solve", JS_NewFloat64(js, profile.solve)); + JS_SetPropertyStr(js, profile_obj, "buildIslands", JS_NewFloat64(js, profile.buildIslands)); + JS_SetPropertyStr(js, profile_obj, "solveConstraints", JS_NewFloat64(js, profile.solveConstraints)); + JS_SetPropertyStr(js, profile_obj, "prepareTasks", JS_NewFloat64(js, profile.prepareTasks)); + JS_SetPropertyStr(js, profile_obj, "solverTasks", JS_NewFloat64(js, profile.solverTasks)); + JS_SetPropertyStr(js, profile_obj, "prepareConstraints", JS_NewFloat64(js, profile.prepareConstraints)); + JS_SetPropertyStr(js, profile_obj, "integrateVelocities", JS_NewFloat64(js, profile.integrateVelocities)); + JS_SetPropertyStr(js, profile_obj, "warmStart", JS_NewFloat64(js, profile.warmStart)); + JS_SetPropertyStr(js, profile_obj, "solveVelocities", JS_NewFloat64(js, profile.solveVelocities)); + JS_SetPropertyStr(js, profile_obj, "integratePositions", JS_NewFloat64(js, profile.integratePositions)); + JS_SetPropertyStr(js, profile_obj, "relaxVelocities", JS_NewFloat64(js, profile.relaxVelocities)); + JS_SetPropertyStr(js, profile_obj, "applyRestitution", JS_NewFloat64(js, profile.applyRestitution)); + JS_SetPropertyStr(js, profile_obj, "storeImpulses", JS_NewFloat64(js, profile.storeImpulses)); + JS_SetPropertyStr(js, profile_obj, "finalizeBodies", JS_NewFloat64(js, profile.finalizeBodies)); + JS_SetPropertyStr(js, profile_obj, "splitIslands", JS_NewFloat64(js, profile.splitIslands)); + JS_SetPropertyStr(js, profile_obj, "sleepIslands", JS_NewFloat64(js, profile.sleepIslands)); + JS_SetPropertyStr(js, profile_obj, "hitEvents", JS_NewFloat64(js, profile.hitEvents)); + JS_SetPropertyStr(js, profile_obj, "broadphase", JS_NewFloat64(js, profile.broadphase)); + JS_SetPropertyStr(js, profile_obj, "continuous", JS_NewFloat64(js, profile.continuous)); + + return profile_obj; +) + +// World.prototype.getCounters() +JSC_CCALL(box2d_world_getCounters, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + b2Counters counters = b2World_GetCounters(world->id); + + JSValue counters_obj = JS_NewObject(js); + JS_SetPropertyStr(js, counters_obj, "staticBodyCount", JS_NewInt32(js, counters.staticBodyCount)); + JS_SetPropertyStr(js, counters_obj, "bodyCount", JS_NewInt32(js, counters.bodyCount)); + JS_SetPropertyStr(js, counters_obj, "shapeCount", JS_NewInt32(js, counters.shapeCount)); + JS_SetPropertyStr(js, counters_obj, "contactCount", JS_NewInt32(js, counters.contactCount)); + JS_SetPropertyStr(js, counters_obj, "jointCount", JS_NewInt32(js, counters.jointCount)); + JS_SetPropertyStr(js, counters_obj, "islandCount", JS_NewInt32(js, counters.islandCount)); + JS_SetPropertyStr(js, counters_obj, "stackUsed", JS_NewInt32(js, counters.stackUsed)); + JS_SetPropertyStr(js, counters_obj, "staticTreeHeight", JS_NewInt32(js, counters.staticTreeHeight)); + JS_SetPropertyStr(js, counters_obj, "treeHeight", JS_NewInt32(js, counters.treeHeight)); + JS_SetPropertyStr(js, counters_obj, "byteCount", JS_NewInt32(js, counters.byteCount)); + JS_SetPropertyStr(js, counters_obj, "taskCount", JS_NewInt32(js, counters.taskCount)); + JS_SetPropertyStr(js, counters_obj, "colorCounts", JS_NewArray(js)); + + JSValue colorArray = JS_GetPropertyStr(js, counters_obj, "colorCounts"); + for (int i = 0; i < b2_graphColorCount; i++) { + JS_SetPropertyUint32(js, colorArray, i, JS_NewInt32(js, counters.colorCounts[i])); + } + JS_FreeValue(js, colorArray); + + return counters_obj; +) + +// World.prototype.getContactEvents() +JSC_CCALL(box2d_world_getContactEvents, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + b2ContactEvents events = b2World_GetContactEvents(world->id); + + JSValue events_obj = JS_NewObject(js); + + // Begin events + JSValue beginArray = JS_NewArray(js); + for (int i = 0; i < events.beginCount; i++) { + JSValue event = JS_NewObject(js); + JS_SetPropertyStr(js, event, "shapeIdA", JS_NewInt32(js, events.beginEvents[i].shapeIdA.index1)); + JS_SetPropertyStr(js, event, "shapeIdB", JS_NewInt32(js, events.beginEvents[i].shapeIdB.index1)); + JS_SetPropertyUint32(js, beginArray, i, event); + } + JS_SetPropertyStr(js, events_obj, "beginEvents", beginArray); + + // End events + JSValue endArray = JS_NewArray(js); + for (int i = 0; i < events.endCount; i++) { + JSValue event = JS_NewObject(js); + JS_SetPropertyStr(js, event, "shapeIdA", JS_NewInt32(js, events.endEvents[i].shapeIdA.index1)); + JS_SetPropertyStr(js, event, "shapeIdB", JS_NewInt32(js, events.endEvents[i].shapeIdB.index1)); + JS_SetPropertyUint32(js, endArray, i, event); + } + JS_SetPropertyStr(js, events_obj, "endEvents", endArray); + + // Hit events + JSValue hitArray = JS_NewArray(js); + for (int i = 0; i < events.hitCount; i++) { + JSValue event = JS_NewObject(js); + JS_SetPropertyStr(js, event, "shapeIdA", JS_NewInt32(js, events.hitEvents[i].shapeIdA.index1)); + JS_SetPropertyStr(js, event, "shapeIdB", JS_NewInt32(js, events.hitEvents[i].shapeIdB.index1)); + JS_SetPropertyStr(js, event, "point", b2vec22js(js, events.hitEvents[i].point)); + JS_SetPropertyStr(js, event, "normal", b2vec22js(js, events.hitEvents[i].normal)); + JS_SetPropertyStr(js, event, "approachSpeed", JS_NewFloat64(js, events.hitEvents[i].approachSpeed)); + JS_SetPropertyUint32(js, hitArray, i, event); + } + JS_SetPropertyStr(js, events_obj, "hitEvents", hitArray); + + return events_obj; +) + +// World.prototype.getSensorEvents() +JSC_CCALL(box2d_world_getSensorEvents, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + b2SensorEvents events = b2World_GetSensorEvents(world->id); + + JSValue events_obj = JS_NewObject(js); + + // Begin events + JSValue beginArray = JS_NewArray(js); + for (int i = 0; i < events.beginCount; i++) { + JSValue event = JS_NewObject(js); + JS_SetPropertyStr(js, event, "sensorShapeId", JS_NewInt32(js, events.beginEvents[i].sensorShapeId.index1)); + JS_SetPropertyStr(js, event, "visitorShapeId", JS_NewInt32(js, events.beginEvents[i].visitorShapeId.index1)); + JS_SetPropertyUint32(js, beginArray, i, event); + } + JS_SetPropertyStr(js, events_obj, "beginEvents", beginArray); + + // End events + JSValue endArray = JS_NewArray(js); + for (int i = 0; i < events.endCount; i++) { + JSValue event = JS_NewObject(js); + JS_SetPropertyStr(js, event, "sensorShapeId", JS_NewInt32(js, events.endEvents[i].sensorShapeId.index1)); + JS_SetPropertyStr(js, event, "visitorShapeId", JS_NewInt32(js, events.endEvents[i].visitorShapeId.index1)); + JS_SetPropertyUint32(js, endArray, i, event); + } + JS_SetPropertyStr(js, events_obj, "endEvents", endArray); + + return events_obj; +) + +// World.prototype.overlapAABB() +JSC_CCALL(box2d_world_overlapAABB, + box2d_world *world = js2box2d_world(js, self); + if (!b2World_IsValid(world->id)) return JS_ThrowReferenceError(js, "World is invalid"); + + if (argc < 2) return JS_ThrowTypeError(js, "AABB and callback function required"); + + if (!JS_IsObject(argv[0])) return JS_ThrowTypeError(js, "AABB object required"); + if (!JS_IsFunction(js, argv[1])) return JS_ThrowTypeError(js, "Callback function required"); + + // Parse AABB + b2AABB aabb; + JSValue lowerBound = JS_GetPropertyStr(js, argv[0], "lowerBound"); + JSValue upperBound = JS_GetPropertyStr(js, argv[0], "upperBound"); + + if (JS_IsUndefined(lowerBound) || JS_IsUndefined(upperBound)) { + JS_FreeValue(js, lowerBound); + JS_FreeValue(js, upperBound); + return JS_ThrowTypeError(js, "AABB must have lowerBound and upperBound"); + } + + aabb.lowerBound = js2b2vec2(js, lowerBound); + aabb.upperBound = js2b2vec2(js, upperBound); + JS_FreeValue(js, lowerBound); + JS_FreeValue(js, upperBound); + + // TODO: Implement overlap callback - this requires C callback bridge + // For now, return undefined + return JS_UNDEFINED; +) + +// Function lists static const JSCFunctionListEntry js_box2d_world_funcs[] = { - JS_CFUNC_DEF("body", 0, js_box2d_world_body), - JS_CFUNC_DEF("step", 2, js_box2d_world_step), + // Creation methods + MIST_FUNC_DEF(box2d_world, createBody, 1), + MIST_FUNC_DEF(box2d_world, createDistanceJoint, 1), + MIST_FUNC_DEF(box2d_world, createRevoluteJoint, 1), + MIST_FUNC_DEF(box2d_world, createMotorJoint, 1), + MIST_FUNC_DEF(box2d_world, createMouseJoint, 1), + MIST_FUNC_DEF(box2d_world, createPrismaticJoint, 1), + MIST_FUNC_DEF(box2d_world, createWeldJoint, 1), + MIST_FUNC_DEF(box2d_world, createWheelJoint, 1), + + // Simulation methods + MIST_FUNC_DEF(box2d_world, step, 2), + MIST_FUNC_DEF(box2d_world, rayCast, 3), + + // Callback setters + MIST_FUNC_DEF(box2d_world, setContactCallback, 1), + MIST_FUNC_DEF(box2d_world, setFilterCallback, 1), + MIST_FUNC_DEF(box2d_world, setPreSolveCallback, 1), + + // Configuration methods + MIST_FUNC_DEF(box2d_world, setRestitutionThreshold, 1), + MIST_FUNC_DEF(box2d_world, setHitEventThreshold, 1), + MIST_FUNC_DEF(box2d_world, setContactTuning, 3), + + // Query methods + MIST_FUNC_DEF(box2d_world, getProfile, 0), + MIST_FUNC_DEF(box2d_world, getCounters, 0), + MIST_FUNC_DEF(box2d_world, getContactEvents, 0), + MIST_FUNC_DEF(box2d_world, getSensorEvents, 0), + MIST_FUNC_DEF(box2d_world, overlapAABB, 2), + + // Properties JS_CGETSET_DEF("gravity", js_box2d_world_get_gravity, js_box2d_world_set_gravity), }; -static JSValue js_box2d_world(JSContext *js, JSValueConst self, int argc, JSValueConst *argv) -{ - b2WorldDef world = b2DefaultWorldDef(); - b2WorldId *wid = malloc(sizeof(*wid)); - *wid = b2CreateWorld(&world); - JSValue obj = JS_NewObjectClass(js, box2d_world_id); - JS_SetOpaque(obj, wid); - return obj; -} - -static const JSCFunctionListEntry js_box2d_funcs[] = { - JS_CFUNC_DEF("world", 0, js_box2d_world) +static const JSCFunctionListEntry js_box2d_body_funcs[] = { + MIST_FUNC_DEF(box2d_body, createCircleShape, 1), + MIST_FUNC_DEF(box2d_body, createBoxShape, 1), + MIST_FUNC_DEF(box2d_body, createSegmentShape, 3), + MIST_FUNC_DEF(box2d_body, createCapsuleShape, 1), + MIST_FUNC_DEF(box2d_body, createPolygonShape, 2), + MIST_FUNC_DEF(box2d_body, applyForce, 2), + MIST_FUNC_DEF(box2d_body, applyImpulse, 2), + MIST_FUNC_DEF(box2d_body, applyLinearImpulse, 2), + MIST_FUNC_DEF(box2d_body, getMass, 0), + JS_CGETSET_DEF("position", js_box2d_body_get_position, js_box2d_body_set_position), + JS_CGETSET_DEF("angle", js_box2d_body_get_angle, js_box2d_body_set_angle), + JS_CGETSET_DEF("linearVelocity", js_box2d_body_get_linearVelocity, js_box2d_body_set_linearVelocity), + JS_CGETSET_DEF("angularVelocity", js_box2d_body_get_angularVelocity, js_box2d_body_set_angularVelocity), + JS_CGETSET_DEF("type", js_box2d_body_get_type, js_box2d_body_set_type), }; -JSValue js_box2d_use(JSContext *ctx) { - JS_NewClassID(&box2d_world_id); - JS_NewClass(JS_GetRuntime(ctx), box2d_world_id, &box2d_world); - JSValue world_proto = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, world_proto, js_box2d_world_funcs, countof(js_box2d_world_funcs)); - JS_SetClassProto(ctx, box2d_world_id, world_proto); +// Shape.prototype.density getter +JSValue js_box2d_shape_get_density(JSContext *js, JSValueConst self) +{ + box2d_shape *shape = js2box2d_shape(js, self); + if (!b2Shape_IsValid(shape->id)) return JS_ThrowReferenceError(js, "Shape is invalid"); - JS_NewClassID(&box2d_body_id); - JS_NewClass(JS_GetRuntime(ctx), box2d_body_id, &box2d_body); - JSValue body_proto = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, body_proto, js_box2d_body_funcs, countof(js_box2d_body_funcs)); - JS_SetClassProto(ctx, box2d_body_id, body_proto); - - JSValue export = JS_NewObject(ctx); - JS_SetPropertyFunctionList(ctx, export, js_box2d_funcs, countof(js_box2d_funcs)); - - return export; + float density = b2Shape_GetDensity(shape->id); + return JS_NewFloat64(js, density); } -static int js_box2d_init(JSContext *ctx, JSModuleDef *m) { - JSValue export_val = js_box2d_use(ctx); - return JS_SetModuleExport(ctx, m, "default", export_val); +// Shape.prototype.density setter +JSValue js_box2d_shape_set_density(JSContext *js, JSValueConst self, JSValue val) +{ + box2d_shape *shape = js2box2d_shape(js, self); + if (!b2Shape_IsValid(shape->id)) return JS_ThrowReferenceError(js, "Shape is invalid"); + + double density; + if (JS_ToFloat64(js, &density, val)) return JS_EXCEPTION; + + b2Shape_SetDensity(shape->id, (float)density); + return JS_UNDEFINED; +} + +// Shape.prototype.friction getter +JSValue js_box2d_shape_get_friction(JSContext *js, JSValueConst self) +{ + box2d_shape *shape = js2box2d_shape(js, self); + if (!b2Shape_IsValid(shape->id)) return JS_ThrowReferenceError(js, "Shape is invalid"); + + float friction = b2Shape_GetFriction(shape->id); + return JS_NewFloat64(js, friction); +} + +// Shape.prototype.friction setter +JSValue js_box2d_shape_set_friction(JSContext *js, JSValueConst self, JSValue val) +{ + box2d_shape *shape = js2box2d_shape(js, self); + if (!b2Shape_IsValid(shape->id)) return JS_ThrowReferenceError(js, "Shape is invalid"); + + double friction; + if (JS_ToFloat64(js, &friction, val)) return JS_EXCEPTION; + + b2Shape_SetFriction(shape->id, (float)friction); + return JS_UNDEFINED; +} + +// Shape.prototype.restitution getter +JSValue js_box2d_shape_get_restitution(JSContext *js, JSValueConst self) +{ + box2d_shape *shape = js2box2d_shape(js, self); + if (!b2Shape_IsValid(shape->id)) return JS_ThrowReferenceError(js, "Shape is invalid"); + + float restitution = b2Shape_GetRestitution(shape->id); + return JS_NewFloat64(js, restitution); +} + +// Shape.prototype.restitution setter +JSValue js_box2d_shape_set_restitution(JSContext *js, JSValueConst self, JSValue val) +{ + box2d_shape *shape = js2box2d_shape(js, self); + if (!b2Shape_IsValid(shape->id)) return JS_ThrowReferenceError(js, "Shape is invalid"); + + double restitution; + if (JS_ToFloat64(js, &restitution, val)) return JS_EXCEPTION; + + b2Shape_SetRestitution(shape->id, (float)restitution); + return JS_UNDEFINED; +} + +// Shape.prototype.isSensor getter +JSValue js_box2d_shape_get_isSensor(JSContext *js, JSValueConst self) +{ + box2d_shape *shape = js2box2d_shape(js, self); + if (!b2Shape_IsValid(shape->id)) return JS_ThrowReferenceError(js, "Shape is invalid"); + + bool isSensor = b2Shape_IsSensor(shape->id); + return JS_NewBool(js, isSensor); +} + +// Shape.prototype.getType() +JSC_CCALL(box2d_shape_getType, + box2d_shape *shape = js2box2d_shape(js, self); + if (!b2Shape_IsValid(shape->id)) return JS_ThrowReferenceError(js, "Shape is invalid"); + + b2ShapeType type = b2Shape_GetType(shape->id); + + switch (type) { + case b2_circleShape: return JS_NewString(js, "circle"); + case b2_polygonShape: return JS_NewString(js, "polygon"); + case b2_segmentShape: return JS_NewString(js, "segment"); + case b2_capsuleShape: return JS_NewString(js, "capsule"); + case b2_smoothSegmentShape: return JS_NewString(js, "smoothSegment"); + default: return JS_NewString(js, "unknown"); + } +) + +// Shape.prototype.enableSensorEvents() +JSC_CCALL(box2d_shape_enableSensorEvents, + box2d_shape *shape = js2box2d_shape(js, self); + if (!b2Shape_IsValid(shape->id)) return JS_ThrowReferenceError(js, "Shape is invalid"); + + bool enable = true; + if (argc > 0) { + enable = JS_ToBool(js, argv[0]); + } + + b2Shape_EnableSensorEvents(shape->id, enable); + return JS_UNDEFINED; +) + +// Shape.prototype.enableContactEvents() +JSC_CCALL(box2d_shape_enableContactEvents, + box2d_shape *shape = js2box2d_shape(js, self); + if (!b2Shape_IsValid(shape->id)) return JS_ThrowReferenceError(js, "Shape is invalid"); + + bool enable = true; + if (argc > 0) { + enable = JS_ToBool(js, argv[0]); + } + + b2Shape_EnableContactEvents(shape->id, enable); + return JS_UNDEFINED; +) + +// Shape.prototype.setFilter() +JSC_CCALL(box2d_shape_setFilter, + box2d_shape *shape = js2box2d_shape(js, self); + if (!b2Shape_IsValid(shape->id)) return JS_ThrowReferenceError(js, "Shape is invalid"); + + if (argc < 1 || !JS_IsObject(argv[0])) { + return JS_ThrowTypeError(js, "Filter object required"); + } + + b2Filter filter = b2DefaultFilter(); + + JSValue categoryBits_val = JS_GetPropertyStr(js, argv[0], "categoryBits"); + if (!JS_IsUndefined(categoryBits_val)) { + uint32_t bits; + if (!JS_ToUint32(js, &bits, categoryBits_val)) { + filter.categoryBits = bits; + } + JS_FreeValue(js, categoryBits_val); + } + + JSValue maskBits_val = JS_GetPropertyStr(js, argv[0], "maskBits"); + if (!JS_IsUndefined(maskBits_val)) { + uint32_t bits; + if (!JS_ToUint32(js, &bits, maskBits_val)) { + filter.maskBits = bits; + } + JS_FreeValue(js, maskBits_val); + } + + JSValue groupIndex_val = JS_GetPropertyStr(js, argv[0], "groupIndex"); + if (!JS_IsUndefined(groupIndex_val)) { + int32_t index; + if (!JS_ToInt32(js, &index, groupIndex_val)) { + filter.groupIndex = index; + } + JS_FreeValue(js, groupIndex_val); + } + + b2Shape_SetFilter(shape->id, filter); + return JS_UNDEFINED; +) + +// Shape.prototype.getFilter() +JSC_CCALL(box2d_shape_getFilter, + box2d_shape *shape = js2box2d_shape(js, self); + if (!b2Shape_IsValid(shape->id)) return JS_ThrowReferenceError(js, "Shape is invalid"); + + b2Filter filter = b2Shape_GetFilter(shape->id); + + JSValue filter_obj = JS_NewObject(js); + JS_SetPropertyStr(js, filter_obj, "categoryBits", JS_NewUint32(js, filter.categoryBits)); + JS_SetPropertyStr(js, filter_obj, "maskBits", JS_NewUint32(js, filter.maskBits)); + JS_SetPropertyStr(js, filter_obj, "groupIndex", JS_NewInt32(js, filter.groupIndex)); + + return filter_obj; +) + +static const JSCFunctionListEntry js_box2d_shape_funcs[] = { + MIST_FUNC_DEF(box2d_shape, getType, 0), + MIST_FUNC_DEF(box2d_shape, enableSensorEvents, 1), + MIST_FUNC_DEF(box2d_shape, enableContactEvents, 1), + MIST_FUNC_DEF(box2d_shape, setFilter, 1), + MIST_FUNC_DEF(box2d_shape, getFilter, 0), + JS_CGETSET_DEF("density", js_box2d_shape_get_density, js_box2d_shape_set_density), + JS_CGETSET_DEF("friction", js_box2d_shape_get_friction, js_box2d_shape_set_friction), + JS_CGETSET_DEF("restitution", js_box2d_shape_get_restitution, js_box2d_shape_set_restitution), + JS_CGETSET_DEF("isSensor", js_box2d_shape_get_isSensor, NULL), +}; + +// Joint.prototype.getType() +JSC_CCALL(box2d_joint_getType, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + b2JointType type = b2Joint_GetType(joint->id); + + switch (type) { + case b2_distanceJoint: return JS_NewString(js, "distance"); + case b2_motorJoint: return JS_NewString(js, "motor"); + case b2_mouseJoint: return JS_NewString(js, "mouse"); + case b2_prismaticJoint: return JS_NewString(js, "prismatic"); + case b2_revoluteJoint: return JS_NewString(js, "revolute"); + case b2_weldJoint: return JS_NewString(js, "weld"); + case b2_wheelJoint: return JS_NewString(js, "wheel"); + default: return JS_NewString(js, "unknown"); + } +) + +// Joint.prototype.getBodyA() +JSC_CCALL(box2d_joint_getBodyA, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + b2BodyId bodyId = b2Joint_GetBodyA(joint->id); + + // Find the body wrapper + // Note: This is a simplified approach - in a full implementation you'd maintain a lookup table + box2d_body *body = malloc(sizeof(*body)); + body->id = bodyId; + body->world = joint->world; + + return box2d_body2js(js, body); +) + +// Joint.prototype.getBodyB() +JSC_CCALL(box2d_joint_getBodyB, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + b2BodyId bodyId = b2Joint_GetBodyB(joint->id); + + // Find the body wrapper + box2d_body *body = malloc(sizeof(*body)); + body->id = bodyId; + body->world = joint->world; + + return box2d_body2js(js, body); +) + +// Joint.prototype.getLocalAnchorA() +JSC_CCALL(box2d_joint_getLocalAnchorA, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + b2Vec2 anchor = b2Joint_GetLocalAnchorA(joint->id); + return b2vec22js(js, anchor); +) + +// Joint.prototype.getLocalAnchorB() +JSC_CCALL(box2d_joint_getLocalAnchorB, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + b2Vec2 anchor = b2Joint_GetLocalAnchorB(joint->id); + return b2vec22js(js, anchor); +) + +// Joint.prototype.getConstraintForce() +JSC_CCALL(box2d_joint_getConstraintForce, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + b2Vec2 force = b2Joint_GetConstraintForce(joint->id); + return b2vec22js(js, force); +) + +// Joint.prototype.getConstraintTorque() +JSC_CCALL(box2d_joint_getConstraintTorque, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + float torque = b2Joint_GetConstraintTorque(joint->id); + return JS_NewFloat64(js, torque); +) + +// Joint.prototype.setCollideConnected() +JSC_CCALL(box2d_joint_setCollideConnected, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + bool shouldCollide = true; + if (argc > 0) { + shouldCollide = JS_ToBool(js, argv[0]); + } + + b2Joint_SetCollideConnected(joint->id, shouldCollide); + return JS_UNDEFINED; +) + +// Joint.prototype.getCollideConnected() +JSC_CCALL(box2d_joint_getCollideConnected, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + bool collideConnected = b2Joint_GetCollideConnected(joint->id); + return JS_NewBool(js, collideConnected); +) + +// Joint.prototype.wakeBodies() +JSC_CCALL(box2d_joint_wakeBodies, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + b2Joint_WakeBodies(joint->id); + return JS_UNDEFINED; +) + +// Distance Joint specific methods +// Joint.prototype.setLength() - for distance joints +JSC_CCALL(box2d_joint_setLength, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + if (b2Joint_GetType(joint->id) != b2_distanceJoint) { + return JS_ThrowTypeError(js, "setLength only applies to distance joints"); + } + + if (argc < 1) return JS_ThrowTypeError(js, "Length value required"); + + double length; + if (JS_ToFloat64(js, &length, argv[0])) return JS_EXCEPTION; + + b2DistanceJoint_SetLength(joint->id, (float)length); + return JS_UNDEFINED; +) + +// Joint.prototype.getLength() - for distance joints +JSC_CCALL(box2d_joint_getLength, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + if (b2Joint_GetType(joint->id) != b2_distanceJoint) { + return JS_ThrowTypeError(js, "getLength only applies to distance joints"); + } + + float length = b2DistanceJoint_GetLength(joint->id); + return JS_NewFloat64(js, length); +) + +// Motor Joint specific methods +// Joint.prototype.setLinearOffset() - for motor joints +JSC_CCALL(box2d_joint_setLinearOffset, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + if (b2Joint_GetType(joint->id) != b2_motorJoint) { + return JS_ThrowTypeError(js, "setLinearOffset only applies to motor joints"); + } + + if (argc < 1) return JS_ThrowTypeError(js, "Linear offset vector required"); + + b2Vec2 offset = js2b2vec2(js, argv[0]); + b2MotorJoint_SetLinearOffset(joint->id, offset); + return JS_UNDEFINED; +) + +// Joint.prototype.getLinearOffset() - for motor joints +JSC_CCALL(box2d_joint_getLinearOffset, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + if (b2Joint_GetType(joint->id) != b2_motorJoint) { + return JS_ThrowTypeError(js, "getLinearOffset only applies to motor joints"); + } + + b2Vec2 offset = b2MotorJoint_GetLinearOffset(joint->id); + return b2vec22js(js, offset); +) + +// Joint.prototype.setAngularOffset() - for motor joints +JSC_CCALL(box2d_joint_setAngularOffset, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + if (b2Joint_GetType(joint->id) != b2_motorJoint) { + return JS_ThrowTypeError(js, "setAngularOffset only applies to motor joints"); + } + + if (argc < 1) return JS_ThrowTypeError(js, "Angular offset value required"); + + double offset; + if (JS_ToFloat64(js, &offset, argv[0])) return JS_EXCEPTION; + + b2MotorJoint_SetAngularOffset(joint->id, (float)offset); + return JS_UNDEFINED; +) + +// Joint.prototype.getAngularOffset() - for motor joints +JSC_CCALL(box2d_joint_getAngularOffset, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + if (b2Joint_GetType(joint->id) != b2_motorJoint) { + return JS_ThrowTypeError(js, "getAngularOffset only applies to motor joints"); + } + + float offset = b2MotorJoint_GetAngularOffset(joint->id); + return JS_NewFloat64(js, offset); +) + +// Joint.prototype.setMaxForce() - for motor joints +JSC_CCALL(box2d_joint_setMaxForce, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + if (b2Joint_GetType(joint->id) != b2_motorJoint) { + return JS_ThrowTypeError(js, "setMaxForce only applies to motor joints"); + } + + if (argc < 1) return JS_ThrowTypeError(js, "Max force value required"); + + double force; + if (JS_ToFloat64(js, &force, argv[0])) return JS_EXCEPTION; + + b2MotorJoint_SetMaxForce(joint->id, (float)force); + return JS_UNDEFINED; +) + +// Joint.prototype.getMaxForce() - for motor joints +JSC_CCALL(box2d_joint_getMaxForce, + box2d_joint *joint = js2box2d_joint(js, self); + if (!b2Joint_IsValid(joint->id)) return JS_ThrowReferenceError(js, "Joint is invalid"); + + if (b2Joint_GetType(joint->id) != b2_motorJoint) { + return JS_ThrowTypeError(js, "getMaxForce only applies to motor joints"); + } + + float force = b2MotorJoint_GetMaxForce(joint->id); + return JS_NewFloat64(js, force); +) + +static const JSCFunctionListEntry js_box2d_joint_funcs[] = { + MIST_FUNC_DEF(box2d_joint, getType, 0), + MIST_FUNC_DEF(box2d_joint, getBodyA, 0), + MIST_FUNC_DEF(box2d_joint, getBodyB, 0), + MIST_FUNC_DEF(box2d_joint, getLocalAnchorA, 0), + MIST_FUNC_DEF(box2d_joint, getLocalAnchorB, 0), + MIST_FUNC_DEF(box2d_joint, getConstraintForce, 0), + MIST_FUNC_DEF(box2d_joint, getConstraintTorque, 0), + MIST_FUNC_DEF(box2d_joint, setCollideConnected, 1), + MIST_FUNC_DEF(box2d_joint, getCollideConnected, 0), + MIST_FUNC_DEF(box2d_joint, wakeBodies, 0), + // Distance Joint specific + MIST_FUNC_DEF(box2d_joint, setLength, 1), + MIST_FUNC_DEF(box2d_joint, getLength, 0), + // Motor Joint specific + MIST_FUNC_DEF(box2d_joint, setLinearOffset, 1), + MIST_FUNC_DEF(box2d_joint, getLinearOffset, 0), + MIST_FUNC_DEF(box2d_joint, setAngularOffset, 1), + MIST_FUNC_DEF(box2d_joint, getAngularOffset, 0), + MIST_FUNC_DEF(box2d_joint, setMaxForce, 1), + MIST_FUNC_DEF(box2d_joint, getMaxForce, 0), +}; + +// Module initialization +JSValue js_box2d_use(JSContext *js) +{ + // Register classes + QJSCLASSPREP_FUNCS(box2d_world); + QJSCLASSPREP_FUNCS(box2d_body); + QJSCLASSPREP_FUNCS(box2d_shape); + QJSCLASSPREP_FUNCS(box2d_joint); + + // Create module object + JSValue module = JS_NewObject(js); + + // World constructor + JSValue world_ctor = JS_NewCFunction2(js, js_box2d_world_constructor, "World", 1, JS_CFUNC_constructor, 0); + JSValue world_proto = JS_GetClassProto(js, js_box2d_world_id); + JS_SetConstructor(js, world_ctor, world_proto); + JS_FreeValue(js, world_proto); + JS_SetPropertyStr(js, module, "World", world_ctor); + + // Body type constants + JS_SetPropertyStr(js, module, "STATIC_BODY", JS_NewInt32(js, b2_staticBody)); + JS_SetPropertyStr(js, module, "KINEMATIC_BODY", JS_NewInt32(js, b2_kinematicBody)); + JS_SetPropertyStr(js, module, "DYNAMIC_BODY", JS_NewInt32(js, b2_dynamicBody)); + + return module; +} + +static int js_box2d_init(JSContext *js, JSModuleDef *m) +{ + JSValue module = js_box2d_use(js); + JS_SetModuleExport(js, m, "default", module); + return 0; } #ifdef JS_SHARED_LIBRARY @@ -163,9 +2332,10 @@ static int js_box2d_init(JSContext *ctx, JSModuleDef *m) { #define JS_INIT_MODULE js_init_module_box2d #endif -JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) { - JSModuleDef *m = JS_NewCModule(ctx, module_name, js_box2d_init); +JSModuleDef *JS_INIT_MODULE(JSContext *js, const char *module_name) +{ + JSModuleDef *m = JS_NewCModule(js, module_name, js_box2d_init); if (!m) return NULL; - JS_AddModuleExport(ctx, m, "default"); + JS_AddModuleExport(js, m, "default"); return m; -} +} \ No newline at end of file diff --git a/tests/box2d.js b/tests/box2d.js index 24984596..b68422bc 100644 --- a/tests/box2d.js +++ b/tests/box2d.js @@ -1,13 +1,90 @@ var box2d = use('box2d') -var world = box2d.world() -console.log(world.gravity) -world.gravity = {x:0,y:-9.8} -console.log(world.gravity) -var body = world.body() +// Create world with constructor +var world = new box2d.World({gravity: {x: 0, y: -9.8}}) +console.log("Initial gravity:", world.gravity) -world.step(10, 4); -world.step(10, 4); -world.step(10, 4); +// Create a dynamic body +var body = world.createBody({ + type: 'dynamic', + position: {x: 0, y: 10} +}) -console.log(body.getPosition()) +// Add a box shape to the body +var shape = body.createBoxShape({ + width: 1, + height: 1, + density: 1.0, + friction: 0.3, + restitution: 0.5 +}) + +console.log("Initial position:", body.position) +console.log("Initial velocity:", body.linearVelocity) + +// Simulate +world.step(1/60, 4); +console.log("After 1 step:", body.position) + +world.step(1/60, 4); +console.log("After 2 steps:", body.position) + +world.step(1/60, 4); +console.log("After 3 steps:", body.position) + +// Test applying forces +body.applyForce({x: 100, y: 0}) +world.step(1/60, 4); +console.log("After force:", body.position, "velocity:", body.linearVelocity) + +// Test properties +console.log("Body type:", body.type) +console.log("Body mass:", body.getMass()) +console.log("Body angle:", body.angle) + +// Test string body types +var staticBody = world.createBody({ + type: 'static', + position: {x: 0, y: 0} +}) +console.log("Static body type:", staticBody.type) + +// Test ray casting +var rayResult = world.rayCast({x: -5, y: 10}, {x: 10, y: 0}, 1.0) +console.log("Ray cast result:", rayResult) + +// Test distance joint +var bodyA = world.createBody({ + type: 'dynamic', + position: {x: -2, y: 5} +}) +bodyA.createCircleShape({ + radius: 0.5, + density: 1.0 +}) + +var bodyB = world.createBody({ + type: 'dynamic', + position: {x: 2, y: 5} +}) +bodyB.createCircleShape({ + radius: 0.5, + density: 1.0 +}) + +var joint = world.createDistanceJoint({ + bodyA: bodyA, + bodyB: bodyB, + localAnchorA: {x: 0, y: 0}, + localAnchorB: {x: 0, y: 0}, + length: 4.0 +}) + +console.log("Created distance joint") + +// Test multiple steps with joint +for (var i = 0; i < 10; i++) { + world.step(1/60, 4) +} +console.log("Body A position after joint simulation:", bodyA.position) +console.log("Body B position after joint simulation:", bodyB.position)