exprforge 0.1.0 ā 0.2.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/README.md +71 -0
- package/ast.js +22 -1
- package/index.js +7 -2
- package/math/index.js +105 -0
- package/package.json +8 -2
- package/samples/math-demo.js +44 -0
- package/samples/spline-frame.js +60 -42
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# ExprForge š¢šØ
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/exprforge)
|
|
3
4
|
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-typescript.yml)
|
|
4
5
|
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-python.yml)
|
|
5
6
|
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-csharp.yml)
|
|
@@ -69,6 +70,9 @@ package individually, or together as `samples`):
|
|
|
69
70
|
purely as a conformance-test fixture. It's what caught Go's and Rust's
|
|
70
71
|
`sign()` disagreeing with everyone else at exactly zero (see below) ā
|
|
71
72
|
the other samples between them only ever exercised 5 of the 22.
|
|
73
|
+
- `samples/math-demo.js` ā also not a worked example: a conformance-test
|
|
74
|
+
fixture exercising every `exprforge/math` helper (see below) in one
|
|
75
|
+
suite.
|
|
72
76
|
|
|
73
77
|
`npm run build` emits all of them, for every target language, into `out/`.
|
|
74
78
|
|
|
@@ -80,6 +84,46 @@ floor ceil round trunc sign min max hypot`
|
|
|
80
84
|
Add more by extending a target's `calls` table in `emitters/<lang>.js`.
|
|
81
85
|
Requesting an unmapped function throws at build time, not silently.
|
|
82
86
|
|
|
87
|
+
## Math utilities (`exprforge/math`)
|
|
88
|
+
|
|
89
|
+
A separate, additive export ā `require("exprforge")` is unchanged ā of
|
|
90
|
+
pre-built compositions of the core AST builders for common 3-D math
|
|
91
|
+
patterns, so consumers stop re-implementing the same safe-math and vector
|
|
92
|
+
code in every project (`samples/spline-frame.js` had local, hand-rolled
|
|
93
|
+
versions of most of these before this module existed).
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
const { num, v } = require("exprforge");
|
|
97
|
+
const { safeDiv, dot3, len3, cross3, normalize3, clamp, EPS } = require("exprforge/math");
|
|
98
|
+
|
|
99
|
+
// Safe-normalize x component, falling back to 0 near zero length.
|
|
100
|
+
safeDiv(v("x"), len3(v("x"), v("y"), v("z")), num(0));
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- `safeDiv(numerator, denominatorExpr, fallback)` ā `numerator /
|
|
104
|
+
denominatorExpr` when `|denominatorExpr| > EPS`, else `fallback`. Clamps
|
|
105
|
+
the denominator before dividing rather than guarding the division
|
|
106
|
+
directly, since `select()` always evaluates both branches (see below).
|
|
107
|
+
- `dot3(ax, ay, az, bx, by, bz)` ā `ax*bx + ay*by + az*bz`.
|
|
108
|
+
- `len3(x, y, z)` ā `sqrt(x² + y² + z²)`.
|
|
109
|
+
- `cross3(ax, ay, az, bx, by, bz)` ā 3-D cross product. Returns a plain JS
|
|
110
|
+
object `{ x, y, z }` of AST nodes (not a Node itself), for destructuring
|
|
111
|
+
into your own `letIn` chain.
|
|
112
|
+
- `normalize3(x, y, z, fx?, fy?, fz?)` ā safe-normalize; same `{ x, y, z }`
|
|
113
|
+
shape as `cross3`. Falls back to `(fx, fy, fz)` (default `(0, 1, 0)`)
|
|
114
|
+
below `EPS` length. Computes the length once and shares it across all
|
|
115
|
+
three divisions.
|
|
116
|
+
- `clamp(val, lo, hi)` ā clamps to `[lo, hi]` via nested `select`/`cmp`; no
|
|
117
|
+
runtime intrinsic.
|
|
118
|
+
- `EPS` ā `num(0.000001)`, the epsilon every guard above uses; exported for
|
|
119
|
+
callers who want the same threshold in their own `cmp()` calls.
|
|
120
|
+
|
|
121
|
+
What deliberately stays out (project-specific conventions, not general
|
|
122
|
+
math): a "near-vertical" world-up check, baked-in-PI degree/radian
|
|
123
|
+
conversion, Rodrigues rotation, and a full Gram-Schmidt frame ā see
|
|
124
|
+
`samples/spline-frame.js` for those, and
|
|
125
|
+
`docs/v0.2.0-math-utilities.md` for the full design rationale.
|
|
126
|
+
|
|
83
127
|
## Adding a language
|
|
84
128
|
|
|
85
129
|
Write `emitters/<lang>.js` exporting an `Emitter` instance (see any
|
|
@@ -97,6 +141,33 @@ expression model without introducing control flow:
|
|
|
97
141
|
components by it). Every `let` in a function gets lifted into an ordered
|
|
98
142
|
list of local declarations ahead of the return statement/expression, in
|
|
99
143
|
every target.
|
|
144
|
+
|
|
145
|
+
Chaining several is normally hand-nested `letIn` calls, one inside the
|
|
146
|
+
next, closing parens piling up at the end with no real hierarchy behind
|
|
147
|
+
them ā just bookkeeping to get everything hoisted before it's used.
|
|
148
|
+
**`letChain(bindings, body)`** is that same nesting, built for you from a
|
|
149
|
+
flat, ordered list instead:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
const { v, num, mul, add, letChain, outputs } = require("exprforge");
|
|
153
|
+
|
|
154
|
+
letChain(
|
|
155
|
+
[
|
|
156
|
+
["t2", mul(v("t"), v("t"))],
|
|
157
|
+
["t3", mul(v("t2"), v("t"))],
|
|
158
|
+
],
|
|
159
|
+
outputs({ t2: v("t2"), t3: v("t3") }),
|
|
160
|
+
);
|
|
161
|
+
// same tree as letIn("t2", ..., letIn("t3", ..., outputs({...})))
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`bindings` is an ordered array of `[name, valueNode]` pairs, not a
|
|
165
|
+
`{name: valueNode}` object like `outputs()` takes ā order is
|
|
166
|
+
load-bearing here (a later binding's value can reference an earlier
|
|
167
|
+
one's name), and that's clearer as an explicit sequence than resting on
|
|
168
|
+
an object's key order. Pure authoring sugar: builds the identical `let`
|
|
169
|
+
node structure `letIn` would, so it needs no emitter changes and
|
|
170
|
+
round-trips through `collectLets` the same way.
|
|
100
171
|
- **`select(cond, then, else)` + `cmp(left, op, right)`** ā conditional
|
|
101
172
|
*value* selection. Every target has a genuinely different way to spell
|
|
102
173
|
this: a native ternary where one exists (C, Java, C#), `if`-as-expression
|
package/ast.js
CHANGED
|
@@ -62,6 +62,27 @@ function letIn(name, value, body) {
|
|
|
62
62
|
return { type: "let", name, value, body };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Chains N letIn bindings without hand-nesting them (and hand-balancing the
|
|
66
|
+
// resulting N closing parens ā the nesting depth reflects no real
|
|
67
|
+
// hierarchy, only that each binding must be lifted ahead of anything using
|
|
68
|
+
// it). Builds the exact same nested `let` structure letIn() would if
|
|
69
|
+
// written out by hand: pure authoring sugar, not a new node type, so
|
|
70
|
+
// collectLets and every emitter already understand the result unchanged.
|
|
71
|
+
//
|
|
72
|
+
// `bindings` is an ORDERED array of [name, valueNode] pairs, not a
|
|
73
|
+
// {name: valueNode} object like outputs() takes ā order is load-bearing
|
|
74
|
+
// here (a later binding's value can reference an earlier one's name via
|
|
75
|
+
// v(name)), and a plain object's key order isn't reliably that: a binding
|
|
76
|
+
// named e.g. "0" would silently sort ahead of everything else. An array
|
|
77
|
+
// keeps "this is a strict sequence" explicit instead of resting on that.
|
|
78
|
+
//
|
|
79
|
+
// Doesn't check for duplicate names itself ā collectLets already does,
|
|
80
|
+
// with the whole function body in view (see its doc comment); duplicating
|
|
81
|
+
// that check here would only see this one chain, not the whole picture.
|
|
82
|
+
function letChain(bindings, body) {
|
|
83
|
+
return bindings.reduceRight((acc, [name, value]) => letIn(name, value, acc), body);
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
// Comparison predicate ā only valid as the `cond` of a select(); not a
|
|
66
87
|
// general boolean expression, and shouldn't appear anywhere else in a tree.
|
|
67
88
|
function cmp(left, op, right) {
|
|
@@ -139,4 +160,4 @@ function collectLets(node) {
|
|
|
139
160
|
return { bindings, body };
|
|
140
161
|
}
|
|
141
162
|
|
|
142
|
-
module.exports = { num, v, bin, call, add, mul, sub, div, neg, letIn, cmp, select, outputs, collectLets };
|
|
163
|
+
module.exports = { num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, collectLets };
|
package/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// exprforge/index.js
|
|
2
|
-
const { num, v, bin, call, add, mul, sub, div, neg, letIn, cmp, select, outputs, collectLets } = require("./ast.js");
|
|
2
|
+
const { num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, collectLets } = require("./ast.js");
|
|
3
3
|
const { forComponents } = require("./util.js");
|
|
4
4
|
const emitters = require("./emitters/registry.js");
|
|
5
5
|
const { catmullRomAst } = require("./samples/catmull-rom.js");
|
|
6
6
|
const { fibonacciAst } = require("./samples/fibonacci.js");
|
|
7
7
|
const { splineFrameAsts } = require("./samples/spline-frame.js");
|
|
8
8
|
const { kitchenSinkAst } = require("./samples/kitchen-sink.js");
|
|
9
|
+
const { mathDemoAst } = require("./samples/math-demo.js");
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Run every registered emitter against one AST function definition.
|
|
@@ -21,7 +22,7 @@ function emitAll(fn) {
|
|
|
21
22
|
|
|
22
23
|
module.exports = {
|
|
23
24
|
// AST builders ā use these to define your own formulas.
|
|
24
|
-
num, v, bin, call, add, mul, sub, div, neg, letIn, cmp, select, outputs, collectLets,
|
|
25
|
+
num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, collectLets,
|
|
25
26
|
// Authoring convenience ā not an AST primitive, see util.js.
|
|
26
27
|
forComponents,
|
|
27
28
|
// Built-in example formulas ā see samples/ for the source.
|
|
@@ -31,11 +32,15 @@ module.exports = {
|
|
|
31
32
|
// Not a worked example -- a conformance-test fixture that calls every
|
|
32
33
|
// supported Math function once. See samples/kitchen-sink.js.
|
|
33
34
|
kitchenSinkAst,
|
|
35
|
+
// Also not a worked example -- a conformance-test fixture for
|
|
36
|
+
// require("exprforge/math"). See samples/math-demo.js.
|
|
37
|
+
mathDemoAst,
|
|
34
38
|
samples: {
|
|
35
39
|
catmullRom: catmullRomAst,
|
|
36
40
|
fibonacci: fibonacciAst,
|
|
37
41
|
splineFrame: splineFrameAsts,
|
|
38
42
|
kitchenSink: kitchenSinkAst,
|
|
43
|
+
mathDemo: mathDemoAst,
|
|
39
44
|
},
|
|
40
45
|
// Per-language emitter instances, keyed by name (js, qb64, c, java, go, rust).
|
|
41
46
|
emitters,
|
package/math/index.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// exprforge/math/index.js
|
|
2
|
+
// Standard math utilities ā see docs/v0.2.0-math-utilities.md for the
|
|
3
|
+
// design doc this implements. This module is level 1: pure compositions of
|
|
4
|
+
// the level-0 AST primitives (ast.js), built to save every consumer from
|
|
5
|
+
// re-deriving the same safe-math/vector patterns samples/spline-frame.js
|
|
6
|
+
// used to hand-roll locally (dot3/len3/safeDiv/EPS there predate this file
|
|
7
|
+
// and motivated it). No new emitter logic ā emitAll handles these
|
|
8
|
+
// transparently, same as any other AST the caller builds by hand.
|
|
9
|
+
//
|
|
10
|
+
// require("exprforge/math") is a separate export path from require("exprforge")
|
|
11
|
+
// itself (see package.json's "exports" map) ā additive, not merged into the
|
|
12
|
+
// core barrel.
|
|
13
|
+
const { num, v, call, add, mul, sub, div, letIn, cmp, select } = require("../ast.js");
|
|
14
|
+
|
|
15
|
+
// Shared epsilon for all near-zero guards below. Exposed so callers can
|
|
16
|
+
// reuse it in their own cmp() calls for consistency with safeDiv/normalize3,
|
|
17
|
+
// same convention as samples/spline-frame.js's local EPS.
|
|
18
|
+
const EPS = num(0.000001);
|
|
19
|
+
|
|
20
|
+
// Guard against division by zero: numerator/denominatorExpr when
|
|
21
|
+
// |denominatorExpr| > EPS, else fallback. Per select()'s doc comment in
|
|
22
|
+
// ast.js, both branches of a select are always evaluated on every target ā
|
|
23
|
+
// so this does NOT guard the division directly (div(numerator,
|
|
24
|
+
// denominatorExpr) would still be reached with a near-zero denominator on
|
|
25
|
+
// any target that can't short-circuit, e.g. QB64). Instead the denominator
|
|
26
|
+
// is clamped to a safe, always-nonzero value by its own select first,
|
|
27
|
+
// mirroring the local safeDiv in samples/spline-frame.js.
|
|
28
|
+
//
|
|
29
|
+
// denominatorExpr is referenced twice in the resulting tree (once for the
|
|
30
|
+
// |.| > EPS check, once in the clamped-denominator select) ā cheap if it's
|
|
31
|
+
// a var reference or a simple expression, but if it's an expensive
|
|
32
|
+
// subexpression (e.g. a len3() call), pass an already-letIn-bound v(name)
|
|
33
|
+
// instead of the raw expression to avoid computing it twice per emitted
|
|
34
|
+
// target. normalize3() below does exactly that internally.
|
|
35
|
+
function safeDiv(numerator, denominatorExpr, fallback) {
|
|
36
|
+
const isSafe = cmp(call("abs", denominatorExpr), ">", EPS);
|
|
37
|
+
const safeDenom = select(isSafe, denominatorExpr, num(1));
|
|
38
|
+
return select(isSafe, div(numerator, safeDenom), fallback);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 3-D dot product: ax*bx + ay*by + az*bz.
|
|
42
|
+
function dot3(ax, ay, az, bx, by, bz) {
|
|
43
|
+
return add(mul(ax, bx), mul(ay, by), mul(az, bz));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 3-D Euclidean length: sqrt(x² + y² + z²). Emits a sqrt intrinsic (see
|
|
47
|
+
// README's "Supported Math functions").
|
|
48
|
+
function len3(x, y, z) {
|
|
49
|
+
return call("sqrt", dot3(x, y, z, x, y, z));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 3-D cross product. Returns a plain JS object { x, y, z } of AST nodes,
|
|
53
|
+
// NOT an AST node itself ā a deliberate ergonomic choice so callers can
|
|
54
|
+
// destructure and name each component in their own letIn chain, rather
|
|
55
|
+
// than exprforge picking the names for them.
|
|
56
|
+
function cross3(ax, ay, az, bx, by, bz) {
|
|
57
|
+
return {
|
|
58
|
+
x: sub(mul(ay, bz), mul(az, by)),
|
|
59
|
+
y: sub(mul(az, bx), mul(ax, bz)),
|
|
60
|
+
z: sub(mul(ax, by), mul(ay, bx)),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Monotonic counter behind normalize3's internal let-binding names ā see
|
|
65
|
+
// the comment inside normalize3 for why it needs one at all. Global (not
|
|
66
|
+
// per-call-site) is deliberately overkill: it only has to avoid colliding
|
|
67
|
+
// with another normalize3() binding inside the same function body, and a
|
|
68
|
+
// process-wide counter trivially guarantees that regardless of how many
|
|
69
|
+
// times normalize3 is called across however many functions.
|
|
70
|
+
let normalizeGensymCounter = 0;
|
|
71
|
+
|
|
72
|
+
// Safe-normalize a 3-D vector. Returns { x, y, z } (same shape as cross3).
|
|
73
|
+
// Falls back to (fx, fy, fz) ā default (0, 1, 0) ā when the vector's length
|
|
74
|
+
// is at or below EPS.
|
|
75
|
+
//
|
|
76
|
+
// Per the spec's recommendation, this computes len3(x, y, z) ONCE and
|
|
77
|
+
// shares it across all three divisions (one EPS check, one sqrt), instead
|
|
78
|
+
// of calling safeDiv three times against three independent len3() calls.
|
|
79
|
+
// The mechanism: the length is let-bound inside the `x` field's own tree,
|
|
80
|
+
// and `y`/`z` just reference that bound name bare. collectLets (ast.js)
|
|
81
|
+
// hoists a let found anywhere in a function body to one flat, ordered list
|
|
82
|
+
// regardless of which sibling subtree it was found in ā see
|
|
83
|
+
// test/ast.test.js's "collectLets hoists a let nested inside one output
|
|
84
|
+
// field's own value" for the exact behavior this relies on. The gensym'd
|
|
85
|
+
// name avoids a "duplicate let binding name" throw if normalize3 is called
|
|
86
|
+
// more than once inside one function (e.g. normalizing two vectors).
|
|
87
|
+
function normalize3(x, y, z, fx = num(0), fy = num(1), fz = num(0)) {
|
|
88
|
+
const lenName = `__exprforgeMathNrmLen${normalizeGensymCounter++}`;
|
|
89
|
+
return {
|
|
90
|
+
x: letIn(lenName, len3(x, y, z), safeDiv(x, v(lenName), fx)),
|
|
91
|
+
y: safeDiv(y, v(lenName), fy),
|
|
92
|
+
z: safeDiv(z, v(lenName), fz),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Clamps val to [lo, hi]. Expressed as nested select/cmp ā no runtime
|
|
97
|
+
// intrinsic required, matching cmp/select's existing usage elsewhere (see
|
|
98
|
+
// samples/spline-frame.js). val is referenced three times in the resulting
|
|
99
|
+
// tree; pass a var reference (or an already-let-bound one) if it's not
|
|
100
|
+
// already cheap to re-evaluate.
|
|
101
|
+
function clamp(val, lo, hi) {
|
|
102
|
+
return select(cmp(val, "<", lo), lo, select(cmp(val, ">", hi), hi, val));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { EPS, safeDiv, dot3, len3, cross3, normalize3, clamp };
|
package/package.json
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exprforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Author a math expression once as an AST, emit identical-behavior implementations in JS, TypeScript, Python, C#, Lua, QB64, C, Java, Go, and Rust.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./math": "./math/index.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
7
12
|
"files": [
|
|
8
13
|
"index.js",
|
|
9
14
|
"ast.js",
|
|
10
15
|
"util.js",
|
|
11
16
|
"build.js",
|
|
12
17
|
"emitters/",
|
|
13
|
-
"samples/"
|
|
18
|
+
"samples/",
|
|
19
|
+
"math/"
|
|
14
20
|
],
|
|
15
21
|
"scripts": {
|
|
16
22
|
"build": "node build.js",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// exprforge/samples/math-demo.js
|
|
2
|
+
// Not a worked example -- a conformance-test fixture for math/index.js,
|
|
3
|
+
// same role samples/kitchen-sink.js plays for the core Math intrinsics:
|
|
4
|
+
// exercises every exprforge/math helper in one suite, purely so
|
|
5
|
+
// test/conformance.test.js can prove the generated code agrees across
|
|
6
|
+
// every target. Calls normalize3() twice (once per input vector) in the
|
|
7
|
+
// same function specifically to prove its internal gensym'd let-binding
|
|
8
|
+
// doesn't collide with itself -- see the comment on normalizeGensymCounter
|
|
9
|
+
// in math/index.js.
|
|
10
|
+
const { v, num, outputs } = require("../ast.js");
|
|
11
|
+
const { safeDiv, dot3, len3, cross3, normalize3, clamp } = require("../math/index.js");
|
|
12
|
+
|
|
13
|
+
const MATH_DEMO_PARAMS = ["ax", "ay", "az", "bx", "by", "bz", "t", "lo", "hi"];
|
|
14
|
+
|
|
15
|
+
const cross = cross3(v("ax"), v("ay"), v("az"), v("bx"), v("by"), v("bz"));
|
|
16
|
+
const normA = normalize3(v("ax"), v("ay"), v("az"));
|
|
17
|
+
const normB = normalize3(v("bx"), v("by"), v("bz"));
|
|
18
|
+
|
|
19
|
+
const MathDemo = {
|
|
20
|
+
name: "MathDemo",
|
|
21
|
+
params: MATH_DEMO_PARAMS,
|
|
22
|
+
body: outputs({
|
|
23
|
+
dot: dot3(v("ax"), v("ay"), v("az"), v("bx"), v("by"), v("bz")),
|
|
24
|
+
// "mag", not "len": LEN is a reserved QB64 builtin (string/array
|
|
25
|
+
// length) -- see test/conformance.test.js's normalizeXAst comment.
|
|
26
|
+
mag: len3(v("ax"), v("ay"), v("az")),
|
|
27
|
+
crossX: cross.x,
|
|
28
|
+
crossY: cross.y,
|
|
29
|
+
crossZ: cross.z,
|
|
30
|
+
normAX: normA.x,
|
|
31
|
+
normAY: normA.y,
|
|
32
|
+
normAZ: normA.z,
|
|
33
|
+
normBX: normB.x,
|
|
34
|
+
normBY: normB.y,
|
|
35
|
+
normBZ: normB.z,
|
|
36
|
+
clamped: clamp(v("t"), v("lo"), v("hi")),
|
|
37
|
+
// ax/bx as numerator/denominator: exercises both the safe path
|
|
38
|
+
// (bx away from zero) and the fallback (bx == 0, see
|
|
39
|
+
// test/conformance.test.js's math-demo input rows).
|
|
40
|
+
safeDivResult: safeDiv(v("ax"), v("bx"), num(-1)),
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
module.exports = { mathDemoAst: MathDemo };
|
package/samples/spline-frame.js
CHANGED
|
@@ -15,7 +15,12 @@
|
|
|
15
15
|
// IMPORTANT for QB64: function/SUB names must be unique across the entire
|
|
16
16
|
// QB64 compilation unit. The SpEf prefix (SplineExprforge) exists to avoid
|
|
17
17
|
// collisions with hand-written code elsewhere in that project.
|
|
18
|
-
|
|
18
|
+
//
|
|
19
|
+
// mfLetChain/apLetChain/rfLetChain/SpEfCrWeights use letChain() (ast.js)
|
|
20
|
+
// instead of hand-nested letIn calls -- apLetChain was 14 levels deep
|
|
21
|
+
// before, all hand-balanced closing parens with no real hierarchy behind
|
|
22
|
+
// the nesting, exactly the kind of thing that's easy to miscount editing.
|
|
23
|
+
const { num, v, call, add, mul, sub, div, neg, letChain, select, cmp, outputs } = require("../ast.js");
|
|
19
24
|
|
|
20
25
|
// āā Helpers āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
21
26
|
const PI = num(3.141592653589793);
|
|
@@ -66,17 +71,20 @@ const MF_PARAMS = ["tx", "ty", "tz"];
|
|
|
66
71
|
const nearVert = cmp(call("abs", v("ty")), ">", num(0.98));
|
|
67
72
|
|
|
68
73
|
function mfLetChain(body) {
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
return letChain(
|
|
75
|
+
[
|
|
76
|
+
["wy", select(nearVert, num(0), num(1))],
|
|
77
|
+
["wz", select(nearVert, num(1), num(0))],
|
|
78
|
+
["crossX", sub(mul(v("ty"), v("wz")), mul(v("tz"), v("wy")))],
|
|
79
|
+
["crossY", neg(mul(v("tx"), v("wz")))],
|
|
80
|
+
["crossZ", mul(v("tx"), v("wy"))],
|
|
81
|
+
["rLen", len3(v("crossX"), v("crossY"), v("crossZ"))],
|
|
82
|
+
["rxN", safeDiv(v("crossX"), "rLen", num(0))],
|
|
83
|
+
["ryN", safeDiv(v("crossY"), "rLen", num(0))],
|
|
84
|
+
["rzN", safeDiv(v("crossZ"), "rLen", num(1))],
|
|
85
|
+
],
|
|
86
|
+
body,
|
|
87
|
+
);
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
// U = R Ć T (using normalized R) ā a cyclic permutation, not one formula,
|
|
@@ -101,23 +109,26 @@ const SpEfMkFrame = {
|
|
|
101
109
|
const AP_PARAMS = ["wx", "wy_wire", "wz_wire", "tx", "ty", "tz", "prDeg", "so"];
|
|
102
110
|
|
|
103
111
|
function apLetChain(body) {
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
return letChain(
|
|
113
|
+
[
|
|
114
|
+
["wy", select(nearVert, num(0), num(1))],
|
|
115
|
+
["wz", select(nearVert, num(1), num(0))],
|
|
116
|
+
["crossX", sub(mul(v("ty"), v("wz")), mul(v("tz"), v("wy")))],
|
|
117
|
+
["crossY", neg(mul(v("tx"), v("wz")))],
|
|
118
|
+
["crossZ", mul(v("tx"), v("wy"))],
|
|
119
|
+
["rLen", len3(v("crossX"), v("crossY"), v("crossZ"))],
|
|
120
|
+
["rxN", safeDiv(v("crossX"), "rLen", num(0))],
|
|
121
|
+
["ryN", safeDiv(v("crossY"), "rLen", num(0))],
|
|
122
|
+
["rzN", safeDiv(v("crossZ"), "rLen", num(1))],
|
|
123
|
+
["ux", sub(mul(v("ryN"), v("tz")), mul(v("rzN"), v("ty")))],
|
|
124
|
+
["uy", sub(mul(v("rzN"), v("tx")), mul(v("rxN"), v("tz")))],
|
|
125
|
+
["uz", sub(mul(v("rxN"), v("ty")), mul(v("ryN"), v("tx")))],
|
|
126
|
+
["rad", degToRad(v("prDeg"))],
|
|
127
|
+
["c", call("cos", v("rad"))],
|
|
128
|
+
["s", call("sin", v("rad"))],
|
|
129
|
+
],
|
|
130
|
+
body,
|
|
131
|
+
);
|
|
121
132
|
}
|
|
122
133
|
|
|
123
134
|
const SpEfActualPos = {
|
|
@@ -139,10 +150,14 @@ const SpEfActualPos = {
|
|
|
139
150
|
const RF_PARAMS = ["ux", "uy", "uz", "rx", "ry", "rz", "crDeg"];
|
|
140
151
|
|
|
141
152
|
function rfLetChain(body) {
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
153
|
+
return letChain(
|
|
154
|
+
[
|
|
155
|
+
["rad", degToRad(v("crDeg"))],
|
|
156
|
+
["c", call("cos", v("rad"))],
|
|
157
|
+
["s", call("sin", v("rad"))],
|
|
158
|
+
],
|
|
159
|
+
body,
|
|
160
|
+
);
|
|
146
161
|
}
|
|
147
162
|
|
|
148
163
|
const SpEfRollFrame = {
|
|
@@ -167,15 +182,18 @@ const CRW_PARAMS = ["t"];
|
|
|
167
182
|
const SpEfCrWeights = {
|
|
168
183
|
name: "SpEfCrWeights",
|
|
169
184
|
params: CRW_PARAMS,
|
|
170
|
-
body:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
185
|
+
body: letChain(
|
|
186
|
+
[
|
|
187
|
+
["t2", mul(v("t"), v("t"))],
|
|
188
|
+
["t3", mul(v("t2"), v("t"))],
|
|
189
|
+
],
|
|
190
|
+
outputs({
|
|
191
|
+
w0: mul(num(0.5), add(neg(v("t3")), mul(num(2), v("t2")), neg(v("t")))),
|
|
192
|
+
w1: mul(num(0.5), add(mul(num(3), v("t3")), mul(num(-5), v("t2")), num(2))),
|
|
193
|
+
w2: mul(num(0.5), add(mul(num(-3), v("t3")), mul(num(4), v("t2")), v("t"))),
|
|
194
|
+
w3: mul(num(0.5), add(v("t3"), neg(v("t2")))),
|
|
195
|
+
}),
|
|
196
|
+
),
|
|
179
197
|
};
|
|
180
198
|
|
|
181
199
|
// āā Exports āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|