refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
45
packages/physics/physics-rapier2d/module.json
Normal file
45
packages/physics/physics-rapier2d/module.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"id": "physics-rapier2d",
|
||||
"name": "@esengine/physics-rapier2d",
|
||||
"globalKey": "physicsRapier2d",
|
||||
"displayName": "Physics 2D (Rapier)",
|
||||
"description": "2D physics using Rapier engine | 使用 Rapier 引擎的 2D 物理",
|
||||
"version": "1.0.0",
|
||||
"category": "Physics",
|
||||
"icon": "Atom",
|
||||
"tags": [
|
||||
"physics",
|
||||
"2d",
|
||||
"rapier",
|
||||
"collision"
|
||||
],
|
||||
"isCore": false,
|
||||
"defaultEnabled": false,
|
||||
"isEngineModule": true,
|
||||
"canContainContent": false,
|
||||
"platforms": [
|
||||
"web",
|
||||
"desktop"
|
||||
],
|
||||
"dependencies": [
|
||||
"core",
|
||||
"math"
|
||||
],
|
||||
"externalDependencies": [
|
||||
"@esengine/rapier2d"
|
||||
],
|
||||
"exports": {
|
||||
"components": [
|
||||
"RigidBody2D",
|
||||
"Collider2D",
|
||||
"BoxCollider2D",
|
||||
"CircleCollider2D"
|
||||
],
|
||||
"systems": [
|
||||
"PhysicsSystem2D"
|
||||
]
|
||||
},
|
||||
"outputPath": "dist/index.js",
|
||||
"pluginExport": "PhysicsPlugin",
|
||||
"includes": ["chunk-*.js"]
|
||||
}
|
||||
65
packages/physics/physics-rapier2d/package.json
Normal file
65
packages/physics/physics-rapier2d/package.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "@esengine/physics-rapier2d",
|
||||
"version": "1.0.0",
|
||||
"description": "Deterministic 2D physics engine based on Rapier2D with enhanced-determinism",
|
||||
"esengine": {
|
||||
"plugin": true,
|
||||
"pluginExport": "PhysicsPlugin",
|
||||
"category": "physics"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./runtime": {
|
||||
"types": "./dist/runtime.d.ts",
|
||||
"import": "./dist/runtime.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"physics",
|
||||
"rapier2d",
|
||||
"deterministic",
|
||||
"game-physics",
|
||||
"2d-physics"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@esengine/rapier2d": "workspace:*",
|
||||
"@esengine/platform-common": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/ecs-framework-math": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/esengine.git",
|
||||
"directory": "packages/physics-rapier2d"
|
||||
}
|
||||
}
|
||||
22
packages/physics/physics-rapier2d/plugin.json
Normal file
22
packages/physics/physics-rapier2d/plugin.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "@esengine/physics-rapier2d",
|
||||
"name": "Rapier 2D Physics",
|
||||
"version": "1.0.0",
|
||||
"description": "Deterministic 2D physics engine based on Rapier2D with enhanced-determinism support",
|
||||
"category": "physics",
|
||||
"loadingPhase": "default",
|
||||
"enabledByDefault": true,
|
||||
"canContainContent": false,
|
||||
"isEnginePlugin": true,
|
||||
"modules": [
|
||||
{
|
||||
"name": "PhysicsRuntime",
|
||||
"type": "runtime",
|
||||
"entry": "./src/runtime.ts"
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
"@esengine/ecs-framework",
|
||||
"@esengine/ecs-components"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Physics 2D Components Module (Lightweight)
|
||||
* 2D 物理组件模块(轻量级)
|
||||
*
|
||||
* 仅注册组件,不包含 WASM 依赖
|
||||
* 用于编辑器中的组件序列化/反序列化
|
||||
*/
|
||||
|
||||
import type { IComponentRegistry } from '@esengine/ecs-framework';
|
||||
import type { IRuntimeModule } from '@esengine/engine-core';
|
||||
|
||||
// Components (no WASM dependency)
|
||||
import { Rigidbody2DComponent } from './components/Rigidbody2DComponent';
|
||||
import { BoxCollider2DComponent } from './components/BoxCollider2DComponent';
|
||||
import { CircleCollider2DComponent } from './components/CircleCollider2DComponent';
|
||||
import { CapsuleCollider2DComponent } from './components/CapsuleCollider2DComponent';
|
||||
import { PolygonCollider2DComponent } from './components/PolygonCollider2DComponent';
|
||||
|
||||
/**
|
||||
* Physics 2D Components Module (Lightweight)
|
||||
* 2D 物理组件模块(轻量级)
|
||||
*
|
||||
* 仅实现组件注册,不包含系统创建和 WASM 初始化
|
||||
* 用于编辑器场景序列化
|
||||
*/
|
||||
export class Physics2DComponentsModule implements IRuntimeModule {
|
||||
/**
|
||||
* 注册组件到 ComponentRegistry
|
||||
* Register components to ComponentRegistry
|
||||
*/
|
||||
registerComponents(registry: IComponentRegistry): void {
|
||||
registry.register(Rigidbody2DComponent);
|
||||
registry.register(BoxCollider2DComponent);
|
||||
registry.register(CircleCollider2DComponent);
|
||||
registry.register(CapsuleCollider2DComponent);
|
||||
registry.register(PolygonCollider2DComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 不创建系统(完整运行时模块负责)
|
||||
*/
|
||||
createSystems(): void {
|
||||
// No-op: Systems are created by the full runtime module
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认导出模块实例
|
||||
*/
|
||||
export const physics2DComponentsModule = new Physics2DComponentsModule();
|
||||
export default physics2DComponentsModule;
|
||||
47
packages/physics/physics-rapier2d/src/PhysicsEditorPlugin.ts
Normal file
47
packages/physics/physics-rapier2d/src/PhysicsEditorPlugin.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Physics Editor Plugin
|
||||
*
|
||||
* 编辑器版本的物理插件,不包含 WASM 依赖。
|
||||
* Editor version of physics plugin, without WASM dependencies.
|
||||
*
|
||||
* 使用轻量级 Physics2DComponentsModule 注册组件,
|
||||
* 使场景中的物理组件可以正确序列化/反序列化。
|
||||
* Uses lightweight Physics2DComponentsModule to register components,
|
||||
* enabling proper serialization/deserialization of physics components in scenes.
|
||||
*/
|
||||
|
||||
import type { IRuntimePlugin, ModuleManifest } from '@esengine/engine-core';
|
||||
import { Physics2DComponentsModule } from './Physics2DComponentsModule';
|
||||
|
||||
const manifest: ModuleManifest = {
|
||||
id: '@esengine/physics-rapier2d',
|
||||
name: '@esengine/physics-rapier2d',
|
||||
displayName: 'Physics 2D',
|
||||
version: '1.0.0',
|
||||
description: 'Deterministic 2D physics with Rapier2D',
|
||||
category: 'Physics',
|
||||
isCore: false,
|
||||
defaultEnabled: false,
|
||||
isEngineModule: true,
|
||||
canContainContent: false,
|
||||
requiresWasm: true,
|
||||
dependencies: ['engine-core'],
|
||||
exports: {
|
||||
components: ['Rigidbody2DComponent', 'BoxCollider2DComponent', 'CircleCollider2DComponent'],
|
||||
systems: ['PhysicsSystem']
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 编辑器物理插件(轻量级运行时模块)
|
||||
* Editor physics plugin (lightweight runtime module)
|
||||
*
|
||||
* 使用 Physics2DComponentsModule 注册组件,用于场景反序列化。
|
||||
* 不包含 WASM 依赖,不创建物理系统。
|
||||
* Uses Physics2DComponentsModule for component registration (scene deserialization).
|
||||
* No WASM dependency, no physics system creation.
|
||||
*/
|
||||
export const Physics2DPlugin: IRuntimePlugin = {
|
||||
manifest,
|
||||
runtimeModule: new Physics2DComponentsModule()
|
||||
};
|
||||
207
packages/physics/physics-rapier2d/src/PhysicsRuntimeModule.ts
Normal file
207
packages/physics/physics-rapier2d/src/PhysicsRuntimeModule.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* 物理运行时模块
|
||||
*
|
||||
* 提供 Rapier2D 物理引擎的 ECS 集成
|
||||
*/
|
||||
|
||||
import type { IScene, ServiceContainer, IComponentRegistry } from '@esengine/ecs-framework';
|
||||
import type { IRuntimeModule, IRuntimePlugin, ModuleManifest, SystemContext } from '@esengine/engine-core';
|
||||
import { WasmLibraryLoaderFactory } from '@esengine/platform-common';
|
||||
import type * as RAPIER from '@esengine/rapier2d';
|
||||
|
||||
import { Rigidbody2DComponent } from './components/Rigidbody2DComponent';
|
||||
import { BoxCollider2DComponent } from './components/BoxCollider2DComponent';
|
||||
import { CircleCollider2DComponent } from './components/CircleCollider2DComponent';
|
||||
import { CapsuleCollider2DComponent } from './components/CapsuleCollider2DComponent';
|
||||
import { PolygonCollider2DComponent } from './components/PolygonCollider2DComponent';
|
||||
import { Physics2DSystem } from './systems/Physics2DSystem';
|
||||
import { Physics2DService } from './services/Physics2DService';
|
||||
import {
|
||||
Physics2DQueryToken,
|
||||
Physics2DSystemToken,
|
||||
Physics2DWorldToken,
|
||||
PhysicsConfigToken,
|
||||
CollisionLayerConfigToken,
|
||||
type IPhysics2DQuery,
|
||||
type PhysicsConfig
|
||||
} from './tokens';
|
||||
import { CollisionLayerConfig } from './services/CollisionLayerConfig';
|
||||
|
||||
// 注册 Rapier2D 加载器
|
||||
import './loaders';
|
||||
|
||||
// 重新导出 tokens 和类型 | Re-export tokens and types
|
||||
export {
|
||||
Physics2DQueryToken,
|
||||
Physics2DSystemToken,
|
||||
Physics2DWorldToken,
|
||||
PhysicsConfigToken,
|
||||
CollisionLayerConfigToken,
|
||||
type IPhysics2DQuery,
|
||||
type PhysicsConfig
|
||||
} from './tokens';
|
||||
|
||||
/**
|
||||
* 物理运行时模块
|
||||
*
|
||||
* 负责:
|
||||
* 1. 加载并初始化 Rapier2D WASM 模块(跨平台)
|
||||
* 2. 注册物理组件
|
||||
* 3. 注册物理服务
|
||||
* 4. 创建物理系统
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 作为插件使用
|
||||
* runtimePluginManager.register(PhysicsPlugin);
|
||||
* runtimePluginManager.enable('@esengine/physics-rapier2d');
|
||||
*
|
||||
* // 插件会自动:
|
||||
* // 1. 检测平台并选择合适的加载器
|
||||
* // 2. 安装必要的 polyfills(如微信小游戏的 TextDecoder)
|
||||
* // 3. 加载 Rapier2D WASM 模块
|
||||
* // 4. 注册物理组件和系统
|
||||
* ```
|
||||
*/
|
||||
class PhysicsRuntimeModule implements IRuntimeModule {
|
||||
private _rapierModule: typeof RAPIER | null = null;
|
||||
private _physicsSystem: Physics2DSystem | null = null;
|
||||
|
||||
/**
|
||||
* 初始化物理模块
|
||||
*
|
||||
* 使用平台适配的加载器加载 Rapier2D
|
||||
*/
|
||||
async onInitialize(): Promise<void> {
|
||||
// 使用工厂创建平台对应的加载器
|
||||
const loader = WasmLibraryLoaderFactory.createLoader<typeof RAPIER>('rapier2d');
|
||||
|
||||
// 获取平台信息
|
||||
const platformInfo = loader.getPlatformInfo();
|
||||
console.log(`[Physics] 平台: ${platformInfo.type}`);
|
||||
console.log(`[Physics] WASM 支持: ${platformInfo.supportsWasm}`);
|
||||
|
||||
if (platformInfo.needsPolyfills.length > 0) {
|
||||
console.log(`[Physics] 需要 Polyfills: ${platformInfo.needsPolyfills.join(', ')}`);
|
||||
}
|
||||
|
||||
// 检查平台支持
|
||||
if (!loader.isSupported()) {
|
||||
throw new Error(
|
||||
`[Physics] 当前平台不支持 Rapier2D: ${platformInfo.type}。` +
|
||||
'请检查 WebAssembly 支持情况。'
|
||||
);
|
||||
}
|
||||
|
||||
// 加载 Rapier2D
|
||||
this._rapierModule = await loader.load();
|
||||
console.log('[Physics] Rapier2D 加载完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册物理组件
|
||||
* Register physics components
|
||||
*
|
||||
* @param registry - 组件注册表 | Component registry
|
||||
*/
|
||||
registerComponents(registry: IComponentRegistry): void {
|
||||
registry.register(Rigidbody2DComponent);
|
||||
registry.register(BoxCollider2DComponent);
|
||||
registry.register(CircleCollider2DComponent);
|
||||
registry.register(CapsuleCollider2DComponent);
|
||||
registry.register(PolygonCollider2DComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册物理服务
|
||||
*
|
||||
* @param services - 服务容器
|
||||
*/
|
||||
registerServices(services: ServiceContainer): void {
|
||||
services.registerSingleton(Physics2DService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建物理系统
|
||||
*
|
||||
* @param scene - 目标场景
|
||||
* @param context - 系统上下文
|
||||
*/
|
||||
createSystems(scene: IScene, context: SystemContext): void {
|
||||
// 从服务注册表获取配置 | Get config from service registry
|
||||
const physicsConfig = context.services.get(PhysicsConfigToken);
|
||||
|
||||
const physicsSystem = new Physics2DSystem({
|
||||
physics: physicsConfig,
|
||||
updateOrder: -1000
|
||||
});
|
||||
|
||||
scene.addSystem(physicsSystem);
|
||||
this._physicsSystem = physicsSystem;
|
||||
|
||||
if (this._rapierModule) {
|
||||
physicsSystem.initializeWithRapier(this._rapierModule);
|
||||
}
|
||||
|
||||
// 注册服务 | Register services
|
||||
context.services.register(Physics2DSystemToken, physicsSystem);
|
||||
context.services.register(Physics2DWorldToken, physicsSystem.world);
|
||||
context.services.register(Physics2DQueryToken, physicsSystem);
|
||||
context.services.register(CollisionLayerConfigToken, CollisionLayerConfig.getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁物理模块
|
||||
*/
|
||||
onDestroy(): void {
|
||||
this._physicsSystem = null;
|
||||
this._rapierModule = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Rapier 模块
|
||||
*
|
||||
* @returns Rapier 模块,如果未加载则返回 null
|
||||
*/
|
||||
getRapierModule(): typeof RAPIER | null {
|
||||
return this._rapierModule;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取物理系统
|
||||
*
|
||||
* @returns 物理系统,如果未创建则返回 null
|
||||
*/
|
||||
getPhysicsSystem(): Physics2DSystem | null {
|
||||
return this._physicsSystem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模块清单
|
||||
*/
|
||||
const manifest: ModuleManifest = {
|
||||
id: 'physics-rapier2d',
|
||||
name: '@esengine/physics-rapier2d',
|
||||
displayName: 'Physics 2D (Rapier)',
|
||||
version: '1.0.0',
|
||||
description: '基于 Rapier2D 的确定性 2D 物理引擎(支持跨平台)',
|
||||
category: 'Physics',
|
||||
icon: 'Atom',
|
||||
isCore: false,
|
||||
defaultEnabled: false,
|
||||
isEngineModule: true,
|
||||
dependencies: ['core', 'math'],
|
||||
exports: { components: ['RigidBody2D'] },
|
||||
requiresWasm: true
|
||||
};
|
||||
|
||||
/**
|
||||
* 物理插件
|
||||
*/
|
||||
export const PhysicsPlugin: IRuntimePlugin = {
|
||||
manifest,
|
||||
runtimeModule: new PhysicsRuntimeModule()
|
||||
};
|
||||
|
||||
export { PhysicsRuntimeModule };
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* BoxCollider2D Component
|
||||
* 2D 矩形碰撞体组件
|
||||
*/
|
||||
|
||||
import { Property, Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework';
|
||||
import type { IVector2 } from '@esengine/ecs-framework-math';
|
||||
import { Collider2DBase } from './Collider2DBase';
|
||||
|
||||
/**
|
||||
* 2D 矩形碰撞体
|
||||
*
|
||||
* 用于创建矩形形状的碰撞体。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const entity = scene.createEntity('Box');
|
||||
* const collider = entity.addComponent(BoxCollider2DComponent);
|
||||
* collider.width = 2;
|
||||
* collider.height = 1;
|
||||
* ```
|
||||
*/
|
||||
@ECSComponent('BoxCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||
export class BoxCollider2DComponent extends Collider2DBase {
|
||||
private _width: number = 10;
|
||||
private _height: number = 10;
|
||||
|
||||
/**
|
||||
* 矩形宽度(半宽度的2倍)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Width', min: 0.01, step: 0.1 })
|
||||
public get width(): number {
|
||||
return this._width;
|
||||
}
|
||||
|
||||
public set width(value: number) {
|
||||
if (this._width !== value) {
|
||||
this._width = value;
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 矩形高度(半高度的2倍)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Height', min: 0.01, step: 0.1 })
|
||||
public get height(): number {
|
||||
return this._height;
|
||||
}
|
||||
|
||||
public set height(value: number) {
|
||||
if (this._height !== value) {
|
||||
this._height = value;
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取半宽度
|
||||
*/
|
||||
public get halfWidth(): number {
|
||||
return this.width / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取半高度
|
||||
*/
|
||||
public get halfHeight(): number {
|
||||
return this.height / 2;
|
||||
}
|
||||
|
||||
public override getShapeType(): string {
|
||||
return 'box';
|
||||
}
|
||||
|
||||
public override calculateArea(): number {
|
||||
return this.width * this.height;
|
||||
}
|
||||
|
||||
public override calculateAABB(): { min: IVector2; max: IVector2 } {
|
||||
const hw = this.halfWidth;
|
||||
const hh = this.halfHeight;
|
||||
|
||||
// 简化版本,不考虑旋转偏移
|
||||
return {
|
||||
min: { x: this.offset.x - hw, y: this.offset.y - hh },
|
||||
max: { x: this.offset.x + hw, y: this.offset.y + hh }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置尺寸
|
||||
* @param width 宽度
|
||||
* @param height 高度
|
||||
*/
|
||||
public setSize(width: number, height: number): void {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* CapsuleCollider2D Component
|
||||
* 2D 胶囊碰撞体组件
|
||||
*/
|
||||
|
||||
import { Property, Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework';
|
||||
import type { IVector2 } from '@esengine/ecs-framework-math';
|
||||
import { Collider2DBase } from './Collider2DBase';
|
||||
|
||||
/**
|
||||
* 胶囊方向
|
||||
*/
|
||||
export enum CapsuleDirection2D {
|
||||
/** 垂直方向(默认) */
|
||||
Vertical = 0,
|
||||
/** 水平方向 */
|
||||
Horizontal = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 2D 胶囊碰撞体
|
||||
*
|
||||
* 胶囊由两个半圆和一个矩形组成。
|
||||
* 常用于角色碰撞体。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const entity = scene.createEntity('Character');
|
||||
* const collider = entity.addComponent(CapsuleCollider2DComponent);
|
||||
* collider.radius = 0.25;
|
||||
* collider.height = 1;
|
||||
* collider.direction = CapsuleDirection2D.Vertical;
|
||||
* ```
|
||||
*/
|
||||
@ECSComponent('CapsuleCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'CapsuleCollider2D' })
|
||||
export class CapsuleCollider2DComponent extends Collider2DBase {
|
||||
private _radius: number = 3;
|
||||
private _height: number = 10;
|
||||
private _direction: CapsuleDirection2D = CapsuleDirection2D.Vertical;
|
||||
|
||||
/**
|
||||
* 胶囊半径
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Radius', min: 0.01, step: 0.1 })
|
||||
public get radius(): number {
|
||||
return this._radius;
|
||||
}
|
||||
|
||||
public set radius(value: number) {
|
||||
if (this._radius !== value) {
|
||||
this._radius = value;
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 胶囊总高度(包括两端的半圆)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Height', min: 0.01, step: 0.1 })
|
||||
public get height(): number {
|
||||
return this._height;
|
||||
}
|
||||
|
||||
public set height(value: number) {
|
||||
if (this._height !== value) {
|
||||
this._height = value;
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 胶囊方向
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Direction',
|
||||
options: [
|
||||
{ label: 'Vertical', value: 0 },
|
||||
{ label: 'Horizontal', value: 1 }
|
||||
]
|
||||
})
|
||||
public get direction(): CapsuleDirection2D {
|
||||
return this._direction;
|
||||
}
|
||||
|
||||
public set direction(value: CapsuleDirection2D) {
|
||||
if (this._direction !== value) {
|
||||
this._direction = value;
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取半高度(中间矩形部分的一半)
|
||||
*/
|
||||
public get halfHeight(): number {
|
||||
return Math.max(0, (this.height - this.radius * 2) / 2);
|
||||
}
|
||||
|
||||
public override getShapeType(): string {
|
||||
return 'capsule';
|
||||
}
|
||||
|
||||
public override calculateArea(): number {
|
||||
// 胶囊面积 = 矩形面积 + 圆面积
|
||||
const rectArea = this.radius * 2 * this.halfHeight * 2;
|
||||
const circleArea = Math.PI * this.radius * this.radius;
|
||||
return rectArea + circleArea;
|
||||
}
|
||||
|
||||
public override calculateAABB(): { min: IVector2; max: IVector2 } {
|
||||
if (this.direction === CapsuleDirection2D.Vertical) {
|
||||
return {
|
||||
min: { x: this.offset.x - this.radius, y: this.offset.y - this.height / 2 },
|
||||
max: { x: this.offset.x + this.radius, y: this.offset.y + this.height / 2 }
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
min: { x: this.offset.x - this.height / 2, y: this.offset.y - this.radius },
|
||||
max: { x: this.offset.x + this.height / 2, y: this.offset.y + this.radius }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置胶囊尺寸
|
||||
* @param radius 半径
|
||||
* @param height 总高度
|
||||
*/
|
||||
public setSize(radius: number, height: number): void {
|
||||
this.radius = radius;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置方向
|
||||
* @param direction 方向
|
||||
*/
|
||||
public setDirection(direction: CapsuleDirection2D): void {
|
||||
this.direction = direction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* CircleCollider2D Component
|
||||
* 2D 圆形碰撞体组件
|
||||
*/
|
||||
|
||||
import { Property, Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework';
|
||||
import type { IVector2 } from '@esengine/ecs-framework-math';
|
||||
import { Collider2DBase } from './Collider2DBase';
|
||||
|
||||
/**
|
||||
* 2D 圆形碰撞体
|
||||
*
|
||||
* 用于创建圆形形状的碰撞体。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const entity = scene.createEntity('Ball');
|
||||
* const collider = entity.addComponent(CircleCollider2DComponent);
|
||||
* collider.radius = 0.5;
|
||||
* ```
|
||||
*/
|
||||
@ECSComponent('CircleCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||
export class CircleCollider2DComponent extends Collider2DBase {
|
||||
private _radius: number = 5;
|
||||
|
||||
/**
|
||||
* 圆的半径
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Radius', min: 0.01, step: 0.1 })
|
||||
public get radius(): number {
|
||||
return this._radius;
|
||||
}
|
||||
|
||||
public set radius(value: number) {
|
||||
if (this._radius !== value) {
|
||||
this._radius = value;
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override getShapeType(): string {
|
||||
return 'circle';
|
||||
}
|
||||
|
||||
public override calculateArea(): number {
|
||||
return Math.PI * this.radius * this.radius;
|
||||
}
|
||||
|
||||
public override calculateAABB(): { min: IVector2; max: IVector2 } {
|
||||
return {
|
||||
min: { x: this.offset.x - this.radius, y: this.offset.y - this.radius },
|
||||
max: { x: this.offset.x + this.radius, y: this.offset.y + this.radius }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置半径
|
||||
* @param radius 半径
|
||||
*/
|
||||
public setRadius(radius: number): void {
|
||||
this.radius = radius;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Collider2D Base Component
|
||||
* 2D 碰撞体基类组件
|
||||
*/
|
||||
|
||||
import { Component, Property, Serialize } from '@esengine/ecs-framework';
|
||||
import type { IVector2 } from '@esengine/ecs-framework-math';
|
||||
import { CollisionLayer2D } from '../types/Physics2DTypes';
|
||||
|
||||
/**
|
||||
* 2D 碰撞体基类
|
||||
*
|
||||
* 定义了所有 2D 碰撞体的共同属性和接口。
|
||||
* 具体的碰撞体形状由子类实现。
|
||||
*/
|
||||
export abstract class Collider2DBase extends Component {
|
||||
// ==================== 物理材质属性 ====================
|
||||
|
||||
/**
|
||||
* 摩擦系数 [0, 1]
|
||||
* 0 = 完全光滑,1 = 最大摩擦
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Friction', min: 0, max: 1, step: 0.01 })
|
||||
public friction: number = 0.5;
|
||||
|
||||
/**
|
||||
* 弹性系数(恢复系数)[0, 1]
|
||||
* 0 = 完全非弹性碰撞,1 = 完全弹性碰撞
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Restitution', min: 0, max: 1, step: 0.01 })
|
||||
public restitution: number = 0;
|
||||
|
||||
/**
|
||||
* 密度 (kg/m²)
|
||||
* 用于计算质量(与碰撞体面积相乘)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Density', min: 0.001, step: 0.1 })
|
||||
public density: number = 1;
|
||||
|
||||
// ==================== 碰撞过滤 ====================
|
||||
|
||||
/**
|
||||
* 是否为触发器
|
||||
* 触发器不产生物理碰撞响应,只触发事件
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Is Trigger' })
|
||||
public isTrigger: boolean = false;
|
||||
|
||||
/**
|
||||
* 碰撞层(该碰撞体所在的层)
|
||||
* 使用位掩码,可以属于多个层
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'collisionLayer', label: 'Collision Layer' })
|
||||
public collisionLayer: number = CollisionLayer2D.Default;
|
||||
|
||||
/**
|
||||
* 碰撞掩码(该碰撞体可以与哪些层碰撞)
|
||||
* 使用位掩码
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'collisionMask', label: 'Collision Mask' })
|
||||
public collisionMask: number = CollisionLayer2D.All;
|
||||
|
||||
// ==================== 偏移 ====================
|
||||
|
||||
/**
|
||||
* 相对于实体 Transform 的位置偏移
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'vector2', label: 'Offset' })
|
||||
public offset: IVector2 = { x: 0, y: 0 };
|
||||
|
||||
/**
|
||||
* 相对于实体 Transform 的旋转偏移(度)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Rotation Offset', min: -180, max: 180, step: 1 })
|
||||
public rotationOffset: number = 0;
|
||||
|
||||
// ==================== 内部状态 ====================
|
||||
|
||||
/**
|
||||
* Rapier 碰撞体句柄
|
||||
* @internal
|
||||
*/
|
||||
public _colliderHandle: number | null = null;
|
||||
|
||||
/**
|
||||
* 关联的刚体实体 ID(如果有)
|
||||
* @internal
|
||||
*/
|
||||
public _attachedBodyEntityId: number | null = null;
|
||||
|
||||
/**
|
||||
* 是否需要重建碰撞体
|
||||
* @internal
|
||||
*/
|
||||
public _needsRebuild: boolean = false;
|
||||
|
||||
// ==================== 抽象方法 ====================
|
||||
|
||||
/**
|
||||
* 获取碰撞体形状类型名称
|
||||
*/
|
||||
public abstract getShapeType(): string;
|
||||
|
||||
/**
|
||||
* 计算碰撞体的面积(用于质量计算)
|
||||
*/
|
||||
public abstract calculateArea(): number;
|
||||
|
||||
/**
|
||||
* 计算碰撞体的 AABB(轴对齐包围盒)
|
||||
*/
|
||||
public abstract calculateAABB(): { min: IVector2; max: IVector2 };
|
||||
|
||||
// ==================== API 方法 ====================
|
||||
|
||||
/**
|
||||
* 设置碰撞层
|
||||
* @param layer 层标识
|
||||
*/
|
||||
public setLayer(layer: CollisionLayer2D): void {
|
||||
this.collisionLayer = layer;
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加碰撞层
|
||||
* @param layer 层标识
|
||||
*/
|
||||
public addLayer(layer: CollisionLayer2D): void {
|
||||
this.collisionLayer |= layer;
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除碰撞层
|
||||
* @param layer 层标识
|
||||
*/
|
||||
public removeLayer(layer: CollisionLayer2D): void {
|
||||
this.collisionLayer &= ~layer;
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否在指定层
|
||||
* @param layer 层标识
|
||||
*/
|
||||
public isInLayer(layer: CollisionLayer2D): boolean {
|
||||
return (this.collisionLayer & layer) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置碰撞掩码
|
||||
* @param mask 掩码值
|
||||
*/
|
||||
public setCollisionMask(mask: number): void {
|
||||
this.collisionMask = mask;
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以与指定层碰撞
|
||||
* @param layer 层标识
|
||||
*/
|
||||
public canCollideWith(layer: CollisionLayer2D): boolean {
|
||||
return (this.collisionMask & layer) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记需要重建
|
||||
*/
|
||||
public markNeedsRebuild(): void {
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
|
||||
public override onRemovedFromEntity(): void {
|
||||
this._colliderHandle = null;
|
||||
this._attachedBodyEntityId = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* PolygonCollider2D Component
|
||||
* 2D 多边形碰撞体组件
|
||||
*/
|
||||
|
||||
import { Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework';
|
||||
import type { IVector2 } from '@esengine/ecs-framework-math';
|
||||
import { Collider2DBase } from './Collider2DBase';
|
||||
|
||||
/**
|
||||
* 2D 多边形碰撞体
|
||||
*
|
||||
* 用于创建任意凸多边形形状的碰撞体。
|
||||
* 注意:Rapier 只支持凸多边形,非凸多边形需要分解。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const entity = scene.createEntity('Triangle');
|
||||
* const collider = entity.addComponent(PolygonCollider2DComponent);
|
||||
* collider.setVertices([
|
||||
* { x: 0, y: 1 },
|
||||
* { x: -1, y: -1 },
|
||||
* { x: 1, y: -1 }
|
||||
* ]);
|
||||
* ```
|
||||
*/
|
||||
@ECSComponent('PolygonCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'PolygonCollider2D' })
|
||||
export class PolygonCollider2DComponent extends Collider2DBase {
|
||||
/**
|
||||
* 多边形顶点(局部坐标,逆时针顺序)
|
||||
* 最少3个,最多不超过引擎限制(通常是 8-16 个)
|
||||
*/
|
||||
@Serialize()
|
||||
public vertices: IVector2[] = [
|
||||
{ x: -5, y: -5 },
|
||||
{ x: 5, y: -5 },
|
||||
{ x: 5, y: 5 },
|
||||
{ x: -5, y: 5 }
|
||||
];
|
||||
|
||||
public override getShapeType(): string {
|
||||
return 'polygon';
|
||||
}
|
||||
|
||||
public override calculateArea(): number {
|
||||
// 使用鞋带公式计算多边形面积
|
||||
if (this.vertices.length < 3) return 0;
|
||||
|
||||
let area = 0;
|
||||
const n = this.vertices.length;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const j = (i + 1) % n;
|
||||
area += this.vertices[i].x * this.vertices[j].y;
|
||||
area -= this.vertices[j].x * this.vertices[i].y;
|
||||
}
|
||||
|
||||
return Math.abs(area) / 2;
|
||||
}
|
||||
|
||||
public override calculateAABB(): { min: IVector2; max: IVector2 } {
|
||||
if (this.vertices.length === 0) {
|
||||
return {
|
||||
min: { x: this.offset.x, y: this.offset.y },
|
||||
max: { x: this.offset.x, y: this.offset.y }
|
||||
};
|
||||
}
|
||||
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
for (const v of this.vertices) {
|
||||
minX = Math.min(minX, v.x);
|
||||
minY = Math.min(minY, v.y);
|
||||
maxX = Math.max(maxX, v.x);
|
||||
maxY = Math.max(maxY, v.y);
|
||||
}
|
||||
|
||||
return {
|
||||
min: { x: this.offset.x + minX, y: this.offset.y + minY },
|
||||
max: { x: this.offset.x + maxX, y: this.offset.y + maxY }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置顶点
|
||||
* @param vertices 顶点数组(逆时针顺序)
|
||||
*/
|
||||
public setVertices(vertices: IVector2[]): void {
|
||||
if (vertices.length < 3) {
|
||||
console.warn('PolygonCollider2D: 至少需要3个顶点');
|
||||
return;
|
||||
}
|
||||
this.vertices = vertices.map((v) => ({ x: v.x, y: v.y }));
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建正多边形
|
||||
* @param sides 边数(至少3)
|
||||
* @param radius 外接圆半径
|
||||
*/
|
||||
public setRegularPolygon(sides: number, radius: number): void {
|
||||
if (sides < 3) {
|
||||
console.warn('PolygonCollider2D: 正多边形至少需要3条边');
|
||||
return;
|
||||
}
|
||||
|
||||
const vertices: IVector2[] = [];
|
||||
const angleStep = (Math.PI * 2) / sides;
|
||||
|
||||
for (let i = 0; i < sides; i++) {
|
||||
const angle = angleStep * i - Math.PI / 2; // 从顶部开始
|
||||
vertices.push({
|
||||
x: Math.cos(angle) * radius,
|
||||
y: Math.sin(angle) * radius
|
||||
});
|
||||
}
|
||||
|
||||
this.setVertices(vertices);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证多边形是否为凸多边形
|
||||
* @returns 是否为凸多边形
|
||||
*/
|
||||
public isConvex(): boolean {
|
||||
if (this.vertices.length < 3) return false;
|
||||
|
||||
const n = this.vertices.length;
|
||||
let sign = 0;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const v0 = this.vertices[i];
|
||||
const v1 = this.vertices[(i + 1) % n];
|
||||
const v2 = this.vertices[(i + 2) % n];
|
||||
|
||||
const cross = (v1.x - v0.x) * (v2.y - v1.y) - (v1.y - v0.y) * (v2.x - v1.x);
|
||||
|
||||
if (cross !== 0) {
|
||||
if (sign === 0) {
|
||||
sign = cross > 0 ? 1 : -1;
|
||||
} else if ((cross > 0 ? 1 : -1) !== sign) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* Rigidbody2D Component
|
||||
* 2D 刚体组件
|
||||
*/
|
||||
|
||||
import { Component, Property, Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework';
|
||||
import type { IVector2 } from '@esengine/ecs-framework-math';
|
||||
import { RigidbodyType2D, CollisionDetectionMode2D } from '../types/Physics2DTypes';
|
||||
|
||||
/**
|
||||
* 刚体约束配置
|
||||
*/
|
||||
export interface RigidbodyConstraints2D {
|
||||
/** 冻结 X 轴位置 */
|
||||
freezePositionX: boolean;
|
||||
/** 冻结 Y 轴位置 */
|
||||
freezePositionY: boolean;
|
||||
/** 冻结旋转 */
|
||||
freezeRotation: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 2D 刚体组件
|
||||
*
|
||||
* 用于给实体添加物理模拟能力。必须与 TransformComponent 配合使用。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const entity = scene.createEntity('Player');
|
||||
* entity.addComponent(TransformComponent);
|
||||
* const rb = entity.addComponent(Rigidbody2DComponent);
|
||||
* rb.bodyType = RigidbodyType2D.Dynamic;
|
||||
* rb.mass = 1;
|
||||
* rb.gravityScale = 1;
|
||||
* ```
|
||||
*/
|
||||
@ECSComponent('Rigidbody2D')
|
||||
@Serializable({ version: 1, typeId: 'Rigidbody2D' })
|
||||
export class Rigidbody2DComponent extends Component {
|
||||
// ==================== 基础属性 ====================
|
||||
|
||||
/**
|
||||
* 刚体类型
|
||||
* - Dynamic: 动态刚体,受力和碰撞影响
|
||||
* - Kinematic: 运动学刚体,手动控制
|
||||
* - Static: 静态刚体,不移动
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Body Type',
|
||||
options: [
|
||||
{ label: 'Dynamic', value: 0 },
|
||||
{ label: 'Kinematic', value: 1 },
|
||||
{ label: 'Static', value: 2 }
|
||||
]
|
||||
})
|
||||
public bodyType: RigidbodyType2D = RigidbodyType2D.Dynamic;
|
||||
|
||||
/**
|
||||
* 质量(kg)
|
||||
* 仅对 Dynamic 刚体有效
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Mass', min: 0.001, step: 0.1 })
|
||||
public mass: number = 1;
|
||||
|
||||
/**
|
||||
* 重力缩放
|
||||
* 0 = 不受重力影响,1 = 正常重力,-1 = 反重力
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Gravity Scale', min: -10, max: 10, step: 0.1 })
|
||||
public gravityScale: number = 1;
|
||||
|
||||
// ==================== 阻尼 ====================
|
||||
|
||||
/**
|
||||
* 线性阻尼
|
||||
* 值越大,移动速度衰减越快
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Linear Damping', min: 0, max: 100, step: 0.1 })
|
||||
public linearDamping: number = 0;
|
||||
|
||||
/**
|
||||
* 角速度阻尼
|
||||
* 值越大,旋转速度衰减越快
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Angular Damping', min: 0, max: 100, step: 0.01 })
|
||||
public angularDamping: number = 0.05;
|
||||
|
||||
// ==================== 约束 ====================
|
||||
|
||||
/**
|
||||
* 冻结 X 轴位置
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Freeze Position X' })
|
||||
public freezePositionX: boolean = false;
|
||||
|
||||
/**
|
||||
* 冻结 Y 轴位置
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Freeze Position Y' })
|
||||
public freezePositionY: boolean = false;
|
||||
|
||||
/**
|
||||
* 冻结旋转
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Freeze Rotation' })
|
||||
public freezeRotation: boolean = false;
|
||||
|
||||
/**
|
||||
* 运动约束(兼容旧代码)
|
||||
* @deprecated 使用 freezePositionX, freezePositionY, freezeRotation 代替
|
||||
*/
|
||||
public get constraints(): RigidbodyConstraints2D {
|
||||
return {
|
||||
freezePositionX: this.freezePositionX,
|
||||
freezePositionY: this.freezePositionY,
|
||||
freezeRotation: this.freezeRotation
|
||||
};
|
||||
}
|
||||
|
||||
public set constraints(value: RigidbodyConstraints2D) {
|
||||
this.freezePositionX = value.freezePositionX;
|
||||
this.freezePositionY = value.freezePositionY;
|
||||
this.freezeRotation = value.freezeRotation;
|
||||
}
|
||||
|
||||
// ==================== 碰撞检测 ====================
|
||||
|
||||
/**
|
||||
* 碰撞检测模式
|
||||
* - Discrete: 离散检测,性能好
|
||||
* - Continuous: 连续检测,防穿透
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Collision Detection',
|
||||
options: [
|
||||
{ label: 'Discrete', value: 0 },
|
||||
{ label: 'Continuous', value: 1 }
|
||||
]
|
||||
})
|
||||
public collisionDetection: CollisionDetectionMode2D = CollisionDetectionMode2D.Discrete;
|
||||
|
||||
// ==================== 休眠 ====================
|
||||
|
||||
/**
|
||||
* 是否允许休眠
|
||||
* 休眠的刚体不参与物理计算,提高性能
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Can Sleep' })
|
||||
public canSleep: boolean = true;
|
||||
|
||||
/**
|
||||
* 是否处于唤醒状态
|
||||
*/
|
||||
@Property({ type: 'boolean', label: 'Is Awake', readOnly: true })
|
||||
public isAwake: boolean = true;
|
||||
|
||||
// ==================== 运行时状态(不序列化)====================
|
||||
|
||||
/**
|
||||
* 当前线速度
|
||||
*/
|
||||
public velocity: IVector2 = { x: 0, y: 0 };
|
||||
|
||||
/**
|
||||
* 当前角速度(弧度/秒)
|
||||
*/
|
||||
public angularVelocity: number = 0;
|
||||
|
||||
// ==================== 内部状态 ====================
|
||||
|
||||
/**
|
||||
* Rapier 刚体句柄
|
||||
* @internal
|
||||
*/
|
||||
public _bodyHandle: number | null = null;
|
||||
|
||||
/**
|
||||
* 是否需要同步 Transform 到物理世界
|
||||
* @internal
|
||||
*/
|
||||
public _needsSync: boolean = true;
|
||||
|
||||
/**
|
||||
* 上一帧的位置(用于插值)
|
||||
* @internal
|
||||
*/
|
||||
public _previousPosition: IVector2 = { x: 0, y: 0 };
|
||||
|
||||
/**
|
||||
* 上一帧的旋转角度
|
||||
* @internal
|
||||
*/
|
||||
public _previousRotation: number = 0;
|
||||
|
||||
// ==================== API 方法 ====================
|
||||
|
||||
/**
|
||||
* 添加力(在下一个物理步进中应用)
|
||||
* 这是一个标记方法,实际力的应用由 Physics2DSystem 处理
|
||||
*/
|
||||
public addForce(force: IVector2): void {
|
||||
this._pendingForce.x += force.x;
|
||||
this._pendingForce.y += force.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加冲量(立即改变速度)
|
||||
*/
|
||||
public addImpulse(impulse: IVector2): void {
|
||||
this._pendingImpulse.x += impulse.x;
|
||||
this._pendingImpulse.y += impulse.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加扭矩
|
||||
*/
|
||||
public addTorque(torque: number): void {
|
||||
this._pendingTorque += torque;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加角冲量
|
||||
*/
|
||||
public addAngularImpulse(impulse: number): void {
|
||||
this._pendingAngularImpulse += impulse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置线速度
|
||||
*/
|
||||
public setVelocity(velocity: IVector2): void {
|
||||
this._targetVelocity = { ...velocity };
|
||||
this._hasTargetVelocity = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置角速度
|
||||
*/
|
||||
public setAngularVelocity(angularVelocity: number): void {
|
||||
this._targetAngularVelocity = angularVelocity;
|
||||
this._hasTargetAngularVelocity = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 唤醒刚体
|
||||
*/
|
||||
public wakeUp(): void {
|
||||
this._shouldWakeUp = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使刚体休眠
|
||||
*/
|
||||
public sleep(): void {
|
||||
this._shouldSleep = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记需要重新同步 Transform
|
||||
*/
|
||||
public markNeedsSync(): void {
|
||||
this._needsSync = true;
|
||||
}
|
||||
|
||||
// ==================== 待处理的力和冲量 ====================
|
||||
|
||||
/** @internal */
|
||||
public _pendingForce: IVector2 = { x: 0, y: 0 };
|
||||
/** @internal */
|
||||
public _pendingImpulse: IVector2 = { x: 0, y: 0 };
|
||||
/** @internal */
|
||||
public _pendingTorque: number = 0;
|
||||
/** @internal */
|
||||
public _pendingAngularImpulse: number = 0;
|
||||
/** @internal */
|
||||
public _targetVelocity: IVector2 = { x: 0, y: 0 };
|
||||
/** @internal */
|
||||
public _hasTargetVelocity: boolean = false;
|
||||
/** @internal */
|
||||
public _targetAngularVelocity: number = 0;
|
||||
/** @internal */
|
||||
public _hasTargetAngularVelocity: boolean = false;
|
||||
/** @internal */
|
||||
public _shouldWakeUp: boolean = false;
|
||||
/** @internal */
|
||||
public _shouldSleep: boolean = false;
|
||||
|
||||
/**
|
||||
* 清除待处理的力和冲量
|
||||
* @internal
|
||||
*/
|
||||
public _clearPendingForces(): void {
|
||||
this._pendingForce.x = 0;
|
||||
this._pendingForce.y = 0;
|
||||
this._pendingImpulse.x = 0;
|
||||
this._pendingImpulse.y = 0;
|
||||
this._pendingTorque = 0;
|
||||
this._pendingAngularImpulse = 0;
|
||||
this._hasTargetVelocity = false;
|
||||
this._hasTargetAngularVelocity = false;
|
||||
this._shouldWakeUp = false;
|
||||
this._shouldSleep = false;
|
||||
}
|
||||
|
||||
public override onRemovedFromEntity(): void {
|
||||
// 清理句柄,实际的物理对象清理由系统处理
|
||||
this._bodyHandle = null;
|
||||
this._clearPendingForces();
|
||||
}
|
||||
}
|
||||
11
packages/physics/physics-rapier2d/src/components/index.ts
Normal file
11
packages/physics/physics-rapier2d/src/components/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Physics 2D Components
|
||||
* 2D 物理组件导出
|
||||
*/
|
||||
|
||||
export { Rigidbody2DComponent, type RigidbodyConstraints2D } from './Rigidbody2DComponent';
|
||||
export { Collider2DBase } from './Collider2DBase';
|
||||
export { BoxCollider2DComponent } from './BoxCollider2DComponent';
|
||||
export { CircleCollider2DComponent } from './CircleCollider2DComponent';
|
||||
export { CapsuleCollider2DComponent, CapsuleDirection2D } from './CapsuleCollider2DComponent';
|
||||
export { PolygonCollider2DComponent } from './PolygonCollider2DComponent';
|
||||
42
packages/physics/physics-rapier2d/src/index.ts
Normal file
42
packages/physics/physics-rapier2d/src/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @esengine/physics-rapier2d
|
||||
*
|
||||
* Deterministic 2D physics engine based on Rapier2D with enhanced-determinism support.
|
||||
* 基于 Rapier2D 的确定性 2D 物理引擎。
|
||||
*
|
||||
* 注意:此入口不包含 WASM 依赖,可安全地在编辑器中同步导入。
|
||||
* 运行时模块(含 WASM)请使用 '@esengine/physics-rapier2d/runtime' 导入。
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
// Types (no WASM dependency)
|
||||
export * from './types';
|
||||
|
||||
// Components (no WASM dependency)
|
||||
export * from './components';
|
||||
|
||||
// Services (no WASM dependency)
|
||||
export * from './services';
|
||||
|
||||
// Systems (type only for editor usage)
|
||||
export type { Physics2DSystem } from './systems/Physics2DSystem';
|
||||
|
||||
// Editor plugin (no WASM dependency)
|
||||
export { Physics2DPlugin } from './PhysicsEditorPlugin';
|
||||
|
||||
// Runtime plugin (for game builds)
|
||||
export { PhysicsPlugin } from './PhysicsRuntimeModule';
|
||||
|
||||
// Service tokens and interfaces (谁定义接口,谁导出 Token)
|
||||
export {
|
||||
Physics2DQueryToken,
|
||||
Physics2DSystemToken,
|
||||
Physics2DWorldToken,
|
||||
PhysicsConfigToken,
|
||||
CollisionLayerConfigToken,
|
||||
type IPhysics2DQuery,
|
||||
type IPhysics2DWorld,
|
||||
type ICollisionLayerConfig,
|
||||
type PhysicsConfig
|
||||
} from './tokens';
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Rapier2D 加载器配置
|
||||
* Rapier2D loader configuration
|
||||
*/
|
||||
|
||||
import type { WasmLibraryConfig } from '@esengine/platform-common';
|
||||
import { isEditorEnvironment } from '@esengine/platform-common';
|
||||
|
||||
/**
|
||||
* 获取 WASM 路径
|
||||
* Get WASM path based on environment
|
||||
*
|
||||
* Editor: engine/rapier2d/pkg/rapier_wasm2d_bg.wasm (deployed by vite build plugin)
|
||||
* Runtime: wasm/rapier_wasm2d_bg.wasm (deployed by game build)
|
||||
*/
|
||||
function getWasmPath(): string {
|
||||
const isEditor = isEditorEnvironment();
|
||||
// Editor uses dist/engine/rapier2d/pkg/ structure (from vite copy-engine-modules plugin)
|
||||
// 编辑器使用 dist/engine/rapier2d/pkg/ 结构(来自 vite copy-engine-modules 插件)
|
||||
const path = isEditor
|
||||
? 'engine/rapier2d/pkg/rapier_wasm2d_bg.wasm'
|
||||
: 'wasm/rapier_wasm2d_bg.wasm';
|
||||
|
||||
console.log(`[Rapier2D] isEditor=${isEditor}, wasmPath=${path}`);
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rapier2D 加载器配置
|
||||
*
|
||||
* Web 平台:使用标准版(独立 WASM 文件)
|
||||
* 小游戏平台:使用独立 WASM 文件 + WXWebAssembly 加载
|
||||
*/
|
||||
export const Rapier2DLoaderConfig: WasmLibraryConfig = {
|
||||
name: 'Rapier2D',
|
||||
|
||||
web: {
|
||||
/**
|
||||
* WASM 文件路径
|
||||
* 编辑器: engine/rapier2d/pkg/rapier_wasm2d_bg.wasm
|
||||
* 运行时: wasm/rapier_wasm2d_bg.wasm
|
||||
*/
|
||||
get wasmPath(): string {
|
||||
return getWasmPath();
|
||||
}
|
||||
},
|
||||
|
||||
minigame: {
|
||||
/**
|
||||
* WASM 文件路径(相对于小游戏根目录)
|
||||
*/
|
||||
wasmPath: 'wasm/rapier_wasm2d_bg.wasm',
|
||||
|
||||
/**
|
||||
* iOS 微信小游戏需要 TextDecoder polyfill
|
||||
*/
|
||||
needsTextDecoderPolyfill: true,
|
||||
|
||||
/**
|
||||
* iOS 微信小游戏需要 TextEncoder polyfill
|
||||
*/
|
||||
needsTextEncoderPolyfill: true,
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* 微信小游戏平台 Rapier2D 加载器
|
||||
*
|
||||
* 使用 WXWebAssembly 加载独立的 .wasm 文件
|
||||
*/
|
||||
|
||||
import type {
|
||||
IWasmLibraryLoader,
|
||||
WasmLibraryConfig,
|
||||
PlatformInfo
|
||||
} from '@esengine/platform-common';
|
||||
import {
|
||||
PlatformType,
|
||||
installTextDecoderPolyfill,
|
||||
installTextEncoderPolyfill
|
||||
} from '@esengine/platform-common';
|
||||
|
||||
/**
|
||||
* Rapier2D 模块类型
|
||||
*/
|
||||
type RapierModule = typeof import('@esengine/rapier2d');
|
||||
|
||||
/**
|
||||
* 微信小游戏 WASM API 类型声明
|
||||
*/
|
||||
declare const WXWebAssembly: {
|
||||
instantiate(
|
||||
path: string,
|
||||
imports?: WebAssembly.Imports
|
||||
): Promise<WebAssembly.Instance>;
|
||||
Memory: typeof WebAssembly.Memory;
|
||||
Table: typeof WebAssembly.Table;
|
||||
};
|
||||
|
||||
/**
|
||||
* 微信小游戏平台 Rapier2D 加载器
|
||||
*
|
||||
* 特殊处理:
|
||||
* 1. 安装 TextDecoder/TextEncoder polyfill
|
||||
* 2. 使用 WXWebAssembly 加载 .wasm 文件
|
||||
* 3. 临时替换全局 WebAssembly 对象
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const loader = new WeChatRapier2DLoader(config);
|
||||
* if (loader.isSupported()) {
|
||||
* const RAPIER = await loader.load();
|
||||
* // 使用 RAPIER...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class WeChatRapier2DLoader implements IWasmLibraryLoader<RapierModule> {
|
||||
private _config: WasmLibraryConfig;
|
||||
|
||||
/**
|
||||
* 创建微信小游戏平台 Rapier2D 加载器
|
||||
*
|
||||
* @param config - 加载器配置
|
||||
*/
|
||||
constructor(config: WasmLibraryConfig) {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 Rapier2D 模块
|
||||
*
|
||||
* @returns 初始化完成的 Rapier2D 模块
|
||||
*/
|
||||
async load(): Promise<RapierModule> {
|
||||
console.log(`[${this._config.name}] 正在加载微信小游戏版本...`);
|
||||
|
||||
// 1. 安装必要的 polyfills
|
||||
this.installPolyfills();
|
||||
|
||||
// 2. 检查 WXWebAssembly 支持
|
||||
if (typeof WXWebAssembly === 'undefined') {
|
||||
throw new Error(
|
||||
`[${this._config.name}] 当前微信基础库版本不支持 WebAssembly,` +
|
||||
'请升级微信或使用更高版本的基础库'
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 加载 Rapier2D
|
||||
const RAPIER = await this.loadRapierWithWXWasm();
|
||||
|
||||
console.log(`[${this._config.name}] 加载完成`);
|
||||
return RAPIER;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装必要的 polyfills
|
||||
*/
|
||||
private installPolyfills(): void {
|
||||
const config = this._config.minigame;
|
||||
|
||||
if (config?.needsTextDecoderPolyfill) {
|
||||
installTextDecoderPolyfill();
|
||||
}
|
||||
|
||||
if (config?.needsTextEncoderPolyfill) {
|
||||
installTextEncoderPolyfill();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 WXWebAssembly 加载 Rapier2D
|
||||
*
|
||||
* 通过临时替换全局 WebAssembly 对象来使 Rapier2D 使用 WXWebAssembly
|
||||
*
|
||||
* @returns 初始化完成的 Rapier2D 模块
|
||||
*/
|
||||
private async loadRapierWithWXWasm(): Promise<RapierModule> {
|
||||
// 保存原始 WebAssembly 对象
|
||||
const originalWebAssembly = (globalThis as any).WebAssembly;
|
||||
|
||||
try {
|
||||
// 创建一个包装的 WebAssembly 对象
|
||||
// 让 Rapier2D 的初始化代码使用 WXWebAssembly
|
||||
(globalThis as any).WebAssembly = this.createWXWebAssemblyWrapper();
|
||||
|
||||
// 导入 Rapier2D 标准版
|
||||
const RAPIER = await import('@esengine/rapier2d');
|
||||
|
||||
// 初始化 WASM - 标准版需要提供 WASM 路径
|
||||
const wasmPath = this._config.minigame?.wasmPath || 'wasm/rapier_wasm2d_bg.wasm';
|
||||
await RAPIER.init(wasmPath);
|
||||
|
||||
return RAPIER;
|
||||
} finally {
|
||||
// 恢复原始 WebAssembly 对象
|
||||
if (originalWebAssembly) {
|
||||
(globalThis as any).WebAssembly = originalWebAssembly;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 WXWebAssembly 包装器
|
||||
*
|
||||
* 将 WXWebAssembly 包装成与标准 WebAssembly API 兼容的形式
|
||||
*
|
||||
* @returns 包装后的 WebAssembly 对象
|
||||
*/
|
||||
private createWXWebAssemblyWrapper(): typeof WebAssembly {
|
||||
const wasmPath = this._config.minigame?.wasmPath || 'wasm/rapier2d_bg.wasm';
|
||||
|
||||
return {
|
||||
instantiate: async (
|
||||
bufferSource: BufferSource | WebAssembly.Module,
|
||||
imports?: WebAssembly.Imports
|
||||
): Promise<WebAssembly.WebAssemblyInstantiatedSource> => {
|
||||
// WXWebAssembly.instantiate 直接接受文件路径
|
||||
const instance = await WXWebAssembly.instantiate(wasmPath, imports);
|
||||
return {
|
||||
instance,
|
||||
module: {} as WebAssembly.Module
|
||||
};
|
||||
},
|
||||
|
||||
instantiateStreaming: async (
|
||||
response: Response | PromiseLike<Response>,
|
||||
imports?: WebAssembly.Imports
|
||||
): Promise<WebAssembly.WebAssemblyInstantiatedSource> => {
|
||||
// 微信不支持 streaming,直接使用 instantiate
|
||||
const instance = await WXWebAssembly.instantiate(wasmPath, imports);
|
||||
return {
|
||||
instance,
|
||||
module: {} as WebAssembly.Module
|
||||
};
|
||||
},
|
||||
|
||||
compile: async (bytes: BufferSource): Promise<WebAssembly.Module> => {
|
||||
// 微信小游戏不支持单独编译
|
||||
throw new Error('WXWebAssembly 不支持 compile 方法');
|
||||
},
|
||||
|
||||
compileStreaming: async (source: Response | PromiseLike<Response>): Promise<WebAssembly.Module> => {
|
||||
throw new Error('WXWebAssembly 不支持 compileStreaming 方法');
|
||||
},
|
||||
|
||||
validate: (bytes: BufferSource): boolean => {
|
||||
// 简单返回 true,实际验证在 instantiate 时进行
|
||||
return true;
|
||||
},
|
||||
|
||||
Memory: WXWebAssembly.Memory,
|
||||
Table: WXWebAssembly.Table,
|
||||
Global: (globalThis as any).WebAssembly?.Global,
|
||||
Tag: (globalThis as any).WebAssembly?.Tag,
|
||||
Exception: (globalThis as any).WebAssembly?.Exception,
|
||||
CompileError: (globalThis as any).WebAssembly?.CompileError || Error,
|
||||
LinkError: (globalThis as any).WebAssembly?.LinkError || Error,
|
||||
RuntimeError: (globalThis as any).WebAssembly?.RuntimeError || Error,
|
||||
} as unknown as typeof WebAssembly;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持 WXWebAssembly
|
||||
*
|
||||
* @returns 是否支持
|
||||
*/
|
||||
isSupported(): boolean {
|
||||
return typeof WXWebAssembly !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台信息
|
||||
* Get platform information
|
||||
*/
|
||||
getPlatformInfo(): PlatformInfo {
|
||||
const needsPolyfills: string[] = [];
|
||||
|
||||
if (typeof globalThis.TextDecoder === 'undefined') {
|
||||
needsPolyfills.push('TextDecoder');
|
||||
}
|
||||
if (typeof globalThis.TextEncoder === 'undefined') {
|
||||
needsPolyfills.push('TextEncoder');
|
||||
}
|
||||
|
||||
return {
|
||||
type: PlatformType.WeChatMiniGame,
|
||||
supportsWasm: typeof WXWebAssembly !== 'undefined',
|
||||
supportsSharedArrayBuffer: false,
|
||||
needsPolyfills,
|
||||
isEditor: false // 微信小游戏不可能是编辑器环境 | WeChat cannot be editor
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加载器配置
|
||||
*
|
||||
* @returns 配置对象
|
||||
*/
|
||||
getConfig(): WasmLibraryConfig {
|
||||
return this._config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Web 平台 Rapier2D 加载器
|
||||
*
|
||||
* 使用 @esengine/rapier2d 标准版(独立 WASM 文件)
|
||||
*/
|
||||
|
||||
import type {
|
||||
IWasmLibraryLoader,
|
||||
WasmLibraryConfig,
|
||||
PlatformInfo
|
||||
} from '@esengine/platform-common';
|
||||
import { PlatformType, isEditorEnvironment } from '@esengine/platform-common';
|
||||
|
||||
/**
|
||||
* Rapier2D 模块类型
|
||||
*/
|
||||
type RapierModule = typeof import('@esengine/rapier2d');
|
||||
|
||||
/**
|
||||
* Web 平台 Rapier2D 加载器
|
||||
*
|
||||
* 使用标准版,需要配置 WASM 路径
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const loader = new WebRapier2DLoader(config);
|
||||
* if (loader.isSupported()) {
|
||||
* const RAPIER = await loader.load();
|
||||
* // 使用 RAPIER...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class WebRapier2DLoader implements IWasmLibraryLoader<RapierModule> {
|
||||
private _config: WasmLibraryConfig;
|
||||
|
||||
/**
|
||||
* 创建 Web 平台 Rapier2D 加载器
|
||||
*
|
||||
* @param config - 加载器配置
|
||||
*/
|
||||
constructor(config: WasmLibraryConfig) {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 Rapier2D 模块
|
||||
*
|
||||
* @returns 初始化完成的 Rapier2D 模块
|
||||
*/
|
||||
async load(): Promise<RapierModule> {
|
||||
console.log(`[${this._config.name}] 正在加载 Web 版本...`);
|
||||
|
||||
// 动态导入标准版
|
||||
const RAPIER = await import('@esengine/rapier2d');
|
||||
|
||||
// 初始化 WASM - 标准版需要提供 WASM 路径
|
||||
// 构建时 WASM 文件会被复制到 wasm/ 目录
|
||||
const wasmPath = this._config.web?.wasmPath || 'wasm/rapier_wasm2d_bg.wasm';
|
||||
await RAPIER.init(wasmPath);
|
||||
|
||||
console.log(`[${this._config.name}] 加载完成`);
|
||||
return RAPIER;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持 WebAssembly
|
||||
*
|
||||
* @returns 是否支持
|
||||
*/
|
||||
isSupported(): boolean {
|
||||
return typeof WebAssembly !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台信息
|
||||
* Get platform information
|
||||
*/
|
||||
getPlatformInfo(): PlatformInfo {
|
||||
return {
|
||||
type: PlatformType.Web,
|
||||
supportsWasm: typeof WebAssembly !== 'undefined',
|
||||
supportsSharedArrayBuffer: typeof SharedArrayBuffer !== 'undefined',
|
||||
needsPolyfills: [],
|
||||
isEditor: isEditorEnvironment()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加载器配置
|
||||
*
|
||||
* @returns 配置对象
|
||||
*/
|
||||
getConfig(): WasmLibraryConfig {
|
||||
return this._config;
|
||||
}
|
||||
}
|
||||
58
packages/physics/physics-rapier2d/src/loaders/index.ts
Normal file
58
packages/physics/physics-rapier2d/src/loaders/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Rapier2D 加载器
|
||||
*
|
||||
* 提供跨平台的 Rapier2D 物理引擎加载支持
|
||||
*/
|
||||
|
||||
export { Rapier2DLoaderConfig } from './Rapier2DLoaderConfig';
|
||||
export { WebRapier2DLoader } from './WebRapier2DLoader';
|
||||
export { WeChatRapier2DLoader } from './WeChatRapier2DLoader';
|
||||
|
||||
import { PlatformType, WasmLibraryLoaderFactory } from '@esengine/platform-common';
|
||||
import { Rapier2DLoaderConfig } from './Rapier2DLoaderConfig';
|
||||
import { WebRapier2DLoader } from './WebRapier2DLoader';
|
||||
import { WeChatRapier2DLoader } from './WeChatRapier2DLoader';
|
||||
|
||||
/**
|
||||
* 注册 Rapier2D 加载器到工厂
|
||||
*
|
||||
* 在模块加载时自动执行
|
||||
*/
|
||||
export function registerRapier2DLoaders(): void {
|
||||
// Web 平台加载器
|
||||
WasmLibraryLoaderFactory.registerLoader(
|
||||
'rapier2d',
|
||||
PlatformType.Web,
|
||||
() => new WebRapier2DLoader(Rapier2DLoaderConfig)
|
||||
);
|
||||
|
||||
// 微信小游戏平台加载器
|
||||
WasmLibraryLoaderFactory.registerLoader(
|
||||
'rapier2d',
|
||||
PlatformType.WeChatMiniGame,
|
||||
() => new WeChatRapier2DLoader(Rapier2DLoaderConfig)
|
||||
);
|
||||
|
||||
// 其他小游戏平台可以复用微信加载器(API 类似)
|
||||
// 如果需要特殊处理,可以创建专门的加载器
|
||||
WasmLibraryLoaderFactory.registerLoader(
|
||||
'rapier2d',
|
||||
PlatformType.ByteDanceMiniGame,
|
||||
() => new WeChatRapier2DLoader(Rapier2DLoaderConfig)
|
||||
);
|
||||
|
||||
WasmLibraryLoaderFactory.registerLoader(
|
||||
'rapier2d',
|
||||
PlatformType.AlipayMiniGame,
|
||||
() => new WeChatRapier2DLoader(Rapier2DLoaderConfig)
|
||||
);
|
||||
|
||||
WasmLibraryLoaderFactory.registerLoader(
|
||||
'rapier2d',
|
||||
PlatformType.BaiduMiniGame,
|
||||
() => new WeChatRapier2DLoader(Rapier2DLoaderConfig)
|
||||
);
|
||||
}
|
||||
|
||||
// 模块加载时自动注册
|
||||
registerRapier2DLoaders();
|
||||
65
packages/physics/physics-rapier2d/src/runtime.ts
Normal file
65
packages/physics/physics-rapier2d/src/runtime.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @esengine/physics-rapier2d Runtime Entry Point
|
||||
*
|
||||
* This entry point exports only runtime-related code without any editor dependencies.
|
||||
* Use this for standalone game runtime builds.
|
||||
*/
|
||||
|
||||
// Types
|
||||
export {
|
||||
RigidbodyType2D,
|
||||
CollisionDetectionMode2D,
|
||||
type Physics2DConfig,
|
||||
DEFAULT_PHYSICS_CONFIG,
|
||||
CollisionLayer2D,
|
||||
ForceMode2D,
|
||||
type RaycastHit2D,
|
||||
type ShapeCastHit2D,
|
||||
type OverlapResult2D,
|
||||
PhysicsMaterial2DPreset,
|
||||
getPhysicsMaterialPreset,
|
||||
JointType2D
|
||||
} from './types/Physics2DTypes';
|
||||
|
||||
// Re-export IVector2 from math package
|
||||
export type { IVector2 } from '@esengine/ecs-framework-math';
|
||||
|
||||
export {
|
||||
type CollisionEventType,
|
||||
type TriggerEventType,
|
||||
type ContactPoint2D,
|
||||
type CollisionEvent2D,
|
||||
type TriggerEvent2D,
|
||||
PHYSICS_EVENTS,
|
||||
type Physics2DEventMap
|
||||
} from './types/Physics2DEvents';
|
||||
|
||||
// Components
|
||||
export { Rigidbody2DComponent, type RigidbodyConstraints2D } from './components/Rigidbody2DComponent';
|
||||
export { Collider2DBase } from './components/Collider2DBase';
|
||||
export { BoxCollider2DComponent } from './components/BoxCollider2DComponent';
|
||||
export { CircleCollider2DComponent } from './components/CircleCollider2DComponent';
|
||||
export { CapsuleCollider2DComponent, CapsuleDirection2D } from './components/CapsuleCollider2DComponent';
|
||||
export { PolygonCollider2DComponent } from './components/PolygonCollider2DComponent';
|
||||
|
||||
// World
|
||||
export { Physics2DWorld, type Physics2DWorldState } from './world/Physics2DWorld';
|
||||
|
||||
// Systems
|
||||
export { Physics2DSystem, type Physics2DSystemConfig } from './systems/Physics2DSystem';
|
||||
|
||||
// Services
|
||||
export { Physics2DService } from './services/Physics2DService';
|
||||
|
||||
// Runtime module and plugin
|
||||
export { PhysicsRuntimeModule, PhysicsPlugin } from './PhysicsRuntimeModule';
|
||||
|
||||
// Service tokens
|
||||
export {
|
||||
Physics2DQueryToken,
|
||||
Physics2DSystemToken,
|
||||
Physics2DWorldToken,
|
||||
PhysicsConfigToken,
|
||||
type IPhysics2DQuery,
|
||||
type PhysicsConfig
|
||||
} from './tokens';
|
||||
17
packages/physics/physics-rapier2d/src/runtime/index.ts
Normal file
17
packages/physics/physics-rapier2d/src/runtime/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Physics 2D Runtime Entry
|
||||
* 2D 物理运行时入口
|
||||
*
|
||||
* 包含 WASM 依赖,用于实际运行时环境
|
||||
* Contains WASM dependencies, for actual runtime environment
|
||||
*/
|
||||
|
||||
// Re-export runtime module and plugin with WASM
|
||||
export { PhysicsRuntimeModule, PhysicsPlugin } from '../PhysicsRuntimeModule';
|
||||
|
||||
// Re-export world and system (they have WASM type dependencies)
|
||||
export { Physics2DWorld } from '../world/Physics2DWorld';
|
||||
export type { Physics2DWorldState } from '../world/Physics2DWorld';
|
||||
|
||||
export { Physics2DSystem } from '../systems/Physics2DSystem';
|
||||
export type { Physics2DSystemConfig } from '../systems/Physics2DSystem';
|
||||
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Collision Layer Configuration Service
|
||||
* 碰撞层配置服务
|
||||
*
|
||||
* 管理碰撞层的定义和碰撞矩阵配置
|
||||
*/
|
||||
|
||||
/**
|
||||
* 碰撞层定义
|
||||
*/
|
||||
export interface CollisionLayerDefinition {
|
||||
/** 层索引 (0-15) */
|
||||
index: number;
|
||||
/** 层名称 */
|
||||
name: string;
|
||||
/** 层描述 */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 碰撞层配置
|
||||
*/
|
||||
export interface CollisionLayerSettings {
|
||||
/** 层定义列表 */
|
||||
layers: CollisionLayerDefinition[];
|
||||
/** 碰撞矩阵 (16x16 位图,matrix[i] 表示第 i 层可以与哪些层碰撞) */
|
||||
collisionMatrix: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认碰撞层配置
|
||||
*/
|
||||
export const DEFAULT_COLLISION_LAYERS: CollisionLayerDefinition[] = [
|
||||
{ index: 0, name: 'Default', description: '默认层' },
|
||||
{ index: 1, name: 'Player', description: '玩家' },
|
||||
{ index: 2, name: 'Enemy', description: '敌人' },
|
||||
{ index: 3, name: 'Projectile', description: '投射物' },
|
||||
{ index: 4, name: 'Ground', description: '地面' },
|
||||
{ index: 5, name: 'Platform', description: '平台' },
|
||||
{ index: 6, name: 'Trigger', description: '触发器' },
|
||||
{ index: 7, name: 'Item', description: '物品' },
|
||||
{ index: 8, name: 'Layer8', description: '自定义层8' },
|
||||
{ index: 9, name: 'Layer9', description: '自定义层9' },
|
||||
{ index: 10, name: 'Layer10', description: '自定义层10' },
|
||||
{ index: 11, name: 'Layer11', description: '自定义层11' },
|
||||
{ index: 12, name: 'Layer12', description: '自定义层12' },
|
||||
{ index: 13, name: 'Layer13', description: '自定义层13' },
|
||||
{ index: 14, name: 'Layer14', description: '自定义层14' },
|
||||
{ index: 15, name: 'Layer15', description: '自定义层15' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 默认碰撞矩阵 - 所有层都可以互相碰撞
|
||||
*/
|
||||
export const DEFAULT_COLLISION_MATRIX: number[] = Array(16).fill(0xFFFF);
|
||||
|
||||
/**
|
||||
* 碰撞层配置管理器
|
||||
*/
|
||||
export class CollisionLayerConfig {
|
||||
private static instance: CollisionLayerConfig | null = null;
|
||||
|
||||
private layers: CollisionLayerDefinition[] = [...DEFAULT_COLLISION_LAYERS];
|
||||
private collisionMatrix: number[] = [...DEFAULT_COLLISION_MATRIX];
|
||||
private listeners: Set<() => void> = new Set();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): CollisionLayerConfig {
|
||||
if (!CollisionLayerConfig.instance) {
|
||||
CollisionLayerConfig.instance = new CollisionLayerConfig();
|
||||
}
|
||||
return CollisionLayerConfig.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有层定义
|
||||
*/
|
||||
public getLayers(): readonly CollisionLayerDefinition[] {
|
||||
return this.layers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取层名称
|
||||
*/
|
||||
public getLayerName(index: number): string {
|
||||
if (index < 0 || index >= 16) return `Layer${index}`;
|
||||
return this.layers[index]?.name ?? `Layer${index}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置层名称
|
||||
*/
|
||||
public setLayerName(index: number, name: string): void {
|
||||
if (index < 0 || index >= 16) return;
|
||||
if (this.layers[index]) {
|
||||
this.layers[index].name = name;
|
||||
this.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置层描述
|
||||
*/
|
||||
public setLayerDescription(index: number, description: string): void {
|
||||
if (index < 0 || index >= 16) return;
|
||||
if (this.layers[index]) {
|
||||
this.layers[index].description = description;
|
||||
this.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取碰撞矩阵
|
||||
*/
|
||||
public getCollisionMatrix(): readonly number[] {
|
||||
return this.collisionMatrix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查两个层是否可以碰撞
|
||||
*/
|
||||
public canLayersCollide(layerA: number, layerB: number): boolean {
|
||||
if (layerA < 0 || layerA >= 16 || layerB < 0 || layerB >= 16) {
|
||||
return false;
|
||||
}
|
||||
return (this.collisionMatrix[layerA] & (1 << layerB)) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置两个层是否可以碰撞
|
||||
*/
|
||||
public setLayersCanCollide(layerA: number, layerB: number, canCollide: boolean): void {
|
||||
if (layerA < 0 || layerA >= 16 || layerB < 0 || layerB >= 16) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (canCollide) {
|
||||
this.collisionMatrix[layerA] |= (1 << layerB);
|
||||
this.collisionMatrix[layerB] |= (1 << layerA);
|
||||
} else {
|
||||
this.collisionMatrix[layerA] &= ~(1 << layerB);
|
||||
this.collisionMatrix[layerB] &= ~(1 << layerA);
|
||||
}
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定层的碰撞掩码
|
||||
*/
|
||||
public getLayerMask(layerIndex: number): number {
|
||||
if (layerIndex < 0 || layerIndex >= 16) return 0xFFFF;
|
||||
return this.collisionMatrix[layerIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据层索引获取层位值
|
||||
*/
|
||||
public getLayerBit(layerIndex: number): number {
|
||||
if (layerIndex < 0 || layerIndex >= 16) return 1;
|
||||
return 1 << layerIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从层位值获取层索引
|
||||
*/
|
||||
public getLayerIndex(layerBit: number): number {
|
||||
for (let i = 0; i < 16; i++) {
|
||||
if (layerBit === (1 << i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// 如果是多层位值,返回第一个设置的位
|
||||
for (let i = 0; i < 16; i++) {
|
||||
if ((layerBit & (1 << i)) !== 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载配置
|
||||
*/
|
||||
public loadSettings(settings: Partial<CollisionLayerSettings>): void {
|
||||
if (settings.layers) {
|
||||
this.layers = settings.layers.map((layer, i) => ({
|
||||
index: layer.index ?? i,
|
||||
name: layer.name ?? `Layer${i}`,
|
||||
description: layer.description
|
||||
}));
|
||||
// 确保有16个层
|
||||
while (this.layers.length < 16) {
|
||||
const idx = this.layers.length;
|
||||
this.layers.push({ index: idx, name: `Layer${idx}` });
|
||||
}
|
||||
}
|
||||
if (settings.collisionMatrix) {
|
||||
this.collisionMatrix = [...settings.collisionMatrix];
|
||||
while (this.collisionMatrix.length < 16) {
|
||||
this.collisionMatrix.push(0xFFFF);
|
||||
}
|
||||
}
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出配置
|
||||
*/
|
||||
public exportSettings(): CollisionLayerSettings {
|
||||
return {
|
||||
layers: [...this.layers],
|
||||
collisionMatrix: [...this.collisionMatrix]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置为默认配置
|
||||
*/
|
||||
public resetToDefault(): void {
|
||||
this.layers = [...DEFAULT_COLLISION_LAYERS];
|
||||
this.collisionMatrix = [...DEFAULT_COLLISION_MATRIX];
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加监听器
|
||||
*/
|
||||
public addListener(listener: () => void): void {
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除监听器
|
||||
*/
|
||||
public removeListener(listener: () => void): void {
|
||||
this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
private notifyListeners(): void {
|
||||
for (const listener of this.listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Physics2DService
|
||||
* 2D 物理服务
|
||||
*
|
||||
* 提供全局物理配置和实用方法
|
||||
*/
|
||||
|
||||
import type { IService } from '@esengine/ecs-framework';
|
||||
import type { IVector2 } from '@esengine/ecs-framework-math';
|
||||
import type { Physics2DConfig, RaycastHit2D, OverlapResult2D } from '../types/Physics2DTypes';
|
||||
import { DEFAULT_PHYSICS_CONFIG, CollisionLayer2D } from '../types/Physics2DTypes';
|
||||
import type { Physics2DSystem } from '../systems/Physics2DSystem';
|
||||
|
||||
/**
|
||||
* 2D 物理服务
|
||||
*
|
||||
* 提供场景级别的物理配置和全局查询方法。
|
||||
* 作为服务注册到 ServiceContainer 中。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 从服务容器获取
|
||||
* const physicsService = scene.services.resolve(Physics2DService);
|
||||
*
|
||||
* // 使用射线检测
|
||||
* const hit = physicsService.raycast(origin, direction, 100);
|
||||
* if (hit) {
|
||||
* console.log('Hit entity:', hit.entityId);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class Physics2DService implements IService {
|
||||
private _config: Physics2DConfig = { ...DEFAULT_PHYSICS_CONFIG };
|
||||
private _physicsSystem: Physics2DSystem | null = null;
|
||||
|
||||
/**
|
||||
* 设置物理系统引用
|
||||
* @internal
|
||||
*/
|
||||
public setPhysicsSystem(system: Physics2DSystem): void {
|
||||
this._physicsSystem = system;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取物理系统
|
||||
*/
|
||||
public getPhysicsSystem(): Physics2DSystem | null {
|
||||
return this._physicsSystem;
|
||||
}
|
||||
|
||||
// ==================== 配置 ====================
|
||||
|
||||
/**
|
||||
* 获取物理配置
|
||||
*/
|
||||
public getConfig(): Readonly<Physics2DConfig> {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置重力
|
||||
*/
|
||||
public setGravity(gravity: IVector2): void {
|
||||
this._config.gravity = { ...gravity };
|
||||
this._physicsSystem?.setGravity(gravity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取重力
|
||||
*/
|
||||
public getGravity(): IVector2 {
|
||||
return this._physicsSystem?.getGravity() ?? { ...this._config.gravity };
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置时间步长
|
||||
*/
|
||||
public setTimestep(timestep: number): void {
|
||||
this._config.timestep = timestep;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时间步长
|
||||
*/
|
||||
public getTimestep(): number {
|
||||
return this._config.timestep;
|
||||
}
|
||||
|
||||
// ==================== 查询 ====================
|
||||
|
||||
/**
|
||||
* 射线检测(第一个命中)
|
||||
* @param origin 起点
|
||||
* @param direction 方向(归一化)
|
||||
* @param maxDistance 最大距离
|
||||
* @param collisionMask 碰撞掩码(默认所有层)
|
||||
*/
|
||||
public raycast(
|
||||
origin: IVector2,
|
||||
direction: IVector2,
|
||||
maxDistance: number,
|
||||
collisionMask: number = CollisionLayer2D.All
|
||||
): RaycastHit2D | null {
|
||||
return this._physicsSystem?.raycast(origin, direction, maxDistance, collisionMask) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 射线检测(所有命中)
|
||||
* @param origin 起点
|
||||
* @param direction 方向(归一化)
|
||||
* @param maxDistance 最大距离
|
||||
* @param collisionMask 碰撞掩码(默认所有层)
|
||||
*/
|
||||
public raycastAll(
|
||||
origin: IVector2,
|
||||
direction: IVector2,
|
||||
maxDistance: number,
|
||||
collisionMask: number = CollisionLayer2D.All
|
||||
): RaycastHit2D[] {
|
||||
return this._physicsSystem?.raycastAll(origin, direction, maxDistance, collisionMask) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 点重叠检测
|
||||
* @param point 检测点
|
||||
* @param collisionMask 碰撞掩码
|
||||
*/
|
||||
public overlapPoint(point: IVector2, collisionMask: number = CollisionLayer2D.All): OverlapResult2D {
|
||||
return this._physicsSystem?.overlapPoint(point, collisionMask) ?? { entityIds: [], colliderHandles: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 圆形重叠检测
|
||||
* @param center 圆心
|
||||
* @param radius 半径
|
||||
* @param collisionMask 碰撞掩码
|
||||
*/
|
||||
public overlapCircle(
|
||||
center: IVector2,
|
||||
radius: number,
|
||||
collisionMask: number = CollisionLayer2D.All
|
||||
): OverlapResult2D {
|
||||
return this._physicsSystem?.overlapCircle(center, radius, collisionMask) ?? { entityIds: [], colliderHandles: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 矩形重叠检测
|
||||
* @param center 中心点
|
||||
* @param halfExtents 半宽高
|
||||
* @param rotation 旋转角度
|
||||
* @param collisionMask 碰撞掩码
|
||||
*/
|
||||
public overlapBox(
|
||||
center: IVector2,
|
||||
halfExtents: IVector2,
|
||||
rotation: number = 0,
|
||||
collisionMask: number = CollisionLayer2D.All
|
||||
): OverlapResult2D {
|
||||
return (
|
||||
this._physicsSystem?.overlapBox(center, halfExtents, rotation, collisionMask) ?? {
|
||||
entityIds: [],
|
||||
colliderHandles: []
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 归一化向量
|
||||
*/
|
||||
public normalize(v: IVector2): IVector2 {
|
||||
const length = Math.sqrt(v.x * v.x + v.y * v.y);
|
||||
if (length === 0) return { x: 0, y: 0 };
|
||||
return { x: v.x / length, y: v.y / length };
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点之间的距离
|
||||
*/
|
||||
public distance(a: IVector2, b: IVector2): number {
|
||||
const dx = b.x - a.x;
|
||||
const dy = b.y - a.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算向量长度
|
||||
*/
|
||||
public magnitude(v: IVector2): number {
|
||||
return Math.sqrt(v.x * v.x + v.y * v.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向量点积
|
||||
*/
|
||||
public dot(a: IVector2, b: IVector2): number {
|
||||
return a.x * b.x + a.y * b.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向量叉积(返回标量,2D 特有)
|
||||
*/
|
||||
public cross(a: IVector2, b: IVector2): number {
|
||||
return a.x * b.y - a.y * b.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
this._physicsSystem = null;
|
||||
}
|
||||
}
|
||||
14
packages/physics/physics-rapier2d/src/services/index.ts
Normal file
14
packages/physics/physics-rapier2d/src/services/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Physics 2D Services exports
|
||||
*/
|
||||
|
||||
export { Physics2DService } from './Physics2DService';
|
||||
export {
|
||||
CollisionLayerConfig,
|
||||
DEFAULT_COLLISION_LAYERS,
|
||||
DEFAULT_COLLISION_MATRIX
|
||||
} from './CollisionLayerConfig';
|
||||
export type {
|
||||
CollisionLayerDefinition,
|
||||
CollisionLayerSettings
|
||||
} from './CollisionLayerConfig';
|
||||
565
packages/physics/physics-rapier2d/src/systems/Physics2DSystem.ts
Normal file
565
packages/physics/physics-rapier2d/src/systems/Physics2DSystem.ts
Normal file
@@ -0,0 +1,565 @@
|
||||
/**
|
||||
* Physics2DSystem
|
||||
* 2D 物理系统
|
||||
*
|
||||
* 负责更新物理世界并同步 Transform
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, type Entity } from '@esengine/ecs-framework';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
import { Physics2DWorld } from '../world/Physics2DWorld';
|
||||
import { Rigidbody2DComponent } from '../components/Rigidbody2DComponent';
|
||||
import { Collider2DBase } from '../components/Collider2DBase';
|
||||
import { BoxCollider2DComponent } from '../components/BoxCollider2DComponent';
|
||||
import { CircleCollider2DComponent } from '../components/CircleCollider2DComponent';
|
||||
import { CapsuleCollider2DComponent } from '../components/CapsuleCollider2DComponent';
|
||||
import { PolygonCollider2DComponent } from '../components/PolygonCollider2DComponent';
|
||||
import type { IVector2 } from '@esengine/ecs-framework-math';
|
||||
import type { Physics2DConfig } from '../types/Physics2DTypes';
|
||||
import { PHYSICS_EVENTS, type CollisionEvent2D, type TriggerEvent2D } from '../types/Physics2DEvents';
|
||||
|
||||
/**
|
||||
* 物理系统配置
|
||||
*/
|
||||
export interface Physics2DSystemConfig {
|
||||
/** 物理世界配置 */
|
||||
physics?: Partial<Physics2DConfig>;
|
||||
/** 是否在 lateUpdate 中同步 Transform(用于渲染插值) */
|
||||
interpolateInLateUpdate?: boolean;
|
||||
/** 更新优先级(默认 -1000,保证在其他系统之前更新) */
|
||||
updateOrder?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 2D 物理系统
|
||||
*
|
||||
* 管理物理世界的更新和实体的物理属性同步。
|
||||
*
|
||||
* 职责:
|
||||
* - 初始化和管理 Physics2DWorld
|
||||
* - 同步 Entity Transform 与物理世界
|
||||
* - 应用力和冲量
|
||||
* - 发送碰撞/触发器事件
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 注册物理系统
|
||||
* scene.addEntityProcessor(Physics2DSystem);
|
||||
*
|
||||
* // 或使用自定义配置
|
||||
* const physicsSystem = new Physics2DSystem({
|
||||
* physics: {
|
||||
* gravity: { x: 0, y: -20 }
|
||||
* }
|
||||
* });
|
||||
* scene.addEntityProcessor(physicsSystem);
|
||||
* ```
|
||||
*/
|
||||
export class Physics2DSystem extends EntitySystem {
|
||||
private _world: Physics2DWorld;
|
||||
private _rapierModule: typeof import('@esengine/rapier2d') | null = null;
|
||||
private _rapierInitialized: boolean = false;
|
||||
private _config: Physics2DSystemConfig;
|
||||
|
||||
// 实体到物理对象的映射
|
||||
private _entityBodies: Map<number, { bodyHandle: number; colliderHandles: number[] }> = new Map();
|
||||
|
||||
// 待处理的新实体队列
|
||||
private _pendingEntities: Entity[] = [];
|
||||
|
||||
constructor(config?: Physics2DSystemConfig) {
|
||||
// 匹配所有拥有 Rigidbody2DComponent 的实体
|
||||
super(Matcher.empty().all(Rigidbody2DComponent));
|
||||
|
||||
this._config = {
|
||||
interpolateInLateUpdate: true,
|
||||
updateOrder: -1000,
|
||||
...config
|
||||
};
|
||||
|
||||
this._world = new Physics2DWorld(this._config.physics);
|
||||
this.setUpdateOrder(this._config.updateOrder ?? -1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取物理世界实例
|
||||
*/
|
||||
public get world(): Physics2DWorld {
|
||||
return this._world;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统初始化
|
||||
*/
|
||||
protected override onInitialize(): void {
|
||||
// Rapier 模块由外部通过 initializeWithRapier 注入
|
||||
this.logger.debug('Physics2DSystem initialized, waiting for Rapier module');
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Rapier 模块初始化物理世界
|
||||
*
|
||||
* 必须在系统开始处理前调用此方法
|
||||
*
|
||||
* @param rapier Rapier2D 模块
|
||||
*/
|
||||
public async initializeWithRapier(rapier: typeof import('@esengine/rapier2d')): Promise<void> {
|
||||
if (this._rapierInitialized) {
|
||||
this.logger.warn('Physics2DSystem already initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
this._rapierModule = rapier;
|
||||
await this._world.initialize(rapier);
|
||||
this._rapierInitialized = true;
|
||||
|
||||
// 注册碰撞事件回调
|
||||
this._world.onCollision((event) => this._handleCollisionEvent(event));
|
||||
this._world.onTrigger((event) => this._handleTriggerEvent(event));
|
||||
|
||||
// 处理在初始化前添加的实体
|
||||
for (const entity of this._pendingEntities) {
|
||||
this._createPhysicsBody(entity);
|
||||
}
|
||||
this._pendingEntities = [];
|
||||
|
||||
this.logger.info('Physics2DSystem initialized with Rapier2D');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以处理
|
||||
*/
|
||||
protected override onCheckProcessing(): boolean {
|
||||
return this._rapierInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当实体添加到系统时
|
||||
*/
|
||||
protected override onAdded(entity: Entity): void {
|
||||
if (!this._rapierInitialized) {
|
||||
// 延迟创建物理体,等待 Rapier 初始化
|
||||
this._pendingEntities.push(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
this._createPhysicsBody(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当实体从系统移除时
|
||||
*/
|
||||
protected override onRemoved(entity: Entity): void {
|
||||
this._removePhysicsBody(entity);
|
||||
|
||||
// 从待处理队列中移除(如果存在)
|
||||
const pendingIndex = this._pendingEntities.indexOf(entity);
|
||||
if (pendingIndex >= 0) {
|
||||
this._pendingEntities.splice(pendingIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理更新
|
||||
*/
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
if (!this._rapierInitialized || !this.scene) return;
|
||||
|
||||
const deltaTime = this._getDeltaTime();
|
||||
|
||||
// 发送 pre-step 事件
|
||||
this.scene.eventSystem.emitSync(PHYSICS_EVENTS.PRE_STEP, { deltaTime });
|
||||
|
||||
// 同步 Transform 到物理世界
|
||||
this._syncTransformsToPhysics(entities);
|
||||
|
||||
// 应用待处理的力和冲量
|
||||
this._applyPendingForces(entities);
|
||||
|
||||
// 物理世界步进
|
||||
this._world.step(deltaTime);
|
||||
|
||||
// 同步物理世界到 Transform
|
||||
this._syncPhysicsToTransforms(entities);
|
||||
|
||||
// 发送 post-step 事件
|
||||
this.scene.eventSystem.emitSync(PHYSICS_EVENTS.POST_STEP, { deltaTime });
|
||||
}
|
||||
|
||||
/**
|
||||
* 后期更新(用于渲染插值)
|
||||
*/
|
||||
protected override lateProcess(_entities: readonly Entity[]): void {
|
||||
if (!this._config.interpolateInLateUpdate || !this._rapierInitialized) return;
|
||||
|
||||
// 可在此处实现渲染插值
|
||||
// const alpha = this._world.getAlpha();
|
||||
// 插值逻辑...
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统销毁
|
||||
*/
|
||||
protected override onDestroy(): void {
|
||||
this._world.destroy();
|
||||
this._entityBodies.clear();
|
||||
this._pendingEntities = [];
|
||||
this._rapierInitialized = false;
|
||||
this.logger.info('Physics2DSystem destroyed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置物理系统状态(保持初始化状态,但清除所有物理对象)
|
||||
* 用于场景重载/预览重置
|
||||
*/
|
||||
public reset(): void {
|
||||
this._world.reset();
|
||||
this._entityBodies.clear();
|
||||
this._pendingEntities = [];
|
||||
// 完全重置实体跟踪,强制下次 update 时重新扫描所有实体并触发 onAdded
|
||||
this.resetEntityTracking();
|
||||
this.logger.info('Physics2DSystem reset');
|
||||
}
|
||||
|
||||
// ==================== 物理 API ====================
|
||||
|
||||
/**
|
||||
* 设置重力
|
||||
* @param gravity 重力向量
|
||||
*/
|
||||
public setGravity(gravity: IVector2): void {
|
||||
this._world.setGravity(gravity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取重力
|
||||
*/
|
||||
public getGravity(): IVector2 {
|
||||
return this._world.getGravity();
|
||||
}
|
||||
|
||||
/**
|
||||
* 射线检测
|
||||
*/
|
||||
public raycast(origin: IVector2, direction: IVector2, maxDistance: number, collisionMask?: number) {
|
||||
return this._world.raycast(origin, direction, maxDistance, collisionMask);
|
||||
}
|
||||
|
||||
/**
|
||||
* 射线检测所有
|
||||
*/
|
||||
public raycastAll(origin: IVector2, direction: IVector2, maxDistance: number, collisionMask?: number) {
|
||||
return this._world.raycastAll(origin, direction, maxDistance, collisionMask);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点重叠检测
|
||||
*/
|
||||
public overlapPoint(point: IVector2, collisionMask?: number) {
|
||||
return this._world.overlapPoint(point, collisionMask);
|
||||
}
|
||||
|
||||
/**
|
||||
* 圆形重叠检测
|
||||
*/
|
||||
public overlapCircle(center: IVector2, radius: number, collisionMask?: number) {
|
||||
return this._world.overlapCircle(center, radius, collisionMask);
|
||||
}
|
||||
|
||||
/**
|
||||
* 矩形重叠检测
|
||||
*/
|
||||
public overlapBox(center: IVector2, halfExtents: IVector2, rotation?: number, collisionMask?: number) {
|
||||
return this._world.overlapBox(center, halfExtents, rotation, collisionMask);
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/**
|
||||
* 获取时间增量
|
||||
*/
|
||||
private _getDeltaTime(): number {
|
||||
// TODO: 从全局 Time 服务获取
|
||||
return 1 / 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建物理体
|
||||
*/
|
||||
private _createPhysicsBody(entity: Entity): void {
|
||||
const rigidbody = entity.getComponent(Rigidbody2DComponent);
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
|
||||
if (!rigidbody || !transform) {
|
||||
const missing: string[] = [];
|
||||
if (!rigidbody) missing.push('Rigidbody2DComponent');
|
||||
if (!transform) missing.push('TransformComponent');
|
||||
this.logger.warn(`Entity ${entity.name} missing required components: ${missing.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取位置和旋转
|
||||
const position: IVector2 = {
|
||||
x: transform.position.x,
|
||||
y: transform.position.y
|
||||
};
|
||||
const rotation = transform.rotation.z;
|
||||
|
||||
// 创建刚体
|
||||
const bodyHandle = this._world.createBody(entity.id, rigidbody, position, rotation);
|
||||
if (bodyHandle === null) {
|
||||
this.logger.error(`Failed to create physics body for entity ${entity.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 收集并创建碰撞体
|
||||
const colliderHandles: number[] = [];
|
||||
const colliders = this._getColliders(entity);
|
||||
const scale: IVector2 = { x: transform.scale.x, y: transform.scale.y };
|
||||
|
||||
for (const collider of colliders) {
|
||||
const colliderHandle = this._world.createCollider(entity.id, collider, bodyHandle, scale);
|
||||
if (colliderHandle !== null) {
|
||||
colliderHandles.push(colliderHandle);
|
||||
}
|
||||
}
|
||||
|
||||
// 记录映射
|
||||
this._entityBodies.set(entity.id, { bodyHandle, colliderHandles });
|
||||
|
||||
// 存储初始位置用于插值
|
||||
rigidbody._previousPosition = { ...position };
|
||||
rigidbody._previousRotation = rotation;
|
||||
rigidbody._needsSync = false;
|
||||
|
||||
this.logger.debug(`Created physics body for entity ${entity.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除物理体
|
||||
*/
|
||||
private _removePhysicsBody(entity: Entity): void {
|
||||
const mapping = this._entityBodies.get(entity.id);
|
||||
if (!mapping) return;
|
||||
|
||||
// 移除碰撞体
|
||||
for (const colliderHandle of mapping.colliderHandles) {
|
||||
this._world.removeCollider(colliderHandle);
|
||||
}
|
||||
|
||||
// 移除刚体
|
||||
this._world.removeBody(mapping.bodyHandle);
|
||||
|
||||
// 清除映射
|
||||
this._entityBodies.delete(entity.id);
|
||||
|
||||
this.logger.debug(`Removed physics body for entity ${entity.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步 Transform 到物理世界
|
||||
*/
|
||||
private _syncTransformsToPhysics(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const rigidbody = entity.getComponent(Rigidbody2DComponent);
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
const mapping = this._entityBodies.get(entity.id);
|
||||
|
||||
if (!rigidbody || !transform || !mapping) continue;
|
||||
|
||||
// 只有当需要同步时才更新物理世界
|
||||
if (rigidbody._needsSync) {
|
||||
const position: IVector2 = {
|
||||
x: transform.position.x,
|
||||
y: transform.position.y
|
||||
};
|
||||
const rotation = transform.rotation.z;
|
||||
|
||||
this._world.setBodyTransform(mapping.bodyHandle, position, rotation);
|
||||
rigidbody._needsSync = false;
|
||||
}
|
||||
|
||||
// 检查碰撞体是否需要重建
|
||||
const colliders = this._getColliders(entity);
|
||||
const scale: IVector2 = { x: transform.scale.x, y: transform.scale.y };
|
||||
for (const collider of colliders) {
|
||||
if (collider._needsRebuild) {
|
||||
// 移除旧碰撞体
|
||||
if (collider._colliderHandle !== null) {
|
||||
this._world.removeCollider(collider._colliderHandle);
|
||||
const handleIndex = mapping.colliderHandles.indexOf(collider._colliderHandle);
|
||||
if (handleIndex >= 0) {
|
||||
mapping.colliderHandles.splice(handleIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新碰撞体
|
||||
const newHandle = this._world.createCollider(entity.id, collider, mapping.bodyHandle, scale);
|
||||
if (newHandle !== null) {
|
||||
mapping.colliderHandles.push(newHandle);
|
||||
}
|
||||
|
||||
collider._needsRebuild = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用待处理的力和冲量
|
||||
*/
|
||||
private _applyPendingForces(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const rigidbody = entity.getComponent(Rigidbody2DComponent);
|
||||
const mapping = this._entityBodies.get(entity.id);
|
||||
|
||||
if (!rigidbody || !mapping) continue;
|
||||
|
||||
const bodyHandle = mapping.bodyHandle;
|
||||
|
||||
// 应用力
|
||||
if (rigidbody._pendingForce.x !== 0 || rigidbody._pendingForce.y !== 0) {
|
||||
this._world.applyForce(bodyHandle, rigidbody._pendingForce);
|
||||
}
|
||||
|
||||
// 应用冲量
|
||||
if (rigidbody._pendingImpulse.x !== 0 || rigidbody._pendingImpulse.y !== 0) {
|
||||
this._world.applyImpulse(bodyHandle, rigidbody._pendingImpulse);
|
||||
}
|
||||
|
||||
// 应用扭矩
|
||||
if (rigidbody._pendingTorque !== 0) {
|
||||
this._world.applyTorque(bodyHandle, rigidbody._pendingTorque);
|
||||
}
|
||||
|
||||
// 应用角冲量
|
||||
if (rigidbody._pendingAngularImpulse !== 0) {
|
||||
this._world.applyAngularImpulse(bodyHandle, rigidbody._pendingAngularImpulse);
|
||||
}
|
||||
|
||||
// 设置目标速度
|
||||
if (rigidbody._hasTargetVelocity) {
|
||||
this._world.setVelocity(bodyHandle, rigidbody._targetVelocity);
|
||||
}
|
||||
|
||||
// 设置目标角速度
|
||||
if (rigidbody._hasTargetAngularVelocity) {
|
||||
this._world.setAngularVelocity(bodyHandle, rigidbody._targetAngularVelocity);
|
||||
}
|
||||
|
||||
// 唤醒/休眠
|
||||
if (rigidbody._shouldWakeUp) {
|
||||
this._world.wakeUp(bodyHandle);
|
||||
}
|
||||
if (rigidbody._shouldSleep) {
|
||||
this._world.sleep(bodyHandle);
|
||||
}
|
||||
|
||||
// 清除待处理状态
|
||||
rigidbody._clearPendingForces();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步物理世界到 Transform
|
||||
*/
|
||||
private _syncPhysicsToTransforms(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const rigidbody = entity.getComponent(Rigidbody2DComponent);
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
const mapping = this._entityBodies.get(entity.id);
|
||||
|
||||
if (!rigidbody || !transform || !mapping) continue;
|
||||
|
||||
// 存储上一帧位置用于插值
|
||||
rigidbody._previousPosition = {
|
||||
x: transform.position.x,
|
||||
y: transform.position.y
|
||||
};
|
||||
rigidbody._previousRotation = transform.rotation.z;
|
||||
|
||||
// 从物理世界获取新位置
|
||||
const newPosition = this._world.getBodyPosition(mapping.bodyHandle);
|
||||
const newRotation = this._world.getBodyRotation(mapping.bodyHandle);
|
||||
const newVelocity = this._world.getBodyVelocity(mapping.bodyHandle);
|
||||
const newAngularVelocity = this._world.getBodyAngularVelocity(mapping.bodyHandle);
|
||||
|
||||
if (newPosition) {
|
||||
transform.position.x = newPosition.x;
|
||||
transform.position.y = newPosition.y;
|
||||
}
|
||||
|
||||
if (newRotation !== null) {
|
||||
transform.rotation.z = newRotation;
|
||||
}
|
||||
|
||||
if (newVelocity) {
|
||||
rigidbody.velocity = newVelocity;
|
||||
}
|
||||
|
||||
if (newAngularVelocity !== null) {
|
||||
rigidbody.angularVelocity = newAngularVelocity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理碰撞事件
|
||||
*/
|
||||
private _handleCollisionEvent(event: CollisionEvent2D): void {
|
||||
if (!this.scene) return;
|
||||
|
||||
let eventName: string;
|
||||
switch (event.type) {
|
||||
case 'enter':
|
||||
eventName = PHYSICS_EVENTS.COLLISION_ENTER;
|
||||
break;
|
||||
case 'stay':
|
||||
eventName = PHYSICS_EVENTS.COLLISION_STAY;
|
||||
break;
|
||||
case 'exit':
|
||||
eventName = PHYSICS_EVENTS.COLLISION_EXIT;
|
||||
break;
|
||||
}
|
||||
|
||||
this.scene.eventSystem.emitSync(eventName, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理触发器事件
|
||||
*/
|
||||
private _handleTriggerEvent(event: TriggerEvent2D): void {
|
||||
if (!this.scene) return;
|
||||
|
||||
let eventName: string;
|
||||
switch (event.type) {
|
||||
case 'enter':
|
||||
eventName = PHYSICS_EVENTS.TRIGGER_ENTER;
|
||||
break;
|
||||
case 'stay':
|
||||
eventName = PHYSICS_EVENTS.TRIGGER_STAY;
|
||||
break;
|
||||
case 'exit':
|
||||
eventName = PHYSICS_EVENTS.TRIGGER_EXIT;
|
||||
break;
|
||||
}
|
||||
|
||||
this.scene.eventSystem.emitSync(eventName, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体上的所有碰撞体组件
|
||||
* @param entity 实体
|
||||
*/
|
||||
private _getColliders(entity: Entity): Collider2DBase[] {
|
||||
const colliders: Collider2DBase[] = [];
|
||||
|
||||
// 收集所有类型的碰撞体
|
||||
colliders.push(...entity.getComponents(BoxCollider2DComponent));
|
||||
colliders.push(...entity.getComponents(CircleCollider2DComponent));
|
||||
colliders.push(...entity.getComponents(CapsuleCollider2DComponent));
|
||||
colliders.push(...entity.getComponents(PolygonCollider2DComponent));
|
||||
|
||||
return colliders;
|
||||
}
|
||||
}
|
||||
5
packages/physics/physics-rapier2d/src/systems/index.ts
Normal file
5
packages/physics/physics-rapier2d/src/systems/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Physics 2D Systems exports
|
||||
*/
|
||||
|
||||
export { Physics2DSystem, type Physics2DSystemConfig } from './Physics2DSystem';
|
||||
213
packages/physics/physics-rapier2d/src/tokens.ts
Normal file
213
packages/physics/physics-rapier2d/src/tokens.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* 物理模块服务令牌
|
||||
* Physics module service tokens
|
||||
*
|
||||
* 定义 physics-rapier2d 模块导出的服务令牌和接口。
|
||||
* 谁定义接口,谁导出 Token。
|
||||
*
|
||||
* Defines service tokens and interfaces exported by physics-rapier2d module.
|
||||
* Who defines the interface, who exports the Token.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 消费方导入 Token | Consumer imports Token
|
||||
* import { Physics2DQueryToken, type IPhysics2DQuery } from '@esengine/physics-rapier2d';
|
||||
*
|
||||
* // 获取服务 | Get service
|
||||
* const physicsQuery = context.services.get(Physics2DQueryToken);
|
||||
* if (physicsQuery) {
|
||||
* physicsQuery.raycast(...);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { createServiceToken } from '@esengine/ecs-framework';
|
||||
import type { Physics2DSystem } from './systems/Physics2DSystem';
|
||||
|
||||
// ============================================================================
|
||||
// 共享物理接口 | Shared Physics Interfaces
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 2D 物理查询接口
|
||||
* 2D Physics query interface
|
||||
*
|
||||
* 跨模块共享的物理查询契约。
|
||||
* 由 Physics2DWorld 实现,粒子等模块可选依赖。
|
||||
*
|
||||
* Cross-module shared physics query contract.
|
||||
* Implemented by Physics2DWorld, optionally depended by particle and other modules.
|
||||
*/
|
||||
export interface IPhysics2DQuery {
|
||||
/**
|
||||
* 圆形区域检测
|
||||
* Circle overlap detection
|
||||
*
|
||||
* @param center 圆心 | Circle center
|
||||
* @param radius 半径 | Radius
|
||||
* @param collisionMask 碰撞掩码 | Collision mask
|
||||
* @returns 检测结果 | Detection result
|
||||
*/
|
||||
overlapCircle(
|
||||
center: { x: number; y: number },
|
||||
radius: number,
|
||||
collisionMask?: number
|
||||
): { entityIds: number[]; colliderHandles: number[] };
|
||||
|
||||
/**
|
||||
* 射线检测
|
||||
* Raycast detection
|
||||
*
|
||||
* @param origin 起点 | Origin point
|
||||
* @param direction 方向(归一化)| Direction (normalized)
|
||||
* @param maxDistance 最大距离 | Max distance
|
||||
* @param collisionMask 碰撞掩码 | Collision mask
|
||||
* @returns 命中结果或 null | Hit result or null
|
||||
*/
|
||||
raycast(
|
||||
origin: { x: number; y: number },
|
||||
direction: { x: number; y: number },
|
||||
maxDistance: number,
|
||||
collisionMask?: number
|
||||
): {
|
||||
entityId: number;
|
||||
point: { x: number; y: number };
|
||||
normal: { x: number; y: number };
|
||||
distance: number;
|
||||
colliderHandle: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 2D 物理世界接口
|
||||
* 2D Physics world interface
|
||||
*
|
||||
* 跨模块共享的物理世界契约。
|
||||
* 由 Physics2DWorld 实现,tilemap 等模块可选依赖。
|
||||
*
|
||||
* Cross-module shared physics world contract.
|
||||
* Implemented by Physics2DWorld, optionally depended by tilemap and other modules.
|
||||
*/
|
||||
export interface IPhysics2DWorld {
|
||||
/**
|
||||
* 创建静态碰撞体
|
||||
* Create static collider
|
||||
*
|
||||
* @param entityId 实体 ID | Entity ID
|
||||
* @param position 碰撞体中心位置 | Collider center position
|
||||
* @param halfExtents 半宽高 | Half extents
|
||||
* @param collisionLayer 碰撞层 | Collision layer
|
||||
* @param collisionMask 碰撞掩码 | Collision mask
|
||||
* @param friction 摩擦系数 | Friction coefficient
|
||||
* @param restitution 弹性系数 | Restitution coefficient
|
||||
* @param isTrigger 是否为触发器 | Whether is trigger
|
||||
* @returns 碰撞体句柄或 null | Collider handle or null
|
||||
*/
|
||||
createStaticCollider(
|
||||
entityId: number,
|
||||
position: { x: number; y: number },
|
||||
halfExtents: { x: number; y: number },
|
||||
collisionLayer: number,
|
||||
collisionMask: number,
|
||||
friction: number,
|
||||
restitution: number,
|
||||
isTrigger: boolean
|
||||
): number | null;
|
||||
|
||||
/**
|
||||
* 移除碰撞体
|
||||
* Remove collider
|
||||
*
|
||||
* @param handle 碰撞体句柄 | Collider handle
|
||||
*/
|
||||
removeCollider(handle: number): void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 物理模块特有的接口 | Physics module specific interfaces
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 物理配置
|
||||
* Physics configuration
|
||||
*/
|
||||
export interface PhysicsConfig {
|
||||
gravity?: { x: number; y: number };
|
||||
timestep?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 碰撞层配置接口
|
||||
* Collision layer config interface
|
||||
*
|
||||
* 跨模块共享的碰撞层配置契约。
|
||||
* Cross-module shared collision layer config contract.
|
||||
*/
|
||||
export interface ICollisionLayerConfig {
|
||||
/**
|
||||
* 获取所有层定义
|
||||
* Get all layer definitions
|
||||
*/
|
||||
getLayers(): ReadonlyArray<{ name: string }>;
|
||||
|
||||
/**
|
||||
* 添加监听器
|
||||
* Add listener
|
||||
*/
|
||||
addListener(callback: () => void): void;
|
||||
|
||||
/**
|
||||
* 移除监听器
|
||||
* Remove listener
|
||||
*/
|
||||
removeListener(callback: () => void): void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 服务令牌 | Service Tokens
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 2D 物理查询服务令牌
|
||||
* 2D Physics query service token
|
||||
*
|
||||
* 用于获取物理查询能力(射线检测、区域检测等)。
|
||||
* For getting physics query capabilities (raycast, overlap detection, etc.).
|
||||
*/
|
||||
export const Physics2DQueryToken = createServiceToken<IPhysics2DQuery>('physics2DQuery');
|
||||
|
||||
/**
|
||||
* 2D 物理世界服务令牌
|
||||
* 2D Physics world service token
|
||||
*
|
||||
* 用于获取物理世界实例(需要底层访问时使用)。
|
||||
* For getting physics world instance (when low-level access is needed).
|
||||
*/
|
||||
export const Physics2DWorldToken = createServiceToken<IPhysics2DWorld>('physics2DWorld');
|
||||
|
||||
/**
|
||||
* 物理系统令牌
|
||||
* Physics system token
|
||||
*
|
||||
* 用于获取完整的物理系统实例。
|
||||
* For getting the full physics system instance.
|
||||
*/
|
||||
export const Physics2DSystemToken = createServiceToken<Physics2DSystem>('physics2DSystem');
|
||||
|
||||
/**
|
||||
* 物理配置令牌
|
||||
* Physics config token
|
||||
*
|
||||
* 用于传入物理配置(如重力、时间步)。
|
||||
* For passing physics configuration (gravity, timestep, etc.).
|
||||
*/
|
||||
export const PhysicsConfigToken = createServiceToken<PhysicsConfig>('physicsConfig');
|
||||
|
||||
/**
|
||||
* 碰撞层配置令牌
|
||||
* Collision layer config token
|
||||
*
|
||||
* 用于获取碰撞层配置服务。
|
||||
* For getting collision layer config service.
|
||||
*/
|
||||
export const CollisionLayerConfigToken = createServiceToken<ICollisionLayerConfig>('collisionLayerConfig');
|
||||
104
packages/physics/physics-rapier2d/src/types/Physics2DEvents.ts
Normal file
104
packages/physics/physics-rapier2d/src/types/Physics2DEvents.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Physics 2D Events
|
||||
* 2D 物理事件定义
|
||||
*/
|
||||
|
||||
import type { IVector2 } from '@esengine/ecs-framework-math';
|
||||
|
||||
/**
|
||||
* 碰撞事件类型
|
||||
*/
|
||||
export type CollisionEventType = 'enter' | 'stay' | 'exit';
|
||||
|
||||
/**
|
||||
* 触发器事件类型
|
||||
*/
|
||||
export type TriggerEventType = 'enter' | 'stay' | 'exit';
|
||||
|
||||
/**
|
||||
* 碰撞接触点信息
|
||||
*/
|
||||
export interface ContactPoint2D {
|
||||
/** 接触点位置 */
|
||||
point: IVector2;
|
||||
/** 接触点法线 */
|
||||
normal: IVector2;
|
||||
/** 穿透深度 */
|
||||
penetration: number;
|
||||
/** 冲量大小 */
|
||||
impulse: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 碰撞事件数据
|
||||
*/
|
||||
export interface CollisionEvent2D {
|
||||
/** 事件类型 */
|
||||
type: CollisionEventType;
|
||||
/** 实体 A 的 ID */
|
||||
entityA: number;
|
||||
/** 实体 B 的 ID */
|
||||
entityB: number;
|
||||
/** 碰撞体 A 的句柄 */
|
||||
colliderHandleA: number;
|
||||
/** 碰撞体 B 的句柄 */
|
||||
colliderHandleB: number;
|
||||
/** 接触点列表(仅在 enter 和 stay 时有效) */
|
||||
contacts: ContactPoint2D[];
|
||||
/** 相对速度 */
|
||||
relativeVelocity: IVector2;
|
||||
/** 总冲量大小 */
|
||||
totalImpulse: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发器事件数据
|
||||
*/
|
||||
export interface TriggerEvent2D {
|
||||
/** 事件类型 */
|
||||
type: TriggerEventType;
|
||||
/** 触发器实体 ID */
|
||||
triggerEntityId: number;
|
||||
/** 进入触发器的实体 ID */
|
||||
otherEntityId: number;
|
||||
/** 触发器碰撞体句柄 */
|
||||
triggerColliderHandle: number;
|
||||
/** 其他碰撞体句柄 */
|
||||
otherColliderHandle: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理事件名称常量
|
||||
*/
|
||||
export const PHYSICS_EVENTS = {
|
||||
/** 碰撞开始 */
|
||||
COLLISION_ENTER: 'physics2d:collision-enter',
|
||||
/** 碰撞持续 */
|
||||
COLLISION_STAY: 'physics2d:collision-stay',
|
||||
/** 碰撞结束 */
|
||||
COLLISION_EXIT: 'physics2d:collision-exit',
|
||||
/** 触发器进入 */
|
||||
TRIGGER_ENTER: 'physics2d:trigger-enter',
|
||||
/** 触发器持续 */
|
||||
TRIGGER_STAY: 'physics2d:trigger-stay',
|
||||
/** 触发器离开 */
|
||||
TRIGGER_EXIT: 'physics2d:trigger-exit',
|
||||
/** 物理世界步进前 */
|
||||
PRE_STEP: 'physics2d:pre-step',
|
||||
/** 物理世界步进后 */
|
||||
POST_STEP: 'physics2d:post-step'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 物理事件映射类型
|
||||
*/
|
||||
export interface Physics2DEventMap {
|
||||
[PHYSICS_EVENTS.COLLISION_ENTER]: CollisionEvent2D;
|
||||
[PHYSICS_EVENTS.COLLISION_STAY]: CollisionEvent2D;
|
||||
[PHYSICS_EVENTS.COLLISION_EXIT]: CollisionEvent2D;
|
||||
[PHYSICS_EVENTS.TRIGGER_ENTER]: TriggerEvent2D;
|
||||
[PHYSICS_EVENTS.TRIGGER_STAY]: TriggerEvent2D;
|
||||
[PHYSICS_EVENTS.TRIGGER_EXIT]: TriggerEvent2D;
|
||||
[PHYSICS_EVENTS.PRE_STEP]: { deltaTime: number };
|
||||
[PHYSICS_EVENTS.POST_STEP]: { deltaTime: number };
|
||||
}
|
||||
178
packages/physics/physics-rapier2d/src/types/Physics2DTypes.ts
Normal file
178
packages/physics/physics-rapier2d/src/types/Physics2DTypes.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Physics 2D Types
|
||||
* 2D 物理引擎类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 刚体类型
|
||||
*/
|
||||
export enum RigidbodyType2D {
|
||||
/** 动态刚体,受力和碰撞影响 */
|
||||
Dynamic = 0,
|
||||
/** 运动学刚体,手动控制位置,不受力影响 */
|
||||
Kinematic = 1,
|
||||
/** 静态刚体,不移动,用于地形等 */
|
||||
Static = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* 碰撞检测模式
|
||||
*/
|
||||
export enum CollisionDetectionMode2D {
|
||||
/** 离散检测,性能好但可能穿透 */
|
||||
Discrete = 0,
|
||||
/** 连续检测,防止高速物体穿透 */
|
||||
Continuous = 1
|
||||
}
|
||||
|
||||
// 使用 IVector2 接口 | Use IVector2 interface
|
||||
import type { IVector2 } from '@esengine/ecs-framework-math';
|
||||
|
||||
/**
|
||||
* 物理配置
|
||||
*/
|
||||
export interface Physics2DConfig {
|
||||
/** 重力向量 */
|
||||
gravity: IVector2;
|
||||
/** 固定时间步长(秒) */
|
||||
timestep: number;
|
||||
/** 每帧最大子步数 */
|
||||
maxSubsteps: number;
|
||||
/** 速度求解器迭代次数 */
|
||||
velocityIterations: number;
|
||||
/** 位置求解器迭代次数 */
|
||||
positionIterations: number;
|
||||
/** 是否启用休眠 */
|
||||
allowSleep: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认物理配置
|
||||
*/
|
||||
export const DEFAULT_PHYSICS_CONFIG: Physics2DConfig = {
|
||||
gravity: { x: 0, y: -9.81 },
|
||||
timestep: 1 / 60,
|
||||
maxSubsteps: 8,
|
||||
velocityIterations: 4,
|
||||
positionIterations: 1,
|
||||
allowSleep: true
|
||||
};
|
||||
|
||||
/**
|
||||
* 碰撞层定义
|
||||
*/
|
||||
export enum CollisionLayer2D {
|
||||
Default = 1 << 0,
|
||||
Player = 1 << 1,
|
||||
Enemy = 1 << 2,
|
||||
Projectile = 1 << 3,
|
||||
Ground = 1 << 4,
|
||||
Platform = 1 << 5,
|
||||
Trigger = 1 << 6,
|
||||
All = 0xFFFF
|
||||
}
|
||||
|
||||
/**
|
||||
* 力的模式
|
||||
*/
|
||||
export enum ForceMode2D {
|
||||
/** 持续力(考虑质量) */
|
||||
Force = 0,
|
||||
/** 瞬时冲量(考虑质量) */
|
||||
Impulse = 1,
|
||||
/** 直接设置速度变化(不考虑质量) */
|
||||
VelocityChange = 2,
|
||||
/** 持续加速度(不考虑质量) */
|
||||
Acceleration = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* 射线检测结果
|
||||
*/
|
||||
export interface RaycastHit2D {
|
||||
/** 命中的实体 ID */
|
||||
entityId: number;
|
||||
/** 命中点 */
|
||||
point: IVector2;
|
||||
/** 命中面的法线 */
|
||||
normal: IVector2;
|
||||
/** 射线起点到命中点的距离 */
|
||||
distance: number;
|
||||
/** 命中的碰撞体句柄 */
|
||||
colliderHandle: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 形状投射结果
|
||||
*/
|
||||
export interface ShapeCastHit2D extends RaycastHit2D {
|
||||
/** 投射开始时与命中物体的穿透深度 */
|
||||
penetration: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重叠检测结果
|
||||
*/
|
||||
export interface OverlapResult2D {
|
||||
/** 重叠的实体 ID 列表 */
|
||||
entityIds: number[];
|
||||
/** 重叠的碰撞体句柄列表 */
|
||||
colliderHandles: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理材质预设
|
||||
*/
|
||||
export enum PhysicsMaterial2DPreset {
|
||||
/** 默认材质 */
|
||||
Default = 0,
|
||||
/** 弹性材质 */
|
||||
Bouncy = 1,
|
||||
/** 光滑材质(低摩擦) */
|
||||
Slippery = 2,
|
||||
/** 粘性材质(高摩擦) */
|
||||
Sticky = 3,
|
||||
/** 金属材质 */
|
||||
Metal = 4,
|
||||
/** 橡胶材质 */
|
||||
Rubber = 5
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预设材质参数
|
||||
*/
|
||||
export function getPhysicsMaterialPreset(preset: PhysicsMaterial2DPreset): { friction: number; restitution: number } {
|
||||
switch (preset) {
|
||||
case PhysicsMaterial2DPreset.Bouncy:
|
||||
return { friction: 0.2, restitution: 0.9 };
|
||||
case PhysicsMaterial2DPreset.Slippery:
|
||||
return { friction: 0.05, restitution: 0.1 };
|
||||
case PhysicsMaterial2DPreset.Sticky:
|
||||
return { friction: 1.0, restitution: 0.0 };
|
||||
case PhysicsMaterial2DPreset.Metal:
|
||||
return { friction: 0.4, restitution: 0.3 };
|
||||
case PhysicsMaterial2DPreset.Rubber:
|
||||
return { friction: 0.8, restitution: 0.7 };
|
||||
case PhysicsMaterial2DPreset.Default:
|
||||
default:
|
||||
return { friction: 0.5, restitution: 0.0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关节类型
|
||||
*/
|
||||
export enum JointType2D {
|
||||
/** 固定关节 */
|
||||
Fixed = 0,
|
||||
/** 铰链关节(旋转) */
|
||||
Revolute = 1,
|
||||
/** 棱柱关节(滑动) */
|
||||
Prismatic = 2,
|
||||
/** 弹簧关节 */
|
||||
Spring = 3,
|
||||
/** 绳索关节 */
|
||||
Rope = 4,
|
||||
/** 距离关节 */
|
||||
Distance = 5
|
||||
}
|
||||
31
packages/physics/physics-rapier2d/src/types/index.ts
Normal file
31
packages/physics/physics-rapier2d/src/types/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Physics 2D Types exports
|
||||
*/
|
||||
|
||||
export {
|
||||
RigidbodyType2D,
|
||||
CollisionDetectionMode2D,
|
||||
type Physics2DConfig,
|
||||
DEFAULT_PHYSICS_CONFIG,
|
||||
CollisionLayer2D,
|
||||
ForceMode2D,
|
||||
type RaycastHit2D,
|
||||
type ShapeCastHit2D,
|
||||
type OverlapResult2D,
|
||||
PhysicsMaterial2DPreset,
|
||||
getPhysicsMaterialPreset,
|
||||
JointType2D
|
||||
} from './Physics2DTypes';
|
||||
|
||||
// Re-export IVector2 for type compatibility
|
||||
export type { IVector2 } from '@esengine/ecs-framework-math';
|
||||
|
||||
export {
|
||||
type CollisionEventType,
|
||||
type TriggerEventType,
|
||||
type ContactPoint2D,
|
||||
type CollisionEvent2D,
|
||||
type TriggerEvent2D,
|
||||
PHYSICS_EVENTS,
|
||||
type Physics2DEventMap
|
||||
} from './Physics2DEvents';
|
||||
1206
packages/physics/physics-rapier2d/src/world/Physics2DWorld.ts
Normal file
1206
packages/physics/physics-rapier2d/src/world/Physics2DWorld.ts
Normal file
File diff suppressed because it is too large
Load Diff
5
packages/physics/physics-rapier2d/src/world/index.ts
Normal file
5
packages/physics/physics-rapier2d/src/world/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Physics 2D World exports
|
||||
*/
|
||||
|
||||
export { Physics2DWorld, type Physics2DWorldState } from './Physics2DWorld';
|
||||
23
packages/physics/physics-rapier2d/tsconfig.build.json
Normal file
23
packages/physics/physics-rapier2d/tsconfig.build.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
45
packages/physics/physics-rapier2d/tsconfig.json
Normal file
45
packages/physics/physics-rapier2d/tsconfig.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM"
|
||||
],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"plugin.json"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../framework/core"
|
||||
},
|
||||
{
|
||||
"path": "../../engine/engine-core"
|
||||
}
|
||||
]
|
||||
}
|
||||
25
packages/physics/physics-rapier2d/tsup.config.ts
Normal file
25
packages/physics/physics-rapier2d/tsup.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { STANDARD_EXTERNALS } from '../../tools/build-config/src/types';
|
||||
|
||||
// Physics-rapier2d keeps runtime entry for WASM loading
|
||||
// Chunks are shared between index and runtime entries
|
||||
// 保留 chunk 分割,index 和 runtime 入口共享代码
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
index: 'src/index.ts',
|
||||
runtime: 'src/runtime.ts'
|
||||
},
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
tsconfig: 'tsconfig.build.json',
|
||||
// 使用标准外部依赖列表,确保所有 @esengine/* 包都被外部化
|
||||
// 这避免了类被重复打包导致 instanceof 检查失败的问题
|
||||
external: [
|
||||
...STANDARD_EXTERNALS,
|
||||
],
|
||||
esbuildOptions(options) {
|
||||
options.jsx = 'automatic';
|
||||
}
|
||||
});
|
||||
38
packages/physics/rapier2d/module.json
Normal file
38
packages/physics/rapier2d/module.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "rapier2d",
|
||||
"name": "@esengine/rapier2d",
|
||||
"displayName": "Rapier2D",
|
||||
"description": "Rapier2D physics engine WASM bindings | Rapier2D 物理引擎 WASM 绑定",
|
||||
"version": "0.14.0",
|
||||
"category": "Physics",
|
||||
"icon": "Atom",
|
||||
"tags": [
|
||||
"physics",
|
||||
"wasm",
|
||||
"rapier"
|
||||
],
|
||||
"isCore": false,
|
||||
"defaultEnabled": false,
|
||||
"isEngineModule": true,
|
||||
"canContainContent": false,
|
||||
"platforms": [
|
||||
"web",
|
||||
"desktop"
|
||||
],
|
||||
"dependencies": [],
|
||||
"exports": {
|
||||
"other": ["RAPIER"]
|
||||
},
|
||||
"requiresWasm": true,
|
||||
"wasmConfig": {
|
||||
"files": [
|
||||
{
|
||||
"src": ["rapier2d/pkg/rapier_wasm2d_bg.wasm", "rapier2d/rapier_wasm2d_bg.wasm"],
|
||||
"dst": "wasm/rapier_wasm2d_bg.wasm"
|
||||
}
|
||||
],
|
||||
"runtimePath": "wasm/rapier_wasm2d_bg.wasm"
|
||||
},
|
||||
"outputPath": "dist/index.js",
|
||||
"isExternalDependency": true
|
||||
}
|
||||
31
packages/physics/rapier2d/package.json
Normal file
31
packages/physics/rapier2d/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@esengine/rapier2d",
|
||||
"version": "0.14.0",
|
||||
"description": "Rapier2D physics engine with dynamic WASM loading support",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./pkg/*": "./pkg/*"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"pkg/*.wasm"
|
||||
],
|
||||
"scripts": {
|
||||
"gen:src": "node scripts/gen-src.mjs",
|
||||
"build": "pnpm gen:src && tsup",
|
||||
"clean": "rimraf dist src"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"rimraf": "^5.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
125
packages/physics/rapier2d/scripts/gen-src.mjs
Normal file
125
packages/physics/rapier2d/scripts/gen-src.mjs
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Generate 2D-specific source code from rapier.js source.
|
||||
* 从 rapier.js 源码生成 2D 专用代码。
|
||||
*
|
||||
* This script:
|
||||
* 1. Copies TypeScript source from rapier.js/src.ts
|
||||
* 2. Removes #if DIM3 ... #endif blocks (keeps only 2D code)
|
||||
* 3. Overwrites raw.ts and init.ts with 2D-specific versions
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, readdirSync, mkdirSync, cpSync, existsSync, renameSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const packageRoot = join(__dirname, '..');
|
||||
const rapierRoot = join(packageRoot, '..', '..', 'thirdparty', 'rapier.js');
|
||||
const srcTsDir = join(rapierRoot, 'src.ts');
|
||||
const src2dDir = join(rapierRoot, 'rapier-compat', 'src2d');
|
||||
const outputDir = join(packageRoot, 'src');
|
||||
|
||||
// Check if rapier.js exists
|
||||
if (!existsSync(srcTsDir)) {
|
||||
console.error(`Error: rapier.js source not found at ${rapierRoot}`);
|
||||
console.error('Please clone https://github.com/esengine/rapier.js.git to thirdparty/rapier.js');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove #if DIM3 ... #endif blocks from source code
|
||||
*/
|
||||
function removeDim3Blocks(content) {
|
||||
// Remove lines between #if DIM3 and #endif (inclusive)
|
||||
const lines = content.split('\n');
|
||||
const result = [];
|
||||
let skipDepth = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed.startsWith('//#if DIM3') || trimmed.startsWith('// #if DIM3')) {
|
||||
skipDepth++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (skipDepth > 0 && (trimmed.startsWith('//#endif') || trimmed.startsWith('// #endif'))) {
|
||||
skipDepth--;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (skipDepth === 0) {
|
||||
// Also remove #if DIM2 and its #endif (but keep the content)
|
||||
if (trimmed.startsWith('//#if DIM2') || trimmed.startsWith('// #if DIM2')) {
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith('//#endif') || trimmed.startsWith('// #endif')) {
|
||||
continue;
|
||||
}
|
||||
result.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return result.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single TypeScript file
|
||||
*/
|
||||
function processFile(srcPath, destPath) {
|
||||
const content = readFileSync(srcPath, 'utf-8');
|
||||
const processed = removeDim3Blocks(content);
|
||||
writeFileSync(destPath, processed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively copy and process directory
|
||||
*/
|
||||
function processDirectory(srcDir, destDir) {
|
||||
mkdirSync(destDir, { recursive: true });
|
||||
|
||||
const entries = readdirSync(srcDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = join(srcDir, entry.name);
|
||||
const destPath = join(destDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
processDirectory(srcPath, destPath);
|
||||
} else if (entry.name.endsWith('.ts')) {
|
||||
processFile(srcPath, destPath);
|
||||
console.log(`Processed: ${entry.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main
|
||||
console.log('Generating 2D source code...');
|
||||
console.log(`Source: ${srcTsDir}`);
|
||||
console.log(`Output: ${outputDir}`);
|
||||
|
||||
// Step 1: Copy and process src.ts directory
|
||||
processDirectory(srcTsDir, outputDir);
|
||||
|
||||
// Step 2: Overwrite with 2D-specific files (raw.ts, init.ts)
|
||||
if (existsSync(src2dDir)) {
|
||||
const entries = readdirSync(src2dDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && entry.name.endsWith('.ts')) {
|
||||
const srcPath = join(src2dDir, entry.name);
|
||||
const destPath = join(outputDir, entry.name);
|
||||
cpSync(srcPath, destPath);
|
||||
console.log(`Overwrote: ${entry.name} (2D-specific)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Rename rapier.ts to index.ts
|
||||
const rapierTs = join(outputDir, 'rapier.ts');
|
||||
const indexTs = join(outputDir, 'index.ts');
|
||||
if (existsSync(rapierTs)) {
|
||||
renameSync(rapierTs, indexTs);
|
||||
console.log('Renamed: rapier.ts -> index.ts');
|
||||
}
|
||||
|
||||
console.log('Done!');
|
||||
70
packages/physics/rapier2d/src/coarena.ts
Normal file
70
packages/physics/rapier2d/src/coarena.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export class Coarena<T> {
|
||||
fconv: Float64Array;
|
||||
uconv: Uint32Array;
|
||||
data: Array<T>;
|
||||
size: number;
|
||||
|
||||
public constructor() {
|
||||
this.fconv = new Float64Array(1);
|
||||
this.uconv = new Uint32Array(this.fconv.buffer);
|
||||
this.data = new Array<T>();
|
||||
this.size = 0;
|
||||
}
|
||||
|
||||
public set(handle: number, data: T) {
|
||||
let i = this.index(handle);
|
||||
while (this.data.length <= i) {
|
||||
this.data.push(null);
|
||||
}
|
||||
|
||||
if (this.data[i] == null) this.size += 1;
|
||||
this.data[i] = data;
|
||||
}
|
||||
|
||||
public len(): number {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
public delete(handle: number) {
|
||||
let i = this.index(handle);
|
||||
if (i < this.data.length) {
|
||||
if (this.data[i] != null) this.size -= 1;
|
||||
this.data[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.data = new Array<T>();
|
||||
}
|
||||
|
||||
public get(handle: number): T | null {
|
||||
let i = this.index(handle);
|
||||
if (i < this.data.length) {
|
||||
return this.data[i];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public forEach(f: (elt: T) => void) {
|
||||
for (const elt of this.data) {
|
||||
if (elt != null) f(elt);
|
||||
}
|
||||
}
|
||||
|
||||
public getAll(): Array<T> {
|
||||
return this.data.filter((elt) => elt != null);
|
||||
}
|
||||
|
||||
private index(handle: number): number {
|
||||
/// Extracts the index part of a handle (the lower 32 bits).
|
||||
/// This is done by first injecting the handle into an Float64Array
|
||||
/// which is itself injected into an Uint32Array (at construction time).
|
||||
/// The 0-th value of the Uint32Array will become the `number` integer
|
||||
/// representation of the lower 32 bits.
|
||||
/// Also `this.uconv[1]` then contains the generation number as a `number`,
|
||||
/// which we don’t really need.
|
||||
this.fconv[0] = handle;
|
||||
return this.uconv[0];
|
||||
}
|
||||
}
|
||||
387
packages/physics/rapier2d/src/control/character_controller.ts
Normal file
387
packages/physics/rapier2d/src/control/character_controller.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
import {RawKinematicCharacterController, RawCharacterCollision} from "../raw";
|
||||
import {Rotation, Vector, VectorOps} from "../math";
|
||||
import {
|
||||
BroadPhase,
|
||||
Collider,
|
||||
ColliderSet,
|
||||
InteractionGroups,
|
||||
NarrowPhase,
|
||||
Shape,
|
||||
} from "../geometry";
|
||||
import {QueryFilterFlags, World} from "../pipeline";
|
||||
import {IntegrationParameters, RigidBody, RigidBodySet} from "../dynamics";
|
||||
|
||||
/**
|
||||
* A collision between the character and an obstacle hit on its path.
|
||||
*/
|
||||
export class CharacterCollision {
|
||||
/** The collider involved in the collision. Null if the collider no longer exists in the physics world. */
|
||||
public collider: Collider | null;
|
||||
/** The translation delta applied to the character before this collision took place. */
|
||||
public translationDeltaApplied: Vector;
|
||||
/** The translation delta the character would move after this collision if there is no other obstacles. */
|
||||
public translationDeltaRemaining: Vector;
|
||||
/** The time-of-impact between the character and the obstacles. */
|
||||
public toi: number;
|
||||
/** The world-space contact point on the collider when the collision happens. */
|
||||
public witness1: Vector;
|
||||
/** The local-space contact point on the character when the collision happens. */
|
||||
public witness2: Vector;
|
||||
/** The world-space outward contact normal on the collider when the collision happens. */
|
||||
public normal1: Vector;
|
||||
/** The local-space outward contact normal on the character when the collision happens. */
|
||||
public normal2: Vector;
|
||||
}
|
||||
|
||||
/**
|
||||
* A character controller for controlling kinematic bodies and parentless colliders by hitting
|
||||
* and sliding against obstacles.
|
||||
*/
|
||||
export class KinematicCharacterController {
|
||||
private raw: RawKinematicCharacterController;
|
||||
private rawCharacterCollision: RawCharacterCollision;
|
||||
|
||||
private params: IntegrationParameters;
|
||||
private broadPhase: BroadPhase;
|
||||
private narrowPhase: NarrowPhase;
|
||||
private bodies: RigidBodySet;
|
||||
private colliders: ColliderSet;
|
||||
private _applyImpulsesToDynamicBodies: boolean;
|
||||
private _characterMass: number | null;
|
||||
|
||||
constructor(
|
||||
offset: number,
|
||||
params: IntegrationParameters,
|
||||
broadPhase: BroadPhase,
|
||||
narrowPhase: NarrowPhase,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
) {
|
||||
this.params = params;
|
||||
this.bodies = bodies;
|
||||
this.colliders = colliders;
|
||||
this.broadPhase = broadPhase;
|
||||
this.narrowPhase = narrowPhase;
|
||||
this.raw = new RawKinematicCharacterController(offset);
|
||||
this.rawCharacterCollision = new RawCharacterCollision();
|
||||
this._applyImpulsesToDynamicBodies = false;
|
||||
this._characterMass = null;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
this.rawCharacterCollision.free();
|
||||
}
|
||||
|
||||
this.raw = undefined;
|
||||
this.rawCharacterCollision = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The direction that goes "up". Used to determine where the floor is, and the floor’s angle.
|
||||
*/
|
||||
public up(): Vector {
|
||||
return this.raw.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the direction that goes "up". Used to determine where the floor is, and the floor’s angle.
|
||||
*/
|
||||
public setUp(vector: Vector) {
|
||||
let rawVect = VectorOps.intoRaw(vector);
|
||||
const result = this.raw.setUp(rawVect);
|
||||
rawVect.free();
|
||||
return result;
|
||||
}
|
||||
|
||||
public applyImpulsesToDynamicBodies(): boolean {
|
||||
return this._applyImpulsesToDynamicBodies;
|
||||
}
|
||||
|
||||
public setApplyImpulsesToDynamicBodies(enabled: boolean) {
|
||||
this._applyImpulsesToDynamicBodies = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom value of the character mass, if it was set by `this.setCharacterMass`.
|
||||
*/
|
||||
public characterMass(): number | null {
|
||||
return this._characterMass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the mass of the character to be used for impulse resolution if `self.applyImpulsesToDynamicBodies`
|
||||
* is set to `true`.
|
||||
*
|
||||
* If no character mass is set explicitly (or if it is set to `null`) it is automatically assumed to be equal
|
||||
* to the mass of the rigid-body the character collider is attached to; or equal to 0 if the character collider
|
||||
* isn’t attached to any rigid-body.
|
||||
*
|
||||
* @param mass - The mass to set.
|
||||
*/
|
||||
public setCharacterMass(mass: number | null) {
|
||||
this._characterMass = mass;
|
||||
}
|
||||
|
||||
/**
|
||||
* A small gap to preserve between the character and its surroundings.
|
||||
*
|
||||
* This value should not be too large to avoid visual artifacts, but shouldn’t be too small
|
||||
* (must not be zero) to improve numerical stability of the character controller.
|
||||
*/
|
||||
public offset(): number {
|
||||
return this.raw.offset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a small gap to preserve between the character and its surroundings.
|
||||
*
|
||||
* This value should not be too large to avoid visual artifacts, but shouldn’t be too small
|
||||
* (must not be zero) to improve numerical stability of the character controller.
|
||||
*/
|
||||
public setOffset(value: number) {
|
||||
this.raw.setOffset(value);
|
||||
}
|
||||
|
||||
/// Increase this number if your character appears to get stuck when sliding against surfaces.
|
||||
///
|
||||
/// This is a small distance applied to the movement toward the contact normals of shapes hit
|
||||
/// by the character controller. This helps shape-casting not getting stuck in an always-penetrating
|
||||
/// state during the sliding calculation.
|
||||
///
|
||||
/// This value should remain fairly small since it can introduce artificial "bumps" when sliding
|
||||
/// along a flat surface.
|
||||
public normalNudgeFactor(): number {
|
||||
return this.raw.normalNudgeFactor();
|
||||
}
|
||||
|
||||
/// Increase this number if your character appears to get stuck when sliding against surfaces.
|
||||
///
|
||||
/// This is a small distance applied to the movement toward the contact normals of shapes hit
|
||||
/// by the character controller. This helps shape-casting not getting stuck in an always-penetrating
|
||||
/// state during the sliding calculation.
|
||||
///
|
||||
/// This value should remain fairly small since it can introduce artificial "bumps" when sliding
|
||||
/// along a flat surface.
|
||||
public setNormalNudgeFactor(value: number) {
|
||||
this.raw.setNormalNudgeFactor(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is sliding against obstacles enabled?
|
||||
*/
|
||||
public slideEnabled(): boolean {
|
||||
return this.raw.slideEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable sliding against obstacles.
|
||||
*/
|
||||
public setSlideEnabled(enabled: boolean) {
|
||||
this.raw.setSlideEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum step height a character can automatically step over.
|
||||
*/
|
||||
public autostepMaxHeight(): number | null {
|
||||
return this.raw.autostepMaxHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum width of free space that must be available after stepping on a stair.
|
||||
*/
|
||||
public autostepMinWidth(): number | null {
|
||||
return this.raw.autostepMinWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the character automatically step over dynamic bodies too?
|
||||
*/
|
||||
public autostepIncludesDynamicBodies(): boolean | null {
|
||||
return this.raw.autostepIncludesDynamicBodies();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is automatically stepping over small objects enabled?
|
||||
*/
|
||||
public autostepEnabled(): boolean {
|
||||
return this.raw.autostepEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enabled automatically stepping over small objects.
|
||||
*
|
||||
* @param maxHeight - The maximum step height a character can automatically step over.
|
||||
* @param minWidth - The minimum width of free space that must be available after stepping on a stair.
|
||||
* @param includeDynamicBodies - Can the character automatically step over dynamic bodies too?
|
||||
*/
|
||||
public enableAutostep(
|
||||
maxHeight: number,
|
||||
minWidth: number,
|
||||
includeDynamicBodies: boolean,
|
||||
) {
|
||||
this.raw.enableAutostep(maxHeight, minWidth, includeDynamicBodies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable automatically stepping over small objects.
|
||||
*/
|
||||
public disableAutostep() {
|
||||
return this.raw.disableAutostep();
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum angle (radians) between the floor’s normal and the `up` vector that the
|
||||
* character is able to climb.
|
||||
*/
|
||||
public maxSlopeClimbAngle(): number {
|
||||
return this.raw.maxSlopeClimbAngle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum angle (radians) between the floor’s normal and the `up` vector that the
|
||||
* character is able to climb.
|
||||
*/
|
||||
public setMaxSlopeClimbAngle(angle: number) {
|
||||
this.raw.setMaxSlopeClimbAngle(angle);
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum angle (radians) between the floor’s normal and the `up` vector before the
|
||||
* character starts to slide down automatically.
|
||||
*/
|
||||
public minSlopeSlideAngle(): number {
|
||||
return this.raw.minSlopeSlideAngle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum angle (radians) between the floor’s normal and the `up` vector before the
|
||||
* character starts to slide down automatically.
|
||||
*/
|
||||
public setMinSlopeSlideAngle(angle: number) {
|
||||
this.raw.setMinSlopeSlideAngle(angle);
|
||||
}
|
||||
|
||||
/**
|
||||
* If snap-to-ground is enabled, should the character be automatically snapped to the ground if
|
||||
* the distance between the ground and its feet are smaller than the specified threshold?
|
||||
*/
|
||||
public snapToGroundDistance(): number | null {
|
||||
return this.raw.snapToGroundDistance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables automatically snapping the character to the ground if the distance between
|
||||
* the ground and its feet are smaller than the specified threshold.
|
||||
*/
|
||||
public enableSnapToGround(distance: number) {
|
||||
this.raw.enableSnapToGround(distance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables automatically snapping the character to the ground.
|
||||
*/
|
||||
public disableSnapToGround() {
|
||||
this.raw.disableSnapToGround();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is automatically snapping the character to the ground enabled?
|
||||
*/
|
||||
public snapToGroundEnabled(): boolean {
|
||||
return this.raw.snapToGroundEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the movement the given collider is able to execute after hitting and sliding on obstacles.
|
||||
*
|
||||
* @param collider - The collider to move.
|
||||
* @param desiredTranslationDelta - The desired collider movement.
|
||||
* @param filterFlags - Flags for excluding whole subsets of colliders from the obstacles taken into account.
|
||||
* @param filterGroups - Groups for excluding colliders with incompatible collision groups from the obstacles
|
||||
* taken into account.
|
||||
* @param filterPredicate - Any collider for which this closure returns `false` will be excluded from the
|
||||
* obstacles taken into account.
|
||||
*/
|
||||
public computeColliderMovement(
|
||||
collider: Collider,
|
||||
desiredTranslationDelta: Vector,
|
||||
filterFlags?: QueryFilterFlags,
|
||||
filterGroups?: InteractionGroups,
|
||||
filterPredicate?: (collider: Collider) => boolean,
|
||||
) {
|
||||
let rawTranslationDelta = VectorOps.intoRaw(desiredTranslationDelta);
|
||||
this.raw.computeColliderMovement(
|
||||
this.params.dt,
|
||||
this.broadPhase.raw,
|
||||
this.narrowPhase.raw,
|
||||
this.bodies.raw,
|
||||
this.colliders.raw,
|
||||
collider.handle,
|
||||
rawTranslationDelta,
|
||||
this._applyImpulsesToDynamicBodies,
|
||||
this._characterMass,
|
||||
filterFlags,
|
||||
filterGroups,
|
||||
this.colliders.castClosure(filterPredicate),
|
||||
);
|
||||
rawTranslationDelta.free();
|
||||
}
|
||||
|
||||
/**
|
||||
* The movement computed by the last call to `this.computeColliderMovement`.
|
||||
*/
|
||||
public computedMovement(): Vector {
|
||||
return VectorOps.fromRaw(this.raw.computedMovement());
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of ground detection computed by the last call to `this.computeColliderMovement`.
|
||||
*/
|
||||
public computedGrounded(): boolean {
|
||||
return this.raw.computedGrounded();
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of collisions against obstacles detected along the path of the last call
|
||||
* to `this.computeColliderMovement`.
|
||||
*/
|
||||
public numComputedCollisions(): number {
|
||||
return this.raw.numComputedCollisions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the collision against one of the obstacles detected along the path of the last
|
||||
* call to `this.computeColliderMovement`.
|
||||
*
|
||||
* @param i - The i-th collision will be returned.
|
||||
* @param out - If this argument is set, it will be filled with the collision information.
|
||||
*/
|
||||
public computedCollision(
|
||||
i: number,
|
||||
out?: CharacterCollision,
|
||||
): CharacterCollision | null {
|
||||
if (!this.raw.computedCollision(i, this.rawCharacterCollision)) {
|
||||
return null;
|
||||
} else {
|
||||
let c = this.rawCharacterCollision;
|
||||
out = out ?? new CharacterCollision();
|
||||
out.translationDeltaApplied = VectorOps.fromRaw(
|
||||
c.translationDeltaApplied(),
|
||||
);
|
||||
out.translationDeltaRemaining = VectorOps.fromRaw(
|
||||
c.translationDeltaRemaining(),
|
||||
);
|
||||
out.toi = c.toi();
|
||||
out.witness1 = VectorOps.fromRaw(c.worldWitness1());
|
||||
out.witness2 = VectorOps.fromRaw(c.worldWitness2());
|
||||
out.normal1 = VectorOps.fromRaw(c.worldNormal1());
|
||||
out.normal2 = VectorOps.fromRaw(c.worldNormal2());
|
||||
out.collider = this.colliders.get(c.handle());
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
packages/physics/rapier2d/src/control/index.ts
Normal file
3
packages/physics/rapier2d/src/control/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./character_controller";
|
||||
export * from "./pid_controller";
|
||||
|
||||
153
packages/physics/rapier2d/src/control/pid_controller.ts
Normal file
153
packages/physics/rapier2d/src/control/pid_controller.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import {RawPidController} from "../raw";
|
||||
import {Rotation, RotationOps, Vector, VectorOps} from "../math";
|
||||
import {Collider, ColliderSet, InteractionGroups, Shape} from "../geometry";
|
||||
import {QueryFilterFlags, World} from "../pipeline";
|
||||
import {IntegrationParameters, RigidBody, RigidBodySet} from "../dynamics";
|
||||
|
||||
// TODO: unify with the JointAxesMask
|
||||
/**
|
||||
* An enum representing the possible joint axes controlled by a PidController.
|
||||
* They can be ORed together, like:
|
||||
* PidAxesMask.LinX || PidAxesMask.LinY
|
||||
* to get a pid controller that only constraints the translational X and Y axes.
|
||||
*
|
||||
* Possible axes are:
|
||||
*
|
||||
* - `X`: X translation axis
|
||||
* - `Y`: Y translation axis
|
||||
* - `Z`: Z translation axis
|
||||
* - `AngX`: X angular rotation axis (3D only)
|
||||
* - `AngY`: Y angular rotation axis (3D only)
|
||||
* - `AngZ`: Z angular rotation axis
|
||||
*/
|
||||
export enum PidAxesMask {
|
||||
None = 0,
|
||||
LinX = 1 << 0,
|
||||
LinY = 1 << 1,
|
||||
LinZ = 1 << 2,
|
||||
AngZ = 1 << 5,
|
||||
AllLin = PidAxesMask.LinX | PidAxesMask.LinY,
|
||||
AllAng = PidAxesMask.AngZ,
|
||||
All = PidAxesMask.AllLin | PidAxesMask.AllAng,
|
||||
}
|
||||
|
||||
/**
|
||||
* A controller for controlling dynamic bodies using the
|
||||
* Proportional-Integral-Derivative correction model.
|
||||
*/
|
||||
export class PidController {
|
||||
private raw: RawPidController;
|
||||
|
||||
private params: IntegrationParameters;
|
||||
private bodies: RigidBodySet;
|
||||
|
||||
constructor(
|
||||
params: IntegrationParameters,
|
||||
bodies: RigidBodySet,
|
||||
kp: number,
|
||||
ki: number,
|
||||
kd: number,
|
||||
axes: PidAxesMask,
|
||||
) {
|
||||
this.params = params;
|
||||
this.bodies = bodies;
|
||||
this.raw = new RawPidController(kp, ki, kd, axes);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
|
||||
this.raw = undefined;
|
||||
}
|
||||
|
||||
public setKp(kp: number, axes: PidAxesMask) {
|
||||
this.raw.set_kp(kp, axes);
|
||||
}
|
||||
|
||||
public setKi(ki: number, axes: PidAxesMask) {
|
||||
this.raw.set_kp(ki, axes);
|
||||
}
|
||||
|
||||
public setKd(kd: number, axes: PidAxesMask) {
|
||||
this.raw.set_kp(kd, axes);
|
||||
}
|
||||
|
||||
public setAxes(axes: PidAxesMask) {
|
||||
this.raw.set_axes_mask(axes);
|
||||
}
|
||||
|
||||
public resetIntegrals() {
|
||||
this.raw.reset_integrals();
|
||||
}
|
||||
|
||||
public applyLinearCorrection(
|
||||
body: RigidBody,
|
||||
targetPosition: Vector,
|
||||
targetLinvel: Vector,
|
||||
) {
|
||||
let rawPos = VectorOps.intoRaw(targetPosition);
|
||||
let rawVel = VectorOps.intoRaw(targetLinvel);
|
||||
this.raw.apply_linear_correction(
|
||||
this.params.dt,
|
||||
this.bodies.raw,
|
||||
body.handle,
|
||||
rawPos,
|
||||
rawVel,
|
||||
);
|
||||
rawPos.free();
|
||||
rawVel.free();
|
||||
}
|
||||
|
||||
public applyAngularCorrection(
|
||||
body: RigidBody,
|
||||
targetRotation: number,
|
||||
targetAngVel: number,
|
||||
) {
|
||||
this.raw.apply_angular_correction(
|
||||
this.params.dt,
|
||||
this.bodies.raw,
|
||||
body.handle,
|
||||
targetRotation,
|
||||
targetAngVel,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public linearCorrection(
|
||||
body: RigidBody,
|
||||
targetPosition: Vector,
|
||||
targetLinvel: Vector,
|
||||
): Vector {
|
||||
let rawPos = VectorOps.intoRaw(targetPosition);
|
||||
let rawVel = VectorOps.intoRaw(targetLinvel);
|
||||
let correction = this.raw.linear_correction(
|
||||
this.params.dt,
|
||||
this.bodies.raw,
|
||||
body.handle,
|
||||
rawPos,
|
||||
rawVel,
|
||||
);
|
||||
rawPos.free();
|
||||
rawVel.free();
|
||||
|
||||
return VectorOps.fromRaw(correction);
|
||||
}
|
||||
|
||||
public angularCorrection(
|
||||
body: RigidBody,
|
||||
targetRotation: number,
|
||||
targetAngVel: number,
|
||||
): number {
|
||||
return this.raw.angular_correction(
|
||||
this.params.dt,
|
||||
this.bodies.raw,
|
||||
body.handle,
|
||||
targetRotation,
|
||||
targetAngVel,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
import {RawDynamicRayCastVehicleController} from "../raw";
|
||||
import {Vector, VectorOps} from "../math";
|
||||
import {
|
||||
BroadPhase,
|
||||
Collider,
|
||||
ColliderSet,
|
||||
InteractionGroups,
|
||||
NarrowPhase,
|
||||
} from "../geometry";
|
||||
import {QueryFilterFlags} from "../pipeline";
|
||||
import {RigidBody, RigidBodyHandle, RigidBodySet} from "../dynamics";
|
||||
|
||||
/**
|
||||
* A character controller to simulate vehicles using ray-casting for the wheels.
|
||||
*/
|
||||
export class DynamicRayCastVehicleController {
|
||||
private raw: RawDynamicRayCastVehicleController;
|
||||
private broadPhase: BroadPhase;
|
||||
private narrowPhase: NarrowPhase;
|
||||
private bodies: RigidBodySet;
|
||||
private colliders: ColliderSet;
|
||||
private _chassis: RigidBody;
|
||||
|
||||
constructor(
|
||||
chassis: RigidBody,
|
||||
broadPhase: BroadPhase,
|
||||
narrowPhase: NarrowPhase,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
) {
|
||||
if (typeof RawDynamicRayCastVehicleController === 'undefined') {
|
||||
throw new Error('DynamicRayCastVehicleController is not available in 2D mode');
|
||||
}
|
||||
this.raw = new RawDynamicRayCastVehicleController(chassis.handle);
|
||||
this.broadPhase = broadPhase;
|
||||
this.narrowPhase = narrowPhase;
|
||||
this.bodies = bodies;
|
||||
this.colliders = colliders;
|
||||
this._chassis = chassis;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
|
||||
this.raw = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the vehicle’s velocity based on its suspension, engine force, and brake.
|
||||
*
|
||||
* This directly updates the velocity of its chassis rigid-body.
|
||||
*
|
||||
* @param dt - Time increment used to integrate forces.
|
||||
* @param filterFlags - Flag to exclude categories of objects from the wheels’ ray-cast.
|
||||
* @param filterGroups - Only colliders compatible with these groups will be hit by the wheels’ ray-casts.
|
||||
* @param filterPredicate - Callback to filter out which collider will be hit by the wheels’ ray-casts.
|
||||
*/
|
||||
public updateVehicle(
|
||||
dt: number,
|
||||
filterFlags?: QueryFilterFlags,
|
||||
filterGroups?: InteractionGroups,
|
||||
filterPredicate?: (collider: Collider) => boolean,
|
||||
) {
|
||||
this.raw.update_vehicle(
|
||||
dt,
|
||||
this.broadPhase.raw,
|
||||
this.narrowPhase.raw,
|
||||
this.bodies.raw,
|
||||
this.colliders.raw,
|
||||
filterFlags,
|
||||
filterGroups,
|
||||
this.colliders.castClosure(filterPredicate),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The current forward speed of the vehicle.
|
||||
*/
|
||||
public currentVehicleSpeed(): number {
|
||||
return this.raw.current_vehicle_speed();
|
||||
}
|
||||
|
||||
/**
|
||||
* The rigid-body used as the chassis.
|
||||
*/
|
||||
public chassis(): RigidBody {
|
||||
return this._chassis;
|
||||
}
|
||||
|
||||
/**
|
||||
* The chassis’ local _up_ direction (`0 = x, 1 = y, 2 = z`).
|
||||
*/
|
||||
get indexUpAxis(): number {
|
||||
return this.raw.index_up_axis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the chassis’ local _up_ direction (`0 = x, 1 = y, 2 = z`).
|
||||
*/
|
||||
set indexUpAxis(axis: number) {
|
||||
this.raw.set_index_up_axis(axis);
|
||||
}
|
||||
|
||||
/**
|
||||
* The chassis’ local _forward_ direction (`0 = x, 1 = y, 2 = z`).
|
||||
*/
|
||||
get indexForwardAxis(): number {
|
||||
return this.raw.index_forward_axis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the chassis’ local _forward_ direction (`0 = x, 1 = y, 2 = z`).
|
||||
*/
|
||||
set setIndexForwardAxis(axis: number) {
|
||||
this.raw.set_index_forward_axis(axis);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new wheel attached to this vehicle.
|
||||
* @param chassisConnectionCs - The position of the wheel relative to the chassis.
|
||||
* @param directionCs - The direction of the wheel’s suspension, relative to the chassis. The ray-casting will
|
||||
* happen following this direction to detect the ground.
|
||||
* @param axleCs - The wheel’s axle axis, relative to the chassis.
|
||||
* @param suspensionRestLength - The rest length of the wheel’s suspension spring.
|
||||
* @param radius - The wheel’s radius.
|
||||
*/
|
||||
public addWheel(
|
||||
chassisConnectionCs: Vector,
|
||||
directionCs: Vector,
|
||||
axleCs: Vector,
|
||||
suspensionRestLength: number,
|
||||
radius: number,
|
||||
) {
|
||||
let rawChassisConnectionCs = VectorOps.intoRaw(chassisConnectionCs);
|
||||
let rawDirectionCs = VectorOps.intoRaw(directionCs);
|
||||
let rawAxleCs = VectorOps.intoRaw(axleCs);
|
||||
|
||||
this.raw.add_wheel(
|
||||
rawChassisConnectionCs,
|
||||
rawDirectionCs,
|
||||
rawAxleCs,
|
||||
suspensionRestLength,
|
||||
radius,
|
||||
);
|
||||
|
||||
rawChassisConnectionCs.free();
|
||||
rawDirectionCs.free();
|
||||
rawAxleCs.free();
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of wheels attached to this vehicle.
|
||||
*/
|
||||
public numWheels(): number {
|
||||
return this.raw.num_wheels();
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
* Access to wheel properties.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Getters + setters
|
||||
*/
|
||||
/**
|
||||
* The position of the i-th wheel, relative to the chassis.
|
||||
*/
|
||||
public wheelChassisConnectionPointCs(i: number): Vector | null {
|
||||
return VectorOps.fromRaw(this.raw.wheel_chassis_connection_point_cs(i));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the position of the i-th wheel, relative to the chassis.
|
||||
*/
|
||||
public setWheelChassisConnectionPointCs(i: number, value: Vector) {
|
||||
let rawValue = VectorOps.intoRaw(value);
|
||||
this.raw.set_wheel_chassis_connection_point_cs(i, rawValue);
|
||||
rawValue.free();
|
||||
}
|
||||
|
||||
/**
|
||||
* The rest length of the i-th wheel’s suspension spring.
|
||||
*/
|
||||
public wheelSuspensionRestLength(i: number): number | null {
|
||||
return this.raw.wheel_suspension_rest_length(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the rest length of the i-th wheel’s suspension spring.
|
||||
*/
|
||||
public setWheelSuspensionRestLength(i: number, value: number) {
|
||||
this.raw.set_wheel_suspension_rest_length(i, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum distance the i-th wheel suspension can travel before and after its resting length.
|
||||
*/
|
||||
public wheelMaxSuspensionTravel(i: number): number | null {
|
||||
return this.raw.wheel_max_suspension_travel(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum distance the i-th wheel suspension can travel before and after its resting length.
|
||||
*/
|
||||
public setWheelMaxSuspensionTravel(i: number, value: number) {
|
||||
this.raw.set_wheel_max_suspension_travel(i, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The i-th wheel’s radius.
|
||||
*/
|
||||
public wheelRadius(i: number): number | null {
|
||||
return this.raw.wheel_radius(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the i-th wheel’s radius.
|
||||
*/
|
||||
public setWheelRadius(i: number, value: number) {
|
||||
this.raw.set_wheel_radius(i, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The i-th wheel’s suspension stiffness.
|
||||
*
|
||||
* Increase this value if the suspension appears to not push the vehicle strong enough.
|
||||
*/
|
||||
public wheelSuspensionStiffness(i: number): number | null {
|
||||
return this.raw.wheel_suspension_stiffness(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the i-th wheel’s suspension stiffness.
|
||||
*
|
||||
* Increase this value if the suspension appears to not push the vehicle strong enough.
|
||||
*/
|
||||
public setWheelSuspensionStiffness(i: number, value: number) {
|
||||
this.raw.set_wheel_suspension_stiffness(i, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The i-th wheel’s suspension’s damping when it is being compressed.
|
||||
*/
|
||||
public wheelSuspensionCompression(i: number): number | null {
|
||||
return this.raw.wheel_suspension_compression(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* The i-th wheel’s suspension’s damping when it is being compressed.
|
||||
*/
|
||||
public setWheelSuspensionCompression(i: number, value: number) {
|
||||
this.raw.set_wheel_suspension_compression(i, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The i-th wheel’s suspension’s damping when it is being released.
|
||||
*
|
||||
* Increase this value if the suspension appears to overshoot.
|
||||
*/
|
||||
public wheelSuspensionRelaxation(i: number): number | null {
|
||||
return this.raw.wheel_suspension_relaxation(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the i-th wheel’s suspension’s damping when it is being released.
|
||||
*
|
||||
* Increase this value if the suspension appears to overshoot.
|
||||
*/
|
||||
public setWheelSuspensionRelaxation(i: number, value: number) {
|
||||
this.raw.set_wheel_suspension_relaxation(i, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum force applied by the i-th wheel’s suspension.
|
||||
*/
|
||||
public wheelMaxSuspensionForce(i: number): number | null {
|
||||
return this.raw.wheel_max_suspension_force(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum force applied by the i-th wheel’s suspension.
|
||||
*/
|
||||
public setWheelMaxSuspensionForce(i: number, value: number) {
|
||||
this.raw.set_wheel_max_suspension_force(i, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum amount of braking impulse applied on the i-th wheel to slow down the vehicle.
|
||||
*/
|
||||
public wheelBrake(i: number): number | null {
|
||||
return this.raw.wheel_brake(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum amount of braking impulse applied on the i-th wheel to slow down the vehicle.
|
||||
*/
|
||||
public setWheelBrake(i: number, value: number) {
|
||||
this.raw.set_wheel_brake(i, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The steering angle (radians) for the i-th wheel.
|
||||
*/
|
||||
public wheelSteering(i: number): number | null {
|
||||
return this.raw.wheel_steering(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the steering angle (radians) for the i-th wheel.
|
||||
*/
|
||||
public setWheelSteering(i: number, value: number) {
|
||||
this.raw.set_wheel_steering(i, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The forward force applied by the i-th wheel on the chassis.
|
||||
*/
|
||||
public wheelEngineForce(i: number): number | null {
|
||||
return this.raw.wheel_engine_force(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the forward force applied by the i-th wheel on the chassis.
|
||||
*/
|
||||
public setWheelEngineForce(i: number, value: number) {
|
||||
this.raw.set_wheel_engine_force(i, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The direction of the i-th wheel’s suspension, relative to the chassis.
|
||||
*
|
||||
* The ray-casting will happen following this direction to detect the ground.
|
||||
*/
|
||||
public wheelDirectionCs(i: number): Vector | null {
|
||||
return VectorOps.fromRaw(this.raw.wheel_direction_cs(i));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the direction of the i-th wheel’s suspension, relative to the chassis.
|
||||
*
|
||||
* The ray-casting will happen following this direction to detect the ground.
|
||||
*/
|
||||
public setWheelDirectionCs(i: number, value: Vector) {
|
||||
let rawValue = VectorOps.intoRaw(value);
|
||||
this.raw.set_wheel_direction_cs(i, rawValue);
|
||||
rawValue.free();
|
||||
}
|
||||
|
||||
/**
|
||||
* The i-th wheel’s axle axis, relative to the chassis.
|
||||
*
|
||||
* The axis index defined as 0 = X, 1 = Y, 2 = Z.
|
||||
*/
|
||||
public wheelAxleCs(i: number): Vector | null {
|
||||
return VectorOps.fromRaw(this.raw.wheel_axle_cs(i));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the i-th wheel’s axle axis, relative to the chassis.
|
||||
*
|
||||
* The axis index defined as 0 = X, 1 = Y, 2 = Z.
|
||||
*/
|
||||
public setWheelAxleCs(i: number, value: Vector) {
|
||||
let rawValue = VectorOps.intoRaw(value);
|
||||
this.raw.set_wheel_axle_cs(i, rawValue);
|
||||
rawValue.free();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter controlling how much traction the tire has.
|
||||
*
|
||||
* The larger the value, the more instantaneous braking will happen (with the risk of
|
||||
* causing the vehicle to flip if it’s too strong).
|
||||
*/
|
||||
public wheelFrictionSlip(i: number): number | null {
|
||||
return this.raw.wheel_friction_slip(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the parameter controlling how much traction the tire has.
|
||||
*
|
||||
* The larger the value, the more instantaneous braking will happen (with the risk of
|
||||
* causing the vehicle to flip if it’s too strong).
|
||||
*/
|
||||
public setWheelFrictionSlip(i: number, value: number) {
|
||||
this.raw.set_wheel_friction_slip(i, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The multiplier of friction between a tire and the collider it’s on top of.
|
||||
*
|
||||
* The larger the value, the stronger side friction will be.
|
||||
*/
|
||||
public wheelSideFrictionStiffness(i: number): number | null {
|
||||
return this.raw.wheel_side_friction_stiffness(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* The multiplier of friction between a tire and the collider it’s on top of.
|
||||
*
|
||||
* The larger the value, the stronger side friction will be.
|
||||
*/
|
||||
public setWheelSideFrictionStiffness(i: number, value: number) {
|
||||
this.raw.set_wheel_side_friction_stiffness(i, value);
|
||||
}
|
||||
|
||||
/*
|
||||
* Getters only.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The i-th wheel’s current rotation angle (radians) on its axle.
|
||||
*/
|
||||
public wheelRotation(i: number): number | null {
|
||||
return this.raw.wheel_rotation(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* The forward impulses applied by the i-th wheel on the chassis.
|
||||
*/
|
||||
public wheelForwardImpulse(i: number): number | null {
|
||||
return this.raw.wheel_forward_impulse(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* The side impulses applied by the i-th wheel on the chassis.
|
||||
*/
|
||||
public wheelSideImpulse(i: number): number | null {
|
||||
return this.raw.wheel_side_impulse(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* The force applied by the i-th wheel suspension.
|
||||
*/
|
||||
public wheelSuspensionForce(i: number): number | null {
|
||||
return this.raw.wheel_suspension_force(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* The (world-space) contact normal between the i-th wheel and the floor.
|
||||
*/
|
||||
public wheelContactNormal(i: number): Vector | null {
|
||||
return VectorOps.fromRaw(this.raw.wheel_contact_normal_ws(i));
|
||||
}
|
||||
|
||||
/**
|
||||
* The (world-space) point hit by the wheel’s ray-cast for the i-th wheel.
|
||||
*/
|
||||
public wheelContactPoint(i: number): Vector | null {
|
||||
return VectorOps.fromRaw(this.raw.wheel_contact_point_ws(i));
|
||||
}
|
||||
|
||||
/**
|
||||
* The suspension length for the i-th wheel.
|
||||
*/
|
||||
public wheelSuspensionLength(i: number): number | null {
|
||||
return this.raw.wheel_suspension_length(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* The (world-space) starting point of the ray-cast for the i-th wheel.
|
||||
*/
|
||||
public wheelHardPoint(i: number): Vector | null {
|
||||
return VectorOps.fromRaw(this.raw.wheel_hard_point_ws(i));
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the i-th wheel in contact with the ground?
|
||||
*/
|
||||
public wheelIsInContact(i: number): boolean {
|
||||
return this.raw.wheel_is_in_contact(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* The collider hit by the ray-cast for the i-th wheel.
|
||||
*/
|
||||
public wheelGroundObject(i: number): Collider | null {
|
||||
return this.colliders.get(this.raw.wheel_ground_object(i));
|
||||
}
|
||||
}
|
||||
25
packages/physics/rapier2d/src/dynamics/ccd_solver.ts
Normal file
25
packages/physics/rapier2d/src/dynamics/ccd_solver.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {RawCCDSolver} from "../raw";
|
||||
|
||||
/**
|
||||
* The CCD solver responsible for resolving Continuous Collision Detection.
|
||||
*
|
||||
* To avoid leaking WASM resources, this MUST be freed manually with `ccdSolver.free()`
|
||||
* once you are done using it.
|
||||
*/
|
||||
export class CCDSolver {
|
||||
raw: RawCCDSolver;
|
||||
|
||||
/**
|
||||
* Release the WASM memory occupied by this narrow-phase.
|
||||
*/
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
this.raw = undefined;
|
||||
}
|
||||
|
||||
constructor(raw?: RawCCDSolver) {
|
||||
this.raw = raw || new RawCCDSolver();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* A rule applied to combine coefficients.
|
||||
*
|
||||
* Use this when configuring the `ColliderDesc` to specify
|
||||
* how friction and restitution coefficient should be combined
|
||||
* in a contact.
|
||||
*/
|
||||
export enum CoefficientCombineRule {
|
||||
Average = 0,
|
||||
Min = 1,
|
||||
Multiply = 2,
|
||||
Max = 3,
|
||||
}
|
||||
485
packages/physics/rapier2d/src/dynamics/impulse_joint.ts
Normal file
485
packages/physics/rapier2d/src/dynamics/impulse_joint.ts
Normal file
@@ -0,0 +1,485 @@
|
||||
import {Rotation, Vector, VectorOps, RotationOps} from "../math";
|
||||
import {
|
||||
RawGenericJoint,
|
||||
RawImpulseJointSet,
|
||||
RawRigidBodySet,
|
||||
RawJointAxis,
|
||||
RawJointType,
|
||||
RawMotorModel,
|
||||
} from "../raw";
|
||||
import {RigidBody, RigidBodyHandle} from "./rigid_body";
|
||||
import {RigidBodySet} from "./rigid_body_set";
|
||||
|
||||
/**
|
||||
* The integer identifier of a collider added to a `ColliderSet`.
|
||||
*/
|
||||
export type ImpulseJointHandle = number;
|
||||
|
||||
/**
|
||||
* An enum grouping all possible types of joints:
|
||||
*
|
||||
* - `Revolute`: A revolute joint that removes all degrees of freedom between the affected
|
||||
* bodies except for the rotation along one axis.
|
||||
* - `Fixed`: A fixed joint that removes all relative degrees of freedom between the affected bodies.
|
||||
* - `Prismatic`: A prismatic joint that removes all degrees of freedom between the affected
|
||||
* bodies except for the translation along one axis.
|
||||
* - `Spherical`: (3D only) A spherical joint that removes all relative linear degrees of freedom between the affected bodies.
|
||||
* - `Generic`: (3D only) A joint with customizable degrees of freedom, allowing any of the 6 axes to be locked.
|
||||
*/
|
||||
export enum JointType {
|
||||
Revolute,
|
||||
Fixed,
|
||||
Prismatic,
|
||||
Rope,
|
||||
Spring,
|
||||
}
|
||||
|
||||
export enum MotorModel {
|
||||
AccelerationBased,
|
||||
ForceBased,
|
||||
}
|
||||
|
||||
/**
|
||||
* An enum representing the possible joint axes of a generic joint.
|
||||
* They can be ORed together, like:
|
||||
* JointAxesMask.LinX || JointAxesMask.LinY
|
||||
* to get a joint that is only free in the X and Y translational (positional) axes.
|
||||
*
|
||||
* Possible free axes are:
|
||||
*
|
||||
* - `X`: X translation axis
|
||||
* - `Y`: Y translation axis
|
||||
* - `Z`: Z translation axis
|
||||
* - `AngX`: X angular rotation axis
|
||||
* - `AngY`: Y angular rotations axis
|
||||
* - `AngZ`: Z angular rotation axis
|
||||
*/
|
||||
export enum JointAxesMask {
|
||||
LinX = 1 << 0,
|
||||
LinY = 1 << 1,
|
||||
LinZ = 1 << 2,
|
||||
AngX = 1 << 3,
|
||||
AngY = 1 << 4,
|
||||
AngZ = 1 << 5,
|
||||
}
|
||||
|
||||
export class ImpulseJoint {
|
||||
protected rawSet: RawImpulseJointSet; // The ImpulseJoint won't need to free this.
|
||||
protected bodySet: RigidBodySet; // The ImpulseJoint won’t need to free this.
|
||||
handle: ImpulseJointHandle;
|
||||
|
||||
constructor(
|
||||
rawSet: RawImpulseJointSet,
|
||||
bodySet: RigidBodySet,
|
||||
handle: ImpulseJointHandle,
|
||||
) {
|
||||
this.rawSet = rawSet;
|
||||
this.bodySet = bodySet;
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
public static newTyped(
|
||||
rawSet: RawImpulseJointSet,
|
||||
bodySet: RigidBodySet,
|
||||
handle: ImpulseJointHandle,
|
||||
): ImpulseJoint {
|
||||
switch (rawSet.jointType(handle)) {
|
||||
case RawJointType.Revolute:
|
||||
return new RevoluteImpulseJoint(rawSet, bodySet, handle);
|
||||
case RawJointType.Prismatic:
|
||||
return new PrismaticImpulseJoint(rawSet, bodySet, handle);
|
||||
case RawJointType.Fixed:
|
||||
return new FixedImpulseJoint(rawSet, bodySet, handle);
|
||||
case RawJointType.Spring:
|
||||
return new SpringImpulseJoint(rawSet, bodySet, handle);
|
||||
case RawJointType.Rope:
|
||||
return new RopeImpulseJoint(rawSet, bodySet, handle);
|
||||
default:
|
||||
return new ImpulseJoint(rawSet, bodySet, handle);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public finalizeDeserialization(bodySet: RigidBodySet) {
|
||||
this.bodySet = bodySet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this joint is still valid (i.e. that it has
|
||||
* not been deleted from the joint set yet).
|
||||
*/
|
||||
public isValid(): boolean {
|
||||
return this.rawSet.contains(this.handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* The first rigid-body this joint it attached to.
|
||||
*/
|
||||
public body1(): RigidBody {
|
||||
return this.bodySet.get(this.rawSet.jointBodyHandle1(this.handle));
|
||||
}
|
||||
|
||||
/**
|
||||
* The second rigid-body this joint is attached to.
|
||||
*/
|
||||
public body2(): RigidBody {
|
||||
return this.bodySet.get(this.rawSet.jointBodyHandle2(this.handle));
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of this joint given as a string.
|
||||
*/
|
||||
public type(): JointType {
|
||||
return this.rawSet.jointType(this.handle) as number as JointType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The position of the first anchor of this joint.
|
||||
*
|
||||
* The first anchor gives the position of the application point on the
|
||||
* local frame of the first rigid-body it is attached to.
|
||||
*/
|
||||
public anchor1(): Vector {
|
||||
return VectorOps.fromRaw(this.rawSet.jointAnchor1(this.handle));
|
||||
}
|
||||
|
||||
/**
|
||||
* The position of the second anchor of this joint.
|
||||
*
|
||||
* The second anchor gives the position of the application point on the
|
||||
* local frame of the second rigid-body it is attached to.
|
||||
*/
|
||||
public anchor2(): Vector {
|
||||
return VectorOps.fromRaw(this.rawSet.jointAnchor2(this.handle));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the position of the first anchor of this joint.
|
||||
*
|
||||
* The first anchor gives the position of the application point on the
|
||||
* local frame of the first rigid-body it is attached to.
|
||||
*/
|
||||
public setAnchor1(newPos: Vector) {
|
||||
const rawPoint = VectorOps.intoRaw(newPos);
|
||||
this.rawSet.jointSetAnchor1(this.handle, rawPoint);
|
||||
rawPoint.free();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the position of the second anchor of this joint.
|
||||
*
|
||||
* The second anchor gives the position of the application point on the
|
||||
* local frame of the second rigid-body it is attached to.
|
||||
*/
|
||||
public setAnchor2(newPos: Vector) {
|
||||
const rawPoint = VectorOps.intoRaw(newPos);
|
||||
this.rawSet.jointSetAnchor2(this.handle, rawPoint);
|
||||
rawPoint.free();
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls whether contacts are computed between colliders attached
|
||||
* to the rigid-bodies linked by this joint.
|
||||
*/
|
||||
public setContactsEnabled(enabled: boolean) {
|
||||
this.rawSet.jointSetContactsEnabled(this.handle, enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if contacts are enabled between colliders attached
|
||||
* to the rigid-bodies linked by this joint.
|
||||
*/
|
||||
public contactsEnabled(): boolean {
|
||||
return this.rawSet.jointContactsEnabled(this.handle);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnitImpulseJoint extends ImpulseJoint {
|
||||
/**
|
||||
* The axis left free by this joint.
|
||||
*/
|
||||
protected rawAxis?(): RawJointAxis;
|
||||
|
||||
/**
|
||||
* Are the limits enabled for this joint?
|
||||
*/
|
||||
public limitsEnabled(): boolean {
|
||||
return this.rawSet.jointLimitsEnabled(this.handle, this.rawAxis());
|
||||
}
|
||||
|
||||
/**
|
||||
* The min limit of this joint.
|
||||
*/
|
||||
public limitsMin(): number {
|
||||
return this.rawSet.jointLimitsMin(this.handle, this.rawAxis());
|
||||
}
|
||||
|
||||
/**
|
||||
* The max limit of this joint.
|
||||
*/
|
||||
public limitsMax(): number {
|
||||
return this.rawSet.jointLimitsMax(this.handle, this.rawAxis());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the limits of this joint.
|
||||
*
|
||||
* @param min - The minimum bound of this joint’s free coordinate.
|
||||
* @param max - The maximum bound of this joint’s free coordinate.
|
||||
*/
|
||||
public setLimits(min: number, max: number) {
|
||||
this.rawSet.jointSetLimits(this.handle, this.rawAxis(), min, max);
|
||||
}
|
||||
|
||||
public configureMotorModel(model: MotorModel) {
|
||||
this.rawSet.jointConfigureMotorModel(
|
||||
this.handle,
|
||||
this.rawAxis(),
|
||||
model as number as RawMotorModel,
|
||||
);
|
||||
}
|
||||
|
||||
public configureMotorVelocity(targetVel: number, factor: number) {
|
||||
this.rawSet.jointConfigureMotorVelocity(
|
||||
this.handle,
|
||||
this.rawAxis(),
|
||||
targetVel,
|
||||
factor,
|
||||
);
|
||||
}
|
||||
|
||||
public configureMotorPosition(
|
||||
targetPos: number,
|
||||
stiffness: number,
|
||||
damping: number,
|
||||
) {
|
||||
this.rawSet.jointConfigureMotorPosition(
|
||||
this.handle,
|
||||
this.rawAxis(),
|
||||
targetPos,
|
||||
stiffness,
|
||||
damping,
|
||||
);
|
||||
}
|
||||
|
||||
public configureMotor(
|
||||
targetPos: number,
|
||||
targetVel: number,
|
||||
stiffness: number,
|
||||
damping: number,
|
||||
) {
|
||||
this.rawSet.jointConfigureMotor(
|
||||
this.handle,
|
||||
this.rawAxis(),
|
||||
targetPos,
|
||||
targetVel,
|
||||
stiffness,
|
||||
damping,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class FixedImpulseJoint extends ImpulseJoint {}
|
||||
|
||||
export class RopeImpulseJoint extends ImpulseJoint {}
|
||||
|
||||
export class SpringImpulseJoint extends ImpulseJoint {}
|
||||
|
||||
export class PrismaticImpulseJoint extends UnitImpulseJoint {
|
||||
public rawAxis(): RawJointAxis {
|
||||
return RawJointAxis.LinX;
|
||||
}
|
||||
}
|
||||
|
||||
export class RevoluteImpulseJoint extends UnitImpulseJoint {
|
||||
public rawAxis(): RawJointAxis {
|
||||
return RawJointAxis.AngX;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class JointData {
|
||||
anchor1: Vector;
|
||||
anchor2: Vector;
|
||||
axis: Vector;
|
||||
frame1: Rotation;
|
||||
frame2: Rotation;
|
||||
jointType: JointType;
|
||||
limitsEnabled: boolean;
|
||||
limits: Array<number>;
|
||||
axesMask: JointAxesMask;
|
||||
stiffness: number;
|
||||
damping: number;
|
||||
length: number;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Creates a new joint descriptor that builds a Fixed joint.
|
||||
*
|
||||
* A fixed joint removes all the degrees of freedom between the affected bodies, ensuring their
|
||||
* anchor and local frames coincide in world-space.
|
||||
*
|
||||
* @param anchor1 - Point where the joint is attached on the first rigid-body affected by this joint. Expressed in the
|
||||
* local-space of the rigid-body.
|
||||
* @param frame1 - The reference orientation of the joint wrt. the first rigid-body.
|
||||
* @param anchor2 - Point where the joint is attached on the second rigid-body affected by this joint. Expressed in the
|
||||
* local-space of the rigid-body.
|
||||
* @param frame2 - The reference orientation of the joint wrt. the second rigid-body.
|
||||
*/
|
||||
public static fixed(
|
||||
anchor1: Vector,
|
||||
frame1: Rotation,
|
||||
anchor2: Vector,
|
||||
frame2: Rotation,
|
||||
): JointData {
|
||||
let res = new JointData();
|
||||
res.anchor1 = anchor1;
|
||||
res.anchor2 = anchor2;
|
||||
res.frame1 = frame1;
|
||||
res.frame2 = frame2;
|
||||
res.jointType = JointType.Fixed;
|
||||
return res;
|
||||
}
|
||||
|
||||
public static spring(
|
||||
rest_length: number,
|
||||
stiffness: number,
|
||||
damping: number,
|
||||
anchor1: Vector,
|
||||
anchor2: Vector,
|
||||
): JointData {
|
||||
let res = new JointData();
|
||||
res.anchor1 = anchor1;
|
||||
res.anchor2 = anchor2;
|
||||
res.length = rest_length;
|
||||
res.stiffness = stiffness;
|
||||
res.damping = damping;
|
||||
res.jointType = JointType.Spring;
|
||||
return res;
|
||||
}
|
||||
|
||||
public static rope(
|
||||
length: number,
|
||||
anchor1: Vector,
|
||||
anchor2: Vector,
|
||||
): JointData {
|
||||
let res = new JointData();
|
||||
res.anchor1 = anchor1;
|
||||
res.anchor2 = anchor2;
|
||||
res.length = length;
|
||||
res.jointType = JointType.Rope;
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new joint descriptor that builds revolute joints.
|
||||
*
|
||||
* A revolute joint allows three relative rotational degrees of freedom
|
||||
* by preventing any relative translation between the anchors of the
|
||||
* two attached rigid-bodies.
|
||||
*
|
||||
* @param anchor1 - Point where the joint is attached on the first rigid-body affected by this joint. Expressed in the
|
||||
* local-space of the rigid-body.
|
||||
* @param anchor2 - Point where the joint is attached on the second rigid-body affected by this joint. Expressed in the
|
||||
* local-space of the rigid-body.
|
||||
*/
|
||||
public static revolute(anchor1: Vector, anchor2: Vector): JointData {
|
||||
let res = new JointData();
|
||||
res.anchor1 = anchor1;
|
||||
res.anchor2 = anchor2;
|
||||
res.jointType = JointType.Revolute;
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new joint descriptor that builds a Prismatic joint.
|
||||
*
|
||||
* A prismatic joint removes all the degrees of freedom between the
|
||||
* affected bodies, except for the translation along one axis.
|
||||
*
|
||||
* @param anchor1 - Point where the joint is attached on the first rigid-body affected by this joint. Expressed in the
|
||||
* local-space of the rigid-body.
|
||||
* @param anchor2 - Point where the joint is attached on the second rigid-body affected by this joint. Expressed in the
|
||||
* local-space of the rigid-body.
|
||||
* @param axis - Axis of the joint, expressed in the local-space of the rigid-bodies it is attached to.
|
||||
*/
|
||||
public static prismatic(
|
||||
anchor1: Vector,
|
||||
anchor2: Vector,
|
||||
axis: Vector,
|
||||
): JointData {
|
||||
let res = new JointData();
|
||||
res.anchor1 = anchor1;
|
||||
res.anchor2 = anchor2;
|
||||
res.axis = axis;
|
||||
res.jointType = JointType.Prismatic;
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public intoRaw(): RawGenericJoint {
|
||||
let rawA1 = VectorOps.intoRaw(this.anchor1);
|
||||
let rawA2 = VectorOps.intoRaw(this.anchor2);
|
||||
let rawAx;
|
||||
let result;
|
||||
let limitsEnabled = false;
|
||||
let limitsMin = 0.0;
|
||||
let limitsMax = 0.0;
|
||||
|
||||
switch (this.jointType) {
|
||||
case JointType.Fixed:
|
||||
let rawFra1 = RotationOps.intoRaw(this.frame1);
|
||||
let rawFra2 = RotationOps.intoRaw(this.frame2);
|
||||
result = RawGenericJoint.fixed(rawA1, rawFra1, rawA2, rawFra2);
|
||||
rawFra1.free();
|
||||
rawFra2.free();
|
||||
break;
|
||||
case JointType.Spring:
|
||||
result = RawGenericJoint.spring(
|
||||
this.length,
|
||||
this.stiffness,
|
||||
this.damping,
|
||||
rawA1,
|
||||
rawA2,
|
||||
);
|
||||
break;
|
||||
case JointType.Rope:
|
||||
result = RawGenericJoint.rope(this.length, rawA1, rawA2);
|
||||
break;
|
||||
case JointType.Prismatic:
|
||||
rawAx = VectorOps.intoRaw(this.axis);
|
||||
|
||||
if (!!this.limitsEnabled) {
|
||||
limitsEnabled = true;
|
||||
limitsMin = this.limits[0];
|
||||
limitsMax = this.limits[1];
|
||||
}
|
||||
|
||||
result = RawGenericJoint.prismatic(
|
||||
rawA1,
|
||||
rawA2,
|
||||
rawAx,
|
||||
limitsEnabled,
|
||||
limitsMin,
|
||||
limitsMax,
|
||||
);
|
||||
|
||||
|
||||
rawAx.free();
|
||||
break;
|
||||
case JointType.Revolute:
|
||||
result = RawGenericJoint.revolute(rawA1, rawA2);
|
||||
break;
|
||||
}
|
||||
|
||||
rawA1.free();
|
||||
rawA2.free();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
162
packages/physics/rapier2d/src/dynamics/impulse_joint_set.ts
Normal file
162
packages/physics/rapier2d/src/dynamics/impulse_joint_set.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import {RawImpulseJointSet} from "../raw";
|
||||
import {Coarena} from "../coarena";
|
||||
import {RigidBodySet} from "./rigid_body_set";
|
||||
import {
|
||||
RevoluteImpulseJoint,
|
||||
FixedImpulseJoint,
|
||||
ImpulseJoint,
|
||||
ImpulseJointHandle,
|
||||
JointData,
|
||||
JointType,
|
||||
PrismaticImpulseJoint,
|
||||
} from "./impulse_joint";
|
||||
import {IslandManager} from "./island_manager";
|
||||
import {RigidBodyHandle} from "./rigid_body";
|
||||
import {Collider, ColliderHandle} from "../geometry";
|
||||
|
||||
/**
|
||||
* A set of joints.
|
||||
*
|
||||
* To avoid leaking WASM resources, this MUST be freed manually with `jointSet.free()`
|
||||
* once you are done using it (and all the joints it created).
|
||||
*/
|
||||
export class ImpulseJointSet {
|
||||
raw: RawImpulseJointSet;
|
||||
private map: Coarena<ImpulseJoint>;
|
||||
|
||||
/**
|
||||
* Release the WASM memory occupied by this joint set.
|
||||
*/
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
this.raw = undefined;
|
||||
|
||||
if (!!this.map) {
|
||||
this.map.clear();
|
||||
}
|
||||
this.map = undefined;
|
||||
}
|
||||
|
||||
constructor(raw?: RawImpulseJointSet) {
|
||||
this.raw = raw || new RawImpulseJointSet();
|
||||
this.map = new Coarena<ImpulseJoint>();
|
||||
// Initialize the map with the existing elements, if any.
|
||||
if (raw) {
|
||||
raw.forEachJointHandle((handle: ImpulseJointHandle) => {
|
||||
this.map.set(handle, ImpulseJoint.newTyped(raw, null, handle));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public finalizeDeserialization(bodies: RigidBodySet) {
|
||||
this.map.forEach((joint) => joint.finalizeDeserialization(bodies));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new joint and return its integer handle.
|
||||
*
|
||||
* @param bodies - The set of rigid-bodies containing the bodies the joint is attached to.
|
||||
* @param desc - The joint's parameters.
|
||||
* @param parent1 - The handle of the first rigid-body this joint is attached to.
|
||||
* @param parent2 - The handle of the second rigid-body this joint is attached to.
|
||||
* @param wakeUp - Should the attached rigid-bodies be awakened?
|
||||
*/
|
||||
public createJoint(
|
||||
bodies: RigidBodySet,
|
||||
desc: JointData,
|
||||
parent1: RigidBodyHandle,
|
||||
parent2: RigidBodyHandle,
|
||||
wakeUp: boolean,
|
||||
): ImpulseJoint {
|
||||
const rawParams = desc.intoRaw();
|
||||
const handle = this.raw.createJoint(
|
||||
rawParams,
|
||||
parent1,
|
||||
parent2,
|
||||
wakeUp,
|
||||
);
|
||||
rawParams.free();
|
||||
let joint = ImpulseJoint.newTyped(this.raw, bodies, handle);
|
||||
this.map.set(handle, joint);
|
||||
return joint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a joint from this set.
|
||||
*
|
||||
* @param handle - The integer handle of the joint.
|
||||
* @param wakeUp - If `true`, the rigid-bodies attached by the removed joint will be woken-up automatically.
|
||||
*/
|
||||
public remove(handle: ImpulseJointHandle, wakeUp: boolean) {
|
||||
this.raw.remove(handle, wakeUp);
|
||||
this.unmap(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the given closure with the integer handle of each impulse joint attached to this rigid-body.
|
||||
*
|
||||
* @param f - The closure called with the integer handle of each impulse joint attached to the rigid-body.
|
||||
*/
|
||||
public forEachJointHandleAttachedToRigidBody(
|
||||
handle: RigidBodyHandle,
|
||||
f: (handle: ImpulseJointHandle) => void,
|
||||
) {
|
||||
this.raw.forEachJointAttachedToRigidBody(handle, f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal function, do not call directly.
|
||||
* @param handle
|
||||
*/
|
||||
public unmap(handle: ImpulseJointHandle) {
|
||||
this.map.delete(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of joints on this set.
|
||||
*/
|
||||
public len(): number {
|
||||
return this.map.len();
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this set contain a joint with the given handle?
|
||||
*
|
||||
* @param handle - The joint handle to check.
|
||||
*/
|
||||
public contains(handle: ImpulseJointHandle): boolean {
|
||||
return this.get(handle) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the joint with the given handle.
|
||||
*
|
||||
* Returns `null` if no joint with the specified handle exists.
|
||||
*
|
||||
* @param handle - The integer handle of the joint to retrieve.
|
||||
*/
|
||||
public get(handle: ImpulseJointHandle): ImpulseJoint | null {
|
||||
return this.map.get(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given closure to each joint contained by this set.
|
||||
*
|
||||
* @param f - The closure to apply.
|
||||
*/
|
||||
public forEach(f: (joint: ImpulseJoint) => void) {
|
||||
this.map.forEach(f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all joints in the list.
|
||||
*
|
||||
* @returns joint list.
|
||||
*/
|
||||
public getAll(): ImpulseJoint[] {
|
||||
return this.map.getAll();
|
||||
}
|
||||
}
|
||||
10
packages/physics/rapier2d/src/dynamics/index.ts
Normal file
10
packages/physics/rapier2d/src/dynamics/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export * from "./rigid_body";
|
||||
export * from "./rigid_body_set";
|
||||
export * from "./integration_parameters";
|
||||
export * from "./impulse_joint";
|
||||
export * from "./impulse_joint_set";
|
||||
export * from "./multibody_joint";
|
||||
export * from "./multibody_joint_set";
|
||||
export * from "./coefficient_combine_rule";
|
||||
export * from "./ccd_solver";
|
||||
export * from "./island_manager";
|
||||
126
packages/physics/rapier2d/src/dynamics/integration_parameters.ts
Normal file
126
packages/physics/rapier2d/src/dynamics/integration_parameters.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import {RawIntegrationParameters} from "../raw";
|
||||
|
||||
export class IntegrationParameters {
|
||||
raw: RawIntegrationParameters;
|
||||
|
||||
constructor(raw?: RawIntegrationParameters) {
|
||||
this.raw = raw || new RawIntegrationParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the WASM memory used by these integration parameters.
|
||||
*/
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
this.raw = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestep length (default: `1.0 / 60.0`)
|
||||
*/
|
||||
get dt(): number {
|
||||
return this.raw.dt;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Error Reduction Parameter in `[0, 1]` is the proportion of
|
||||
* the positional error to be corrected at each time step (default: `0.2`).
|
||||
*/
|
||||
get contact_erp(): number {
|
||||
return this.raw.contact_erp;
|
||||
}
|
||||
|
||||
get lengthUnit(): number {
|
||||
return this.raw.lengthUnit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized amount of penetration the engine won’t attempt to correct (default: `0.001m`).
|
||||
*
|
||||
* This threshold considered by the physics engine is this value multiplied by the `lengthUnit`.
|
||||
*/
|
||||
get normalizedAllowedLinearError(): number {
|
||||
return this.raw.normalizedAllowedLinearError;
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximal normalized distance separating two objects that will generate predictive contacts (default: `0.002`).
|
||||
*
|
||||
* This threshold considered by the physics engine is this value multiplied by the `lengthUnit`.
|
||||
*/
|
||||
get normalizedPredictionDistance(): number {
|
||||
return this.raw.normalizedPredictionDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of solver iterations run by the constraints solver for calculating forces (default: `4`).
|
||||
*/
|
||||
get numSolverIterations(): number {
|
||||
return this.raw.numSolverIterations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration (default: `1`).
|
||||
*/
|
||||
get numInternalPgsIterations(): number {
|
||||
return this.raw.numInternalPgsIterations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum number of dynamic bodies in each active island (default: `128`).
|
||||
*/
|
||||
get minIslandSize(): number {
|
||||
return this.raw.minIslandSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum number of substeps performed by the solver (default: `1`).
|
||||
*/
|
||||
get maxCcdSubsteps(): number {
|
||||
return this.raw.maxCcdSubsteps;
|
||||
}
|
||||
|
||||
set dt(value: number) {
|
||||
this.raw.dt = value;
|
||||
}
|
||||
|
||||
set contact_natural_frequency(value: number) {
|
||||
this.raw.contact_natural_frequency = value;
|
||||
}
|
||||
|
||||
set lengthUnit(value: number) {
|
||||
this.raw.lengthUnit = value;
|
||||
}
|
||||
|
||||
set normalizedAllowedLinearError(value: number) {
|
||||
this.raw.normalizedAllowedLinearError = value;
|
||||
}
|
||||
|
||||
set normalizedPredictionDistance(value: number) {
|
||||
this.raw.normalizedPredictionDistance = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of solver iterations run by the constraints solver for calculating forces (default: `4`).
|
||||
*/
|
||||
set numSolverIterations(value: number) {
|
||||
this.raw.numSolverIterations = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration (default: `1`).
|
||||
*/
|
||||
set numInternalPgsIterations(value: number) {
|
||||
this.raw.numInternalPgsIterations = value;
|
||||
}
|
||||
|
||||
set minIslandSize(value: number) {
|
||||
this.raw.minIslandSize = value;
|
||||
}
|
||||
|
||||
set maxCcdSubsteps(value: number) {
|
||||
this.raw.maxCcdSubsteps = value;
|
||||
}
|
||||
}
|
||||
37
packages/physics/rapier2d/src/dynamics/island_manager.ts
Normal file
37
packages/physics/rapier2d/src/dynamics/island_manager.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {RawIslandManager} from "../raw";
|
||||
import {RigidBodyHandle} from "./rigid_body";
|
||||
|
||||
/**
|
||||
* The CCD solver responsible for resolving Continuous Collision Detection.
|
||||
*
|
||||
* To avoid leaking WASM resources, this MUST be freed manually with `ccdSolver.free()`
|
||||
* once you are done using it.
|
||||
*/
|
||||
export class IslandManager {
|
||||
raw: RawIslandManager;
|
||||
|
||||
/**
|
||||
* Release the WASM memory occupied by this narrow-phase.
|
||||
*/
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
this.raw = undefined;
|
||||
}
|
||||
|
||||
constructor(raw?: RawIslandManager) {
|
||||
this.raw = raw || new RawIslandManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given closure to the handle of each active rigid-bodies contained by this set.
|
||||
*
|
||||
* A rigid-body is active if it is not sleeping, i.e., if it moved recently.
|
||||
*
|
||||
* @param f - The closure to apply.
|
||||
*/
|
||||
public forEachActiveRigidBodyHandle(f: (handle: RigidBodyHandle) => void) {
|
||||
this.raw.forEachActiveRigidBodyHandle(f);
|
||||
}
|
||||
}
|
||||
190
packages/physics/rapier2d/src/dynamics/multibody_joint.ts
Normal file
190
packages/physics/rapier2d/src/dynamics/multibody_joint.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import {
|
||||
RawImpulseJointSet,
|
||||
RawJointAxis,
|
||||
RawJointType,
|
||||
RawMultibodyJointSet,
|
||||
} from "../raw";
|
||||
import {
|
||||
FixedImpulseJoint,
|
||||
ImpulseJointHandle,
|
||||
JointType,
|
||||
MotorModel,
|
||||
PrismaticImpulseJoint,
|
||||
RevoluteImpulseJoint,
|
||||
} from "./impulse_joint";
|
||||
|
||||
|
||||
/**
|
||||
* The integer identifier of a collider added to a `ColliderSet`.
|
||||
*/
|
||||
export type MultibodyJointHandle = number;
|
||||
|
||||
export class MultibodyJoint {
|
||||
protected rawSet: RawMultibodyJointSet; // The MultibodyJoint won't need to free this.
|
||||
handle: MultibodyJointHandle;
|
||||
|
||||
constructor(rawSet: RawMultibodyJointSet, handle: MultibodyJointHandle) {
|
||||
this.rawSet = rawSet;
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
public static newTyped(
|
||||
rawSet: RawMultibodyJointSet,
|
||||
handle: MultibodyJointHandle,
|
||||
): MultibodyJoint {
|
||||
switch (rawSet.jointType(handle)) {
|
||||
case RawJointType.Revolute:
|
||||
return new RevoluteMultibodyJoint(rawSet, handle);
|
||||
case RawJointType.Prismatic:
|
||||
return new PrismaticMultibodyJoint(rawSet, handle);
|
||||
case RawJointType.Fixed:
|
||||
return new FixedMultibodyJoint(rawSet, handle);
|
||||
default:
|
||||
return new MultibodyJoint(rawSet, handle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this joint is still valid (i.e. that it has
|
||||
* not been deleted from the joint set yet).
|
||||
*/
|
||||
public isValid(): boolean {
|
||||
return this.rawSet.contains(this.handle);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * The unique integer identifier of the first rigid-body this joint it attached to.
|
||||
// */
|
||||
// public bodyHandle1(): RigidBodyHandle {
|
||||
// return this.rawSet.jointBodyHandle1(this.handle);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * The unique integer identifier of the second rigid-body this joint is attached to.
|
||||
// */
|
||||
// public bodyHandle2(): RigidBodyHandle {
|
||||
// return this.rawSet.jointBodyHandle2(this.handle);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * The type of this joint given as a string.
|
||||
// */
|
||||
// public type(): JointType {
|
||||
// return this.rawSet.jointType(this.handle);
|
||||
// }
|
||||
//
|
||||
// // #if DIM3
|
||||
// /**
|
||||
// * The rotation quaternion that aligns this joint's first local axis to the `x` axis.
|
||||
// */
|
||||
// public frameX1(): Rotation {
|
||||
// return RotationOps.fromRaw(this.rawSet.jointFrameX1(this.handle));
|
||||
// }
|
||||
//
|
||||
// // #endif
|
||||
//
|
||||
// // #if DIM3
|
||||
// /**
|
||||
// * The rotation matrix that aligns this joint's second local axis to the `x` axis.
|
||||
// */
|
||||
// public frameX2(): Rotation {
|
||||
// return RotationOps.fromRaw(this.rawSet.jointFrameX2(this.handle));
|
||||
// }
|
||||
//
|
||||
// // #endif
|
||||
//
|
||||
// /**
|
||||
// * The position of the first anchor of this joint.
|
||||
// *
|
||||
// * The first anchor gives the position of the points application point on the
|
||||
// * local frame of the first rigid-body it is attached to.
|
||||
// */
|
||||
// public anchor1(): Vector {
|
||||
// return VectorOps.fromRaw(this.rawSet.jointAnchor1(this.handle));
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * The position of the second anchor of this joint.
|
||||
// *
|
||||
// * The second anchor gives the position of the points application point on the
|
||||
// * local frame of the second rigid-body it is attached to.
|
||||
// */
|
||||
// public anchor2(): Vector {
|
||||
// return VectorOps.fromRaw(this.rawSet.jointAnchor2(this.handle));
|
||||
// }
|
||||
|
||||
/**
|
||||
* Controls whether contacts are computed between colliders attached
|
||||
* to the rigid-bodies linked by this joint.
|
||||
*/
|
||||
public setContactsEnabled(enabled: boolean) {
|
||||
this.rawSet.jointSetContactsEnabled(this.handle, enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if contacts are enabled between colliders attached
|
||||
* to the rigid-bodies linked by this joint.
|
||||
*/
|
||||
public contactsEnabled(): boolean {
|
||||
return this.rawSet.jointContactsEnabled(this.handle);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnitMultibodyJoint extends MultibodyJoint {
|
||||
/**
|
||||
* The axis left free by this joint.
|
||||
*/
|
||||
protected rawAxis?(): RawJointAxis;
|
||||
|
||||
// /**
|
||||
// * Are the limits enabled for this joint?
|
||||
// */
|
||||
// public limitsEnabled(): boolean {
|
||||
// return this.rawSet.jointLimitsEnabled(this.handle, this.rawAxis());
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * The min limit of this joint.
|
||||
// */
|
||||
// public limitsMin(): number {
|
||||
// return this.rawSet.jointLimitsMin(this.handle, this.rawAxis());
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * The max limit of this joint.
|
||||
// */
|
||||
// public limitsMax(): number {
|
||||
// return this.rawSet.jointLimitsMax(this.handle, this.rawAxis());
|
||||
// }
|
||||
//
|
||||
// public configureMotorModel(model: MotorModel) {
|
||||
// this.rawSet.jointConfigureMotorModel(this.handle, this.rawAxis(), model);
|
||||
// }
|
||||
//
|
||||
// public configureMotorVelocity(targetVel: number, factor: number) {
|
||||
// this.rawSet.jointConfigureMotorVelocity(this.handle, this.rawAxis(), targetVel, factor);
|
||||
// }
|
||||
//
|
||||
// public configureMotorPosition(targetPos: number, stiffness: number, damping: number) {
|
||||
// this.rawSet.jointConfigureMotorPosition(this.handle, this.rawAxis(), targetPos, stiffness, damping);
|
||||
// }
|
||||
//
|
||||
// public configureMotor(targetPos: number, targetVel: number, stiffness: number, damping: number) {
|
||||
// this.rawSet.jointConfigureMotor(this.handle, this.rawAxis(), targetPos, targetVel, stiffness, damping);
|
||||
// }
|
||||
}
|
||||
|
||||
export class FixedMultibodyJoint extends MultibodyJoint {}
|
||||
|
||||
export class PrismaticMultibodyJoint extends UnitMultibodyJoint {
|
||||
public rawAxis(): RawJointAxis {
|
||||
return RawJointAxis.LinX;
|
||||
}
|
||||
}
|
||||
|
||||
export class RevoluteMultibodyJoint extends UnitMultibodyJoint {
|
||||
public rawAxis(): RawJointAxis {
|
||||
return RawJointAxis.AngX;
|
||||
}
|
||||
}
|
||||
|
||||
154
packages/physics/rapier2d/src/dynamics/multibody_joint_set.ts
Normal file
154
packages/physics/rapier2d/src/dynamics/multibody_joint_set.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import {RawMultibodyJointSet} from "../raw";
|
||||
import {Coarena} from "../coarena";
|
||||
import {RigidBodySet} from "./rigid_body_set";
|
||||
import {
|
||||
MultibodyJoint,
|
||||
MultibodyJointHandle,
|
||||
RevoluteMultibodyJoint,
|
||||
FixedMultibodyJoint,
|
||||
PrismaticMultibodyJoint,
|
||||
} from "./multibody_joint";
|
||||
import {ImpulseJointHandle, JointData, JointType} from "./impulse_joint";
|
||||
import {IslandManager} from "./island_manager";
|
||||
import {ColliderHandle} from "../geometry";
|
||||
import {RigidBodyHandle} from "./rigid_body";
|
||||
|
||||
/**
|
||||
* A set of joints.
|
||||
*
|
||||
* To avoid leaking WASM resources, this MUST be freed manually with `jointSet.free()`
|
||||
* once you are done using it (and all the joints it created).
|
||||
*/
|
||||
export class MultibodyJointSet {
|
||||
raw: RawMultibodyJointSet;
|
||||
private map: Coarena<MultibodyJoint>;
|
||||
|
||||
/**
|
||||
* Release the WASM memory occupied by this joint set.
|
||||
*/
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
this.raw = undefined;
|
||||
|
||||
if (!!this.map) {
|
||||
this.map.clear();
|
||||
}
|
||||
this.map = undefined;
|
||||
}
|
||||
|
||||
constructor(raw?: RawMultibodyJointSet) {
|
||||
this.raw = raw || new RawMultibodyJointSet();
|
||||
this.map = new Coarena<MultibodyJoint>();
|
||||
// Initialize the map with the existing elements, if any.
|
||||
if (raw) {
|
||||
raw.forEachJointHandle((handle: MultibodyJointHandle) => {
|
||||
this.map.set(handle, MultibodyJoint.newTyped(this.raw, handle));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new joint and return its integer handle.
|
||||
*
|
||||
* @param desc - The joint's parameters.
|
||||
* @param parent1 - The handle of the first rigid-body this joint is attached to.
|
||||
* @param parent2 - The handle of the second rigid-body this joint is attached to.
|
||||
* @param wakeUp - Should the attached rigid-bodies be awakened?
|
||||
*/
|
||||
public createJoint(
|
||||
desc: JointData,
|
||||
parent1: RigidBodyHandle,
|
||||
parent2: RigidBodyHandle,
|
||||
wakeUp: boolean,
|
||||
): MultibodyJoint {
|
||||
const rawParams = desc.intoRaw();
|
||||
const handle = this.raw.createJoint(
|
||||
rawParams,
|
||||
parent1,
|
||||
parent2,
|
||||
wakeUp,
|
||||
);
|
||||
rawParams.free();
|
||||
let joint = MultibodyJoint.newTyped(this.raw, handle);
|
||||
this.map.set(handle, joint);
|
||||
return joint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a joint from this set.
|
||||
*
|
||||
* @param handle - The integer handle of the joint.
|
||||
* @param wake_up - If `true`, the rigid-bodies attached by the removed joint will be woken-up automatically.
|
||||
*/
|
||||
public remove(handle: MultibodyJointHandle, wake_up: boolean) {
|
||||
this.raw.remove(handle, wake_up);
|
||||
this.map.delete(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal function, do not call directly.
|
||||
* @param handle
|
||||
*/
|
||||
public unmap(handle: MultibodyJointHandle) {
|
||||
this.map.delete(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of joints on this set.
|
||||
*/
|
||||
public len(): number {
|
||||
return this.map.len();
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this set contain a joint with the given handle?
|
||||
*
|
||||
* @param handle - The joint handle to check.
|
||||
*/
|
||||
public contains(handle: MultibodyJointHandle): boolean {
|
||||
return this.get(handle) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the joint with the given handle.
|
||||
*
|
||||
* Returns `null` if no joint with the specified handle exists.
|
||||
*
|
||||
* @param handle - The integer handle of the joint to retrieve.
|
||||
*/
|
||||
public get(handle: MultibodyJointHandle): MultibodyJoint | null {
|
||||
return this.map.get(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given closure to each joint contained by this set.
|
||||
*
|
||||
* @param f - The closure to apply.
|
||||
*/
|
||||
public forEach(f: (joint: MultibodyJoint) => void) {
|
||||
this.map.forEach(f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the given closure with the integer handle of each multibody joint attached to this rigid-body.
|
||||
*
|
||||
* @param f - The closure called with the integer handle of each multibody joint attached to the rigid-body.
|
||||
*/
|
||||
public forEachJointHandleAttachedToRigidBody(
|
||||
handle: RigidBodyHandle,
|
||||
f: (handle: MultibodyJointHandle) => void,
|
||||
) {
|
||||
this.raw.forEachJointAttachedToRigidBody(handle, f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all joints in the list.
|
||||
*
|
||||
* @returns joint list.
|
||||
*/
|
||||
public getAll(): MultibodyJoint[] {
|
||||
return this.map.getAll();
|
||||
}
|
||||
}
|
||||
1162
packages/physics/rapier2d/src/dynamics/rigid_body.ts
Normal file
1162
packages/physics/rapier2d/src/dynamics/rigid_body.ts
Normal file
File diff suppressed because it is too large
Load Diff
211
packages/physics/rapier2d/src/dynamics/rigid_body_set.ts
Normal file
211
packages/physics/rapier2d/src/dynamics/rigid_body_set.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import {RawRigidBodySet, RawRigidBodyType} from "../raw";
|
||||
import {Coarena} from "../coarena";
|
||||
import {VectorOps, RotationOps} from "../math";
|
||||
import {
|
||||
RigidBody,
|
||||
RigidBodyDesc,
|
||||
RigidBodyHandle,
|
||||
RigidBodyType,
|
||||
} from "./rigid_body";
|
||||
import {ColliderSet} from "../geometry";
|
||||
import {ImpulseJointSet} from "./impulse_joint_set";
|
||||
import {MultibodyJointSet} from "./multibody_joint_set";
|
||||
import {IslandManager} from "./island_manager";
|
||||
|
||||
/**
|
||||
* A set of rigid bodies that can be handled by a physics pipeline.
|
||||
*
|
||||
* To avoid leaking WASM resources, this MUST be freed manually with `rigidBodySet.free()`
|
||||
* once you are done using it (and all the rigid-bodies it created).
|
||||
*/
|
||||
export class RigidBodySet {
|
||||
raw: RawRigidBodySet;
|
||||
private map: Coarena<RigidBody>;
|
||||
|
||||
/**
|
||||
* Release the WASM memory occupied by this rigid-body set.
|
||||
*/
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
this.raw = undefined;
|
||||
|
||||
if (!!this.map) {
|
||||
this.map.clear();
|
||||
}
|
||||
this.map = undefined;
|
||||
}
|
||||
|
||||
constructor(raw?: RawRigidBodySet) {
|
||||
this.raw = raw || new RawRigidBodySet();
|
||||
this.map = new Coarena<RigidBody>();
|
||||
// deserialize
|
||||
if (raw) {
|
||||
raw.forEachRigidBodyHandle((handle: RigidBodyHandle) => {
|
||||
this.map.set(handle, new RigidBody(raw, null, handle));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method, do not call this explicitly.
|
||||
*/
|
||||
public finalizeDeserialization(colliderSet: ColliderSet) {
|
||||
this.map.forEach((rb) => rb.finalizeDeserialization(colliderSet));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new rigid-body and return its integer handle.
|
||||
*
|
||||
* @param desc - The description of the rigid-body to create.
|
||||
*/
|
||||
public createRigidBody(
|
||||
colliderSet: ColliderSet,
|
||||
desc: RigidBodyDesc,
|
||||
): RigidBody {
|
||||
let rawTra = VectorOps.intoRaw(desc.translation);
|
||||
let rawRot = RotationOps.intoRaw(desc.rotation);
|
||||
let rawLv = VectorOps.intoRaw(desc.linvel);
|
||||
let rawCom = VectorOps.intoRaw(desc.centerOfMass);
|
||||
|
||||
|
||||
let handle = this.raw.createRigidBody(
|
||||
desc.enabled,
|
||||
rawTra,
|
||||
rawRot,
|
||||
desc.gravityScale,
|
||||
desc.mass,
|
||||
desc.massOnly,
|
||||
rawCom,
|
||||
rawLv,
|
||||
desc.angvel,
|
||||
desc.principalAngularInertia,
|
||||
desc.translationsEnabledX,
|
||||
desc.translationsEnabledY,
|
||||
desc.rotationsEnabled,
|
||||
desc.linearDamping,
|
||||
desc.angularDamping,
|
||||
desc.status as number as RawRigidBodyType,
|
||||
desc.canSleep,
|
||||
desc.sleeping,
|
||||
desc.softCcdPrediction,
|
||||
desc.ccdEnabled,
|
||||
desc.dominanceGroup,
|
||||
desc.additionalSolverIterations,
|
||||
);
|
||||
|
||||
rawTra.free();
|
||||
rawRot.free();
|
||||
rawLv.free();
|
||||
rawCom.free();
|
||||
|
||||
|
||||
const body = new RigidBody(this.raw, colliderSet, handle);
|
||||
body.userData = desc.userData;
|
||||
|
||||
this.map.set(handle, body);
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a rigid-body from this set.
|
||||
*
|
||||
* This will also remove all the colliders and joints attached to the rigid-body.
|
||||
*
|
||||
* @param handle - The integer handle of the rigid-body to remove.
|
||||
* @param colliders - The set of colliders that may contain colliders attached to the removed rigid-body.
|
||||
* @param impulseJoints - The set of impulse joints that may contain joints attached to the removed rigid-body.
|
||||
* @param multibodyJoints - The set of multibody joints that may contain joints attached to the removed rigid-body.
|
||||
*/
|
||||
public remove(
|
||||
handle: RigidBodyHandle,
|
||||
islands: IslandManager,
|
||||
colliders: ColliderSet,
|
||||
impulseJoints: ImpulseJointSet,
|
||||
multibodyJoints: MultibodyJointSet,
|
||||
) {
|
||||
// Unmap the entities that will be removed automatically because of the rigid-body removals.
|
||||
for (let i = 0; i < this.raw.rbNumColliders(handle); i += 1) {
|
||||
colliders.unmap(this.raw.rbCollider(handle, i));
|
||||
}
|
||||
|
||||
impulseJoints.forEachJointHandleAttachedToRigidBody(handle, (handle) =>
|
||||
impulseJoints.unmap(handle),
|
||||
);
|
||||
multibodyJoints.forEachJointHandleAttachedToRigidBody(
|
||||
handle,
|
||||
(handle) => multibodyJoints.unmap(handle),
|
||||
);
|
||||
|
||||
// Remove the rigid-body.
|
||||
this.raw.remove(
|
||||
handle,
|
||||
islands.raw,
|
||||
colliders.raw,
|
||||
impulseJoints.raw,
|
||||
multibodyJoints.raw,
|
||||
);
|
||||
this.map.delete(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of rigid-bodies on this set.
|
||||
*/
|
||||
public len(): number {
|
||||
return this.map.len();
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this set contain a rigid-body with the given handle?
|
||||
*
|
||||
* @param handle - The rigid-body handle to check.
|
||||
*/
|
||||
public contains(handle: RigidBodyHandle): boolean {
|
||||
return this.get(handle) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the rigid-body with the given handle.
|
||||
*
|
||||
* @param handle - The handle of the rigid-body to retrieve.
|
||||
*/
|
||||
public get(handle: RigidBodyHandle): RigidBody | null {
|
||||
return this.map.get(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given closure to each rigid-body contained by this set.
|
||||
*
|
||||
* @param f - The closure to apply.
|
||||
*/
|
||||
public forEach(f: (body: RigidBody) => void) {
|
||||
this.map.forEach(f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given closure to each active rigid-bodies contained by this set.
|
||||
*
|
||||
* A rigid-body is active if it is not sleeping, i.e., if it moved recently.
|
||||
*
|
||||
* @param f - The closure to apply.
|
||||
*/
|
||||
public forEachActiveRigidBody(
|
||||
islands: IslandManager,
|
||||
f: (body: RigidBody) => void,
|
||||
) {
|
||||
islands.forEachActiveRigidBodyHandle((handle) => {
|
||||
f(this.get(handle));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all rigid-bodies in the list.
|
||||
*
|
||||
* @returns rigid-bodies list.
|
||||
*/
|
||||
public getAll(): RigidBody[] {
|
||||
return this.map.getAll();
|
||||
}
|
||||
}
|
||||
27
packages/physics/rapier2d/src/exports.ts
Normal file
27
packages/physics/rapier2d/src/exports.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {version as vers, reserve_memory as reserve} from "./raw";
|
||||
|
||||
export function version(): string {
|
||||
return vers();
|
||||
}
|
||||
|
||||
/// Reserves additional memory in WASM land.
|
||||
///
|
||||
/// This will grow the internal WASM memory buffer so that it can fit at least
|
||||
/// the specified amount of extra bytes. This can help reduce future runtime
|
||||
/// overhead due to dynamic internal memory growth once the limit of the
|
||||
/// pre-allocated memory is reached.
|
||||
///
|
||||
/// This feature is still experimental. Due to the nature of the internal
|
||||
/// allocator, there can be situations where the allocator decides to perform
|
||||
/// additional internal memory growth even though not all `extraBytesCount`
|
||||
/// are occupied yet.
|
||||
export function reserveMemory(extraBytesCount: number) {
|
||||
reserve(extraBytesCount);
|
||||
}
|
||||
|
||||
export * from "./math";
|
||||
export * from "./dynamics";
|
||||
export * from "./geometry";
|
||||
export * from "./pipeline";
|
||||
export * from "./init";
|
||||
export * from "./control";
|
||||
520
packages/physics/rapier2d/src/geometry/broad_phase.ts
Normal file
520
packages/physics/rapier2d/src/geometry/broad_phase.ts
Normal file
@@ -0,0 +1,520 @@
|
||||
import {RawBroadPhase, RawRayColliderIntersection} from "../raw";
|
||||
import {RigidBodyHandle, RigidBodySet} from "../dynamics";
|
||||
import {ColliderSet} from "./collider_set";
|
||||
import {Ray, RayColliderHit, RayColliderIntersection} from "./ray";
|
||||
import {InteractionGroups} from "./interaction_groups";
|
||||
import {ColliderHandle} from "./collider";
|
||||
import {Rotation, RotationOps, Vector, VectorOps} from "../math";
|
||||
import {Shape} from "./shape";
|
||||
import {PointColliderProjection} from "./point";
|
||||
import {ColliderShapeCastHit} from "./toi";
|
||||
import {QueryFilterFlags} from "../pipeline";
|
||||
import {NarrowPhase} from "./narrow_phase";
|
||||
|
||||
/**
|
||||
* The broad-phase used for coarse collision-detection.
|
||||
*
|
||||
* To avoid leaking WASM resources, this MUST be freed manually with `broadPhase.free()`
|
||||
* once you are done using it.
|
||||
*/
|
||||
export class BroadPhase {
|
||||
raw: RawBroadPhase;
|
||||
|
||||
/**
|
||||
* Release the WASM memory occupied by this broad-phase.
|
||||
*/
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
this.raw = undefined;
|
||||
}
|
||||
|
||||
constructor(raw?: RawBroadPhase) {
|
||||
this.raw = raw || new RawBroadPhase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest intersection between a ray and a set of collider.
|
||||
*
|
||||
* @param colliders - The set of colliders taking part in this pipeline.
|
||||
* @param ray - The ray to cast.
|
||||
* @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively
|
||||
* limits the length of the ray to `ray.dir.norm() * maxToi`.
|
||||
* @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its
|
||||
* origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain,
|
||||
* whereas `false` implies that all shapes are hollow for this ray-cast.
|
||||
* @param groups - Used to filter the colliders that can or cannot be hit by the ray.
|
||||
* @param filter - The callback to filter out which collider will be hit.
|
||||
*/
|
||||
public castRay(
|
||||
narrowPhase: NarrowPhase,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
ray: Ray,
|
||||
maxToi: number,
|
||||
solid: boolean,
|
||||
filterFlags?: QueryFilterFlags,
|
||||
filterGroups?: InteractionGroups,
|
||||
filterExcludeCollider?: ColliderHandle,
|
||||
filterExcludeRigidBody?: RigidBodyHandle,
|
||||
filterPredicate?: (collider: ColliderHandle) => boolean,
|
||||
): RayColliderHit | null {
|
||||
let rawOrig = VectorOps.intoRaw(ray.origin);
|
||||
let rawDir = VectorOps.intoRaw(ray.dir);
|
||||
let result = RayColliderHit.fromRaw(
|
||||
colliders,
|
||||
this.raw.castRay(
|
||||
narrowPhase.raw,
|
||||
bodies.raw,
|
||||
colliders.raw,
|
||||
rawOrig,
|
||||
rawDir,
|
||||
maxToi,
|
||||
solid,
|
||||
filterFlags,
|
||||
filterGroups,
|
||||
filterExcludeCollider,
|
||||
filterExcludeRigidBody,
|
||||
filterPredicate,
|
||||
),
|
||||
);
|
||||
|
||||
rawOrig.free();
|
||||
rawDir.free();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest intersection between a ray and a set of collider.
|
||||
*
|
||||
* This also computes the normal at the hit point.
|
||||
* @param colliders - The set of colliders taking part in this pipeline.
|
||||
* @param ray - The ray to cast.
|
||||
* @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively
|
||||
* limits the length of the ray to `ray.dir.norm() * maxToi`.
|
||||
* @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its
|
||||
* origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain,
|
||||
* whereas `false` implies that all shapes are hollow for this ray-cast.
|
||||
* @param groups - Used to filter the colliders that can or cannot be hit by the ray.
|
||||
*/
|
||||
public castRayAndGetNormal(
|
||||
narrowPhase: NarrowPhase,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
ray: Ray,
|
||||
maxToi: number,
|
||||
solid: boolean,
|
||||
filterFlags?: QueryFilterFlags,
|
||||
filterGroups?: InteractionGroups,
|
||||
filterExcludeCollider?: ColliderHandle,
|
||||
filterExcludeRigidBody?: RigidBodyHandle,
|
||||
filterPredicate?: (collider: ColliderHandle) => boolean,
|
||||
): RayColliderIntersection | null {
|
||||
let rawOrig = VectorOps.intoRaw(ray.origin);
|
||||
let rawDir = VectorOps.intoRaw(ray.dir);
|
||||
let result = RayColliderIntersection.fromRaw(
|
||||
colliders,
|
||||
this.raw.castRayAndGetNormal(
|
||||
narrowPhase.raw,
|
||||
bodies.raw,
|
||||
colliders.raw,
|
||||
rawOrig,
|
||||
rawDir,
|
||||
maxToi,
|
||||
solid,
|
||||
filterFlags,
|
||||
filterGroups,
|
||||
filterExcludeCollider,
|
||||
filterExcludeRigidBody,
|
||||
filterPredicate,
|
||||
),
|
||||
);
|
||||
|
||||
rawOrig.free();
|
||||
rawDir.free();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast a ray and collects all the intersections between a ray and the scene.
|
||||
*
|
||||
* @param colliders - The set of colliders taking part in this pipeline.
|
||||
* @param ray - The ray to cast.
|
||||
* @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively
|
||||
* limits the length of the ray to `ray.dir.norm() * maxToi`.
|
||||
* @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its
|
||||
* origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain,
|
||||
* whereas `false` implies that all shapes are hollow for this ray-cast.
|
||||
* @param groups - Used to filter the colliders that can or cannot be hit by the ray.
|
||||
* @param callback - The callback called once per hit (in no particular order) between a ray and a collider.
|
||||
* If this callback returns `false`, then the cast will stop and no further hits will be detected/reported.
|
||||
*/
|
||||
public intersectionsWithRay(
|
||||
narrowPhase: NarrowPhase,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
ray: Ray,
|
||||
maxToi: number,
|
||||
solid: boolean,
|
||||
callback: (intersect: RayColliderIntersection) => boolean,
|
||||
filterFlags?: QueryFilterFlags,
|
||||
filterGroups?: InteractionGroups,
|
||||
filterExcludeCollider?: ColliderHandle,
|
||||
filterExcludeRigidBody?: RigidBodyHandle,
|
||||
filterPredicate?: (collider: ColliderHandle) => boolean,
|
||||
) {
|
||||
let rawOrig = VectorOps.intoRaw(ray.origin);
|
||||
let rawDir = VectorOps.intoRaw(ray.dir);
|
||||
let rawCallback = (rawInter: RawRayColliderIntersection) => {
|
||||
return callback(
|
||||
RayColliderIntersection.fromRaw(colliders, rawInter),
|
||||
);
|
||||
};
|
||||
|
||||
this.raw.intersectionsWithRay(
|
||||
narrowPhase.raw,
|
||||
bodies.raw,
|
||||
colliders.raw,
|
||||
rawOrig,
|
||||
rawDir,
|
||||
maxToi,
|
||||
solid,
|
||||
rawCallback,
|
||||
filterFlags,
|
||||
filterGroups,
|
||||
filterExcludeCollider,
|
||||
filterExcludeRigidBody,
|
||||
filterPredicate,
|
||||
);
|
||||
|
||||
rawOrig.free();
|
||||
rawDir.free();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the handle of up to one collider intersecting the given shape.
|
||||
*
|
||||
* @param colliders - The set of colliders taking part in this pipeline.
|
||||
* @param shapePos - The position of the shape used for the intersection test.
|
||||
* @param shapeRot - The orientation of the shape used for the intersection test.
|
||||
* @param shape - The shape used for the intersection test.
|
||||
* @param groups - The bit groups and filter associated to the ray, in order to only
|
||||
* hit the colliders with collision groups compatible with the ray's group.
|
||||
*/
|
||||
public intersectionWithShape(
|
||||
narrowPhase: NarrowPhase,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
shapePos: Vector,
|
||||
shapeRot: Rotation,
|
||||
shape: Shape,
|
||||
filterFlags?: QueryFilterFlags,
|
||||
filterGroups?: InteractionGroups,
|
||||
filterExcludeCollider?: ColliderHandle,
|
||||
filterExcludeRigidBody?: RigidBodyHandle,
|
||||
filterPredicate?: (collider: ColliderHandle) => boolean,
|
||||
): ColliderHandle | null {
|
||||
let rawPos = VectorOps.intoRaw(shapePos);
|
||||
let rawRot = RotationOps.intoRaw(shapeRot);
|
||||
let rawShape = shape.intoRaw();
|
||||
let result = this.raw.intersectionWithShape(
|
||||
narrowPhase.raw,
|
||||
bodies.raw,
|
||||
colliders.raw,
|
||||
rawPos,
|
||||
rawRot,
|
||||
rawShape,
|
||||
filterFlags,
|
||||
filterGroups,
|
||||
filterExcludeCollider,
|
||||
filterExcludeRigidBody,
|
||||
filterPredicate,
|
||||
);
|
||||
|
||||
rawPos.free();
|
||||
rawRot.free();
|
||||
rawShape.free();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the projection of a point on the closest collider.
|
||||
*
|
||||
* @param colliders - The set of colliders taking part in this pipeline.
|
||||
* @param point - The point to project.
|
||||
* @param solid - If this is set to `true` then the collider shapes are considered to
|
||||
* be plain (if the point is located inside of a plain shape, its projection is the point
|
||||
* itself). If it is set to `false` the collider shapes are considered to be hollow
|
||||
* (if the point is located inside of an hollow shape, it is projected on the shape's
|
||||
* boundary).
|
||||
* @param groups - The bit groups and filter associated to the point to project, in order to only
|
||||
* project on colliders with collision groups compatible with the ray's group.
|
||||
*/
|
||||
public projectPoint(
|
||||
narrowPhase: NarrowPhase,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
point: Vector,
|
||||
solid: boolean,
|
||||
filterFlags?: QueryFilterFlags,
|
||||
filterGroups?: InteractionGroups,
|
||||
filterExcludeCollider?: ColliderHandle,
|
||||
filterExcludeRigidBody?: RigidBodyHandle,
|
||||
filterPredicate?: (collider: ColliderHandle) => boolean,
|
||||
): PointColliderProjection | null {
|
||||
let rawPoint = VectorOps.intoRaw(point);
|
||||
let result = PointColliderProjection.fromRaw(
|
||||
colliders,
|
||||
this.raw.projectPoint(
|
||||
narrowPhase.raw,
|
||||
bodies.raw,
|
||||
colliders.raw,
|
||||
rawPoint,
|
||||
solid,
|
||||
filterFlags,
|
||||
filterGroups,
|
||||
filterExcludeCollider,
|
||||
filterExcludeRigidBody,
|
||||
filterPredicate,
|
||||
),
|
||||
);
|
||||
|
||||
rawPoint.free();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the projection of a point on the closest collider.
|
||||
*
|
||||
* @param colliders - The set of colliders taking part in this pipeline.
|
||||
* @param point - The point to project.
|
||||
* @param groups - The bit groups and filter associated to the point to project, in order to only
|
||||
* project on colliders with collision groups compatible with the ray's group.
|
||||
*/
|
||||
public projectPointAndGetFeature(
|
||||
narrowPhase: NarrowPhase,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
point: Vector,
|
||||
filterFlags?: QueryFilterFlags,
|
||||
filterGroups?: InteractionGroups,
|
||||
filterExcludeCollider?: ColliderHandle,
|
||||
filterExcludeRigidBody?: RigidBodyHandle,
|
||||
filterPredicate?: (collider: ColliderHandle) => boolean,
|
||||
): PointColliderProjection | null {
|
||||
let rawPoint = VectorOps.intoRaw(point);
|
||||
let result = PointColliderProjection.fromRaw(
|
||||
colliders,
|
||||
this.raw.projectPointAndGetFeature(
|
||||
narrowPhase.raw,
|
||||
bodies.raw,
|
||||
colliders.raw,
|
||||
rawPoint,
|
||||
filterFlags,
|
||||
filterGroups,
|
||||
filterExcludeCollider,
|
||||
filterExcludeRigidBody,
|
||||
filterPredicate,
|
||||
),
|
||||
);
|
||||
|
||||
rawPoint.free();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all the colliders containing the given point.
|
||||
*
|
||||
* @param colliders - The set of colliders taking part in this pipeline.
|
||||
* @param point - The point used for the containment test.
|
||||
* @param groups - The bit groups and filter associated to the point to test, in order to only
|
||||
* test on colliders with collision groups compatible with the ray's group.
|
||||
* @param callback - A function called with the handles of each collider with a shape
|
||||
* containing the `point`.
|
||||
*/
|
||||
public intersectionsWithPoint(
|
||||
narrowPhase: NarrowPhase,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
point: Vector,
|
||||
callback: (handle: ColliderHandle) => boolean,
|
||||
filterFlags?: QueryFilterFlags,
|
||||
filterGroups?: InteractionGroups,
|
||||
filterExcludeCollider?: ColliderHandle,
|
||||
filterExcludeRigidBody?: RigidBodyHandle,
|
||||
filterPredicate?: (collider: ColliderHandle) => boolean,
|
||||
) {
|
||||
let rawPoint = VectorOps.intoRaw(point);
|
||||
|
||||
this.raw.intersectionsWithPoint(
|
||||
narrowPhase.raw,
|
||||
bodies.raw,
|
||||
colliders.raw,
|
||||
rawPoint,
|
||||
callback,
|
||||
filterFlags,
|
||||
filterGroups,
|
||||
filterExcludeCollider,
|
||||
filterExcludeRigidBody,
|
||||
filterPredicate,
|
||||
);
|
||||
|
||||
rawPoint.free();
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts a shape at a constant linear velocity and retrieve the first collider it hits.
|
||||
* This is similar to ray-casting except that we are casting a whole shape instead of
|
||||
* just a point (the ray origin).
|
||||
*
|
||||
* @param colliders - The set of colliders taking part in this pipeline.
|
||||
* @param shapePos - The initial position of the shape to cast.
|
||||
* @param shapeRot - The initial rotation of the shape to cast.
|
||||
* @param shapeVel - The constant velocity of the shape to cast (i.e. the cast direction).
|
||||
* @param shape - The shape to cast.
|
||||
* @param targetDistance − If the shape moves closer to this distance from a collider, a hit
|
||||
* will be returned.
|
||||
* @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively
|
||||
* limits the distance traveled by the shape to `shapeVel.norm() * maxToi`.
|
||||
* @param stopAtPenetration - If set to `false`, the linear shape-cast won’t immediately stop if
|
||||
* the shape is penetrating another shape at its starting point **and** its trajectory is such
|
||||
* that it’s on a path to exit that penetration state.
|
||||
* @param groups - The bit groups and filter associated to the shape to cast, in order to only
|
||||
* test on colliders with collision groups compatible with this group.
|
||||
*/
|
||||
public castShape(
|
||||
narrowPhase: NarrowPhase,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
shapePos: Vector,
|
||||
shapeRot: Rotation,
|
||||
shapeVel: Vector,
|
||||
shape: Shape,
|
||||
targetDistance: number,
|
||||
maxToi: number,
|
||||
stopAtPenetration: boolean,
|
||||
filterFlags?: QueryFilterFlags,
|
||||
filterGroups?: InteractionGroups,
|
||||
filterExcludeCollider?: ColliderHandle,
|
||||
filterExcludeRigidBody?: RigidBodyHandle,
|
||||
filterPredicate?: (collider: ColliderHandle) => boolean,
|
||||
): ColliderShapeCastHit | null {
|
||||
let rawPos = VectorOps.intoRaw(shapePos);
|
||||
let rawRot = RotationOps.intoRaw(shapeRot);
|
||||
let rawVel = VectorOps.intoRaw(shapeVel);
|
||||
let rawShape = shape.intoRaw();
|
||||
|
||||
let result = ColliderShapeCastHit.fromRaw(
|
||||
colliders,
|
||||
this.raw.castShape(
|
||||
narrowPhase.raw,
|
||||
bodies.raw,
|
||||
colliders.raw,
|
||||
rawPos,
|
||||
rawRot,
|
||||
rawVel,
|
||||
rawShape,
|
||||
targetDistance,
|
||||
maxToi,
|
||||
stopAtPenetration,
|
||||
filterFlags,
|
||||
filterGroups,
|
||||
filterExcludeCollider,
|
||||
filterExcludeRigidBody,
|
||||
filterPredicate,
|
||||
),
|
||||
);
|
||||
|
||||
rawPos.free();
|
||||
rawRot.free();
|
||||
rawVel.free();
|
||||
rawShape.free();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all the colliders intersecting the given shape.
|
||||
*
|
||||
* @param colliders - The set of colliders taking part in this pipeline.
|
||||
* @param shapePos - The position of the shape to test.
|
||||
* @param shapeRot - The orientation of the shape to test.
|
||||
* @param shape - The shape to test.
|
||||
* @param groups - The bit groups and filter associated to the shape to test, in order to only
|
||||
* test on colliders with collision groups compatible with this group.
|
||||
* @param callback - A function called with the handles of each collider intersecting the `shape`.
|
||||
*/
|
||||
public intersectionsWithShape(
|
||||
narrowPhase: NarrowPhase,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
shapePos: Vector,
|
||||
shapeRot: Rotation,
|
||||
shape: Shape,
|
||||
callback: (handle: ColliderHandle) => boolean,
|
||||
filterFlags?: QueryFilterFlags,
|
||||
filterGroups?: InteractionGroups,
|
||||
filterExcludeCollider?: ColliderHandle,
|
||||
filterExcludeRigidBody?: RigidBodyHandle,
|
||||
filterPredicate?: (collider: ColliderHandle) => boolean,
|
||||
) {
|
||||
let rawPos = VectorOps.intoRaw(shapePos);
|
||||
let rawRot = RotationOps.intoRaw(shapeRot);
|
||||
let rawShape = shape.intoRaw();
|
||||
|
||||
this.raw.intersectionsWithShape(
|
||||
narrowPhase.raw,
|
||||
bodies.raw,
|
||||
colliders.raw,
|
||||
rawPos,
|
||||
rawRot,
|
||||
rawShape,
|
||||
callback,
|
||||
filterFlags,
|
||||
filterGroups,
|
||||
filterExcludeCollider,
|
||||
filterExcludeRigidBody,
|
||||
filterPredicate,
|
||||
);
|
||||
|
||||
rawPos.free();
|
||||
rawRot.free();
|
||||
rawShape.free();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the handles of all the colliders with an AABB intersecting the given AABB.
|
||||
*
|
||||
* @param aabbCenter - The center of the AABB to test.
|
||||
* @param aabbHalfExtents - The half-extents of the AABB to test.
|
||||
* @param callback - The callback that will be called with the handles of all the colliders
|
||||
* currently intersecting the given AABB.
|
||||
*/
|
||||
public collidersWithAabbIntersectingAabb(
|
||||
narrowPhase: NarrowPhase,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
aabbCenter: Vector,
|
||||
aabbHalfExtents: Vector,
|
||||
callback: (handle: ColliderHandle) => boolean,
|
||||
) {
|
||||
let rawCenter = VectorOps.intoRaw(aabbCenter);
|
||||
let rawHalfExtents = VectorOps.intoRaw(aabbHalfExtents);
|
||||
this.raw.collidersWithAabbIntersectingAabb(
|
||||
narrowPhase.raw,
|
||||
bodies.raw,
|
||||
colliders.raw,
|
||||
rawCenter,
|
||||
rawHalfExtents,
|
||||
callback,
|
||||
);
|
||||
rawCenter.free();
|
||||
rawHalfExtents.free();
|
||||
}
|
||||
}
|
||||
1603
packages/physics/rapier2d/src/geometry/collider.ts
Normal file
1603
packages/physics/rapier2d/src/geometry/collider.ts
Normal file
File diff suppressed because it is too large
Load Diff
195
packages/physics/rapier2d/src/geometry/collider_set.ts
Normal file
195
packages/physics/rapier2d/src/geometry/collider_set.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import {RawColliderSet} from "../raw";
|
||||
import {Coarena} from "../coarena";
|
||||
import {RotationOps, VectorOps} from "../math";
|
||||
import {Collider, ColliderDesc, ColliderHandle} from "./collider";
|
||||
import {ImpulseJointHandle, IslandManager, RigidBodyHandle} from "../dynamics";
|
||||
import {RigidBodySet} from "../dynamics";
|
||||
|
||||
/**
|
||||
* A set of rigid bodies that can be handled by a physics pipeline.
|
||||
*
|
||||
* To avoid leaking WASM resources, this MUST be freed manually with `colliderSet.free()`
|
||||
* once you are done using it (and all the rigid-bodies it created).
|
||||
*/
|
||||
export class ColliderSet {
|
||||
raw: RawColliderSet;
|
||||
private map: Coarena<Collider>;
|
||||
|
||||
/**
|
||||
* Release the WASM memory occupied by this collider set.
|
||||
*/
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
this.raw = undefined;
|
||||
|
||||
if (!!this.map) {
|
||||
this.map.clear();
|
||||
}
|
||||
this.map = undefined;
|
||||
}
|
||||
|
||||
constructor(raw?: RawColliderSet) {
|
||||
this.raw = raw || new RawColliderSet();
|
||||
this.map = new Coarena<Collider>();
|
||||
// Initialize the map with the existing elements, if any.
|
||||
if (raw) {
|
||||
raw.forEachColliderHandle((handle: ColliderHandle) => {
|
||||
this.map.set(handle, new Collider(this, handle, null));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public castClosure<Res>(
|
||||
f?: (collider: Collider) => Res,
|
||||
): (handle: ColliderHandle) => Res | undefined {
|
||||
return (handle) => {
|
||||
if (!!f) {
|
||||
return f(this.get(handle));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public finalizeDeserialization(bodies: RigidBodySet) {
|
||||
this.map.forEach((collider) =>
|
||||
collider.finalizeDeserialization(bodies),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new collider and return its integer handle.
|
||||
*
|
||||
* @param bodies - The set of bodies where the collider's parent can be found.
|
||||
* @param desc - The collider's description.
|
||||
* @param parentHandle - The integer handle of the rigid-body this collider is attached to.
|
||||
*/
|
||||
public createCollider(
|
||||
bodies: RigidBodySet,
|
||||
desc: ColliderDesc,
|
||||
parentHandle: RigidBodyHandle,
|
||||
): Collider {
|
||||
let hasParent = parentHandle != undefined && parentHandle != null;
|
||||
|
||||
if (hasParent && isNaN(parentHandle))
|
||||
throw Error(
|
||||
"Cannot create a collider with a parent rigid-body handle that is not a number.",
|
||||
);
|
||||
|
||||
let rawShape = desc.shape.intoRaw();
|
||||
let rawTra = VectorOps.intoRaw(desc.translation);
|
||||
let rawRot = RotationOps.intoRaw(desc.rotation);
|
||||
let rawCom = VectorOps.intoRaw(desc.centerOfMass);
|
||||
|
||||
|
||||
let handle = this.raw.createCollider(
|
||||
desc.enabled,
|
||||
rawShape,
|
||||
rawTra,
|
||||
rawRot,
|
||||
desc.massPropsMode,
|
||||
desc.mass,
|
||||
rawCom,
|
||||
desc.principalAngularInertia,
|
||||
desc.density,
|
||||
desc.friction,
|
||||
desc.restitution,
|
||||
desc.frictionCombineRule,
|
||||
desc.restitutionCombineRule,
|
||||
desc.isSensor,
|
||||
desc.collisionGroups,
|
||||
desc.solverGroups,
|
||||
desc.activeCollisionTypes,
|
||||
desc.activeHooks,
|
||||
desc.activeEvents,
|
||||
desc.contactForceEventThreshold,
|
||||
desc.contactSkin,
|
||||
hasParent,
|
||||
hasParent ? parentHandle : 0,
|
||||
bodies.raw,
|
||||
);
|
||||
|
||||
rawShape.free();
|
||||
rawTra.free();
|
||||
rawRot.free();
|
||||
rawCom.free();
|
||||
|
||||
|
||||
let parent = hasParent ? bodies.get(parentHandle) : null;
|
||||
let collider = new Collider(this, handle, parent, desc.shape);
|
||||
this.map.set(handle, collider);
|
||||
return collider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a collider from this set.
|
||||
*
|
||||
* @param handle - The integer handle of the collider to remove.
|
||||
* @param bodies - The set of rigid-body containing the rigid-body the collider is attached to.
|
||||
* @param wakeUp - If `true`, the rigid-body the removed collider is attached to will be woken-up automatically.
|
||||
*/
|
||||
public remove(
|
||||
handle: ColliderHandle,
|
||||
islands: IslandManager,
|
||||
bodies: RigidBodySet,
|
||||
wakeUp: boolean,
|
||||
) {
|
||||
this.raw.remove(handle, islands.raw, bodies.raw, wakeUp);
|
||||
this.unmap(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal function, do not call directly.
|
||||
* @param handle
|
||||
*/
|
||||
public unmap(handle: ImpulseJointHandle) {
|
||||
this.map.delete(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the rigid-body with the given handle.
|
||||
*
|
||||
* @param handle - The handle of the rigid-body to retrieve.
|
||||
*/
|
||||
public get(handle: ColliderHandle): Collider | null {
|
||||
return this.map.get(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of colliders on this set.
|
||||
*/
|
||||
public len(): number {
|
||||
return this.map.len();
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this set contain a collider with the given handle?
|
||||
*
|
||||
* @param handle - The collider handle to check.
|
||||
*/
|
||||
public contains(handle: ColliderHandle): boolean {
|
||||
return this.get(handle) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given closure to each collider contained by this set.
|
||||
*
|
||||
* @param f - The closure to apply.
|
||||
*/
|
||||
public forEach(f: (collider: Collider) => void) {
|
||||
this.map.forEach(f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all colliders in the list.
|
||||
*
|
||||
* @returns collider list.
|
||||
*/
|
||||
public getAll(): Collider[] {
|
||||
return this.map.getAll();
|
||||
}
|
||||
}
|
||||
62
packages/physics/rapier2d/src/geometry/contact.ts
Normal file
62
packages/physics/rapier2d/src/geometry/contact.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import {Vector, VectorOps} from "../math";
|
||||
import {RawShapeContact} from "../raw";
|
||||
|
||||
/**
|
||||
* The contact info between two shapes.
|
||||
*/
|
||||
export class ShapeContact {
|
||||
/**
|
||||
* Distance between the two contact points.
|
||||
* If this is negative, this contact represents a penetration.
|
||||
*/
|
||||
distance: number;
|
||||
|
||||
/**
|
||||
* Position of the contact on the first shape.
|
||||
*/
|
||||
point1: Vector;
|
||||
|
||||
/**
|
||||
* Position of the contact on the second shape.
|
||||
*/
|
||||
point2: Vector;
|
||||
|
||||
/**
|
||||
* Contact normal, pointing towards the exterior of the first shape.
|
||||
*/
|
||||
normal1: Vector;
|
||||
|
||||
/**
|
||||
* Contact normal, pointing towards the exterior of the second shape.
|
||||
* If these contact data are expressed in world-space, this normal is equal to -normal1.
|
||||
*/
|
||||
normal2: Vector;
|
||||
|
||||
constructor(
|
||||
dist: number,
|
||||
point1: Vector,
|
||||
point2: Vector,
|
||||
normal1: Vector,
|
||||
normal2: Vector,
|
||||
) {
|
||||
this.distance = dist;
|
||||
this.point1 = point1;
|
||||
this.point2 = point2;
|
||||
this.normal1 = normal1;
|
||||
this.normal2 = normal2;
|
||||
}
|
||||
|
||||
public static fromRaw(raw: RawShapeContact): ShapeContact {
|
||||
if (!raw) return null;
|
||||
|
||||
const result = new ShapeContact(
|
||||
raw.distance(),
|
||||
VectorOps.fromRaw(raw.point1()),
|
||||
VectorOps.fromRaw(raw.point2()),
|
||||
VectorOps.fromRaw(raw.normal1()),
|
||||
VectorOps.fromRaw(raw.normal2()),
|
||||
);
|
||||
raw.free();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
6
packages/physics/rapier2d/src/geometry/feature.ts
Normal file
6
packages/physics/rapier2d/src/geometry/feature.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export enum FeatureType {
|
||||
Vertex,
|
||||
Face,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
11
packages/physics/rapier2d/src/geometry/index.ts
Normal file
11
packages/physics/rapier2d/src/geometry/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from "./broad_phase";
|
||||
export * from "./narrow_phase";
|
||||
export * from "./shape";
|
||||
export * from "./collider";
|
||||
export * from "./collider_set";
|
||||
export * from "./feature";
|
||||
export * from "./ray";
|
||||
export * from "./point";
|
||||
export * from "./toi";
|
||||
export * from "./interaction_groups";
|
||||
export * from "./contact";
|
||||
18
packages/physics/rapier2d/src/geometry/interaction_groups.ts
Normal file
18
packages/physics/rapier2d/src/geometry/interaction_groups.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Pairwise filtering using bit masks.
|
||||
*
|
||||
* This filtering method is based on two 16-bit values:
|
||||
* - The interaction groups (the 16 left-most bits of `self.0`).
|
||||
* - The interaction mask (the 16 right-most bits of `self.0`).
|
||||
*
|
||||
* An interaction is allowed between two filters `a` and `b` two conditions
|
||||
* are met simultaneously:
|
||||
* - The interaction groups of `a` has at least one bit set to `1` in common with the interaction mask of `b`.
|
||||
* - The interaction groups of `b` has at least one bit set to `1` in common with the interaction mask of `a`.
|
||||
* In other words, interactions are allowed between two filter iff. the following condition is met:
|
||||
*
|
||||
* ```
|
||||
* ((a >> 16) & b) != 0 && ((b >> 16) & a) != 0
|
||||
* ```
|
||||
*/
|
||||
export type InteractionGroups = number;
|
||||
192
packages/physics/rapier2d/src/geometry/narrow_phase.ts
Normal file
192
packages/physics/rapier2d/src/geometry/narrow_phase.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import {RawNarrowPhase, RawContactManifold} from "../raw";
|
||||
import {ColliderHandle} from "./collider";
|
||||
import {Vector, VectorOps} from "../math";
|
||||
|
||||
/**
|
||||
* The narrow-phase used for precise collision-detection.
|
||||
*
|
||||
* To avoid leaking WASM resources, this MUST be freed manually with `narrowPhase.free()`
|
||||
* once you are done using it.
|
||||
*/
|
||||
export class NarrowPhase {
|
||||
raw: RawNarrowPhase;
|
||||
tempManifold: TempContactManifold;
|
||||
|
||||
/**
|
||||
* Release the WASM memory occupied by this narrow-phase.
|
||||
*/
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
this.raw = undefined;
|
||||
}
|
||||
|
||||
constructor(raw?: RawNarrowPhase) {
|
||||
this.raw = raw || new RawNarrowPhase();
|
||||
this.tempManifold = new TempContactManifold(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerates all the colliders potentially in contact with the given collider.
|
||||
*
|
||||
* @param collider1 - The second collider involved in the contact.
|
||||
* @param f - Closure that will be called on each collider that is in contact with `collider1`.
|
||||
*/
|
||||
public contactPairsWith(
|
||||
collider1: ColliderHandle,
|
||||
f: (collider2: ColliderHandle) => void,
|
||||
) {
|
||||
this.raw.contact_pairs_with(collider1, f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerates all the colliders intersecting the given colliders, assuming one of them
|
||||
* is a sensor.
|
||||
*/
|
||||
public intersectionPairsWith(
|
||||
collider1: ColliderHandle,
|
||||
f: (collider2: ColliderHandle) => void,
|
||||
) {
|
||||
this.raw.intersection_pairs_with(collider1, f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates through all the contact manifolds between the given pair of colliders.
|
||||
*
|
||||
* @param collider1 - The first collider involved in the contact.
|
||||
* @param collider2 - The second collider involved in the contact.
|
||||
* @param f - Closure that will be called on each contact manifold between the two colliders. If the second argument
|
||||
* passed to this closure is `true`, then the contact manifold data is flipped, i.e., methods like `localNormal1`
|
||||
* actually apply to the `collider2` and fields like `localNormal2` apply to the `collider1`.
|
||||
*/
|
||||
public contactPair(
|
||||
collider1: ColliderHandle,
|
||||
collider2: ColliderHandle,
|
||||
f: (manifold: TempContactManifold, flipped: boolean) => void,
|
||||
) {
|
||||
const rawPair = this.raw.contact_pair(collider1, collider2);
|
||||
|
||||
if (!!rawPair) {
|
||||
const flipped = rawPair.collider1() != collider1;
|
||||
|
||||
let i;
|
||||
for (i = 0; i < rawPair.numContactManifolds(); ++i) {
|
||||
this.tempManifold.raw = rawPair.contactManifold(i);
|
||||
if (!!this.tempManifold.raw) {
|
||||
f(this.tempManifold, flipped);
|
||||
}
|
||||
|
||||
// SAFETY: The RawContactManifold stores a raw pointer that will be invalidated
|
||||
// at the next timestep. So we must be sure to free the pair here
|
||||
// to avoid unsoundness in the Rust code.
|
||||
this.tempManifold.free();
|
||||
}
|
||||
rawPair.free();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if `collider1` and `collider2` intersect and at least one of them is a sensor.
|
||||
* @param collider1 − The first collider involved in the intersection.
|
||||
* @param collider2 − The second collider involved in the intersection.
|
||||
*/
|
||||
public intersectionPair(
|
||||
collider1: ColliderHandle,
|
||||
collider2: ColliderHandle,
|
||||
): boolean {
|
||||
return this.raw.intersection_pair(collider1, collider2);
|
||||
}
|
||||
}
|
||||
|
||||
export class TempContactManifold {
|
||||
raw: RawContactManifold;
|
||||
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
this.raw = undefined;
|
||||
}
|
||||
|
||||
constructor(raw: RawContactManifold) {
|
||||
this.raw = raw;
|
||||
}
|
||||
|
||||
public normal(): Vector {
|
||||
return VectorOps.fromRaw(this.raw.normal());
|
||||
}
|
||||
|
||||
public localNormal1(): Vector {
|
||||
return VectorOps.fromRaw(this.raw.local_n1());
|
||||
}
|
||||
|
||||
public localNormal2(): Vector {
|
||||
return VectorOps.fromRaw(this.raw.local_n2());
|
||||
}
|
||||
|
||||
public subshape1(): number {
|
||||
return this.raw.subshape1();
|
||||
}
|
||||
|
||||
public subshape2(): number {
|
||||
return this.raw.subshape2();
|
||||
}
|
||||
|
||||
public numContacts(): number {
|
||||
return this.raw.num_contacts();
|
||||
}
|
||||
|
||||
public localContactPoint1(i: number): Vector | null {
|
||||
return VectorOps.fromRaw(this.raw.contact_local_p1(i));
|
||||
}
|
||||
|
||||
public localContactPoint2(i: number): Vector | null {
|
||||
return VectorOps.fromRaw(this.raw.contact_local_p2(i));
|
||||
}
|
||||
|
||||
public contactDist(i: number): number {
|
||||
return this.raw.contact_dist(i);
|
||||
}
|
||||
|
||||
public contactFid1(i: number): number {
|
||||
return this.raw.contact_fid1(i);
|
||||
}
|
||||
|
||||
public contactFid2(i: number): number {
|
||||
return this.raw.contact_fid2(i);
|
||||
}
|
||||
|
||||
public contactImpulse(i: number): number {
|
||||
return this.raw.contact_impulse(i);
|
||||
}
|
||||
|
||||
public contactTangentImpulse(i: number): number {
|
||||
return this.raw.contact_tangent_impulse(i);
|
||||
}
|
||||
|
||||
|
||||
public numSolverContacts(): number {
|
||||
return this.raw.num_solver_contacts();
|
||||
}
|
||||
|
||||
public solverContactPoint(i: number): Vector {
|
||||
return VectorOps.fromRaw(this.raw.solver_contact_point(i));
|
||||
}
|
||||
|
||||
public solverContactDist(i: number): number {
|
||||
return this.raw.solver_contact_dist(i);
|
||||
}
|
||||
|
||||
public solverContactFriction(i: number): number {
|
||||
return this.raw.solver_contact_friction(i);
|
||||
}
|
||||
|
||||
public solverContactRestitution(i: number): number {
|
||||
return this.raw.solver_contact_restitution(i);
|
||||
}
|
||||
|
||||
public solverContactTangentVelocity(i: number): Vector {
|
||||
return VectorOps.fromRaw(this.raw.solver_contact_tangent_velocity(i));
|
||||
}
|
||||
}
|
||||
98
packages/physics/rapier2d/src/geometry/point.ts
Normal file
98
packages/physics/rapier2d/src/geometry/point.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import {Collider, ColliderHandle} from "./collider";
|
||||
import {Vector, VectorOps} from "../math";
|
||||
import {
|
||||
RawFeatureType,
|
||||
RawPointColliderProjection,
|
||||
RawPointProjection,
|
||||
} from "../raw";
|
||||
import {FeatureType} from "./feature";
|
||||
import {ColliderSet} from "./collider_set";
|
||||
|
||||
/**
|
||||
* The projection of a point on a collider.
|
||||
*/
|
||||
export class PointProjection {
|
||||
/**
|
||||
* The projection of the point on the collider.
|
||||
*/
|
||||
point: Vector;
|
||||
/**
|
||||
* Is the point inside of the collider?
|
||||
*/
|
||||
isInside: boolean;
|
||||
|
||||
constructor(point: Vector, isInside: boolean) {
|
||||
this.point = point;
|
||||
this.isInside = isInside;
|
||||
}
|
||||
|
||||
public static fromRaw(raw: RawPointProjection): PointProjection {
|
||||
if (!raw) return null;
|
||||
|
||||
const result = new PointProjection(
|
||||
VectorOps.fromRaw(raw.point()),
|
||||
raw.isInside(),
|
||||
);
|
||||
raw.free();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The projection of a point on a collider (includes the collider handle).
|
||||
*/
|
||||
export class PointColliderProjection {
|
||||
/**
|
||||
* The collider hit by the ray.
|
||||
*/
|
||||
collider: Collider;
|
||||
/**
|
||||
* The projection of the point on the collider.
|
||||
*/
|
||||
point: Vector;
|
||||
/**
|
||||
* Is the point inside of the collider?
|
||||
*/
|
||||
isInside: boolean;
|
||||
|
||||
/**
|
||||
* The type of the geometric feature the point was projected on.
|
||||
*/
|
||||
featureType = FeatureType.Unknown;
|
||||
|
||||
/**
|
||||
* The id of the geometric feature the point was projected on.
|
||||
*/
|
||||
featureId: number | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
collider: Collider,
|
||||
point: Vector,
|
||||
isInside: boolean,
|
||||
featureType?: FeatureType,
|
||||
featureId?: number,
|
||||
) {
|
||||
this.collider = collider;
|
||||
this.point = point;
|
||||
this.isInside = isInside;
|
||||
if (featureId !== undefined) this.featureId = featureId;
|
||||
if (featureType !== undefined) this.featureType = featureType;
|
||||
}
|
||||
|
||||
public static fromRaw(
|
||||
colliderSet: ColliderSet,
|
||||
raw: RawPointColliderProjection,
|
||||
): PointColliderProjection {
|
||||
if (!raw) return null;
|
||||
|
||||
const result = new PointColliderProjection(
|
||||
colliderSet.get(raw.colliderHandle()),
|
||||
VectorOps.fromRaw(raw.point()),
|
||||
raw.isInside(),
|
||||
raw.featureType() as number as FeatureType,
|
||||
raw.featureId(),
|
||||
);
|
||||
raw.free();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
189
packages/physics/rapier2d/src/geometry/ray.ts
Normal file
189
packages/physics/rapier2d/src/geometry/ray.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import {Vector, VectorOps} from "../math";
|
||||
import {
|
||||
RawFeatureType,
|
||||
RawRayColliderIntersection,
|
||||
RawRayColliderHit,
|
||||
RawRayIntersection,
|
||||
} from "../raw";
|
||||
import {Collider} from "./collider";
|
||||
import {FeatureType} from "./feature";
|
||||
import {ColliderSet} from "./collider_set";
|
||||
|
||||
/**
|
||||
* A ray. This is a directed half-line.
|
||||
*/
|
||||
export class Ray {
|
||||
/**
|
||||
* The starting point of the ray.
|
||||
*/
|
||||
public origin: Vector;
|
||||
/**
|
||||
* The direction of propagation of the ray.
|
||||
*/
|
||||
public dir: Vector;
|
||||
|
||||
/**
|
||||
* Builds a ray from its origin and direction.
|
||||
*
|
||||
* @param origin - The ray's starting point.
|
||||
* @param dir - The ray's direction of propagation.
|
||||
*/
|
||||
constructor(origin: Vector, dir: Vector) {
|
||||
this.origin = origin;
|
||||
this.dir = dir;
|
||||
}
|
||||
|
||||
public pointAt(t: number): Vector {
|
||||
return {
|
||||
x: this.origin.x + this.dir.x * t,
|
||||
y: this.origin.y + this.dir.y * t,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The intersection between a ray and a collider.
|
||||
*/
|
||||
export class RayIntersection {
|
||||
/**
|
||||
* The time-of-impact of the ray with the collider.
|
||||
*
|
||||
* The hit point is obtained from the ray's origin and direction: `origin + dir * timeOfImpact`.
|
||||
*/
|
||||
timeOfImpact: number;
|
||||
/**
|
||||
* The normal of the collider at the hit point.
|
||||
*/
|
||||
normal: Vector;
|
||||
|
||||
/**
|
||||
* The type of the geometric feature the point was projected on.
|
||||
*/
|
||||
featureType = FeatureType.Unknown;
|
||||
|
||||
/**
|
||||
* The id of the geometric feature the point was projected on.
|
||||
*/
|
||||
featureId: number | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
timeOfImpact: number,
|
||||
normal: Vector,
|
||||
featureType?: FeatureType,
|
||||
featureId?: number,
|
||||
) {
|
||||
this.timeOfImpact = timeOfImpact;
|
||||
this.normal = normal;
|
||||
if (featureId !== undefined) this.featureId = featureId;
|
||||
if (featureType !== undefined) this.featureType = featureType;
|
||||
}
|
||||
|
||||
public static fromRaw(raw: RawRayIntersection): RayIntersection {
|
||||
if (!raw) return null;
|
||||
|
||||
const result = new RayIntersection(
|
||||
raw.time_of_impact(),
|
||||
VectorOps.fromRaw(raw.normal()),
|
||||
raw.featureType() as number as FeatureType,
|
||||
raw.featureId(),
|
||||
);
|
||||
raw.free();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The intersection between a ray and a collider (includes the collider handle).
|
||||
*/
|
||||
export class RayColliderIntersection {
|
||||
/**
|
||||
* The collider hit by the ray.
|
||||
*/
|
||||
collider: Collider;
|
||||
/**
|
||||
* The time-of-impact of the ray with the collider.
|
||||
*
|
||||
* The hit point is obtained from the ray's origin and direction: `origin + dir * timeOfImpact`.
|
||||
*/
|
||||
timeOfImpact: number;
|
||||
/**
|
||||
* The normal of the collider at the hit point.
|
||||
*/
|
||||
normal: Vector;
|
||||
|
||||
/**
|
||||
* The type of the geometric feature the point was projected on.
|
||||
*/
|
||||
featureType = FeatureType.Unknown;
|
||||
|
||||
/**
|
||||
* The id of the geometric feature the point was projected on.
|
||||
*/
|
||||
featureId: number | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
collider: Collider,
|
||||
timeOfImpact: number,
|
||||
normal: Vector,
|
||||
featureType?: FeatureType,
|
||||
featureId?: number,
|
||||
) {
|
||||
this.collider = collider;
|
||||
this.timeOfImpact = timeOfImpact;
|
||||
this.normal = normal;
|
||||
if (featureId !== undefined) this.featureId = featureId;
|
||||
if (featureType !== undefined) this.featureType = featureType;
|
||||
}
|
||||
|
||||
public static fromRaw(
|
||||
colliderSet: ColliderSet,
|
||||
raw: RawRayColliderIntersection,
|
||||
): RayColliderIntersection {
|
||||
if (!raw) return null;
|
||||
|
||||
const result = new RayColliderIntersection(
|
||||
colliderSet.get(raw.colliderHandle()),
|
||||
raw.time_of_impact(),
|
||||
VectorOps.fromRaw(raw.normal()),
|
||||
raw.featureType() as number as FeatureType,
|
||||
raw.featureId(),
|
||||
);
|
||||
raw.free();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The time of impact between a ray and a collider.
|
||||
*/
|
||||
export class RayColliderHit {
|
||||
/**
|
||||
* The handle of the collider hit by the ray.
|
||||
*/
|
||||
collider: Collider;
|
||||
/**
|
||||
* The time-of-impact of the ray with the collider.
|
||||
*
|
||||
* The hit point is obtained from the ray's origin and direction: `origin + dir * timeOfImpact`.
|
||||
*/
|
||||
timeOfImpact: number;
|
||||
|
||||
constructor(collider: Collider, timeOfImpact: number) {
|
||||
this.collider = collider;
|
||||
this.timeOfImpact = timeOfImpact;
|
||||
}
|
||||
|
||||
public static fromRaw(
|
||||
colliderSet: ColliderSet,
|
||||
raw: RawRayColliderHit,
|
||||
): RayColliderHit {
|
||||
if (!raw) return null;
|
||||
|
||||
const result = new RayColliderHit(
|
||||
colliderSet.get(raw.colliderHandle()),
|
||||
raw.timeOfImpact(),
|
||||
);
|
||||
raw.free();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
1060
packages/physics/rapier2d/src/geometry/shape.ts
Normal file
1060
packages/physics/rapier2d/src/geometry/shape.ts
Normal file
File diff suppressed because it is too large
Load Diff
105
packages/physics/rapier2d/src/geometry/toi.ts
Normal file
105
packages/physics/rapier2d/src/geometry/toi.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import {Collider} from "./collider";
|
||||
import {Vector, VectorOps} from "../math";
|
||||
import {RawShapeCastHit, RawColliderShapeCastHit} from "../raw";
|
||||
import {ColliderSet} from "./collider_set";
|
||||
|
||||
/**
|
||||
* The intersection between a ray and a collider.
|
||||
*/
|
||||
export class ShapeCastHit {
|
||||
/**
|
||||
* The time of impact of the two shapes.
|
||||
*/
|
||||
time_of_impact: number;
|
||||
/**
|
||||
* The local-space contact point on the first shape, at
|
||||
* the time of impact.
|
||||
*/
|
||||
witness1: Vector;
|
||||
/**
|
||||
* The local-space contact point on the second shape, at
|
||||
* the time of impact.
|
||||
*/
|
||||
witness2: Vector;
|
||||
/**
|
||||
* The local-space normal on the first shape, at
|
||||
* the time of impact.
|
||||
*/
|
||||
normal1: Vector;
|
||||
/**
|
||||
* The local-space normal on the second shape, at
|
||||
* the time of impact.
|
||||
*/
|
||||
normal2: Vector;
|
||||
|
||||
constructor(
|
||||
time_of_impact: number,
|
||||
witness1: Vector,
|
||||
witness2: Vector,
|
||||
normal1: Vector,
|
||||
normal2: Vector,
|
||||
) {
|
||||
this.time_of_impact = time_of_impact;
|
||||
this.witness1 = witness1;
|
||||
this.witness2 = witness2;
|
||||
this.normal1 = normal1;
|
||||
this.normal2 = normal2;
|
||||
}
|
||||
|
||||
public static fromRaw(
|
||||
colliderSet: ColliderSet,
|
||||
raw: RawShapeCastHit,
|
||||
): ShapeCastHit {
|
||||
if (!raw) return null;
|
||||
|
||||
const result = new ShapeCastHit(
|
||||
raw.time_of_impact(),
|
||||
VectorOps.fromRaw(raw.witness1()),
|
||||
VectorOps.fromRaw(raw.witness2()),
|
||||
VectorOps.fromRaw(raw.normal1()),
|
||||
VectorOps.fromRaw(raw.normal2()),
|
||||
);
|
||||
raw.free();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The intersection between a ray and a collider.
|
||||
*/
|
||||
export class ColliderShapeCastHit extends ShapeCastHit {
|
||||
/**
|
||||
* The handle of the collider hit by the ray.
|
||||
*/
|
||||
collider: Collider;
|
||||
|
||||
constructor(
|
||||
collider: Collider,
|
||||
time_of_impact: number,
|
||||
witness1: Vector,
|
||||
witness2: Vector,
|
||||
normal1: Vector,
|
||||
normal2: Vector,
|
||||
) {
|
||||
super(time_of_impact, witness1, witness2, normal1, normal2);
|
||||
this.collider = collider;
|
||||
}
|
||||
|
||||
public static fromRaw(
|
||||
colliderSet: ColliderSet,
|
||||
raw: RawColliderShapeCastHit,
|
||||
): ColliderShapeCastHit {
|
||||
if (!raw) return null;
|
||||
|
||||
const result = new ColliderShapeCastHit(
|
||||
colliderSet.get(raw.colliderHandle()),
|
||||
raw.time_of_impact(),
|
||||
VectorOps.fromRaw(raw.witness1()),
|
||||
VectorOps.fromRaw(raw.witness2()),
|
||||
VectorOps.fromRaw(raw.normal1()),
|
||||
VectorOps.fromRaw(raw.normal2()),
|
||||
);
|
||||
raw.free();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
3
packages/physics/rapier2d/src/index.ts
Normal file
3
packages/physics/rapier2d/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import * as RAPIER from "./exports";
|
||||
export * from "./exports";
|
||||
export default RAPIER;
|
||||
60
packages/physics/rapier2d/src/init.ts
Normal file
60
packages/physics/rapier2d/src/init.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* RAPIER initialization module with dynamic WASM loading support.
|
||||
* RAPIER 初始化模块,支持动态 WASM 加载。
|
||||
*/
|
||||
|
||||
import wasmInit from "../pkg/rapier_wasm2d";
|
||||
|
||||
/**
|
||||
* Input types for WASM initialization.
|
||||
* WASM 初始化的输入类型。
|
||||
*/
|
||||
export type InitInput =
|
||||
| RequestInfo // URL string or Request object
|
||||
| URL // URL object
|
||||
| Response // Fetch Response object
|
||||
| BufferSource // ArrayBuffer or TypedArray
|
||||
| WebAssembly.Module; // Pre-compiled module
|
||||
|
||||
let initialized = false;
|
||||
|
||||
/**
|
||||
* Initializes RAPIER.
|
||||
* Has to be called and awaited before using any library methods.
|
||||
*
|
||||
* 初始化 RAPIER。
|
||||
* 必须在使用任何库方法之前调用并等待。
|
||||
*
|
||||
* @param input - WASM source (required). Can be URL, Response, ArrayBuffer, etc.
|
||||
* WASM 源(必需)。可以是 URL、Response、ArrayBuffer 等。
|
||||
*
|
||||
* @example
|
||||
* // Load from URL | 从 URL 加载
|
||||
* await RAPIER.init('wasm/rapier_wasm2d_bg.wasm');
|
||||
*
|
||||
* @example
|
||||
* // Load from fetch response | 从 fetch 响应加载
|
||||
* const response = await fetch('wasm/rapier_wasm2d_bg.wasm');
|
||||
* await RAPIER.init(response);
|
||||
*
|
||||
* @example
|
||||
* // Load from ArrayBuffer | 从 ArrayBuffer 加载
|
||||
* const buffer = await fetch('wasm/rapier_wasm2d_bg.wasm').then(r => r.arrayBuffer());
|
||||
* await RAPIER.init(buffer);
|
||||
*/
|
||||
export async function init(input?: InitInput): Promise<void> {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
await wasmInit(input);
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if RAPIER is already initialized.
|
||||
* 检查 RAPIER 是否已初始化。
|
||||
*/
|
||||
export function isInitialized(): boolean {
|
||||
return initialized;
|
||||
}
|
||||
72
packages/physics/rapier2d/src/math.ts
Normal file
72
packages/physics/rapier2d/src/math.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import {RawVector, RawRotation} from "./raw";
|
||||
|
||||
export interface Vector {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A 2D vector.
|
||||
*/
|
||||
export class Vector2 implements Vector {
|
||||
x: number;
|
||||
y: number;
|
||||
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
export class VectorOps {
|
||||
public static new(x: number, y: number): Vector {
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
|
||||
public static zeros(): Vector {
|
||||
return VectorOps.new(0.0, 0.0);
|
||||
}
|
||||
|
||||
// FIXME: type ram: RawVector?
|
||||
public static fromRaw(raw: RawVector): Vector | null {
|
||||
if (!raw) return null;
|
||||
|
||||
let res = VectorOps.new(raw.x, raw.y);
|
||||
raw.free();
|
||||
return res;
|
||||
}
|
||||
|
||||
public static intoRaw(v: Vector): RawVector {
|
||||
return new RawVector(v.x, v.y);
|
||||
}
|
||||
|
||||
public static copy(out: Vector, input: Vector) {
|
||||
out.x = input.x;
|
||||
out.y = input.y;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A rotation angle in radians.
|
||||
*/
|
||||
export type Rotation = number;
|
||||
|
||||
export class RotationOps {
|
||||
public static identity(): number {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public static fromRaw(raw: RawRotation): Rotation | null {
|
||||
if (!raw) return null;
|
||||
|
||||
let res = raw.angle;
|
||||
raw.free();
|
||||
return res;
|
||||
}
|
||||
|
||||
public static intoRaw(angle: Rotation): RawRotation {
|
||||
return RawRotation.fromAngle(angle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import {RawDebugRenderPipeline} from "../raw";
|
||||
import {Vector, VectorOps} from "../math";
|
||||
import {
|
||||
IntegrationParameters,
|
||||
IslandManager,
|
||||
ImpulseJointSet,
|
||||
MultibodyJointSet,
|
||||
RigidBodySet,
|
||||
} from "../dynamics";
|
||||
import {BroadPhase, Collider, ColliderSet, NarrowPhase} from "../geometry";
|
||||
import {QueryFilterFlags} from "./query_pipeline";
|
||||
|
||||
/**
|
||||
* The vertex and color buffers for debug-redering the physics scene.
|
||||
*/
|
||||
export class DebugRenderBuffers {
|
||||
/**
|
||||
* The lines to render. This is a flat array containing all the lines
|
||||
* to render. Each line is described as two consecutive point. Each
|
||||
* point is described as two (in 2D) or three (in 3D) consecutive
|
||||
* floats. For example, in 2D, the array: `[1, 2, 3, 4, 5, 6, 7, 8]`
|
||||
* describes the two segments `[[1, 2], [3, 4]]` and `[[5, 6], [7, 8]]`.
|
||||
*/
|
||||
public vertices: Float32Array;
|
||||
/**
|
||||
* The color buffer. There is one color per vertex, and each color
|
||||
* has four consecutive components (in RGBA format).
|
||||
*/
|
||||
public colors: Float32Array;
|
||||
|
||||
constructor(vertices: Float32Array, colors: Float32Array) {
|
||||
this.vertices = vertices;
|
||||
this.colors = colors;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A pipeline for rendering the physics scene.
|
||||
*
|
||||
* To avoid leaking WASM resources, this MUST be freed manually with `debugRenderPipeline.free()`
|
||||
* once you are done using it (and all the rigid-bodies it created).
|
||||
*/
|
||||
export class DebugRenderPipeline {
|
||||
raw: RawDebugRenderPipeline;
|
||||
public vertices: Float32Array;
|
||||
public colors: Float32Array;
|
||||
|
||||
/**
|
||||
* Release the WASM memory occupied by this serialization pipeline.
|
||||
*/
|
||||
free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
this.raw = undefined;
|
||||
this.vertices = undefined;
|
||||
this.colors = undefined;
|
||||
}
|
||||
|
||||
constructor(raw?: RawDebugRenderPipeline) {
|
||||
this.raw = raw || new RawDebugRenderPipeline();
|
||||
}
|
||||
|
||||
public render(
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
impulse_joints: ImpulseJointSet,
|
||||
multibody_joints: MultibodyJointSet,
|
||||
narrow_phase: NarrowPhase,
|
||||
filterFlags?: QueryFilterFlags,
|
||||
filterPredicate?: (collider: Collider) => boolean,
|
||||
) {
|
||||
this.raw.render(
|
||||
bodies.raw,
|
||||
colliders.raw,
|
||||
impulse_joints.raw,
|
||||
multibody_joints.raw,
|
||||
narrow_phase.raw,
|
||||
filterFlags,
|
||||
colliders.castClosure(filterPredicate),
|
||||
);
|
||||
this.vertices = this.raw.vertices();
|
||||
this.colors = this.raw.colors();
|
||||
}
|
||||
}
|
||||
158
packages/physics/rapier2d/src/pipeline/event_queue.ts
Normal file
158
packages/physics/rapier2d/src/pipeline/event_queue.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import {RawContactForceEvent, RawEventQueue} from "../raw";
|
||||
import {RigidBodyHandle} from "../dynamics";
|
||||
import {Collider, ColliderHandle} from "../geometry";
|
||||
import {Vector, VectorOps} from "../math";
|
||||
|
||||
/**
|
||||
* Flags indicating what events are enabled for colliders.
|
||||
*/
|
||||
export enum ActiveEvents {
|
||||
NONE = 0,
|
||||
/**
|
||||
* Enable collision events.
|
||||
*/
|
||||
COLLISION_EVENTS = 0b0001,
|
||||
/**
|
||||
* Enable contact force events.
|
||||
*/
|
||||
CONTACT_FORCE_EVENTS = 0b0010,
|
||||
}
|
||||
|
||||
/**
|
||||
* Event occurring when the sum of the magnitudes of the
|
||||
* contact forces between two colliders exceed a threshold.
|
||||
*
|
||||
* This object should **not** be stored anywhere. Its properties can only be
|
||||
* read from within the closure given to `EventHandler.drainContactForceEvents`.
|
||||
*/
|
||||
export class TempContactForceEvent {
|
||||
raw: RawContactForceEvent;
|
||||
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
this.raw = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The first collider involved in the contact.
|
||||
*/
|
||||
public collider1(): ColliderHandle {
|
||||
return this.raw.collider1();
|
||||
}
|
||||
|
||||
/**
|
||||
* The second collider involved in the contact.
|
||||
*/
|
||||
public collider2(): ColliderHandle {
|
||||
return this.raw.collider2();
|
||||
}
|
||||
|
||||
/**
|
||||
* The sum of all the forces between the two colliders.
|
||||
*/
|
||||
public totalForce(): Vector {
|
||||
return VectorOps.fromRaw(this.raw.total_force());
|
||||
}
|
||||
|
||||
/**
|
||||
* The sum of the magnitudes of each force between the two colliders.
|
||||
*
|
||||
* Note that this is **not** the same as the magnitude of `self.total_force`.
|
||||
* Here we are summing the magnitude of all the forces, instead of taking
|
||||
* the magnitude of their sum.
|
||||
*/
|
||||
public totalForceMagnitude(): number {
|
||||
return this.raw.total_force_magnitude();
|
||||
}
|
||||
|
||||
/**
|
||||
* The world-space (unit) direction of the force with strongest magnitude.
|
||||
*/
|
||||
public maxForceDirection(): Vector {
|
||||
return VectorOps.fromRaw(this.raw.max_force_direction());
|
||||
}
|
||||
|
||||
/**
|
||||
* The magnitude of the largest force at a contact point of this contact pair.
|
||||
*/
|
||||
public maxForceMagnitude(): number {
|
||||
return this.raw.max_force_magnitude();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A structure responsible for collecting events generated
|
||||
* by the physics engine.
|
||||
*
|
||||
* To avoid leaking WASM resources, this MUST be freed manually with `eventQueue.free()`
|
||||
* once you are done using it.
|
||||
*/
|
||||
export class EventQueue {
|
||||
raw: RawEventQueue;
|
||||
|
||||
/**
|
||||
* Creates a new event collector.
|
||||
*
|
||||
* @param autoDrain -setting this to `true` is strongly recommended. If true, the collector will
|
||||
* be automatically drained before each `world.step(collector)`. If false, the collector will
|
||||
* keep all events in memory unless it is manually drained/cleared; this may lead to unbounded use of
|
||||
* RAM if no drain is performed.
|
||||
*/
|
||||
constructor(autoDrain: boolean, raw?: RawEventQueue) {
|
||||
this.raw = raw || new RawEventQueue(autoDrain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the WASM memory occupied by this event-queue.
|
||||
*/
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
this.raw = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given javascript closure on each collision event of this collector, then clear
|
||||
* the internal collision event buffer.
|
||||
*
|
||||
* @param f - JavaScript closure applied to each collision event. The
|
||||
* closure must take three arguments: two integers representing the handles of the colliders
|
||||
* involved in the collision, and a boolean indicating if the collision started (true) or stopped
|
||||
* (false).
|
||||
*/
|
||||
public drainCollisionEvents(
|
||||
f: (
|
||||
handle1: ColliderHandle,
|
||||
handle2: ColliderHandle,
|
||||
started: boolean,
|
||||
) => void,
|
||||
) {
|
||||
this.raw.drainCollisionEvents(f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given javascript closure on each contact force event of this collector, then clear
|
||||
* the internal collision event buffer.
|
||||
*
|
||||
* @param f - JavaScript closure applied to each collision event. The
|
||||
* closure must take one `TempContactForceEvent` argument.
|
||||
*/
|
||||
public drainContactForceEvents(f: (event: TempContactForceEvent) => void) {
|
||||
let event = new TempContactForceEvent();
|
||||
this.raw.drainContactForceEvents((raw: RawContactForceEvent) => {
|
||||
event.raw = raw;
|
||||
f(event);
|
||||
event.free();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all events contained by this collector
|
||||
*/
|
||||
public clear() {
|
||||
this.raw.clear();
|
||||
}
|
||||
}
|
||||
7
packages/physics/rapier2d/src/pipeline/index.ts
Normal file
7
packages/physics/rapier2d/src/pipeline/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./world";
|
||||
export * from "./physics_pipeline";
|
||||
export * from "./serialization_pipeline";
|
||||
export * from "./event_queue";
|
||||
export * from "./physics_hooks";
|
||||
export * from "./debug_render_pipeline";
|
||||
export * from "./query_pipeline";
|
||||
54
packages/physics/rapier2d/src/pipeline/physics_hooks.ts
Normal file
54
packages/physics/rapier2d/src/pipeline/physics_hooks.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import {RigidBodyHandle} from "../dynamics";
|
||||
import {ColliderHandle} from "../geometry";
|
||||
|
||||
export enum ActiveHooks {
|
||||
NONE = 0,
|
||||
FILTER_CONTACT_PAIRS = 0b0001,
|
||||
FILTER_INTERSECTION_PAIRS = 0b0010,
|
||||
// MODIFY_SOLVER_CONTACTS = 0b0100, /* Not supported yet in JS. */
|
||||
}
|
||||
|
||||
export enum SolverFlags {
|
||||
EMPTY = 0b000,
|
||||
COMPUTE_IMPULSE = 0b001,
|
||||
}
|
||||
|
||||
export interface PhysicsHooks {
|
||||
/**
|
||||
* Function that determines if contacts computation should happen between two colliders, and how the
|
||||
* constraints solver should behave for these contacts.
|
||||
*
|
||||
* This will only be executed and taken into account if at least one of the involved colliders contains the
|
||||
* `ActiveHooks.FILTER_CONTACT_PAIR` flag in its active hooks.
|
||||
*
|
||||
* @param collider1 − Handle of the first collider involved in the potential contact.
|
||||
* @param collider2 − Handle of the second collider involved in the potential contact.
|
||||
* @param body1 − Handle of the first body involved in the potential contact.
|
||||
* @param body2 − Handle of the second body involved in the potential contact.
|
||||
*/
|
||||
filterContactPair(
|
||||
collider1: ColliderHandle,
|
||||
collider2: ColliderHandle,
|
||||
body1: RigidBodyHandle,
|
||||
body2: RigidBodyHandle,
|
||||
): SolverFlags | null;
|
||||
|
||||
/**
|
||||
* Function that determines if intersection computation should happen between two colliders (where at least
|
||||
* one is a sensor).
|
||||
*
|
||||
* This will only be executed and taken into account if `one of the involved colliders contains the
|
||||
* `ActiveHooks.FILTER_INTERSECTION_PAIR` flag in its active hooks.
|
||||
*
|
||||
* @param collider1 − Handle of the first collider involved in the potential contact.
|
||||
* @param collider2 − Handle of the second collider involved in the potential contact.
|
||||
* @param body1 − Handle of the first body involved in the potential contact.
|
||||
* @param body2 − Handle of the second body involved in the potential contact.
|
||||
*/
|
||||
filterIntersectionPair(
|
||||
collider1: ColliderHandle,
|
||||
collider2: ColliderHandle,
|
||||
body1: RigidBodyHandle,
|
||||
body2: RigidBodyHandle,
|
||||
): boolean;
|
||||
}
|
||||
85
packages/physics/rapier2d/src/pipeline/physics_pipeline.ts
Normal file
85
packages/physics/rapier2d/src/pipeline/physics_pipeline.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import {RawPhysicsPipeline} from "../raw";
|
||||
import {Vector, VectorOps} from "../math";
|
||||
import {
|
||||
IntegrationParameters,
|
||||
ImpulseJointSet,
|
||||
MultibodyJointSet,
|
||||
RigidBodyHandle,
|
||||
RigidBodySet,
|
||||
CCDSolver,
|
||||
IslandManager,
|
||||
} from "../dynamics";
|
||||
import {
|
||||
BroadPhase,
|
||||
ColliderHandle,
|
||||
ColliderSet,
|
||||
NarrowPhase,
|
||||
} from "../geometry";
|
||||
import {EventQueue} from "./event_queue";
|
||||
import {PhysicsHooks} from "./physics_hooks";
|
||||
|
||||
export class PhysicsPipeline {
|
||||
raw: RawPhysicsPipeline;
|
||||
|
||||
public free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
this.raw = undefined;
|
||||
}
|
||||
|
||||
constructor(raw?: RawPhysicsPipeline) {
|
||||
this.raw = raw || new RawPhysicsPipeline();
|
||||
}
|
||||
|
||||
public step(
|
||||
gravity: Vector,
|
||||
integrationParameters: IntegrationParameters,
|
||||
islands: IslandManager,
|
||||
broadPhase: BroadPhase,
|
||||
narrowPhase: NarrowPhase,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
impulseJoints: ImpulseJointSet,
|
||||
multibodyJoints: MultibodyJointSet,
|
||||
ccdSolver: CCDSolver,
|
||||
eventQueue?: EventQueue,
|
||||
hooks?: PhysicsHooks,
|
||||
) {
|
||||
let rawG = VectorOps.intoRaw(gravity);
|
||||
|
||||
if (!!eventQueue) {
|
||||
this.raw.stepWithEvents(
|
||||
rawG,
|
||||
integrationParameters.raw,
|
||||
islands.raw,
|
||||
broadPhase.raw,
|
||||
narrowPhase.raw,
|
||||
bodies.raw,
|
||||
colliders.raw,
|
||||
impulseJoints.raw,
|
||||
multibodyJoints.raw,
|
||||
ccdSolver.raw,
|
||||
eventQueue.raw,
|
||||
hooks,
|
||||
!!hooks ? hooks.filterContactPair : null,
|
||||
!!hooks ? hooks.filterIntersectionPair : null,
|
||||
);
|
||||
} else {
|
||||
this.raw.step(
|
||||
rawG,
|
||||
integrationParameters.raw,
|
||||
islands.raw,
|
||||
broadPhase.raw,
|
||||
narrowPhase.raw,
|
||||
bodies.raw,
|
||||
colliders.raw,
|
||||
impulseJoints.raw,
|
||||
multibodyJoints.raw,
|
||||
ccdSolver.raw,
|
||||
);
|
||||
}
|
||||
|
||||
rawG.free();
|
||||
}
|
||||
}
|
||||
57
packages/physics/rapier2d/src/pipeline/query_pipeline.ts
Normal file
57
packages/physics/rapier2d/src/pipeline/query_pipeline.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {RawRayColliderIntersection} from "../raw";
|
||||
import {
|
||||
ColliderHandle,
|
||||
ColliderSet,
|
||||
InteractionGroups,
|
||||
PointColliderProjection,
|
||||
Ray,
|
||||
RayColliderIntersection,
|
||||
RayColliderHit,
|
||||
Shape,
|
||||
ColliderShapeCastHit,
|
||||
} from "../geometry";
|
||||
import {IslandManager, RigidBodyHandle, RigidBodySet} from "../dynamics";
|
||||
import {Rotation, RotationOps, Vector, VectorOps} from "../math";
|
||||
|
||||
// NOTE: must match the bits in the QueryFilterFlags on the Rust side.
|
||||
/**
|
||||
* Flags for excluding whole sets of colliders from a scene query.
|
||||
*/
|
||||
export enum QueryFilterFlags {
|
||||
/**
|
||||
* Exclude from the query any collider attached to a fixed rigid-body and colliders with no rigid-body attached.
|
||||
*/
|
||||
EXCLUDE_FIXED = 0b0000_0001,
|
||||
/**
|
||||
* Exclude from the query any collider attached to a dynamic rigid-body.
|
||||
*/
|
||||
EXCLUDE_KINEMATIC = 0b0000_0010,
|
||||
/**
|
||||
* Exclude from the query any collider attached to a kinematic rigid-body.
|
||||
*/
|
||||
EXCLUDE_DYNAMIC = 0b0000_0100,
|
||||
/**
|
||||
* Exclude from the query any collider that is a sensor.
|
||||
*/
|
||||
EXCLUDE_SENSORS = 0b0000_1000,
|
||||
/**
|
||||
* Exclude from the query any collider that is not a sensor.
|
||||
*/
|
||||
EXCLUDE_SOLIDS = 0b0001_0000,
|
||||
/**
|
||||
* Excludes all colliders not attached to a dynamic rigid-body.
|
||||
*/
|
||||
ONLY_DYNAMIC = QueryFilterFlags.EXCLUDE_FIXED |
|
||||
QueryFilterFlags.EXCLUDE_KINEMATIC,
|
||||
/**
|
||||
* Excludes all colliders not attached to a kinematic rigid-body.
|
||||
*/
|
||||
ONLY_KINEMATIC = QueryFilterFlags.EXCLUDE_DYNAMIC |
|
||||
QueryFilterFlags.EXCLUDE_FIXED,
|
||||
/**
|
||||
* Exclude all colliders attached to a non-fixed rigid-body
|
||||
* (this will not exclude colliders not attached to any rigid-body).
|
||||
*/
|
||||
ONLY_FIXED = QueryFilterFlags.EXCLUDE_DYNAMIC |
|
||||
QueryFilterFlags.EXCLUDE_KINEMATIC,
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import {RawSerializationPipeline} from "../raw";
|
||||
import {Vector, VectorOps} from "../math";
|
||||
import {
|
||||
IntegrationParameters,
|
||||
IslandManager,
|
||||
ImpulseJointSet,
|
||||
MultibodyJointSet,
|
||||
RigidBodySet,
|
||||
} from "../dynamics";
|
||||
import {BroadPhase, ColliderSet, NarrowPhase} from "../geometry";
|
||||
import {World} from "./world";
|
||||
|
||||
/**
|
||||
* A pipeline for serializing the physics scene.
|
||||
*
|
||||
* To avoid leaking WASM resources, this MUST be freed manually with `serializationPipeline.free()`
|
||||
* once you are done using it (and all the rigid-bodies it created).
|
||||
*/
|
||||
export class SerializationPipeline {
|
||||
raw: RawSerializationPipeline;
|
||||
|
||||
/**
|
||||
* Release the WASM memory occupied by this serialization pipeline.
|
||||
*/
|
||||
free() {
|
||||
if (!!this.raw) {
|
||||
this.raw.free();
|
||||
}
|
||||
this.raw = undefined;
|
||||
}
|
||||
|
||||
constructor(raw?: RawSerializationPipeline) {
|
||||
this.raw = raw || new RawSerializationPipeline();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a complete physics state into a single byte array.
|
||||
* @param gravity - The current gravity affecting the simulation.
|
||||
* @param integrationParameters - The integration parameters of the simulation.
|
||||
* @param broadPhase - The broad-phase of the simulation.
|
||||
* @param narrowPhase - The narrow-phase of the simulation.
|
||||
* @param bodies - The rigid-bodies taking part into the simulation.
|
||||
* @param colliders - The colliders taking part into the simulation.
|
||||
* @param impulseJoints - The impulse joints taking part into the simulation.
|
||||
* @param multibodyJoints - The multibody joints taking part into the simulation.
|
||||
*/
|
||||
public serializeAll(
|
||||
gravity: Vector,
|
||||
integrationParameters: IntegrationParameters,
|
||||
islands: IslandManager,
|
||||
broadPhase: BroadPhase,
|
||||
narrowPhase: NarrowPhase,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
impulseJoints: ImpulseJointSet,
|
||||
multibodyJoints: MultibodyJointSet,
|
||||
): Uint8Array {
|
||||
let rawGra = VectorOps.intoRaw(gravity);
|
||||
|
||||
const res = this.raw.serializeAll(
|
||||
rawGra,
|
||||
integrationParameters.raw,
|
||||
islands.raw,
|
||||
broadPhase.raw,
|
||||
narrowPhase.raw,
|
||||
bodies.raw,
|
||||
colliders.raw,
|
||||
impulseJoints.raw,
|
||||
multibodyJoints.raw,
|
||||
);
|
||||
rawGra.free();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize the complete physics state from a single byte array.
|
||||
*
|
||||
* @param data - The byte array to deserialize.
|
||||
*/
|
||||
public deserializeAll(data: Uint8Array): World {
|
||||
return World.fromRaw(this.raw.deserializeAll(data));
|
||||
}
|
||||
}
|
||||
1260
packages/physics/rapier2d/src/pipeline/world.ts
Normal file
1260
packages/physics/rapier2d/src/pipeline/world.ts
Normal file
File diff suppressed because it is too large
Load Diff
1
packages/physics/rapier2d/src/raw.ts
Normal file
1
packages/physics/rapier2d/src/raw.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "../pkg/rapier_wasm2d";
|
||||
10
packages/physics/rapier2d/tsconfig.json
Normal file
10
packages/physics/rapier2d/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": false,
|
||||
"strictNullChecks": false
|
||||
},
|
||||
"include": ["src/**/*", "pkg/*.d.ts"]
|
||||
}
|
||||
10
packages/physics/rapier2d/tsup.config.ts
Normal file
10
packages/physics/rapier2d/tsup.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts"],
|
||||
format: ["esm"],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
external: ["../pkg/rapier_wasm2d.js"],
|
||||
});
|
||||
Reference in New Issue
Block a user