3 Commits

Author SHA1 Message Date
John Alanbrook
ad4f3d3f58 correct deletion
Some checks failed
Build and Deploy / build-linux (push) Failing after 39s
Build and Deploy / build-windows (CLANG64) (push) Failing after 8m19s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-02-26 10:07:32 -06:00
John Alanbrook
b23dca6934 attempt at handling automatic removal on destroy
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m17s
Build and Deploy / build-windows (CLANG64) (push) Successful in 9m50s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-02-25 15:49:32 -06:00
John Alanbrook
8fba19c820 WIP: Chipmunk integration
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-02-24 18:24:45 -06:00
7 changed files with 2161 additions and 17 deletions

View File

@@ -72,6 +72,18 @@ sdl3_opts.add_cmake_defines({
'CMAKE_BUILD_TYPE': 'Release' 'CMAKE_BUILD_TYPE': 'Release'
}) })
chipmunk_opts = cmake.subproject_options()
chipmunk_opts.add_cmake_defines({
'BUILD_DEMOS': 'OFF',
'BUILD_SHARED': 'OFF',
'BUILD_STATIC': 'ON',
'CMAKE_BUILD_TYPE': 'Release',
# uncomment to use floats instead of doubles
# 'CP_USE_DOUBLES': 'OFF',
})
chipmunk_proj = cmake.subproject('chipmunk', options: chipmunk_opts)
deps += chipmunk_proj.dependency('chipmunk_static')
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
if host_machine.system() == 'darwin' if host_machine.system() == 'darwin'
@@ -131,14 +143,11 @@ deps += dependency('physfs', static:true)
#deps += cc.find_library('opencv') #deps += cc.find_library('opencv')
deps += dependency('threads') deps += dependency('threads')
deps += dependency('chipmunk', static:true)
deps += dependency('enet', static:true) deps += dependency('enet', static:true)
deps += dependency('soloud', static:true) deps += dependency('soloud', static:true)
#deps += dependency('qjs-chipmunk', static:false)
sources = [] sources = []
src += ['anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c','render.c','script.c','simplex.c','spline.c', 'timer.c', 'transform.c','prosperon.c', 'wildmatch.c', 'sprite.c', 'rtree.c', 'qjs_dmon.c', 'qjs_nota.c', 'qjs_enet.c', 'qjs_soloud.c'] src += ['anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c','render.c','script.c','simplex.c','spline.c', 'timer.c', 'transform.c','prosperon.c', 'wildmatch.c', 'sprite.c', 'rtree.c', 'qjs_dmon.c', 'qjs_nota.c', 'qjs_enet.c', 'qjs_soloud.c', 'qjs_chipmunk.c']
imsrc = ['GraphEditor.cpp','ImCurveEdit.cpp','ImGradient.cpp','imgui_draw.cpp','imgui_tables.cpp','imgui_widgets.cpp','imgui.cpp','ImGuizmo.cpp','imnodes.cpp','implot_items.cpp','implot.cpp', 'imgui_impl_sdlrenderer3.cpp', 'imgui_impl_sdl3.cpp', 'imgui_impl_sdlgpu3.cpp'] imsrc = ['GraphEditor.cpp','ImCurveEdit.cpp','ImGradient.cpp','imgui_draw.cpp','imgui_tables.cpp','imgui_widgets.cpp','imgui.cpp','ImGuizmo.cpp','imnodes.cpp','implot_items.cpp','implot.cpp', 'imgui_impl_sdlrenderer3.cpp', 'imgui_impl_sdl3.cpp', 'imgui_impl_sdlgpu3.cpp']
@@ -231,7 +240,8 @@ tests = [
'spawn_actor', 'spawn_actor',
'empty', 'empty',
'nota', 'nota',
'enet' 'enet',
'chipmunk2d'
] ]
foreach file : tests foreach file : tests

View File

@@ -0,0 +1,564 @@
var chipmunk = this;
return chipmunk
//------------------------------------------------
// Top-level Chipmunk2D functions
//------------------------------------------------
chipmunk.make_space[prosperon.DOC] = `
Create and return a new Chipmunk2D cpSpace instance. By default, this space has
no bodies or shapes. You can add bodies via space.add_body() and step the simulation
with space.step().
:return: A newly created Space object.
`;
//------------------------------------------------
// Space methods and properties
//------------------------------------------------
var cpSpace = prosperon.c_types.cpSpace;
cpSpace.step[prosperon.DOC] = `
Advance the physics simulation by the specified timestep.
:param dt: A number representing the time step to simulate (e.g., 1/60 for 60 FPS).
:return: None
`;
cpSpace.add_body[prosperon.DOC] = `
Create and add a new dynamic body to this space. Returns the newly created cpBody
object, which you can then configure (mass, moment, etc.) or attach shapes to.
:return: A cpBody object representing the new body.
`;
cpSpace.eachBody[prosperon.DOC] = `
Iterate over every cpBody in this space, calling the provided callback for each one.
Useful for enumerating or modifying all bodies.
:param callback: A function(body) called for each cpBody in this space.
:return: None
`;
cpSpace.eachShape[prosperon.DOC] = `
Iterate over every cpShape in this space, calling the provided callback for each one.
:param callback: A function(shape) called for each cpShape in this space.
:return: None
`;
cpSpace.eachConstraint[prosperon.DOC] = `
Iterate over every cpConstraint in this space, calling the provided callback for each.
:param callback: A function(constraint) called for each cpConstraint in this space.
:return: None
`;
cpSpace.gravity[prosperon.DOC] = `
The gravity vector for this space, typically something like { x: 0, y: -9.8 } for
Earth-like gravity in 2D. You can read or write this property to change gravity.
:return: An object { x, y } for read. Assign a similar object to set.
`;
cpSpace.iterations[prosperon.DOC] = `
The number of solver iterations that Chipmunk2D performs each time step. Higher values
improve stability at a performance cost. You can read or write this property.
:return: Number of iterations.
`;
cpSpace.idle_speed[prosperon.DOC] = `
Bodies with a speed (velocity magnitude) lower than idle_speed for a certain amount of
time can enter sleep mode, which saves CPU. Read or set this threshold.
:return: A number indicating the idle speed threshold.
`;
cpSpace.sleep_time[prosperon.DOC] = `
A duration threshold (in seconds) for which a body must remain below idle_speed before it
can sleep. Read or set this property.
:return: Number of seconds for the sleep threshold.
`;
cpSpace.collision_slop[prosperon.DOC] = `
Extra distance allowed to mitigate floating-point issues, preventing shapes from
interpenetrating excessively. Can be read or set.
:return: A number representing the collision slop distance.
`;
cpSpace.collision_bias[prosperon.DOC] = `
Rate at which interpenetration errors are corrected each step. Usually a number close to
1.0 (a bit less). Can be read or set.
:return: A number controlling bias for shape collision resolution.
`;
cpSpace.collision_persistence[prosperon.DOC] = `
How many steps a contact persists if the two shapes are still overlapping. Read or set.
:return: An integer count of persistence steps.
`;
//------------------------------------------------
// Space joint-creation methods
//------------------------------------------------
cpSpace.pin[prosperon.DOC] = `
Create a cpPinJoint between two bodies (argv[0] and argv[1]) and add it to this space.
If called without arguments, returns the PinJoint prototype object.
:return: A cpPinJoint constraint object if arguments are provided, or the prototype if none.
`;
cpSpace.pivot[prosperon.DOC] = `
Create a cpPivotJoint between two bodies at a given pivot point. If called without
arguments, returns the PivotJoint prototype object.
:param bodyA: The first cpBody.
:param bodyB: The second cpBody.
:param pivotPoint: An object { x, y } for the pivot location in world coordinates.
:return: A cpPivotJoint constraint object or the prototype if no arguments.
`;
cpSpace.gear[prosperon.DOC] = `
Create a cpGearJoint between two bodies, controlling their relative angular motion.
If called without arguments, returns the GearJoint prototype object.
:param bodyA: The first cpBody.
:param bodyB: The second cpBody.
:param phase: Initial angular offset in radians.
:param ratio: Gear ratio linking rotations of bodyA and bodyB.
:return: A cpGearJoint constraint object or prototype if no arguments.
`;
cpSpace.rotary[prosperon.DOC] = `
Create a cpRotaryLimitJoint that constrains rotation between two bodies to a min and max
angle. Without arguments, returns the RotaryLimitJoint prototype.
:param bodyA: The first cpBody.
:param bodyB: The second cpBody.
:param minAngle: Minimum relative angle in radians.
:param maxAngle: Maximum relative angle in radians.
:return: A cpRotaryLimitJoint constraint object or prototype if no arguments.
`;
cpSpace.damped_rotary[prosperon.DOC] = `
Create a cpDampedRotarySpring that uses a spring-damper system to keep two bodies at a
relative rest angle. Without arguments, returns the prototype.
:param bodyA: The first cpBody.
:param bodyB: The second cpBody.
:param restAngle: Rest angle between the bodies.
:param stiffness: Spring stiffness.
:param damping: Damping factor.
:return: A cpDampedRotarySpring object or prototype if no arguments.
`;
cpSpace.damped_spring[prosperon.DOC] = `
Create a cpDampedSpring between two bodies, each with an anchor point and a rest length.
Without arguments, returns the prototype.
:param bodyA: The first cpBody.
:param bodyB: The second cpBody.
:param anchorA: { x, y } anchor relative to bodyA.
:param anchorB: { x, y } anchor relative to bodyB.
:param restLength: The spring's natural rest length.
:param stiffness: The spring stiffness.
:param damping: The damping factor.
:return: A cpDampedSpring constraint or prototype if no arguments.
`;
cpSpace.groove[prosperon.DOC] = `
Create a cpGrooveJoint, which allows one bodys anchor to slide along a groove defined
in the other body. Without arguments, returns the prototype.
:param bodyA: The first cpBody.
:param bodyB: The second cpBody.
:param grooveA: { x, y } start of the groove in bodyA.
:param grooveB: { x, y } end of the groove in bodyA.
:param anchorB: { x, y } anchor point on bodyB.
:return: A cpGrooveJoint object or prototype if no arguments.
`;
cpSpace.slide[prosperon.DOC] = `
Create a cpSlideJoint that limits the distance between two anchor points on two bodies
to a min and max. Without arguments, returns the prototype.
:param bodyA: The first cpBody.
:param bodyB: The second cpBody.
:param anchorA: { x, y } anchor relative to bodyA.
:param anchorB: { x, y } anchor relative to bodyB.
:param minDistance: Minimum distance allowed.
:param maxDistance: Maximum distance allowed.
:return: A cpSlideJoint object or prototype if no arguments.
`;
cpSpace.ratchet[prosperon.DOC] = `
Create a cpRatchetJoint, allowing relative rotation in steps (like a ratchet). Without
arguments, returns the prototype.
:param bodyA: The first cpBody.
:param bodyB: The second cpBody.
:param phase: Initial angular offset.
:param ratchet: The angle increment for each "click" of the ratchet.
:return: A cpRatchetJoint object or prototype if no arguments.
`;
cpSpace.motor[prosperon.DOC] = `
Create a cpSimpleMotor that enforces a relative angular velocity between two bodies.
Without arguments, returns the prototype.
:param bodyA: The first cpBody.
:param bodyB: The second cpBody.
:param rate: The desired relative angular speed (radians per second).
:return: A cpSimpleMotor object or prototype if no arguments.
`;
//------------------------------------------------
// Body methods and properties
//------------------------------------------------
var cpBody = prosperon.c_types.cpBody;
cpBody.position[prosperon.DOC] = `
The current position (x, y) of this body in world coordinates. Read or assign { x, y }.
`;
cpBody.angle[prosperon.DOC] = `
The body's rotation in radians. 0 is "no rotation". Read or set this property.
`;
cpBody.velocity[prosperon.DOC] = `
The linear velocity of this body (x, y). Typically updated each time step, but you
can also set it directly.
`;
cpBody.angularVelocity[prosperon.DOC] = `
The body's angular velocity (radians per second).
`;
cpBody.moment[prosperon.DOC] = `
The moment of inertia (rotational inertia) for this body. Must not be changed if
the body is static or kinematic.
`;
cpBody.torque[prosperon.DOC] = `
Accumulated torque on this body. Setting this directly allows you to apply a net
torque each frame.
`;
cpBody.mass[prosperon.DOC] = `
Mass of the body. Must not be changed if the body is static or kinematic.
`;
cpBody.centerOfGravity[prosperon.DOC] = `
Offset of the center of gravity (relative to the body's local origin). Typically
set before shapes are attached.
`;
cpBody.force[prosperon.DOC] = `
Accumulated force on this body. Setting this is another way to apply a direct force
each frame.
`;
cpBody.type[prosperon.DOC] = `
The body's type. 0 = CP_BODY_TYPE_DYNAMIC, 1 = CP_BODY_TYPE_KINEMATIC, 2 = CP_BODY_TYPE_STATIC.
Changing the type for a body can move it in or out of the simulation.
`;
cpBody.isSleeping[prosperon.DOC] = `
Returns true if this body is currently sleeping. Sleep is managed automatically.
`;
cpBody.activate[prosperon.DOC] = `
Wake up this body if it is sleeping, making it active again. Typically needed if
you manually move a sleeping body.
`;
cpBody.sleep[prosperon.DOC] = `
Force this body to sleep immediately. Useful if you know it won't move for a while.
`;
cpBody.activateStatic[prosperon.DOC] = `
Wake up any dynamic bodies touching this static/kinematic body. Optionally pass a shape
to wake only bodies that touch that shape.
:param shape: (optional) A cpShape that is part of this body.
:return: None
`;
cpBody.sleepWithGroup[prosperon.DOC] = `
Put this body to sleep as though it were in the same group as another sleeping body.
Allows linking bodies for group sleep.
:param otherBody: Another body that is already sleeping.
:return: None
`;
cpBody.applyForceAtWorldPoint[prosperon.DOC] = `
Apply a force to this body at a specific world space point. This can move or rotate the
body depending on the offset from its center of gravity.
:param force: { x, y } force vector in world coordinates.
:param point: { x, y } point in world coordinates.
:return: None
`;
cpBody.applyForceAtLocalPoint[prosperon.DOC] = `
Apply a force at a local point on the body (local coords). The bodys transform is used
to find its world effect.
:param force: { x, y } force vector in body local coordinates.
:param point: { x, y } local point relative to the body's origin.
:return: None
`;
cpBody.applyImpulseAtWorldPoint[prosperon.DOC] = `
Apply an instantaneous impulse (like a collision) at a point in world coordinates.
:param impulse: { x, y } impulse vector in world coordinates.
:param point: { x, y } where to apply the impulse, in world coordinates.
:return: None
`;
cpBody.applyImpulseAtLocalPoint[prosperon.DOC] = `
Apply an instantaneous impulse at a local point. Similar to applyForceAtLocalPoint, but
impulse is used instead of a continuous force.
:param impulse: { x, y } impulse vector in local coordinates.
:param point: { x, y } local point on the body.
:return: None
`;
cpBody.eachShape[prosperon.DOC] = `
Iterate over all cpShape objects attached to this body, calling the provided callback.
:param callback: A function(shape) that is called once per shape.
:return: None
`;
cpBody.eachConstraint[prosperon.DOC] = `
Iterate over all cpConstraint objects attached to this body, calling the provided
callback.
:param callback: A function(constraint) that is called once per constraint.
:return: None
`;
cpBody.eachArbiter[prosperon.DOC] = `
Iterate over all cpArbiters (contact pairs) for this body. Not currently implemented
for JavaScript, so its a no-op in this binding.
:return: None
`;
cpBody.add_circle_shape[prosperon.DOC] = `
Attach a circle shape to this body. Does not automatically add the shape to the space.
You may need to space.addShape(...) or store it.
:param radius: The radius of the circle shape.
:param offset: { x, y } local offset of the circle center from the bodys origin.
:return: A cpShape representing the circle.
`;
cpBody.add_segment_shape[prosperon.DOC] = `
Attach a line segment shape to this body. Does not automatically add it to the space.
Useful for edges or walls.
:param a: { x, y } start point of the segment in local coords.
:param b: { x, y } end point of the segment in local coords.
:param radius: Thickness radius for the segment.
:return: A cpShape representing the segment.
`;
cpBody.add_poly_shape[prosperon.DOC] = `
Attach a polygon shape to this body. Currently a stub that uses a placeholder for verts.
Does not automatically add the shape to the space.
:param count: Number of vertices. (Actual vertex data is not fully implemented in the stub.)
:return: A cpShape representing the polygon.
`;
//------------------------------------------------
// Shape methods and properties
//------------------------------------------------
var cpShape = prosperon.c_types.cpShape;
cpShape.getBB[prosperon.DOC] = `
Retrieve the bounding box of this shape. Returns an object with x, y, width, and height
corresponding to the bounding box in world coordinates.
:return: An object { x, y, width, height }.
`;
cpShape.collisionType[prosperon.DOC] = `
An integer used to identify the collision type of this shape. Used with collision handlers
if you have custom collision code. Assign any int to set.
:return: The collision type integer.
`;
cpShape.sensor[prosperon.DOC] = `
If true, the shape is a sensor that detects collisions without generating contact forces.
Set to false for normal collisions.
:return: Boolean indicating sensor status.
`;
cpShape.elasticity[prosperon.DOC] = `
Coefficient of restitution (bounciness). Ranges from 0 (inelastic) to 1 (fully elastic),
though values above 1 are possible (super bouncy).
:return: A number for shape elasticity.
`;
cpShape.friction[prosperon.DOC] = `
Coefficient of friction for this shape. Typically 0 for no friction to 1 or more for
high friction.
:return: A number for friction.
`;
cpShape.surfaceVelocity[prosperon.DOC] = `
Relative velocity of the shapes surface, useful for conveyors. Typically { x:0, y:0 }
for normal shapes.
:return: { x, y } velocity vector.
`;
cpShape.filter[prosperon.DOC] = `
Collision filtering parameters. An object { categories, mask, group } controlling which
objects this shape collides with. E.g., shape.filter = { categories: 1, mask: 0xFFFFFFFF, group: 0 }
:return: Object with fields categories, mask, and group.
`;
//------------------------------------------------
// Circle shape (subtype of cpShape)
//------------------------------------------------
var cpCircleShape = prosperon.c_types.cpCircleShape;
cpCircleShape.radius[prosperon.DOC] = `
The radius of this circle shape.
:return: A number representing circle radius.
`;
cpCircleShape.offset[prosperon.DOC] = `
A local offset of the circle center relative to the body's origin. Typically { x: 0, y: 0 }.
:return: { x, y } local offset.
`;
//------------------------------------------------
// Segment shape (subtype of cpShape)
//------------------------------------------------
var cpSegmentShape = prosperon.c_types.cpSegmentShape;
cpSegmentShape.setEndpoints[prosperon.DOC] = `
Change the endpoints of this line segment. Each endpoint is specified as an { x, y }
vector in the body's local coordinates.
:param startPoint: { x, y } local coordinates for the segment start.
:param endPoint: { x, y } local coordinates for the segment end.
:return: None
`;
cpSegmentShape.radius[prosperon.DOC] = `
Thickness radius of this segment shape.
:return: Number representing the segment's thickness radius.
`;
//------------------------------------------------
// Poly shape (subtype of cpShape)
//------------------------------------------------
var cpPolyShape = prosperon.c_types.cpPolyShape;
cpPolyShape.setVerts[prosperon.DOC] = `
Set the vertices of this polygon shape. Currently a stub: in reality youd pass an array
of { x, y } points to define the polygon outline.
:param count: Number of vertices (integer).
:param verts: (stub) Array of { x, y } points in local coords.
:return: None
`;
cpPolyShape.radius[prosperon.DOC] = `
Radius used to give the polygon a beveled edge. 0 for a sharp polygon, >0 for smoothing
corners.
:return: Number representing the bevel radius.
`;
//------------------------------------------------
// Constraint methods and properties
//------------------------------------------------
var cpConstraint = prosperon.c_types.cpConstraint;
cpConstraint.bodyA[prosperon.DOC] = `
Return the first body attached to this constraint.
:return: The cpBody object for body A.
`;
cpConstraint.bodyB[prosperon.DOC] = `
Return the second body attached to this constraint.
:return: The cpBody object for body B.
`;
cpConstraint.max_force[prosperon.DOC] = `
The maximum force the constraint can apply. If the constraint would need more than this
force to maintain its constraints, it won't hold them fully.
:return: Number for max force.
`;
cpConstraint.max_bias[prosperon.DOC] = `
Limits how quickly the constraint can correct errors each step. Setting a maxBias too low
can cause "soft" constraints.
:return: Number controlling maximum correction speed.
`;
cpConstraint.error_bias[prosperon.DOC] = `
Bias factor controlling how quickly overlap/penetration is corrected. Typically close to 1.0.
:return: Number (0..1 range).
`;
cpConstraint.collide_bodies[prosperon.DOC] = `
If true, the connected bodies can still collide with each other. If false, the bodies
won't collide. Usually false for "connected" objects.
:return: Boolean indicating if bodies collide.
`;
cpConstraint.broken[prosperon.DOC] = `
Check if the constraint is still in the space. Returns true if the space still contains
this constraint, false otherwise.
:return: Boolean indicating whether the constraint remains in the space.
`;
cpConstraint.break[prosperon.DOC] = `
Remove this constraint from the space immediately, effectively "breaking" it.
:return: None
`;
//------------------------------------------------
// Return the chipmunk object
//------------------------------------------------
return chipmunk;

View File

@@ -7585,6 +7585,7 @@ JSValue js_imgui_use(JSContext *js);
#include "qjs_nota.h" #include "qjs_nota.h"
#include "qjs_enet.h" #include "qjs_enet.h"
#include "qjs_soloud.h" #include "qjs_soloud.h"
#include "qjs_chipmunk.h"
#define MISTLINE(NAME) (ModuleEntry){#NAME, js_##NAME##_use} #define MISTLINE(NAME) (ModuleEntry){#NAME, js_##NAME##_use}
@@ -7610,6 +7611,7 @@ void ffi_load(JSContext *js, int argc, char **argv) {
arrput(module_registry, MISTLINE(dmon)); arrput(module_registry, MISTLINE(dmon));
arrput(module_registry, MISTLINE(nota)); arrput(module_registry, MISTLINE(nota));
arrput(module_registry, MISTLINE(enet)); arrput(module_registry, MISTLINE(enet));
arrput(module_registry, MISTLINE(chipmunk2d));
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
arrput(module_registry, MISTLINE(tracy)); arrput(module_registry, MISTLINE(tracy));

1278
source/qjs_chipmunk.c Normal file

File diff suppressed because it is too large Load Diff

8
source/qjs_chipmunk.h Normal file
View File

@@ -0,0 +1,8 @@
#ifndef QJS_CHIPMUNK_H
#define QJS_CHIPMUNK_H
#include "quickjs.h"
JSValue js_chipmunk2d_use(JSContext*);
#endif

View File

@@ -1,13 +1,5 @@
[wrap-file] [wrap-git]
directory = Chipmunk2D-Chipmunk-7.0.3 url = https://github.com/slembcke/Chipmunk2D.git
source_url = https://github.com/slembcke/Chipmunk2D/archive/Chipmunk-7.0.3.tar.gz revision = Chipmunk-7.0.3
source_filename = Chipmunk2D-Chipmunk-7.0.3.tar.gz depth = 1
source_hash = 1e6f093812d6130e45bdf4cb80280cb3c93d1e1833d8cf989d554d7963b7899a
patch_filename = chipmunk_7.0.3-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/chipmunk_7.0.3-1/get_patch
patch_hash = 90e5e1be7712812cd303974910c37e7c4f2d2c8060312521b3a7995daa54f66a
wrapdb_version = 7.0.3-1
[provide]
chipmunk = chipmunk_dep

290
tests/chipmunk2d.js Normal file
View File

@@ -0,0 +1,290 @@
// chipmunk_test.js
var chipmunk = use('chipmunk2d');
var os = use('os');
// Constants
var EPSILON = 1e-12; // Tolerance for floating-point comparisons
// Helper function to create a vector
function vec(x, y) {
return { x: x, y: y };
}
// Deep comparison function for objects and physics properties
function deepCompare(expected, actual, path = '') {
if (expected === actual) return { passed: true, messages: [] };
// Handle vector comparison
if (expected && expected.x !== undefined && expected.y !== undefined &&
actual && actual.x !== undefined && actual.y !== undefined) {
const dx = Math.abs(expected.x - actual.x);
const dy = Math.abs(expected.y - actual.y);
if (dx <= EPSILON && dy <= EPSILON) {
return { passed: true, messages: [] };
}
return {
passed: false,
messages: [
`Vector mismatch at ${path}: expected (${expected.x}, ${expected.y}), got (${actual.x}, ${actual.y})`,
`Differences: x=${dx}, y=${dy} exceed tolerance ${EPSILON}`
]
};
}
// Number comparison with tolerance
if (typeof expected === 'number' && typeof actual === 'number') {
const diff = Math.abs(expected - actual);
if (diff <= EPSILON) {
return { passed: true, messages: [] };
}
return {
passed: false,
messages: [
`Number mismatch at ${path}: expected ${expected}, got ${actual}`,
`Difference ${diff} exceeds tolerance ${EPSILON}`
]
};
}
// Array comparison
if (Array.isArray(expected) && Array.isArray(actual)) {
if (expected.length !== actual.length) {
return {
passed: false,
messages: [`Array length mismatch at ${path}: expected ${expected.length}, got ${actual.length}`]
};
}
let messages = [];
for (let i = 0; i < expected.length; i++) {
const result = deepCompare(expected[i], actual[i], `${path}[${i}]`);
if (!result.passed) messages.push(...result.messages);
}
return { passed: messages.length === 0, messages };
}
// Object comparison
if (typeof expected === 'object' && expected !== null &&
typeof actual === 'object' && actual !== null) {
const expKeys = Object.keys(expected).sort();
const actKeys = Object.keys(actual).sort();
if (JSON.stringify(expKeys) !== JSON.stringify(actKeys)) {
return {
passed: false,
messages: [`Object keys mismatch at ${path}: expected ${expKeys}, got ${actKeys}`]
};
}
let messages = [];
for (let key of expKeys) {
const result = deepCompare(expected[key], actual[key], `${path}.${key}`);
if (!result.passed) messages.push(...result.messages);
}
return { passed: messages.length === 0, messages };
}
return {
passed: false,
messages: [`Value mismatch at ${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`]
};
}
// Test cases for Chipmunk functionality
var testCases = [
// Space tests
{
name: "Space creation and gravity",
run: () => {
const space = chipmunk.make_space();
space.gravity = vec(0, -9.81);
return deepCompare(vec(0, -9.81), space.gravity);
}
},
{
name: "Space iterations",
run: () => {
const space = chipmunk.make_space();
space.iterations = 5;
return deepCompare(5, space.iterations);
}
},
// Body tests
{
name: "Body creation and position",
run: () => {
const space = chipmunk.make_space();
const body = space.body();
body.position = vec(10, 20);
return deepCompare(vec(10, 20), body.position);
}
},
{
name: "Body mass and moment",
run: () => {
const space = chipmunk.make_space();
const body = space.body();
body.type = 0
body.mass = 10;
body.moment = 100;
return {
passed: deepCompare(10, body.mass).passed && deepCompare(100, body.moment).passed,
messages: [
...deepCompare(10, body.mass).messages,
...deepCompare(100, body.moment).messages
]
};
}
},
{
name: "Body velocity",
run: () => {
const space = chipmunk.make_space();
const body = space.body();
body.velocity = vec(5, -5);
return deepCompare(vec(5, -5), body.velocity);
}
},
{
name: "Body force application",
run: () => {
const space = chipmunk.make_space();
const body = space.body();
body.type = 0
body.mass = 1;
body.moment = 1
body.applyForceAtWorldPoint(vec(10, 0), vec(0, 0));
space.step(1/60); // One frame at 60 FPS
const expectedVelocity = vec(10/60, 0); // F = ma, v = at
return deepCompare(expectedVelocity, body.velocity);
}
},
// Shape tests
{
name: "Circle shape creation",
run: () => {
const space = chipmunk.make_space();
const body = space.body();
const shape = body.circle({radius:5, offset:vec(0, 0)});
const radiusResult = deepCompare(5, shape.radius);
const offsetResult = deepCompare(vec(0, 0), shape.offset);
return {
passed: radiusResult.passed && offsetResult.passed,
messages: [...radiusResult.messages, ...offsetResult.messages]
};
}
},
{
name: "Segment shape creation",
run: () => {
const space = chipmunk.make_space();
const body = space.body();
const shape = body.add_segment_shape(vec(0, 0), vec(10, 10), 2);
for (var i in shape) console.log(i)
shape.setEndpoints(vec(1, 1), vec(11, 11));
const radiusResult = deepCompare(2, shape.radius);
// Note: Chipmunk doesn't provide endpoint getters, so we test indirectly
return radiusResult;
}
},
// Constraint tests
{
name: "Pin joint",
run: () => {
const space = chipmunk.make_space();
const body1 = space.body();
const body2 = space.body();
body1.position = vec(0, 0);
body2.position = vec(10, 0);
const joint = space.pin(body1, body2);
joint.distance = 10;
return deepCompare(10, joint.distance);
}
},
{
name: "Pivot joint",
run: () => {
const space = chipmunk.make_space();
const body1 = space.body();
const body2 = space.body();
const joint = space.pivot(body1, body2, vec(5, 5));
const anchorAResult = deepCompare(vec(5, 5), joint.anchor_a);
const anchorBResult = deepCompare(vec(5, 5), joint.anchor_b);
return {
passed: anchorAResult.passed && anchorBResult.passed,
messages: [...anchorAResult.messages, ...anchorBResult.messages]
};
}
},
// Simulation test
{
name: "Basic gravity simulation",
run: () => {
const space = chipmunk.make_space();
console.log(space.gravity)
space.gravity = vec(0, -9.81);
console.log(space.gravity)
const body = space.body();
body.circle(5);
body.mass = 1;
body.moment = 1;
body.position = vec(0, 100);
for (var i = 0; i < 61; i++) space.step(1/60)
const expectedPos = vec(0, 100 - (9.81/2)); // s = ut + (1/2)at^2
return deepCompare(expectedPos, body.position);
}
}
];
// Run tests and collect results
let results = [];
let testCount = 0;
for (let test of testCases) {
testCount++;
let testName = `Test ${testCount}: ${test.name}`;
try {
const result = test.run();
results.push({
testName,
passed: result.passed,
messages: result.messages
});
if (!result.passed) {
console.log(`\nDetailed Failure Report for ${testName}:`);
console.log(result.messages.join("\n"));
console.log("");
}
} catch (e) {
results.push({
testName,
passed: false,
messages: [`Test threw exception: ${e.message}`]
});
console.log(`\nException in ${testName}:`);
console.log(e.stack || e.message);
console.log("");
}
}
// Summary
console.log("\nTest Summary:");
results.forEach(result => {
console.log(`${result.testName} - ${result.passed ? "Passed" : "Failed"}`);
if (!result.passed) {
console.log(result.messages.map(m => " " + m).join("\n"));
}
});
let passedCount = results.filter(r => r.passed).length;
console.log(`\nResult: ${passedCount}/${testCount} tests passed`);
if (passedCount < testCount) {
console.log("Overall: FAILED");
os.exit(1);
} else {
console.log("Overall: PASSED");
os.exit(0);
}