diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..683e830 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: mlugg/setup-zig@v2 + with: + version: 0.15.2 + - run: zig build + - run: zig build test diff --git a/.gitignore b/.gitignore index e73c965..3389c86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -zig-cache/ +.zig-cache/ zig-out/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index eec9eda..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "vendor/ode"] - path = vendor/ode - url = https://bitbucket.org/odedevs/ode.git diff --git a/Sdk.zig b/Sdk.zig deleted file mode 100644 index 54b2ee1..0000000 --- a/Sdk.zig +++ /dev/null @@ -1,458 +0,0 @@ -const std = @import("std"); - -const Sdk = @This(); - -fn sdkPath(comptime suffix: []const u8) []const u8 { - if (suffix[0] != '/') @compileError("sdkPath requires an absolute path!"); - return comptime blk: { - const root_dir = std.fs.path.dirname(@src().file) orelse "."; - break :blk root_dir ++ suffix; - }; -} - -pub const Config = struct { - index_size: IndexSize = .u16, - - /// Disable built-in multithreaded threading implementation. - no_builtin_threading_impl: bool = false, - - /// Disable threading interface support (external implementations cannot be assigned. - no_threading_intf: bool = false, - - trimesh: TrimeshLibrary = .opcode, - - libccd: ?LibCcdConfig = null, - - /// Use TLS for global caches (allows threaded collision checks for separated spaces). - ou: bool = false, - - precision: Precision = .single, -}; - -/// Defines the precision which ODE uses for its API -pub const Precision = enum { single, double }; - -/// Defines the size of indices in the trimesh. -pub const IndexSize = enum { u16, u32 }; - -pub const TrimeshLibrary = enum { - /// This disables the trimesh collider - none, - - /// Use old OPCODE trimesh-trimesh collider. - opcode, - - /// Use GIMPACT for trimesh collisions (experimental). - gimpact, - - /// Use old OPCODE trimesh-trimesh collider. - opcode_old, -}; - -pub const LibCcdConfig = struct { - box_cyl: bool = true, - cap_cyl: bool = true, - cyl_cyl: bool = true, - convex_box: bool = true, - convex_cap: bool = true, - convex_convex: bool = true, - convex_cyl: bool = true, - convex_sphere: bool = true, - - /// Links the system libcc - system: bool = false, -}; - -builder: *std.build.Builder, - -translate_single_step: *std.build.TranslateCStep, -translate_double_step: *std.build.TranslateCStep, - -pub fn init(b: *std.build.Builder) *Sdk { - const sdk = b.allocator.create(Sdk) catch @panic("out of memory"); - sdk.* = Sdk{ - .builder = b, - .translate_single_step = b.addTranslateC(.{ .path = sdkPath("/include/single/template.h") }), - .translate_double_step = b.addTranslateC(.{ .path = sdkPath("/include/double/template.h") }), - }; - - sdk.translate_single_step.addIncludeDir(sdkPath("/vendor/ode/include")); - sdk.translate_double_step.addIncludeDir(sdkPath("/vendor/ode/include")); - - sdk.translate_single_step.addIncludeDir(sdkPath("/include/common")); - sdk.translate_double_step.addIncludeDir(sdkPath("/include/common")); - - sdk.translate_single_step.addIncludeDir(sdkPath("/include/single")); - sdk.translate_double_step.addIncludeDir(sdkPath("/include/double")); - - return sdk; -} - -const pkg_single = std.build.Pkg{ - .name = "precision", - .source = .{ .path = sdkPath("/src/ode-single.zig") }, -}; - -const pkg_double = std.build.Pkg{ - .name = "precision", - .source = .{ .path = sdkPath("/src/ode-double.zig") }, -}; - -pub fn getPackage(self: *Sdk, name: []const u8, config: Config) std.build.Pkg { - var prec_pkg = switch (config.precision) { - .single => pkg_single, - .double => pkg_double, - }; - var native = std.build.Pkg{ - .name = "native", - .source = switch (config.precision) { - .single => .{ .generated = &self.translate_single_step.output_file }, - .double => .{ .generated = &self.translate_double_step.output_file }, - }, - }; - return self.builder.dupePkg(std.build.Pkg{ - .name = self.builder.dupe(name), - .source = .{ .path = sdkPath("/src/ode.zig") }, - .dependencies = &[_]std.build.Pkg{ prec_pkg, native }, - }); -} - -pub fn linkTo(self: *Sdk, target: *std.build.LibExeObjStep, linkage: std.build.LibExeObjStep.Linkage, config: Config) void { - const lib = self.createCoreLibrary(linkage, config); - lib.setTarget(target.target); - lib.setBuildMode(target.build_mode); - lib.setLibCFile(target.libc_file); - target.linkLibrary(lib); - target.linkLibC(); -} - -fn createCoreLibrary(self: *Sdk, linkage: std.build.LibExeObjStep.Linkage, config: Config) *std.build.LibExeObjStep { - const lib = switch (linkage) { - .static => self.builder.addStaticLibrary("ode", null), - .dynamic => self.builder.addSharedLibrary("ode", null, .unversioned), - }; - lib.linkLibC(); - lib.linkLibCpp(); - - switch (linkage) { - .static => lib.defineCMacro("DODE_LIB", null), - .dynamic => lib.defineCMacro("DODE_DLL", null), - } - - lib.addIncludePath(sdkPath("/include/common")); - switch (config.precision) { - .single => { - lib.addIncludePath(sdkPath("/include/single")); - lib.defineCMacro("dIDESINGLE", null); - lib.defineCMacro("CCD_IDESINGLE", null); - }, - .double => { - lib.addIncludePath(sdkPath("/include/double")); - lib.defineCMacro("dIDEDOUBLE", null); - lib.defineCMacro("CCD_IDEDOUBLE", null); - }, - } - - lib.addIncludePath(sdkPath("/vendor/ode/include")); - lib.addIncludePath(sdkPath("/vendor/ode/ode/src")); - lib.addIncludePath(sdkPath("/vendor/ode/ode/src/joints")); - lib.addIncludePath(sdkPath("/vendor/ode/ou/include")); - - // if(APPLE) - // target_compile_definitions(ODE PRIVATE -DMAC_OS_X_VERSION=${MAC_OS_X_VERSION}) - // endif() - - // if(WIN32) - // target_compile_definitions(ODE PRIVATE -D_CRT_SECURE_NO_DEPRECATE -D_SCL_SECURE_NO_WARNINGS -D_USE_MATH_DEFINES) - // endif() - - // if(WIN32 OR CYGWIN) - // set(_OU_TARGET_OS _OU_TARGET_OS_WINDOWS) - // elseif(APPLE) - // set(_OU_TARGET_OS _OU_TARGET_OS_MAC) - // elseif(QNXNTO) - // set(_OU_TARGET_OS _OU_TARGET_OS_QNX) - // elseif(CMAKE_SYSTEM MATCHES "SunOS-4") - // set(_OU_TARGET_OS _OU_TARGET_OS_SUNOS) - // else() - // set(_OU_TARGET_OS _OU_TARGET_OS_GENUNIX) - // endif() - // lib.defineCMacro("_OU_TARGET_OS", "${_OU_TARGET_OS}"); - - // lib.defineCMacro("dNODEBUG", null) - - lib.defineCMacro("_OU_NAMESPACE", "odeou"); - lib.defineCMacro("_OU_FEATURE_SET", if (config.ou) - "_OU_FEATURE_SET_TLS" - else if (!config.no_threading_intf) - "_OU_FEATURE_SET_ATOMICS" - else - "_OU_FEATURE_SET_BASICS"); - lib.defineCMacro("dOU_ENABLED", null); - - if (config.ou) { - lib.defineCMacro("dATOMICS_ENABLED", null); - lib.defineCMacro("dTLS_ENABLED", null); - } else if (!config.no_threading_intf) { - lib.defineCMacro("dATOMICS_ENABLED", null); - } - - const c_flags = [_][]const u8{}; - lib.addCSourceFiles(&ode_sources, &c_flags); - - // $ - // $ - - switch (config.index_size) { - .u16 => lib.defineCMacro("dTRIMESH_16BIT_INDICES", null), - .u32 => {}, - } - - if (!config.no_builtin_threading_impl) { - lib.defineCMacro("dBUILTIN_THREADING_IMPL_ENABLED", null); - } - if (config.no_threading_intf) { - lib.defineCMacro("dTHREADING_INTF_DISABLED", null); - } - - if (config.no_builtin_threading_impl and config.no_threading_intf) { - lib.single_threaded = true; - } - - if (config.libccd) |libccd| { - lib.defineCMacro("dLIBCCD_ENABLED", null); - - if (libccd.system) { - lib.defineCMacro("dLIBCCD_SYSTEM", null); - lib.linkSystemLibrary("ccd"); - } else { - lib.addIncludePath(sdkPath("/libccd/src")); - lib.defineCMacro("dLIBCCD_INTERNAL", null); - lib.addCSourceFiles(&libccd_sources, &c_flags); - } - - lib.addCSourceFiles(&libccd_addon_sources, &c_flags); - lib.addIncludePath(sdkPath("/libccd/src/custom")); - - if (libccd.box_cyl) { - lib.defineCMacro("dLIBCCD_BOX_CYL", null); - } - if (libccd.cap_cyl) { - lib.defineCMacro("dLIBCCD_CAP_CYL", null); - } - if (libccd.cyl_cyl) { - lib.defineCMacro("dLIBCCD_CYL_CYL", null); - } - if (libccd.convex_box) { - lib.defineCMacro("dLIBCCD_CONVEX_BOX", null); - } - if (libccd.convex_cap) { - lib.defineCMacro("dLIBCCD_CONVEX_CAP", null); - } - if (libccd.convex_convex) { - lib.defineCMacro("dLIBCCD_CONVEX_CONVEX", null); - } - if (libccd.convex_cyl) { - lib.defineCMacro("dLIBCCD_CONVEX_CYL", null); - } - if (libccd.convex_sphere) { - lib.defineCMacro("dLIBCCD_CONVEX_SPHERE", null); - } - } - - switch (config.trimesh) { - .none => {}, - .opcode, .opcode_old => { - lib.addCSourceFiles(&opcode_sources, &c_flags); - - lib.defineCMacro("dTRIMESH_ENABLED", null); - lib.defineCMacro("dTRIMESH_OPCODE", null); - - if (config.trimesh == config.trimesh) { - lib.defineCMacro("dTRIMESH_OPCODE_USE_OLD_TRIMESH_TRIMESH_COLLIDER", null); - } - - lib.addIncludePath(sdkPath("/vendor/ode/OPCODE")); - lib.addIncludePath(sdkPath("/vendor/ode/OPCODE/Ice")); - }, - .gimpact => { - lib.addCSourceFiles(&gimpact_sources, &c_flags); - - lib.defineCMacro("dTRIMESH_ENABLED", null); - lib.defineCMacro("dTRIMESH_GIMPACT", null); - - lib.addIncludePath(sdkPath("/vendor/ode/GIMPACT/include")); - }, - } - - // TODO: Add all sources here - - return lib; -} - -const ode_sources = [_][]const u8{ - // ODE: - sdkPath("/vendor/ode/ode/src/array.cpp"), - sdkPath("/vendor/ode/ode/src/box.cpp"), - sdkPath("/vendor/ode/ode/src/capsule.cpp"), - sdkPath("/vendor/ode/ode/src/collision_cylinder_box.cpp"), - sdkPath("/vendor/ode/ode/src/collision_cylinder_plane.cpp"), - sdkPath("/vendor/ode/ode/src/collision_cylinder_sphere.cpp"), - sdkPath("/vendor/ode/ode/src/collision_kernel.cpp"), - sdkPath("/vendor/ode/ode/src/collision_quadtreespace.cpp"), - sdkPath("/vendor/ode/ode/src/collision_sapspace.cpp"), - sdkPath("/vendor/ode/ode/src/collision_space.cpp"), - sdkPath("/vendor/ode/ode/src/collision_transform.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_disabled.cpp"), - sdkPath("/vendor/ode/ode/src/collision_util.cpp"), - sdkPath("/vendor/ode/ode/src/convex.cpp"), - sdkPath("/vendor/ode/ode/src/cylinder.cpp"), - sdkPath("/vendor/ode/ode/src/default_threading.cpp"), - sdkPath("/vendor/ode/ode/src/error.cpp"), - sdkPath("/vendor/ode/ode/src/export-dif.cpp"), - sdkPath("/vendor/ode/ode/src/fastdot.cpp"), - sdkPath("/vendor/ode/ode/src/fastldltfactor.cpp"), - sdkPath("/vendor/ode/ode/src/fastldltsolve.cpp"), - sdkPath("/vendor/ode/ode/src/fastlsolve.cpp"), - sdkPath("/vendor/ode/ode/src/fastltsolve.cpp"), - sdkPath("/vendor/ode/ode/src/fastvecscale.cpp"), - sdkPath("/vendor/ode/ode/src/heightfield.cpp"), - sdkPath("/vendor/ode/ode/src/lcp.cpp"), - sdkPath("/vendor/ode/ode/src/mass.cpp"), - sdkPath("/vendor/ode/ode/src/mat.cpp"), - sdkPath("/vendor/ode/ode/src/matrix.cpp"), - sdkPath("/vendor/ode/ode/src/memory.cpp"), - sdkPath("/vendor/ode/ode/src/misc.cpp"), - sdkPath("/vendor/ode/ode/src/nextafterf.c"), - sdkPath("/vendor/ode/ode/src/objects.cpp"), - sdkPath("/vendor/ode/ode/src/obstack.cpp"), - sdkPath("/vendor/ode/ode/src/ode.cpp"), - sdkPath("/vendor/ode/ode/src/odeinit.cpp"), - sdkPath("/vendor/ode/ode/src/odemath.cpp"), - sdkPath("/vendor/ode/ode/src/plane.cpp"), - sdkPath("/vendor/ode/ode/src/quickstep.cpp"), - sdkPath("/vendor/ode/ode/src/ray.cpp"), - sdkPath("/vendor/ode/ode/src/resource_control.cpp"), - sdkPath("/vendor/ode/ode/src/rotation.cpp"), - sdkPath("/vendor/ode/ode/src/simple_cooperative.cpp"), - sdkPath("/vendor/ode/ode/src/sphere.cpp"), - sdkPath("/vendor/ode/ode/src/step.cpp"), - sdkPath("/vendor/ode/ode/src/threading_base.cpp"), - sdkPath("/vendor/ode/ode/src/threading_impl.cpp"), - sdkPath("/vendor/ode/ode/src/threading_pool_posix.cpp"), - sdkPath("/vendor/ode/ode/src/threading_pool_win.cpp"), - sdkPath("/vendor/ode/ode/src/timer.cpp"), - sdkPath("/vendor/ode/ode/src/util.cpp"), - sdkPath("/vendor/ode/ode/src/joints/amotor.cpp"), - sdkPath("/vendor/ode/ode/src/joints/ball.cpp"), - sdkPath("/vendor/ode/ode/src/joints/contact.cpp"), - sdkPath("/vendor/ode/ode/src/joints/dball.cpp"), - sdkPath("/vendor/ode/ode/src/joints/dhinge.cpp"), - sdkPath("/vendor/ode/ode/src/joints/fixed.cpp"), - sdkPath("/vendor/ode/ode/src/joints/hinge.cpp"), - sdkPath("/vendor/ode/ode/src/joints/hinge2.cpp"), - sdkPath("/vendor/ode/ode/src/joints/joint.cpp"), - sdkPath("/vendor/ode/ode/src/joints/lmotor.cpp"), - sdkPath("/vendor/ode/ode/src/joints/null.cpp"), - sdkPath("/vendor/ode/ode/src/joints/piston.cpp"), - sdkPath("/vendor/ode/ode/src/joints/plane2d.cpp"), - sdkPath("/vendor/ode/ode/src/joints/pr.cpp"), - sdkPath("/vendor/ode/ode/src/joints/pu.cpp"), - sdkPath("/vendor/ode/ode/src/joints/slider.cpp"), - sdkPath("/vendor/ode/ode/src/joints/transmission.cpp"), - sdkPath("/vendor/ode/ode/src/joints/universal.cpp"), - - // OU: - sdkPath("/vendor/ode/ode/src/odeou.cpp"), - sdkPath("/vendor/ode/ode/src/odetls.cpp"), - sdkPath("/vendor/ode/ou/src/ou/atomic.cpp"), - sdkPath("/vendor/ode/ou/src/ou/customization.cpp"), - sdkPath("/vendor/ode/ou/src/ou/malloc.cpp"), - sdkPath("/vendor/ode/ou/src/ou/threadlocalstorage.cpp"), -}; - -const gimpact_sources = [_][]const u8{ - sdkPath("/vendor/ode/GIMPACT/src/gim_boxpruning.cpp"), - sdkPath("/vendor/ode/GIMPACT/src/gim_contact.cpp"), - sdkPath("/vendor/ode/GIMPACT/src/gim_math.cpp"), - sdkPath("/vendor/ode/GIMPACT/src/gim_memory.cpp"), - sdkPath("/vendor/ode/GIMPACT/src/gim_tri_tri_overlap.cpp"), - sdkPath("/vendor/ode/GIMPACT/src/gim_trimesh.cpp"), - sdkPath("/vendor/ode/GIMPACT/src/gim_trimesh_capsule_collision.cpp"), - sdkPath("/vendor/ode/GIMPACT/src/gim_trimesh_ray_collision.cpp"), - sdkPath("/vendor/ode/GIMPACT/src/gim_trimesh_sphere_collision.cpp"), - sdkPath("/vendor/ode/GIMPACT/src/gim_trimesh_trimesh_collision.cpp"), - sdkPath("/vendor/ode/GIMPACT/src/gimpact.cpp"), - sdkPath("/vendor/ode/ode/src/collision_convex_trimesh.cpp"), - sdkPath("/vendor/ode/ode/src/collision_cylinder_trimesh.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_box.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_ccylinder.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_gimpact.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_internal.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_plane.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_ray.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_sphere.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_trimesh.cpp"), - sdkPath("/vendor/ode/ode/src/gimpact_contact_export_helper.cpp"), -}; - -const libccd_sources = [_][]const u8{ - sdkPath("/vendor/ode/libccd/src/alloc.c"), - sdkPath("/vendor/ode/libccd/src/ccd.c"), - sdkPath("/vendor/ode/libccd/src/mpr.c"), - sdkPath("/vendor/ode/libccd/src/polytope.c"), - sdkPath("/vendor/ode/libccd/src/support.c"), - sdkPath("/vendor/ode/libccd/src/vec3.c"), -}; - -const libccd_addon_sources = [_][]const u8{ - sdkPath("/vendor/ode/ode/src/collision_libccd.cpp"), -}; - -const opcode_sources = [_][]const u8{ - sdkPath("/vendor/ode/ode/src/collision_convex_trimesh.cpp"), - sdkPath("/vendor/ode/ode/src/collision_cylinder_trimesh.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_box.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_ccylinder.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_internal.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_opcode.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_plane.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_ray.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_sphere.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_trimesh.cpp"), - sdkPath("/vendor/ode/ode/src/collision_trimesh_trimesh_old.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_AABBCollider.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_AABBTree.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_BaseModel.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_Collider.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_Common.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_HybridModel.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_LSSCollider.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_MeshInterface.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_Model.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_OBBCollider.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_OptimizedTree.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_Picking.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_PlanesCollider.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_RayCollider.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_SphereCollider.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_TreeBuilders.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_TreeCollider.cpp"), - sdkPath("/vendor/ode/OPCODE/OPC_VolumeCollider.cpp"), - sdkPath("/vendor/ode/OPCODE/Opcode.cpp"), - sdkPath("/vendor/ode/OPCODE/Ice/IceAABB.cpp"), - sdkPath("/vendor/ode/OPCODE/Ice/IceContainer.cpp"), - sdkPath("/vendor/ode/OPCODE/Ice/IceHPoint.cpp"), - sdkPath("/vendor/ode/OPCODE/Ice/IceIndexedTriangle.cpp"), - sdkPath("/vendor/ode/OPCODE/Ice/IceMatrix3x3.cpp"), - sdkPath("/vendor/ode/OPCODE/Ice/IceMatrix4x4.cpp"), - sdkPath("/vendor/ode/OPCODE/Ice/IceOBB.cpp"), - sdkPath("/vendor/ode/OPCODE/Ice/IcePlane.cpp"), - sdkPath("/vendor/ode/OPCODE/Ice/IcePoint.cpp"), - sdkPath("/vendor/ode/OPCODE/Ice/IceRandom.cpp"), - sdkPath("/vendor/ode/OPCODE/Ice/IceRay.cpp"), - sdkPath("/vendor/ode/OPCODE/Ice/IceRevisitedRadix.cpp"), - sdkPath("/vendor/ode/OPCODE/Ice/IceSegment.cpp"), - sdkPath("/vendor/ode/OPCODE/Ice/IceTriangle.cpp"), - sdkPath("/vendor/ode/OPCODE/Ice/IceUtils.cpp"), -}; diff --git a/build.zig b/build.zig index 28e9805..e348750 100644 --- a/build.zig +++ b/build.zig @@ -1,17 +1,552 @@ const std = @import("std"); -pub fn build(b: *std.build.Builder) void { - // Standard release options allow the person running `zig build` to select - // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. - const mode = b.standardReleaseOptions(); +pub const Precision = enum { single, double }; +pub const TrimeshLibrary = enum { none, opcode, gimpact, opcode_old }; +pub const IndexSize = enum { u16, u32 }; - const lib = b.addStaticLibrary("zig-ode", "src/main.zig"); - lib.setBuildMode(mode); - lib.install(); +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); - var main_tests = b.addTest("src/main.zig"); - main_tests.setBuildMode(mode); + const precision = b.option(Precision, "precision", "ODE floating-point precision") orelse .single; + const trimesh = b.option(TrimeshLibrary, "trimesh", "Trimesh collision library") orelse .opcode; + const index_size = b.option(IndexSize, "index_size", "Trimesh index size") orelse .u16; + const no_builtin_threading_impl = b.option(bool, "no_builtin_threading_impl", "Disable built-in multithreaded threading implementation") orelse false; + const no_threading_intf = b.option(bool, "no_threading_intf", "Disable threading interface support") orelse false; + const ou = b.option(bool, "ou", "Use TLS for global caches (allows threaded collision checks for separated spaces)") orelse false; + const libccd = b.option(bool, "libccd", "Enable libccd collision detection") orelse false; + const libccd_system = b.option(bool, "libccd_system", "Link system libccd instead of compiling internal") orelse false; + const libccd_box_cyl = b.option(bool, "libccd_box_cyl", "Enable libccd box-cylinder collisions") orelse true; + const libccd_cap_cyl = b.option(bool, "libccd_cap_cyl", "Enable libccd capsule-cylinder collisions") orelse true; + const libccd_cyl_cyl = b.option(bool, "libccd_cyl_cyl", "Enable libccd cylinder-cylinder collisions") orelse true; + const libccd_convex_box = b.option(bool, "libccd_convex_box", "Enable libccd convex-box collisions") orelse true; + const libccd_convex_cap = b.option(bool, "libccd_convex_cap", "Enable libccd convex-capsule collisions") orelse true; + const libccd_convex_convex = b.option(bool, "libccd_convex_convex", "Enable libccd convex-convex collisions") orelse true; + const libccd_convex_cyl = b.option(bool, "libccd_convex_cyl", "Enable libccd convex-cylinder collisions") orelse true; + const libccd_convex_sphere = b.option(bool, "libccd_convex_sphere", "Enable libccd convex-sphere collisions") orelse true; - const test_step = b.step("test", "Run library tests"); - test_step.dependOn(&main_tests.step); + const upstream = b.dependency("ode", .{}); + + const lib = b.addLibrary(.{ + .name = "ode", + .linkage = .static, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + .link_libcpp = true, + }), + }); + + const lib_mod = lib.root_module; + + lib_mod.addCMacro("DODE_LIB", "1"); + + // Shared include paths and precision macros (used by both C lib and Zig module) + configureOdeIncludes(lib_mod, b, upstream, precision, trimesh, index_size); + + // Additional internal include paths (C lib only) + lib_mod.addIncludePath(upstream.path("ode/src")); + lib_mod.addIncludePath(upstream.path("ode/src/joints")); + lib_mod.addIncludePath(upstream.path("ou/include")); + + // Platform-specific configuration + if (target.result.os.tag.isDarwin()) { + lib_mod.addCMacro("MAC_OS_X_VERSION", "1050"); + } + + // OU configuration + lib_mod.addCMacro("_OU_NAMESPACE", "odeou"); + lib_mod.addCMacro("_OU_FEATURE_SET", if (ou) + "_OU_FEATURE_SET_TLS" + else if (!no_threading_intf) + "_OU_FEATURE_SET_ATOMICS" + else + "_OU_FEATURE_SET_BASICS"); + lib_mod.addCMacro("dOU_ENABLED", "1"); + + if (ou) { + lib_mod.addCMacro("dATOMICS_ENABLED", "1"); + lib_mod.addCMacro("dTLS_ENABLED", "1"); + } else if (!no_threading_intf) { + lib_mod.addCMacro("dATOMICS_ENABLED", "1"); + } + + // Threading configuration + if (!no_builtin_threading_impl) { + lib_mod.addCMacro("dBUILTIN_THREADING_IMPL_ENABLED", "1"); + } + if (no_threading_intf) { + lib_mod.addCMacro("dTHREADING_INTF_DISABLED", "1"); + } + if (no_builtin_threading_impl and no_threading_intf) { + lib_mod.single_threaded = true; + } + + const cpp_flags: []const []const u8 = &.{ + "-Wno-implicit-int-float-conversion", + "-Wno-implicit-float-conversion", + "-Wno-implicit-const-int-float-conversion", + "-Wno-deprecated-declarations", + "-Wno-null-dereference", + }; + + // Disable ODE internal assertions in non-Debug builds (matches CMake behavior) + if (optimize != .Debug) { + lib_mod.addCMacro("dNODEBUG", "1"); + } + + // Core ODE sources + lib_mod.addCSourceFiles(.{ + .root = upstream.path(""), + .files = ode_sources, + .flags = cpp_flags, + }); + + // lcp.cpp has an upstream bug: an uninitialized bool array is swapped before + // being written, which is harmless but triggers UBSan. Compile it separately + // with bool sanitization disabled so dWorldStep works in Debug builds. + lib_mod.addCSourceFiles(.{ + .root = upstream.path(""), + .files = &.{"ode/src/lcp.cpp"}, + .flags = cpp_flags ++ &[_][]const u8{"-fno-sanitize=bool"}, + }); + + // Libccd configuration + if (libccd) { + lib_mod.addCMacro("dLIBCCD_ENABLED", "1"); + + if (libccd_system) { + lib_mod.addCMacro("dLIBCCD_SYSTEM", "1"); + lib_mod.linkSystemLibrary("ccd", .{}); + } else { + lib_mod.addIncludePath(upstream.path("libccd/src")); + lib_mod.addCMacro("dLIBCCD_INTERNAL", "1"); + lib_mod.addCSourceFiles(.{ + .root = upstream.path(""), + .files = libccd_sources, + .flags = cpp_flags, + }); + } + + lib_mod.addCSourceFiles(.{ + .root = upstream.path(""), + .files = libccd_addon_sources, + .flags = cpp_flags, + }); + lib_mod.addIncludePath(upstream.path("libccd/src/custom")); + + if (libccd_box_cyl) lib_mod.addCMacro("dLIBCCD_BOX_CYL", "1"); + if (libccd_cap_cyl) lib_mod.addCMacro("dLIBCCD_CAP_CYL", "1"); + if (libccd_cyl_cyl) lib_mod.addCMacro("dLIBCCD_CYL_CYL", "1"); + if (libccd_convex_box) lib_mod.addCMacro("dLIBCCD_CONVEX_BOX", "1"); + if (libccd_convex_cap) lib_mod.addCMacro("dLIBCCD_CONVEX_CAP", "1"); + if (libccd_convex_convex) lib_mod.addCMacro("dLIBCCD_CONVEX_CONVEX", "1"); + if (libccd_convex_cyl) lib_mod.addCMacro("dLIBCCD_CONVEX_CYL", "1"); + if (libccd_convex_sphere) lib_mod.addCMacro("dLIBCCD_CONVEX_SPHERE", "1"); + } + + // Trimesh configuration + switch (trimesh) { + .none => {}, + .opcode, .opcode_old => { + lib_mod.addCSourceFiles(.{ + .root = upstream.path(""), + .files = opcode_sources, + .flags = cpp_flags, + }); + + if (trimesh == .opcode_old) { + lib_mod.addCMacro("dTRIMESH_OPCODE_USE_OLD_TRIMESH_TRIMESH_COLLIDER", "1"); + } + + lib_mod.addIncludePath(upstream.path("OPCODE")); + lib_mod.addIncludePath(upstream.path("OPCODE/Ice")); + }, + .gimpact => { + lib_mod.addCSourceFiles(.{ + .root = upstream.path(""), + .files = gimpact_sources, + .flags = cpp_flags, + }); + + lib_mod.addIncludePath(upstream.path("GIMPACT/include")); + }, + } + + b.installArtifact(lib); + + // Zig module + const mod = b.addModule("ode", .{ + .root_source_file = b.path("src/ode.zig"), + }); + configureOdeIncludes(mod, b, upstream, precision, trimesh, index_size); + mod.linkLibrary(lib); + + // Tests + const tests = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path("src/ode.zig"), + .target = target, + .optimize = optimize, + }), + }); + configureOdeIncludes(tests.root_module, b, upstream, precision, trimesh, index_size); + tests.root_module.linkLibrary(lib); + const run_tests = b.addRunArtifact(tests); + b.step("test", "Run ODE binding tests").dependOn(&run_tests.step); + + // Examples + const examples = [_]struct { name: []const u8, path: []const u8 }{ + .{ .name = "chain", .path = "examples/chain.zig" }, + .{ .name = "hinge", .path = "examples/hinge.zig" }, + .{ .name = "slider", .path = "examples/slider.zig" }, + }; + + for (examples) |ex| { + const exe = b.addExecutable(.{ + .name = ex.name, + .root_module = b.createModule(.{ + .root_source_file = b.path(ex.path), + .target = target, + .optimize = optimize, + .imports = &.{.{ .name = "ode", .module = mod }}, + }), + }); + b.installArtifact(exe); + const run = b.addRunArtifact(exe); + b.step(b.fmt("example-{s}", .{ex.name}), b.fmt("Run {s} example", .{ex.name})).dependOn(&run.step); + } +} + +fn configureOdeIncludes( + m: *std.Build.Module, + b: *std.Build, + upstream: *std.Build.Dependency, + precision: Precision, + trimesh: TrimeshLibrary, + index_size: IndexSize, +) void { + // Generate config headers at build time (replaces checked-in include/ directory). + // The upstream ODE headers expect ode/precision.h, ode/version.h, and config.h + // which are normally produced by CMake's configure_file(). + const gen = b.addWriteFiles(); + _ = gen.add("config.h", config_h); + _ = gen.add("ode/precision.h", precision_h); + _ = gen.add("ode/version.h", version_h); + m.addIncludePath(gen.getDirectory()); + + m.addIncludePath(upstream.path("include")); + switch (precision) { + .single => { + m.addCMacro("dIDESINGLE", "1"); + m.addCMacro("CCD_IDESINGLE", "1"); + }, + .double => { + m.addCMacro("dIDEDOUBLE", "1"); + m.addCMacro("CCD_IDEDOUBLE", "1"); + }, + } + switch (trimesh) { + .none => {}, + .opcode, .opcode_old => { + m.addCMacro("dTRIMESH_ENABLED", "1"); + m.addCMacro("dTRIMESH_OPCODE", "1"); + }, + .gimpact => { + m.addCMacro("dTRIMESH_ENABLED", "1"); + m.addCMacro("dTRIMESH_GIMPACT", "1"); + }, + } + switch (index_size) { + .u16 => m.addCMacro("dTRIMESH_16BIT_INDICES", "1"), + .u32 => {}, + } } + +const precision_h = + \\#ifndef _ODE_PRECISION_H_ + \\#define _ODE_PRECISION_H_ + \\ + \\#if defined(dIDESINGLE) + \\#define dSINGLE + \\#elif defined(dIDEDOUBLE) + \\#define dDOUBLE + \\#else + \\#error "ODE precision not defined: set dIDESINGLE or dIDEDOUBLE" + \\#endif + \\ + \\#endif + \\ +; + +const version_h = + \\#ifndef _ODE_VERSION_H_ + \\#define _ODE_VERSION_H_ + \\ + \\#define dODE_VERSION "0.16.0" + \\ + \\#endif + \\ +; + +const config_h = + \\#ifndef ODE_CONFIG_H + \\#define ODE_CONFIG_H + \\ + \\/* Platform identification */ + \\#if defined(_XENON) + \\#define ODE_PLATFORM_XBOX360 + \\#elif defined(SN_TARGET_PSP_HW) + \\#define ODE_PLATFORM_PSP + \\#elif defined(SN_TARGET_PS3) + \\#define ODE_PLATFORM_PS3 + \\#elif defined(_MSC_VER) || defined(__CYGWIN__) || defined(__MINGW32__) + \\#define ODE_PLATFORM_WINDOWS + \\#elif defined(__linux__) + \\#define ODE_PLATFORM_LINUX + \\#elif defined(__APPLE__) && defined(__MACH__) + \\#define ODE_PLATFORM_OSX + \\#elif defined(__FreeBSD__) + \\#define ODE_PLATFORM_FREEBSD + \\#else + \\#error "Need some help identifying the platform!" + \\#endif + \\ + \\#if defined(ODE_PLATFORM_WINDOWS) && !defined(WIN32) + \\#define WIN32 + \\#endif + \\ + \\#if defined(__CYGWIN__) || defined(__MINGW32__) + \\#define CYGWIN + \\#endif + \\ + \\#if defined(ODE_PLATFORM_OSX) + \\#define macintosh + \\#endif + \\ + \\/* POSIX features */ + \\#if !defined(ODE_PLATFORM_WINDOWS) + \\#define HAVE_ALLOCA_H 1 + \\#define HAVE_GETTIMEOFDAY 1 + \\#define HAVE_SYS_TIME_H 1 + \\#define HAVE_UNISTD_H 1 + \\#endif + \\ + \\#if !defined(__APPLE__) + \\#define HAVE_MALLOC_H 1 + \\#endif + \\ + \\/* Standard C99+ headers */ + \\#define HAVE_STDINT_H 1 + \\#define HAVE_INTTYPES_H 1 + \\#define HAVE_SYS_TYPES_H 1 + \\ + \\/* isnan variants */ + \\#define HAVE_ISNAN 1 + \\#if !defined(_MSC_VER) + \\#define HAVE_ISNANF 1 + \\#define HAVE___ISNAN 1 + \\#define HAVE___ISNANF 1 + \\#endif + \\#if defined(_MSC_VER) + \\#define HAVE__ISNAN 1 + \\#define HAVE__ISNANF 1 + \\#endif + \\ + \\/* pthread features */ + \\#if !defined(__APPLE__) && !defined(ODE_PLATFORM_WINDOWS) + \\#define HAVE_PTHREAD_CONDATTR_SETCLOCK 1 + \\#endif + \\ + \\/* Apple OpenGL framework */ + \\#if defined(ODE_PLATFORM_OSX) + \\#define HAVE_APPLE_OPENGL_FRAMEWORK 1 + \\#endif + \\ + \\#ifdef HAVE_ALLOCA_H + \\#include + \\#endif + \\ + \\#ifdef HAVE_MALLOC_H + \\#include + \\#endif + \\ + \\#ifdef HAVE_STDINT_H + \\#include + \\#endif + \\ + \\#ifdef HAVE_INTTYPES_H + \\#include + \\#endif + \\ + \\#include "typedefs.h" + \\ + \\#endif /* ODE_CONFIG_H */ + \\ +; + +const ode_sources: []const []const u8 = &.{ + "ode/src/array.cpp", + "ode/src/box.cpp", + "ode/src/capsule.cpp", + "ode/src/collision_cylinder_box.cpp", + "ode/src/collision_cylinder_plane.cpp", + "ode/src/collision_cylinder_sphere.cpp", + "ode/src/collision_kernel.cpp", + "ode/src/collision_quadtreespace.cpp", + "ode/src/collision_sapspace.cpp", + "ode/src/collision_space.cpp", + "ode/src/collision_transform.cpp", + "ode/src/collision_trimesh_disabled.cpp", + "ode/src/collision_util.cpp", + "ode/src/convex.cpp", + "ode/src/cylinder.cpp", + "ode/src/default_threading.cpp", + "ode/src/error.cpp", + "ode/src/export-dif.cpp", + "ode/src/fastdot.cpp", + "ode/src/fastldltfactor.cpp", + "ode/src/fastldltsolve.cpp", + "ode/src/fastlsolve.cpp", + "ode/src/fastltsolve.cpp", + "ode/src/fastvecscale.cpp", + "ode/src/heightfield.cpp", + // lcp.cpp compiled separately with -fno-sanitize=bool (see above) + "ode/src/mass.cpp", + "ode/src/mat.cpp", + "ode/src/matrix.cpp", + "ode/src/memory.cpp", + "ode/src/misc.cpp", + "ode/src/nextafterf.c", + "ode/src/objects.cpp", + "ode/src/obstack.cpp", + "ode/src/ode.cpp", + "ode/src/odeinit.cpp", + "ode/src/odemath.cpp", + "ode/src/plane.cpp", + "ode/src/quickstep.cpp", + "ode/src/ray.cpp", + "ode/src/resource_control.cpp", + "ode/src/rotation.cpp", + "ode/src/simple_cooperative.cpp", + "ode/src/sphere.cpp", + "ode/src/step.cpp", + "ode/src/threading_base.cpp", + "ode/src/threading_impl.cpp", + "ode/src/threading_pool_posix.cpp", + "ode/src/threading_pool_win.cpp", + "ode/src/timer.cpp", + "ode/src/util.cpp", + "ode/src/joints/amotor.cpp", + "ode/src/joints/ball.cpp", + "ode/src/joints/contact.cpp", + "ode/src/joints/dball.cpp", + "ode/src/joints/dhinge.cpp", + "ode/src/joints/fixed.cpp", + "ode/src/joints/hinge.cpp", + "ode/src/joints/hinge2.cpp", + "ode/src/joints/joint.cpp", + "ode/src/joints/lmotor.cpp", + "ode/src/joints/null.cpp", + "ode/src/joints/piston.cpp", + "ode/src/joints/plane2d.cpp", + "ode/src/joints/pr.cpp", + "ode/src/joints/pu.cpp", + "ode/src/joints/slider.cpp", + "ode/src/joints/transmission.cpp", + "ode/src/joints/universal.cpp", + // OU: + "ode/src/odeou.cpp", + "ode/src/odetls.cpp", + "ou/src/ou/atomic.cpp", + "ou/src/ou/customization.cpp", + "ou/src/ou/malloc.cpp", + "ou/src/ou/threadlocalstorage.cpp", +}; + +const opcode_sources: []const []const u8 = &.{ + "ode/src/collision_convex_trimesh.cpp", + "ode/src/collision_cylinder_trimesh.cpp", + "ode/src/collision_trimesh_box.cpp", + "ode/src/collision_trimesh_ccylinder.cpp", + "ode/src/collision_trimesh_internal.cpp", + "ode/src/collision_trimesh_opcode.cpp", + "ode/src/collision_trimesh_plane.cpp", + "ode/src/collision_trimesh_ray.cpp", + "ode/src/collision_trimesh_sphere.cpp", + "ode/src/collision_trimesh_trimesh.cpp", + "ode/src/collision_trimesh_trimesh_old.cpp", + "OPCODE/OPC_AABBCollider.cpp", + "OPCODE/OPC_AABBTree.cpp", + "OPCODE/OPC_BaseModel.cpp", + "OPCODE/OPC_Collider.cpp", + "OPCODE/OPC_Common.cpp", + "OPCODE/OPC_HybridModel.cpp", + "OPCODE/OPC_LSSCollider.cpp", + "OPCODE/OPC_MeshInterface.cpp", + "OPCODE/OPC_Model.cpp", + "OPCODE/OPC_OBBCollider.cpp", + "OPCODE/OPC_OptimizedTree.cpp", + "OPCODE/OPC_Picking.cpp", + "OPCODE/OPC_PlanesCollider.cpp", + "OPCODE/OPC_RayCollider.cpp", + "OPCODE/OPC_SphereCollider.cpp", + "OPCODE/OPC_TreeBuilders.cpp", + "OPCODE/OPC_TreeCollider.cpp", + "OPCODE/OPC_VolumeCollider.cpp", + "OPCODE/Opcode.cpp", + "OPCODE/Ice/IceAABB.cpp", + "OPCODE/Ice/IceContainer.cpp", + "OPCODE/Ice/IceHPoint.cpp", + "OPCODE/Ice/IceIndexedTriangle.cpp", + "OPCODE/Ice/IceMatrix3x3.cpp", + "OPCODE/Ice/IceMatrix4x4.cpp", + "OPCODE/Ice/IceOBB.cpp", + "OPCODE/Ice/IcePlane.cpp", + "OPCODE/Ice/IcePoint.cpp", + "OPCODE/Ice/IceRandom.cpp", + "OPCODE/Ice/IceRay.cpp", + "OPCODE/Ice/IceRevisitedRadix.cpp", + "OPCODE/Ice/IceSegment.cpp", + "OPCODE/Ice/IceTriangle.cpp", + "OPCODE/Ice/IceUtils.cpp", +}; + +const gimpact_sources: []const []const u8 = &.{ + "GIMPACT/src/gim_boxpruning.cpp", + "GIMPACT/src/gim_contact.cpp", + "GIMPACT/src/gim_math.cpp", + "GIMPACT/src/gim_memory.cpp", + "GIMPACT/src/gim_tri_tri_overlap.cpp", + "GIMPACT/src/gim_trimesh.cpp", + "GIMPACT/src/gim_trimesh_capsule_collision.cpp", + "GIMPACT/src/gim_trimesh_ray_collision.cpp", + "GIMPACT/src/gim_trimesh_sphere_collision.cpp", + "GIMPACT/src/gim_trimesh_trimesh_collision.cpp", + "GIMPACT/src/gimpact.cpp", + "ode/src/collision_convex_trimesh.cpp", + "ode/src/collision_cylinder_trimesh.cpp", + "ode/src/collision_trimesh_box.cpp", + "ode/src/collision_trimesh_ccylinder.cpp", + "ode/src/collision_trimesh_gimpact.cpp", + "ode/src/collision_trimesh_internal.cpp", + "ode/src/collision_trimesh_plane.cpp", + "ode/src/collision_trimesh_ray.cpp", + "ode/src/collision_trimesh_sphere.cpp", + "ode/src/collision_trimesh_trimesh.cpp", + "ode/src/gimpact_contact_export_helper.cpp", +}; + +const libccd_sources: []const []const u8 = &.{ + "libccd/src/alloc.c", + "libccd/src/ccd.c", + "libccd/src/mpr.c", + "libccd/src/polytope.c", + "libccd/src/support.c", + "libccd/src/vec3.c", +}; + +const libccd_addon_sources: []const []const u8 = &.{ + "ode/src/collision_libccd.cpp", +}; diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..02d0aad --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,18 @@ +.{ + .name = .ode, + .version = "0.0.0", + .fingerprint = 0x80836e7b100ffbf9, + .paths = .{ + "build.zig", + "build.zig.zon", + "include", + "src", + "examples", + }, + .dependencies = .{ + .ode = .{ + .url = "https://bitbucket.org/odedevs/ode/get/52c5632958de8471a89918a293a722e0234c430b.tar.gz", + .hash = "N-V-__8AALixngDBh5gZrIvXOND6ljBaL00GnKhFLpRNxU9-", + }, + }, +} diff --git a/examples/chain.zig b/examples/chain.zig new file mode 100644 index 0000000..77eeebb --- /dev/null +++ b/examples/chain.zig @@ -0,0 +1,128 @@ +/// Headless port of ODE demo_chain1.c +/// +/// 10 spheres linked by ball-and-socket joints, falling under gravity, +/// colliding with a ground plane. An oscillating force drives the chain end. +/// Prints the position of the last body each step. +const std = @import("std"); +const ode = @import("ode"); + +const Real = ode.Real; +const Body = ode.Body; +const World = ode.World; +const Mass = ode.Mass; +const Joint = ode.Joint; +const Space = ode.Space; +const Geom = ode.Geom; +const collision = ode.collision; + +const NUM = 10; +const SIDE = 0.2; +const RADIUS = 0.1732; +const STEP_SIZE = 0.05; +const NUM_STEPS = 200; +const MAX_CONTACTS = 4; + +var world: World = undefined; +var contact_group: Joint.Group = undefined; + +var bodies: [NUM]Body = undefined; +var joints: [NUM - 1]Joint.Ball = undefined; +var spheres: [NUM]Geom.Sphere = undefined; + +fn near_callback(_: ?*anyopaque, o1: ode.c.dGeomID, o2: ode.c.dGeomID) callconv(.c) void { + const g1 = Geom.Generic{ .id = o1 }; + const g2 = Geom.Generic{ .id = o2 }; + + var contact_geoms: [MAX_CONTACTS]collision.ContactGeom = undefined; + const contacts = collision.collide(g1, g2, &contact_geoms); + if (contacts.len == 0) return; + + for (contacts) |*cg| { + var contact: collision.Contact = std.mem.zeroes(collision.Contact); + contact.surface.mode = collision.contact_bounce | collision.contact_soft_cfm; + contact.surface.mu = @as(Real, std.math.inf(f64)); + contact.surface.bounce = 0.1; + contact.surface.bounce_vel = 0.1; + contact.surface.soft_cfm = 0.01; + contact.geom = cg.*; + + const cj = Joint.Contact.create(world, contact_group, &contact); + cj.attach(g1.get_body(), g2.get_body()); + } +} + +pub fn main() !void { + var buf: [4096]u8 = undefined; + var stdout = std.fs.File.stdout().writer(&buf); + + std.debug.assert(ode.init.init_ode2(.{})); + defer ode.init.close_ode(); + std.debug.assert(ode.init.allocate_data_for_thread(.{ .collision_data = true })); + + world = World.create(); + defer world.destroy(); + + const space = Space.Hash.create(null); + defer space.destroy(); + + contact_group = Joint.Group.create(); + defer contact_group.destroy(); + + world.set_gravity(.{ 0, 0, -0.5 }); + _ = Geom.Plane.create(space.to_generic(), 0, 0, 1, 0); + + // Create bodies, masses, and geoms + for (0..NUM) |i| { + bodies[i] = world.create_body(); + const k: Real = @floatFromInt(i); + bodies[i].set_position(.{ k * SIDE, 0, k * SIDE + 0.4 }); + + var mass = Mass.sphere(1.0, RADIUS); + mass.adjust(1.0); + bodies[i].set_mass(&mass); + + spheres[i] = Geom.Sphere.create(space.to_generic(), RADIUS); + spheres[i].set_body(bodies[i]); + } + + // Create ball joints between adjacent bodies + for (0..NUM - 1) |i| { + joints[i] = Joint.Ball.create(world, null); + joints[i].attach(bodies[i], bodies[i + 1]); + const k: Real = @floatFromInt(i); + joints[i].set_anchor(.{ (k + 0.5) * SIDE, 0, (k + 0.5) * SIDE + 0.4 }); + } + + try stdout.interface.print("step,x,y,z\n", .{}); + + for (0..NUM_STEPS) |step| { + // Apply oscillating force to the last body + const t: Real = @as(Real, @floatFromInt(step)) * STEP_SIZE; + const fx = 0.3 * @sin(t * 4.0); + const fz = 0.3 * @cos(t * 4.0); + bodies[NUM - 1].add_force(.{ fx, 0, fz }); + + // Collision detection + space.collide(null, &near_callback); + + // Step the world + _ = world.step(STEP_SIZE); + + // Remove contact joints + contact_group.empty(); + + // Print the last body's position + const pos = bodies[NUM - 1].get_position(); + try stdout.interface.print("{d},{d:.6},{d:.6},{d:.6}\n", .{ step, pos[0], pos[1], pos[2] }); + } + try stdout.interface.flush(); + + // Cleanup bodies and geoms + for (0..NUM) |i| { + spheres[i].destroy(); + bodies[i].destroy(); + } + for (0..NUM - 1) |i| { + joints[i].destroy(); + } +} diff --git a/examples/hinge.zig b/examples/hinge.zig new file mode 100644 index 0000000..b37134b --- /dev/null +++ b/examples/hinge.zig @@ -0,0 +1,78 @@ +/// Headless port of ODE demo_hinge.cpp +/// +/// Two boxes connected by a hinge joint. Oscillating torque is applied and +/// angular velocity damping keeps things stable. No collision. +/// Prints the hinge angle and angular rate each step. +const std = @import("std"); +const ode = @import("ode"); + +const Real = ode.Real; + +const STEP_SIZE = 0.05; +const NUM_STEPS = 200; + +pub fn main() !void { + var buf: [4096]u8 = undefined; + var stdout = std.fs.File.stdout().writer(&buf); + + std.debug.assert(ode.init.init_ode2(.{})); + defer ode.init.close_ode(); + + const world = ode.World.create(); + defer world.destroy(); + + world.set_gravity(.{ 0, 0, -9.81 }); + + // Body 1: fixed to the world via the hinge + const body1 = world.create_body(); + defer body1.destroy(); + body1.set_position(.{ 0, 0, 1.0 }); + var mass1 = ode.Mass.box(1.0, 0.4, 0.4, 0.4); + mass1.adjust(1.0); + body1.set_mass(&mass1); + + // Body 2: connected to body1 via hinge + const body2 = world.create_body(); + defer body2.destroy(); + body2.set_position(.{ 0.6, 0, 1.0 }); + var mass2 = ode.Mass.box(1.0, 0.4, 0.4, 0.4); + mass2.adjust(1.0); + body2.set_mass(&mass2); + + // Create hinge joint between body1 and body2 + const hinge = ode.Joint.Hinge.create(world, null); + defer hinge.destroy(); + hinge.attach(body1, body2); + hinge.set_anchor(.{ 0.3, 0, 1.0 }); + hinge.set_axis(.{ 0, 1, 0 }); // rotate around Y + + // Pin body1 to the world with a fixed joint equivalent: + // attach body1 to static environment with a hinge at its center + const anchor_hinge = ode.Joint.Hinge.create(world, null); + defer anchor_hinge.destroy(); + anchor_hinge.attach(body1, null); + anchor_hinge.set_anchor(.{ 0, 0, 1.0 }); + anchor_hinge.set_axis(.{ 0, 1, 0 }); + + try stdout.interface.print("step,angle,angle_rate\n", .{}); + + for (0..NUM_STEPS) |step| { + // Apply oscillating torque about the hinge axis + const t: Real = @as(Real, @floatFromInt(step)) * STEP_SIZE; + const torque = 0.5 * @sin(t * 3.0); + hinge.add_torque(torque); + + // Simple angular velocity damping on both bodies + const av1 = body1.get_angular_vel(); + body1.add_torque(.{ -0.1 * av1[0], -0.1 * av1[1], -0.1 * av1[2] }); + const av2 = body2.get_angular_vel(); + body2.add_torque(.{ -0.1 * av2[0], -0.1 * av2[1], -0.1 * av2[2] }); + + _ = world.step(STEP_SIZE); + + const angle = hinge.get_angle(); + const rate = hinge.get_angle_rate(); + try stdout.interface.print("{d},{d:.6},{d:.6}\n", .{ step, angle, rate }); + } + try stdout.interface.flush(); +} diff --git a/examples/slider.zig b/examples/slider.zig new file mode 100644 index 0000000..1034ef8 --- /dev/null +++ b/examples/slider.zig @@ -0,0 +1,84 @@ +/// Headless port of ODE demo_slider.cpp +/// +/// Two boxes on a slider (prismatic) joint with a manual spring force +/// holding them together and oscillating torque. No collision. +/// Prints the slider position and velocity each step. +const std = @import("std"); +const ode = @import("ode"); + +const Real = ode.Real; + +const STEP_SIZE = 0.05; +const NUM_STEPS = 200; + +pub fn main() !void { + var buf: [4096]u8 = undefined; + var stdout = std.fs.File.stdout().writer(&buf); + + std.debug.assert(ode.init.init_ode2(.{})); + defer ode.init.close_ode(); + + const world = ode.World.create(); + defer world.destroy(); + + world.set_gravity(.{ 0, 0, -9.81 }); + + // Body 1 + const body1 = world.create_body(); + defer body1.destroy(); + body1.set_position(.{ 0, 0, 1.5 }); + var mass1 = ode.Mass.box(1.0, 0.3, 0.3, 0.3); + mass1.adjust(1.0); + body1.set_mass(&mass1); + + // Body 2 + const body2 = world.create_body(); + defer body2.destroy(); + body2.set_position(.{ 0.5, 0, 1.5 }); + var mass2 = ode.Mass.box(1.0, 0.3, 0.3, 0.3); + mass2.adjust(1.0); + body2.set_mass(&mass2); + + // Slider joint along X axis + const slider = ode.Joint.Slider.create(world, null); + defer slider.destroy(); + slider.attach(body1, body2); + slider.set_axis(.{ 1, 0, 0 }); + + // Pin body1 to world with a hinge so the whole assembly doesn't fall + const anchor_hinge = ode.Joint.Hinge.create(world, null); + defer anchor_hinge.destroy(); + anchor_hinge.attach(body1, null); + anchor_hinge.set_anchor(.{ 0, 0, 1.5 }); + anchor_hinge.set_axis(.{ 0, 1, 0 }); + + try stdout.interface.print("step,position,velocity\n", .{}); + + for (0..NUM_STEPS) |step| { + // Spring force pulling bodies together (Hooke's law) + const pos = slider.get_position(); + const vel = slider.get_position_rate(); + const spring_k: Real = 5.0; + const damping_c: Real = 0.5; + const spring_force = -spring_k * pos - damping_c * vel; + slider.add_force(spring_force); + + // Oscillating torque on the anchor hinge + const t: Real = @as(Real, @floatFromInt(step)) * STEP_SIZE; + const torque = 0.3 * @sin(t * 2.0); + anchor_hinge.add_torque(torque); + + // Angular damping + const av1 = body1.get_angular_vel(); + body1.add_torque(.{ -0.1 * av1[0], -0.1 * av1[1], -0.1 * av1[2] }); + const av2 = body2.get_angular_vel(); + body2.add_torque(.{ -0.1 * av2[0], -0.1 * av2[1], -0.1 * av2[2] }); + + _ = world.step(STEP_SIZE); + + const final_pos = slider.get_position(); + const final_vel = slider.get_position_rate(); + try stdout.interface.print("{d},{d:.6},{d:.6}\n", .{ step, final_pos, final_vel }); + } + try stdout.interface.flush(); +} diff --git a/include/common/config.h b/include/common/config.h deleted file mode 100644 index 795927a..0000000 --- a/include/common/config.h +++ /dev/null @@ -1,111 +0,0 @@ -#ifndef ODE_CONFIG_H -#define ODE_CONFIG_H - -/* Define to 1 if you have and it should be used (not on Ultrix). */ -#define HAVE_ALLOCA_H 1 - -/* Use the Apple OpenGL framework. */ -#define HAVE_APPLE_OPENGL_FRAMEWORK 1 - -/* Define to 1 if you have the `gettimeofday' function. */ -#define HAVE_GETTIMEOFDAY 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_INTTYPES_H 1 - -/* Define to 1 if you have the `isnan' function. */ -#define HAVE_ISNAN 1 - -/* Define to 1 if you have the `isnanf' function. */ -#define HAVE_ISNANF 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_MALLOC_H 1 - -/* Define to 1 if you have the `pthread_attr_setstacklazy' function. */ -// #define HAVE_PTHREAD_ATTR_SETSTACKLAZY 1 - -/* Define to 1 if you have the `pthread_condattr_setclock' function. */ -#define HAVE_PTHREAD_CONDATTR_SETCLOCK 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDINT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TIME_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TYPES_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_UNISTD_H 1 - -/* Define to 1 if you have the `_isnan' function. */ -#define HAVE__ISNAN 1 - -/* Define to 1 if you have the `_isnanf' function. */ -#define HAVE__ISNANF 1 - -/* Define to 1 if you have the `__isnan' function. */ -#define HAVE___ISNAN 1 - -/* Define to 1 if you have the `__isnanf' function. */ -#define HAVE___ISNANF 1 - -/* compiling for a pentium on a gcc-based platform? */ -// #define PENTIUM 1 - -/* compiling for a X86_64 system on a gcc-based platform? */ -// #define X86_64_SYSTEM 1 - -/* Try to identify the platform */ -#if defined(_XENON) -#define ODE_PLATFORM_XBOX360 -#elif defined(SN_TARGET_PSP_HW) -#define ODE_PLATFORM_PSP -#elif defined(SN_TARGET_PS3) -#define ODE_PLATFORM_PS3 -#elif defined(_MSC_VER) || defined(__CYGWIN32__) || defined(__MINGW32__) -#define ODE_PLATFORM_WINDOWS -#elif defined(__linux__) -#define ODE_PLATFORM_LINUX -#elif defined(__APPLE__) && defined(__MACH__) -#define ODE_PLATFORM_OSX -#elif defined(__FreeBSD__) -#define ODE_PLATFORM_FREEBSD -#else -#error "Need some help identifying the platform!" -#endif - -/* Additional platform defines used in the code */ -#if defined(ODE_PLATFORM_WINDOWS) && !defined(WIN32) -#define WIN32 -#endif - -#if defined(__CYGWIN32__) || defined(__MINGW32__) -#define CYGWIN -#endif - -#if defined(ODE_PLATFORM_OSX) -#define macintosh -#endif - -#ifdef HAVE_ALLOCA_H -#include -#endif - -#ifdef HAVE_MALLOC_H -#include -#endif - -#ifdef HAVE_STDINT_H -#include -#endif - -#ifdef HAVE_INTTYPES_H -#include -#endif - -#include "typedefs.h" - -#endif // ODE_CONFIG_H diff --git a/include/double/ode/precision.h b/include/double/ode/precision.h deleted file mode 100644 index 5c93b59..0000000 --- a/include/double/ode/precision.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef _ODE_PRECISION_H_ -#define _ODE_PRECISION_H_ - -#define dDOUBLE - -#endif diff --git a/include/double/ode/version.h b/include/double/ode/version.h deleted file mode 100644 index b8cb64f..0000000 --- a/include/double/ode/version.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef _ODE_VERSION_H_ -#define _ODE_VERSION_H_ - -#define dODE_VERSION "0.16.0" - -#endif diff --git a/include/double/template.h b/include/double/template.h deleted file mode 100644 index c66a4ef..0000000 --- a/include/double/template.h +++ /dev/null @@ -1,2 +0,0 @@ -#define TRANSLATE_C -#include \ No newline at end of file diff --git a/include/single/ode/precision.h b/include/single/ode/precision.h deleted file mode 100644 index 9645c77..0000000 --- a/include/single/ode/precision.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef _ODE_PRECISION_H_ -#define _ODE_PRECISION_H_ - -#define dSINGLE - -#endif diff --git a/include/single/ode/version.h b/include/single/ode/version.h deleted file mode 100644 index b8cb64f..0000000 --- a/include/single/ode/version.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef _ODE_VERSION_H_ -#define _ODE_VERSION_H_ - -#define dODE_VERSION "0.16.0" - -#endif diff --git a/include/single/template.h b/include/single/template.h deleted file mode 100644 index c66a4ef..0000000 --- a/include/single/template.h +++ /dev/null @@ -1,2 +0,0 @@ -#define TRANSLATE_C -#include \ No newline at end of file diff --git a/src/Body.zig b/src/Body.zig new file mode 100644 index 0000000..c6a93d1 --- /dev/null +++ b/src/Body.zig @@ -0,0 +1,470 @@ +//! A rigid body in the simulation world. Has position, orientation, linear/angular velocity, +//! mass properties, and accumulated forces. Attach collision geometry (Geom) and connect +//! to other bodies with joints to build articulated structures. + +const c = @import("c.zig").c; +const Real = c.dReal; +const Mass = @import("Mass.zig"); +const World = @import("World.zig"); +const Joint = @import("Joint.zig"); +const Geom = @import("Geom.zig"); + +id: c.dBodyID, + +const Self = @This(); + +/// Create a new body in the given world, initially at the origin with zero velocity. +pub fn create(world: World) Self { + return .{ .id = c.dBodyCreate(world.id) }; +} + +/// Remove this body from the world and free its resources. Any attached joints +/// will be put into limbo (still exist but have no effect until re-attached). +pub fn destroy(self: Self) void { + c.dBodyDestroy(self.id); +} + +/// Get the world this body belongs to. +pub fn get_world(self: Self) World { + return .{ .id = c.dBodyGetWorld(self.id) }; +} + +/// Attach an arbitrary user pointer (e.g. your game entity) to this body. +pub fn set_data(self: Self, data: ?*anyopaque) void { + c.dBodySetData(self.id, data); +} + +pub fn get_data(self: Self) ?*anyopaque { + return c.dBodyGetData(self.id); +} + +// --- Position & orientation --- + +/// Set the body's world-space position. +pub fn set_position(self: Self, pos: [3]Real) void { + c.dBodySetPosition(self.id, pos[0], pos[1], pos[2]); +} + +/// Get the body's world-space position. Returns a pointer into ODE's internal state; +/// valid until the next step or position change. +pub fn get_position(self: Self) [3]Real { + const p = c.dBodyGetPosition(self.id); + return .{ p[0], p[1], p[2] }; +} + +/// Like `get_position` but copies the data, so it's safe to store. +pub fn copy_position(self: Self) [3]Real { + var pos: c.dVector3 = undefined; + c.dBodyCopyPosition(self.id, &pos); + return .{ pos[0], pos[1], pos[2] }; +} + +/// Set orientation as a 3x3 rotation matrix (12 elements with padding). +pub fn set_rotation(self: Self, r: *const [12]Real) void { + c.dBodySetRotation(self.id, r); +} + +/// Get the orientation as a pointer to ODE's internal 3x3 rotation matrix. +pub fn get_rotation(self: Self) *const [12]Real { + return c.dBodyGetRotation(self.id); +} + +/// Copy the rotation matrix into a local value. +pub fn copy_rotation(self: Self) [12]Real { + var r: c.dMatrix3 = undefined; + c.dBodyCopyRotation(self.id, &r); + return r; +} + +/// Set orientation as a quaternion (w, x, y, z). +pub fn set_quaternion(self: Self, q: *const [4]Real) void { + c.dBodySetQuaternion(self.id, q); +} + +/// Get orientation as a quaternion (w, x, y, z). +pub fn get_quaternion(self: Self) [4]Real { + const q = c.dBodyGetQuaternion(self.id); + return .{ q[0], q[1], q[2], q[3] }; +} + +/// Copy the quaternion into a local value. +pub fn copy_quaternion(self: Self) [4]Real { + var q: c.dQuaternion = undefined; + c.dBodyCopyQuaternion(self.id, &q); + return q; +} + +// --- Velocity --- + +/// Set the body's linear velocity in world-space (meters/second). +pub fn set_linear_vel(self: Self, v: [3]Real) void { + c.dBodySetLinearVel(self.id, v[0], v[1], v[2]); +} + +pub fn get_linear_vel(self: Self) [3]Real { + const v = c.dBodyGetLinearVel(self.id); + return .{ v[0], v[1], v[2] }; +} + +/// Set the body's angular velocity in world-space (radians/second). +pub fn set_angular_vel(self: Self, v: [3]Real) void { + c.dBodySetAngularVel(self.id, v[0], v[1], v[2]); +} + +pub fn get_angular_vel(self: Self) [3]Real { + const v = c.dBodyGetAngularVel(self.id); + return .{ v[0], v[1], v[2] }; +} + +// --- Mass --- + +/// Assign mass properties to this body. The mass must be valid (positive mass, +/// positive-definite inertia tensor) or the simulation may become unstable. +pub fn set_mass(self: Self, mass: *const Mass) void { + c.dBodySetMass(self.id, &mass.raw); +} + +/// Read back the body's current mass properties. +pub fn get_mass(self: Self) Mass { + var m: c.dMass = undefined; + c.dBodyGetMass(self.id, &m); + return .{ .raw = m }; +} + +// --- Forces & torques --- + +/// Add a force (in world coordinates) to the body's center of mass. +/// Forces accumulate and are applied during the next step, then reset to zero. +pub fn add_force(self: Self, f: [3]Real) void { + c.dBodyAddForce(self.id, f[0], f[1], f[2]); +} + +/// Add a torque (in world coordinates). +pub fn add_torque(self: Self, t: [3]Real) void { + c.dBodyAddTorque(self.id, t[0], t[1], t[2]); +} + +/// Add a force expressed in the body's local coordinate frame. +pub fn add_rel_force(self: Self, f: [3]Real) void { + c.dBodyAddRelForce(self.id, f[0], f[1], f[2]); +} + +/// Add a torque expressed in the body's local coordinate frame. +pub fn add_rel_torque(self: Self, t: [3]Real) void { + c.dBodyAddRelTorque(self.id, t[0], t[1], t[2]); +} + +/// Add a force (world coords) at a specific world-space point. This can generate both +/// linear force and torque if the point is not at the center of mass. +pub fn add_force_at_pos(self: Self, f: [3]Real, p: [3]Real) void { + c.dBodyAddForceAtPos(self.id, f[0], f[1], f[2], p[0], p[1], p[2]); +} + +/// Add a force (world coords) at a body-relative point. +pub fn add_force_at_rel_pos(self: Self, f: [3]Real, p: [3]Real) void { + c.dBodyAddForceAtRelPos(self.id, f[0], f[1], f[2], p[0], p[1], p[2]); +} + +/// Add a body-relative force at a world-space point. +pub fn add_rel_force_at_pos(self: Self, f: [3]Real, p: [3]Real) void { + c.dBodyAddRelForceAtPos(self.id, f[0], f[1], f[2], p[0], p[1], p[2]); +} + +/// Add a body-relative force at a body-relative point. +pub fn add_rel_force_at_rel_pos(self: Self, f: [3]Real, p: [3]Real) void { + c.dBodyAddRelForceAtRelPos(self.id, f[0], f[1], f[2], p[0], p[1], p[2]); +} + +/// Read the accumulated force vector (world coords). Reset to zero after each step. +pub fn get_force(self: Self) [3]Real { + const f = c.dBodyGetForce(self.id); + return .{ f[0], f[1], f[2] }; +} + +/// Read the accumulated torque vector (world coords). Reset to zero after each step. +pub fn get_torque(self: Self) [3]Real { + const t = c.dBodyGetTorque(self.id); + return .{ t[0], t[1], t[2] }; +} + +/// Directly set the accumulated force (world coords). Normally you should use `add_force`. +pub fn set_force(self: Self, f: [3]Real) void { + c.dBodySetForce(self.id, f[0], f[1], f[2]); +} + +/// Directly set the accumulated torque (world coords). Normally you should use `add_torque`. +pub fn set_torque(self: Self, t: [3]Real) void { + c.dBodySetTorque(self.id, t[0], t[1], t[2]); +} + +// --- Coordinate conversions --- + +/// Transform a body-relative point to world coordinates. +pub fn get_rel_point_pos(self: Self, p: [3]Real) [3]Real { + var result: c.dVector3 = undefined; + c.dBodyGetRelPointPos(self.id, p[0], p[1], p[2], &result); + return .{ result[0], result[1], result[2] }; +} + +/// Get the world-space velocity of a body-relative point (accounts for both linear +/// and angular velocity). +pub fn get_rel_point_vel(self: Self, p: [3]Real) [3]Real { + var result: c.dVector3 = undefined; + c.dBodyGetRelPointVel(self.id, p[0], p[1], p[2], &result); + return .{ result[0], result[1], result[2] }; +} + +/// Get the world-space velocity of a world-space point on this body. +pub fn get_point_vel(self: Self, p: [3]Real) [3]Real { + var result: c.dVector3 = undefined; + c.dBodyGetPointVel(self.id, p[0], p[1], p[2], &result); + return .{ result[0], result[1], result[2] }; +} + +/// Transform a world-space point into body-relative coordinates. +pub fn get_pos_rel_point(self: Self, p: [3]Real) [3]Real { + var result: c.dVector3 = undefined; + c.dBodyGetPosRelPoint(self.id, p[0], p[1], p[2], &result); + return .{ result[0], result[1], result[2] }; +} + +/// Rotate a direction vector from body-local to world coordinates (no translation). +pub fn vector_to_world(self: Self, v: [3]Real) [3]Real { + var result: c.dVector3 = undefined; + c.dBodyVectorToWorld(self.id, v[0], v[1], v[2], &result); + return .{ result[0], result[1], result[2] }; +} + +/// Rotate a direction vector from world to body-local coordinates (no translation). +pub fn vector_from_world(self: Self, v: [3]Real) [3]Real { + var result: c.dVector3 = undefined; + c.dBodyVectorFromWorld(self.id, v[0], v[1], v[2], &result); + return .{ result[0], result[1], result[2] }; +} + +// --- Finite rotation --- + +/// Control how rotation is integrated. 0 = infinitesimal (fast, default), +/// 1 = finite (more accurate for fast-spinning bodies, slower). +pub fn set_finite_rotation_mode(self: Self, mode: c_int) void { + c.dBodySetFiniteRotationMode(self.id, mode); +} + +pub fn get_finite_rotation_mode(self: Self) c_int { + return c.dBodyGetFiniteRotationMode(self.id); +} + +/// When using finite rotation mode, set the axis around which the body is primarily +/// spinning. This improves stability for gyroscope-like objects. +pub fn set_finite_rotation_axis(self: Self, axis: [3]Real) void { + c.dBodySetFiniteRotationAxis(self.id, axis[0], axis[1], axis[2]); +} + +pub fn get_finite_rotation_axis(self: Self) [3]Real { + var result: c.dVector3 = undefined; + c.dBodyGetFiniteRotationAxis(self.id, &result); + return .{ result[0], result[1], result[2] }; +} + +// --- Joints & geoms --- + +/// Number of joints attached to this body. +pub fn get_num_joints(self: Self) c_int { + return c.dBodyGetNumJoints(self.id); +} + +/// Get the i-th joint attached to this body (0-based). +pub fn get_joint(self: Self, index: c_int) Joint.Generic { + return .{ .id = c.dBodyGetJoint(self.id, index) }; +} + +/// Get the first collision geom attached to this body, or null if none. +/// To iterate all geoms, use `Geom.get_body_next` on the returned geom. +pub fn get_first_geom(self: Self) ?Geom.Generic { + const g = c.dBodyGetFirstGeom(self.id); + return if (g != null) .{ .id = g } else null; +} + +// --- Enable/disable --- + +/// Wake up a disabled body so it participates in simulation again. +pub fn enable(self: Self) void { + c.dBodyEnable(self.id); +} + +/// Disable this body (freeze it). Disabled bodies are not simulated, saving CPU. +/// They are automatically re-enabled if an enabled body touches them. +pub fn disable(self: Self) void { + c.dBodyDisable(self.id); +} + +pub fn is_enabled(self: Self) bool { + return c.dBodyIsEnabled(self.id) != 0; +} + +// --- Dynamic/kinematic --- + +/// Set this body to dynamic mode (default). Dynamic bodies are affected by forces +/// and constraints. +pub fn set_dynamic(self: Self) void { + c.dBodySetDynamic(self.id); +} + +/// Set this body to kinematic mode. Kinematic bodies have infinite mass — they are +/// not affected by forces or collisions but can push dynamic bodies around. +/// Useful for moving platforms, animated characters, etc. +pub fn set_kinematic(self: Self) void { + c.dBodySetKinematic(self.id); +} + +pub fn is_kinematic(self: Self) bool { + return c.dBodyIsKinematic(self.id) != 0; +} + +// --- Gravity mode --- + +/// Enable or disable gravity for this specific body. Useful for objects that should +/// float (e.g. balloons, spacecraft) while other bodies still fall normally. +pub fn set_gravity_mode(self: Self, mode: bool) void { + c.dBodySetGravityMode(self.id, @intFromBool(mode)); +} + +pub fn get_gravity_mode(self: Self) bool { + return c.dBodyGetGravityMode(self.id) != 0; +} + +// --- Moved callback --- + +/// Register a callback invoked whenever this body's position/rotation changes during a step. +/// Useful for synchronizing graphics transforms. +pub fn set_moved_callback(self: Self, callback: ?*const fn (c.dBodyID) callconv(.c) void) void { + c.dBodySetMovedCallback(self.id, callback); +} + +// --- Damping --- + +/// Reset linear and angular damping to the world's default values. +pub fn set_damping_defaults(self: Self) void { + c.dBodySetDampingDefaults(self.id); +} + +/// Per-body linear damping scale (overrides world default). 0 = no damping, 1 = full stop. +pub fn set_linear_damping(self: Self, scale: Real) void { + c.dBodySetLinearDamping(self.id, scale); +} + +pub fn get_linear_damping(self: Self) Real { + return c.dBodyGetLinearDamping(self.id); +} + +/// Per-body angular damping scale. +pub fn set_angular_damping(self: Self, scale: Real) void { + c.dBodySetAngularDamping(self.id, scale); +} + +pub fn get_angular_damping(self: Self) Real { + return c.dBodyGetAngularDamping(self.id); +} + +/// Set both linear and angular damping at once. +pub fn set_damping(self: Self, linear_scale: Real, angular_scale: Real) void { + c.dBodySetDamping(self.id, linear_scale, angular_scale); +} + +/// Linear velocity below this threshold won't be damped. +pub fn set_linear_damping_threshold(self: Self, threshold: Real) void { + c.dBodySetLinearDampingThreshold(self.id, threshold); +} + +pub fn get_linear_damping_threshold(self: Self) Real { + return c.dBodyGetLinearDampingThreshold(self.id); +} + +/// Angular velocity below this threshold won't be damped. +pub fn set_angular_damping_threshold(self: Self, threshold: Real) void { + c.dBodySetAngularDampingThreshold(self.id, threshold); +} + +pub fn get_angular_damping_threshold(self: Self) Real { + return c.dBodyGetAngularDampingThreshold(self.id); +} + +/// Clamp this body's angular speed to a maximum value. +pub fn set_max_angular_speed(self: Self, max_speed: Real) void { + c.dBodySetMaxAngularSpeed(self.id, max_speed); +} + +pub fn get_max_angular_speed(self: Self) Real { + return c.dBodyGetMaxAngularSpeed(self.id); +} + +/// Enable or disable gyroscopic torque computation. Disabling can improve stability +/// for objects that don't need accurate gyroscopic effects (most game objects). +pub fn set_gyroscopic_mode(self: Self, enabled: bool) void { + c.dBodySetGyroscopicMode(self.id, @intFromBool(enabled)); +} + +pub fn get_gyroscopic_mode(self: Self) bool { + return c.dBodyGetGyroscopicMode(self.id) != 0; +} + +// --- Auto-disable (per-body overrides) --- + +/// Override the world's auto-disable setting for this body. +pub fn set_auto_disable_flag(self: Self, do_auto_disable: bool) void { + c.dBodySetAutoDisableFlag(self.id, @intFromBool(do_auto_disable)); +} + +pub fn get_auto_disable_flag(self: Self) bool { + return c.dBodyGetAutoDisableFlag(self.id) != 0; +} + +/// Per-body linear velocity threshold for auto-disable. +pub fn set_auto_disable_linear_threshold(self: Self, threshold: Real) void { + c.dBodySetAutoDisableLinearThreshold(self.id, threshold); +} + +pub fn get_auto_disable_linear_threshold(self: Self) Real { + return c.dBodyGetAutoDisableLinearThreshold(self.id); +} + +/// Per-body angular velocity threshold for auto-disable. +pub fn set_auto_disable_angular_threshold(self: Self, threshold: Real) void { + c.dBodySetAutoDisableAngularThreshold(self.id, threshold); +} + +pub fn get_auto_disable_angular_threshold(self: Self) Real { + return c.dBodyGetAutoDisableAngularThreshold(self.id); +} + +/// Per-body step count threshold for auto-disable. +pub fn set_auto_disable_steps(self: Self, steps: c_int) void { + c.dBodySetAutoDisableSteps(self.id, steps); +} + +pub fn get_auto_disable_steps(self: Self) c_int { + return c.dBodyGetAutoDisableSteps(self.id); +} + +/// Per-body time threshold for auto-disable. +pub fn set_auto_disable_time(self: Self, time: Real) void { + c.dBodySetAutoDisableTime(self.id, time); +} + +pub fn get_auto_disable_time(self: Self) Real { + return c.dBodyGetAutoDisableTime(self.id); +} + +/// Per-body averaging sample count for auto-disable. +pub fn set_auto_disable_average_samples_count(self: Self, count: c_uint) void { + c.dBodySetAutoDisableAverageSamplesCount(self.id, count); +} + +pub fn get_auto_disable_average_samples_count(self: Self) c_int { + return c.dBodyGetAutoDisableAverageSamplesCount(self.id); +} + +/// Reset all auto-disable parameters on this body to the world defaults. +pub fn set_auto_disable_defaults(self: Self) void { + c.dBodySetAutoDisableDefaults(self.id); +} diff --git a/src/Geom.zig b/src/Geom.zig new file mode 100644 index 0000000..7060721 --- /dev/null +++ b/src/Geom.zig @@ -0,0 +1,850 @@ +//! Collision geometry (geom) types and operations. +//! +//! Geoms are the fundamental collision shapes in ODE. They can be added to a Space +//! for broad-phase collision culling, attached to a Body to follow its motion, or +//! placed statically in the world. Each concrete type (Sphere, Box, etc.) wraps a +//! type-erased `Generic` handle and provides shape-specific methods alongside the +//! common geom interface. + +const c = @import("c.zig").c; +const Real = c.dReal; +const Body = @import("Body.zig"); +const Space = @import("Space.zig"); + +/// Returns a struct of common geom methods parameterized for the given type. +/// Every geom struct (Generic, Sphere, Box, etc.) re-exports these as its own methods. +fn GeomMethods(comptime Self: type) type { + return struct { + /// Destroys the geom, removing it from any space it belongs to and freeing its resources. + pub fn destroy(self: Self) void { c.dGeomDestroy(self.id); } + /// Attaches this geom to a rigid body so it follows the body's motion, or pass null to detach. + pub fn set_body(self: Self, body: ?Body) void { c.dGeomSetBody(self.id, if (body) |b| b.id else null); } + /// Returns the body this geom is attached to, or null if it is a static geom. + pub fn get_body(self: Self) ?Body { const b = c.dGeomGetBody(self.id); return if (b != null) .{ .id = b } else null; } + /// Sets the geom's position in world coordinates. + pub fn set_position(self: Self, pos: [3]Real) void { c.dGeomSetPosition(self.id, pos[0], pos[1], pos[2]); } + /// Returns the geom's position in world coordinates. + pub fn get_position(self: Self) [3]Real { const p = c.dGeomGetPosition(self.id); return .{ p[0], p[1], p[2] }; } + /// Sets the geom's orientation as a 3x4 rotation matrix (row-major, 12 elements). + pub fn set_rotation(self: Self, r: *const [12]Real) void { c.dGeomSetRotation(self.id, r); } + /// Returns a pointer to the geom's 3x4 rotation matrix (row-major, 12 elements). + pub fn get_rotation(self: Self) *const [12]Real { return c.dGeomGetRotation(self.id); } + /// Sets the geom's orientation as a quaternion [w, x, y, z]. + pub fn set_quaternion(self: Self, q: *const [4]Real) void { c.dGeomSetQuaternion(self.id, q); } + /// Returns the geom's orientation as a quaternion [w, x, y, z]. + pub fn get_quaternion(self: Self) [4]Real { var q: c.dQuaternion = undefined; c.dGeomGetQuaternion(self.id, &q); return q; } + /// Returns the axis-aligned bounding box as [minx, maxx, miny, maxy, minz, maxz]. + pub fn get_aabb(self: Self) [6]Real { var aabb: [6]Real = undefined; c.dGeomGetAABB(self.id, &aabb); return aabb; } + /// Stores an arbitrary user-data pointer on the geom. + pub fn set_data(self: Self, data: ?*anyopaque) void { c.dGeomSetData(self.id, data); } + /// Retrieves the user-data pointer previously set with `set_data`. + pub fn get_data(self: Self) ?*anyopaque { return c.dGeomGetData(self.id); } + /// Returns true if this geom handle actually represents a Space (spaces are also geoms in ODE). + pub fn is_space(self: Self) bool { return c.dGeomIsSpace(self.id) != 0; } + /// Returns the space this geom belongs to, or null if it has not been added to any space. + pub fn get_space(self: Self) ?Space.Generic { const s = c.dGeomGetSpace(self.id); return if (s != null) .{ .id = s } else null; } + /// Returns the ODE class identifier for this geom's shape type. + pub fn get_class(self: Self) c_int { return c.dGeomGetClass(self.id); } + /// Sets the category bitmask that identifies which collision group(s) this geom belongs to. + pub fn set_category_bits(self: Self, bits: c_ulong) void { c.dGeomSetCategoryBits(self.id, bits); } + /// Returns the category bitmask identifying which collision group(s) this geom belongs to. + pub fn get_category_bits(self: Self) c_ulong { return c.dGeomGetCategoryBits(self.id); } + /// Sets the collide bitmask controlling which categories this geom can collide with. + pub fn set_collide_bits(self: Self, bits: c_ulong) void { c.dGeomSetCollideBits(self.id, bits); } + /// Returns the collide bitmask controlling which categories this geom can collide with. + pub fn get_collide_bits(self: Self) c_ulong { return c.dGeomGetCollideBits(self.id); } + /// Enables this geom so it participates in collision detection. + pub fn enable(self: Self) void { c.dGeomEnable(self.id); } + /// Disables this geom so it is skipped during collision detection. + pub fn disable(self: Self) void { c.dGeomDisable(self.id); } + /// Returns true if this geom is enabled for collision detection. + pub fn is_enabled(self: Self) bool { return c.dGeomIsEnabled(self.id) != 0; } + /// Sets the geom's position offset relative to its attached body (local frame). + pub fn set_offset_position(self: Self, pos: [3]Real) void { c.dGeomSetOffsetPosition(self.id, pos[0], pos[1], pos[2]); } + /// Returns the geom's position offset relative to its attached body (local frame). + pub fn get_offset_position(self: Self) [3]Real { const p = c.dGeomGetOffsetPosition(self.id); return .{ p[0], p[1], p[2] }; } + /// Sets the geom's rotation offset relative to its attached body as a 3x4 matrix. + pub fn set_offset_rotation(self: Self, r: *const [12]Real) void { c.dGeomSetOffsetRotation(self.id, r); } + /// Returns a pointer to the geom's rotation offset relative to its attached body. + pub fn get_offset_rotation(self: Self) *const [12]Real { return c.dGeomGetOffsetRotation(self.id); } + /// Sets the geom's rotation offset relative to its attached body as a quaternion. + pub fn set_offset_quaternion(self: Self, q: *const [4]Real) void { c.dGeomSetOffsetQuaternion(self.id, q); } + /// Returns the geom's rotation offset relative to its attached body as a quaternion. + pub fn get_offset_quaternion(self: Self) [4]Real { var q: c.dQuaternion = undefined; c.dGeomGetOffsetQuaternion(self.id, &q); return q; } + /// Sets the geom's offset so that it ends up at the given world position (computes the local offset from the body). + pub fn set_offset_world_position(self: Self, pos: [3]Real) void { c.dGeomSetOffsetWorldPosition(self.id, pos[0], pos[1], pos[2]); } + /// Sets the geom's offset so that it ends up at the given world rotation (computes the local offset from the body). + pub fn set_offset_world_rotation(self: Self, r: *const [12]Real) void { c.dGeomSetOffsetWorldRotation(self.id, r); } + /// Sets the geom's offset so that it ends up at the given world quaternion (computes the local offset from the body). + pub fn set_offset_world_quaternion(self: Self, q: *const [4]Real) void { c.dGeomSetOffsetWorldQuaternion(self.id, q); } + /// Removes any body-relative offset, resetting the geom to be centered on its body. + pub fn clear_offset(self: Self) void { c.dGeomClearOffset(self.id); } + /// Returns true if this geom has a body-relative offset transform applied. + pub fn is_offset(self: Self) bool { return c.dGeomIsOffset(self.id) != 0; } + /// Converts this typed geom handle into a type-erased `Generic` handle. + pub fn to_generic(self: Self) Generic { return .{ .id = self.id }; } + }; +} + +/// Type-erased geom handle. Can represent any collision shape. Use this when +/// the concrete shape type is unknown or irrelevant (e.g., in callbacks). +pub const Generic = struct { + id: c.dGeomID, + + const methods = GeomMethods(Generic); + pub const destroy = methods.destroy; + pub const set_body = methods.set_body; + pub const get_body = methods.get_body; + pub const set_position = methods.set_position; + pub const get_position = methods.get_position; + pub const set_rotation = methods.set_rotation; + pub const get_rotation = methods.get_rotation; + pub const set_quaternion = methods.set_quaternion; + pub const get_quaternion = methods.get_quaternion; + pub const get_aabb = methods.get_aabb; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const is_space = methods.is_space; + pub const get_space = methods.get_space; + pub const get_class = methods.get_class; + pub const set_category_bits = methods.set_category_bits; + pub const get_category_bits = methods.get_category_bits; + pub const set_collide_bits = methods.set_collide_bits; + pub const get_collide_bits = methods.get_collide_bits; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const set_offset_position = methods.set_offset_position; + pub const get_offset_position = methods.get_offset_position; + pub const set_offset_rotation = methods.set_offset_rotation; + pub const get_offset_rotation = methods.get_offset_rotation; + pub const set_offset_quaternion = methods.set_offset_quaternion; + pub const get_offset_quaternion = methods.get_offset_quaternion; + pub const set_offset_world_position = methods.set_offset_world_position; + pub const set_offset_world_rotation = methods.set_offset_world_rotation; + pub const set_offset_world_quaternion = methods.set_offset_world_quaternion; + pub const clear_offset = methods.clear_offset; + pub const is_offset = methods.is_offset; +}; + +/// Sphere collision shape defined by a center and radius. +pub const Sphere = struct { + id: c.dGeomID, + + const methods = GeomMethods(Sphere); + pub const destroy = methods.destroy; + pub const set_body = methods.set_body; + pub const get_body = methods.get_body; + pub const set_position = methods.set_position; + pub const get_position = methods.get_position; + pub const set_rotation = methods.set_rotation; + pub const get_rotation = methods.get_rotation; + pub const set_quaternion = methods.set_quaternion; + pub const get_quaternion = methods.get_quaternion; + pub const get_aabb = methods.get_aabb; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const is_space = methods.is_space; + pub const get_space = methods.get_space; + pub const get_class = methods.get_class; + pub const set_category_bits = methods.set_category_bits; + pub const get_category_bits = methods.get_category_bits; + pub const set_collide_bits = methods.set_collide_bits; + pub const get_collide_bits = methods.get_collide_bits; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const set_offset_position = methods.set_offset_position; + pub const get_offset_position = methods.get_offset_position; + pub const set_offset_rotation = methods.set_offset_rotation; + pub const get_offset_rotation = methods.get_offset_rotation; + pub const set_offset_quaternion = methods.set_offset_quaternion; + pub const get_offset_quaternion = methods.get_offset_quaternion; + pub const set_offset_world_position = methods.set_offset_world_position; + pub const set_offset_world_rotation = methods.set_offset_world_rotation; + pub const set_offset_world_quaternion = methods.set_offset_world_quaternion; + pub const clear_offset = methods.clear_offset; + pub const is_offset = methods.is_offset; + pub const to_generic = methods.to_generic; + + /// Creates a sphere geom with the given radius, optionally inserting it into a space. + pub fn create(space: ?Space.Generic, radius: Real) Sphere { + return .{ .id = c.dCreateSphere(if (space) |s| s.id else null, radius) }; + } + + /// Changes the sphere's radius. + pub fn set_radius(self: Sphere, radius: Real) void { + c.dGeomSphereSetRadius(self.id, radius); + } + + /// Returns the sphere's radius. + pub fn get_radius(self: Sphere) Real { + return c.dGeomSphereGetRadius(self.id); + } + + /// Returns how deep the point `p` is inside the sphere (positive = inside, negative = outside). + pub fn point_depth(self: Sphere, p: [3]Real) Real { + return c.dGeomSpherePointDepth(self.id, p[0], p[1], p[2]); + } +}; + +/// Axis-aligned box collision shape defined by side lengths along each axis. +pub const Box = struct { + id: c.dGeomID, + + const methods = GeomMethods(Box); + pub const destroy = methods.destroy; + pub const set_body = methods.set_body; + pub const get_body = methods.get_body; + pub const set_position = methods.set_position; + pub const get_position = methods.get_position; + pub const set_rotation = methods.set_rotation; + pub const get_rotation = methods.get_rotation; + pub const set_quaternion = methods.set_quaternion; + pub const get_quaternion = methods.get_quaternion; + pub const get_aabb = methods.get_aabb; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const is_space = methods.is_space; + pub const get_space = methods.get_space; + pub const get_class = methods.get_class; + pub const set_category_bits = methods.set_category_bits; + pub const get_category_bits = methods.get_category_bits; + pub const set_collide_bits = methods.set_collide_bits; + pub const get_collide_bits = methods.get_collide_bits; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const set_offset_position = methods.set_offset_position; + pub const get_offset_position = methods.get_offset_position; + pub const set_offset_rotation = methods.set_offset_rotation; + pub const get_offset_rotation = methods.get_offset_rotation; + pub const set_offset_quaternion = methods.set_offset_quaternion; + pub const get_offset_quaternion = methods.get_offset_quaternion; + pub const set_offset_world_position = methods.set_offset_world_position; + pub const set_offset_world_rotation = methods.set_offset_world_rotation; + pub const set_offset_world_quaternion = methods.set_offset_world_quaternion; + pub const clear_offset = methods.clear_offset; + pub const is_offset = methods.is_offset; + pub const to_generic = methods.to_generic; + + /// Creates a box geom with the given side lengths (lx, ly, lz), optionally inserting it into a space. + pub fn create(space: ?Space.Generic, lx: Real, ly: Real, lz: Real) Box { + return .{ .id = c.dCreateBox(if (space) |s| s.id else null, lx, ly, lz) }; + } + + /// Changes the box's side lengths. + pub fn set_lengths(self: Box, lx: Real, ly: Real, lz: Real) void { + c.dGeomBoxSetLengths(self.id, lx, ly, lz); + } + + /// Returns the box's side lengths as [lx, ly, lz]. + pub fn get_lengths(self: Box) [3]Real { + var result: c.dVector3 = undefined; + c.dGeomBoxGetLengths(self.id, &result); + return .{ result[0], result[1], result[2] }; + } + + /// Returns how deep the point `p` is inside the box (positive = inside, negative = outside). + pub fn point_depth(self: Box, p: [3]Real) Real { + return c.dGeomBoxPointDepth(self.id, p[0], p[1], p[2]); + } +}; + +/// Capsule (capped cylinder) collision shape -- a cylinder with hemispherical end caps, +/// defined by a radius and the length of the cylindrical section along the local Z axis. +pub const Capsule = struct { + id: c.dGeomID, + + const methods = GeomMethods(Capsule); + pub const destroy = methods.destroy; + pub const set_body = methods.set_body; + pub const get_body = methods.get_body; + pub const set_position = methods.set_position; + pub const get_position = methods.get_position; + pub const set_rotation = methods.set_rotation; + pub const get_rotation = methods.get_rotation; + pub const set_quaternion = methods.set_quaternion; + pub const get_quaternion = methods.get_quaternion; + pub const get_aabb = methods.get_aabb; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const is_space = methods.is_space; + pub const get_space = methods.get_space; + pub const get_class = methods.get_class; + pub const set_category_bits = methods.set_category_bits; + pub const get_category_bits = methods.get_category_bits; + pub const set_collide_bits = methods.set_collide_bits; + pub const get_collide_bits = methods.get_collide_bits; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const set_offset_position = methods.set_offset_position; + pub const get_offset_position = methods.get_offset_position; + pub const set_offset_rotation = methods.set_offset_rotation; + pub const get_offset_rotation = methods.get_offset_rotation; + pub const set_offset_quaternion = methods.set_offset_quaternion; + pub const get_offset_quaternion = methods.get_offset_quaternion; + pub const set_offset_world_position = methods.set_offset_world_position; + pub const set_offset_world_rotation = methods.set_offset_world_rotation; + pub const set_offset_world_quaternion = methods.set_offset_world_quaternion; + pub const clear_offset = methods.clear_offset; + pub const is_offset = methods.is_offset; + pub const to_generic = methods.to_generic; + + /// Creates a capsule geom with the given radius and cylinder length, optionally inserting it into a space. + pub fn create(space: ?Space.Generic, radius: Real, length: Real) Capsule { + return .{ .id = c.dCreateCapsule(if (space) |s| s.id else null, radius, length) }; + } + + /// Changes the capsule's radius and cylinder section length. + pub fn set_params(self: Capsule, radius: Real, length: Real) void { + c.dGeomCapsuleSetParams(self.id, radius, length); + } + + /// Returns the capsule's radius and cylinder section length. + pub fn get_params(self: Capsule) struct { radius: Real, length: Real } { + var radius: Real = undefined; + var length: Real = undefined; + c.dGeomCapsuleGetParams(self.id, &radius, &length); + return .{ .radius = radius, .length = length }; + } + + /// Returns how deep the point `p` is inside the capsule (positive = inside, negative = outside). + pub fn point_depth(self: Capsule, p: [3]Real) Real { + return c.dGeomCapsulePointDepth(self.id, p[0], p[1], p[2]); + } +}; + +/// Flat-ended cylinder collision shape defined by a radius and length along the local Z axis. +/// Unlike a Capsule, this has flat circular end caps rather than hemispherical ones. +pub const Cylinder = struct { + id: c.dGeomID, + + const methods = GeomMethods(Cylinder); + pub const destroy = methods.destroy; + pub const set_body = methods.set_body; + pub const get_body = methods.get_body; + pub const set_position = methods.set_position; + pub const get_position = methods.get_position; + pub const set_rotation = methods.set_rotation; + pub const get_rotation = methods.get_rotation; + pub const set_quaternion = methods.set_quaternion; + pub const get_quaternion = methods.get_quaternion; + pub const get_aabb = methods.get_aabb; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const is_space = methods.is_space; + pub const get_space = methods.get_space; + pub const get_class = methods.get_class; + pub const set_category_bits = methods.set_category_bits; + pub const get_category_bits = methods.get_category_bits; + pub const set_collide_bits = methods.set_collide_bits; + pub const get_collide_bits = methods.get_collide_bits; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const set_offset_position = methods.set_offset_position; + pub const get_offset_position = methods.get_offset_position; + pub const set_offset_rotation = methods.set_offset_rotation; + pub const get_offset_rotation = methods.get_offset_rotation; + pub const set_offset_quaternion = methods.set_offset_quaternion; + pub const get_offset_quaternion = methods.get_offset_quaternion; + pub const set_offset_world_position = methods.set_offset_world_position; + pub const set_offset_world_rotation = methods.set_offset_world_rotation; + pub const set_offset_world_quaternion = methods.set_offset_world_quaternion; + pub const clear_offset = methods.clear_offset; + pub const is_offset = methods.is_offset; + pub const to_generic = methods.to_generic; + + /// Creates a cylinder geom with the given radius and length, optionally inserting it into a space. + pub fn create(space: ?Space.Generic, radius: Real, length: Real) Cylinder { + return .{ .id = c.dCreateCylinder(if (space) |s| s.id else null, radius, length) }; + } + + /// Changes the cylinder's radius and length. + pub fn set_params(self: Cylinder, radius: Real, length: Real) void { + c.dGeomCylinderSetParams(self.id, radius, length); + } + + /// Returns the cylinder's radius and length. + pub fn get_params(self: Cylinder) struct { radius: Real, length: Real } { + var radius: Real = undefined; + var length: Real = undefined; + c.dGeomCylinderGetParams(self.id, &radius, &length); + return .{ .radius = radius, .length = length }; + } +}; + +/// Infinite, non-placeable plane defined by the equation ax + by + cz = d. +/// Planes are always static (cannot be attached to a body) and have no position +/// or rotation -- they are specified entirely by their normal (a, b, c) and distance d. +pub const Plane = struct { + id: c.dGeomID, + + const methods = GeomMethods(Plane); + pub const destroy = methods.destroy; + pub const set_body = methods.set_body; + pub const get_body = methods.get_body; + pub const set_position = methods.set_position; + pub const get_position = methods.get_position; + pub const set_rotation = methods.set_rotation; + pub const get_rotation = methods.get_rotation; + pub const set_quaternion = methods.set_quaternion; + pub const get_quaternion = methods.get_quaternion; + pub const get_aabb = methods.get_aabb; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const is_space = methods.is_space; + pub const get_space = methods.get_space; + pub const get_class = methods.get_class; + pub const set_category_bits = methods.set_category_bits; + pub const get_category_bits = methods.get_category_bits; + pub const set_collide_bits = methods.set_collide_bits; + pub const get_collide_bits = methods.get_collide_bits; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const set_offset_position = methods.set_offset_position; + pub const get_offset_position = methods.get_offset_position; + pub const set_offset_rotation = methods.set_offset_rotation; + pub const get_offset_rotation = methods.get_offset_rotation; + pub const set_offset_quaternion = methods.set_offset_quaternion; + pub const get_offset_quaternion = methods.get_offset_quaternion; + pub const set_offset_world_position = methods.set_offset_world_position; + pub const set_offset_world_rotation = methods.set_offset_world_rotation; + pub const set_offset_world_quaternion = methods.set_offset_world_quaternion; + pub const clear_offset = methods.clear_offset; + pub const is_offset = methods.is_offset; + pub const to_generic = methods.to_generic; + + /// Creates a plane geom defined by the equation ax + by + cz = d, optionally inserting it into a space. + /// The normal (a, b, c) does not need to be unit-length -- ODE will normalize it. + pub fn create(space: ?Space.Generic, a: Real, b: Real, _c: Real, d: Real) Plane { + return .{ .id = c.dCreatePlane(if (space) |s| s.id else null, a, b, _c, d) }; + } + + /// Changes the plane equation parameters (a, b, c, d). + pub fn set_params(self: Plane, a: Real, b: Real, _c: Real, d: Real) void { + c.dGeomPlaneSetParams(self.id, a, b, _c, d); + } + + /// Returns the plane equation parameters as [a, b, c, d]. + pub fn get_params(self: Plane) [4]Real { + var result: c.dVector4 = undefined; + c.dGeomPlaneGetParams(self.id, &result); + return result; + } + + /// Returns the signed distance from the point `p` to the plane surface (positive = on the normal side). + pub fn point_depth(self: Plane, p: [3]Real) Real { + return c.dGeomPlanePointDepth(self.id, p[0], p[1], p[2]); + } +}; + +/// Ray collision shape used for raycasting. Defined by an origin, direction, and length. +/// Rays are one-directional and infinitely thin. +pub const Ray = struct { + id: c.dGeomID, + + const methods = GeomMethods(Ray); + pub const destroy = methods.destroy; + pub const set_body = methods.set_body; + pub const get_body = methods.get_body; + pub const set_position = methods.set_position; + pub const get_position = methods.get_position; + pub const set_rotation = methods.set_rotation; + pub const get_rotation = methods.get_rotation; + pub const set_quaternion = methods.set_quaternion; + pub const get_quaternion = methods.get_quaternion; + pub const get_aabb = methods.get_aabb; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const is_space = methods.is_space; + pub const get_space = methods.get_space; + pub const get_class = methods.get_class; + pub const set_category_bits = methods.set_category_bits; + pub const get_category_bits = methods.get_category_bits; + pub const set_collide_bits = methods.set_collide_bits; + pub const get_collide_bits = methods.get_collide_bits; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const set_offset_position = methods.set_offset_position; + pub const get_offset_position = methods.get_offset_position; + pub const set_offset_rotation = methods.set_offset_rotation; + pub const get_offset_rotation = methods.get_offset_rotation; + pub const set_offset_quaternion = methods.set_offset_quaternion; + pub const get_offset_quaternion = methods.get_offset_quaternion; + pub const set_offset_world_position = methods.set_offset_world_position; + pub const set_offset_world_rotation = methods.set_offset_world_rotation; + pub const set_offset_world_quaternion = methods.set_offset_world_quaternion; + pub const clear_offset = methods.clear_offset; + pub const is_offset = methods.is_offset; + pub const to_generic = methods.to_generic; + + /// Creates a ray geom with the given maximum length, optionally inserting it into a space. + pub fn create(space: ?Space.Generic, length: Real) Ray { + return .{ .id = c.dCreateRay(if (space) |s| s.id else null, length) }; + } + + /// Changes the ray's maximum cast length. + pub fn set_length(self: Ray, length: Real) void { + c.dGeomRaySetLength(self.id, length); + } + + /// Returns the ray's maximum cast length. + pub fn get_length(self: Ray) Real { + return c.dGeomRayGetLength(self.id); + } + + /// Sets the ray's origin point and direction vector simultaneously. + pub fn set(self: Ray, origin: [3]Real, direction: [3]Real) void { + c.dGeomRaySet(self.id, origin[0], origin[1], origin[2], direction[0], direction[1], direction[2]); + } + + /// Returns the ray's origin point and direction vector. + pub fn get(self: Ray) struct { origin: [3]Real, direction: [3]Real } { + var start: c.dVector3 = undefined; + var dir: c.dVector3 = undefined; + c.dGeomRayGet(self.id, &start, &dir); + return .{ + .origin = .{ start[0], start[1], start[2] }, + .direction = .{ dir[0], dir[1], dir[2] }, + }; + } + + /// When true, the ray stops at the first contact instead of searching for all contacts. + pub fn set_first_contact(self: Ray, first_contact: bool) void { + c.dGeomRaySetFirstContact(self.id, @intFromBool(first_contact)); + } + + /// Returns whether first-contact mode is enabled. + pub fn get_first_contact(self: Ray) bool { + return c.dGeomRayGetFirstContact(self.id) != 0; + } + + /// When true, contacts with backfacing triangles are ignored during raycasting. + pub fn set_backface_cull(self: Ray, backface_cull: bool) void { + c.dGeomRaySetBackfaceCull(self.id, @intFromBool(backface_cull)); + } + + /// Returns whether backface culling is enabled for this ray. + pub fn get_backface_cull(self: Ray) bool { + return c.dGeomRayGetBackfaceCull(self.id) != 0; + } + + /// When true, only the closest hit along the ray is reported. + pub fn set_closest_hit(self: Ray, closest_hit: bool) void { + c.dGeomRaySetClosestHit(self.id, @intFromBool(closest_hit)); + } + + /// Returns whether closest-hit mode is enabled. + pub fn get_closest_hit(self: Ray) bool { + return c.dGeomRayGetClosestHit(self.id) != 0; + } +}; + +/// Convex hull collision shape defined by a set of bounding planes, vertices, and +/// polygon connectivity. Suitable for arbitrary convex polyhedra. +pub const Convex = struct { + id: c.dGeomID, + + const methods = GeomMethods(Convex); + pub const destroy = methods.destroy; + pub const set_body = methods.set_body; + pub const get_body = methods.get_body; + pub const set_position = methods.set_position; + pub const get_position = methods.get_position; + pub const set_rotation = methods.set_rotation; + pub const get_rotation = methods.get_rotation; + pub const set_quaternion = methods.set_quaternion; + pub const get_quaternion = methods.get_quaternion; + pub const get_aabb = methods.get_aabb; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const is_space = methods.is_space; + pub const get_space = methods.get_space; + pub const get_class = methods.get_class; + pub const set_category_bits = methods.set_category_bits; + pub const get_category_bits = methods.get_category_bits; + pub const set_collide_bits = methods.set_collide_bits; + pub const get_collide_bits = methods.get_collide_bits; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const set_offset_position = methods.set_offset_position; + pub const get_offset_position = methods.get_offset_position; + pub const set_offset_rotation = methods.set_offset_rotation; + pub const get_offset_rotation = methods.get_offset_rotation; + pub const set_offset_quaternion = methods.set_offset_quaternion; + pub const get_offset_quaternion = methods.get_offset_quaternion; + pub const set_offset_world_position = methods.set_offset_world_position; + pub const set_offset_world_rotation = methods.set_offset_world_rotation; + pub const set_offset_world_quaternion = methods.set_offset_world_quaternion; + pub const clear_offset = methods.clear_offset; + pub const is_offset = methods.is_offset; + pub const to_generic = methods.to_generic; + + /// Creates a convex hull geom from bounding planes, vertex positions, and polygon index data. + /// `polygons` encodes face connectivity: each face is prefixed by its vertex count followed by indices. + pub fn create( + space: ?Space.Generic, + planes: [*]const Real, + plane_count: c_uint, + points: [*]const Real, + point_count: c_uint, + polygons: [*]const c_uint, + ) Convex { + return .{ .id = c.dCreateConvex( + if (space) |s| s.id else null, + planes, + plane_count, + points, + point_count, + polygons, + ) }; + } + + /// Replaces this convex hull's geometry data (planes, points, and polygon connectivity). + pub fn set_convex( + self: Convex, + planes: [*]const Real, + plane_count: c_uint, + points: [*]const Real, + point_count: c_uint, + polygons: [*]const c_uint, + ) void { + c.dGeomSetConvex(self.id, planes, plane_count, points, point_count, polygons); + } +}; + +/// Triangle mesh collision shape built from indexed vertex data. +/// Suitable for complex static environments or detailed collision geometry. +pub const TriMesh = struct { + id: c.dGeomID, + + const methods = GeomMethods(TriMesh); + pub const destroy = methods.destroy; + pub const set_body = methods.set_body; + pub const get_body = methods.get_body; + pub const set_position = methods.set_position; + pub const get_position = methods.get_position; + pub const set_rotation = methods.set_rotation; + pub const get_rotation = methods.get_rotation; + pub const set_quaternion = methods.set_quaternion; + pub const get_quaternion = methods.get_quaternion; + pub const get_aabb = methods.get_aabb; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const is_space = methods.is_space; + pub const get_space = methods.get_space; + pub const get_class = methods.get_class; + pub const set_category_bits = methods.set_category_bits; + pub const get_category_bits = methods.get_category_bits; + pub const set_collide_bits = methods.set_collide_bits; + pub const get_collide_bits = methods.get_collide_bits; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const set_offset_position = methods.set_offset_position; + pub const get_offset_position = methods.get_offset_position; + pub const set_offset_rotation = methods.set_offset_rotation; + pub const get_offset_rotation = methods.get_offset_rotation; + pub const set_offset_quaternion = methods.set_offset_quaternion; + pub const get_offset_quaternion = methods.get_offset_quaternion; + pub const set_offset_world_position = methods.set_offset_world_position; + pub const set_offset_world_rotation = methods.set_offset_world_rotation; + pub const set_offset_world_quaternion = methods.set_offset_world_quaternion; + pub const clear_offset = methods.clear_offset; + pub const is_offset = methods.is_offset; + pub const to_generic = methods.to_generic; + + /// Opaque handle to pre-built triangle mesh data (vertices, indices, and optional normals). + /// Must be created and populated before constructing a TriMesh geom. + pub const Data = struct { + id: c.dTriMeshDataID, + + /// Allocates a new empty trimesh data object. + pub fn create() Data { + return .{ .id = c.dGeomTriMeshDataCreate() }; + } + + /// Frees the trimesh data object and its internal storage. + pub fn destroy_data(self: Data) void { + c.dGeomTriMeshDataDestroy(self.id); + } + + /// Fills the trimesh data from single-precision (float) vertex and index arrays. + pub fn build_single( + self: Data, + vertices: *const anyopaque, + vertex_stride: c_int, + vertex_count: c_int, + indices: *const anyopaque, + index_count: c_int, + tri_stride: c_int, + ) void { + c.dGeomTriMeshDataBuildSingle(self.id, vertices, vertex_stride, vertex_count, indices, index_count, tri_stride); + } + + /// Like `build_single` but also accepts a per-triangle normals array for faster collision. + pub fn build_single1( + self: Data, + vertices: *const anyopaque, + vertex_stride: c_int, + vertex_count: c_int, + indices: *const anyopaque, + index_count: c_int, + tri_stride: c_int, + normals: *const anyopaque, + ) void { + c.dGeomTriMeshDataBuildSingle1(self.id, vertices, vertex_stride, vertex_count, indices, index_count, tri_stride, normals); + } + + /// Fills the trimesh data from double-precision (f64) vertex and index arrays. + pub fn build_double( + self: Data, + vertices: *const anyopaque, + vertex_stride: c_int, + vertex_count: c_int, + indices: *const anyopaque, + index_count: c_int, + tri_stride: c_int, + ) void { + c.dGeomTriMeshDataBuildDouble(self.id, vertices, vertex_stride, vertex_count, indices, index_count, tri_stride); + } + + /// Preprocesses the mesh data to build internal acceleration structures. Returns true on success. + pub fn preprocess(self: Data) bool { + return c.dGeomTriMeshDataPreprocess(self.id) != 0; + } + }; + + /// Creates a trimesh geom from pre-built mesh data, optionally inserting it into a space. + pub fn create_trimesh(space: ?Space.Generic, data: Data) TriMesh { + return .{ .id = c.dCreateTriMesh( + if (space) |s| s.id else null, + data.id, + null, + null, + null, + ) }; + } + + /// Replaces the trimesh data associated with this geom. + pub fn set_trimesh_data(self: TriMesh, data: Data) void { + c.dGeomTriMeshSetData(self.id, data.id); + } + + /// Returns the trimesh data handle associated with this geom. + pub fn get_trimesh_data(self: TriMesh) Data { + return .{ .id = c.dGeomTriMeshGetData(self.id) }; + } + + /// Enables or disables temporal coherence caching for collisions against a specific geom class. + /// This can speed up repeated collision checks between the same pair of geoms. + pub fn enable_tc(self: TriMesh, geom_class: c_int, en: bool) void { + c.dGeomTriMeshEnableTC(self.id, geom_class, @intFromBool(en)); + } + + /// Returns whether temporal coherence caching is enabled for the given geom class. + pub fn is_tc_enabled(self: TriMesh, geom_class: c_int) bool { + return c.dGeomTriMeshIsTCEnabled(self.id, geom_class) != 0; + } + + /// Clears the temporal coherence cache, forcing fresh collision computation on the next step. + pub fn clear_tc_cache(self: TriMesh) void { + c.dGeomTriMeshClearTCCache(self.id); + } + + /// Returns the total number of triangles in this trimesh. + pub fn get_triangle_count(self: TriMesh) c_int { + return c.dGeomTriMeshGetTriangleCount(self.id); + } + + /// Computes a point on a triangle using barycentric coordinates (u, v). + /// `index` is the triangle index; the returned point is in world coordinates. + pub fn get_point(self: TriMesh, index: c_int, u: Real, v: Real) [3]Real { + var out: c.dVector3 = undefined; + c.dGeomTriMeshGetPoint(self.id, index, u, v, &out); + return .{ out[0], out[1], out[2] }; + } +}; + +/// Deprecated wrapper that applies a relative transform to another geom. +/// Prefer using the body-relative offset functions (`set_offset_position`, etc.) instead. +pub const GeomTransform = struct { + id: c.dGeomID, + + const methods = GeomMethods(GeomTransform); + pub const destroy = methods.destroy; + pub const set_body = methods.set_body; + pub const get_body = methods.get_body; + pub const set_position = methods.set_position; + pub const get_position = methods.get_position; + pub const set_rotation = methods.set_rotation; + pub const get_rotation = methods.get_rotation; + pub const set_quaternion = methods.set_quaternion; + pub const get_quaternion = methods.get_quaternion; + pub const get_aabb = methods.get_aabb; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const is_space = methods.is_space; + pub const get_space = methods.get_space; + pub const get_class = methods.get_class; + pub const set_category_bits = methods.set_category_bits; + pub const get_category_bits = methods.get_category_bits; + pub const set_collide_bits = methods.set_collide_bits; + pub const get_collide_bits = methods.get_collide_bits; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const set_offset_position = methods.set_offset_position; + pub const get_offset_position = methods.get_offset_position; + pub const set_offset_rotation = methods.set_offset_rotation; + pub const get_offset_rotation = methods.get_offset_rotation; + pub const set_offset_quaternion = methods.set_offset_quaternion; + pub const get_offset_quaternion = methods.get_offset_quaternion; + pub const set_offset_world_position = methods.set_offset_world_position; + pub const set_offset_world_rotation = methods.set_offset_world_rotation; + pub const set_offset_world_quaternion = methods.set_offset_world_quaternion; + pub const clear_offset = methods.clear_offset; + pub const is_offset = methods.is_offset; + pub const to_generic = methods.to_generic; + + /// Creates a geom transform wrapper, optionally inserting it into a space. + pub fn create_transform(space: ?Space.Generic) GeomTransform { + return .{ .id = c.dCreateGeomTransform(if (space) |s| s.id else null) }; + } + + /// Sets the child geom whose collision shape is used, positioned relative to this transform. + pub fn set_geom(self: GeomTransform, geom: Generic) void { + c.dGeomTransformSetGeom(self.id, geom.id); + } + + /// Returns the child geom wrapped by this transform. + pub fn get_geom(self: GeomTransform) Generic { + return .{ .id = c.dGeomTransformGetGeom(self.id) }; + } + + /// When cleanup is true, destroying this GeomTransform also destroys the child geom. + pub fn set_cleanup(self: GeomTransform, mode: bool) void { + c.dGeomTransformSetCleanup(self.id, @intFromBool(mode)); + } + + /// Returns whether auto-cleanup of the child geom is enabled. + pub fn get_cleanup(self: GeomTransform) bool { + return c.dGeomTransformGetCleanup(self.id) != 0; + } + + /// Sets the info mode, controlling what information is returned for collisions (0 or 1). + pub fn set_info(self: GeomTransform, mode: c_int) void { + c.dGeomTransformSetInfo(self.id, mode); + } + + /// Returns the current info mode. + pub fn get_info(self: GeomTransform) c_int { + return c.dGeomTransformGetInfo(self.id); + } +}; diff --git a/src/Heightfield.zig b/src/Heightfield.zig new file mode 100644 index 0000000..a489d1a --- /dev/null +++ b/src/Heightfield.zig @@ -0,0 +1,149 @@ +//! Heightfield collision geometry: a regular 2D grid of height samples forming terrain. +//! Build a `Data` handle from height arrays or a callback, then create a Heightfield geom from it. + +const c = @import("c.zig").c; +const Real = c.dReal; +const Geom = @import("Geom.zig"); +const Space = @import("Space.zig"); + +/// Stores the height sample data. One Data can be shared by multiple Heightfield geoms. +pub const Data = struct { + id: c.dHeightfieldDataID, + + pub fn create() Data { + return .{ .id = c.dGeomHeightfieldDataCreate() }; + } + + pub fn destroy(self: Data) void { + c.dGeomHeightfieldDataDestroy(self.id); + } + + /// Signature for the per-sample callback used by `build_callback`. + /// Returns the height at grid coordinates (x, z). + pub const HeightCallback = *const fn (userdata: ?*anyopaque, x: c_int, z: c_int) callconv(.c) Real; + + /// Build height data from a callback that returns the height at each grid cell. + /// `width`/`depth` are the world-space dimensions, `width_samples`/`depth_samples` are the grid resolution. + /// `scale` multiplies the callback return value, `offset` shifts it vertically. + /// `thickness` adds a solid shell below the surface for objects that tunnel through. + /// `wrap` tiles the heightfield infinitely if true. + pub fn build_callback( + self: Data, + userdata: ?*anyopaque, + callback: HeightCallback, + width: Real, + depth: Real, + width_samples: c_int, + depth_samples: c_int, + scale: Real, + offset: Real, + thickness: Real, + wrap: bool, + ) void { + c.dGeomHeightfieldDataBuildCallback(self.id, userdata, callback, width, depth, width_samples, depth_samples, scale, offset, thickness, @intFromBool(wrap)); + } + + /// Build height data from an array of u8 height samples. If `copy` is true, ODE + /// makes an internal copy; otherwise the pointer must remain valid for the Data's lifetime. + pub fn build_byte( + self: Data, + height_data: [*]const u8, + copy: bool, + width: Real, + depth: Real, + width_samples: c_int, + depth_samples: c_int, + scale: Real, + offset: Real, + thickness: Real, + wrap: bool, + ) void { + c.dGeomHeightfieldDataBuildByte(self.id, height_data, @intFromBool(copy), width, depth, width_samples, depth_samples, scale, offset, thickness, @intFromBool(wrap)); + } + + /// Build height data from an array of i16 height samples. + pub fn build_short( + self: Data, + height_data: [*]const i16, + copy: bool, + width: Real, + depth: Real, + width_samples: c_int, + depth_samples: c_int, + scale: Real, + offset: Real, + thickness: Real, + wrap: bool, + ) void { + c.dGeomHeightfieldDataBuildShort(self.id, height_data, @intFromBool(copy), width, depth, width_samples, depth_samples, scale, offset, thickness, @intFromBool(wrap)); + } + + /// Build height data from an array of f32 height samples. + pub fn build_single( + self: Data, + height_data: [*]const f32, + copy: bool, + width: Real, + depth: Real, + width_samples: c_int, + depth_samples: c_int, + scale: Real, + offset: Real, + thickness: Real, + wrap: bool, + ) void { + c.dGeomHeightfieldDataBuildSingle(self.id, height_data, @intFromBool(copy), width, depth, width_samples, depth_samples, scale, offset, thickness, @intFromBool(wrap)); + } + + /// Build height data from an array of f64 height samples. + pub fn build_double( + self: Data, + height_data: [*]const f64, + copy: bool, + width: Real, + depth: Real, + width_samples: c_int, + depth_samples: c_int, + scale: Real, + offset: Real, + thickness: Real, + wrap: bool, + ) void { + c.dGeomHeightfieldDataBuildDouble(self.id, height_data, @intFromBool(copy), width, depth, width_samples, depth_samples, scale, offset, thickness, @intFromBool(wrap)); + } + + /// Override the automatically computed min/max height bounds. Useful when the AABB + /// computed from sample data is too conservative. + pub fn set_bounds(self: Data, min_height: Real, max_height: Real) void { + c.dGeomHeightfieldDataSetBounds(self.id, min_height, max_height); + } +}; + +id: c.dGeomID, + +const Self = @This(); + +/// Create a heightfield geom from pre-built Data. If `placeable` is true the heightfield +/// can be positioned/rotated freely; otherwise it is fixed at the origin (more efficient). +pub fn create(space: ?Space.Generic, data: Data, placeable: bool) Self { + return .{ .id = c.dCreateHeightfield( + if (space) |s| s.id else null, + data.id, + @intFromBool(placeable), + ) }; +} + +/// Replace the height data used by this geom. +pub fn set_data(self: Self, data: Data) void { + c.dGeomHeightfieldSetHeightfieldData(self.id, data.id); +} + +/// Get the height data currently used by this geom. +pub fn get_data(self: Self) Data { + return .{ .id = c.dGeomHeightfieldGetHeightfieldData(self.id) }; +} + +/// Convert to a generic Geom handle for use with collision functions and space operations. +pub fn to_geom(self: Self) Geom.Generic { + return .{ .id = self.id }; +} diff --git a/src/Joint.zig b/src/Joint.zig new file mode 100644 index 0000000..c164cc9 --- /dev/null +++ b/src/Joint.zig @@ -0,0 +1,1099 @@ +//! ODE joint types and their Zig bindings. +//! +//! Joints constrain the relative motion of two rigid bodies (or one body +//! relative to the static environment). Each concrete joint type removes +//! specific degrees of freedom -- for example a `Hinge` allows only +//! single-axis rotation, while a `Ball` permits free rotation about a point. +//! +//! Temporary per-step joints (typically `Contact`) should be allocated in a +//! `Group` so they can be destroyed efficiently with a single `empty` call. + +const c = @import("c.zig").c; +const Real = c.dReal; +const Body = @import("Body.zig"); +const World = @import("World.zig"); +const collision = @import("collision.zig"); + +/// Per-joint motor, limit, and softness parameters. +/// +/// Unsuffixed values apply to the joint's first axis. The `2` and `3` +/// suffixes address the second and third axes of multi-axis joints +/// (e.g. Universal, AMotor). +/// +/// Key groups: +/// - `lo_stop` / `hi_stop` -- angular or linear travel limits. +/// - `vel` / `f_max` -- motor target velocity and maximum force. +/// - `bounce` -- restitution coefficient when a limit is hit. +/// - `cfm` / `erp` -- per-joint constraint-force mixing and error-reduction, +/// controlling softness and error correction strength. +/// - `stop_erp` / `stop_cfm` -- softness at the limit stops specifically. +/// - `suspension_erp` / `suspension_cfm` -- used by Hinge2 for vehicle +/// suspension spring/damper behavior. +pub const Param = enum(c_int) { + lo_stop = c.dParamLoStop, + hi_stop = c.dParamHiStop, + vel = c.dParamVel, + lo_vel = c.dParamLoVel, + hi_vel = c.dParamHiVel, + f_max = c.dParamFMax, + fudge_factor = c.dParamFudgeFactor, + bounce = c.dParamBounce, + cfm = c.dParamCFM, + stop_erp = c.dParamStopERP, + stop_cfm = c.dParamStopCFM, + suspension_erp = c.dParamSuspensionERP, + suspension_cfm = c.dParamSuspensionCFM, + erp = c.dParamERP, + lo_stop2 = c.dParamLoStop2, + hi_stop2 = c.dParamHiStop2, + vel2 = c.dParamVel2, + lo_vel2 = c.dParamLoVel2, + hi_vel2 = c.dParamHiVel2, + f_max2 = c.dParamFMax2, + fudge_factor2 = c.dParamFudgeFactor2, + bounce2 = c.dParamBounce2, + cfm2 = c.dParamCFM2, + stop_erp2 = c.dParamStopERP2, + stop_cfm2 = c.dParamStopCFM2, + suspension_erp2 = c.dParamSuspensionERP2, + suspension_cfm2 = c.dParamSuspensionCFM2, + erp2 = c.dParamERP2, + lo_stop3 = c.dParamLoStop3, + hi_stop3 = c.dParamHiStop3, + vel3 = c.dParamVel3, + lo_vel3 = c.dParamLoVel3, + hi_vel3 = c.dParamHiVel3, + f_max3 = c.dParamFMax3, + fudge_factor3 = c.dParamFudgeFactor3, + bounce3 = c.dParamBounce3, + cfm3 = c.dParamCFM3, + stop_erp3 = c.dParamStopERP3, + stop_cfm3 = c.dParamStopCFM3, + suspension_erp3 = c.dParamSuspensionERP3, + suspension_cfm3 = c.dParamSuspensionCFM3, + erp3 = c.dParamERP3, +}; + +/// Discriminant for the concrete ODE joint type behind a joint handle. +pub const JointType = enum(c_int) { + /// No joint / invalid. + none = c.dJointTypeNone, + /// Ball-and-socket: 3 rotational degrees of freedom. + ball = c.dJointTypeBall, + /// Hinge: single-axis rotation. + hinge = c.dJointTypeHinge, + /// Slider: single-axis translation (prismatic). + slider = c.dJointTypeSlider, + /// Contact: temporary collision-response joint, usually in a Group. + contact = c.dJointTypeContact, + /// Universal (Cardan): 2 perpendicular rotation axes. + universal = c.dJointTypeUniversal, + /// Hinge2: suspension joint with 2 hinge axes (e.g. car wheel). + hinge2 = c.dJointTypeHinge2, + /// Fixed: rigidly locks two bodies together (mainly for debugging). + fixed = c.dJointTypeFixed, + /// Null: placeholder joint with no effect on the simulation. + @"null" = c.dJointTypeNull, + /// Angular motor: drives or limits rotation on up to 3 axes. + a_motor = c.dJointTypeAMotor, + /// Linear motor: drives or limits translation on up to 3 axes. + l_motor = c.dJointTypeLMotor, + /// Plane2D: constrains a body to the XY plane. + plane2d = c.dJointTypePlane2D, + /// Prismatic-rotoide: combined slider + hinge. + pr = c.dJointTypePR, + /// Prismatic-universal: combined slider + universal. + pu = c.dJointTypePU, + /// Piston: slider that also allows rotation around the slide axis. + piston = c.dJointTypePiston, + /// Distance-preserving ball: spring-like, maintains distance between anchors. + d_ball = c.dJointTypeDBall, + /// Distance-preserving hinge: spring-like hinge that preserves anchor distance. + d_hinge = c.dJointTypeDHinge, + /// Transmission: gear, belt, or chain coupling two rotating bodies. + transmission = c.dJointTypeTransmission, +}; + +/// Returns a struct of common joint methods parameterized for the given type. +/// Every joint struct (Generic, Ball, Hinge, etc.) re-exports these as its own methods. +fn JointMethods(comptime Self: type) type { + return struct { + /// Remove this joint from the simulation and free its resources. + pub fn destroy(self: Self) void { c.dJointDestroy(self.id); } + /// Connect this joint to two bodies. Pass `null` for either body to + /// attach that side to the static environment. + pub fn attach(self: Self, body1: ?Body, body2: ?Body) void { c.dJointAttach(self.id, if (body1) |b| b.id else null, if (body2) |b| b.id else null); } + /// Allow this joint's constraint to take effect in the simulation. + pub fn enable(self: Self) void { c.dJointEnable(self.id); } + /// Temporarily suspend this joint's constraint without destroying it. + pub fn disable(self: Self) void { c.dJointDisable(self.id); } + /// Returns true if this joint is currently active in the simulation. + pub fn is_enabled(self: Self) bool { return c.dJointIsEnabled(self.id) != 0; } + /// Returns the number of bodies attached to this joint (0, 1, or 2). + pub fn get_num_bodies(self: Self) c_int { return c.dJointGetNumBodies(self.id); } + /// Returns the body attached at the given index (0 or 1), or null if + /// that side is attached to the static environment. + pub fn get_body(self: Self, index: c_int) ?Body { const b = c.dJointGetBody(self.id, index); return if (b != null) .{ .id = b } else null; } + /// Store an arbitrary user pointer on this joint. + pub fn set_data(self: Self, data: ?*anyopaque) void { c.dJointSetData(self.id, data); } + /// Retrieve the user pointer previously stored with `set_data`. + pub fn get_data(self: Self) ?*anyopaque { return c.dJointGetData(self.id); } + /// Returns the concrete `JointType` discriminant for this joint. + pub fn get_type(self: Self) JointType { return @enumFromInt(c.dJointGetType(self.id)); } + /// Attach a feedback struct that ODE will fill each step with the + /// constraint forces and torques applied by this joint. + pub fn set_feedback(self: Self, feedback: ?*c.dJointFeedback) void { c.dJointSetFeedback(self.id, feedback); } + /// Returns the feedback struct, or null if none was set. + pub fn get_feedback(self: Self) ?*c.dJointFeedback { return c.dJointGetFeedback(self.id); } + /// Erase the concrete type and return a type-erased `Generic` handle. + pub fn to_generic(self: Self) Generic { return .{ .id = self.id }; } + }; +} + +/// Type-erased joint handle. Provides only the operations common to all +/// joint types. Useful when storing heterogeneous joints in a single collection. +pub const Generic = struct { + id: c.dJointID, + + const methods = JointMethods(Generic); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; +}; + +/// Container for temporary joints (typically contact joints created during +/// collision handling). Call `empty` once per simulation step to destroy all +/// contained joints efficiently in bulk. +pub const Group = struct { + id: c.dJointGroupID, + + /// Allocate a new, empty joint group. + pub fn create() Group { + return .{ .id = c.dJointGroupCreate(0) }; + } + + /// Destroy the group and all joints it contains. + pub fn destroy(self: Group) void { + c.dJointGroupDestroy(self.id); + } + + /// Destroy all joints in the group without destroying the group itself. + /// Call this each step before creating new contact joints. + pub fn empty(self: Group) void { + c.dJointGroupEmpty(self.id); + } +}; + +/// Ball-and-socket joint -- allows 3 rotational degrees of freedom around a +/// single anchor point, like a shoulder joint. +pub const Ball = struct { + id: c.dJointID, + + const methods = JointMethods(Ball); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a ball joint in the given world, optionally adding it to a group. + pub fn create(world: World, group: ?Group) Ball { + return .{ .id = c.dJointCreateBall(world.id, if (group) |g| g.id else null) }; + } + + /// Set the anchor point in world coordinates where the two bodies connect. + pub fn set_anchor(self: Ball, anchor: [3]Real) void { + c.dJointSetBallAnchor(self.id, anchor[0], anchor[1], anchor[2]); + } + + /// Read back the anchor point on body 1 in world coordinates. + pub fn get_anchor(self: Ball) [3]Real { + var result: c.dVector3 = undefined; + c.dJointGetBallAnchor(self.id, &result); + return .{ result[0], result[1], result[2] }; + } + + /// Read back the anchor point on body 2 in world coordinates. Drift + /// between `get_anchor` and `get_anchor2` indicates joint error. + pub fn get_anchor2(self: Ball) [3]Real { + var result: c.dVector3 = undefined; + c.dJointGetBallAnchor2(self.id, &result); + return .{ result[0], result[1], result[2] }; + } + + /// Set a joint parameter (motor/limit value) for this ball joint. + pub fn set_param(self: Ball, parameter: Param, value: Real) void { + c.dJointSetBallParam(self.id, @intFromEnum(parameter), value); + } + + /// Get a joint parameter (motor/limit value) for this ball joint. + pub fn get_param(self: Ball, parameter: Param) Real { + return c.dJointGetBallParam(self.id, @intFromEnum(parameter)); + } +}; + +/// Hinge joint -- single-axis rotation, like a door hinge. The joint +/// constrains the two bodies to share an anchor point and rotate only +/// around the specified axis. +pub const Hinge = struct { + id: c.dJointID, + + const methods = JointMethods(Hinge); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a hinge joint in the given world, optionally adding it to a group. + pub fn create(world: World, group: ?Group) Hinge { + return .{ .id = c.dJointCreateHinge(world.id, if (group) |g| g.id else null) }; + } + + /// Set the anchor point in world coordinates. + pub fn set_anchor(self: Hinge, anchor: [3]Real) void { + c.dJointSetHingeAnchor(self.id, anchor[0], anchor[1], anchor[2]); + } + + /// Read back the anchor point on body 1 in world coordinates. + pub fn get_anchor(self: Hinge) [3]Real { + var result: c.dVector3 = undefined; + c.dJointGetHingeAnchor(self.id, &result); + return .{ result[0], result[1], result[2] }; + } + + /// Read back the anchor point on body 2. Drift from `get_anchor` + /// indicates accumulated joint error. + pub fn get_anchor2(self: Hinge) [3]Real { + var result: c.dVector3 = undefined; + c.dJointGetHingeAnchor2(self.id, &result); + return .{ result[0], result[1], result[2] }; + } + + /// Set the hinge rotation axis in world coordinates. + pub fn set_axis(self: Hinge, axis: [3]Real) void { + c.dJointSetHingeAxis(self.id, axis[0], axis[1], axis[2]); + } + + /// Get the hinge rotation axis in world coordinates. + pub fn get_axis(self: Hinge) [3]Real { + var result: c.dVector3 = undefined; + c.dJointGetHingeAxis(self.id, &result); + return .{ result[0], result[1], result[2] }; + } + + /// Current rotation angle (radians) relative to the initial configuration. + pub fn get_angle(self: Hinge) Real { + return c.dJointGetHingeAngle(self.id); + } + + /// Time derivative of the hinge angle (radians/second). + pub fn get_angle_rate(self: Hinge) Real { + return c.dJointGetHingeAngleRate(self.id); + } + + /// Set a joint parameter (motor/limit value) for this hinge. + pub fn set_param(self: Hinge, parameter: Param, value: Real) void { + c.dJointSetHingeParam(self.id, @intFromEnum(parameter), value); + } + + /// Get a joint parameter (motor/limit value) for this hinge. + pub fn get_param(self: Hinge, parameter: Param) Real { + return c.dJointGetHingeParam(self.id, @intFromEnum(parameter)); + } + + /// Apply a torque about the hinge axis to both attached bodies. + pub fn add_torque(self: Hinge, torque: Real) void { + c.dJointAddHingeTorque(self.id, torque); + } +}; + +/// Slider (prismatic) joint -- allows translation along a single axis with +/// no rotation. Think of a piston rod constrained to slide without twisting. +pub const Slider = struct { + id: c.dJointID, + + const methods = JointMethods(Slider); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a slider joint in the given world, optionally adding it to a group. + pub fn create(world: World, group: ?Group) Slider { + return .{ .id = c.dJointCreateSlider(world.id, if (group) |g| g.id else null) }; + } + + /// Set the sliding axis direction in world coordinates. + pub fn set_axis(self: Slider, axis: [3]Real) void { + c.dJointSetSliderAxis(self.id, axis[0], axis[1], axis[2]); + } + + /// Get the sliding axis direction in world coordinates. + pub fn get_axis(self: Slider) [3]Real { + var result: c.dVector3 = undefined; + c.dJointGetSliderAxis(self.id, &result); + return .{ result[0], result[1], result[2] }; + } + + /// Linear displacement along the slider axis relative to the initial position. + pub fn get_position(self: Slider) Real { + return c.dJointGetSliderPosition(self.id); + } + + /// Time derivative of the slider position (linear velocity along the axis). + pub fn get_position_rate(self: Slider) Real { + return c.dJointGetSliderPositionRate(self.id); + } + + /// Set a joint parameter (motor/limit value) for this slider. + pub fn set_param(self: Slider, parameter: Param, value: Real) void { + c.dJointSetSliderParam(self.id, @intFromEnum(parameter), value); + } + + /// Get a joint parameter (motor/limit value) for this slider. + pub fn get_param(self: Slider, parameter: Param) Real { + return c.dJointGetSliderParam(self.id, @intFromEnum(parameter)); + } + + /// Apply a force along the slider axis to both attached bodies. + pub fn add_force(self: Slider, force: Real) void { + c.dJointAddSliderForce(self.id, force); + } +}; + +/// Contact joint -- a temporary constraint created from a collision contact +/// point. These are typically created each simulation step inside a `Group` +/// and destroyed in bulk via `Group.empty`. +pub const Contact = struct { + id: c.dJointID, + + const methods = JointMethods(Contact); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a contact joint from a collision `Contact` struct. The joint + /// should be added to a group so it can be bulk-destroyed each step. + pub fn create(world: World, group: ?Group, contact: *const collision.Contact) Contact { + return .{ .id = c.dJointCreateContact(world.id, if (group) |g| g.id else null, contact) }; + } +}; + +/// Universal (Cardan) joint -- two perpendicular rotation axes, like a +/// universal joint in a drive shaft. Axis 1 is attached to body 1, axis 2 +/// to body 2, and they are kept perpendicular by the constraint. +pub const Universal = struct { + id: c.dJointID, + + const methods = JointMethods(Universal); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a universal joint in the given world, optionally adding it to a group. + pub fn create(world: World, group: ?Group) Universal { + return .{ .id = c.dJointCreateUniversal(world.id, if (group) |g| g.id else null) }; + } + + /// Set the anchor point in world coordinates. + pub fn set_anchor(self: Universal, anchor: [3]Real) void { + c.dJointSetUniversalAnchor(self.id, anchor[0], anchor[1], anchor[2]); + } + + /// Read back the anchor point on body 1 in world coordinates. + pub fn get_anchor(self: Universal) [3]Real { + var result: c.dVector3 = undefined; + c.dJointGetUniversalAnchor(self.id, &result); + return .{ result[0], result[1], result[2] }; + } + + /// Read back the anchor point on body 2. Drift from `get_anchor` + /// indicates accumulated joint error. + pub fn get_anchor2(self: Universal) [3]Real { + var result: c.dVector3 = undefined; + c.dJointGetUniversalAnchor2(self.id, &result); + return .{ result[0], result[1], result[2] }; + } + + /// Set rotation axis 1 (attached to body 1) in world coordinates. + pub fn set_axis1(self: Universal, axis: [3]Real) void { + c.dJointSetUniversalAxis1(self.id, axis[0], axis[1], axis[2]); + } + + /// Get rotation axis 1 in world coordinates. + pub fn get_axis1(self: Universal) [3]Real { + var result: c.dVector3 = undefined; + c.dJointGetUniversalAxis1(self.id, &result); + return .{ result[0], result[1], result[2] }; + } + + /// Set rotation axis 2 (attached to body 2) in world coordinates. + pub fn set_axis2(self: Universal, axis: [3]Real) void { + c.dJointSetUniversalAxis2(self.id, axis[0], axis[1], axis[2]); + } + + /// Get rotation axis 2 in world coordinates. + pub fn get_axis2(self: Universal) [3]Real { + var result: c.dVector3 = undefined; + c.dJointGetUniversalAxis2(self.id, &result); + return .{ result[0], result[1], result[2] }; + } + + /// Current rotation angle (radians) around axis 1. + pub fn get_angle1(self: Universal) Real { return c.dJointGetUniversalAngle1(self.id); } + /// Current rotation angle (radians) around axis 2. + pub fn get_angle2(self: Universal) Real { return c.dJointGetUniversalAngle2(self.id); } + /// Angular velocity (radians/second) around axis 1. + pub fn get_angle1_rate(self: Universal) Real { return c.dJointGetUniversalAngle1Rate(self.id); } + /// Angular velocity (radians/second) around axis 2. + pub fn get_angle2_rate(self: Universal) Real { return c.dJointGetUniversalAngle2Rate(self.id); } + + /// Set a joint parameter. Use suffixed params (e.g. `vel2`) for axis 2. + pub fn set_param(self: Universal, parameter: Param, value: Real) void { + c.dJointSetUniversalParam(self.id, @intFromEnum(parameter), value); + } + + /// Get a joint parameter. Use suffixed params (e.g. `vel2`) for axis 2. + pub fn get_param(self: Universal, parameter: Param) Real { + return c.dJointGetUniversalParam(self.id, @intFromEnum(parameter)); + } + + /// Apply torques about axis 1 and axis 2 to both attached bodies. + pub fn add_torques(self: Universal, torque1: Real, torque2: Real) void { + c.dJointAddUniversalTorques(self.id, torque1, torque2); + } +}; + +/// Hinge2 (suspension) joint -- two hinge axes where axis 1 is the steering +/// axis and axis 2 is the wheel spin axis. Commonly used for vehicle wheel +/// suspension; the `suspension_erp`/`suspension_cfm` parameters control the +/// spring/damper behavior. +pub const Hinge2 = struct { + id: c.dJointID, + + const methods = JointMethods(Hinge2); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a hinge2 joint in the given world, optionally adding it to a group. + pub fn create(world: World, group: ?Group) Hinge2 { + return .{ .id = c.dJointCreateHinge2(world.id, if (group) |g| g.id else null) }; + } + + /// Set the anchor point (typically the wheel hub) in world coordinates. + pub fn set_anchor(self: Hinge2, anchor: [3]Real) void { c.dJointSetHinge2Anchor(self.id, anchor[0], anchor[1], anchor[2]); } + /// Read back the anchor on body 1 in world coordinates. + pub fn get_anchor(self: Hinge2) [3]Real { var r: c.dVector3 = undefined; c.dJointGetHinge2Anchor(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Read back the anchor on body 2. Drift from `get_anchor` shows joint error. + pub fn get_anchor2(self: Hinge2) [3]Real { var r: c.dVector3 = undefined; c.dJointGetHinge2Anchor2(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Set axis 1 (steering axis, attached to body 1) in world coordinates. + pub fn set_axis1(self: Hinge2, axis: [3]Real) void { c.dJointSetHinge2Axis1(self.id, axis[0], axis[1], axis[2]); } + /// Get axis 1 (steering axis) in world coordinates. + pub fn get_axis1(self: Hinge2) [3]Real { var r: c.dVector3 = undefined; c.dJointGetHinge2Axis1(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Set axis 2 (wheel spin axis, attached to body 2) in world coordinates. + pub fn set_axis2(self: Hinge2, axis: [3]Real) void { c.dJointSetHinge2Axis2(self.id, axis[0], axis[1], axis[2]); } + /// Get axis 2 (wheel spin axis) in world coordinates. + pub fn get_axis2(self: Hinge2) [3]Real { var r: c.dVector3 = undefined; c.dJointGetHinge2Axis2(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Current rotation angle (radians) of axis 1 (steering angle). + pub fn get_angle1(self: Hinge2) Real { return c.dJointGetHinge2Angle1(self.id); } + /// Angular velocity (radians/second) around axis 1. + pub fn get_angle1_rate(self: Hinge2) Real { return c.dJointGetHinge2Angle1Rate(self.id); } + /// Angular velocity (radians/second) around axis 2 (wheel spin rate). + pub fn get_angle2_rate(self: Hinge2) Real { return c.dJointGetHinge2Angle2Rate(self.id); } + /// Set a joint parameter. Use suffixed params (e.g. `vel2`) for axis 2. + pub fn set_param(self: Hinge2, parameter: Param, value: Real) void { c.dJointSetHinge2Param(self.id, @intFromEnum(parameter), value); } + /// Get a joint parameter. Use suffixed params (e.g. `vel2`) for axis 2. + pub fn get_param(self: Hinge2, parameter: Param) Real { return c.dJointGetHinge2Param(self.id, @intFromEnum(parameter)); } + /// Apply torques about axis 1 and axis 2 to both attached bodies. + pub fn add_torques(self: Hinge2, torque1: Real, torque2: Real) void { c.dJointAddHinge2Torques(self.id, torque1, torque2); } +}; + +/// Fixed joint -- rigidly locks two bodies (or one body and the world) in +/// their current relative position and orientation. Mainly useful for +/// debugging; in production prefer compound collision geometries instead. +pub const Fixed = struct { + id: c.dJointID, + + const methods = JointMethods(Fixed); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a fixed joint in the given world, optionally adding it to a group. + pub fn create(world: World, group: ?Group) Fixed { + return .{ .id = c.dJointCreateFixed(world.id, if (group) |g| g.id else null) }; + } + + /// Lock the attached bodies at their current relative pose. Must be called + /// after `attach` and after positioning the bodies. + pub fn set(self: Fixed) void { c.dJointSetFixed(self.id); } + /// Set a joint parameter for this fixed joint. + pub fn set_param(self: Fixed, parameter: Param, value: Real) void { c.dJointSetFixedParam(self.id, @intFromEnum(parameter), value); } + /// Get a joint parameter for this fixed joint. + pub fn get_param(self: Fixed, parameter: Param) Real { return c.dJointGetFixedParam(self.id, @intFromEnum(parameter)); } +}; + +/// Null joint -- a placeholder that has no effect on the simulation. +/// Can be used as a sentinel or for bookkeeping purposes. +pub const Null = struct { + id: c.dJointID, + + const methods = JointMethods(Null); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a null joint in the given world, optionally adding it to a group. + pub fn create(world: World, group: ?Group) Null { + return .{ .id = c.dJointCreateNull(world.id, if (group) |g| g.id else null) }; + } +}; + +/// Angular motor -- drives or limits rotation on up to 3 axes independently. +/// Supports two modes: user-specified axes or Euler angles. Use `set_param` +/// with suffixed parameters (`vel2`, `f_max3`, etc.) to control each axis. +pub const AMotor = struct { + id: c.dJointID, + + const methods = JointMethods(AMotor); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create an angular motor joint in the given world. + pub fn create(world: World, group: ?Group) AMotor { + return .{ .id = c.dJointCreateAMotor(world.id, if (group) |g| g.id else null) }; + } + + /// Set the motor mode: 0 = user-specified axes, 1 = Euler angles. + pub fn set_mode(self: AMotor, mode: c_int) void { c.dJointSetAMotorMode(self.id, mode); } + /// Get the current motor mode. + pub fn get_mode(self: AMotor) c_int { return c.dJointGetAMotorMode(self.id); } + /// Set how many axes (0-3) this motor controls. + pub fn set_num_axes(self: AMotor, num: c_int) void { c.dJointSetAMotorNumAxes(self.id, num); } + /// Get the number of active axes. + pub fn get_num_axes(self: AMotor) c_int { return c.dJointGetAMotorNumAxes(self.id); } + /// Set the direction and reference frame for axis `anum` (0-2). + /// `rel`: 0 = global, 1 = relative to body 1, 2 = relative to body 2. + pub fn set_axis(self: AMotor, anum: c_int, rel: c_int, axis: [3]Real) void { c.dJointSetAMotorAxis(self.id, anum, rel, axis[0], axis[1], axis[2]); } + /// Get the direction of axis `anum` in world coordinates. + pub fn get_axis(self: AMotor, anum: c_int) [3]Real { var r: c.dVector3 = undefined; c.dJointGetAMotorAxis(self.id, anum, &r); return .{ r[0], r[1], r[2] }; } + /// Get the reference frame (0=global, 1=body1, 2=body2) of axis `anum`. + pub fn get_axis_rel(self: AMotor, anum: c_int) c_int { return c.dJointGetAMotorAxisRel(self.id, anum); } + /// Set the current angle (radians) for axis `anum`. Only needed in user mode; + /// Euler mode computes angles automatically. + pub fn set_angle(self: AMotor, anum: c_int, angle: Real) void { c.dJointSetAMotorAngle(self.id, anum, angle); } + /// Get the current angle (radians) of axis `anum`. + pub fn get_angle(self: AMotor, anum: c_int) Real { return c.dJointGetAMotorAngle(self.id, anum); } + /// Get the angular velocity (radians/second) of axis `anum`. + pub fn get_angle_rate(self: AMotor, anum: c_int) Real { return c.dJointGetAMotorAngleRate(self.id, anum); } + /// Set a joint parameter. Use suffixed params for axes 2 and 3. + pub fn set_param(self: AMotor, parameter: Param, value: Real) void { c.dJointSetAMotorParam(self.id, @intFromEnum(parameter), value); } + /// Get a joint parameter. Use suffixed params for axes 2 and 3. + pub fn get_param(self: AMotor, parameter: Param) Real { return c.dJointGetAMotorParam(self.id, @intFromEnum(parameter)); } + /// Apply torques about all three motor axes simultaneously. + pub fn add_torques(self: AMotor, torque0: Real, torque1: Real, torque2: Real) void { c.dJointAddAMotorTorques(self.id, torque0, torque1, torque2); } +}; + +/// Linear motor -- drives or limits translation on up to 3 axes independently. +/// Use `set_param` with suffixed parameters (`vel2`, `f_max3`, etc.) to +/// control each axis. +pub const LMotor = struct { + id: c.dJointID, + + const methods = JointMethods(LMotor); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a linear motor joint in the given world. + pub fn create(world: World, group: ?Group) LMotor { + return .{ .id = c.dJointCreateLMotor(world.id, if (group) |g| g.id else null) }; + } + + /// Set how many axes (0-3) this motor controls. + pub fn set_num_axes(self: LMotor, num: c_int) void { c.dJointSetLMotorNumAxes(self.id, num); } + /// Get the number of active axes. + pub fn get_num_axes(self: LMotor) c_int { return c.dJointGetLMotorNumAxes(self.id); } + /// Set the direction and reference frame for axis `anum` (0-2). + /// `rel`: 0 = global, 1 = relative to body 1, 2 = relative to body 2. + pub fn set_axis(self: LMotor, anum: c_int, rel: c_int, axis: [3]Real) void { c.dJointSetLMotorAxis(self.id, anum, rel, axis[0], axis[1], axis[2]); } + /// Get the direction of axis `anum` in world coordinates. + pub fn get_axis(self: LMotor, anum: c_int) [3]Real { var r: c.dVector3 = undefined; c.dJointGetLMotorAxis(self.id, anum, &r); return .{ r[0], r[1], r[2] }; } + /// Set a joint parameter. Use suffixed params for axes 2 and 3. + pub fn set_param(self: LMotor, parameter: Param, value: Real) void { c.dJointSetLMotorParam(self.id, @intFromEnum(parameter), value); } + /// Get a joint parameter. Use suffixed params for axes 2 and 3. + pub fn get_param(self: LMotor, parameter: Param) Real { return c.dJointGetLMotorParam(self.id, @intFromEnum(parameter)); } +}; + +/// Plane2D joint -- constrains a body to move only in the XY plane (z=0) +/// with rotation only around the Z axis. Useful for 2D physics simulations. +pub const Plane2D = struct { + id: c.dJointID, + + const methods = JointMethods(Plane2D); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a Plane2D joint in the given world. + pub fn create(world: World, group: ?Group) Plane2D { + return .{ .id = c.dJointCreatePlane2D(world.id, if (group) |g| g.id else null) }; + } + + /// Set a motor/limit parameter for the X translational axis. + pub fn set_x_param(self: Plane2D, parameter: Param, value: Real) void { c.dJointSetPlane2DXParam(self.id, @intFromEnum(parameter), value); } + /// Set a motor/limit parameter for the Y translational axis. + pub fn set_y_param(self: Plane2D, parameter: Param, value: Real) void { c.dJointSetPlane2DYParam(self.id, @intFromEnum(parameter), value); } + /// Set a motor/limit parameter for the Z rotational axis. + pub fn set_angle_param(self: Plane2D, parameter: Param, value: Real) void { c.dJointSetPlane2DAngleParam(self.id, @intFromEnum(parameter), value); } +}; + +/// Prismatic-rotoide (PR) joint -- combines a slider (prismatic, axis 1) +/// with a hinge (rotoide, axis 2). The body can translate along axis 1 and +/// rotate around axis 2. +pub const PR = struct { + id: c.dJointID, + + const methods = JointMethods(PR); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a PR joint in the given world. + pub fn create(world: World, group: ?Group) PR { + return .{ .id = c.dJointCreatePR(world.id, if (group) |g| g.id else null) }; + } + + /// Set the anchor point in world coordinates. + pub fn set_anchor(self: PR, anchor: [3]Real) void { c.dJointSetPRAnchor(self.id, anchor[0], anchor[1], anchor[2]); } + /// Get the anchor point in world coordinates. + pub fn get_anchor(self: PR) [3]Real { var r: c.dVector3 = undefined; c.dJointGetPRAnchor(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Set the prismatic (sliding) axis direction. + pub fn set_axis1(self: PR, axis: [3]Real) void { c.dJointSetPRAxis1(self.id, axis[0], axis[1], axis[2]); } + /// Get the prismatic (sliding) axis direction. + pub fn get_axis1(self: PR) [3]Real { var r: c.dVector3 = undefined; c.dJointGetPRAxis1(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Set the rotoide (hinge) axis direction. + pub fn set_axis2(self: PR, axis: [3]Real) void { c.dJointSetPRAxis2(self.id, axis[0], axis[1], axis[2]); } + /// Get the rotoide (hinge) axis direction. + pub fn get_axis2(self: PR) [3]Real { var r: c.dVector3 = undefined; c.dJointGetPRAxis2(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Linear displacement along the prismatic axis. + pub fn get_position(self: PR) Real { return c.dJointGetPRPosition(self.id); } + /// Rotation angle (radians) around the rotoide axis. + pub fn get_angle(self: PR) Real { return c.dJointGetPRAngle(self.id); } + /// Set a joint parameter. Use suffixed params (e.g. `vel2`) for axis 2. + pub fn set_param(self: PR, parameter: Param, value: Real) void { c.dJointSetPRParam(self.id, @intFromEnum(parameter), value); } + /// Get a joint parameter. Use suffixed params (e.g. `vel2`) for axis 2. + pub fn get_param(self: PR, parameter: Param) Real { return c.dJointGetPRParam(self.id, @intFromEnum(parameter)); } + /// Apply a torque about the rotoide axis. + pub fn add_torque(self: PR, torque: Real) void { c.dJointAddPRTorque(self.id, torque); } +}; + +/// Prismatic-universal (PU) joint -- combines a slider (prismatic) with a +/// universal joint. Allows translation along one axis plus rotation around +/// two perpendicular axes. +pub const PU = struct { + id: c.dJointID, + + const methods = JointMethods(PU); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a PU joint in the given world. + pub fn create(world: World, group: ?Group) PU { + return .{ .id = c.dJointCreatePU(world.id, if (group) |g| g.id else null) }; + } + + /// Set the anchor point in world coordinates. + pub fn set_anchor(self: PU, anchor: [3]Real) void { c.dJointSetPUAnchor(self.id, anchor[0], anchor[1], anchor[2]); } + /// Get the anchor point in world coordinates. + pub fn get_anchor(self: PU) [3]Real { var r: c.dVector3 = undefined; c.dJointGetPUAnchor(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Set universal rotation axis 1. + pub fn set_axis1(self: PU, axis: [3]Real) void { c.dJointSetPUAxis1(self.id, axis[0], axis[1], axis[2]); } + /// Get universal rotation axis 1. + pub fn get_axis1(self: PU) [3]Real { var r: c.dVector3 = undefined; c.dJointGetPUAxis1(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Set universal rotation axis 2. + pub fn set_axis2(self: PU, axis: [3]Real) void { c.dJointSetPUAxis2(self.id, axis[0], axis[1], axis[2]); } + /// Get universal rotation axis 2. + pub fn get_axis2(self: PU) [3]Real { var r: c.dVector3 = undefined; c.dJointGetPUAxis2(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Set axis 3 (alias for the prismatic axis). + pub fn set_axis3(self: PU, axis: [3]Real) void { c.dJointSetPUAxis3(self.id, axis[0], axis[1], axis[2]); } + /// Set the prismatic (sliding) axis direction. + pub fn set_axis_p(self: PU, axis: [3]Real) void { c.dJointSetPUAxisP(self.id, axis[0], axis[1], axis[2]); } + /// Get the prismatic (sliding) axis direction. + pub fn get_axis_p(self: PU) [3]Real { var r: c.dVector3 = undefined; c.dJointGetPUAxisP(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Linear displacement along the prismatic axis. + pub fn get_position(self: PU) Real { return c.dJointGetPUPosition(self.id); } + /// Time derivative of the prismatic position. + pub fn get_position_rate(self: PU) Real { return c.dJointGetPUPositionRate(self.id); } + /// Current rotation angle (radians) around universal axis 1. + pub fn get_angle1(self: PU) Real { return c.dJointGetPUAngle1(self.id); } + /// Current rotation angle (radians) around universal axis 2. + pub fn get_angle2(self: PU) Real { return c.dJointGetPUAngle2(self.id); } + /// Angular velocity (radians/second) around universal axis 1. + pub fn get_angle1_rate(self: PU) Real { return c.dJointGetPUAngle1Rate(self.id); } + /// Angular velocity (radians/second) around universal axis 2. + pub fn get_angle2_rate(self: PU) Real { return c.dJointGetPUAngle2Rate(self.id); } + /// Set a joint parameter. Use suffixed params for additional axes. + pub fn set_param(self: PU, parameter: Param, value: Real) void { c.dJointSetPUParam(self.id, @intFromEnum(parameter), value); } + /// Get a joint parameter. Use suffixed params for additional axes. + pub fn get_param(self: PU, parameter: Param) Real { return c.dJointGetPUParam(self.id, @intFromEnum(parameter)); } +}; + +/// Piston joint -- allows both translation along and rotation around a single +/// axis, like a real piston that can also spin. Combines the freedoms of a +/// slider and a hinge sharing the same axis. +pub const Piston = struct { + id: c.dJointID, + + const methods = JointMethods(Piston); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a piston joint in the given world. + pub fn create(world: World, group: ?Group) Piston { + return .{ .id = c.dJointCreatePiston(world.id, if (group) |g| g.id else null) }; + } + + /// Set the anchor point in world coordinates. + pub fn set_anchor(self: Piston, anchor: [3]Real) void { c.dJointSetPistonAnchor(self.id, anchor[0], anchor[1], anchor[2]); } + /// Read back the anchor on body 1 in world coordinates. + pub fn get_anchor(self: Piston) [3]Real { var r: c.dVector3 = undefined; c.dJointGetPistonAnchor(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Read back the anchor on body 2. Drift from `get_anchor` shows joint error. + pub fn get_anchor2(self: Piston) [3]Real { var r: c.dVector3 = undefined; c.dJointGetPistonAnchor2(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Set the piston axis (shared slide + rotation axis) in world coordinates. + pub fn set_axis(self: Piston, axis: [3]Real) void { c.dJointSetPistonAxis(self.id, axis[0], axis[1], axis[2]); } + /// Get the piston axis in world coordinates. + pub fn get_axis(self: Piston) [3]Real { var r: c.dVector3 = undefined; c.dJointGetPistonAxis(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Linear displacement along the piston axis. + pub fn get_position(self: Piston) Real { return c.dJointGetPistonPosition(self.id); } + /// Linear velocity along the piston axis. + pub fn get_position_rate(self: Piston) Real { return c.dJointGetPistonPositionRate(self.id); } + /// Rotation angle (radians) around the piston axis. + pub fn get_angle(self: Piston) Real { return c.dJointGetPistonAngle(self.id); } + /// Angular velocity (radians/second) around the piston axis. + pub fn get_angle_rate(self: Piston) Real { return c.dJointGetPistonAngleRate(self.id); } + /// Set a joint parameter. Use suffixed params (e.g. `vel2`) for the rotational axis. + pub fn set_param(self: Piston, parameter: Param, value: Real) void { c.dJointSetPistonParam(self.id, @intFromEnum(parameter), value); } + /// Get a joint parameter. + pub fn get_param(self: Piston, parameter: Param) Real { return c.dJointGetPistonParam(self.id, @intFromEnum(parameter)); } + /// Apply a force along the piston axis to both attached bodies. + pub fn add_force(self: Piston, force: Real) void { c.dJointAddPistonForce(self.id, force); } +}; + +/// Distance-preserving ball joint -- a spring-like ball-and-socket that +/// maintains a target distance between the two anchor points rather than +/// requiring them to coincide. Useful for soft constraints and ragdolls. +pub const DBall = struct { + id: c.dJointID, + + const methods = JointMethods(DBall); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a distance ball joint in the given world. + pub fn create(world: World, group: ?Group) DBall { + return .{ .id = c.dJointCreateDBall(world.id, if (group) |g| g.id else null) }; + } + + /// Set the anchor point on body 1 in world coordinates. + pub fn set_anchor1(self: DBall, anchor: [3]Real) void { c.dJointSetDBallAnchor1(self.id, anchor[0], anchor[1], anchor[2]); } + /// Set the anchor point on body 2 in world coordinates. + pub fn set_anchor2(self: DBall, anchor: [3]Real) void { c.dJointSetDBallAnchor2(self.id, anchor[0], anchor[1], anchor[2]); } + /// Get the anchor point on body 1 in world coordinates. + pub fn get_anchor1(self: DBall) [3]Real { var r: c.dVector3 = undefined; c.dJointGetDBallAnchor1(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Get the anchor point on body 2 in world coordinates. + pub fn get_anchor2(self: DBall) [3]Real { var r: c.dVector3 = undefined; c.dJointGetDBallAnchor2(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Explicitly set the target distance maintained between the two anchors. + pub fn set_distance(self: DBall, dist: Real) void { c.dJointSetDBallDistance(self.id, dist); } + /// Get the target distance between the two anchors. + pub fn get_distance(self: DBall) Real { return c.dJointGetDBallDistance(self.id); } + /// Set a joint parameter (e.g. ERP/CFM for spring/damper tuning). + pub fn set_param(self: DBall, parameter: Param, value: Real) void { c.dJointSetDBallParam(self.id, @intFromEnum(parameter), value); } + /// Get a joint parameter. + pub fn get_param(self: DBall, parameter: Param) Real { return c.dJointGetDBallParam(self.id, @intFromEnum(parameter)); } +}; + +/// Distance-preserving hinge -- a spring-like hinge that maintains a target +/// distance between the two anchor points while constraining rotation to a +/// single axis. Combines distance preservation with hinge behavior. +pub const DHinge = struct { + id: c.dJointID, + + const methods = JointMethods(DHinge); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a distance hinge joint in the given world. + pub fn create(world: World, group: ?Group) DHinge { + return .{ .id = c.dJointCreateDHinge(world.id, if (group) |g| g.id else null) }; + } + + /// Set the hinge rotation axis in world coordinates. + pub fn set_axis(self: DHinge, axis: [3]Real) void { c.dJointSetDHingeAxis(self.id, axis[0], axis[1], axis[2]); } + /// Get the hinge rotation axis in world coordinates. + pub fn get_axis(self: DHinge) [3]Real { var r: c.dVector3 = undefined; c.dJointGetDHingeAxis(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Set the anchor point on body 1 in world coordinates. + pub fn set_anchor1(self: DHinge, anchor: [3]Real) void { c.dJointSetDHingeAnchor1(self.id, anchor[0], anchor[1], anchor[2]); } + /// Set the anchor point on body 2 in world coordinates. + pub fn set_anchor2(self: DHinge, anchor: [3]Real) void { c.dJointSetDHingeAnchor2(self.id, anchor[0], anchor[1], anchor[2]); } + /// Get the anchor point on body 1 in world coordinates. + pub fn get_anchor1(self: DHinge) [3]Real { var r: c.dVector3 = undefined; c.dJointGetDHingeAnchor1(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Get the anchor point on body 2 in world coordinates. + pub fn get_anchor2(self: DHinge) [3]Real { var r: c.dVector3 = undefined; c.dJointGetDHingeAnchor2(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Get the target distance maintained between the two anchors. + pub fn get_distance(self: DHinge) Real { return c.dJointGetDHingeDistance(self.id); } + /// Set a joint parameter (e.g. ERP/CFM for spring/damper tuning). + pub fn set_param(self: DHinge, parameter: Param, value: Real) void { c.dJointSetDHingeParam(self.id, @intFromEnum(parameter), value); } + /// Get a joint parameter. + pub fn get_param(self: DHinge, parameter: Param) Real { return c.dJointGetDHingeParam(self.id, @intFromEnum(parameter)); } +}; + +/// Transmission joint -- couples two rotating bodies via a gear, belt, or +/// chain mechanism. Supports different transmission modes and configurable +/// gear ratios. +pub const Transmission = struct { + id: c.dJointID, + + const methods = JointMethods(Transmission); + pub const destroy = methods.destroy; + pub const attach = methods.attach; + pub const enable = methods.enable; + pub const disable = methods.disable; + pub const is_enabled = methods.is_enabled; + pub const get_num_bodies = methods.get_num_bodies; + pub const get_body = methods.get_body; + pub const set_data = methods.set_data; + pub const get_data = methods.get_data; + pub const get_type = methods.get_type; + pub const set_feedback = methods.set_feedback; + pub const get_feedback = methods.get_feedback; + pub const to_generic = methods.to_generic; + + /// Create a transmission joint in the given world. + pub fn create(world: World, group: ?Group) Transmission { + return .{ .id = c.dJointCreateTransmission(world.id, if (group) |g| g.id else null) }; + } + + /// Set the transmission mode: 0 = parallel axes, 1 = intersecting axes, + /// 2 = chain drive. + pub fn set_mode(self: Transmission, mode: c_int) void { c.dJointSetTransmissionMode(self.id, mode); } + /// Get the current transmission mode. + pub fn get_mode(self: Transmission) c_int { return c.dJointGetTransmissionMode(self.id); } + /// Set the gear ratio (body1 angular velocity / body2 angular velocity). + pub fn set_ratio(self: Transmission, ratio: Real) void { c.dJointSetTransmissionRatio(self.id, ratio); } + /// Get the current gear ratio. + pub fn get_ratio(self: Transmission) Real { return c.dJointGetTransmissionRatio(self.id); } + /// Set the contact/attachment point on body 1 in world coordinates. + pub fn set_anchor1(self: Transmission, anchor: [3]Real) void { c.dJointSetTransmissionAnchor1(self.id, anchor[0], anchor[1], anchor[2]); } + /// Get the contact/attachment point on body 1 in world coordinates. + pub fn get_anchor1(self: Transmission) [3]Real { var r: c.dVector3 = undefined; c.dJointGetTransmissionAnchor1(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Set the contact/attachment point on body 2 in world coordinates. + pub fn set_anchor2(self: Transmission, anchor: [3]Real) void { c.dJointSetTransmissionAnchor2(self.id, anchor[0], anchor[1], anchor[2]); } + /// Get the contact/attachment point on body 2 in world coordinates. + pub fn get_anchor2(self: Transmission) [3]Real { var r: c.dVector3 = undefined; c.dJointGetTransmissionAnchor2(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Set the rotation axis for body 1. + pub fn set_axis1(self: Transmission, axis: [3]Real) void { c.dJointSetTransmissionAxis1(self.id, axis[0], axis[1], axis[2]); } + /// Get the rotation axis for body 1. + pub fn get_axis1(self: Transmission) [3]Real { var r: c.dVector3 = undefined; c.dJointGetTransmissionAxis1(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Set the rotation axis for body 2. + pub fn set_axis2(self: Transmission, axis: [3]Real) void { c.dJointSetTransmissionAxis2(self.id, axis[0], axis[1], axis[2]); } + /// Get the rotation axis for body 2. + pub fn get_axis2(self: Transmission) [3]Real { var r: c.dVector3 = undefined; c.dJointGetTransmissionAxis2(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Set both rotation axes to the same direction (convenience for parallel axes mode). + pub fn set_axis(self: Transmission, axis: [3]Real) void { c.dJointSetTransmissionAxis(self.id, axis[0], axis[1], axis[2]); } + /// Get the point where the belt/chain contacts wheel 1 (chain mode). + pub fn get_contact_point1(self: Transmission) [3]Real { var r: c.dVector3 = undefined; c.dJointGetTransmissionContactPoint1(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Get the point where the belt/chain contacts wheel 2 (chain mode). + pub fn get_contact_point2(self: Transmission) [3]Real { var r: c.dVector3 = undefined; c.dJointGetTransmissionContactPoint2(self.id, &r); return .{ r[0], r[1], r[2] }; } + /// Set the wheel radius for body 1 (used in chain/belt modes). + pub fn set_radius1(self: Transmission, radius: Real) void { c.dJointSetTransmissionRadius1(self.id, radius); } + /// Get the wheel radius for body 1. + pub fn get_radius1(self: Transmission) Real { return c.dJointGetTransmissionRadius1(self.id); } + /// Set the wheel radius for body 2 (used in chain/belt modes). + pub fn set_radius2(self: Transmission, radius: Real) void { c.dJointSetTransmissionRadius2(self.id, radius); } + /// Get the wheel radius for body 2. + pub fn get_radius2(self: Transmission) Real { return c.dJointGetTransmissionRadius2(self.id); } + /// Set the backlash (free play) in the transmission mechanism. + pub fn set_backlash(self: Transmission, backlash: Real) void { c.dJointSetTransmissionBacklash(self.id, backlash); } + /// Get the backlash (free play) in the transmission mechanism. + pub fn get_backlash(self: Transmission) Real { return c.dJointGetTransmissionBacklash(self.id); } + /// Set a joint parameter. + pub fn set_param(self: Transmission, parameter: Param, value: Real) void { c.dJointSetTransmissionParam(self.id, @intFromEnum(parameter), value); } + /// Get a joint parameter. + pub fn get_param(self: Transmission, parameter: Param) Real { return c.dJointGetTransmissionParam(self.id, @intFromEnum(parameter)); } + /// Get the current rotation angle (radians) of body 1's wheel. + pub fn get_angle1(self: Transmission) Real { return c.dJointGetTransmissionAngle1(self.id); } + /// Get the current rotation angle (radians) of body 2's wheel. + pub fn get_angle2(self: Transmission) Real { return c.dJointGetTransmissionAngle2(self.id); } +}; diff --git a/src/Mass.zig b/src/Mass.zig new file mode 100644 index 0000000..e0bb5f5 --- /dev/null +++ b/src/Mass.zig @@ -0,0 +1,141 @@ +//! Mass distribution properties for a rigid body: total mass, center of gravity, and +//! 3x3 inertia tensor. Create from a shape (sphere, box, etc.) or set directly. +//! This is a value type — methods take `*Self` because ODE mutates the struct in place. + +const c = @import("c.zig").c; +const Real = c.dReal; +const Geom = @import("Geom.zig"); + +raw: c.dMass, + +const Self = @This(); + +/// Create a zeroed-out mass (total mass = 0, identity-like inertia). Useful as a starting +/// point before combining multiple masses with `add`. +pub fn zero() Self { + var m: c.dMass = undefined; + c.dMassSetZero(&m); + return .{ .raw = m }; +} + +/// Set all mass parameters explicitly: total mass, center of gravity offset, and the 6 +/// unique elements of the symmetric 3x3 inertia tensor (I11, I22, I33, I12, I13, I23). +pub fn set_parameters(self: *Self, themass: Real, cg: [3]Real, inertia_11: Real, inertia_22: Real, inertia_33: Real, inertia_12: Real, inertia_13: Real, inertia_23: Real) void { + c.dMassSetParameters(&self.raw, themass, cg[0], cg[1], cg[2], inertia_11, inertia_22, inertia_33, inertia_12, inertia_13, inertia_23); +} + +/// Mass of a solid sphere given uniform density and radius. +pub fn sphere(density: Real, radius: Real) Self { + var m: c.dMass = undefined; + c.dMassSetSphere(&m, density, radius); + return .{ .raw = m }; +} + +/// Mass of a solid sphere given total mass and radius (density is computed internally). +pub fn sphere_total(total_mass: Real, radius: Real) Self { + var m: c.dMass = undefined; + c.dMassSetSphereTotal(&m, total_mass, radius); + return .{ .raw = m }; +} + +/// Mass of a capsule (cylinder with hemispherical caps). `direction` is the long axis: +/// 1 = X, 2 = Y, 3 = Z. +pub fn capsule(density: Real, direction: c_int, radius: Real, length: Real) Self { + var m: c.dMass = undefined; + c.dMassSetCapsule(&m, density, direction, radius, length); + return .{ .raw = m }; +} + +/// Mass of a capsule given total mass instead of density. +pub fn capsule_total(total_mass: Real, direction: c_int, radius: Real, length: Real) Self { + var m: c.dMass = undefined; + c.dMassSetCapsuleTotal(&m, total_mass, direction, radius, length); + return .{ .raw = m }; +} + +/// Mass of a solid cylinder. `direction` is the long axis: 1 = X, 2 = Y, 3 = Z. +pub fn cylinder(density: Real, direction: c_int, radius: Real, length: Real) Self { + var m: c.dMass = undefined; + c.dMassSetCylinder(&m, density, direction, radius, length); + return .{ .raw = m }; +} + +/// Mass of a solid cylinder given total mass instead of density. +pub fn cylinder_total(total_mass: Real, direction: c_int, radius: Real, length: Real) Self { + var m: c.dMass = undefined; + c.dMassSetCylinderTotal(&m, total_mass, direction, radius, length); + return .{ .raw = m }; +} + +/// Mass of a solid box given uniform density and side lengths along each axis. +pub fn box(density: Real, lx: Real, ly: Real, lz: Real) Self { + var m: c.dMass = undefined; + c.dMassSetBox(&m, density, lx, ly, lz); + return .{ .raw = m }; +} + +/// Mass of a solid box given total mass instead of density. +pub fn box_total(total_mass: Real, lx: Real, ly: Real, lz: Real) Self { + var m: c.dMass = undefined; + c.dMassSetBoxTotal(&m, total_mass, lx, ly, lz); + return .{ .raw = m }; +} + +/// Compute mass from a triangle mesh geometry, assuming uniform density. +/// The mesh must have a valid TriMesh.Data attached. +pub fn trimesh(density: Real, geom: Geom.Generic) Self { + var m: c.dMass = undefined; + c.dMassSetTrimesh(&m, density, geom.id); + return .{ .raw = m }; +} + +/// Compute mass from a triangle mesh given total mass instead of density. +pub fn trimesh_total(total_mass: Real, geom: Geom.Generic) Self { + var m: c.dMass = undefined; + c.dMassSetTrimeshTotal(&m, total_mass, geom.id); + return .{ .raw = m }; +} + +/// Validate that the mass parameters are physically plausible (positive mass, +/// positive-definite inertia tensor). +pub fn check(self: *const Self) bool { + return c.dMassCheck(&self.raw) != 0; +} + +/// Scale the inertia tensor to match a new total mass value, preserving the shape of +/// the distribution. +pub fn adjust(self: *Self, newmass: Real) void { + c.dMassAdjust(&self.raw, newmass); +} + +/// Shift the center of gravity by the given offset. This also updates the inertia tensor +/// via the parallel axis theorem. +pub fn translate(self: *Self, t: [3]Real) void { + c.dMassTranslate(&self.raw, t[0], t[1], t[2]); +} + +/// Rotate the inertia tensor by a 3x3 rotation matrix. +pub fn rotate(self: *Self, r: *const [12]Real) void { + c.dMassRotate(&self.raw, r); +} + +/// Add another mass distribution to this one. Used to build composite bodies +/// from multiple primitive shapes. +pub fn add(self: *Self, other: *const Self) void { + c.dMassAdd(&self.raw, &other.raw); +} + +/// Read the scalar total mass. +pub fn get_mass(self: *const Self) Real { + return self.raw.mass; +} + +/// Read the center of gravity offset relative to the body's position. +pub fn get_center(self: *const Self) [3]Real { + return .{ self.raw.c[0], self.raw.c[1], self.raw.c[2] }; +} + +/// Read the 3x3 inertia tensor, stored as a 3x4 row-major matrix (12 elements, with padding). +pub fn get_inertia(self: *const Self) [12]Real { + return self.raw.I; +} diff --git a/src/Rotation.zig b/src/Rotation.zig new file mode 100644 index 0000000..c8a5db8 --- /dev/null +++ b/src/Rotation.zig @@ -0,0 +1,106 @@ +//! Utility functions for constructing 3x3 rotation matrices and quaternions from +//! axes, angles, Euler angles, and conversions between representations. + +const c = @import("c.zig").c; +const Real = c.dReal; + +/// Returns a 3x3 identity rotation matrix (no rotation). +pub fn matrix_set_identity() [12]Real { + var r: c.dMatrix3 = undefined; + c.dRSetIdentity(&r); + return r; +} + +/// Build a rotation matrix from an axis and angle (radians). The axis does not need to be normalized. +pub fn matrix_from_axis_and_angle(axis: [3]Real, angle: Real) [12]Real { + var r: c.dMatrix3 = undefined; + c.dRFromAxisAndAngle(&r, axis[0], axis[1], axis[2], angle); + return r; +} + +/// Build a rotation matrix from Euler angles (phi, theta, psi) in radians. +/// Rotation order is Z-X-Z (aerospace convention). +pub fn matrix_from_euler_angles(phi: Real, theta: Real, psi: Real) [12]Real { + var r: c.dMatrix3 = undefined; + c.dRFromEulerAngles(&r, phi, theta, psi); + return r; +} + +/// Build a rotation matrix from two axes. The first axis becomes the X direction, +/// the second is projected onto the YZ plane to determine Y and Z. +pub fn matrix_from_2_axes(a: [3]Real, b_vec: [3]Real) [12]Real { + var r: c.dMatrix3 = undefined; + c.dRFrom2Axes(&r, a[0], a[1], a[2], b_vec[0], b_vec[1], b_vec[2]); + return r; +} + +/// Build a rotation matrix that aligns the local Z axis with the given world-space direction. +pub fn matrix_from_z_axis(axis: [3]Real) [12]Real { + var r: c.dMatrix3 = undefined; + c.dRFromZAxis(&r, axis[0], axis[1], axis[2]); + return r; +} + +/// Returns the identity quaternion (no rotation): (w=1, x=0, y=0, z=0). +pub fn quaternion_set_identity() [4]Real { + var q: c.dQuaternion = undefined; + c.dQSetIdentity(&q); + return q; +} + +/// Build a quaternion from an axis and angle (radians). The axis does not need to be normalized. +pub fn quaternion_from_axis_and_angle(axis: [3]Real, angle: Real) [4]Real { + var q: c.dQuaternion = undefined; + c.dQFromAxisAndAngle(&q, axis[0], axis[1], axis[2], angle); + return q; +} + +/// Quaternion multiplication: result = b * q_c. Both operands are treated as non-inverted. +pub fn quaternion_multiply0(b: [4]Real, q_c: [4]Real) [4]Real { + var qa: c.dQuaternion = undefined; + c.dQMultiply0(&qa, &b, &q_c); + return qa; +} + +/// Quaternion multiplication: result = b_inverse * q_c (first operand is conjugated). +pub fn quaternion_multiply1(b: [4]Real, q_c: [4]Real) [4]Real { + var qa: c.dQuaternion = undefined; + c.dQMultiply1(&qa, &b, &q_c); + return qa; +} + +/// Quaternion multiplication: result = b * q_c_inverse (second operand is conjugated). +pub fn quaternion_multiply2(b: [4]Real, q_c: [4]Real) [4]Real { + var qa: c.dQuaternion = undefined; + c.dQMultiply2(&qa, &b, &q_c); + return qa; +} + +/// Quaternion multiplication: result = b_inverse * q_c_inverse (both operands are conjugated). +pub fn quaternion_multiply3(b: [4]Real, q_c: [4]Real) [4]Real { + var qa: c.dQuaternion = undefined; + c.dQMultiply3(&qa, &b, &q_c); + return qa; +} + +/// Convert a quaternion to a 3x3 rotation matrix. +pub fn matrix_from_quaternion(q: [4]Real) [12]Real { + var r: c.dMatrix3 = undefined; + c.dRfromQ(&r, &q); + return r; +} + +/// Convert a 3x3 rotation matrix to a quaternion. +pub fn quaternion_from_matrix(r: *const [12]Real) [4]Real { + var q: c.dQuaternion = undefined; + c.dQfromR(&q, r); + return q; +} + +/// Compute the quaternion time-derivative from an angular velocity vector and current orientation. +/// Useful for integrating angular velocity into quaternion form: q' = 0.5 * omega * q. +pub fn dq_from_w(w: [3]Real, q: [4]Real) [4]Real { + var dq: [4]Real = undefined; + c.dDQfromW(&dq, &.{ w[0], w[1], w[2], 0 }, &q); + return dq; +} diff --git a/src/Space.zig b/src/Space.zig new file mode 100644 index 0000000..e5e8b8f --- /dev/null +++ b/src/Space.zig @@ -0,0 +1,175 @@ +//! Collision spaces for broad-phase detection. A space contains geoms and quickly +//! determines which pairs are close enough to warrant narrow-phase testing. +//! Use `collide` to iterate over potentially-overlapping pairs. + +const c = @import("c.zig").c; +const Real = c.dReal; +const Geom = @import("Geom.zig"); +const collision = @import("collision.zig"); + +/// Type-erased space handle. Returned by `to_generic` on concrete space types, +/// and accepted by functions that operate on any kind of space. +pub const Generic = struct { + id: c.dSpaceID, + + pub fn destroy(self: Generic) void { c.dSpaceDestroy(self.id); } + /// If true, destroying the space also destroys all geoms inside it. + pub fn set_cleanup(self: Generic, mode: bool) void { c.dSpaceSetCleanup(self.id, @intFromBool(mode)); } + pub fn get_cleanup(self: Generic) bool { return c.dSpaceGetCleanup(self.id) != 0; } + /// Set the nesting depth for hierarchical spaces (used internally for optimization). + pub fn set_sublevel(self: Generic, sublevel: c_int) void { c.dSpaceSetSublevel(self.id, sublevel); } + pub fn get_sublevel(self: Generic) c_int { return c.dSpaceGetSublevel(self.id); } + /// When true, dirty geoms are not automatically re-indexed before collision; you must call `clean` manually. + pub fn set_manual_cleanup(self: Generic, mode: bool) void { c.dSpaceSetManualCleanup(self.id, @intFromBool(mode)); } + pub fn get_manual_cleanup(self: Generic) bool { return c.dSpaceGetManualCleanup(self.id) != 0; } + /// Insert a geom into this space. A geom can only be in one space at a time. + pub fn add(self: Generic, geom: Geom.Generic) void { c.dSpaceAdd(self.id, geom.id); } + /// Remove a geom from this space. + pub fn remove(self: Generic, geom: Geom.Generic) void { c.dSpaceRemove(self.id, geom.id); } + /// Check whether a geom is contained in this space. + pub fn query(self: Generic, geom: Geom.Generic) bool { return c.dSpaceQuery(self.id, geom.id) != 0; } + /// Re-index all dirty geoms. Only needed when `manual_cleanup` is enabled. + pub fn clean(self: Generic) void { c.dSpaceClean(self.id); } + pub fn get_num_geoms(self: Generic) c_int { return c.dSpaceGetNumGeoms(self.id); } + /// Access a geom by index (0-based). Order is not guaranteed to be stable. + pub fn get_geom(self: Generic, i: c_int) Geom.Generic { return .{ .id = c.dSpaceGetGeom(self.id, i) }; } + /// Returns the internal class identifier for this space type. + pub fn get_class(self: Generic) c_int { return c.dSpaceGetClass(self.id); } + + /// Run broad-phase collision: invokes `callback` for each pair of geoms whose AABBs overlap. + pub fn collide(self: Generic, data: ?*anyopaque, callback: collision.NearCallback) void { + c.dSpaceCollide(self.id, data, callback); + } + + /// Cast this space to a Geom handle. Spaces are themselves geoms in ODE, so they + /// can be nested inside other spaces. + pub fn to_geom(self: Generic) Geom.Generic { + return .{ .id = @ptrCast(self.id) }; + } +}; + +/// O(n^2) brute-force space. Tests every pair — only suitable for small numbers of geoms. +pub const Simple = struct { + id: c.dSpaceID, + + /// Create a simple space, optionally nested inside a parent space. + pub fn create(space: ?Generic) Simple { + return .{ .id = c.dSimpleSpaceCreate(if (space) |s| s.id else null) }; + } + + pub fn to_generic(self: Simple) Generic { return .{ .id = self.id }; } + pub fn destroy(self: Simple) void { c.dSpaceDestroy(self.id); } + pub fn set_cleanup(self: Simple, mode: bool) void { c.dSpaceSetCleanup(self.id, @intFromBool(mode)); } + pub fn get_cleanup(self: Simple) bool { return c.dSpaceGetCleanup(self.id) != 0; } + pub fn add(self: Simple, geom: Geom.Generic) void { c.dSpaceAdd(self.id, geom.id); } + pub fn remove(self: Simple, geom: Geom.Generic) void { c.dSpaceRemove(self.id, geom.id); } + pub fn get_num_geoms(self: Simple) c_int { return c.dSpaceGetNumGeoms(self.id); } + pub fn get_geom(self: Simple, i: c_int) Geom.Generic { return .{ .id = c.dSpaceGetGeom(self.id, i) }; } + + pub fn collide(self: Simple, data: ?*anyopaque, callback: collision.NearCallback) void { + c.dSpaceCollide(self.id, data, callback); + } +}; + +/// Grid-based hash space. Geoms are binned into cells at multiple resolutions. +/// Good general-purpose choice for scenes with objects of varying sizes. +pub const Hash = struct { + id: c.dSpaceID, + + /// Create a hash space, optionally nested inside a parent space. + pub fn create(space: ?Generic) Hash { + return .{ .id = c.dHashSpaceCreate(if (space) |s| s.id else null) }; + } + + pub fn to_generic(self: Hash) Generic { return .{ .id = self.id }; } + pub fn destroy(self: Hash) void { c.dSpaceDestroy(self.id); } + + /// Set the minimum and maximum cell-size levels (as powers of 2). For example, + /// levels (-3, 5) gives cell sizes from 2^-3 to 2^5. + pub fn set_levels(self: Hash, minlevel: c_int, maxlevel: c_int) void { + c.dHashSpaceSetLevels(self.id, minlevel, maxlevel); + } + + pub fn get_levels(self: Hash) struct { min: c_int, max: c_int } { + var min: c_int = undefined; + var max: c_int = undefined; + c.dHashSpaceGetLevels(self.id, &min, &max); + return .{ .min = min, .max = max }; + } + + pub fn set_cleanup(self: Hash, mode: bool) void { c.dSpaceSetCleanup(self.id, @intFromBool(mode)); } + pub fn get_cleanup(self: Hash) bool { return c.dSpaceGetCleanup(self.id) != 0; } + pub fn add(self: Hash, geom: Geom.Generic) void { c.dSpaceAdd(self.id, geom.id); } + pub fn remove(self: Hash, geom: Geom.Generic) void { c.dSpaceRemove(self.id, geom.id); } + pub fn get_num_geoms(self: Hash) c_int { return c.dSpaceGetNumGeoms(self.id); } + pub fn get_geom(self: Hash, i: c_int) Geom.Generic { return .{ .id = c.dSpaceGetGeom(self.id, i) }; } + + pub fn collide(self: Hash, data: ?*anyopaque, callback: collision.NearCallback) void { + c.dSpaceCollide(self.id, data, callback); + } +}; + +/// Quad-tree space for static scenes. Best when most geoms don't move, as reinsertion is expensive. +/// Requires a known bounding volume at creation. +pub const QuadTree = struct { + id: c.dSpaceID, + + /// Create a quadtree space with a fixed bounding volume. `center` and `extents` define + /// the root AABB, and `depth` controls the tree subdivision levels. + pub fn create(space: ?Generic, center: [3]Real, extents: [3]Real, depth: c_int) QuadTree { + return .{ .id = c.dQuadTreeSpaceCreate( + if (space) |s| s.id else null, + &.{ center[0], center[1], center[2], 0 }, + &.{ extents[0], extents[1], extents[2], 0 }, + depth, + ) }; + } + + pub fn to_generic(self: QuadTree) Generic { return .{ .id = self.id }; } + pub fn destroy(self: QuadTree) void { c.dSpaceDestroy(self.id); } + pub fn set_cleanup(self: QuadTree, mode: bool) void { c.dSpaceSetCleanup(self.id, @intFromBool(mode)); } + pub fn get_cleanup(self: QuadTree) bool { return c.dSpaceGetCleanup(self.id) != 0; } + pub fn add(self: QuadTree, geom: Geom.Generic) void { c.dSpaceAdd(self.id, geom.id); } + pub fn remove(self: QuadTree, geom: Geom.Generic) void { c.dSpaceRemove(self.id, geom.id); } + pub fn get_num_geoms(self: QuadTree) c_int { return c.dSpaceGetNumGeoms(self.id); } + pub fn get_geom(self: QuadTree, i: c_int) Geom.Generic { return .{ .id = c.dSpaceGetGeom(self.id, i) }; } + + pub fn collide(self: QuadTree, data: ?*anyopaque, callback: collision.NearCallback) void { + c.dSpaceCollide(self.id, data, callback); + } +}; + +/// Sweep-and-prune (SAP) space. Maintains sorted axis lists and is very efficient for +/// scenes where objects move incrementally between frames. Choose the axis order that +/// best distributes your objects. +pub const SweepAndPrune = struct { + id: c.dSpaceID, + + /// Primary sort axis ordering. Choose based on which axis has the most spread. + pub const AxisOrder = enum(c_int) { + xyz = 0, + xzy = 1, + yxz = 2, + yzx = 3, + zxy = 4, + zyx = 5, + }; + + /// Create a SAP space with the given sort axis order. + pub fn create(space: ?Generic, axis_order: AxisOrder) SweepAndPrune { + return .{ .id = c.dSweepAndPruneSpaceCreate(if (space) |s| s.id else null, @intFromEnum(axis_order)) }; + } + + pub fn to_generic(self: SweepAndPrune) Generic { return .{ .id = self.id }; } + pub fn destroy(self: SweepAndPrune) void { c.dSpaceDestroy(self.id); } + pub fn set_cleanup(self: SweepAndPrune, mode: bool) void { c.dSpaceSetCleanup(self.id, @intFromBool(mode)); } + pub fn get_cleanup(self: SweepAndPrune) bool { return c.dSpaceGetCleanup(self.id) != 0; } + pub fn add(self: SweepAndPrune, geom: Geom.Generic) void { c.dSpaceAdd(self.id, geom.id); } + pub fn remove(self: SweepAndPrune, geom: Geom.Generic) void { c.dSpaceRemove(self.id, geom.id); } + pub fn get_num_geoms(self: SweepAndPrune) c_int { return c.dSpaceGetNumGeoms(self.id); } + pub fn get_geom(self: SweepAndPrune, i: c_int) Geom.Generic { return .{ .id = c.dSpaceGetGeom(self.id, i) }; } + + pub fn collide(self: SweepAndPrune, data: ?*anyopaque, callback: collision.NearCallback) void { + c.dSpaceCollide(self.id, data, callback); + } +}; diff --git a/src/World.zig b/src/World.zig new file mode 100644 index 0000000..05ee90b --- /dev/null +++ b/src/World.zig @@ -0,0 +1,261 @@ +//! The simulation world: a container for bodies and joints with global parameters +//! like gravity, ERP/CFM, and stepping methods. Create one world, add bodies to it, +//! connect them with joints, and call `step` or `quick_step` each frame. + +const c = @import("c.zig").c; +const Real = c.dReal; +const Body = @import("Body.zig"); + +id: c.dWorldID, + +const Self = @This(); + +/// Allocate a new empty world with default parameters. +pub fn create() Self { + return .{ .id = c.dWorldCreate() }; +} + +/// Destroy the world and all bodies/joints it contains. +pub fn destroy(self: Self) void { + c.dWorldDestroy(self.id); +} + +/// Attach an arbitrary user pointer to this world (e.g. for your game-state back-reference). +pub fn set_data(self: Self, data: ?*anyopaque) void { + c.dWorldSetData(self.id, data); +} + +pub fn get_data(self: Self) ?*anyopaque { + return c.dWorldGetData(self.id); +} + +/// Set the global gravity vector applied to all bodies each step. +pub fn set_gravity(self: Self, g: [3]Real) void { + c.dWorldSetGravity(self.id, g[0], g[1], g[2]); +} + +pub fn get_gravity(self: Self) [3]Real { + var v: c.dVector3 = undefined; + c.dWorldGetGravity(self.id, &v); + return .{ v[0], v[1], v[2] }; +} + +/// Set the global Error Reduction Parameter (0..1). Controls how aggressively joint +/// errors are corrected each step. Higher values fix errors faster but can cause instability. +pub fn set_erp(self: Self, erp: Real) void { + c.dWorldSetERP(self.id, erp); +} + +pub fn get_erp(self: Self) Real { + return c.dWorldGetERP(self.id); +} + +/// Set the global Constraint Force Mixing value. Adds softness to constraints — +/// higher values make joints springy, lower values make them rigid. Must be >= 0. +pub fn set_cfm(self: Self, cfm: Real) void { + c.dWorldSetCFM(self.id, cfm); +} + +pub fn get_cfm(self: Self) Real { + return c.dWorldGetCFM(self.id); +} + +/// Advance the simulation by `stepsize` seconds using a direct (accurate but slow O(n^3)) solver. +/// Returns false on memory allocation failure. +pub fn step(self: Self, stepsize: Real) bool { + return c.dWorldStep(self.id, stepsize) != 0; +} + +/// Advance the simulation using the iterative QuickStep solver. Much faster than `step` +/// for large worlds, but less accurate. Accuracy improves with more iterations. +pub fn quick_step(self: Self, stepsize: Real) bool { + return c.dWorldQuickStep(self.id, stepsize) != 0; +} + +/// Set the number of SOR (Successive Over-Relaxation) iterations for QuickStep. +/// Default is 20. More iterations = more accurate but slower. +pub fn set_quick_step_num_iterations(self: Self, num: c_int) void { + c.dWorldSetQuickStepNumIterations(self.id, num); +} + +pub fn get_quick_step_num_iterations(self: Self) c_int { + return c.dWorldGetQuickStepNumIterations(self.id); +} + +/// Set the SOR over-relaxation parameter for QuickStep (default 1.3). Values in [1.0, 2.0) +/// can improve convergence; values outside that range may cause divergence. +pub fn set_quick_step_w(self: Self, over_relaxation: Real) void { + c.dWorldSetQuickStepW(self.id, over_relaxation); +} + +pub fn get_quick_step_w(self: Self) Real { + return c.dWorldGetQuickStepW(self.id); +} + +/// Convert an impulse (force * time) to a force suitable for the given step size. +/// Useful for applying impulses through the force-based API. +pub fn impulse_to_force(self: Self, stepsize: Real, impulse: [3]Real) [3]Real { + var force: c.dVector3 = undefined; + c.dWorldImpulseToForce(self.id, stepsize, impulse[0], impulse[1], impulse[2], &force); + return .{ force[0], force[1], force[2] }; +} + +/// Limit the velocity used to correct interpenetration. Prevents large pop-out forces +/// when objects start deeply overlapping. 0 = no limit (infinity). +pub fn set_contact_max_correcting_vel(self: Self, vel: Real) void { + c.dWorldSetContactMaxCorrectingVel(self.id, vel); +} + +pub fn get_contact_max_correcting_vel(self: Self) Real { + return c.dWorldGetContactMaxCorrectingVel(self.id); +} + +/// Set the depth of the surface layer around all geometry. Contacts within this depth +/// are not corrected, which helps prevent jittering for resting objects. +pub fn set_contact_surface_layer(self: Self, depth: Real) void { + c.dWorldSetContactSurfaceLayer(self.id, depth); +} + +pub fn get_contact_surface_layer(self: Self) Real { + return c.dWorldGetContactSurfaceLayer(self.id); +} + +/// Enable/disable automatic disabling of idle bodies for this world. +/// When enabled, bodies that stop moving are deactivated to save CPU. +pub fn set_auto_disable_flag(self: Self, do_auto_disable: bool) void { + c.dWorldSetAutoDisableFlag(self.id, @intFromBool(do_auto_disable)); +} + +pub fn get_auto_disable_flag(self: Self) bool { + return c.dWorldGetAutoDisableFlag(self.id) != 0; +} + +/// Bodies with linear velocity below this threshold (for the required number of steps) +/// are candidates for auto-disable. +pub fn set_auto_disable_linear_threshold(self: Self, threshold: Real) void { + c.dWorldSetAutoDisableLinearThreshold(self.id, threshold); +} + +pub fn get_auto_disable_linear_threshold(self: Self) Real { + return c.dWorldGetAutoDisableLinearThreshold(self.id); +} + +/// Bodies with angular velocity below this threshold are candidates for auto-disable. +pub fn set_auto_disable_angular_threshold(self: Self, threshold: Real) void { + c.dWorldSetAutoDisableAngularThreshold(self.id, threshold); +} + +pub fn get_auto_disable_angular_threshold(self: Self) Real { + return c.dWorldGetAutoDisableAngularThreshold(self.id); +} + +/// Number of consecutive steps a body must be below velocity thresholds before it is disabled. +pub fn set_auto_disable_steps(self: Self, steps: c_int) void { + c.dWorldSetAutoDisableSteps(self.id, steps); +} + +pub fn get_auto_disable_steps(self: Self) c_int { + return c.dWorldGetAutoDisableSteps(self.id); +} + +/// Minimum elapsed simulation time a body must be idle before it is disabled. +pub fn set_auto_disable_time(self: Self, time: Real) void { + c.dWorldSetAutoDisableTime(self.id, time); +} + +pub fn get_auto_disable_time(self: Self) Real { + return c.dWorldGetAutoDisableTime(self.id); +} + +/// Number of recent velocity samples to average when determining if a body should be disabled. +/// Higher values smooth out transients but delay disabling. +pub fn set_auto_disable_average_samples_count(self: Self, count: c_uint) void { + c.dWorldSetAutoDisableAverageSamplesCount(self.id, count); +} + +pub fn get_auto_disable_average_samples_count(self: Self) c_int { + return c.dWorldGetAutoDisableAverageSamplesCount(self.id); +} + +/// Velocity damping factor applied to all bodies' linear velocity each step. +/// 0.0 = no damping, 1.0 = full damping (body stops instantly). Small values like 0.01 are typical. +pub fn set_linear_damping(self: Self, scale: Real) void { + c.dWorldSetLinearDamping(self.id, scale); +} + +pub fn get_linear_damping(self: Self) Real { + return c.dWorldGetLinearDamping(self.id); +} + +/// Velocity damping factor applied to all bodies' angular velocity each step. +pub fn set_angular_damping(self: Self, scale: Real) void { + c.dWorldSetAngularDamping(self.id, scale); +} + +pub fn get_angular_damping(self: Self) Real { + return c.dWorldGetAngularDamping(self.id); +} + +/// Set both linear and angular damping in one call. +pub fn set_damping(self: Self, linear_scale: Real, angular_scale: Real) void { + c.dWorldSetDamping(self.id, linear_scale, angular_scale); +} + +/// Linear velocity magnitude below which damping is not applied. +/// Prevents damping from affecting very slow (nearly resting) motion. +pub fn set_linear_damping_threshold(self: Self, threshold: Real) void { + c.dWorldSetLinearDampingThreshold(self.id, threshold); +} + +pub fn get_linear_damping_threshold(self: Self) Real { + return c.dWorldGetLinearDampingThreshold(self.id); +} + +/// Angular velocity magnitude below which damping is not applied. +pub fn set_angular_damping_threshold(self: Self, threshold: Real) void { + c.dWorldSetAngularDampingThreshold(self.id, threshold); +} + +pub fn get_angular_damping_threshold(self: Self) Real { + return c.dWorldGetAngularDampingThreshold(self.id); +} + +/// Clamp all bodies' angular speed to this maximum. Prevents numerical instability +/// from extremely fast rotations. 0 = no limit. +pub fn set_max_angular_speed(self: Self, max_speed: Real) void { + c.dWorldSetMaxAngularSpeed(self.id, max_speed); +} + +pub fn get_max_angular_speed(self: Self) Real { + return c.dWorldGetMaxAngularSpeed(self.id); +} + +/// Limit how many threads can process constraint islands in parallel during a step. +pub fn set_step_islands_processing_max_thread_count(self: Self, count: c_uint) void { + c.dWorldSetStepIslandsProcessingMaxThreadCount(self.id, count); +} + +pub fn get_step_islands_processing_max_thread_count(self: Self) c_uint { + return c.dWorldGetStepIslandsProcessingMaxThreadCount(self.id); +} + +/// Share internal working memory with another world to reduce allocations when simulating +/// multiple worlds sequentially. Pass null to stop sharing. +pub fn use_shared_working_memory(self: Self, from_world: ?Self) bool { + return c.dWorldUseSharedWorkingMemory(self.id, if (from_world) |w| w.id else null) != 0; +} + +/// Free internal working memory. It will be reallocated automatically on the next step. +pub fn cleanup_working_memory(self: Self) void { + c.dWorldCleanupWorkingMemory(self.id); +} + +/// Assign a custom threading implementation for parallel constraint solving. +pub fn set_step_threading_implementation(self: Self, functions_info: ?*const c.dThreadingFunctionsInfo, threading_impl: c.dThreadingImplementationID) void { + c.dWorldSetStepThreadingImplementation(self.id, functions_info, threading_impl); +} + +/// Create a new rigid body in this world, initially at the origin with zero velocity. +pub fn create_body(self: Self) Body { + return .{ .id = c.dBodyCreate(self.id) }; +} diff --git a/src/c.zig b/src/c.zig new file mode 100644 index 0000000..87ee330 --- /dev/null +++ b/src/c.zig @@ -0,0 +1 @@ +pub const c = @cImport(@cInclude("ode/ode.h")); diff --git a/src/collision.zig b/src/collision.zig new file mode 100644 index 0000000..b1513fa --- /dev/null +++ b/src/collision.zig @@ -0,0 +1,89 @@ +//! Narrow-phase collision detection: tests pairs of geometries for intersection and +//! produces contact points. Also defines the contact data structures used to create +//! contact joints for the simulation step. + +const c = @import("c.zig").c; +const Real = c.dReal; +const Geom = @import("Geom.zig"); +const Space = @import("Space.zig"); + +/// Physical surface properties at a contact point (friction, bounce, softness, slip). +/// Passed inside a `Contact` to `Joint.Contact.create` to control the collision response. +pub const SurfaceParameters = c.dSurfaceParameters; + +/// Geometric description of a single contact point: position, normal, penetration depth, +/// and which two geoms produced it. +pub const ContactGeom = c.dContactGeom; + +/// Complete contact description combining surface parameters, geometry, and friction direction. +/// Pass to `Joint.Contact.create` to generate a contact constraint for the solver. +pub const Contact = c.dContact; + +/// Force/torque feedback from a joint. Assign to a joint with `set_feedback` to record +/// the constraint forces applied each step (useful for breakable joints or diagnostics). +pub const JointFeedback = c.dJointFeedback; + +/// Callback signature for `space_collide` and `space_collide2`. Called for each potentially +/// overlapping pair of geoms; you then call `collide` inside to get actual contacts. +pub const NearCallback = *const fn (data: ?*anyopaque, o1: c.dGeomID, o2: c.dGeomID) callconv(.c) void; + +/// Test two geoms for intersection and fill the contacts slice with contact points. +/// Returns a sub-slice of the input with only the generated contacts (may be empty). +pub fn collide(o1: Geom.Generic, o2: Geom.Generic, contacts: []ContactGeom) []ContactGeom { + const n = c.dCollide( + o1.id, + o2.id, + @intCast(contacts.len), + if (contacts.len > 0) &contacts[0] else null, + @sizeOf(ContactGeom), + ); + return if (n > 0) contacts[0..@intCast(n)] else contacts[0..0]; +} + +/// Broad-phase: iterate over all potentially-overlapping geom pairs within a single space +/// and invoke `callback` for each pair. You typically call `collide` inside the callback. +pub fn space_collide(space: Space.Generic, data: ?*anyopaque, callback: NearCallback) void { + c.dSpaceCollide(space.id, data, callback); +} + +/// Test all geoms in one object against all geoms in another. Either argument can be a +/// space (tests all contained geoms) or a single geom. Useful for testing two separate spaces. +pub fn space_collide2(o1: Geom.Generic, o2: Geom.Generic, data: ?*anyopaque, callback: NearCallback) void { + c.dSpaceCollide2(o1.id, o2.id, data, callback); +} + +// Contact surface mode flags — combine with bitwise OR in SurfaceParameters.mode. +/// Use `mu2` for the second friction direction (otherwise `mu` is used for both). +pub const contact_mu2 = c.dContactMu2; +/// Friction coefficients are axis-dependent (requires `contact_fdir1`). +pub const contact_axis_dep = c.dContactAxisDep; +/// `fdir1` field in Contact specifies the first friction direction (otherwise auto-computed). +pub const contact_fdir1 = c.dContactFDir1; +/// Enable restitution; set `bounce` and `bounce_vel` in SurfaceParameters. +pub const contact_bounce = c.dContactBounce; +/// Use soft constraint ERP for this contact (set `soft_erp` in SurfaceParameters). +pub const contact_soft_erp = c.dContactSoftERP; +/// Use soft constraint CFM for this contact (set `soft_cfm` in SurfaceParameters). +pub const contact_soft_cfm = c.dContactSoftCFM; +/// Surface velocity in friction direction 1 (conveyor belt effect). +pub const contact_motion1 = c.dContactMotion1; +/// Surface velocity in friction direction 2. +pub const contact_motion2 = c.dContactMotion2; +/// Surface velocity in the contact normal direction. +pub const contact_motion_n = c.dContactMotionN; +/// Force-dependent slip in friction direction 1 (like tire slip). +pub const contact_slip1 = c.dContactSlip1; +/// Force-dependent slip in friction direction 2. +pub const contact_slip2 = c.dContactSlip2; +/// Enable rolling friction (set `rho`, `rho2`, `rhoN` in SurfaceParameters). +pub const contact_rolling = c.dContactRolling; +/// Use exact friction model (not a friction pyramid approximation). +pub const contact_approx0 = c.dContactApprox0; +/// Friction pyramid approximation in direction 1. +pub const contact_approx1_1 = c.dContactApprox1_1; +/// Friction pyramid approximation in direction 2. +pub const contact_approx1_2 = c.dContactApprox1_2; +/// Friction pyramid approximation in the normal direction. +pub const contact_approx1_n = c.dContactApprox1_N; +/// Friction pyramid approximation in all directions (combines approx1_1, approx1_2, approx1_n). +pub const contact_approx1 = c.dContactApprox1; diff --git a/src/init.zig b/src/init.zig new file mode 100644 index 0000000..313d9cd --- /dev/null +++ b/src/init.zig @@ -0,0 +1,55 @@ +//! ODE library initialization and shutdown. Must be called before and after all other ODE usage. + +const c = @import("c.zig").c; + +/// Flags for `init_ode2`. +pub const InitFlags = packed struct(c_uint) { + /// When set, ODE will not automatically clean up thread-local data on thread exit. + /// You must call `cleanup_all_data_for_thread` manually before each thread terminates. + manual_thread_cleanup: bool = false, + _padding: @Type(.{ .int = .{ .signedness = .unsigned, .bits = @bitSizeOf(c_uint) - 1 } }) = 0, +}; + +/// Flags for `allocate_data_for_thread`. +pub const AllocateDataFlags = packed struct(c_uint) { + /// Allocate thread-local collision detection caches. Required for any thread that calls collision functions. + collision_data: bool = false, + _padding: @Type(.{ .int = .{ .signedness = .unsigned, .bits = @bitSizeOf(c_uint) - 1 } }) = 0, +}; + +/// Simple initialization with default settings. Prefer `init_ode2` for multithreaded use. +pub fn init_ode() void { + c.dInitODE(); +} + +/// Initialize the library with explicit flags. Must be called before any other ODE function. +/// Returns false if initialization fails. +pub fn init_ode2(flags: InitFlags) bool { + return c.dInitODE2(@bitCast(flags)) != 0; +} + +/// Allocate thread-local data for the calling thread. Each thread that uses ODE +/// (especially collision) must call this after `init_ode2`. Returns false on failure. +pub fn allocate_data_for_thread(flags: AllocateDataFlags) bool { + return c.dAllocateODEDataForThread(@bitCast(flags)) != 0; +} + +/// Free thread-local data for the calling thread. Only needed when `InitFlags.manual_thread_cleanup` was set. +pub fn cleanup_all_data_for_thread() void { + c.dCleanupODEAllDataForThread(); +} + +/// Shut down the library and release all global resources. No ODE calls are valid after this. +pub fn close_ode() void { + c.dCloseODE(); +} + +/// Returns a string describing the build configuration (precision, trimesh backend, etc.). +pub fn get_configuration() [*:0]const u8 { + return c.dGetConfiguration(); +} + +/// Check whether a specific feature token (e.g. "ODE_double_precision") is present in the build. +pub fn check_configuration(token: [*:0]const u8) bool { + return c.dCheckConfiguration(token) != 0; +} diff --git a/src/ode-double.zig b/src/ode-double.zig deleted file mode 100644 index 249ea3e..0000000 --- a/src/ode-double.zig +++ /dev/null @@ -1,3 +0,0 @@ -//! Backing package for ODE (single precision). -//! Is imported by ode.zig via a package name -pub const real = f64; diff --git a/src/ode-single.zig b/src/ode-single.zig deleted file mode 100644 index fbcbe90..0000000 --- a/src/ode-single.zig +++ /dev/null @@ -1,3 +0,0 @@ -//! Backing package for ODE (single precision). -//! Is imported by ode.zig via a package name -pub const real = f32; diff --git a/src/ode.zig b/src/ode.zig index 02337d0..15a5c00 100644 --- a/src/ode.zig +++ b/src/ode.zig @@ -1,5 +1,223 @@ -const std = @import("std"); +//! Idiomatic Zig bindings for the Open Dynamics Engine (ODE), a rigid body physics library. +//! +//! Call `init.init_ode2(.{})` before using any other functions, and `init.close_ode()` when done. +//! For multithreaded use, each thread must also call `init.allocate_data_for_thread(.{ .collision_data = true })`. -pub const real = @import("precision").real; +pub const c = @import("c.zig").c; -pub usingnamespace @import("native"); +/// Floating-point type used throughout ODE. Either f32 or f64 depending on build-time precision setting. +pub const Real = c.dReal; +/// 3-component vector (x, y, z). +pub const Vector3 = [3]Real; +/// 4-component vector (x, y, z, w). +pub const Vector4 = [4]Real; +/// 3x4 row-major rotation matrix. Each of the 3 rows has a 4th padding element (12 floats total). +pub const Matrix3 = [12]Real; +/// 4x4 row-major matrix. +pub const Matrix4 = [16]Real; +/// Quaternion stored as (w, x, y, z). +pub const Quaternion = [4]Real; + +/// Library initialization and shutdown. +pub const init = @import("init.zig"); +/// Simulation world containing global parameters (gravity, ERP, CFM) and stepping functions. +pub const World = @import("World.zig"); +/// Rigid body with position, velocity, mass, and force accumulators. +pub const Body = @import("Body.zig"); +/// Mass distribution (total mass, center of gravity, inertia tensor) assignable to a Body. +pub const Mass = @import("Mass.zig"); +/// Constraints between bodies: ball-and-socket, hinges, sliders, motors, and more. +pub const Joint = @import("Joint.zig"); +/// Spatial indexing structures that accelerate broad-phase collision detection. +pub const Space = @import("Space.zig"); +/// Collision geometry (shapes) that can be attached to bodies or placed as static environment. +pub const Geom = @import("Geom.zig"); +/// Terrain geometry defined by a grid of height samples. +pub const Heightfield = @import("Heightfield.zig"); +/// Utility functions for building rotation matrices and quaternions from axes, angles, and Euler angles. +pub const Rotation = @import("Rotation.zig"); +/// Narrow-phase collision testing and contact data structures. +pub const collision = @import("collision.zig"); + +test { + @import("std").testing.refAllDecls(@This()); +} + +test "smoke: init, world, body, step" { + const std = @import("std"); + + // Init ODE + std.debug.assert(init.init_ode2(.{})); + defer init.close_ode(); + std.debug.assert(init.allocate_data_for_thread(.{ .collision_data = true })); + + // Create world + const world = World.create(); + defer world.destroy(); + world.set_gravity(.{ 0, -9.81, 0 }); + + const gravity = world.get_gravity(); + try std.testing.expectApproxEqAbs(@as(Real, 0), gravity[0], 1e-5); + try std.testing.expectApproxEqAbs(@as(Real, -9.81), gravity[1], 1e-5); + try std.testing.expectApproxEqAbs(@as(Real, 0), gravity[2], 1e-5); + + // Create body + const body = world.create_body(); + defer body.destroy(); + + var mass = Mass.sphere(1.0, 0.5); + body.set_mass(&mass); + body.set_position(.{ 0, 10, 0 }); + + const pos_before = body.get_position(); + try std.testing.expectApproxEqAbs(@as(Real, 10), pos_before[1], 1e-5); + + // Step simulation + _ = world.step(0.01); + + // Verify position changed (body should fall due to gravity) + const pos_after = body.get_position(); + try std.testing.expect(pos_after[1] < pos_before[1]); +} + +test "collision: sphere-plane contacts" { + const std = @import("std"); + std.debug.assert(init.init_ode2(.{})); + defer init.close_ode(); + std.debug.assert(init.allocate_data_for_thread(.{ .collision_data = true })); + + // Ground plane at z=0 with normal pointing up + const plane = Geom.Plane.create(null, 0, 0, 1, 0); + defer plane.destroy(); + + // Sphere touching the ground (center at z=0.4, radius 0.5 => penetration 0.1) + const sphere = Geom.Sphere.create(null, 0.5); + defer sphere.destroy(); + sphere.set_position(.{ 0, 0, 0.4 }); + + var contact_geoms: [4]collision.ContactGeom = undefined; + const contacts = collision.collide(sphere.to_generic(), plane.to_generic(), &contact_geoms); + + try std.testing.expect(contacts.len > 0); + // Contact normal should point up (positive z) + try std.testing.expect(contacts[0].normal[2] > 0.5); +} + +test "joint: hinge constrains rotation" { + const std = @import("std"); + std.debug.assert(init.init_ode2(.{})); + defer init.close_ode(); + + const world = World.create(); + defer world.destroy(); + world.set_gravity(.{ 0, 0, -9.81 }); + + // Single body attached to the static environment via a hinge + // so it swings like a pendulum under gravity. + const body = world.create_body(); + defer body.destroy(); + body.set_position(.{ 1, 0, 0 }); + var m = Mass.box(1.0, 0.2, 0.2, 0.2); + body.set_mass(&m); + + const hinge = Joint.Hinge.create(world, null); + defer hinge.destroy(); + hinge.attach(body, null); + hinge.set_anchor(.{ 0, 0, 0 }); + hinge.set_axis(.{ 0, 1, 0 }); + + const angle_before = hinge.get_angle(); + + // Step a few times + for (0..10) |_| { + _ = world.step(0.05); + } + + const angle_after = hinge.get_angle(); + // Hinge angle should change as body swings down + try std.testing.expect(@abs(angle_after - angle_before) > 0.01); +} + +test "space: add/remove/query geoms" { + const std = @import("std"); + std.debug.assert(init.init_ode2(.{})); + defer init.close_ode(); + std.debug.assert(init.allocate_data_for_thread(.{ .collision_data = true })); + + const space = Space.Hash.create(null); + defer space.destroy(); + + const s1 = Geom.Sphere.create(null, 0.5); + defer s1.destroy(); + const s2 = Geom.Sphere.create(null, 0.5); + defer s2.destroy(); + + const gs = space.to_generic(); + + gs.add(s1.to_generic()); + gs.add(s2.to_generic()); + try std.testing.expectEqual(@as(c_int, 2), gs.get_num_geoms()); + try std.testing.expect(gs.query(s1.to_generic())); + try std.testing.expect(gs.query(s2.to_generic())); + + gs.remove(s1.to_generic()); + try std.testing.expectEqual(@as(c_int, 1), gs.get_num_geoms()); + try std.testing.expect(!gs.query(s1.to_generic())); + try std.testing.expect(gs.query(s2.to_generic())); + + gs.remove(s2.to_generic()); + try std.testing.expectEqual(@as(c_int, 0), gs.get_num_geoms()); +} + +test "mass: sphere properties" { + const std = @import("std"); + std.debug.assert(init.init_ode2(.{})); + defer init.close_ode(); + + var m = Mass.sphere(1.0, 0.5); + try std.testing.expect(m.check()); + const original_mass = m.get_mass(); + try std.testing.expect(original_mass > 0); + + m.adjust(5.0); + try std.testing.expectApproxEqAbs(@as(Real, 5.0), m.get_mass(), 1e-5); + try std.testing.expect(m.check()); + + // Center of gravity should be at origin for a uniform sphere + const center = m.get_center(); + try std.testing.expectApproxEqAbs(@as(Real, 0), center[0], 1e-5); + try std.testing.expectApproxEqAbs(@as(Real, 0), center[1], 1e-5); + try std.testing.expectApproxEqAbs(@as(Real, 0), center[2], 1e-5); +} + +test "body: force accumulation" { + const std = @import("std"); + std.debug.assert(init.init_ode2(.{})); + defer init.close_ode(); + + const world = World.create(); + defer world.destroy(); + world.set_gravity(.{ 0, 0, 0 }); // no gravity for this test + + const body = world.create_body(); + defer body.destroy(); + body.set_position(.{ 0, 0, 0 }); + var m = Mass.sphere(1.0, 0.5); + m.adjust(1.0); + body.set_mass(&m); + + // Add force and check accumulator + body.add_force(.{ 10, 0, 0 }); + const f = body.get_force(); + try std.testing.expectApproxEqAbs(@as(Real, 10), f[0], 1e-5); + try std.testing.expectApproxEqAbs(@as(Real, 0), f[1], 1e-5); + + // Step and verify position changed due to force + _ = world.step(0.1); + const pos = body.get_position(); + try std.testing.expect(pos[0] > 0); + + // Force should be cleared after step + const f_after = body.get_force(); + try std.testing.expectApproxEqAbs(@as(Real, 0), f_after[0], 1e-5); +} diff --git a/vendor/ode b/vendor/ode deleted file mode 160000 index 52c5632..0000000 --- a/vendor/ode +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 52c5632958de8471a89918a293a722e0234c430b