@thi.ng/boids 0.1.15 → 1.0.0

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/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- - **Last updated**: 2023-12-31T09:44:23Z
3
+ - **Last updated**: 2024-01-23T15:58:26Z
4
4
  - **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
5
5
 
6
6
  All notable changes to this project will be documented in this file.
@@ -9,6 +9,37 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
9
9
  **Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
10
10
  and/or version bumps of transitive dependencies.
11
11
 
12
+ # [1.0.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/boids@1.0.0) (2024-01-23)
13
+
14
+ #### 🛑 Breaking changes
15
+
16
+ - major refactoring/restructuring, new behavior system ([22cb42d](https://github.com/thi-ng/umbrella/commit/22cb42d))
17
+ - BREAKING CHANGE: major refactoring/restructuring, new behavior system
18
+ - modular behavior system with currently these 7 behaviors:
19
+ - alignment()
20
+ - attractPolyline()
21
+ - braitenberg2()
22
+ - cohesion()
23
+ - dynamicTarget()
24
+ - followPolyline()
25
+ - separation()
26
+ - add blendedBehaviorUpdate()
27
+ - add clamp2/3() and wrap2/3() global constraints
28
+ - add Flock class (+ defFlock() factory fn)
29
+ - update/simplify Boid class and 2d/3d factory fns
30
+ - update BoidOpts
31
+ - add doc strings
32
+ - add/update deps
33
+
34
+ #### 🚀 Features
35
+
36
+ - update dynamicTarget() ([647d7e1](https://github.com/thi-ng/umbrella/commit/647d7e1))
37
+ - add radius param
38
+
39
+ #### 🩹 Bug fixes
40
+
41
+ - update separation() behavior neighbor lookup ([c8ece89](https://github.com/thi-ng/umbrella/commit/c8ece89))
42
+
12
43
  ### [0.1.3](https://github.com/thi-ng/umbrella/tree/@thi.ng/boids@0.1.3) (2023-11-09)
13
44
 
14
45
  #### ♻️ Refactoring
package/README.md CHANGED
@@ -12,6 +12,8 @@ This is a standalone project, maintained as part of the
12
12
  anti-framework.
13
13
 
14
14
  - [About](#about)
15
+ - [Available behaviors](#available-behaviors)
16
+ - [Acceleration structures](#acceleration-structures)
15
17
  - [Status](#status)
16
18
  - [Installation](#installation)
17
19
  - [Dependencies](#dependencies)
@@ -22,7 +24,57 @@ anti-framework.
22
24
 
23
25
  ## About
24
26
 
25
- n-dimensional boids simulation with highly configurable behaviors.
27
+ n-dimensional boids simulation with modular behavior system.
28
+
29
+ The API of this package is still unstable, but the underlying implementations
30
+ have been used in many of the author's projects since ~2005... The agent/boid
31
+ behaviors are fully modular and can be highly customized via the given
32
+ parameters (which can also be dynamically/spatially adjusted). As with other
33
+ thi.ng packages, the visual representation of the boids is entirely separate and
34
+ out of scope of this package. This package only deals with the simulation of
35
+ agents, their behavioral aspects and essentially only processes points in space
36
+ (and their directions, forces)...
37
+
38
+ ### Available behaviors
39
+
40
+ The following behavior building blocks are provided. All of them can be freely
41
+ combined (incl. multiple instances with different configurations) and assigned
42
+ to individual boids (or groups of them). Each behavior also has an associated
43
+ weight to adjust its impact on the overall movement of the boids (also
44
+ dynamically adjustable).
45
+
46
+ - [`alignment()`](https://docs.thi.ng/umbrella/boids/functions/alignment.html):
47
+ Steer towards the average direction of neighbors within given radius
48
+ - [`attractPolyline()`](https://docs.thi.ng/umbrella/boids/functions/attractPolyline.html):
49
+ Steer towards the nearest point on a pre-configured polyline (or polygon)
50
+ - [`braitenberg2()`](https://docs.thi.ng/umbrella/boids/functions/braitenberg2.html):
51
+ Field-based 3-sensor (left/right/center) Braitenberg vehicle steering
52
+ - [`cohesion()`](https://docs.thi.ng/umbrella/boids/functions/cohesion.html):
53
+ Steer towards the centroid of neighbors within given radius
54
+ - [`dynamicTarget()`](https://docs.thi.ng/umbrella/boids/functions/dynamicTarget.html):
55
+ Steer towards user defined (dynamically changeable) location(s)
56
+ - [`followPolyline()`](https://docs.thi.ng/umbrella/boids/functions/followPolyline.html):
57
+ Steer towards the following/next point on a pre-configured polyline (or
58
+ polygon)
59
+ - [`separation()`](https://docs.thi.ng/umbrella/boids/functions/separation.html):
60
+ Steer away from neighbors within given radius
61
+
62
+ ### Acceleration structures
63
+
64
+ Intended for behaviors requiring neighbor lookups, the package defines &
65
+ utilizes the [`IBoidAccel`
66
+ interface](https://docs.thi.ng/umbrella/boids/interfaces/IBoidAccel.html). It's
67
+ recommended to use a compatible spatial acceleration structure such as
68
+ [`HashGrid2` or
69
+ `HashGrid3`](https://docs.thi.ng/umbrella/geom-accel/classes/HashGrid2.html#queryNeighborhood)
70
+ from the [@thi.ng/geom-accel
71
+ package](https://github.com/thi-ng/umbrella/tree/develop/packages/geom-accel).
72
+ For cases where this isn't needed, the
73
+ [`noAccel`](https://docs.thi.ng/umbrella/boids/functions/noAccel.html) dummy
74
+ implementation of this interface can be used... In all cases, an acceleration
75
+ structure has to be provided to the boid ctor and factory functions
76
+ [`defBoid2()`](https://docs.thi.ng/umbrella/boids/functions/defBoid2.html) /
77
+ [`defBoid3()`](https://docs.thi.ng/umbrella/boids/functions/defBoid3.html).
26
78
 
27
79
  ## Status
28
80
 
@@ -50,13 +102,16 @@ For Node.js REPL:
50
102
  const boids = await import("@thi.ng/boids");
51
103
  ```
52
104
 
53
- Package sizes (brotli'd, pre-treeshake): ESM: 1.13 KB
105
+ Package sizes (brotli'd, pre-treeshake): ESM: 1.92 KB
54
106
 
55
107
  ## Dependencies
56
108
 
57
109
  - [@thi.ng/api](https://github.com/thi-ng/umbrella/tree/develop/packages/api)
110
+ - [@thi.ng/checks](https://github.com/thi-ng/umbrella/tree/develop/packages/checks)
58
111
  - [@thi.ng/distance](https://github.com/thi-ng/umbrella/tree/develop/packages/distance)
59
- - [@thi.ng/geom-accel](https://github.com/thi-ng/umbrella/tree/develop/packages/geom-accel)
112
+ - [@thi.ng/geom-closest-point](https://github.com/thi-ng/umbrella/tree/develop/packages/geom-closest-point)
113
+ - [@thi.ng/geom-resample](https://github.com/thi-ng/umbrella/tree/develop/packages/geom-resample)
114
+ - [@thi.ng/math](https://github.com/thi-ng/umbrella/tree/develop/packages/math)
60
115
  - [@thi.ng/timestep](https://github.com/thi-ng/umbrella/tree/develop/packages/timestep)
61
116
  - [@thi.ng/vectors](https://github.com/thi-ng/umbrella/tree/develop/packages/vectors)
62
117
 
@@ -93,4 +148,4 @@ If this project contributes to an academic publication, please cite it as:
93
148
 
94
149
  ## License
95
150
 
96
- © 2023 Karsten Schmidt // Apache License 2.0
151
+ © 2023 - 2024 Karsten Schmidt // Apache License 2.0
package/accel.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { IBoidAccel } from "./api.js";
2
+ /**
3
+ * Dummy {@link IBoidAccel} implementation (i.e does no spatial indexing and
4
+ * always processes _all_ boids).
5
+ *
6
+ * @remarks
7
+ * Due to no incurred overhead for (re)constructing the acceleration structure,
8
+ * for low numbers of boids this can actually be more efficient than using a
9
+ * `HashGrid` (from thi.ng/geom-accel package).
10
+ */
11
+ export declare const noAccel: () => IBoidAccel;
12
+ //# sourceMappingURL=accel.d.ts.map
package/accel.js ADDED
@@ -0,0 +1,16 @@
1
+ const noAccel = () => {
2
+ let boids;
3
+ return {
4
+ build($boids) {
5
+ boids = $boids;
6
+ },
7
+ queryNeighborhood(neighborhood) {
8
+ for (let b of boids)
9
+ neighborhood.consider(b.pos.curr, b);
10
+ return neighborhood;
11
+ }
12
+ };
13
+ };
14
+ export {
15
+ noAccel
16
+ };
package/api.d.ts CHANGED
@@ -1,28 +1,45 @@
1
- import { type FnU } from "@thi.ng/api";
2
- import type { Vec } from "@thi.ng/vectors";
1
+ import type { Fn, Fn2, IDeref } from "@thi.ng/api";
2
+ import type { INeighborhood } from "@thi.ng/distance";
3
+ import type { ReadonlyVec, Vec } from "@thi.ng/vectors";
4
+ import type { Boid } from "./boid.js";
5
+ export type GlobalConstraint = Fn2<Vec, Boid, Vec>;
3
6
  export interface BoidOpts {
4
- constrain: FnU<Vec>;
5
- maxSpeed: number;
6
- maxForce: number;
7
7
  /**
8
- * Min distance to neighboring boids
8
+ * Boid behaviors
9
+ */
10
+ behaviors: IBoidBehavior[];
11
+ /**
12
+ * Behavior update function. Default: {@link blendedBehaviorUpdate}
9
13
  */
10
- minDist: number;
14
+ update?: Fn<Boid, Vec>;
11
15
  /**
12
- * Max distance to neighboring boids
16
+ * Spatial acceleration structure to use for {@link Boid.neighbors} lookups.
13
17
  */
14
- maxDist: number;
18
+ accel: IBoidAccel;
15
19
  /**
16
- * Relative weight for enforcing separation
20
+ * Position constraint. If used, the function MUST mutate the given vector
21
+ * (1st arg) and also return it.
17
22
  */
18
- separation: number;
23
+ constrain?: GlobalConstraint;
19
24
  /**
20
- * Relative weight for directional alignment
25
+ * Max speed (per second)
21
26
  */
22
- alignment: number;
27
+ maxSpeed: number;
23
28
  /**
24
- * Relative weight for group cohesion
29
+ * Scale factor for applying any of the separation, alignment and cohesion
30
+ * forces.
31
+ *
32
+ * @defaultValue 1
25
33
  */
26
- cohesion: number;
34
+ maxForce?: number;
35
+ }
36
+ export interface IBoidAccel {
37
+ build(boids: Boid[]): void;
38
+ queryNeighborhood<N extends INeighborhood<ReadonlyVec, Boid> & IDeref<Boid[]>>(neighborhood: N): N;
39
+ }
40
+ export interface IBoidBehavior {
41
+ weight(boid: Boid): number;
42
+ update(boid: Boid): ReadonlyVec;
27
43
  }
44
+ export type ScalarOrField = number | Fn<Boid, number>;
28
45
  //# sourceMappingURL=api.d.ts.map
package/api.js CHANGED
@@ -1 +0,0 @@
1
- import {} from "@thi.ng/api";
@@ -0,0 +1,3 @@
1
+ import type { IBoidBehavior, ScalarOrField } from "../api.js";
2
+ export declare const alignment: (maxDist: ScalarOrField, weight?: ScalarOrField) => IBoidBehavior;
3
+ //# sourceMappingURL=alignment.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { __ensureFn } from "../internal/ensure.js";
2
+ const alignment = (maxDist, weight = 1) => {
3
+ const $maxDist = __ensureFn(maxDist);
4
+ const force = [];
5
+ return {
6
+ weight: __ensureFn(weight),
7
+ update: (boid) => {
8
+ const { add, setN } = boid.api;
9
+ const neighbors = boid.neighbors($maxDist(boid), boid.pos.curr);
10
+ const num = neighbors.length;
11
+ setN(force, 0);
12
+ for (let i = 0; i < num; i++) {
13
+ const n = neighbors[i];
14
+ if (n !== boid)
15
+ add(force, force, n.vel.curr);
16
+ }
17
+ return boid.computeSteer(force, num - 1);
18
+ }
19
+ };
20
+ };
21
+ export {
22
+ alignment
23
+ };
@@ -0,0 +1,4 @@
1
+ import type { ReadonlyVec } from "@thi.ng/vectors";
2
+ import type { ScalarOrField, IBoidBehavior } from "../api.js";
3
+ export declare const attractPolyline: (points: ReadonlyVec[], closed: boolean, lookahead?: number, weight?: ScalarOrField) => IBoidBehavior;
4
+ //# sourceMappingURL=attraction.d.ts.map
@@ -0,0 +1,25 @@
1
+ import { closestPointPolyline } from "@thi.ng/geom-closest-point";
2
+ import { __ensureFn } from "../internal/ensure.js";
3
+ const attractPolyline = (points, closed, lookahead = 1, weight = 1) => {
4
+ const closest = [];
5
+ const pos = [];
6
+ return {
7
+ weight: __ensureFn(weight),
8
+ update: (boid) => {
9
+ const { add, normalize } = boid.api;
10
+ return closestPointPolyline(
11
+ lookahead !== 0 ? add(
12
+ pos,
13
+ normalize(pos, boid.vel.curr, lookahead),
14
+ boid.pos.curr
15
+ ) : boid.pos.curr,
16
+ points,
17
+ closed,
18
+ closest
19
+ ) ? boid.steerTowards(closest) : boid.api.ZERO;
20
+ }
21
+ };
22
+ };
23
+ export {
24
+ attractPolyline
25
+ };
@@ -0,0 +1,18 @@
1
+ import type { Fn } from "@thi.ng/api";
2
+ import type { ReadonlyVec } from "@thi.ng/vectors";
3
+ import type { ScalarOrField, IBoidBehavior } from "../api.js";
4
+ /**
5
+ * 2D only behavior. Takes a field function to be sampled, a sensor `lookahead`
6
+ * distance and `angle` between left/right sensors. The returned behavior steers
7
+ * an agent towards the local maxima of the field function based on a given
8
+ * agent's position and direction. Steers toward which ever sensor yields the
9
+ * greater value. If both sensors yield the same reading, the behavior is a
10
+ * no-op (returns a zero force vector).
11
+ *
12
+ * @param field
13
+ * @param lookahead
14
+ * @param angle
15
+ * @param weight
16
+ */
17
+ export declare const braitenberg2: (field: Fn<ReadonlyVec, number>, lookahead: number, angle: number, weight?: ScalarOrField) => IBoidBehavior;
18
+ //# sourceMappingURL=braitenberg.d.ts.map
@@ -0,0 +1,22 @@
1
+ import { rotate } from "@thi.ng/vectors/rotate";
2
+ import { __ensureFn } from "../internal/ensure.js";
3
+ const braitenberg2 = (field, lookahead, angle, weight = 1) => {
4
+ const dir = [];
5
+ const left = [];
6
+ const right = [];
7
+ return {
8
+ weight: __ensureFn(weight),
9
+ update: (boid) => {
10
+ const { add, maddN, normalize } = boid.api;
11
+ normalize(dir, boid.vel.curr, lookahead);
12
+ const pos = boid.pos.curr;
13
+ const valC = field(pos);
14
+ const valL = field(add(left, rotate(left, dir, angle), pos));
15
+ const valR = field(add(right, rotate(right, dir, -angle), pos));
16
+ return valC > valL && valC > valR ? boid.steerTowards(maddN(left, boid.vel.curr, -1, pos)) : valL === valR ? boid.api.ZERO : boid.steerTowards(valL > valR ? left : right);
17
+ }
18
+ };
19
+ };
20
+ export {
21
+ braitenberg2
22
+ };
@@ -0,0 +1,3 @@
1
+ import type { IBoidBehavior, ScalarOrField } from "../api.js";
2
+ export declare const cohesion: (maxDist: ScalarOrField, weight?: ScalarOrField) => IBoidBehavior;
3
+ //# sourceMappingURL=cohesion.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { __ensureFn } from "../internal/ensure.js";
2
+ const cohesion = (maxDist, weight = 1) => {
3
+ const $maxDist = __ensureFn(maxDist);
4
+ const centroid = [];
5
+ return {
6
+ weight: __ensureFn(weight),
7
+ update: (boid) => {
8
+ const { add, mulN, setN } = boid.api;
9
+ const neighbors = boid.neighbors($maxDist(boid), boid.pos.curr);
10
+ const num = neighbors.length;
11
+ setN(centroid, 0);
12
+ for (let i = 0; i < num; i++) {
13
+ const n = neighbors[i];
14
+ if (n !== boid)
15
+ add(centroid, centroid, n.pos.curr);
16
+ }
17
+ return num > 1 ? boid.steerTowards(mulN(centroid, centroid, 1 / (num - 1))) : centroid;
18
+ }
19
+ };
20
+ };
21
+ export {
22
+ cohesion
23
+ };
@@ -0,0 +1,19 @@
1
+ import type { Fn, Nullable } from "@thi.ng/api";
2
+ import type { ReadonlyVec } from "@thi.ng/vectors";
3
+ import type { IBoidBehavior, ScalarOrField } from "../api.js";
4
+ import type { Boid } from "../boid.js";
5
+ /**
6
+ * Boid behavior which steers toward target positions sourced from user provided
7
+ * `target` fn. That `target` function will be called successively for each boid
8
+ * update. If it returns a point, it will be used as steer target until a new
9
+ * one is returned. If the function returns null/undefined the current target
10
+ * will be kept. The behavior is a no-op for boids outside the configured
11
+ * `radius` (around the target) and also has no effect until the target function
12
+ * returns its first point.
13
+ *
14
+ * @param target
15
+ * @param radius
16
+ * @param weight
17
+ */
18
+ export declare const dynamicTarget: (target: Fn<Boid, Nullable<ReadonlyVec>>, radius?: ScalarOrField, weight?: ScalarOrField) => IBoidBehavior;
19
+ //# sourceMappingURL=dynamic.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { __ensureFn } from "../internal/ensure.js";
2
+ const dynamicTarget = (target, radius = Infinity, weight = 1) => {
3
+ let currTarget;
4
+ const $radius = __ensureFn(radius);
5
+ const force = [];
6
+ return {
7
+ weight: __ensureFn(weight),
8
+ update: (boid) => {
9
+ currTarget = target(boid) || currTarget;
10
+ if (!currTarget)
11
+ return boid.api.ZERO;
12
+ const r = $radius(boid);
13
+ return boid.api.distSq(currTarget, boid.pos.curr) < r * r ? boid.steerTowards(currTarget, force) : boid.api.ZERO;
14
+ }
15
+ };
16
+ };
17
+ export {
18
+ dynamicTarget
19
+ };
@@ -0,0 +1,14 @@
1
+ import type { ReadonlyVec } from "@thi.ng/vectors";
2
+ import type { IBoidBehavior, ScalarOrField } from "../api.js";
3
+ /**
4
+ * Similar to {@link attractPolyline}, but forces steering along the path in the
5
+ * given order of points, using normalized `lookahead`. If `closed` is false,
6
+ * the behavior becomes a no-op for boids at the end of the path.
7
+ *
8
+ * @param points
9
+ * @param closed
10
+ * @param lookahead
11
+ * @param weight
12
+ */
13
+ export declare const followPolyline: (points: ReadonlyVec[], closed: boolean, lookahead?: number, weight?: ScalarOrField) => IBoidBehavior;
14
+ //# sourceMappingURL=follow.d.ts.map
@@ -0,0 +1,18 @@
1
+ import { Sampler } from "@thi.ng/geom-resample/sampler";
2
+ import { fract } from "@thi.ng/math/prec";
3
+ import { __ensureFn } from "../internal/ensure.js";
4
+ const followPolyline = (points, closed, lookahead = 0.01, weight = 1) => {
5
+ const sampler = new Sampler(points, closed);
6
+ return {
7
+ weight: __ensureFn(weight),
8
+ update: (boid) => {
9
+ const t = sampler.closestT(boid.pos.curr);
10
+ if (t === void 0 || !closed && t + lookahead > 1)
11
+ return boid.api.ZERO;
12
+ return boid.steerTowards(sampler.pointAt(fract(t + lookahead)));
13
+ }
14
+ };
15
+ };
16
+ export {
17
+ followPolyline
18
+ };
@@ -0,0 +1,4 @@
1
+ import type { Vec } from "@thi.ng/vectors";
2
+ import type { Boid } from "../boid.js";
3
+ export declare const mixedBehaviorUpdate: (boid: Boid) => Vec;
4
+ //# sourceMappingURL=mixed.d.ts.map
@@ -0,0 +1,13 @@
1
+ const mixedBehaviorUpdate = (boid) => {
2
+ const { maddN, zeroes } = boid.api;
3
+ const force = zeroes();
4
+ for (let behavior of boid.behaviors) {
5
+ const weight = behavior.weight(boid);
6
+ if (weight !== 0)
7
+ maddN(force, behavior.update(boid), weight, force);
8
+ }
9
+ return force;
10
+ };
11
+ export {
12
+ mixedBehaviorUpdate
13
+ };
@@ -0,0 +1,4 @@
1
+ import type { ReadonlyVec } from "@thi.ng/vectors";
2
+ import type { ScalarOrField, IBoidBehavior } from "../api.js";
3
+ export declare const followPath: (points: ReadonlyVec[], closed: boolean, weight?: ScalarOrField) => IBoidBehavior;
4
+ //# sourceMappingURL=path.d.ts.map
@@ -0,0 +1,18 @@
1
+ import { closestPointPolyline } from "@thi.ng/geom-closest-point";
2
+ import { __ensureFn } from "../internal/ensure.js";
3
+ const followPath = (points, closed, weight = 1) => {
4
+ const closest = [];
5
+ const pos = [];
6
+ return {
7
+ weight: __ensureFn(weight),
8
+ update: (boid) => closestPointPolyline(
9
+ boid.api.add(pos, boid.pos.curr, boid.vel.curr),
10
+ points,
11
+ closed,
12
+ closest
13
+ ) ? boid.steerTowards(closest) : boid.api.ZERO
14
+ };
15
+ };
16
+ export {
17
+ followPath
18
+ };
@@ -0,0 +1,4 @@
1
+ import type { Vec } from "@thi.ng/vectors";
2
+ import type { Boid } from "../boid.js";
3
+ export declare const probabilisticBehaviorUpdate: (boid: Boid) => Vec;
4
+ //# sourceMappingURL=probabilistic.d.ts.map
@@ -0,0 +1,24 @@
1
+ import { argSort } from "@thi.ng/arrays/arg-sort";
2
+ const probabilisticBehaviorUpdate = (boid) => {
3
+ const { maddN, magSq, zeroes } = boid.api;
4
+ const force = zeroes();
5
+ const behaviors = boid.behaviors;
6
+ const num = behaviors.length;
7
+ const weights = behaviors.map((b) => b.weight(boid));
8
+ const order = argSort(weights, (a, b) => b - a);
9
+ for (let i = 0; i < num; i++) {
10
+ const id = order[i];
11
+ const weight = weights[id];
12
+ if (Math.random() < weight) {
13
+ const f = behaviors[id].update(boid);
14
+ if (magSq(f) > 0) {
15
+ maddN(force, f, weight, force);
16
+ break;
17
+ }
18
+ }
19
+ }
20
+ return force;
21
+ };
22
+ export {
23
+ probabilisticBehaviorUpdate
24
+ };
@@ -0,0 +1,3 @@
1
+ import type { IBoidBehavior, ScalarOrField } from "../api.js";
2
+ export declare const separation: (minDist: ScalarOrField, weight?: ScalarOrField) => IBoidBehavior;
3
+ //# sourceMappingURL=separation.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { __ensureFn } from "../internal/ensure.js";
2
+ const separation = (minDist, weight = 1) => {
3
+ const $minDist = __ensureFn(minDist);
4
+ const force = [];
5
+ const delta = [];
6
+ return {
7
+ weight: __ensureFn(weight),
8
+ update: (boid) => {
9
+ const { maddN, magSq, setN, sub } = boid.api;
10
+ const pos = boid.pos.curr;
11
+ const neighbors = boid.neighbors($minDist(boid), pos);
12
+ const num = neighbors.length;
13
+ let n;
14
+ setN(force, 0);
15
+ for (let i = 0; i < num; i++) {
16
+ n = neighbors[i];
17
+ if (n !== boid) {
18
+ sub(delta, pos, n.pos.curr);
19
+ maddN(force, delta, 1 / (magSq(delta) + 1e-6), force);
20
+ }
21
+ }
22
+ return boid.computeSteer(force, num - 1);
23
+ }
24
+ };
25
+ };
26
+ export {
27
+ separation
28
+ };
@@ -0,0 +1,4 @@
1
+ import type { Vec } from "@thi.ng/vectors";
2
+ import type { Boid } from "../boid.js";
3
+ export declare const blendedBehaviorUpdate: (boid: Boid) => Vec;
4
+ //# sourceMappingURL=update.d.ts.map
@@ -0,0 +1,13 @@
1
+ const blendedBehaviorUpdate = (boid) => {
2
+ const { maddN, zeroes } = boid.api;
3
+ const force = zeroes();
4
+ for (let behavior of boid.behaviors) {
5
+ const weight = behavior.weight(boid);
6
+ if (weight !== 0)
7
+ maddN(force, behavior.update(boid), weight, force);
8
+ }
9
+ return force;
10
+ };
11
+ export {
12
+ blendedBehaviorUpdate
13
+ };
package/boid.d.ts CHANGED
@@ -1,22 +1,18 @@
1
1
  import type { IDistance } from "@thi.ng/distance";
2
- import type { AHashGrid, HashGrid2, HashGrid3 } from "@thi.ng/geom-accel/hash-grid";
3
2
  import type { ITimeStep, ReadonlyTimeStep } from "@thi.ng/timestep";
4
3
  import { VectorState } from "@thi.ng/timestep/state";
5
4
  import type { ReadonlyVec, Vec, VecAPI } from "@thi.ng/vectors";
6
- import type { BoidOpts } from "./api.js";
5
+ import type { BoidOpts, IBoidAccel, IBoidBehavior } from "./api.js";
7
6
  import { Radial } from "./region.js";
8
7
  export declare class Boid implements ITimeStep {
9
- readonly accel: AHashGrid<Boid>;
10
- readonly api: VecAPI;
11
8
  pos: VectorState;
12
9
  vel: VectorState;
13
- opts: BoidOpts;
10
+ api: VecAPI;
11
+ accel: IBoidAccel;
12
+ behaviors: IBoidBehavior[];
14
13
  region: Radial<Boid>;
15
- protected cachedNeighbors: Boid[];
16
- protected tmpSep: Vec;
17
- protected tmpAlign: Vec;
18
- protected tmpCoh: Vec;
19
- constructor(accel: AHashGrid<Boid>, api: VecAPI, distance: IDistance<ReadonlyVec>, pos: Vec, vel: Vec, opts: Partial<BoidOpts>);
14
+ opts: BoidOpts;
15
+ constructor(opts: BoidOpts, api: VecAPI, distance: IDistance<ReadonlyVec>, pos: Vec, vel: Vec);
20
16
  /**
21
17
  * Integration step of the thi.ng/timestep update cycle. See
22
18
  * [`ITimeStep`](https://docs.thi.ng/umbrella/timestep/interfaces/ITimeStep.html)
@@ -47,12 +43,9 @@ export declare class Boid implements ITimeStep {
47
43
  * @param pos
48
44
  */
49
45
  neighbors(r: number, pos?: Vec): Boid[];
50
- protected separate(): Vec;
51
- protected align(): Vec;
52
- protected cohesion(): Vec;
53
- protected steerTowards(target: ReadonlyVec, out?: Vec): Vec;
54
- protected computeSteer(steer: Vec, num: number): Vec;
55
- protected limitSteer(steer: Vec): Vec;
46
+ steerTowards(target: ReadonlyVec, out?: Vec): Vec;
47
+ computeSteer(steer: Vec, num: number): Vec;
48
+ limitSteer(steer: Vec): Vec;
56
49
  }
57
50
  /**
58
51
  * Returns a new {@link Boid} instance configured to use optimized 2D vector
@@ -63,7 +56,7 @@ export declare class Boid implements ITimeStep {
63
56
  * @param vel
64
57
  * @param opts
65
58
  */
66
- export declare const defBoid2: (accel: HashGrid2<Boid>, pos: Vec, vel: Vec, opts: Partial<BoidOpts>) => Boid;
59
+ export declare const defBoid2: (pos: Vec, vel: Vec, opts: BoidOpts) => Boid;
67
60
  /**
68
61
  * Returns a new {@link Boid} instance configured to use optimized 3D vector
69
62
  * operations.
@@ -73,5 +66,5 @@ export declare const defBoid2: (accel: HashGrid2<Boid>, pos: Vec, vel: Vec, opts
73
66
  * @param vel
74
67
  * @param opts
75
68
  */
76
- export declare const defBoid3: (accel: HashGrid3<Boid>, pos: Vec, vel: Vec, opts: Partial<BoidOpts>) => Boid;
69
+ export declare const defBoid3: (pos: Vec, vel: Vec, opts: BoidOpts) => Boid;
77
70
  //# sourceMappingURL=boid.d.ts.map
package/boid.js CHANGED
@@ -2,59 +2,38 @@ import { identity } from "@thi.ng/api";
2
2
  import { DIST_SQ2, DIST_SQ3 } from "@thi.ng/distance/squared";
3
3
  import { VectorState, defVector } from "@thi.ng/timestep/state";
4
4
  import { integrateAll, interpolateAll } from "@thi.ng/timestep/timestep";
5
- import { addW4 } from "@thi.ng/vectors/addw";
6
5
  import { VEC2 } from "@thi.ng/vectors/vec2-api";
7
6
  import { VEC3 } from "@thi.ng/vectors/vec3-api";
7
+ import { blendedBehaviorUpdate } from "./behaviors/update.js";
8
8
  import { Radial } from "./region.js";
9
9
  class Boid {
10
- constructor(accel, api, distance, pos, vel, opts) {
11
- this.accel = accel;
10
+ pos;
11
+ vel;
12
+ api;
13
+ accel;
14
+ behaviors;
15
+ region;
16
+ opts;
17
+ constructor(opts, api, distance, pos, vel) {
12
18
  this.api = api;
13
- this.opts = {
14
- constrain: identity,
15
- maxSpeed: 10,
16
- maxForce: 1,
17
- minDist: 20,
18
- maxDist: 50,
19
- separation: 2,
20
- alignment: 1,
21
- cohesion: 1,
22
- ...opts
23
- };
19
+ this.opts = { maxForce: 1, ...opts };
20
+ this.accel = this.opts.accel;
21
+ this.behaviors = this.opts.behaviors;
22
+ const update = this.opts.update || blendedBehaviorUpdate;
23
+ const constrain = this.opts.constrain || identity;
24
+ const { add, limit, maddN } = api;
24
25
  this.vel = defVector(
25
26
  api,
26
27
  vel,
27
- (vel2) => api.limit(
28
- vel2,
29
- addW4(
30
- vel2,
31
- vel2,
32
- this.separate(),
33
- this.align(),
34
- this.cohesion(),
35
- 1,
36
- this.opts.separation,
37
- this.opts.alignment,
38
- this.opts.cohesion
39
- ),
40
- this.opts.maxSpeed
41
- )
28
+ (vel2) => limit(vel2, add(vel2, vel2, update(this)), this.opts.maxSpeed)
42
29
  );
43
30
  this.pos = defVector(
44
31
  api,
45
32
  pos,
46
- (pos2, dt) => this.opts.constrain(api.maddN(pos2, this.vel.curr, dt, pos2))
33
+ (pos2, dt) => constrain(maddN(pos2, this.vel.curr, dt, pos2), this)
47
34
  );
48
35
  this.region = new Radial(distance, pos, 1);
49
36
  }
50
- pos;
51
- vel;
52
- opts;
53
- region;
54
- cachedNeighbors;
55
- tmpSep = [];
56
- tmpAlign = [];
57
- tmpCoh = [];
58
37
  /**
59
38
  * Integration step of the thi.ng/timestep update cycle. See
60
39
  * [`ITimeStep`](https://docs.thi.ng/umbrella/timestep/interfaces/ITimeStep.html)
@@ -63,10 +42,6 @@ class Boid {
63
42
  * @param ctx
64
43
  */
65
44
  integrate(dt, ctx) {
66
- this.cachedNeighbors = this.neighbors(
67
- this.opts.maxDist,
68
- this.pos.value
69
- );
70
45
  integrateAll(dt, ctx, this.vel, this.pos);
71
46
  }
72
47
  /**
@@ -99,49 +74,6 @@ class Boid {
99
74
  region.setRadius(r);
100
75
  return this.accel.queryNeighborhood(region).deref();
101
76
  }
102
- separate() {
103
- const { maddN, magSq, setN, sub } = this.api;
104
- const pos = this.pos.value;
105
- const neighbors = this.neighbors(this.opts.minDist);
106
- const num = neighbors.length;
107
- const delta = [];
108
- const steer = setN(this.tmpSep, 0);
109
- let n;
110
- for (let i = 0; i < num; i++) {
111
- n = neighbors[i];
112
- if (n !== this) {
113
- sub(delta, pos, n.pos.curr);
114
- maddN(steer, delta, 1 / (magSq(delta) + 1e-6), steer);
115
- }
116
- }
117
- return this.computeSteer(steer, num);
118
- }
119
- align() {
120
- const { add, setN } = this.api;
121
- const neighbors = this.cachedNeighbors;
122
- const num = neighbors.length;
123
- const sum = setN(this.tmpAlign, 0);
124
- let n;
125
- for (let i = 0; i < num; i++) {
126
- n = neighbors[i];
127
- if (n !== this)
128
- add(sum, sum, n.vel.curr);
129
- }
130
- return this.computeSteer(sum, num);
131
- }
132
- cohesion() {
133
- const { add, mulN, setN } = this.api;
134
- const neighbors = this.cachedNeighbors;
135
- const num = neighbors.length;
136
- const sum = setN(this.tmpCoh, 0);
137
- let n;
138
- for (let i = 0; i < num; i++) {
139
- n = neighbors[i];
140
- if (n !== this)
141
- add(sum, sum, n.pos.curr);
142
- }
143
- return num > 0 ? this.steerTowards(mulN(sum, sum, 1 / num)) : sum;
144
- }
145
77
  steerTowards(target, out = target) {
146
78
  return this.limitSteer(this.api.sub(out, target, this.pos.curr));
147
79
  }
@@ -158,15 +90,15 @@ class Boid {
158
90
  msubN(
159
91
  steer,
160
92
  steer,
161
- this.opts.maxSpeed ** 2 / m,
93
+ this.opts.maxSpeed / Math.sqrt(m),
162
94
  this.vel.curr
163
95
  ),
164
96
  this.opts.maxForce
165
97
  ) : steer;
166
98
  }
167
99
  }
168
- const defBoid2 = (accel, pos, vel, opts) => new Boid(accel, VEC2, DIST_SQ2, pos, vel, opts);
169
- const defBoid3 = (accel, pos, vel, opts) => new Boid(accel, VEC3, DIST_SQ3, pos, vel, opts);
100
+ const defBoid2 = (pos, vel, opts) => new Boid(opts, VEC2, DIST_SQ2, pos, vel);
101
+ const defBoid3 = (pos, vel, opts) => new Boid(opts, VEC3, DIST_SQ3, pos, vel);
170
102
  export {
171
103
  Boid,
172
104
  defBoid2,
package/constrain.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { ReadonlyVec } from "@thi.ng/vectors";
2
+ import type { GlobalConstraint } from "./api.js";
3
+ export declare const clamp2: (min: ReadonlyVec, max: ReadonlyVec) => GlobalConstraint;
4
+ export declare const clamp3: (min: ReadonlyVec, max: ReadonlyVec) => GlobalConstraint;
5
+ export declare const wrap2: (min: ReadonlyVec, max: ReadonlyVec) => GlobalConstraint;
6
+ export declare const wrap3: (min: ReadonlyVec, max: ReadonlyVec) => GlobalConstraint;
7
+ //# sourceMappingURL=constrain.d.ts.map
package/constrain.js ADDED
@@ -0,0 +1,44 @@
1
+ import { wrapOnce } from "@thi.ng/math/interval";
2
+ import { clamp2 as $clamp2, clamp3 as $clamp3 } from "@thi.ng/vectors/clamp";
3
+ const clamp2 = (min, max) => (p) => $clamp2(p, p, min, max);
4
+ const clamp3 = (min, max) => (p) => $clamp3(p, p, min, max);
5
+ const wrap2 = (min, max) => (p, boid) => {
6
+ const [x, y] = p;
7
+ let wrap = false;
8
+ if (x < min[0] || x > max[0]) {
9
+ p[0] = wrapOnce(x, min[0], max[0]);
10
+ wrap = true;
11
+ }
12
+ if (y < min[1] || y > max[1]) {
13
+ p[1] = wrapOnce(y, min[1], max[1]);
14
+ wrap = true;
15
+ }
16
+ if (wrap)
17
+ boid.pos.reset(p);
18
+ return p;
19
+ };
20
+ const wrap3 = (min, max) => (p, boid) => {
21
+ let [x, y, z] = p;
22
+ let wrap = false;
23
+ if (x < min[0] || x > max[0]) {
24
+ p[0] = wrapOnce(x, min[0], max[0]);
25
+ wrap = true;
26
+ }
27
+ if (y < min[1] || y > max[1]) {
28
+ p[1] = wrapOnce(y, min[1], max[1]);
29
+ wrap = true;
30
+ }
31
+ if (z < min[2] || z > max[2]) {
32
+ p[2] = wrapOnce(z, min[2], max[2]);
33
+ wrap = true;
34
+ }
35
+ if (wrap)
36
+ boid.pos.reset(p);
37
+ return p;
38
+ };
39
+ export {
40
+ clamp2,
41
+ clamp3,
42
+ wrap2,
43
+ wrap3
44
+ };
package/flock.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { ITimeStep, ReadonlyTimeStep } from "@thi.ng/timestep";
2
+ import type { IBoidAccel } from "./api.js";
3
+ import type { Boid } from "./boid.js";
4
+ /**
5
+ * Returns a new {@link Flock} instance.
6
+ *
7
+ * @param accel
8
+ * @param boids
9
+ */
10
+ export declare const defFlock: (accel: IBoidAccel, boids?: Boid[]) => Flock;
11
+ /**
12
+ * Convenience class for managing a number of boids (can be added
13
+ * dynamically) and their {@link IBoidAccel} structure. Like {@link Boid}
14
+ * itself, this class implements the `ITimeStep` interface too and the
15
+ * {@link ITimeStep.integrate} phase will automatically update the
16
+ * acceleration structure before integrating the boids.
17
+ */
18
+ export declare class Flock implements ITimeStep {
19
+ accel: IBoidAccel;
20
+ boids: Boid[];
21
+ constructor(accel: IBoidAccel, boids?: Boid[]);
22
+ add(boid: Boid): void;
23
+ remove(boid: Boid): void;
24
+ integrate(dt: number, ctx: ReadonlyTimeStep): void;
25
+ interpolate(alpha: number, ctx: ReadonlyTimeStep): void;
26
+ }
27
+ //# sourceMappingURL=flock.d.ts.map
package/flock.js ADDED
@@ -0,0 +1,27 @@
1
+ import { integrateAll, interpolateAll } from "@thi.ng/timestep/timestep";
2
+ const defFlock = (accel, boids) => new Flock(accel, boids);
3
+ class Flock {
4
+ constructor(accel, boids = []) {
5
+ this.accel = accel;
6
+ this.boids = boids;
7
+ }
8
+ add(boid) {
9
+ this.boids.push(boid);
10
+ }
11
+ remove(boid) {
12
+ const idx = this.boids.indexOf(boid);
13
+ if (idx >= 0)
14
+ this.boids.splice(idx, 1);
15
+ }
16
+ integrate(dt, ctx) {
17
+ this.accel.build(this.boids);
18
+ integrateAll(dt, ctx, ...this.boids);
19
+ }
20
+ interpolate(alpha, ctx) {
21
+ interpolateAll(alpha, ctx, ...this.boids);
22
+ }
23
+ }
24
+ export {
25
+ Flock,
26
+ defFlock
27
+ };
package/index.d.ts CHANGED
@@ -1,4 +1,15 @@
1
+ export * from "./accel.js";
1
2
  export * from "./api.js";
2
3
  export * from "./boid.js";
4
+ export * from "./constrain.js";
5
+ export * from "./flock.js";
3
6
  export * from "./region.js";
7
+ export * from "./behaviors/alignment.js";
8
+ export * from "./behaviors/attraction.js";
9
+ export * from "./behaviors/braitenberg.js";
10
+ export * from "./behaviors/cohesion.js";
11
+ export * from "./behaviors/dynamic.js";
12
+ export * from "./behaviors/follow.js";
13
+ export * from "./behaviors/separation.js";
14
+ export * from "./behaviors/update.js";
4
15
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,3 +1,14 @@
1
+ export * from "./accel.js";
1
2
  export * from "./api.js";
2
3
  export * from "./boid.js";
4
+ export * from "./constrain.js";
5
+ export * from "./flock.js";
3
6
  export * from "./region.js";
7
+ export * from "./behaviors/alignment.js";
8
+ export * from "./behaviors/attraction.js";
9
+ export * from "./behaviors/braitenberg.js";
10
+ export * from "./behaviors/cohesion.js";
11
+ export * from "./behaviors/dynamic.js";
12
+ export * from "./behaviors/follow.js";
13
+ export * from "./behaviors/separation.js";
14
+ export * from "./behaviors/update.js";
@@ -0,0 +1,3 @@
1
+ import type { ScalarOrField } from "../api.js";
2
+ export declare const __ensureFn: (x: ScalarOrField) => import("@thi.ng/api").Fn<import("../boid.js").Boid, number>;
3
+ //# sourceMappingURL=ensure.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { isNumber } from "@thi.ng/checks/is-number";
2
+ const __ensureFn = (x) => isNumber(x) ? () => x : x;
3
+ export {
4
+ __ensureFn
5
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thi.ng/boids",
3
- "version": "0.1.15",
4
- "description": "n-dimensional boids simulation with highly configurable behaviors",
3
+ "version": "1.0.0",
4
+ "description": "n-dimensional boids simulation with modular behavior system",
5
5
  "type": "module",
6
6
  "module": "./index.js",
7
7
  "typings": "./index.d.ts",
@@ -35,11 +35,14 @@
35
35
  "test": "bun test"
36
36
  },
37
37
  "dependencies": {
38
- "@thi.ng/api": "^8.9.16",
39
- "@thi.ng/distance": "^2.4.40",
40
- "@thi.ng/geom-accel": "^3.5.40",
41
- "@thi.ng/timestep": "^0.5.15",
42
- "@thi.ng/vectors": "^7.8.15"
38
+ "@thi.ng/api": "^8.9.17",
39
+ "@thi.ng/checks": "^3.4.17",
40
+ "@thi.ng/distance": "^2.4.41",
41
+ "@thi.ng/geom-closest-point": "^2.1.95",
42
+ "@thi.ng/geom-resample": "^2.3.21",
43
+ "@thi.ng/math": "^5.7.12",
44
+ "@thi.ng/timestep": "^0.5.16",
45
+ "@thi.ng/vectors": "^7.9.0"
43
46
  },
44
47
  "devDependencies": {
45
48
  "@microsoft/api-extractor": "^7.39.0",
@@ -52,12 +55,26 @@
52
55
  "nd",
53
56
  "2d",
54
57
  "3d",
58
+ "acceleration",
59
+ "align",
60
+ "attractor",
55
61
  "behavior",
62
+ "braitenberg",
56
63
  "boids",
64
+ "constraint",
57
65
  "flocking",
66
+ "follow",
67
+ "force",
68
+ "modular",
69
+ "path",
70
+ "physics",
71
+ "polyline",
72
+ "polygon",
58
73
  "points",
74
+ "separation",
59
75
  "simulation",
60
76
  "spatial",
77
+ "steering",
61
78
  "time",
62
79
  "typescript",
63
80
  "vector"
@@ -74,22 +91,57 @@
74
91
  },
75
92
  "files": [
76
93
  "./*.js",
77
- "./*.d.ts"
94
+ "./*.d.ts",
95
+ "behaviors",
96
+ "internal"
78
97
  ],
79
98
  "exports": {
80
99
  ".": {
81
100
  "default": "./index.js"
82
101
  },
102
+ "./accel": {
103
+ "default": "./accel.js"
104
+ },
83
105
  "./api": {
84
106
  "default": "./api.js"
85
107
  },
108
+ "./behaviors/alignment": {
109
+ "default": "./behaviors/alignment.js"
110
+ },
111
+ "./behaviors/attraction": {
112
+ "default": "./behaviors/attraction.js"
113
+ },
114
+ "./behaviors/braitenberg": {
115
+ "default": "./behaviors/braitenberg.js"
116
+ },
117
+ "./behaviors/cohesion": {
118
+ "default": "./behaviors/cohesion.js"
119
+ },
120
+ "./behaviors/dynamic": {
121
+ "default": "./behaviors/dynamic.js"
122
+ },
123
+ "./behaviors/follow": {
124
+ "default": "./behaviors/follow.js"
125
+ },
126
+ "./behaviors/separation": {
127
+ "default": "./behaviors/separation.js"
128
+ },
129
+ "./behaviors/update": {
130
+ "default": "./behaviors/update.js"
131
+ },
86
132
  "./boid": {
87
133
  "default": "./boid.js"
134
+ },
135
+ "./constrain": {
136
+ "default": "./constrain.js"
137
+ },
138
+ "./flock": {
139
+ "default": "./flock.js"
88
140
  }
89
141
  },
90
142
  "thi.ng": {
91
143
  "status": "alpha",
92
144
  "year": 2023
93
145
  },
94
- "gitHead": "b3db173682e1148cf08a6bd907b8d90b47b7c066\n"
146
+ "gitHead": "417b5a7ea7bd54a3b4f086fe0fc2ce8e8933c9b2\n"
95
147
  }