diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index efe3908d75..1bcb90c525 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -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', @@ -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 }; + // 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( diff --git a/src/strands/strands_FES.js b/src/strands/strands_FES.js index 3af0aca90b..966fae168f 100644 --- a/src/strands/strands_FES.js +++ b/src/strands/strands_FES.js @@ -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); } \ No newline at end of file diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index ef4c0424c3..d07977f612 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -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; } ////////////////////////////////////////////// @@ -170,6 +213,11 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } } } + ////////////////////////////////////////////// + // Builtins, uniforms, variable constructors + ////////////////////////////////////////////// + installBuiltinGlobalAccessors(strandsContext, fn); + ////////////////////////////////////////////// // Unique Functions ////////////////////////////////////////////// @@ -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( @@ -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; diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 2556c1d25d..74602f6660 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -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; @@ -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() { @@ -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', () => {