Repository navigation
Add stateful helpers for p5.strands addons - #9309
davepagurek merged 7 commits into
Conversation
Journal each temporary install on window, p5.prototype and p5.Graphics.prototype with its previous property descriptor, and restore the journal newest first after the build. Absent properties are removed again and setters never run. A property that can be neither redefined nor assigned is rejected before it is written, and a failed restore never replaces a build's own error. This also restores options.hook properties onto p5.Graphics.prototype rather than p5.Graphics, and a modify() called from inside another build's callback hands the enclosing journal back. Part of processing#9259.
Add p5.registerStrandsState(name, { init, update, read }), which returns
a factory for helpers used inside hooks. Helper state is merged across
if/else branches and carried through for loops, and
pushStrandsState()/popStrandsState() save and restore every helper's
scoped fields.
Uses that cannot work throw clear errors: a helper used outside the hook
call that created it, created inside a branch or loop, or used in a loop
header; break or early return in a loop body while helpers exist;
updates inside ?:, &&, ||, &&= or ||= operands; changing a state's
shape; and unbalanced push/pop.
The addon API is documented in contributor_docs/p5.strands.md.
Part of processing#9259.
If a hook throws and sketch code catches the error inside modify(), the first failure is rethrown as soon as the callback returns, before code generation. The helper scope is cleared so the next build starts clean, and finishHook's "requires a value" check now runs inside the same cleanup. The contributor doc notes this behaviour. Part of processing#9259.
A base shader can pass globalInstance next to hook in modify()'s options. It is called after that hook's begin() with the sketch as `this`, and the helper it returns has its methods installed with _installGlobal() on window, p5.prototype and p5.Graphics.prototype for the rest of the build, through the temporary install journal. The contributor doc covers options.hook, globalInstance, _installGlobal() and the temporary replacement and restoration of names. Part of processing#9259.
davepagurek
left a comment
There was a problem hiding this comment.
Thanks for taking this on, it's looking really promising! I left some somewhat minor comments. I also began updating the sdf addon here to use the branch build of this PR: davepagurek/p5.sdf#1
I did notice in examples/macbook there that it hits some other strands compilation errors. It transpiles to:
function anonymous(__p5
) {
debugger;
const BW = 260, BD = 175, BH = 16;
const LW = 258, LD = 172, LH = 6;
const CR = 4;
const HY = __p5.strandsNode(__p5.neg(BH)).div(2);
const HZ = __p5.strandsNode(__p5.neg(BD)).div(2);
const t = __p5.strandsNode(millis()).mult(0.0008);
const lidAngle = __p5.strandsNode(__p5.strandsNode(__p5.strandsNode(__p5.strandsNode(sin(t)).mult(0.5)).add(0.5)).mult(__p5.strandsNode(__p5.strandsNode(PI).mult(100)).div(180))).add(__p5.strandsNode(__p5.strandsNode(PI).mult(15)).div(180));
push();
fill(185, 185, 190);
roundBox(BW, BH, BD, CR);
pop();
push();
fill(155, 155, 160);
translate(80, HY, HZ);
rotateZ(__p5.strandsNode(Math.PI).div(2));
cylinder(7, 38);
pop();
push();
fill(155, 155, 160);
translate(__p5.neg(80), HY, HZ);
rotateZ(__p5.strandsNode(Math.PI).div(2));
cylinder(7, 38);
pop();
push();
fill(185, 185, 190);
translate(0, HY, HZ);
rotateX(lidAngle);
translate(0, __p5.strandsNode(__p5.neg(LH)).div(2), __p5.strandsNode(BD).div(2));
roundBox(LW, LH, LD, CR);
pop();
push();
fill(15, 15, 20);
translate(0, HY, HZ);
rotateX(lidAngle);
translate(0, __p5.neg(0.5), __p5.strandsNode(BD).div(2));
roundBox(__p5.strandsNode(LW).sub(28), 2, __p5.strandsNode(LD).sub(24), 3);
pop();
push();
fill(10, 10, 12);
translate(0, HY, HZ);
rotateX(lidAngle);
translate(0, __p5.neg(0.5), __p5.strandsNode(__p5.strandsNode(__p5.strandsNode(BD).div(2)).add(__p5.strandsNode(LD).div(2))).sub(10));
roundBox(28, 2, 8, 2);
pop();
push();
fill(38, 38, 42);
translate(0, __p5.strandsNode(HY).sub(0.5), __p5.neg(32));
roundBox(214, 2, 82, 4);
pop();
const cols = 10;
const keyW = 14, keyH = 4, keyD = 14, keyR = 2;
const pitchX = 18, pitchZ = 14;
const kX0 = __p5.strandsNode(__p5.strandsNode(__p5.neg(__p5.strandsNode(cols).sub(1))).mult(pitchX)).div(2);
const kZ0 = __p5.neg(61);
const kY = __p5.strandsNode(HY).sub(2);
{
const __block_1 = __p5.strandsFor(() => {
return 0;
}, loopVar1 =>
(__p5.strandsNode(loopVar1).lessThan(4)), loopVar1 => {
return loopVar1 = __p5.strandsNode(loopVar1).add(1);
}, (loopVar1, vars) => {
let __copy_loopVar0_0 = vars['loopVar0'].copy();
__p5.strandsFor(() => {
return 0;
}, __copy_loopVar0_0 =>
(__p5.strandsNode(__copy_loopVar0_0).lessThan(cols)), __copy_loopVar0_0 => {
return __copy_loopVar0_0 = __p5.strandsNode(__copy_loopVar0_0).add(1);
}, (__copy_loopVar0_0, vars) => {
push();
fill(68, 68, 74);
translate(__p5.strandsNode(kX0).add(__p5.strandsNode(__copy_loopVar0_0).mult(pitchX)), kY, __p5.strandsNode(kZ0).add(__p5.strandsNode(loopVar1).mult(pitchZ)));
roundBox(keyW, keyH, keyD, keyR);
pop();
}, {});
return { 'loopVar0': __copy_loopVar0_0 };
}, { 'loopVar0': __p5.strandsNode(loopVar0) });
loopVar0 = __block_1['loopVar0'];
}
push();
fill(68, 68, 74);
translate(0, kY, __p5.strandsNode(kZ0).add(__p5.strandsNode(4).mult(pitchZ)));
roundBox(100, keyH, keyD, keyR);
pop();
push();
fill(172, 172, 180);
translate(0, __p5.strandsNode(HY).sub(0.5), 42);
roundBox(110, 2, 62, 6);
pop();
apply();
}and gets this error:
Uncaught (in promise) ReferenceError: loopVar0 is not defined
anonymous https://raw.esm.sh/pr/p5@462e53f/lib/p5.min.js line 1872 > Function:80
p5 https://raw.esm.sh/pr/p5@462e53f/lib/p5.min.js:1872
modify https://raw.esm.sh/pr/p5@462e53f/lib/p5.min.js:1876
buildSDF file:///Users/dpagurek/Documents/Projects/p5.sdf/p5.sdf.js:497
setup file:///Users/dpagurek/Documents/Projects/p5.sdf/example/macbook.js:6
#n https://raw.esm.sh/pr/p5@462e53f/lib/p5.min.js:323
#t https://raw.esm.sh/pr/p5@462e53f/lib/p5.min.js:323
t https://raw.esm.sh/pr/p5@462e53f/lib/p5.min.js:323
p5 https://raw.esm.sh/pr/p5@462e53f/lib/p5.min.js:1877
promise callback*p5< https://raw.esm.sh/pr/p5@462e53f/lib/p5.min.js:1877
<anonymous> https://raw.esm.sh/pr/p5@462e53f/lib/p5.min.js:1877
Lmk if you have thoughts on the cause! I'm testing things out on that library on the side to help validate the API etc.
|
|
||
| `p5.registerStrandsState(name, { init, update, read })` returns a factory. Assign it to `fn`; registration installs nothing. `name` identifies errors. `update` and `read` are required but may be empty objects. | ||
|
|
||
| - `init(...args)` receives the factory arguments and returns `{ config, state }`. Any `config` value is passed unchanged to methods. `state` contains `scoped` and `global` objects with at least one field overall. Fields must be Strands float/int scalars or vectors of up to 4 components, for example using `p5.strandsNode()`. |
There was a problem hiding this comment.
We recently added strands matrix nodes too, it could be those too right? Should we update the restriction so that it's mostly that it needs to be a strands node of some kind?
One other thought: in the current p5.sdf, it also pushes functions to a stack to handle modifiers like twist, etc, since unlike positions, they can't be eagerly evaluated into strands nodes as they modify the distance and not the source point. I think there's nothing we can do about that for now because we would need some way to dynamically create functions in glsl, which we don't have. For p5.sdf that might either mean using a fixed pipeline of shape modifiers or to change the API to use an explicit wrapping function like round(4, () => box(60)) instead of using state. No action needed I think, just mentioning it in case you have any ideas!
There was a problem hiding this comment.
Also, we should explain the difference between scoped and global (maybe showing an example of a thing you'd use each for?)
There was a problem hiding this comment.
Added matrix support and an example showing scoped values being restored by pop(), while global values keep their updates. Backend limitations are documented too.
| let modified; | ||
| let restoreErrors = []; | ||
| // Preserve the outer install journal; nested builds remain unsupported. | ||
| const nested = modifyDepth > 0; |
There was a problem hiding this comment.
Agreed that nested modify calls is unsupported; should we just throw an error here?
There was a problem hiding this comment.
Agreed, nested modify() now throws before touching the outer build’s state. Removed the nesting bookkeeping too.
| export function installTemporaryProperty(strandsContext, target, name, value) { | ||
| const descriptor = Object.getOwnPropertyDescriptor(target, name); | ||
| const assignable = descriptor && 'value' in descriptor && descriptor.writable; | ||
| if (descriptor && !descriptor.configurable && !assignable) { |
There was a problem hiding this comment.
Are there known things that would trigger this out of curiosity?
There was a problem hiding this comment.
window.top and document are examples, and location has a setter that could navigate the page. The check protects those; writable sketch globals are still replaced and restored.
There was a problem hiding this comment.
got it, makes sense! I could see those possibly being names an addon might overload without thinking about it.
| }); | ||
| }); | ||
|
|
||
| suite('registered state (#9259)', () => { |
There was a problem hiding this comment.
We generally don't need a PR numbers in the unit tests, especially as multiple PRs modify functionality over time it can get messy trying to reference them all. We can describe the feature instead
There was a problem hiding this comment.
Done, removed the issue numbers.
| } | ||
| scope.stack.push(frame); | ||
| }; | ||
| augmentFn(fn, p5, 'pushStrandsState', p5.pushStrandsState); |
There was a problem hiding this comment.
Are we able to call these just push and pop? That would make the UX look as similar as possible to p5 outside of strands. Since regular push/pop cannot be used outside of strands, I think it should be safe to override their behaviour while strands is active
There was a problem hiding this comment.
Done, switched to push()/pop(). I also tested p5.sdf locally with its old push/pop wrappers removed.
|
I reproduced the MacBook error on the base revision too and tracked it to nested-loop handling. I’ve prepared a separate fix. With that fix and the old p5.sdf push/pop wrappers removed locally, the example renders correctly. |
davepagurek
left a comment
There was a problem hiding this comment.
This is looking good now, thanks!
Resolves #9259
Changes:
Adds
p5.registerStrandsState()so addons can create independent helpers whose values follow branches and loops.push()andpop()restore scoped values while global values keep their updates.The optional
globalInstanceexposes a helper’s methods during a build, then restores the original properties. Nestedmodify()calls are rejected. Errors escaping hook callbacks orbegin()/end()stop code generation, even if the sketch catches them.Includes addon docs and examples, plus a small WGSL fix needed for matrix state.
Tested locally:
Screenshots of the change:
Tests check rendered pixels on both backends.
PR Checklist
npm run lintpassesAI disclosure: AI tools were used for tests and documentation. I reviewed and approved the decisions, and tested the changes locally.