Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/strands/ir_builders.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ export function castToFloat(strandsContext, dep) {

export function structConstructorNode(strandsContext, structTypeInfo, rawUserArgs) {
const { cfg, dag } = strandsContext;
const { identifer, properties } = structTypeInfo;
const { properties } = structTypeInfo;

if (!(rawUserArgs.length === properties.length)) {
FES.userError('type error',
Expand Down Expand Up @@ -379,7 +379,8 @@ export function structConstructorNode(strandsContext, structTypeInfo, rawUserArg
});
const id = DAG.getOrCreateNode(dag, nodeData);
CFG.recordInBasicBlock(cfg, cfg.currentBlock, id);
return { id, dimension: properties.length, components: structTypeInfo.components };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was this part fixing?

// components stores specify the child nodes (members) of the struct for re-binding or inspection
return { id, dimension: properties.length, components: dependsOn };
}

export function functionCallNode(
Expand Down
3 changes: 3 additions & 0 deletions src/strands/strands_FES.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export function internalError(errorMessage) {
}

export function userError(errorType, errorMessage) {
if (typeof window !== 'undefined' && window.mockUserError) {
window.mockUserError(errorType, errorMessage);
}
const prefixedMessage = `[p5.strands ${errorType}]: ${errorMessage}`;
throw new Error(prefixedMessage);
}
80 changes: 64 additions & 16 deletions src/strands/strands_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,24 +94,67 @@ function getBuiltinGlobalNode(strandsContext, name) {
return node;
}

function installBuiltinGlobalAccessors(strandsContext) {
if (strandsContext._builtinGlobalsAccessorsInstalled) return
function installBuiltinGlobalAccessors(strandsContext, fn) {
if (strandsContext._builtinGlobalsAccessorsInstalled) return;

const getRuntimeP5Instance = () => strandsContext.renderer?._pInst || strandsContext.p5?.instance
const GraphicsProto = strandsContext.p5?.Graphics?.prototype;

// Targets where we want these accessors to be available
const targets = [window];
if (fn) targets.push(fn);
if (GraphicsProto) targets.push(GraphicsProto);

for (const name of Object.keys(BUILTIN_GLOBAL_SPECS)) {
const spec = BUILTIN_GLOBAL_SPECS[name]
Object.defineProperty(window, name, {
get: () => {
if (strandsContext.active) {
return getBuiltinGlobalNode(strandsContext, name);
}
const inst = getRuntimeP5Instance()
return spec.get(inst);
},
})
targets.forEach((target) => {
// Capture the original descriptor ONLY on the target itself (recursive-safe)
const originalDesc = Object.getOwnPropertyDescriptor(target, name);

Object.defineProperty(target, name, {
get: function() {
// If a shader is active, return the special uniform Node
if (strandsContext.active) {
return getBuiltinGlobalNode(strandsContext, name);
}

// Fallback: Get the original value to avoid infinite recursion
if (originalDesc) {
if (originalDesc.get) {
return originalDesc.get.call(this);
}
return originalDesc.value;
}

// If no original descriptor exists on this target (e.g. window in instance mode),
// fallback to the active p5 instance if we're not already on it.
const instance = strandsContext.renderer?._pInst || strandsContext.p5?.instance;
if (instance && instance !== target) {
return instance[name];
}

return undefined;
},
set: function(val) {
if (originalDesc && originalDesc.set) {
originalDesc.set.call(this, val);
} else {
// Shadow with a data property on the instance.
// This allows things like 'this.deltaTime = ...' to work in p5._draw
// without hitting the prototype's getter-only definition.
Object.defineProperty(this, name, {
value: val,
writable: true,
configurable: true,
enumerable: true
});
}
},
configurable: true,
enumerable: true
});
});
}
strandsContext._builtinGlobalsAccessorsInstalled = true

strandsContext._builtinGlobalsAccessorsInstalled = true;
}

//////////////////////////////////////////////
Expand Down Expand Up @@ -170,6 +213,11 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
}
}
}
//////////////////////////////////////////////
// Builtins, uniforms, variable constructors
//////////////////////////////////////////////
installBuiltinGlobalAccessors(strandsContext, fn);

//////////////////////////////////////////////
// Unique Functions
//////////////////////////////////////////////
Expand Down Expand Up @@ -587,7 +635,6 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName
return returnedNodeID;
}
export function createShaderHooksFunctions(strandsContext, fn, shader) {
installBuiltinGlobalAccessors(strandsContext)

// Add shader context to hooks before spreading
const vertexHooksWithContext = Object.fromEntries(
Expand Down Expand Up @@ -686,7 +733,8 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) {
const newDeps = returnedNode.dependsOn.slice();
for (let i = 0; i < expectedStructType.properties.length; i++) {
const expectedType = expectedStructType.properties[i].dataType;
const receivedNode = createStrandsNode(returnedNode.dependsOn[i], dag.dependsOn[retNode.id], strandsContext);
const depID = returnedNode.dependsOn[i];
const receivedNode = createStrandsNode(depID, dag.dimensions[depID], strandsContext);
newDeps[i] = enforceReturnTypeMatch(strandsContext, expectedType, receivedNode, hookType.name);
}
dag.dependsOn[retNode.id] = newDeps;
Expand Down
65 changes: 38 additions & 27 deletions test/unit/webgl/p5.Shader.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import p5 from '../../../src/app.js';
import { vi } from 'vitest';
import p5 from '../../../src/app.js';

const mockUserError = vi.fn();
vi.mock('../../../src/strands/strands_FES', () => ({
userError: (...args) => {
mockUserError(...args);
const prefixedMessage = `[p5.strands ${args[0]}]: ${args[1]}`;
throw new Error(prefixedMessage);
},
internalError: (msg) => { throw new Error(`[p5.strands internal error]: ${msg}`); }
}));
window.mockUserError = mockUserError;




suite('p5.Shader', function() {
var myp5;
Expand Down Expand Up @@ -420,6 +416,12 @@ suite('p5.Shader', function() {
});
});
suite('p5.strands', () => {
test('p5.prototype mouseX is a getter', () => {
const desc = Object.getOwnPropertyDescriptor(p5.prototype, 'mouseX');
assert.isDefined(desc, 'Descriptor should be defined');
assert.isDefined(desc.get, 'p5.prototype.mouseX should have a getter');
assert.isFunction(desc.get, 'p5.prototype.mouseX getter should be a function');
});
test('handles named function callbacks', () => {
myp5.createCanvas(5, 5, myp5.WEBGL);
function myMaterial() {
Expand Down Expand Up @@ -474,25 +476,34 @@ suite('p5.Shader', function() {
}).not.toThrowError();
});

test('returns numbers for builtin globals outside hooks and a strandNode when called inside hooks', () => {
myp5.createCanvas(5, 5, myp5.WEBGL);
myp5.baseMaterialShader().modify(() => {
myp5.getPixelInputs(inputs => {
const mxInHook = window.mouseX;
const wInHook = window.width;
assert.isTrue(mxInHook.isStrandsNode);
assert.isTrue(wInHook.isStrandsNode);
inputs.color = [1, 0, 0, 1];
return inputs;
});
}, { myp5 });
test('returns numbers for builtin globals outside hooks and a strandNode when called inside hooks', () => {
myp5.createCanvas(5, 5, myp5.WEBGL);
myp5.baseMaterialShader().modify(() => {
myp5.getPixelInputs(inputs => {
const mxInHook = window.mouseX;
const wInHook = window.width;
const mxInstInHook = myp5.mouseX;
const wInstInHook = myp5.width;
assert.isTrue(mxInHook.isStrandsNode, 'window.mouseX should be a StrandsNode');
assert.isTrue(wInHook.isStrandsNode, 'window.width should be a StrandsNode');
assert.isTrue(mxInstInHook.isStrandsNode, 'myp5.mouseX should be a StrandsNode');
assert.isTrue(wInstInHook.isStrandsNode, 'myp5.width should be a StrandsNode');
inputs.color = [1, 0, 0, 1];
return inputs;
});
}, { myp5 });

const mx = window.mouseX;
const w = window.width;
assert.isNumber(mx);
assert.isNumber(w);
assert.strictEqual(w, myp5.width);
});
const mx = window.mouseX;
const w = window.width;
const mxInst = myp5.mouseX;
const wInst = myp5.width;
assert.isNumber(mx);
assert.isNumber(w);
assert.isNumber(mxInst);
assert.isNumber(wInst);
assert.strictEqual(w, myp5.width);
assert.strictEqual(wInst, myp5.width);
});


test('handle custom uniform names with automatic values', () => {
Expand Down
Loading