bireactive 0.2.1 → 0.2.3
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/README.md +3 -3
- package/dist/coll.d.ts +74 -0
- package/dist/coll.js +208 -0
- package/dist/core/values/num.d.ts +6 -0
- package/dist/core/values/num.js +20 -0
- package/dist/core/values/tri.d.ts +9 -7
- package/dist/core/values/tri.js +11 -11
- package/dist/core/values/vec.d.ts +8 -1
- package/dist/core/values/vec.js +26 -16
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# bi-reactive
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
[npm](https://www.npmjs.com/package/bireactive) · [GitHub](https://github.com/OrionReed/bireactive) · [site](https://orionreed.github.io/bireactive/) · [API](https://orionreed.github.io/bireactive/api/)
|
|
4
|
+
|
|
5
|
+
A signals-like bidirectional reactive programming system where edges can go both ways. Forward and backward propagation are handled by the engine, with the same set of caveats as regular reactive programming.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
package/dist/coll.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type Cell, type Read, type Writable } from "./core/index.js";
|
|
2
|
+
/** Accessor for an element's writable field cell. Forward reads `.value`;
|
|
3
|
+
* the backward pass writes it. */
|
|
4
|
+
export type Field<E, V> = (e: E) => Writable<Cell<V>>;
|
|
5
|
+
/** A forward test over an element's fields, optionally assertable —
|
|
6
|
+
* `assert(e)` makes the test pass by writing fields. */
|
|
7
|
+
export interface FieldPred<E> {
|
|
8
|
+
(e: E): boolean;
|
|
9
|
+
assert?: (e: E) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface Group<K, E> {
|
|
12
|
+
key: K;
|
|
13
|
+
items: readonly E[];
|
|
14
|
+
}
|
|
15
|
+
export interface GroupOpts<E, K> {
|
|
16
|
+
/** Fixed key order; seeds empty buckets and pins column order. */
|
|
17
|
+
order?: readonly K[];
|
|
18
|
+
/** Order field within each group; enables `move(e, key, index)`. */
|
|
19
|
+
sort?: Field<E, number>;
|
|
20
|
+
}
|
|
21
|
+
/** `field === value`, assertable by writing the field. */
|
|
22
|
+
export declare function is<E, V>(field: Field<E, V>, value: V): FieldPred<E>;
|
|
23
|
+
/** Conjunction; asserts every clause. */
|
|
24
|
+
export declare function allPass<E>(...preds: readonly FieldPred<E>[]): FieldPred<E>;
|
|
25
|
+
/** Read-only projection with chainable structural lenses. */
|
|
26
|
+
export declare class View<E> {
|
|
27
|
+
protected readonly list: Read<readonly E[]>;
|
|
28
|
+
readonly key: (e: E) => unknown;
|
|
29
|
+
protected readonly parent: View<E> | null;
|
|
30
|
+
protected constructor(list: Read<readonly E[]>, key: (e: E) => unknown, parent: View<E> | null);
|
|
31
|
+
/** Current members; tracked when read in an effect/derive. */
|
|
32
|
+
get items(): readonly E[];
|
|
33
|
+
/** The source collection at the head of the chain. */
|
|
34
|
+
get root(): Coll<E>;
|
|
35
|
+
/** Make `e` appear in this view: satisfy own constraint, recursively up. */
|
|
36
|
+
assertContains(e: E): void;
|
|
37
|
+
protected assertSelf(_e: E): void;
|
|
38
|
+
filter(pred: FieldPred<E>): View<E>;
|
|
39
|
+
sortBy(field: Field<E, number>): SortView<E>;
|
|
40
|
+
groupBy<K>(field: Field<E, K>, opts?: GroupOpts<E, K>): GroupView<K, E>;
|
|
41
|
+
map<F>(f: (e: E) => F): Read<readonly F[]>;
|
|
42
|
+
/** Remove `e` from the source. */
|
|
43
|
+
remove(e: E): void;
|
|
44
|
+
}
|
|
45
|
+
/** A writable source collection. */
|
|
46
|
+
export declare class Coll<E> extends View<E> {
|
|
47
|
+
#private;
|
|
48
|
+
constructor(items: readonly E[], key: (e: E) => unknown);
|
|
49
|
+
assertContains(e: E): void;
|
|
50
|
+
insert(e: E, at?: number): void;
|
|
51
|
+
removeFromSource(e: E): void;
|
|
52
|
+
}
|
|
53
|
+
/** Sorted view. `move` writes the order field between the drop neighbours. */
|
|
54
|
+
export declare class SortView<E> extends View<E> {
|
|
55
|
+
#private;
|
|
56
|
+
constructor(parent: View<E>, field: Field<E, number>);
|
|
57
|
+
move(e: E, to: number): void;
|
|
58
|
+
}
|
|
59
|
+
/** Grouped view. `move`/`insert` write the group field (and, with a `sort`
|
|
60
|
+
* field, the order field). Backward composition runs the parent chain. */
|
|
61
|
+
export declare class GroupView<K, E> {
|
|
62
|
+
#private;
|
|
63
|
+
readonly groups: Read<readonly Group<K, E>[]>;
|
|
64
|
+
constructor(parent: View<E>, field: Field<E, K>, opts?: GroupOpts<E, K>);
|
|
65
|
+
get value(): readonly Group<K, E>[];
|
|
66
|
+
map<F>(f: (g: Group<K, E>) => F): Read<readonly F[]>;
|
|
67
|
+
/** Place `e` in group `toKey` at `index`. Inserts it into the source if
|
|
68
|
+
* it isn't there yet, asserts every upstream filter, then writes the
|
|
69
|
+
* group key and order field — all in one batch. */
|
|
70
|
+
move(e: E, toKey: K, index?: number): void;
|
|
71
|
+
insert(e: E, toKey: K, index?: number): void;
|
|
72
|
+
remove(e: E): void;
|
|
73
|
+
}
|
|
74
|
+
export declare function coll<E>(items: readonly E[], key: (e: E) => unknown): Coll<E>;
|
package/dist/coll.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// coll.ts — a keyed, ordered, *bidirectional* collection.
|
|
2
|
+
//
|
|
3
|
+
// The structural sibling of `tree.ts`. A `Coll<E>` holds stable element
|
|
4
|
+
// handles (records of cells), and each projection — `filter` / `sortBy` /
|
|
5
|
+
// `groupBy` — is a WRITABLE structural lens. You edit the *view* and the
|
|
6
|
+
// backward pass writes the elements' own fields:
|
|
7
|
+
//
|
|
8
|
+
// sortBy(rank) → move(e, i) writes `rank` (position is a field)
|
|
9
|
+
// groupBy(key) → move(e, k, i) writes `key` (membership is a field)
|
|
10
|
+
// filter(pred) → insert/assert writes whatever pred demands
|
|
11
|
+
//
|
|
12
|
+
// Because position / group / visibility are the elements' own writable
|
|
13
|
+
// cells, these lenses are COMPLEMENT-FREE — the "complement" lives in the
|
|
14
|
+
// field cells, not the lens (contrast `Seq<T>`, where order is imposed and
|
|
15
|
+
// must be remembered). One `move`/`insert` composes the backward passes up
|
|
16
|
+
// the chain in a single `batch`: drop into a filtered + grouped + sorted
|
|
17
|
+
// view and the filter's predicate, the group key, and the rank all get set
|
|
18
|
+
// at once.
|
|
19
|
+
//
|
|
20
|
+
// Two change classes, mirroring the engine's two levels:
|
|
21
|
+
// • value change — a field cell changes; forward derivations re-flow.
|
|
22
|
+
// • structural edit — insert/remove/move; a discrete batched transition.
|
|
23
|
+
// No lens edges are built at runtime, so acyclicity holds as ever.
|
|
24
|
+
import { batch, cell, derive } from "./core/index.js";
|
|
25
|
+
/** `field === value`, assertable by writing the field. */
|
|
26
|
+
export function is(field, value) {
|
|
27
|
+
const p = ((e) => field(e).value === value);
|
|
28
|
+
p.assert = (e) => {
|
|
29
|
+
field(e).value = value;
|
|
30
|
+
};
|
|
31
|
+
return p;
|
|
32
|
+
}
|
|
33
|
+
/** Conjunction; asserts every clause. */
|
|
34
|
+
export function allPass(...preds) {
|
|
35
|
+
const p = ((e) => preds.every(q => q(e)));
|
|
36
|
+
p.assert = (e) => {
|
|
37
|
+
for (const q of preds)
|
|
38
|
+
q.assert?.(e);
|
|
39
|
+
};
|
|
40
|
+
return p;
|
|
41
|
+
}
|
|
42
|
+
/** Read-only projection with chainable structural lenses. */
|
|
43
|
+
export class View {
|
|
44
|
+
list;
|
|
45
|
+
key;
|
|
46
|
+
parent;
|
|
47
|
+
constructor(list, key, parent) {
|
|
48
|
+
this.list = list;
|
|
49
|
+
this.key = key;
|
|
50
|
+
this.parent = parent;
|
|
51
|
+
}
|
|
52
|
+
/** Current members; tracked when read in an effect/derive. */
|
|
53
|
+
get items() {
|
|
54
|
+
return this.list.value;
|
|
55
|
+
}
|
|
56
|
+
/** The source collection at the head of the chain. */
|
|
57
|
+
get root() {
|
|
58
|
+
let v = this;
|
|
59
|
+
while (v.parent)
|
|
60
|
+
v = v.parent;
|
|
61
|
+
return v;
|
|
62
|
+
}
|
|
63
|
+
/** Make `e` appear in this view: satisfy own constraint, recursively up. */
|
|
64
|
+
assertContains(e) {
|
|
65
|
+
this.parent?.assertContains(e);
|
|
66
|
+
this.assertSelf(e);
|
|
67
|
+
}
|
|
68
|
+
assertSelf(_e) { }
|
|
69
|
+
filter(pred) {
|
|
70
|
+
return new FilterView(this, pred);
|
|
71
|
+
}
|
|
72
|
+
sortBy(field) {
|
|
73
|
+
return new SortView(this, field);
|
|
74
|
+
}
|
|
75
|
+
groupBy(field, opts) {
|
|
76
|
+
return new GroupView(this, field, opts);
|
|
77
|
+
}
|
|
78
|
+
map(f) {
|
|
79
|
+
return derive(() => this.list.value.map(f));
|
|
80
|
+
}
|
|
81
|
+
/** Remove `e` from the source. */
|
|
82
|
+
remove(e) {
|
|
83
|
+
this.root.removeFromSource(e);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** A writable source collection. */
|
|
87
|
+
export class Coll extends View {
|
|
88
|
+
#src;
|
|
89
|
+
constructor(items, key) {
|
|
90
|
+
const src = cell(items);
|
|
91
|
+
super(src, key, null);
|
|
92
|
+
this.#src = src;
|
|
93
|
+
}
|
|
94
|
+
assertContains(e) {
|
|
95
|
+
if (!this.#src.value.includes(e))
|
|
96
|
+
this.insert(e);
|
|
97
|
+
}
|
|
98
|
+
insert(e, at) {
|
|
99
|
+
const arr = [...this.#src.value];
|
|
100
|
+
if (at == null)
|
|
101
|
+
arr.push(e);
|
|
102
|
+
else
|
|
103
|
+
arr.splice(at, 0, e);
|
|
104
|
+
this.#src.value = arr;
|
|
105
|
+
}
|
|
106
|
+
removeFromSource(e) {
|
|
107
|
+
this.#src.value = this.#src.value.filter(x => x !== e);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
class FilterView extends View {
|
|
111
|
+
#pred;
|
|
112
|
+
constructor(parent, pred) {
|
|
113
|
+
super(derive(() => parent.items.filter(pred)), parent.key, parent);
|
|
114
|
+
this.#pred = pred;
|
|
115
|
+
}
|
|
116
|
+
assertSelf(e) {
|
|
117
|
+
this.#pred.assert?.(e);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Sorted view. `move` writes the order field between the drop neighbours. */
|
|
121
|
+
export class SortView extends View {
|
|
122
|
+
#field;
|
|
123
|
+
constructor(parent, field) {
|
|
124
|
+
super(derive(() => [...parent.items].sort((a, b) => field(a).value - field(b).value)), parent.key, parent);
|
|
125
|
+
this.#field = field;
|
|
126
|
+
}
|
|
127
|
+
move(e, to) {
|
|
128
|
+
const others = this.items.filter(x => x !== e);
|
|
129
|
+
batch(() => {
|
|
130
|
+
this.assertContains(e);
|
|
131
|
+
this.#field(e).value = between(rankAt(others, this.#field, to - 1), rankAt(others, this.#field, to));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Grouped view. `move`/`insert` write the group field (and, with a `sort`
|
|
136
|
+
* field, the order field). Backward composition runs the parent chain. */
|
|
137
|
+
export class GroupView {
|
|
138
|
+
groups;
|
|
139
|
+
#parent;
|
|
140
|
+
#field;
|
|
141
|
+
#order;
|
|
142
|
+
#sort;
|
|
143
|
+
constructor(parent, field, opts = {}) {
|
|
144
|
+
this.#parent = parent;
|
|
145
|
+
this.#field = field;
|
|
146
|
+
this.#order = opts.order ?? [];
|
|
147
|
+
this.#sort = opts.sort;
|
|
148
|
+
this.groups = derive(() => {
|
|
149
|
+
const sort = this.#sort;
|
|
150
|
+
const items = sort ? [...parent.items].sort((a, b) => sort(a).value - sort(b).value) : parent.items;
|
|
151
|
+
return groupItems(items, e => field(e).value, this.#order);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
get value() {
|
|
155
|
+
return this.groups.value;
|
|
156
|
+
}
|
|
157
|
+
map(f) {
|
|
158
|
+
return derive(() => this.value.map(f));
|
|
159
|
+
}
|
|
160
|
+
/** Place `e` in group `toKey` at `index`. Inserts it into the source if
|
|
161
|
+
* it isn't there yet, asserts every upstream filter, then writes the
|
|
162
|
+
* group key and order field — all in one batch. */
|
|
163
|
+
move(e, toKey, index) {
|
|
164
|
+
const target = (this.value.find(g => Object.is(g.key, toKey))?.items ?? []).filter(x => x !== e);
|
|
165
|
+
const sort = this.#sort;
|
|
166
|
+
batch(() => {
|
|
167
|
+
this.#parent.assertContains(e);
|
|
168
|
+
this.#field(e).value = toKey;
|
|
169
|
+
if (sort && index != null)
|
|
170
|
+
sort(e).value = between(rankAt(target, sort, index - 1), rankAt(target, sort, index));
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
insert(e, toKey, index) {
|
|
174
|
+
this.move(e, toKey, index);
|
|
175
|
+
}
|
|
176
|
+
remove(e) {
|
|
177
|
+
this.#parent.remove(e);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function groupItems(items, keyOf, order) {
|
|
181
|
+
const buckets = new Map();
|
|
182
|
+
for (const k of order)
|
|
183
|
+
buckets.set(k, []);
|
|
184
|
+
for (const e of items) {
|
|
185
|
+
const k = keyOf(e);
|
|
186
|
+
const arr = buckets.get(k);
|
|
187
|
+
if (arr)
|
|
188
|
+
arr.push(e);
|
|
189
|
+
else
|
|
190
|
+
buckets.set(k, [e]);
|
|
191
|
+
}
|
|
192
|
+
return [...buckets].map(([key, items]) => ({ key, items }));
|
|
193
|
+
}
|
|
194
|
+
function rankAt(arr, field, i) {
|
|
195
|
+
return i >= 0 && i < arr.length ? field(arr[i]).value : undefined;
|
|
196
|
+
}
|
|
197
|
+
function between(a, b) {
|
|
198
|
+
if (a != null && b != null)
|
|
199
|
+
return (a + b) / 2;
|
|
200
|
+
if (a != null)
|
|
201
|
+
return a + 1;
|
|
202
|
+
if (b != null)
|
|
203
|
+
return b - 1;
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
export function coll(items, key) {
|
|
207
|
+
return new Coll(items, key);
|
|
208
|
+
}
|
|
@@ -25,6 +25,12 @@ export declare class Num extends Cell<V> {
|
|
|
25
25
|
/** Affine `v ↦ k·v + off`. Invertible iff k ≠ 0; readability alias
|
|
26
26
|
* for `.scale(k).add(off)`. */
|
|
27
27
|
affine(k: Val<number>, off: Val<number>): this;
|
|
28
|
+
/** `sin(this)` (radians). Forward lands in [−1, 1]; the inverse is
|
|
29
|
+
* multi-valued, so a write clamps to that domain and returns the
|
|
30
|
+
* pre-image nearest the current source — the drag stays on its branch. */
|
|
31
|
+
sin(): this;
|
|
32
|
+
/** `exp(this)` — bijection on the reals; inverse is the natural log. */
|
|
33
|
+
exp(): this;
|
|
28
34
|
/** Lossy clamping lens to `[lo, hi]`. PutGet only (a write outside
|
|
29
35
|
* the range reads back clamped, not as written). */
|
|
30
36
|
clamp(lo: Val<V>, hi: Val<V>): this;
|
package/dist/core/values/num.js
CHANGED
|
@@ -11,6 +11,11 @@ export const scale = (a, k) => a * k;
|
|
|
11
11
|
export const lerp = (a, b, t) => a + (b - a) * t;
|
|
12
12
|
export const metric = (a, b) => Math.abs(a - b);
|
|
13
13
|
export const equals = (a, b) => a === b;
|
|
14
|
+
const TAU = 2 * Math.PI;
|
|
15
|
+
/** Representative of `x + 2πk` nearest `s` (shortest-arc branch pick). */
|
|
16
|
+
const nearestTo = (s, x) => x + TAU * Math.round((s - x) / TAU);
|
|
17
|
+
/** Clamp to the sin/cos domain `[-1, 1]`. */
|
|
18
|
+
const unit = (t) => (t < -1 ? -1 : t > 1 ? 1 : t);
|
|
14
19
|
const linearImpl = { add, sub, scale };
|
|
15
20
|
const packImpl = {
|
|
16
21
|
dim: 1,
|
|
@@ -49,6 +54,21 @@ export class Num extends Cell {
|
|
|
49
54
|
const of = reader(off);
|
|
50
55
|
return this.lens(v => v * kf() + of(), n => (n - of()) / kf());
|
|
51
56
|
}
|
|
57
|
+
/** `sin(this)` (radians). Forward lands in [−1, 1]; the inverse is
|
|
58
|
+
* multi-valued, so a write clamps to that domain and returns the
|
|
59
|
+
* pre-image nearest the current source — the drag stays on its branch. */
|
|
60
|
+
sin() {
|
|
61
|
+
return this.lens(v => Math.sin(v), (target, s) => {
|
|
62
|
+
const p = Math.asin(unit(target));
|
|
63
|
+
const a = nearestTo(s, p);
|
|
64
|
+
const b = nearestTo(s, Math.PI - p);
|
|
65
|
+
return Math.abs(a - s) <= Math.abs(b - s) ? a : b;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** `exp(this)` — bijection on the reals; inverse is the natural log. */
|
|
69
|
+
exp() {
|
|
70
|
+
return this.lens(v => Math.exp(v), n => Math.log(n));
|
|
71
|
+
}
|
|
52
72
|
/** Lossy clamping lens to `[lo, hi]`. PutGet only (a write outside
|
|
53
73
|
* the range reads back clamped, not as written). */
|
|
54
74
|
clamp(lo, hi) {
|
|
@@ -17,13 +17,15 @@ export declare class Tri extends Cell<V> {
|
|
|
17
17
|
constructor(v?: V);
|
|
18
18
|
/** Kleene negation. Involution; fixed at `"mixed"`. */
|
|
19
19
|
not(): this;
|
|
20
|
-
/** Aggregate over N writable
|
|
21
|
-
* all-false → `false`, disagreement
|
|
22
|
-
* `
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
/** Aggregate over N writable `Bool` / `Tri` children. Read: all-true →
|
|
21
|
+
* `true`, all-false → `false`, any disagreement (or any child already
|
|
22
|
+
* `"mixed"`) → `"mixed"`. Write: `true` / `false` broadcast to every
|
|
23
|
+
* child, recursing through nested aggregates; `"mixed"` is a no-op. */
|
|
24
|
+
static allOf(parents: readonly (Bool | Tri)[]): Writable<Tri>;
|
|
25
|
+
/** Dual of `allOf` (Kleene OR) over `Bool` / `Tri` children: any-true →
|
|
26
|
+
* `true`, all-false → `false`, else (or any child `"mixed"`) →
|
|
27
|
+
* `"mixed"`. Same broadcast write policy. */
|
|
28
|
+
static anyOf(parents: readonly (Bool | Tri)[]): Writable<Tri>;
|
|
27
29
|
}
|
|
28
30
|
/** Writable `Tri`. Strict factory: `Tri.value | Writable<Tri>` in,
|
|
29
31
|
* `Writable<Tri>` out. Default initial value is `"mixed"`. */
|
package/dist/core/values/tri.js
CHANGED
|
@@ -3,12 +3,6 @@
|
|
|
3
3
|
// `Tri.value ∈ { true, false, "mixed" }` — Bool plus an unknown state
|
|
4
4
|
// fixed under negation. Strong-Kleene AND/OR follow the partial-info
|
|
5
5
|
// reading (`mixed AND false` → `false`, `mixed AND true` → `mixed`).
|
|
6
|
-
//
|
|
7
|
-
// Headline use: aggregate N booleans via `Tri.allOf` / `Tri.anyOf`
|
|
8
|
-
// (all-agree → that value, disagreement → `"mixed"`). Writing the
|
|
9
|
-
// aggregate broadcasts to every parent ("select all" / "deselect all");
|
|
10
|
-
// writing `"mixed"` is a no-op. Morally `Maybe<Bool>` — the basis for
|
|
11
|
-
// mixed-state checkbox trees and "loading" predicate states.
|
|
12
6
|
import { Cell } from "../cell.js";
|
|
13
7
|
const equals = (a, b) => a === b;
|
|
14
8
|
/** Kleene negation: `true` / `false` swap, `"mixed"` is fixed. */
|
|
@@ -40,14 +34,17 @@ export class Tri extends Cell {
|
|
|
40
34
|
not() {
|
|
41
35
|
return this.lens(not, not);
|
|
42
36
|
}
|
|
43
|
-
/** Aggregate over N writable
|
|
44
|
-
* all-false → `false`, disagreement
|
|
45
|
-
* `
|
|
37
|
+
/** Aggregate over N writable `Bool` / `Tri` children. Read: all-true →
|
|
38
|
+
* `true`, all-false → `false`, any disagreement (or any child already
|
|
39
|
+
* `"mixed"`) → `"mixed"`. Write: `true` / `false` broadcast to every
|
|
40
|
+
* child, recursing through nested aggregates; `"mixed"` is a no-op. */
|
|
46
41
|
static allOf(parents) {
|
|
47
42
|
return Tri.lens(parents, (vs) => {
|
|
48
43
|
let anyT = false;
|
|
49
44
|
let anyF = false;
|
|
50
45
|
for (const v of vs) {
|
|
46
|
+
if (v === "mixed")
|
|
47
|
+
return "mixed";
|
|
51
48
|
if (v)
|
|
52
49
|
anyT = true;
|
|
53
50
|
else
|
|
@@ -62,13 +59,16 @@ export class Tri extends Cell {
|
|
|
62
59
|
return parents.map(() => target);
|
|
63
60
|
});
|
|
64
61
|
}
|
|
65
|
-
/** Dual of `allOf` (Kleene OR)
|
|
66
|
-
* `false`, else `"mixed"
|
|
62
|
+
/** Dual of `allOf` (Kleene OR) over `Bool` / `Tri` children: any-true →
|
|
63
|
+
* `true`, all-false → `false`, else (or any child `"mixed"`) →
|
|
64
|
+
* `"mixed"`. Same broadcast write policy. */
|
|
67
65
|
static anyOf(parents) {
|
|
68
66
|
return Tri.lens(parents, (vs) => {
|
|
69
67
|
let anyT = false;
|
|
70
68
|
let anyF = false;
|
|
71
69
|
for (const v of vs) {
|
|
70
|
+
if (v === "mixed")
|
|
71
|
+
return "mixed";
|
|
72
72
|
if (v)
|
|
73
73
|
anyT = true;
|
|
74
74
|
else
|
|
@@ -14,6 +14,8 @@ export declare const metric: (a: V, b: V) => number;
|
|
|
14
14
|
export declare const equals: (a: V, b: V) => boolean;
|
|
15
15
|
export declare const normalize: (v: V) => V;
|
|
16
16
|
export declare const perp: (v: V) => V;
|
|
17
|
+
export declare const rotateAbout: (v: V, p: V, dθ: number) => V;
|
|
18
|
+
export declare const scaleAbout: (v: V, p: V, k: number) => V;
|
|
17
19
|
/** Tangent point on the circle (radius `r`, centre `c`) from external
|
|
18
20
|
* point `p`. `side: -1` picks the CCW tangent from `pc`, `+1` the CW
|
|
19
21
|
* (y-down screen coords flip the visual sense). Returns `c` if `p` is
|
|
@@ -32,7 +34,12 @@ export declare class Vec extends Cell<V> {
|
|
|
32
34
|
constructor(v?: V);
|
|
33
35
|
add(b: Val<V>): this;
|
|
34
36
|
sub(b: Val<V>): this;
|
|
35
|
-
scale
|
|
37
|
+
/** Uniform scale by `k` about `pivot` (default origin). Inverse scales
|
|
38
|
+
* by `1/k`; exact bijection for `k ≠ 0`. */
|
|
39
|
+
scale(k: Val<number>, pivot?: Val<V>): this;
|
|
40
|
+
/** Rotate by `angle` (radians) about `pivot` (default origin). Inverse
|
|
41
|
+
* rotates by `−angle`; exact bijection. */
|
|
42
|
+
rotate(angle: Val<number>, pivot?: Val<V>): this;
|
|
36
43
|
offset(dx: Val<number>, dy: Val<number>): this;
|
|
37
44
|
up(n: Val<number>): this;
|
|
38
45
|
down(n: Val<number>): this;
|
package/dist/core/values/vec.js
CHANGED
|
@@ -20,6 +20,18 @@ export const normalize = (v) => {
|
|
|
20
20
|
return m === 0 ? { x: 0, y: 0 } : { x: v.x / m, y: v.y / m };
|
|
21
21
|
};
|
|
22
22
|
export const perp = (v) => ({ x: v.y, y: -v.x });
|
|
23
|
+
export const rotateAbout = (v, p, dθ) => {
|
|
24
|
+
const cos = Math.cos(dθ);
|
|
25
|
+
const sin = Math.sin(dθ);
|
|
26
|
+
const dx = v.x - p.x;
|
|
27
|
+
const dy = v.y - p.y;
|
|
28
|
+
return { x: p.x + cos * dx - sin * dy, y: p.y + sin * dx + cos * dy };
|
|
29
|
+
};
|
|
30
|
+
export const scaleAbout = (v, p, k) => ({
|
|
31
|
+
x: p.x + k * (v.x - p.x),
|
|
32
|
+
y: p.y + k * (v.y - p.y),
|
|
33
|
+
});
|
|
34
|
+
const ORIGIN = { x: 0, y: 0 };
|
|
23
35
|
/** Tangent point on the circle (radius `r`, centre `c`) from external
|
|
24
36
|
* point `p`. `side: -1` picks the CCW tangent from `pc`, `+1` the CW
|
|
25
37
|
* (y-down screen coords flip the visual sense). Returns `c` if `p` is
|
|
@@ -49,19 +61,7 @@ const packImpl = {
|
|
|
49
61
|
},
|
|
50
62
|
write: (a, o) => ({ x: a[o], y: a[o + 1] }),
|
|
51
63
|
};
|
|
52
|
-
const pivotalImpl = {
|
|
53
|
-
rotateAbout: (v, p, dθ) => {
|
|
54
|
-
const cos = Math.cos(dθ);
|
|
55
|
-
const sin = Math.sin(dθ);
|
|
56
|
-
const dx = v.x - p.x;
|
|
57
|
-
const dy = v.y - p.y;
|
|
58
|
-
return { x: p.x + cos * dx - sin * dy, y: p.y + sin * dx + cos * dy };
|
|
59
|
-
},
|
|
60
|
-
scaleAbout: (v, p, k) => ({
|
|
61
|
-
x: p.x + k * (v.x - p.x),
|
|
62
|
-
y: p.y + k * (v.y - p.y),
|
|
63
|
-
}),
|
|
64
|
-
};
|
|
64
|
+
const pivotalImpl = { rotateAbout, scaleAbout };
|
|
65
65
|
export class Vec extends Cell {
|
|
66
66
|
static traits = {
|
|
67
67
|
linear: linearImpl,
|
|
@@ -94,16 +94,26 @@ export class Vec extends Cell {
|
|
|
94
94
|
return { x: n.x + o.x, y: n.y + o.y };
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
|
-
scale(
|
|
97
|
+
/** Uniform scale by `k` about `pivot` (default origin). Inverse scales
|
|
98
|
+
* by `1/k`; exact bijection for `k ≠ 0`. */
|
|
99
|
+
scale(k, pivot) {
|
|
98
100
|
const kf = reader(k);
|
|
101
|
+
const pf = pivot === undefined ? undefined : reader(pivot);
|
|
99
102
|
return this.lens(v => {
|
|
100
103
|
const k = kf();
|
|
101
|
-
return { x: v.x * k, y: v.y * k };
|
|
104
|
+
return pf ? scaleAbout(v, pf(), k) : { x: v.x * k, y: v.y * k };
|
|
102
105
|
}, n => {
|
|
103
106
|
const k = kf();
|
|
104
|
-
return { x: n.x / k, y: n.y / k };
|
|
107
|
+
return pf ? scaleAbout(n, pf(), 1 / k) : { x: n.x / k, y: n.y / k };
|
|
105
108
|
});
|
|
106
109
|
}
|
|
110
|
+
/** Rotate by `angle` (radians) about `pivot` (default origin). Inverse
|
|
111
|
+
* rotates by `−angle`; exact bijection. */
|
|
112
|
+
rotate(angle, pivot = ORIGIN) {
|
|
113
|
+
const af = reader(angle);
|
|
114
|
+
const pf = reader(pivot);
|
|
115
|
+
return this.lens(v => rotateAbout(v, pf(), af()), n => rotateAbout(n, pf(), -af()));
|
|
116
|
+
}
|
|
107
117
|
offset(dx, dy) {
|
|
108
118
|
const xf = reader(dx);
|
|
109
119
|
const yf = reader(dy);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./animation/index.js";
|
|
2
2
|
export * from "./assert/index.js";
|
|
3
3
|
export { type CodeOpts, CodeShape, code, codeStyles, type Token, tokenize } from "./code/index.js";
|
|
4
|
+
export * from "./coll.js";
|
|
4
5
|
export * from "./core/index.js";
|
|
5
6
|
export * from "./ext/index.js";
|
|
6
7
|
export * from "./shapes/index.js";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./assert/index.js";
|
|
|
3
3
|
// `code` and `tex` both export `Part`; re-export `code`'s other symbols
|
|
4
4
|
// explicitly so the wildcard below lets `tex`'s `Part` win.
|
|
5
5
|
export { CodeShape, code, codeStyles, tokenize } from "./code/index.js";
|
|
6
|
+
export * from "./coll.js";
|
|
6
7
|
export * from "./core/index.js";
|
|
7
8
|
export * from "./ext/index.js";
|
|
8
9
|
export * from "./shapes/index.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bireactive",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Bi-directional reactive programming.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
33
|
"dev": "vite --host",
|
|
34
|
-
"site": "vite-node site/build.ts && vite build && vite-node site/build.ts",
|
|
34
|
+
"site": "vite-node site/build.ts && vite build && vite-node site/build.ts && typedoc",
|
|
35
35
|
"preview": "vite preview",
|
|
36
36
|
"prebuild": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
37
37
|
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json --resolve-full-paths",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"mitata": "^1.0.34",
|
|
64
64
|
"reactive-framework-test-suite": "^0.0.2",
|
|
65
65
|
"tsc-alias": "^1.8.17",
|
|
66
|
+
"typedoc": "^0.28.19",
|
|
66
67
|
"typescript": "^6.0.3",
|
|
67
68
|
"vite": "^7.3.3",
|
|
68
69
|
"vite-node": "^5.3.0",
|