Repository: playcanvas/engine
Pull request #6242 (2024-04-11 11:18:53)

The Preprocessor class now handles includes
The generic c-style preprocessor we use to preprocess shaders now handles includes, where the include line is replaced by the string specified in the includes dictionary. Accepted syntax:

```
#include "include-identifier"
```


Patch:
diff --git a/build.mjs b/build.mjs
index 256aa38d2..2a131bee0 100644
--- a/build.mjs
+++ b/build.mjs
@@ -25,7 +25,7 @@ const ENV_START_MATCHES = [
 
 const env = [];
 for (let i = 0; i < args.length; i++) {
-    if (ENV_START_MATCHES.some(match => args[i].startsWith(match))) {
+    if (ENV_START_MATCHES.some(match => args[i].startsWith(match)) && args[i - 1] !== '--environment') {
         env.push(`--environment ${args[i]}`);
         args.splice(i, 1);
         i--;
diff --git a/examples/src/examples/xr/xr-picking/example.mjs b/examples/src/examples/xr/xr-picking/example.mjs
index e1f30e110..a0f7c0560 100644
--- a/examples/src/examples/xr/xr-picking/example.mjs
+++ b/examples/src/examples/xr/xr-picking/example.mjs
@@ -132,7 +132,7 @@ if (app.xr.supported) {
         let candidateDist = Infinity;
 
         for (let i = 0; i < cubes.length; i++) {
-            const mesh = cubes[i].model.meshInstances[0];
+            const mesh = cubes[i].render.meshInstances[0];
 
             // check if mesh bounding box intersects with input source ray
             ray.set(inputSource.getOrigin(), inputSource.getDirection());
@@ -158,7 +158,6 @@ if (app.xr.supported) {
     });
 
     const tmpVec = new pc.Vec3();
-    const lineColor = new pc.Color(1, 1, 1);
 
     // on each app update
     // render input source rays as a line
@@ -167,10 +166,11 @@ if (app.xr.supported) {
             const inputSource = app.xr.input.inputSources[i];
             const direction = inputSource.getDirection();
             const origin = inputSource.getOrigin();
+            const color = inputSource.selecting ? pc.Color.GREEN : pc.Color.WHITE;
 
             tmpVec.copy(direction).mulScalar(100).add(origin);
 
-            app.drawLine(inputSource.getOrigin(), tmpVec, lineColor);
+            app.drawLine(inputSource.getOrigin(), tmpVec, color);
         }
     });
 } else {
diff --git a/package.json b/package.json
index d7cd61546..4c46e6510 100644
--- a/package.json
+++ b/package.json
@@ -150,7 +150,7 @@
     "docs": "typedoc",
     "lint": "eslint --ext .js,.mjs extras scripts src test utils rollup.config.mjs",
     "publint": "publint",
-    "serve": "serve build -l 51000",
+    "serve": "serve build -l 51000 --cors",
     "test": "mocha --recursive --require test/fixtures.mjs",
     "test:coverage": "c8 npm test",
     "test:karma": "karma start tests/karma.conf.cjs -- --single-run",
diff --git a/src/core/preprocessor.js b/src/core/preprocessor.js
index 0a083e188..269a687c5 100644
--- a/src/core/preprocessor.js
+++ b/src/core/preprocessor.js
@@ -4,7 +4,7 @@ import { Debug } from './debug.js';
 const TRACEID = 'Preprocessor';
 
 // accepted keywords
-const KEYWORD = /[ \t]*#(ifn?def|if|endif|else|elif|define|undef|extension)/g;
+const KEYWORD = /[ \t]*#(ifn?def|if|endif|else|elif|define|undef|extension|include)/g;
 
 // #define EXPRESSION
 const DEFINE = /define[ \t]+([^\n]+)\r?(?:\n|$)/g;
@@ -30,6 +30,9 @@ const DEFINED = /(!|\s)?defined\(([\w-]+)\)/;
 // currently unsupported characters in the expression: | & < > = + -
 const INVALID = /[><=|&+-]/g;
 
+// #include "identifier"
+const INCLUDE = /include[ \t]+"([\w-]+)"\r?(?:\n|$)/g;
+
 /**
  * Pure static class implementing subset of C-style preprocessor.
  * inspired by: https://github.com/dcodeIO/Preprocessor.js
@@ -41,10 +44,12 @@ class Preprocessor {
      * Run c-like preprocessor on the source code, and resolves the code based on the defines and ifdefs
      *
      * @param {string} source - The source code to work on.
+     * @param {Map<string, string>} [includes] - An object containing key-value pairs of include names and their
+     * content.
      * @param {boolean} [stripUnusedColorAttachments] - If true, strips unused color attachments.
      * @returns {string|null} Returns preprocessed source code, or null in case of error.
      */
-    static run(source, stripUnusedColorAttachments = false) {
+    static run(source, includes = new Map(), stripUnusedColorAttachments = false) {
 
         // strips comments, handles // and many cases of /*
         source = source.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1');
@@ -76,7 +81,7 @@ class Preprocessor {
         }
 
         // preprocess defines / ifdefs ..
-        source = this._preprocess(source, defines);
+        source = this._preprocess(source, defines, includes);
 
         // extract defines that evaluate to an integer number
         const intDefines = new Map();
@@ -131,9 +136,11 @@ class Preprocessor {
      * @param {Map<string, string>} defines - Supplied defines which are used in addition to those
      * defined in the source code. Maps a define name to its value. Note that the map is modified
      * by the function.
+     * @param {Map<string, string>} [includes] - An object containing key-value pairs of include names and their
+     * content.
      * @returns {string} Returns preprocessed source code.
      */
-    static _preprocess(source, defines = new Map()) {
+    static _preprocess(source, defines = new Map(), includes) {
 
         const originalSource = source;
 
@@ -301,6 +308,36 @@ class Preprocessor {
 
                     break;
                 }
+
+                case 'include': {
+                    // match the include
+                    INCLUDE.lastIndex = match.index;
+                    const include = INCLUDE.exec(source);
+                    error ||= include === null;
+                    Debug.assert(include, `Invalid [${keyword}]: ${source.substring(match.index, match.index + 100)}...`);
+                    const identifier = include[1].trim();
+
+                    // are we inside if-blocks that are accepted
+                    const keep = Preprocessor._keep(stack);
+
+                    if (keep) {
+
+                        // cut out the include line and replace it with the included string
+                        const includeSource = includes?.get(identifier);
+                        if (includeSource) {
+                            source = source.substring(0, include.index - 1) + includeSource + source.substring(INCLUDE.lastIndex);
+
+                            // process the just included test
+                            KEYWORD.lastIndex = include.index;
+                        } else {
+                            console.error(`Include not found: ${identifier}`);
+                            error = true;
+                        }
+                    }
+
+                    Debug.trace(TRACEID, `${keyword}: [${identifier}] ${keep ? "" : "IGNORED"}`);
+                    break;
+                }
             }
         }
 
diff --git a/src/framework/handlers/script.js b/src/framework/handlers/script.js
index eb59fdcf3..9206eed12 100644
--- a/src/framework/handlers/script.js
+++ b/src/framework/handlers/script.js
@@ -155,9 +155,11 @@ class ScriptHandler extends ResourceHandler {
                 const extendsScriptType = scriptClass.prototype instanceof ScriptType;
 
                 if (extendsScriptType) {
-                    if (scriptClass.attributes) {
+
+                    // Check if attributes is defined directly on the class and not inherited
+                    if (scriptClass.hasOwnProperty('attributes')) {
                         const attributes = new ScriptAttributes(scriptClass);
-                        for (const key in script.attributes) {
+                        for (const key in scriptClass.attributes) {
                             attributes.add(key, scriptClass.attributes[key]);
                         }
                         scriptClass.attributes = attributes;
diff --git a/src/platform/graphics/shader.js b/src/platform/graphics/shader.js
index 195413102..f191e6032 100644
--- a/src/platform/graphics/shader.js
+++ b/src/platform/graphics/shader.js
@@ -112,7 +112,7 @@ class Shader {
             // Note: this is only needed for iOS 15 on WebGL2 where there seems to be a bug where color attachments that are not
             // written to generate metal linking errors. This is fixed on iOS 16, and iOS 14 does not support WebGL2.
             const stripUnusedColorAttachments = graphicsDevice.isWebGL2 && (platform.name === 'osx' || platform.name === 'ios');
-            definition.fshader = Preprocessor.run(definition.fshader, stripUnusedColorAttachments);
+            definition.fshader = Preprocessor.run(definition.fshader, undefined, stripUnusedColorAttachments);
         }
 
         this.impl = graphicsDevice.createShaderImpl(this);
diff --git a/src/platform/graphics/vertex-buffer.js b/src/platform/graphics/vertex-buffer.js
index 834bcaa96..2e393430f 100644
--- a/src/platform/graphics/vertex-buffer.js
+++ b/src/platform/graphics/vertex-buffer.js
@@ -11,6 +11,8 @@ let id = 0;
  * @category Graphics
  */
 class VertexBuffer {
+    usage = BUFFER_STATIC;
+
     /**
      * Create a new VertexBuffer instance.
      *


Test patch:
diff --git a/test/core/preprocessor.test.mjs b/test/core/preprocessor.test.mjs
index 0299a7206..cf26a6724 100644
--- a/test/core/preprocessor.test.mjs
+++ b/test/core/preprocessor.test.mjs
@@ -4,6 +4,15 @@ import { expect } from 'chai';
 
 describe('Preprocessor', function () {
 
+    const includes = new Map([
+        ['inc1', `
+            block1
+            #ifdef FEATURE2
+                nested
+            #endif
+        `],
+        ['inc2', 'block2']
+    ]);
     const srcData = `
         
         #define FEATURE1
@@ -11,6 +20,7 @@ describe('Preprocessor', function () {
 
         #ifdef FEATURE1
             TEST1
+            #include "inc1"
         #endif
 
         #if defined(FEATURE1)
@@ -23,12 +33,13 @@ describe('Preprocessor', function () {
             #endif
         #endif
 
-        #ifndef UNKOWN
+        #ifndef UNKNOWN
             TEST4
         #endif
 
         #if defined (UNKNOWN)
             TEST5
+            #include "inc2"
         #else
             TEST6
         #endif
@@ -64,70 +75,82 @@ describe('Preprocessor', function () {
     `;
 
     it('returns false for MORPH_A', function () {
-        expect(Preprocessor.run(srcData).includes('MORPH_A')).to.equal(false);
+        expect(Preprocessor.run(srcData, includes).includes('MORPH_A')).to.equal(false);
     });
 
     it('returns false for MORPH_B', function () {
-        expect(Preprocessor.run(srcData).includes('MORPH_B')).to.equal(false);
+        expect(Preprocessor.run(srcData, includes).includes('MORPH_B')).to.equal(false);
     });
 
     it('returns true for $', function () {
-        expect(Preprocessor.run(srcData).includes('$')).to.equal(true);
+        expect(Preprocessor.run(srcData, includes).includes('$')).to.equal(true);
     });
 
     it('returns true for TEST1', function () {
-        expect(Preprocessor.run(srcData).includes('TEST1')).to.equal(true);
+        expect(Preprocessor.run(srcData, includes).includes('TEST1')).to.equal(true);
     });
 
     it('returns true for TEST2', function () {
-        expect(Preprocessor.run(srcData).includes('TEST2')).to.equal(true);
+        expect(Preprocessor.run(srcData, includes).includes('TEST2')).to.equal(true);
     });
 
     it('returns true for TEST3', function () {
-        expect(Preprocessor.run(srcData).includes('TEST3')).to.equal(true);
+        expect(Preprocessor.run(srcData, includes).includes('TEST3')).to.equal(true);
     });
 
     it('returns true for TEST4', function () {
-        expect(Preprocessor.run(srcData).includes('TEST4')).to.equal(true);
+        expect(Preprocessor.run(srcData, includes).includes('TEST4')).to.equal(true);
     });
 
     it('returns false for TEST5', function () {
-        expect(Preprocessor.run(srcData).includes('TEST5')).to.equal(false);
+        expect(Preprocessor.run(srcData, includes).includes('TEST5')).to.equal(false);
     });
 
     it('returns true for TEST6', function () {
-        expect(Preprocessor.run(srcData).includes('TEST6')).to.equal(true);
+        expect(Preprocessor.run(srcData, includes).includes('TEST6')).to.equal(true);
     });
 
     it('returns false for TEST7', function () {
-        expect(Preprocessor.run(srcData).includes('TEST7')).to.equal(false);
+        expect(Preprocessor.run(srcData, includes).includes('TEST7')).to.equal(false);
     });
 
     it('returns false for TEST8', function () {
-        expect(Preprocessor.run(srcData).includes('TEST8')).to.equal(false);
+        expect(Preprocessor.run(srcData, includes).includes('TEST8')).to.equal(false);
     });
 
     it('returns false for TEST9', function () {
-        expect(Preprocessor.run(srcData).includes('TEST9')).to.equal(false);
+        expect(Preprocessor.run(srcData, includes).includes('TEST9')).to.equal(false);
     });
 
     it('returns true for TEST10', function () {
-        expect(Preprocessor.run(srcData).includes('TEST10')).to.equal(true);
+        expect(Preprocessor.run(srcData, includes).includes('TEST10')).to.equal(true);
     });
 
     it('returns false for TEST11', function () {
-        expect(Preprocessor.run(srcData).includes('TEST11')).to.equal(false);
+        expect(Preprocessor.run(srcData, includes).includes('TEST11')).to.equal(false);
     });
 
     it('returns false for TEST12', function () {
-        expect(Preprocessor.run(srcData).includes('TEST12')).to.equal(false);
+        expect(Preprocessor.run(srcData, includes).includes('TEST12')).to.equal(false);
     });
 
     it('returns true for TEST13', function () {
-        expect(Preprocessor.run(srcData).includes('TEST13')).to.equal(true);
+        expect(Preprocessor.run(srcData, includes).includes('TEST13')).to.equal(true);
     });
 
     it('returns false for TEST14', function () {
-        expect(Preprocessor.run(srcData).includes('TEST14')).to.equal(false);
+        expect(Preprocessor.run(srcData, includes).includes('TEST14')).to.equal(false);
+    });
+
+    it('returns true for INC1', function () {
+        expect(Preprocessor.run(srcData, includes).includes('block1')).to.equal(true);
+    });
+
+    it('returns false for INC2', function () {
+        expect(Preprocessor.run(srcData, includes).includes('block2')).to.equal(false);
+    });
+
+    it('returns true for nested', function () {
+        expect(Preprocessor.run(srcData, includes).includes('nested')).to.equal(true);
     });
 });