@aura3d/physics 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Crowd.ts DELETED
@@ -1,283 +0,0 @@
1
- import { arriveSteering, type SteeringPoint, type SteeringVector } from "./Steering";
2
-
3
- export type CrowdFormationType = "line" | "wedge" | "column" | "circle";
4
-
5
- export interface CrowdAgentOptions {
6
- readonly id: string;
7
- readonly position: SteeringPoint;
8
- readonly velocity?: SteeringVector;
9
- readonly radius?: number;
10
- readonly maxSpeed?: number;
11
- readonly priority?: number;
12
- }
13
-
14
- export interface CrowdSimulationOptions {
15
- readonly neighborRadius?: number;
16
- readonly maxNeighbors?: number;
17
- readonly separationWeight?: number;
18
- readonly alignmentWeight?: number;
19
- readonly cohesionWeight?: number;
20
- readonly formationWeight?: number;
21
- readonly maxForce?: number;
22
- }
23
-
24
- export interface CrowdFormationOptions {
25
- readonly type: CrowdFormationType;
26
- readonly center: SteeringPoint;
27
- readonly forward?: SteeringVector;
28
- readonly spacing?: number;
29
- }
30
-
31
- export interface CrowdAgentSnapshot {
32
- readonly id: string;
33
- readonly position: SteeringPoint;
34
- readonly velocity: SteeringVector;
35
- readonly speed: number;
36
- readonly neighborCount: number;
37
- readonly formationSlot: SteeringPoint;
38
- }
39
-
40
- export interface CrowdSimulationSnapshot {
41
- readonly agentCount: number;
42
- readonly neighborPairs: number;
43
- readonly averageNeighbors: number;
44
- readonly averageSpeed: number;
45
- readonly maxSpeed: number;
46
- readonly formationType: CrowdFormationType | "none";
47
- readonly center: SteeringPoint;
48
- readonly agents: readonly CrowdAgentSnapshot[];
49
- }
50
-
51
- type CrowdAgentState = {
52
- readonly id: string;
53
- position: SteeringPoint;
54
- velocity: SteeringVector;
55
- readonly radius: number;
56
- readonly maxSpeed: number;
57
- readonly priority: number;
58
- };
59
-
60
- export class CrowdSimulation {
61
- private readonly agents: CrowdAgentState[] = [];
62
- private readonly neighborRadius: number;
63
- private readonly maxNeighbors: number;
64
- private readonly separationWeight: number;
65
- private readonly alignmentWeight: number;
66
- private readonly cohesionWeight: number;
67
- private readonly formationWeight: number;
68
- private readonly maxForce: number;
69
- private formation: CrowdFormationOptions | undefined;
70
- private lastNeighbors = new Map<string, readonly CrowdAgentState[]>();
71
- private lastSlots = new Map<string, SteeringPoint>();
72
-
73
- constructor(options: CrowdSimulationOptions = {}) {
74
- this.neighborRadius = positive(options.neighborRadius ?? 0.55, "neighborRadius");
75
- this.maxNeighbors = Math.max(1, Math.floor(options.maxNeighbors ?? 6));
76
- this.separationWeight = nonNegative(options.separationWeight ?? 1.8, "separationWeight");
77
- this.alignmentWeight = nonNegative(options.alignmentWeight ?? 0.55, "alignmentWeight");
78
- this.cohesionWeight = nonNegative(options.cohesionWeight ?? 0.42, "cohesionWeight");
79
- this.formationWeight = nonNegative(options.formationWeight ?? 0.85, "formationWeight");
80
- this.maxForce = positive(options.maxForce ?? 1.2, "maxForce");
81
- }
82
-
83
- addAgent(options: CrowdAgentOptions): void {
84
- if (this.agents.some((agent) => agent.id === options.id)) throw new Error(`Crowd agent already exists: ${options.id}`);
85
- this.agents.push({
86
- id: options.id,
87
- position: options.position,
88
- velocity: options.velocity ?? [0, 0],
89
- radius: positive(options.radius ?? 0.1, "radius"),
90
- maxSpeed: positive(options.maxSpeed ?? 0.65, "maxSpeed"),
91
- priority: nonNegative(options.priority ?? 50, "priority")
92
- });
93
- }
94
-
95
- removeAgent(id: string): boolean {
96
- const index = this.agents.findIndex((agent) => agent.id === id);
97
- if (index < 0) return false;
98
- this.agents.splice(index, 1);
99
- this.lastNeighbors.delete(id);
100
- this.lastSlots.delete(id);
101
- return true;
102
- }
103
-
104
- setFormation(options: CrowdFormationOptions | undefined): void {
105
- this.formation = options;
106
- }
107
-
108
- update(deltaSeconds: number): CrowdSimulationSnapshot {
109
- const dt = Math.max(0, deltaSeconds);
110
- const neighborsById = new Map(this.agents.map((agent) => [agent.id, this.neighborsFor(agent)]));
111
- const slots = formationSlots(this.agents.length, this.formation);
112
- this.lastNeighbors = neighborsById;
113
- this.lastSlots = new Map(this.agents.map((agent, index) => [agent.id, slots[index] ?? agent.position]));
114
-
115
- for (let index = 0; index < this.agents.length; index += 1) {
116
- const agent = this.agents[index]!;
117
- const neighbors = neighborsById.get(agent.id) ?? [];
118
- const slot = slots[index] ?? agent.position;
119
- const force = clampMagnitude(addMany([
120
- scale(separationForce(agent, neighbors), this.separationWeight),
121
- scale(alignmentForce(agent, neighbors), this.alignmentWeight),
122
- scale(cohesionForce(agent, neighbors), this.cohesionWeight),
123
- scale(formationForce(agent, slot), this.formationWeight)
124
- ]), this.maxForce);
125
- const priorityDamping = Math.max(0.35, Math.min(1, agent.priority / 100));
126
- const velocity = clampMagnitude([
127
- agent.velocity[0] + force[0] * dt * priorityDamping,
128
- agent.velocity[1] + force[1] * dt * priorityDamping
129
- ], agent.maxSpeed);
130
- agent.velocity = velocity;
131
- agent.position = [
132
- round3(agent.position[0] + velocity[0] * dt),
133
- round3(agent.position[1] + velocity[1] * dt)
134
- ];
135
- }
136
-
137
- return this.snapshot();
138
- }
139
-
140
- snapshot(): CrowdSimulationSnapshot {
141
- const agentSnapshots = this.agents.map((agent) => {
142
- const neighbors = this.lastNeighbors.get(agent.id) ?? [];
143
- return {
144
- id: agent.id,
145
- position: [round3(agent.position[0]), round3(agent.position[1])] as const,
146
- velocity: [round3(agent.velocity[0]), round3(agent.velocity[1])] as const,
147
- speed: round3(length(agent.velocity)),
148
- neighborCount: neighbors.length,
149
- formationSlot: this.lastSlots.get(agent.id) ?? agent.position
150
- };
151
- });
152
- const neighborPairs = agentSnapshots.reduce((sum, agent) => sum + agent.neighborCount, 0) / 2;
153
- const averageSpeed = agentSnapshots.length > 0
154
- ? agentSnapshots.reduce((sum, agent) => sum + agent.speed, 0) / agentSnapshots.length
155
- : 0;
156
- return {
157
- agentCount: agentSnapshots.length,
158
- neighborPairs: round3(neighborPairs),
159
- averageNeighbors: agentSnapshots.length > 0 ? round3((neighborPairs * 2) / agentSnapshots.length) : 0,
160
- averageSpeed: round3(averageSpeed),
161
- maxSpeed: round3(Math.max(0, ...agentSnapshots.map((agent) => agent.speed))),
162
- formationType: this.formation?.type ?? "none",
163
- center: centerOf(agentSnapshots.map((agent) => agent.position)),
164
- agents: agentSnapshots
165
- };
166
- }
167
-
168
- private neighborsFor(agent: CrowdAgentState): readonly CrowdAgentState[] {
169
- return this.agents
170
- .filter((candidate) => candidate !== agent && distance(agent.position, candidate.position) <= this.neighborRadius)
171
- .sort((left, right) => distance(agent.position, left.position) - distance(agent.position, right.position) || left.id.localeCompare(right.id))
172
- .slice(0, this.maxNeighbors);
173
- }
174
- }
175
-
176
- function separationForce(agent: CrowdAgentState, neighbors: readonly CrowdAgentState[]): SteeringVector {
177
- const contributions = neighbors.map((neighbor) => {
178
- const away = subtract(agent.position, neighbor.position);
179
- const gap = Math.max(0.001, length(away) - agent.radius - neighbor.radius);
180
- return scale(normalize(away), 1 / gap);
181
- });
182
- return normalize(addMany(contributions));
183
- }
184
-
185
- function alignmentForce(agent: CrowdAgentState, neighbors: readonly CrowdAgentState[]): SteeringVector {
186
- if (neighbors.length === 0) return [0, 0];
187
- const average = scale(addMany(neighbors.map((neighbor) => neighbor.velocity)), 1 / neighbors.length);
188
- return subtract(clampMagnitude(average, agent.maxSpeed), agent.velocity);
189
- }
190
-
191
- function cohesionForce(agent: CrowdAgentState, neighbors: readonly CrowdAgentState[]): SteeringVector {
192
- if (neighbors.length === 0) return [0, 0];
193
- const center = centerOf(neighbors.map((neighbor) => neighbor.position));
194
- return arriveSteering({ position: agent.position, velocity: agent.velocity, target: center, maxSpeed: agent.maxSpeed, slowingRadius: 0.45 }).force;
195
- }
196
-
197
- function formationForce(agent: CrowdAgentState, slot: SteeringPoint): SteeringVector {
198
- return arriveSteering({ position: agent.position, velocity: agent.velocity, target: slot, maxSpeed: agent.maxSpeed, slowingRadius: 0.35, tolerance: 0.025 }).force;
199
- }
200
-
201
- function formationSlots(count: number, formation: CrowdFormationOptions | undefined): readonly SteeringPoint[] {
202
- if (!formation) return [];
203
- const spacing = formation.spacing ?? 0.24;
204
- const forward = normalize(formation.forward ?? [1, 0]);
205
- const right: SteeringVector = [forward[1], -forward[0]];
206
- return Array.from({ length: count }, (_, index) => {
207
- switch (formation.type) {
208
- case "line":
209
- return offset(formation.center, right, (index - (count - 1) / 2) * spacing);
210
- case "column":
211
- return offset(formation.center, forward, -index * spacing);
212
- case "circle": {
213
- const angle = (Math.PI * 2 * index) / Math.max(1, count);
214
- return [round3(formation.center[0] + Math.cos(angle) * spacing), round3(formation.center[1] + Math.sin(angle) * spacing)];
215
- }
216
- case "wedge": {
217
- if (index === 0) return formation.center;
218
- const side = index % 2 === 0 ? 1 : -1;
219
- const row = Math.ceil(index / 2);
220
- return add(offset(formation.center, forward, -row * spacing), scale(right, side * row * spacing));
221
- }
222
- }
223
- });
224
- }
225
-
226
- function offset(origin: SteeringPoint, direction: SteeringVector, amount: number): SteeringPoint {
227
- return [round3(origin[0] + direction[0] * amount), round3(origin[1] + direction[1] * amount)];
228
- }
229
-
230
- function add(left: SteeringPoint, right: SteeringVector): SteeringPoint {
231
- return [round3(left[0] + right[0]), round3(left[1] + right[1])];
232
- }
233
-
234
- function subtract(left: SteeringPoint, right: SteeringPoint): SteeringVector {
235
- return [left[0] - right[0], left[1] - right[1]];
236
- }
237
-
238
- function scale(value: SteeringVector, scalar: number): SteeringVector {
239
- return [value[0] * scalar, value[1] * scalar];
240
- }
241
-
242
- function addMany(values: readonly SteeringVector[]): SteeringVector {
243
- return values.reduce<SteeringVector>((sum, value) => [sum[0] + value[0], sum[1] + value[1]], [0, 0]);
244
- }
245
-
246
- function distance(left: SteeringPoint, right: SteeringPoint): number {
247
- return length(subtract(left, right));
248
- }
249
-
250
- function length(value: SteeringVector): number {
251
- return Math.hypot(value[0], value[1]);
252
- }
253
-
254
- function normalize(value: SteeringVector): SteeringVector {
255
- const magnitude = length(value);
256
- return magnitude === 0 ? [0, 0] : [value[0] / magnitude, value[1] / magnitude];
257
- }
258
-
259
- function clampMagnitude(value: SteeringVector, maxMagnitude: number): SteeringVector {
260
- const magnitude = length(value);
261
- if (magnitude === 0 || magnitude <= maxMagnitude) return value;
262
- return scale(value, maxMagnitude / magnitude);
263
- }
264
-
265
- function centerOf(points: readonly SteeringPoint[]): SteeringPoint {
266
- if (points.length === 0) return [0, 0];
267
- const sum = points.reduce<SteeringVector>((total, point) => [total[0] + point[0], total[1] + point[1]], [0, 0]);
268
- return [round3(sum[0] / points.length), round3(sum[1] / points.length)];
269
- }
270
-
271
- function positive(value: number, name: string): number {
272
- if (!Number.isFinite(value) || value <= 0) throw new RangeError(`Crowd ${name} must be a finite positive number.`);
273
- return value;
274
- }
275
-
276
- function nonNegative(value: number, name: string): number {
277
- if (!Number.isFinite(value) || value < 0) throw new RangeError(`Crowd ${name} must be a finite non-negative number.`);
278
- return value;
279
- }
280
-
281
- function round3(value: number): number {
282
- return Number(value.toFixed(3));
283
- }
@@ -1,53 +0,0 @@
1
- import type { PhysicsWorld } from "./PhysicsWorld.js";
2
- import { interpolatePosition } from "./ScenePhysicsBridge.js";
3
- import type { Vec3 } from "./Shape.js";
4
-
5
- export type ECSTransformLike = {
6
- position: [number, number, number];
7
- };
8
-
9
- export type ECSPhysicsBinding = {
10
- readonly bodyId: number;
11
- readonly transform: ECSTransformLike;
12
- readonly mode?: "dynamic" | "kinematic";
13
- };
14
-
15
- export class ECSPhysicsBridge {
16
- private readonly bindings = new Map<number, ECSPhysicsBinding>();
17
-
18
- bind(binding: ECSPhysicsBinding): void {
19
- if (this.bindings.has(binding.bodyId)) {
20
- throw new Error(`Body ${binding.bodyId} is already bound to an ECS transform.`);
21
- }
22
- this.bindings.set(binding.bodyId, binding);
23
- }
24
-
25
- unbind(bodyId: number): void {
26
- this.bindings.delete(bodyId);
27
- }
28
-
29
- pushKinematic(world: Pick<PhysicsWorld, "getBody">): void {
30
- for (const binding of this.bindings.values()) {
31
- if (binding.mode !== "kinematic") {
32
- continue;
33
- }
34
- world.getBody(binding.bodyId)?.setPosition(binding.transform.position);
35
- }
36
- }
37
-
38
- pullDynamic(world: Pick<PhysicsWorld, "getBody">, alpha = 1): void {
39
- for (const binding of this.bindings.values()) {
40
- if (binding.mode === "kinematic") {
41
- continue;
42
- }
43
- const body = world.getBody(binding.bodyId);
44
- if (!body) {
45
- continue;
46
- }
47
- const interpolated = interpolatePosition(body.previousPosition, body.position, alpha);
48
- binding.transform.position[0] = interpolated[0];
49
- binding.transform.position[1] = interpolated[1];
50
- binding.transform.position[2] = interpolated[2];
51
- }
52
- }
53
- }