diff --git a/package-lock.json b/package-lock.json index b237d49..9dc087e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -294,27 +294,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.9" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -356,9 +356,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", - "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "dev": true, "license": "MIT", "dependencies": { @@ -369,15 +369,15 @@ } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -413,9 +413,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", - "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, "license": "MIT", "dependencies": { @@ -5596,9 +5596,9 @@ } }, "node_modules/fdir": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", - "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -8300,13 +8300,13 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", - "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.3", + "fdir": "^6.4.4", "picomatch": "^4.0.2" }, "engines": { @@ -8789,15 +8789,18 @@ } }, "node_modules/vite": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz", - "integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" diff --git a/public/glyphs/OpenSans-Regular.json b/public/glyphs/OpenSans-Regular.json new file mode 100644 index 0000000..ab00b3f --- /dev/null +++ b/public/glyphs/OpenSans-Regular.json @@ -0,0 +1,1562 @@ +{ + "pages": [ + "OpenSans-Regular.png" + ], + "chars": [ + { + "id": 124, + "index": 95, + "char": "|", + "width": 9, + "height": 53, + "xoffset": 8, + "yoffset": 17, + "xadvance": 26, + "chnl": 15, + "x": 0, + "y": 0, + "page": 0 + }, + { + "id": 106, + "index": 77, + "char": "j", + "width": 17, + "height": 52, + "xoffset": -6, + "yoffset": 18, + "xadvance": 12, + "chnl": 15, + "x": 10, + "y": 0, + "page": 0 + }, + { + "id": 74, + "index": 45, + "char": "J", + "width": 18, + "height": 48, + "xoffset": -7, + "yoffset": 19, + "xadvance": 13, + "chnl": 15, + "x": 28, + "y": 0, + "page": 0 + }, + { + "id": 81, + "index": 52, + "char": "Q", + "width": 37, + "height": 48, + "xoffset": 0, + "yoffset": 19, + "xadvance": 37, + "chnl": 15, + "x": 47, + "y": 0, + "page": 0 + }, + { + "id": 87, + "index": 58, + "char": "W", + "width": 48, + "height": 40, + "xoffset": -2, + "yoffset": 19, + "xadvance": 43, + "chnl": 15, + "x": 0, + "y": 54, + "page": 0 + }, + { + "id": 40, + "index": 11, + "char": "(", + "width": 17, + "height": 47, + "xoffset": -1, + "yoffset": 19, + "xadvance": 14, + "chnl": 15, + "x": 85, + "y": 0, + "page": 0 + }, + { + "id": 41, + "index": 12, + "char": ")", + "width": 17, + "height": 47, + "xoffset": -2, + "yoffset": 19, + "xadvance": 14, + "chnl": 15, + "x": 85, + "y": 48, + "page": 0 + }, + { + "id": 91, + "index": 62, + "char": "[", + "width": 16, + "height": 47, + "xoffset": 1, + "yoffset": 19, + "xadvance": 15, + "chnl": 15, + "x": 0, + "y": 95, + "page": 0 + }, + { + "id": 93, + "index": 64, + "char": "]", + "width": 16, + "height": 47, + "xoffset": -2, + "yoffset": 19, + "xadvance": 15, + "chnl": 15, + "x": 17, + "y": 95, + "page": 0 + }, + { + "id": 123, + "index": 94, + "char": "{", + "width": 21, + "height": 47, + "xoffset": -2, + "yoffset": 19, + "xadvance": 18, + "chnl": 15, + "x": 34, + "y": 95, + "page": 0 + }, + { + "id": 125, + "index": 96, + "char": "}", + "width": 21, + "height": 47, + "xoffset": -1, + "yoffset": 19, + "xadvance": 18, + "chnl": 15, + "x": 56, + "y": 49, + "page": 0 + }, + { + "id": 197, + "index": 135, + "char": "Å", + "width": 36, + "height": 47, + "xoffset": -3, + "yoffset": 12, + "xadvance": 30, + "chnl": 15, + "x": 103, + "y": 0, + "page": 0 + }, + { + "id": 229, + "index": 167, + "char": "å", + "width": 26, + "height": 45, + "xoffset": -1, + "yoffset": 15, + "xadvance": 26, + "chnl": 15, + "x": 56, + "y": 97, + "page": 0 + }, + { + "id": 36, + "index": 7, + "char": "$", + "width": 27, + "height": 44, + "xoffset": 0, + "yoffset": 17, + "xadvance": 27, + "chnl": 15, + "x": 83, + "y": 96, + "page": 0 + }, + { + "id": 64, + "index": 35, + "char": "@", + "width": 43, + "height": 44, + "xoffset": 0, + "yoffset": 19, + "xadvance": 42, + "chnl": 15, + "x": 140, + "y": 0, + "page": 0 + }, + { + "id": 198, + "index": 136, + "char": "Æ", + "width": 44, + "height": 40, + "xoffset": -3, + "yoffset": 19, + "xadvance": 41, + "chnl": 15, + "x": 103, + "y": 48, + "page": 0 + }, + { + "id": 103, + "index": 74, + "char": "g", + "width": 30, + "height": 43, + "xoffset": -2, + "yoffset": 27, + "xadvance": 26, + "chnl": 15, + "x": 148, + "y": 45, + "page": 0 + }, + { + "id": 112, + "index": 83, + "char": "p", + "width": 28, + "height": 43, + "xoffset": 1, + "yoffset": 27, + "xadvance": 29, + "chnl": 15, + "x": 111, + "y": 89, + "page": 0 + }, + { + "id": 113, + "index": 84, + "char": "q", + "width": 28, + "height": 43, + "xoffset": 0, + "yoffset": 27, + "xadvance": 29, + "chnl": 15, + "x": 140, + "y": 89, + "page": 0 + }, + { + "id": 98, + "index": 69, + "char": "b", + "width": 28, + "height": 42, + "xoffset": 1, + "yoffset": 17, + "xadvance": 29, + "chnl": 15, + "x": 0, + "y": 143, + "page": 0 + }, + { + "id": 100, + "index": 71, + "char": "d", + "width": 28, + "height": 42, + "xoffset": 0, + "yoffset": 17, + "xadvance": 29, + "chnl": 15, + "x": 29, + "y": 143, + "page": 0 + }, + { + "id": 102, + "index": 73, + "char": "f", + "width": 23, + "height": 42, + "xoffset": -2, + "yoffset": 17, + "xadvance": 16, + "chnl": 15, + "x": 58, + "y": 143, + "page": 0 + }, + { + "id": 104, + "index": 75, + "char": "h", + "width": 27, + "height": 42, + "xoffset": 1, + "yoffset": 17, + "xadvance": 29, + "chnl": 15, + "x": 82, + "y": 143, + "page": 0 + }, + { + "id": 107, + "index": 78, + "char": "k", + "width": 26, + "height": 42, + "xoffset": 1, + "yoffset": 17, + "xadvance": 25, + "chnl": 15, + "x": 110, + "y": 141, + "page": 0 + }, + { + "id": 108, + "index": 79, + "char": "l", + "width": 10, + "height": 42, + "xoffset": 1, + "yoffset": 17, + "xadvance": 12, + "chnl": 15, + "x": 169, + "y": 89, + "page": 0 + }, + { + "id": 109, + "index": 80, + "char": "m", + "width": 42, + "height": 32, + "xoffset": 1, + "yoffset": 27, + "xadvance": 44, + "chnl": 15, + "x": 137, + "y": 133, + "page": 0 + }, + { + "id": 121, + "index": 92, + "char": "y", + "width": 30, + "height": 42, + "xoffset": -3, + "yoffset": 28, + "xadvance": 24, + "chnl": 15, + "x": 184, + "y": 0, + "page": 0 + }, + { + "id": 230, + "index": 168, + "char": "æ", + "width": 42, + "height": 32, + "xoffset": -1, + "yoffset": 27, + "xadvance": 41, + "chnl": 15, + "x": 0, + "y": 186, + "page": 0 + }, + { + "id": 216, + "index": 154, + "char": "Ø", + "width": 37, + "height": 42, + "xoffset": 0, + "yoffset": 18, + "xadvance": 37, + "chnl": 15, + "x": 137, + "y": 166, + "page": 0 + }, + { + "id": 38, + "index": 9, + "char": "&", + "width": 37, + "height": 41, + "xoffset": 0, + "yoffset": 19, + "xadvance": 34, + "chnl": 15, + "x": 175, + "y": 166, + "page": 0 + }, + { + "id": 48, + "index": 19, + "char": "0", + "width": 28, + "height": 41, + "xoffset": -1, + "yoffset": 19, + "xadvance": 27, + "chnl": 15, + "x": 179, + "y": 45, + "page": 0 + }, + { + "id": 57, + "index": 28, + "char": "9", + "width": 28, + "height": 41, + "xoffset": -1, + "yoffset": 19, + "xadvance": 27, + "chnl": 15, + "x": 180, + "y": 87, + "page": 0 + }, + { + "id": 63, + "index": 34, + "char": "?", + "width": 24, + "height": 41, + "xoffset": -2, + "yoffset": 19, + "xadvance": 20, + "chnl": 15, + "x": 215, + "y": 0, + "page": 0 + }, + { + "id": 79, + "index": 50, + "char": "O", + "width": 37, + "height": 41, + "xoffset": 0, + "yoffset": 19, + "xadvance": 37, + "chnl": 15, + "x": 0, + "y": 219, + "page": 0 + }, + { + "id": 105, + "index": 76, + "char": "i", + "width": 11, + "height": 41, + "xoffset": 1, + "yoffset": 18, + "xadvance": 12, + "chnl": 15, + "x": 38, + "y": 219, + "page": 0 + }, + { + "id": 119, + "index": 90, + "char": "w", + "width": 41, + "height": 31, + "xoffset": -2, + "yoffset": 28, + "xadvance": 36, + "chnl": 15, + "x": 43, + "y": 186, + "page": 0 + }, + { + "id": 33, + "index": 4, + "char": "!", + "width": 11, + "height": 40, + "xoffset": 0, + "yoffset": 19, + "xadvance": 12, + "chnl": 15, + "x": 50, + "y": 218, + "page": 0 + }, + { + "id": 92, + "index": 63, + "char": "\\", + "width": 22, + "height": 40, + "xoffset": -3, + "yoffset": 19, + "xadvance": 17, + "chnl": 15, + "x": 215, + "y": 42, + "page": 0 + }, + { + "id": 35, + "index": 6, + "char": "#", + "width": 34, + "height": 40, + "xoffset": -2, + "yoffset": 19, + "xadvance": 30, + "chnl": 15, + "x": 62, + "y": 218, + "page": 0 + }, + { + "id": 37, + "index": 8, + "char": "%", + "width": 40, + "height": 40, + "xoffset": -1, + "yoffset": 19, + "xadvance": 39, + "chnl": 15, + "x": 97, + "y": 209, + "page": 0 + }, + { + "id": 47, + "index": 18, + "char": "/", + "width": 22, + "height": 40, + "xoffset": -3, + "yoffset": 19, + "xadvance": 17, + "chnl": 15, + "x": 213, + "y": 83, + "page": 0 + }, + { + "id": 49, + "index": 20, + "char": "1", + "width": 18, + "height": 40, + "xoffset": 1, + "yoffset": 19, + "xadvance": 27, + "chnl": 15, + "x": 209, + "y": 124, + "page": 0 + }, + { + "id": 50, + "index": 21, + "char": "2", + "width": 28, + "height": 40, + "xoffset": -1, + "yoffset": 19, + "xadvance": 27, + "chnl": 15, + "x": 138, + "y": 209, + "page": 0 + }, + { + "id": 51, + "index": 22, + "char": "3", + "width": 28, + "height": 40, + "xoffset": -1, + "yoffset": 19, + "xadvance": 27, + "chnl": 15, + "x": 167, + "y": 209, + "page": 0 + }, + { + "id": 52, + "index": 23, + "char": "4", + "width": 31, + "height": 40, + "xoffset": -2, + "yoffset": 19, + "xadvance": 27, + "chnl": 15, + "x": 196, + "y": 208, + "page": 0 + }, + { + "id": 53, + "index": 24, + "char": "5", + "width": 27, + "height": 40, + "xoffset": 0, + "yoffset": 19, + "xadvance": 27, + "chnl": 15, + "x": 240, + "y": 0, + "page": 0 + }, + { + "id": 54, + "index": 25, + "char": "6", + "width": 28, + "height": 40, + "xoffset": 0, + "yoffset": 19, + "xadvance": 27, + "chnl": 15, + "x": 238, + "y": 42, + "page": 0 + }, + { + "id": 55, + "index": 26, + "char": "7", + "width": 28, + "height": 40, + "xoffset": -1, + "yoffset": 19, + "xadvance": 27, + "chnl": 15, + "x": 213, + "y": 165, + "page": 0 + }, + { + "id": 56, + "index": 27, + "char": "8", + "width": 28, + "height": 40, + "xoffset": -1, + "yoffset": 19, + "xadvance": 27, + "chnl": 15, + "x": 228, + "y": 124, + "page": 0 + }, + { + "id": 65, + "index": 36, + "char": "A", + "width": 36, + "height": 40, + "xoffset": -3, + "yoffset": 19, + "xadvance": 30, + "chnl": 15, + "x": 228, + "y": 206, + "page": 0 + }, + { + "id": 66, + "index": 37, + "char": "B", + "width": 29, + "height": 40, + "xoffset": 2, + "yoffset": 19, + "xadvance": 30, + "chnl": 15, + "x": 236, + "y": 83, + "page": 0 + }, + { + "id": 67, + "index": 38, + "char": "C", + "width": 31, + "height": 40, + "xoffset": 0, + "yoffset": 19, + "xadvance": 30, + "chnl": 15, + "x": 0, + "y": 261, + "page": 0 + }, + { + "id": 68, + "index": 39, + "char": "D", + "width": 33, + "height": 40, + "xoffset": 2, + "yoffset": 19, + "xadvance": 34, + "chnl": 15, + "x": 32, + "y": 261, + "page": 0 + }, + { + "id": 69, + "index": 40, + "char": "E", + "width": 25, + "height": 40, + "xoffset": 2, + "yoffset": 19, + "xadvance": 26, + "chnl": 15, + "x": 242, + "y": 165, + "page": 0 + }, + { + "id": 70, + "index": 41, + "char": "F", + "width": 25, + "height": 40, + "xoffset": 2, + "yoffset": 19, + "xadvance": 24, + "chnl": 15, + "x": 66, + "y": 259, + "page": 0 + }, + { + "id": 71, + "index": 42, + "char": "G", + "width": 34, + "height": 40, + "xoffset": 0, + "yoffset": 19, + "xadvance": 34, + "chnl": 15, + "x": 92, + "y": 259, + "page": 0 + }, + { + "id": 72, + "index": 43, + "char": "H", + "width": 31, + "height": 40, + "xoffset": 2, + "yoffset": 19, + "xadvance": 35, + "chnl": 15, + "x": 228, + "y": 247, + "page": 0 + }, + { + "id": 73, + "index": 44, + "char": "I", + "width": 10, + "height": 40, + "xoffset": 2, + "yoffset": 19, + "xadvance": 13, + "chnl": 15, + "x": 257, + "y": 124, + "page": 0 + }, + { + "id": 75, + "index": 46, + "char": "K", + "width": 30, + "height": 40, + "xoffset": 2, + "yoffset": 19, + "xadvance": 29, + "chnl": 15, + "x": 196, + "y": 249, + "page": 0 + }, + { + "id": 76, + "index": 47, + "char": "L", + "width": 25, + "height": 40, + "xoffset": 2, + "yoffset": 19, + "xadvance": 25, + "chnl": 15, + "x": 127, + "y": 250, + "page": 0 + }, + { + "id": 77, + "index": 48, + "char": "M", + "width": 39, + "height": 40, + "xoffset": 2, + "yoffset": 19, + "xadvance": 42, + "chnl": 15, + "x": 153, + "y": 250, + "page": 0 + }, + { + "id": 78, + "index": 49, + "char": "N", + "width": 32, + "height": 40, + "xoffset": 2, + "yoffset": 19, + "xadvance": 35, + "chnl": 15, + "x": 266, + "y": 83, + "page": 0 + }, + { + "id": 80, + "index": 51, + "char": "P", + "width": 27, + "height": 40, + "xoffset": 2, + "yoffset": 19, + "xadvance": 28, + "chnl": 15, + "x": 267, + "y": 41, + "page": 0 + }, + { + "id": 82, + "index": 53, + "char": "R", + "width": 30, + "height": 40, + "xoffset": 2, + "yoffset": 19, + "xadvance": 29, + "chnl": 15, + "x": 268, + "y": 0, + "page": 0 + }, + { + "id": 83, + "index": 54, + "char": "S", + "width": 27, + "height": 40, + "xoffset": -1, + "yoffset": 19, + "xadvance": 26, + "chnl": 15, + "x": 268, + "y": 124, + "page": 0 + }, + { + "id": 84, + "index": 55, + "char": "T", + "width": 31, + "height": 40, + "xoffset": -3, + "yoffset": 19, + "xadvance": 26, + "chnl": 15, + "x": 268, + "y": 165, + "page": 0 + }, + { + "id": 85, + "index": 56, + "char": "U", + "width": 32, + "height": 40, + "xoffset": 1, + "yoffset": 19, + "xadvance": 34, + "chnl": 15, + "x": 265, + "y": 206, + "page": 0 + }, + { + "id": 86, + "index": 57, + "char": "V", + "width": 34, + "height": 40, + "xoffset": -3, + "yoffset": 19, + "xadvance": 28, + "chnl": 15, + "x": 260, + "y": 247, + "page": 0 + }, + { + "id": 88, + "index": 59, + "char": "X", + "width": 33, + "height": 40, + "xoffset": -3, + "yoffset": 19, + "xadvance": 27, + "chnl": 15, + "x": 296, + "y": 124, + "page": 0 + }, + { + "id": 89, + "index": 60, + "char": "Y", + "width": 32, + "height": 40, + "xoffset": -3, + "yoffset": 19, + "xadvance": 26, + "chnl": 15, + "x": 295, + "y": 41, + "page": 0 + }, + { + "id": 90, + "index": 61, + "char": "Z", + "width": 29, + "height": 40, + "xoffset": -1, + "yoffset": 19, + "xadvance": 27, + "chnl": 15, + "x": 299, + "y": 0, + "page": 0 + }, + { + "id": 59, + "index": 30, + "char": ";", + "width": 13, + "height": 38, + "xoffset": -2, + "yoffset": 27, + "xadvance": 12, + "chnl": 15, + "x": 299, + "y": 82, + "page": 0 + }, + { + "id": 116, + "index": 87, + "char": "t", + "width": 21, + "height": 37, + "xoffset": -2, + "yoffset": 22, + "xadvance": 17, + "chnl": 15, + "x": 313, + "y": 82, + "page": 0 + }, + { + "id": 248, + "index": 186, + "char": "ø", + "width": 29, + "height": 34, + "xoffset": 0, + "yoffset": 26, + "xadvance": 28, + "chnl": 15, + "x": 300, + "y": 165, + "page": 0 + }, + { + "id": 58, + "index": 29, + "char": ":", + "width": 11, + "height": 32, + "xoffset": 0, + "yoffset": 27, + "xadvance": 12, + "chnl": 15, + "x": 180, + "y": 129, + "page": 0 + }, + { + "id": 97, + "index": 68, + "char": "a", + "width": 26, + "height": 32, + "xoffset": -1, + "yoffset": 27, + "xadvance": 26, + "chnl": 15, + "x": 300, + "y": 200, + "page": 0 + }, + { + "id": 99, + "index": 70, + "char": "c", + "width": 24, + "height": 32, + "xoffset": 0, + "yoffset": 27, + "xadvance": 23, + "chnl": 15, + "x": 298, + "y": 233, + "page": 0 + }, + { + "id": 101, + "index": 72, + "char": "e", + "width": 27, + "height": 32, + "xoffset": 0, + "yoffset": 27, + "xadvance": 26, + "chnl": 15, + "x": 295, + "y": 266, + "page": 0 + }, + { + "id": 110, + "index": 81, + "char": "n", + "width": 27, + "height": 32, + "xoffset": 1, + "yoffset": 27, + "xadvance": 29, + "chnl": 15, + "x": 0, + "y": 302, + "page": 0 + }, + { + "id": 111, + "index": 82, + "char": "o", + "width": 29, + "height": 32, + "xoffset": 0, + "yoffset": 27, + "xadvance": 28, + "chnl": 15, + "x": 28, + "y": 302, + "page": 0 + }, + { + "id": 114, + "index": 85, + "char": "r", + "width": 20, + "height": 32, + "xoffset": 1, + "yoffset": 27, + "xadvance": 19, + "chnl": 15, + "x": 58, + "y": 302, + "page": 0 + }, + { + "id": 115, + "index": 86, + "char": "s", + "width": 24, + "height": 32, + "xoffset": -1, + "yoffset": 27, + "xadvance": 22, + "chnl": 15, + "x": 79, + "y": 300, + "page": 0 + }, + { + "id": 117, + "index": 88, + "char": "u", + "width": 27, + "height": 32, + "xoffset": 1, + "yoffset": 28, + "xadvance": 29, + "chnl": 15, + "x": 104, + "y": 300, + "page": 0 + }, + { + "id": 118, + "index": 89, + "char": "v", + "width": 29, + "height": 31, + "xoffset": -3, + "yoffset": 28, + "xadvance": 23, + "chnl": 15, + "x": 132, + "y": 299, + "page": 0 + }, + { + "id": 120, + "index": 91, + "char": "x", + "width": 29, + "height": 31, + "xoffset": -2, + "yoffset": 28, + "xadvance": 25, + "chnl": 15, + "x": 162, + "y": 299, + "page": 0 + }, + { + "id": 122, + "index": 93, + "char": "z", + "width": 24, + "height": 31, + "xoffset": -1, + "yoffset": 28, + "xadvance": 22, + "chnl": 15, + "x": 192, + "y": 299, + "page": 0 + }, + { + "id": 43, + "index": 14, + "char": "+", + "width": 28, + "height": 29, + "xoffset": -1, + "yoffset": 25, + "xadvance": 27, + "chnl": 15, + "x": 217, + "y": 299, + "page": 0 + }, + { + "id": 60, + "index": 31, + "char": "<", + "width": 28, + "height": 29, + "xoffset": -1, + "yoffset": 25, + "xadvance": 27, + "chnl": 15, + "x": 246, + "y": 299, + "page": 0 + }, + { + "id": 62, + "index": 33, + "char": ">", + "width": 28, + "height": 29, + "xoffset": -1, + "yoffset": 25, + "xadvance": 27, + "chnl": 15, + "x": 275, + "y": 299, + "page": 0 + }, + { + "id": 94, + "index": 65, + "char": "^", + "width": 29, + "height": 27, + "xoffset": -1, + "yoffset": 19, + "xadvance": 27, + "chnl": 15, + "x": 304, + "y": 299, + "page": 0 + }, + { + "id": 42, + "index": 13, + "char": "*", + "width": 28, + "height": 27, + "xoffset": -1, + "yoffset": 17, + "xadvance": 26, + "chnl": 15, + "x": 335, + "y": 0, + "page": 0 + }, + { + "id": 61, + "index": 32, + "char": "=", + "width": 28, + "height": 18, + "xoffset": 0, + "yoffset": 30, + "xadvance": 27, + "chnl": 15, + "x": 335, + "y": 28, + "page": 0 + }, + { + "id": 126, + "index": 97, + "char": "~", + "width": 28, + "height": 12, + "xoffset": -1, + "yoffset": 33, + "xadvance": 27, + "chnl": 15, + "x": 335, + "y": 47, + "page": 0 + }, + { + "id": 95, + "index": 66, + "char": "_", + "width": 27, + "height": 9, + "xoffset": -3, + "yoffset": 57, + "xadvance": 21, + "chnl": 15, + "x": 227, + "y": 288, + "page": 0 + }, + { + "id": 34, + "index": 5, + "char": "\"", + "width": 19, + "height": 18, + "xoffset": 0, + "yoffset": 19, + "xadvance": 19, + "chnl": 15, + "x": 328, + "y": 60, + "page": 0 + }, + { + "id": 39, + "index": 10, + "char": "'", + "width": 10, + "height": 18, + "xoffset": 0, + "yoffset": 19, + "xadvance": 10, + "chnl": 15, + "x": 85, + "y": 186, + "page": 0 + }, + { + "id": 44, + "index": 15, + "char": ",", + "width": 13, + "height": 18, + "xoffset": -1, + "yoffset": 48, + "xadvance": 12, + "chnl": 15, + "x": 348, + "y": 60, + "page": 0 + }, + { + "id": 45, + "index": 16, + "char": "-", + "width": 17, + "height": 9, + "xoffset": -1, + "yoffset": 39, + "xadvance": 15, + "chnl": 15, + "x": 255, + "y": 288, + "page": 0 + }, + { + "id": 96, + "index": 67, + "char": "`", + "width": 15, + "height": 14, + "xoffset": -1, + "yoffset": 17, + "xadvance": 13, + "chnl": 15, + "x": 192, + "y": 129, + "page": 0 + }, + { + "id": 46, + "index": 17, + "char": ".", + "width": 11, + "height": 12, + "xoffset": 0, + "yoffset": 48, + "xadvance": 12, + "chnl": 15, + "x": 85, + "y": 205, + "page": 0 + }, + { + "id": 32, + "index": 3, + "char": " ", + "width": 0, + "height": 0, + "xoffset": -3, + "yoffset": 53, + "xadvance": 12, + "chnl": 15, + "x": 10, + "y": 53, + "page": 0 + } + ], + "info": { + "face": "OpenSans-Regular", + "size": 47, + "bold": 0, + "italic": 0, + "charset": [ + " ", + "!", + "\\", + "\"", + "#", + "$", + "%", + "&", + "'", + "(", + ")", + "*", + "+", + ",", + "-", + ".", + "/", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + ":", + ";", + "<", + "=", + ">", + "?", + "@", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "[", + "]", + "^", + "_", + "`", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "{", + "|", + "}", + "~", + "Æ", + "æ", + "Ø", + "ø", + "Å", + "å" + ], + "unicode": 1, + "stretchH": 100, + "smooth": 1, + "aa": 1, + "padding": [ + 3, + 3, + 3, + 3 + ], + "spacing": [ + 0, + 0 + ], + "outline": 0 + }, + "common": { + "lineHeight": 64, + "base": 53, + "scaleW": 363, + "scaleH": 334, + "pages": 1, + "packed": 0, + "alphaChnl": 0, + "redChnl": 0, + "greenChnl": 0, + "blueChnl": 0 + }, + "distanceField": { + "fieldType": "msdf", + "distanceRange": 6 + }, + "kernings": [] +} \ No newline at end of file diff --git a/public/glyphs/OpenSans-Regular.png b/public/glyphs/OpenSans-Regular.png new file mode 100644 index 0000000..8309cec Binary files /dev/null and b/public/glyphs/OpenSans-Regular.png differ diff --git a/public/glyphs/OpenSans-Regular.ttf b/public/glyphs/OpenSans-Regular.ttf new file mode 100644 index 0000000..705966c Binary files /dev/null and b/public/glyphs/OpenSans-Regular.ttf differ diff --git a/public/glyphs/chars.txt b/public/glyphs/chars.txt new file mode 100644 index 0000000..a42efd0 --- /dev/null +++ b/public/glyphs/chars.txt @@ -0,0 +1 @@ + !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~ÆæØøÅå \ No newline at end of file diff --git a/src/components/Html/DepthSelector.tsx b/src/components/Html/DepthSelector.tsx index 9c5fa8e..71cf43d 100644 --- a/src/components/Html/DepthSelector.tsx +++ b/src/components/Html/DepthSelector.tsx @@ -1,10 +1,10 @@ import { ChangeEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import { DataContext } from '../../contexts/DataContext' +import { CameraFocusAtPointEvent, CameraSetPositionEvent } from '../../events/camera-events' +import { WellboreSelectedEvent, wellboreSelectedEventType } from '../../events/wellbore-events' import { PositionLog } from '../../sdk/data/types/PositionLog' -import { Vec3 } from '../../sdk/types/common' import { WellboreManager } from '../../sdk/managers/WellboreManager' -import { WellboreSelectedEvent, wellboreSelectedEventType } from '../../events/wellbore-events' -import { CameraFocusAtPointEvent, CameraSetPositionEvent } from '../../events/camera-events' +import { Vec3 } from '../../sdk/types/common' import { getTrajectory, Trajectory } from '../../sdk/utils/trajectory' export const DepthSelector = () => { diff --git a/src/components/SDFTest/SDFTest.stories.tsx b/src/components/SDFTest/SDFTest.stories.tsx new file mode 100644 index 0000000..9233a8a --- /dev/null +++ b/src/components/SDFTest/SDFTest.stories.tsx @@ -0,0 +1,95 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Canvas3dDecorator } from '../../storybook/decorators/canvas-3d-decorator' +import { PerformanceDecorator } from '../../storybook/decorators/performance-decorator' +import { SDFTest } from './SDFTest' + +const meta = { + title: 'Components/Misc/SDFTest', + component: SDFTest, + decorators: [ + PerformanceDecorator, + Canvas3dDecorator, + ], + parameters: { + autoClear: true, + scale: 1000, + cameraPosition: [0, 0, 2000], + cameraTarget: [0, 0, 0], + //pixelRatio: 1 + }, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + text: 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz\n0123456789\nHello glyphs!', + inBias: 0, + outBias: 0, + fontSize: 32, + rotation: 0, + spacing: 0, + verticalAlign: 0, + horizontalAlign: 0 + }, + argTypes: { + inBias: { + control: { + type: 'range', + min: -0.5, + max: 0.5, + step: 0.01, + } + }, + outBias: { + control: { + type: 'range', + min: -0.5, + max: 0.5, + step: 0.01, + } + }, + fontSize: { + control: { + type: 'range', + min: 1, + max: 200, + step: 1, + }, + }, + rotation: { + control: { + type: 'range', + min: -3.14, + max: 3.14, + step: 0.01, + }, + }, + spacing: { + control: { + type: 'range', + min: 0, + max: 100, + step: 1, + } + }, + verticalAlign: { + control: { + type: 'range', + min: -1, + max: 1, + step: 0.01, + } + }, + horizontalAlign: { + control: { + type: 'range', + min: 0, + max: 1, + step: 0.5, + } + }, + } +} diff --git a/src/components/SDFTest/SDFTest.tsx b/src/components/SDFTest/SDFTest.tsx new file mode 100644 index 0000000..919438e --- /dev/null +++ b/src/components/SDFTest/SDFTest.tsx @@ -0,0 +1,122 @@ +import { useTexture } from '@react-three/drei' +import { extend, useFrame } from '@react-three/fiber' +import { MeshLineGeometry, MeshLineMaterial } from 'meshline' +import { useEffect, useMemo, useState } from 'react' +import { DataTexture, DoubleSide, LinearFilter, Texture, Uniform, Vector2 } from 'three' +import { createConfig, GlyphConfig, MsdfFontJson } from '../../sdk/utils/glyphs' +import { get } from '../../storybook/dependencies/api' +import fragmentShader from './shaders/fragment.glsl' +import vertexShader from './shaders/vertex.glsl' + +extend({ MeshLineGeometry, MeshLineMaterial }) + +const WIDTH = 800 +const HEIGHT = 600 + +type Props = { + text: string + inBias?: number + outBias?: number + fontSize?: number + rotation?: number + spacing?: number + verticalAlign?: number + horizontalAlign?: number +} + +const fileName = 'OpenSans-Regular' + + +export const SDFTest = ({ text, inBias = 0, outBias = 0, fontSize = 32, rotation = 0, spacing = 0, verticalAlign = 0.0, horizontalAlign = 0.0 }: Props) => { + + const glyphAtlas = useTexture(`glyphs/${fileName}.png`, (tex: Texture) => { + tex.generateMipmaps = false + tex.magFilter = LinearFilter + tex.minFilter = LinearFilter + tex.flipY = true + }) + + const [glyphConfig, setGlyphConfig] = useState() + + useEffect(() => { + get(`glyphs/${fileName}.json`).then((json: MsdfFontJson) => { + setGlyphConfig(createConfig(json)) + }).catch(() => setGlyphConfig(null)) + }, []) + + const uniforms = useMemo(() => { + return { + time: new Uniform(0), + size: new Uniform(new Vector2(WIDTH, HEIGHT)), + textPointersOffset: new Uniform(0), + textPointersCount: new Uniform(0), + glyphAtlas: new Uniform(null), + textTexture: new Uniform(null), + in_bias: new Uniform(0), + out_bias: new Uniform(0), + fontSize: new Uniform(32), + rotation: new Uniform(0), + spacing: new Uniform(0), + verticalAlign: new Uniform(0.0), + horizontalAlign: new Uniform(0.0), + digits: new Uniform([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + } + }, []) + + useEffect(() => { + if (glyphConfig) { + if (uniforms.textTexture.value) { + uniforms.textTexture.value.dispose() + } + const { texture, textPointersOffset, textPointersCount } = glyphConfig.encodeTextTexture(text.split('\n')) + uniforms.textTexture.value = texture + uniforms.textPointersOffset.value = textPointersOffset + uniforms.textPointersCount.value = textPointersCount + uniforms.digits.value = [...glyphConfig.encodeText('0123456789.-').indices] + } + + return () => { + if (glyphConfig) { + glyphConfig.dispose() + } + } + }, [uniforms, glyphConfig, text]) + + useEffect(() => { + uniforms.glyphAtlas.value = glyphAtlas + uniforms.in_bias.value = inBias + uniforms.out_bias.value = outBias + uniforms.fontSize.value = fontSize + uniforms.rotation.value = rotation + uniforms.spacing.value = spacing + uniforms.verticalAlign.value = verticalAlign + uniforms.horizontalAlign.value = horizontalAlign + }, [uniforms, glyphAtlas, glyphConfig, inBias, outBias, fontSize, rotation, spacing, verticalAlign, horizontalAlign]) + + useFrame(({ clock }) => { + uniforms.time.value = clock.elapsedTime + }) + + if (!glyphConfig) return null + + return ( + + + + {/* + {/* */} + + + + + ) +} \ No newline at end of file diff --git a/src/components/SDFTest/shaders/fragment.glsl b/src/components/SDFTest/shaders/fragment.glsl new file mode 100644 index 0000000..669ab70 --- /dev/null +++ b/src/components/SDFTest/shaders/fragment.glsl @@ -0,0 +1,149 @@ +#include +#include + + +uniform float time; + +uniform float fontSize; +uniform float rotation; +uniform float spacing; +uniform float verticalAlign; +uniform float horizontalAlign; + +#include ../../../sdk/materials/shaderLib/glyphs.glsl +#include ../../../sdk/materials/shaderLib/render-text.glsl +#include ../../../sdk/materials/shaderLib/render-number.glsl +#include ../../../sdk/materials/shaderLib/rotation.glsl +#include ../../../sdk/materials/shaderLib/sdf-functions.glsl + +// Debug +void textGuides(out vec3 outColor, vec2 position) { + float helper; + + // helper = sdfBox(position - vec2(0.0, 8.5), vec2(1000.0, glyphFontSize / 2.0)); + // outColor = mix(outColor, vec3(1.0, 1.0, 0.0), step(helper, 0.0)); + + helper = sdfLine(position + vec2(0.0, glyphLineHeight / 2.0), vec2(0.0), vec2(size.x, 0.0)); + outColor = mix(outColor, vec3(1.0, 0.0, 0.0), smoothstep(1.0, -1.0, helper)); + + helper = sdfLine(position - vec2(0.0, glyphLineHeight / 2.0), vec2(0.0), vec2(size.x, 0.0)); + outColor = mix(outColor, vec3(1.0, 0.0, 0.0), smoothstep(1.0, -1.0, helper)); + + helper = sdfLine(position + vec2(0.0, glyphLineHeight / 2.0 - glyphBaseLine), vec2(0.0), vec2(size.x, 0.0)); + outColor = mix(outColor, vec3(0.0, 0.0, 1.0), smoothstep(1.0, -1.0, helper)); + + helper = sdfLine(position, vec2(0.0), vec2(size.x, 0.0)); + outColor = mix(outColor, vec3(0.0, 1.0, 0.0), smoothstep(1.0, -1.0, helper)); + + + +} + +// Examples +void example1(out vec3 color, vec2 pixelCoords) { + // the scale we need for a specific font size + float scale = glyphFontSize / fontSize; + + // set up rotation + mat2 rotationMatrix = rotation2d(rotation); + + // spacing we want between text segments + float lineSpacing = (glyphLineHeight + 10.0); + + // where we want to position the text + vec2 textPosition = vec2(size.x / 2.0, glyphLineHeight); + + // transform the coordinates + pixelCoords = (pixelCoords - textPosition) * scale * rotationMatrix; + + // calculate an index for the y positions we want so we know which + // text pointer to reference + uint i = uint(round(pixelCoords.y / lineSpacing)); + i = clamp(i, 0u, textPointersCount - 1u); + + // get text pointer from textTexture at index i + uvec3 textPointer = readTextPointerFromTexture(i); + + // advance to line according to the calculated line index (i) + pixelCoords.y -= float(i) * lineSpacing; + + // debug + // textGuides(color, pixelCoords); + + // render text + renderText(color, pixelCoords, textPointer, verticalAlign, horizontalAlign, vec3(0.09, 0.74, 0.51), spacing, scale); +} + +void example2(out vec3 color, vec2 pixelCoords) { + uint i = 0u; + + uvec3 textPointer = readTextPointerFromTexture(i); + + // scale text to a specific with + float width = size.x / 2.0; + float scale = float(textPointer.z) / width; + + // set up rotation + mat2 rotationMatrix = rotation2d(rotation); + + // where we want to position the text + vec2 textPosition = vec2(size.x / 2.0, size.y / 2.0); + + // transform the coordinates + pixelCoords = (pixelCoords - textPosition) * scale * rotationMatrix; + + // debug + // textGuides(color, pixelCoords); + + vec3 textColor = vec3(0.74, 0.09, 0.58); + + // render text + renderText(color, pixelCoords, textPointer, verticalAlign, horizontalAlign, textColor, spacing, scale); +} + +void exmaple3(out vec3 color, vec2 pixelCoords) { + float scale = glyphFontSize / fontSize; + + float number = time;//-495.549221; + vec3 textColor = vec3(0.0, 0.5, 0.0); + + // set up rotation + mat2 rotationMatrix = rotation2d(rotation); + + pixelCoords = pixelCoords * scale * rotationMatrix; + + renderNumber(color, pixelCoords, number, 3u, verticalAlign, horizontalAlign, textColor, spacing, scale); + pixelCoords.y -= 80.0; + renderNumber(color, pixelCoords, -number * 100.0, 3u, verticalAlign, horizontalAlign, textColor, spacing, scale); +} + +void main() { + #include + + // initial color + vec3 color = vec3(1.0); + + vec2 uv = vUv.xy; + if(!gl_FrontFacing) { + uv.x = 1.0 - uv.x; + } + + // transform uv coordinates to unit coordinates according to size + // with origo at the top left + vec2 pixelCoords = vec2(uv.x, 1.0 - uv.y) * size; + + if(pixelCoords.x >= size.x / 2.0) { + color = vec3(0.9); + } + + example1(color, pixelCoords); + example2(color, pixelCoords); + + pixelCoords -= vec2(size.x / 2.0, size.y - 100.0); + exmaple3(color, pixelCoords); + + gl_FragColor = vec4(color, 1.0); + + #include + +} \ No newline at end of file diff --git a/src/components/SDFTest/shaders/vertex.glsl b/src/components/SDFTest/shaders/vertex.glsl new file mode 100644 index 0000000..1b981e5 --- /dev/null +++ b/src/components/SDFTest/shaders/vertex.glsl @@ -0,0 +1,22 @@ +#include +#include + +varying vec2 vUv; + +void main() { + + vec4 mvPosition = vec4(position, 1.0); + + #ifdef USE_INSTANCING + + mvPosition = instanceMatrix * mvPosition; + + #endif + + mvPosition = modelViewMatrix * mvPosition; + gl_Position = projectionMatrix * mvPosition; + + vUv = uv; + + #include +} \ No newline at end of file diff --git a/src/components/Wellbores/WellboreFormationColumn/WellboreFormationColumn.stories.tsx b/src/components/Wellbores/WellboreFormationColumn/WellboreFormationColumn.stories.tsx index 476f9a8..13bf20b 100644 --- a/src/components/Wellbores/WellboreFormationColumn/WellboreFormationColumn.stories.tsx +++ b/src/components/Wellbores/WellboreFormationColumn/WellboreFormationColumn.stories.tsx @@ -1,21 +1,79 @@ import type { Meta, StoryObj } from '@storybook/react' import { useEffect } from 'react' -import { WellboreSelectedEvent } from '../../../events/wellbore-events' +import { GlyphsProvider } from '../../../contexts/GlyphsContextProvider' +import { WellboreFormationColumn, WellboreSelectedEvent } from '../../../main' import { Canvas3dDecorator } from '../../../storybook/decorators/canvas-3d-decorator' import { DataProviderDecorator } from '../../../storybook/decorators/data-provider-decorator' import { DepthSelectorDecorator } from '../../../storybook/decorators/depth-selector-decorator' import { GeneratorsProviderDecorator } from '../../../storybook/decorators/generators-provider-decorator' +import { PerformanceDecorator } from '../../../storybook/decorators/performance-decorator' import storyArgs from '../../../storybook/story-args.json' -import { TubeTrajectory } from '../TubeTrajectory' +import { CameraTargetMarker } from '../../CameraTargetMarker/CameraTargetMarker' +import { BasicTrajectory } from '../BasicTrajectory' +import { Casings } from '../Casings' +import { CompletionTools } from '../CompletionTools' import { Wellbore } from '../Wellbore/Wellbore' -import { WellboreFormationColumn } from './WellboreFormationColumn' +import { FormationsStripe } from '../WellboreRibbon/stripes/FormationsStripe' +import { MeasuredDepthStripe } from '../WellboreRibbon/stripes/MeasuredDepthStripe' +import { WellboreRibbon } from '../WellboreRibbon/WellboreRibbon' + + +type DemoProps = { + showRibbon: boolean + merged: boolean + scaleFactor: number + stepSize: number +} + +const DemoComponent = ({ showRibbon, merged, scaleFactor, stepSize }: DemoProps) => { + useEffect(() => { + dispatchEvent(new WellboreSelectedEvent({ id: wellboreId })) + }, []) + + return ( + <> + + + + {showRibbon && ( + + + {!merged && ( + <> + + + + + )} + {merged && ( + <> + + + )} + + )} + + + + + + + { (!showRibbon) && } + + + + + + + ) +} const meta = { title: 'Components/Wellbores/WellboreFormationColumn', - component: WellboreFormationColumn, -} satisfies Meta + component: DemoComponent, +} satisfies Meta -type StoryArgs = React.ComponentProps +type StoryArgs = React.ComponentProps export default meta type Story = StoryObj @@ -23,65 +81,73 @@ type Story = StoryObj const wellboreId = storyArgs.defaultWellbore const stratColumnId = storyArgs.defaultStratColumn + export const Default: Story = { args: { - stratColumnId, - startRadius: 0.5, - formationWidth: 2, - inverted: true, - opacity: 1, - unitTypes: [...storyArgs.stratUnitTypeOptions], - units: undefined, + showRibbon: false, + merged: true, + scaleFactor: 5, + stepSize: 10, + // stratColumnId, + // startRadius: 0.5, + // formationWidth: 2, + // inverted: true, + // opacity: 1, + // unitTypes: [...storyArgs.stratUnitTypeOptions], + // units: undefined, }, argTypes: { - formationWidth: { + scaleFactor: { control: { type: 'range', - min: 0, - max: 20, - step: 1, - }, + min: 1, + max: 10, + step: 1 + } }, - opacity: { + stepSize: { control: { type: 'range', - min: 0, - max: 1, - step: 0.1, - }, - }, - unitTypes: { - options: storyArgs.stratUnitTypeOptions, - control: 'check', - }, - units: { - options: ['', ...storyArgs.stratUnitOptions], - control: 'select', - mapping: { - '': undefined + min: 5, + max: 100, + step: 1 } - } + }, + // formationWidth: { + // control: { + // type: 'range', + // min: 0, + // max: 20, + // step: 1, + // }, + // }, + // opacity: { + // control: { + // type: 'range', + // min: 0, + // max: 1, + // step: 0.1, + // }, + // }, + // unitTypes: { + // options: storyArgs.stratUnitTypeOptions, + // control: 'check', + // }, + // units: { + // options: ['', ...storyArgs.stratUnitOptions], + // control: 'select', + // mapping: { + // '': undefined + // } + // } }, decorators: [ + PerformanceDecorator, Canvas3dDecorator, GeneratorsProviderDecorator, DepthSelectorDecorator, DataProviderDecorator, ], - render: args => { - useEffect(() => { - dispatchEvent(new WellboreSelectedEvent({ id: wellboreId })) - }, []) - - return ( - - - - - - - ) - }, parameters: { autoClear: true, scale: 100 diff --git a/src/components/Wellbores/WellboreFormationColumn/WellboreFormationColumn.tsx b/src/components/Wellbores/WellboreFormationColumn/WellboreFormationColumn.tsx index 920c814..b26f9eb 100644 --- a/src/components/Wellbores/WellboreFormationColumn/WellboreFormationColumn.tsx +++ b/src/components/Wellbores/WellboreFormationColumn/WellboreFormationColumn.tsx @@ -138,6 +138,7 @@ export const WellboreFormationColumn = ({ uniforms={uniforms} transparent={opacity === undefined || opacity < 1} opacity={opacity} + depthTest={true} /> ) diff --git a/src/components/Wellbores/WellboreRibbon/WellboreRibbon.tsx b/src/components/Wellbores/WellboreRibbon/WellboreRibbon.tsx new file mode 100644 index 0000000..3913d89 --- /dev/null +++ b/src/components/Wellbores/WellboreRibbon/WellboreRibbon.tsx @@ -0,0 +1,127 @@ +import { PropsWithChildren, useEffect, useMemo, useState } from 'react' +import { BufferAttribute, InstancedBufferGeometry, InstancedInterleavedBuffer, InterleavedBufferAttribute } from 'three' +import { CameraFocusAtPointEvent, cameraFocusAtPointEventType, CameraSetPositionEvent, cameraSetPositionEventType, useData, useWellboreContext } from '../../../main' +import { calculateFrenetFrames, getCurveSegments, getTrajectory, PositionLog, Trajectory, Vec3 } from '../../../sdk' +import { WellboreRibbonContext, WellboreRibbonContextProps } from './WellboreRibbonContext' + + + +function createStripeGeometry(trajectory: Trajectory, segmentsPerMeter: number, fromMsl?: number) { + const positions = new Float32Array([ + 0, -0.5, + 1, -0.5, + 1, 0.5, + 0, 0.5, + ]) + const from = fromMsl !== undefined ? trajectory.getPositionAtDepth(fromMsl, true)! : 0 + const segments = getCurveSegments(trajectory.curve, segmentsPerMeter, from, 1) + const frenetFrames = calculateFrenetFrames(trajectory.curve, segments) + + const attributesBuffer = new Float32Array(frenetFrames.length * 7) + + for (let i = 0; i < frenetFrames.length; i++) { + const frame = frenetFrames[i] + const j = i * 7 + attributesBuffer[j] = frame.position[0] + attributesBuffer[j + 1] = frame.position[1] + attributesBuffer[j + 2] = frame.position[2] + attributesBuffer[j + 3] = frame.curvePosition + attributesBuffer[j + 4] = frame.tangent[0] + attributesBuffer[j + 5] = frame.tangent[1] + attributesBuffer[j + 6] = frame.tangent[2] + } + + const buffer = new InstancedInterleavedBuffer(attributesBuffer, 7, 1) + const geometry = new InstancedBufferGeometry() + geometry.instanceCount = frenetFrames.length - 1 + geometry.setIndex(new BufferAttribute(new Uint8Array([0, 1, 2, 0, 2, 3]), 1)) + geometry.setAttribute('position2', new BufferAttribute(positions, 2)) + geometry.setAttribute('point0', new InterleavedBufferAttribute(buffer, 4, 0)) + geometry.setAttribute('point1', new InterleavedBufferAttribute(buffer, 4, 7)) + geometry.setAttribute('tangent0', new InterleavedBufferAttribute(buffer, 3, 4)) + geometry.setAttribute('tangent1', new InterleavedBufferAttribute(buffer, 3, 11)) + + + return geometry +} + +/** + * The WellboreRibbon component serves as a controller for ribbon stripe components. It provides a context for + * child components, including geometry needed to create stripe components. + * + * @example + * + * + * + * + * + * @remarks + * This is an experimental component and may be changed/removed + * + * @see [Storybook](/videx-3d/?path=/docs/components-wellbores-wellboreformationcolumn--docs) + * @see {@link WellboreRibbonContext} + * @see {@link FormationsStripe} + * @see {@link MeasuredDepthStripe} + * + * @group Components + */ +export const WellboreRibbon = ({ children }: PropsWithChildren) => { + const store = useData() + const { id, fromMsl, segmentsPerMeter } = useWellboreContext() + const [trajectory, setTrajectory] = useState(null) + const [direction, setDirection] = useState([0, -1, 0]) + const stripeGeometry = useMemo(() => { + if (trajectory) { + return createStripeGeometry(trajectory, segmentsPerMeter, fromMsl) + } + return null + }, [trajectory, segmentsPerMeter, fromMsl]) + + const context = useMemo(() => { + if (trajectory && stripeGeometry) { + return { + trajectory, + direction, + geometry: stripeGeometry + } + } + return null + }, [trajectory, direction, stripeGeometry]) + + useEffect(() => { + function onCameraPositionSet(event: CameraSetPositionEvent) { + if (trajectory) { + const n = trajectory.curve.nearest(event.detail) + setDirection(trajectory.curve.getTangentAt(n.position)) + } + } + function onCameraFocusAtPoint(event: CameraFocusAtPointEvent) { + if (trajectory) { + const n = trajectory.curve.nearest(event.detail.point) + setDirection(trajectory.curve.getTangentAt(n.position)) + } + } + addEventListener(cameraSetPositionEventType, onCameraPositionSet) + addEventListener(cameraFocusAtPointEventType, onCameraFocusAtPoint) + return () => { + removeEventListener(cameraSetPositionEventType, onCameraPositionSet) + removeEventListener(cameraFocusAtPointEventType, onCameraFocusAtPoint) + } + }, [trajectory]) + + useEffect(() => { + if (store) { + store.get('position-logs', id).then(response => { + const trajectory = getTrajectory(id, response) + setTrajectory(trajectory) + }).catch(err => console.error(err)) + } + }, [store, id]) + //console.log(direction) + if (!trajectory) return null + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/src/components/Wellbores/WellboreRibbon/WellboreRibbonContext.tsx b/src/components/Wellbores/WellboreRibbon/WellboreRibbonContext.tsx new file mode 100644 index 0000000..3c145e3 --- /dev/null +++ b/src/components/Wellbores/WellboreRibbon/WellboreRibbonContext.tsx @@ -0,0 +1,22 @@ + +import { createContext } from 'react' +import { InstancedBufferGeometry } from 'three' +import { Trajectory, Vec3 } from '../../../sdk' + + + +/** + * WellboreRibbonContext props + * @expand + */ +export type WellboreRibbonContextProps = { + trajectory: Trajectory + geometry: InstancedBufferGeometry + direction: Vec3 +} + +/** + * WellboreRibbon context + * @group Contexts + */ +export const WellboreRibbonContext = createContext(null) \ No newline at end of file diff --git a/src/components/Wellbores/WellboreRibbon/index.ts b/src/components/Wellbores/WellboreRibbon/index.ts new file mode 100644 index 0000000..dd2634f --- /dev/null +++ b/src/components/Wellbores/WellboreRibbon/index.ts @@ -0,0 +1,5 @@ +export * from './WellboreRibbon' +export * from './WellboreRibbonContext' + +export * from './stripes/FormationsStripe' +export * from './stripes/MeasuredDepthStripe' diff --git a/src/components/Wellbores/WellboreRibbon/shaders/formations.glsl b/src/components/Wellbores/WellboreRibbon/shaders/formations.glsl new file mode 100644 index 0000000..bb591f6 --- /dev/null +++ b/src/components/Wellbores/WellboreRibbon/shaders/formations.glsl @@ -0,0 +1,119 @@ +#include +#include + +#include ../../../../sdk/materials/shaderLib/glyphs.glsl +#include ../../../../sdk/materials/shaderLib/render-text.glsl +#include ../../../../sdk/materials/shaderLib/sdf-functions.glsl +#include ../../../../sdk/materials/shaderLib/rotation.glsl +#include ../../../../sdk/materials/shaderLib/colors.glsl + +struct Unit { + uint index; + vec3 color; +}; + +uniform vec3 intervals[INTERVALS_LENGTH]; +uniform Unit units[UNITS_LENGTH]; +uniform float startDepth; + +const float padding = 0.9; + +float lines(float v, float lineWidth) { + float uvDeriv = fwidth(v * 2.0); + + float drawWidth = clamp(lineWidth, uvDeriv, 0.5); + float lineAA = uvDeriv * 1.5; + float fraction = 1.0 - abs(fract(v) * 2.0 - 1.0); + float line = smoothstep(drawWidth + lineAA, drawWidth - lineAA, abs(fraction * 2.0)); + + line *= saturate(lineWidth / drawWidth); + + return line; +} + +vec3 currentInterval(vec2 position) { + int index = 0; + int nIntervals = int(INTERVALS_LENGTH); + vec3 interval = intervals[index]; + + while (position.y > interval.y && index < nIntervals - 1) { + index++; + interval = intervals[index]; + } + return interval; +} + +void main() { + #include + vec3 color = LIGHTGRAY; + float colorMultiplier = 1.0; + + vec2 uv = vUv.xy; + + if (!gl_FrontFacing) { + colorMultiplier = 0.5; + uv.x = 1.0 - uv.x; + } + + float alpha = 0.95; + vec2 pixelCoords = vec2(uv.x, 1.0 - uv.y) * size; + pixelCoords.y += startDepth; + + vec3 interval = currentInterval(pixelCoords); + + float intervalLength = interval.y - interval.x; + Unit unit = units[int(interval.z)]; + + if (pixelCoords.y < interval.x || pixelCoords.y > interval.y) { + discard; + //alpha = 0.0; + } + uvec3 textPointer = readTextPointerFromTexture(unit.index); + + float rotation = 0.0; + float span = size.x * padding; + float vscale = glyphLineHeight / (intervalLength * padding); + + if (intervalLength > size.x) { + span = intervalLength * padding; + vscale = glyphLineHeight / (size.x * padding); + rotation = (-PI / 2.0); + } + float hscale = float(textPointer.z) / span; + float scale = max(hscale, vscale); + + scale = max(scale, glyphLineHeight / (size.x * 0.5)); + + + vec2 pos = pixelCoords.xy; + pos.x -= size.x / 2.0; + pos.y -= ((interval.x + intervalLength / 2.0)); + + + float frame = sdfBox(pos, vec2(size.x / 2.0, intervalLength / 2.0)); + float frameAA = min(fwidth(pixelCoords.y), fwidth(pixelCoords.x)); + color = mix( color, BLACK, smoothstep(1.5 * frameAA,0.0,frame) ); + color = mix( color, unit.color, smoothstep(0.0,-frameAA * 1.5,frame) ); + + pos *= scale; + pos *= rotation2d(rotation); + + float luminance = (0.299 * unit.color.r + 0.587 * unit.color.g + 0.114 * unit.color.b); + vec3 textColor = luminance < 0.25 ? LIGHTGRAY : BLACK; + //textColor = mix(textColor, unit.color, 0.25); + renderText( + color, + pos, + textPointer, + 0.17, + 0.5, + textColor, + 0.0, + scale + ); + + gl_FragColor = vec4(color * colorMultiplier, alpha); + + // #include + #include +} \ No newline at end of file diff --git a/src/components/Wellbores/WellboreRibbon/shaders/measured-depth.glsl b/src/components/Wellbores/WellboreRibbon/shaders/measured-depth.glsl new file mode 100644 index 0000000..8838c28 --- /dev/null +++ b/src/components/Wellbores/WellboreRibbon/shaders/measured-depth.glsl @@ -0,0 +1,77 @@ +#include +#include + +uniform float fontSize; +uniform float stepSize; +uniform float startDepth; + +#include ../../../../sdk/materials/shaderLib/glyphs.glsl +#include ../../../../sdk/materials/shaderLib/render-number.glsl +#include ../../../../sdk/materials/shaderLib/colors.glsl + +float lines(float v, float lineWidth) { + float uvDeriv = fwidth(v * 2.0); + + float drawWidth = clamp(lineWidth, uvDeriv, 0.5); + float lineAA = uvDeriv * 1.5; + float fraction = 1.0 - abs(fract(v) * 2.0 - 1.0); + float line = smoothstep(drawWidth + lineAA, drawWidth - lineAA, abs(fraction * 2.0)); + + line *= saturate(lineWidth / drawWidth); + + return line; +} + +void main() { + #include + + vec3 color = WHITE; + + vec2 uv = vUv.xy; + + if(!gl_FrontFacing) { + color = LIGHTGRAY; + uv.x = 1.0 - uv.x; + } + + vec2 pixelCoords = vec2(uv.x, 1.0 - uv.y) * size; + pixelCoords.y += startDepth; + float scale = glyphFontSize / fontSize; + + float ticks = ceil((size.y + 1.0 + startDepth) / stepSize); + + float spacing = stepSize * scale; + + float y = pixelCoords.y; + y *= scale; + + float iy = round(y / spacing); + iy = clamp(iy, 0.0, ticks - 1.0); + + float number = iy * stepSize; + + float x = pixelCoords.x - size.x * 0.65; + + x *= scale; + + vec2 p = vec2(x, y - spacing * iy); + + renderNumber(color, p, number, 0u, 0.17, 1.0, BLACK, 0.0, scale); + + // tick marks + float minY = ((1.0 - vUv.y) * size.y + startDepth) / (stepSize / 10.0); + float minLines = lines(minY, fontSize * 0.05 / (stepSize / 10.0)); + minLines = minLines * step(size.x * 0.75, pixelCoords.x); + color = mix(color, GRAY, minLines); + + float majY = ((1.0 - vUv.y) * size.y + startDepth) / stepSize; + float majLines = lines(majY, fontSize * 0.1 / stepSize); + majLines = majLines * step(size.x * 0.7, pixelCoords.x); + color = mix(color, BLACK, majLines); + + gl_FragColor = vec4(color, 0.95); + + //#include + #include + +} diff --git a/src/components/Wellbores/WellboreRibbon/shaders/vertex.glsl b/src/components/Wellbores/WellboreRibbon/shaders/vertex.glsl new file mode 100644 index 0000000..6667512 --- /dev/null +++ b/src/components/Wellbores/WellboreRibbon/shaders/vertex.glsl @@ -0,0 +1,40 @@ +#include +#include +#include ../../../../sdk/materials/shaderLib/rotation.glsl + +uniform vec3 direction; +uniform float width; +uniform float offset; + +attribute vec2 position2; +attribute vec4 point0; +attribute vec4 point1; +attribute vec3 tangent0; +attribute vec3 tangent1; + +varying vec2 vUv; +varying vec3 vNormal; +varying vec3 vTangent; + +flat varying int instanceID; + +void main() { + vec4 curveDirection = modelViewMatrix * vec4(direction, 0.0); + vec4 p0 = modelViewMatrix * vec4(point0.xyz, 1.0); + vec4 p1 = modelViewMatrix * vec4(point1.xyz, 1.0); + + vec3 tangent = (modelViewMatrix * vec4(mix(tangent0, tangent1, position2.x), 0.0)).xyz; + vec3 binormal = normalize(cross(normalize(curveDirection.xyz), vec3(0.0, 0.0, -1.0))); + vec3 normal = normalize(cross(tangent, binormal)); + vec3 point = mix(p0.xyz, p1.xyz, position2.x) + binormal * position2.y * width + binormal * offset; + + gl_Position = projectionMatrix * vec4(point, 1.0); + + vUv = vec2(position2.y + 0.5, 1.0 - mix(point0.w, point1.w, position2.x)); + vNormal = normal; + vTangent = tangent; + + instanceID = gl_InstanceID; + + #include +} \ No newline at end of file diff --git a/src/components/Wellbores/WellboreRibbon/stripes/FormationsStripe.tsx b/src/components/Wellbores/WellboreRibbon/stripes/FormationsStripe.tsx new file mode 100644 index 0000000..1fcef79 --- /dev/null +++ b/src/components/Wellbores/WellboreRibbon/stripes/FormationsStripe.tsx @@ -0,0 +1,174 @@ +import { useContext, useEffect, useMemo, useState } from 'react' +import { Color, DataTexture, DoubleSide, Texture, Uniform, Vector2, Vector3 } from 'three' +import { GlyphsContext } from '../../../../contexts/GlyphsContext' +import { useData } from '../../../../hooks/useData' +import { createFormationIntervals, getUnitPicks, mergeFormationIntervals } from '../../../../sdk' +import { EncodedTextTexture } from '../../../../sdk/utils/glyphs' +import { WellboreContext } from '../../Wellbore/WellboreContext' +import { WellboreRibbonContext } from '../WellboreRibbonContext' +import fragmentShader from '../shaders/formations.glsl' +import vertexShader from '../shaders/vertex.glsl' + +/** + * FormationsStripe props + * @expand + */ +export type FormationsStripeProps = { + width: number + offset: number + stratColumnId: string + level?: number +} + +type Unit = { + index: number + color: Color +} + +type FormationData = { + text: EncodedTextTexture + intervals: Vector3[] + units: Unit[] +} + +/** + * This is a stripe component used with the WellboreRibbon component for visualizing + * formations along a wellbore trajectory. + * + * @example + * + * + * + * + * @remarks + * This is an experimental component and may be changed/removed + * + * @see [Storybook](/videx-3d/?path=/docs/components-wellbores-wellboreformationcolumn--docs) + * @see {@link WellboreRibbon} + * @see {@link WellboreRibbonContext} + * + * @group Components + */ +export const FormationsStripe = ({ width, offset, stratColumnId, level }: FormationsStripeProps) => { + const store = useData() + const ribbonContext = useContext(WellboreRibbonContext) + const glyphContext = useContext(GlyphsContext) + + const { id } = useContext(WellboreContext) + + const [formationData, setFormationData] = useState(null) + + const uniforms = useMemo(() => { + return { + size: new Uniform(new Vector2()), + direction: new Uniform(new Vector3(0, -1, 0)), + startDepth: new Uniform(0), + width: new Uniform(20), + offset: new Uniform(0), + glyphAtlas: new Uniform(null), + textTexture: new Uniform(null), + textPointersCount: new Uniform(0), + textPointersOffset: new Uniform(0), + intervals: new Uniform([new Vector3()]), + units: new Uniform([{ index: 0, color: new Color() }]) + } + }, []) + + + useEffect(() => { + if (store && glyphContext) { + getUnitPicks(id, stratColumnId, store, true).then(picksData => { + if (picksData) { + const surfaceIntervals = createFormationIntervals(picksData.matched, picksData.wellbore.depthMdMsl) + let filteredIntervals = surfaceIntervals + if (level !== undefined) { + filteredIntervals = surfaceIntervals.filter(d => d.unit.level === level) + } + const mergedIntervals = mergeFormationIntervals(filteredIntervals) + const unitsMap = new Map() + const units: Unit[] = [] + const labels: string[] = [] + + const intervals: Vector3[] = [] + mergedIntervals.forEach(s => { + const label = s.unit.name + let index = unitsMap.get(label) + if (index === undefined) { + + index = units.length + const unit = { + index, + color: new Color(s.unit.color) + } + unitsMap.set(label, index) + units.push(unit) + labels.push(label) + } + intervals.push(new Vector3(s.mdMslTop, s.mdMslBottom, index)) + }) + + const textTexture = glyphContext.encodeTextTexture(labels) + setFormationData(prev => { + if (prev) { + prev.text.texture.dispose() + } + return { + intervals, + text: textTexture, + units + } + }) + } + }).catch(console.error) + } + }, [store, stratColumnId, id, glyphContext, level]) + + useEffect(() => { + uniforms.offset.value = offset + uniforms.width.value = width + if (ribbonContext) { + uniforms.size.value.set(width, ribbonContext.trajectory.measuredLength) + uniforms.direction.value.set(...ribbonContext.direction) + uniforms.startDepth.value = ribbonContext.trajectory.measuredTop + } + if (formationData) { + uniforms.intervals.value = formationData.intervals + uniforms.units.value = formationData.units + uniforms.textTexture.value = formationData.text.texture + uniforms.textPointersCount.value = formationData.text.textPointersCount + uniforms.textPointersOffset.value = formationData.text.textPointersOffset + } + }, [uniforms, width, offset, formationData, ribbonContext]) + + + + useEffect(() => { + if (glyphContext) { + uniforms.glyphAtlas.value = glyphContext.glyphAtlas + } + }, [uniforms, glyphContext]) + + useEffect(() => { + + }, [ribbonContext, uniforms, width]) + + if (!ribbonContext || !glyphContext || !formationData) return null + + return ( + + + + ) +} \ No newline at end of file diff --git a/src/components/Wellbores/WellboreRibbon/stripes/MeasuredDepthStripe.tsx b/src/components/Wellbores/WellboreRibbon/stripes/MeasuredDepthStripe.tsx new file mode 100644 index 0000000..2089ab6 --- /dev/null +++ b/src/components/Wellbores/WellboreRibbon/stripes/MeasuredDepthStripe.tsx @@ -0,0 +1,100 @@ +import { useContext, useEffect, useMemo } from 'react' +import { DoubleSide, Texture, Uniform, Vector2, Vector3 } from 'three' +import { GlyphsContext } from '../../../../contexts/GlyphsContext' +import { WellboreRibbonContext } from '../WellboreRibbonContext' +import fragmentShader from '../shaders/measured-depth.glsl' +import vertexShader from '../shaders/vertex.glsl' + +/** + * MeasuredDepthStripe props + * @expand + */ +export type MeasuredDepthStripeProps = { + width: number + offset: number + stepSize?: number +} + +/** + * This is a stripe component used with the WellboreRibbon component for adding a measured + * depth scale along a wellbore trajectory. + * + * @example + * + * + * + * + * @remarks + * This is an experimental component and may be changed/removed + * + * @see [Storybook](/videx-3d/?path=/docs/components-wellbores-wellboreformationcolumn--docs) + * @see {@link WellboreRibbon} + * @see {@link WellboreRibbonContext} + * + * @group Components + */ +export const MeasuredDepthStripe = ({ width, offset, stepSize = 50 }: MeasuredDepthStripeProps) => { + + const ribbonContext = useContext(WellboreRibbonContext) + const glyphContext = useContext(GlyphsContext) + + const uniforms = useMemo(() => { + return { + direction: new Uniform(new Vector3(0, -1, 0)), + width: new Uniform(20), + offset: new Uniform(0), + fontSize: new Uniform(3), + size: new Uniform(new Vector2()), + startDepth: new Uniform(0), + stepSize: new Uniform(50), + glyphAtlas: new Uniform(null), + digits: new Uniform([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + } + }, []) + + useEffect(() => { + if (glyphContext) { + uniforms.glyphAtlas.value = glyphContext.glyphAtlas + uniforms.digits.value = [...glyphContext.encodeText('0123456789.-').indices] + } + }, [uniforms, glyphContext]) + + useEffect(() => { + uniforms.width.value = width + uniforms.offset.value = offset + uniforms.stepSize.value = stepSize + }, [uniforms, width, offset, stepSize]) + + + useEffect(() => { + if (ribbonContext) { + uniforms.size.value.set(width, ribbonContext.trajectory.measuredLength) + uniforms.startDepth.value = ribbonContext.trajectory.measuredTop + } + }, [uniforms, ribbonContext, width]) + + useEffect(() => { + if (ribbonContext) { + uniforms.direction.value.set(...ribbonContext.direction) + } + }, [ribbonContext, uniforms]) + + if (!ribbonContext || !glyphContext) return null + + return ( + + + + ) + +} \ No newline at end of file diff --git a/src/contexts/GlyphsContext.ts b/src/contexts/GlyphsContext.ts new file mode 100644 index 0000000..ad61de9 --- /dev/null +++ b/src/contexts/GlyphsContext.ts @@ -0,0 +1,23 @@ + +import { createContext } from 'react' +import { Texture, UniformsGroup } from 'three' +import { EncodedTextSegment, EncodedTextTexture } from '../sdk/utils/glyphs' + +/** + * GlyphsContext props + * @expand + */ +export type GlyphsContextProps = { + glyphAtlas: Texture + glyphsCount: number + glyphData: UniformsGroup + encodeText: (text: string) => EncodedTextSegment + encodeTextTexture: (textSegments: string[] | string) => EncodedTextTexture + dispose: () => void +} + +/** + * Glyphs context + * @group Contexts + */ +export const GlyphsContext = createContext(null) \ No newline at end of file diff --git a/src/contexts/GlyphsContextProvider.tsx b/src/contexts/GlyphsContextProvider.tsx new file mode 100644 index 0000000..9091328 --- /dev/null +++ b/src/contexts/GlyphsContextProvider.tsx @@ -0,0 +1,105 @@ + +import { useTexture } from '@react-three/drei' +import { PropsWithChildren, useEffect, useMemo, useState } from 'react' +import { LinearFilter, Texture } from 'three' +import { createConfig, GlyphConfig } from '../sdk/utils/glyphs' +import { GlyphsContext, GlyphsContextProps } from './GlyphsContext' + +/** + * GlyphsProvider props + * @expand + */ +export type GlyphsProviderProps = { + fontAtlasUrl: string + fontConfigUrl: string +} + +async function get(url: string) { + const response = await fetch( + url, + { + method: 'GET', + credentials: 'omit', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }, + ); + + const { status } = response; + + if ([404, 202, 204].includes(status)) { + return null; + } + + if (response.ok) { + const data = await response.json(); + return data; + } + + throw new Error(response.toString()); +} + +/** + * Provides sub components with a GlyphsContext, which contains data and utilities needed to + * add text support to fragment shaders. This includes a reference to a glyph atlas texture + * and a supporting uniforms group, which will contain glyph config and metrics. + * + * It currently relies on a pre-generated font atlas and json config file using [msdf-bmfont-xml](https://github.com/soimy/msdf-bmfont-xml) + * + * @example + * + * { ... } + * + * + * @remarks + * This component should be considered experimental. + * + * @see {@link GlyphsContext} + * + * @group Components + */ +export const GlyphsProvider = ({ fontAtlasUrl, fontConfigUrl, children }: PropsWithChildren) => { + const glyphAtlas = useTexture(fontAtlasUrl, (tex: Texture) => { + tex.generateMipmaps = false + tex.magFilter = LinearFilter + tex.minFilter = LinearFilter + tex.flipY = true + }) + + const [config, setConfig] = useState(null) + + useEffect(() => { + get(`${fontConfigUrl}`).then(response => { + setConfig(createConfig(response)) + }).catch(err => console.error(err)) + }, [fontConfigUrl]) + + const context = useMemo(() => { + if (!config) return null + return { + glyphAtlas, + encodeText: (text: string ) => config.encodeText(text), + encodeTextTexture: (textSegments: string[] | string) => config.encodeTextTexture(textSegments), + glyphData: config.glyphData, + glyphsCount: config.glyphsCount, + dispose: config.dispose + } + }, [config, glyphAtlas]) + + useEffect(() => { + return () => { + if (context) { + context.glyphAtlas.dispose() + context.dispose() + } + } + }, [context]) + + return ( + + { children } + + ) +} \ No newline at end of file diff --git a/src/generators/wellbore-formation-column-generator.ts b/src/generators/wellbore-formation-column-generator.ts index 48e0e13..3ae26b7 100644 --- a/src/generators/wellbore-formation-column-generator.ts +++ b/src/generators/wellbore-formation-column-generator.ts @@ -44,7 +44,7 @@ export async function generateWellboreFormationColumnGeometries( if (!surfaceIntervals.length) return null const mergedIntervals = mergeFormationIntervals(surfaceIntervals) - + const poslogMsl = await limit(() => this.get('position-logs', wellboreId) ) diff --git a/src/main.ts b/src/main.ts index 70bcd9e..b08faae 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,16 +1,16 @@ /** * @module main */ -export * from './components/common' - export * from './components/Annotations' export * from './components/CameraTargetMarker/CameraTargetMarker' +export * from './components/common' export * from './components/Distance' export * from './components/Grids' export * from './components/Handlers/EventEmitter' export * from './components/Handlers/Highlighter' export * from './components/Html' export * from './components/ObservableGroup/ObservableGroup' +export * from './components/SDFTest/SDFTest' export * from './components/Surfaces' export * from './components/Symbol' export * from './components/UtmArea' @@ -27,12 +27,15 @@ export * from './components/Wellbores/Wellbore' export * from './components/Wellbores/WellboreBounds' export * from './components/Wellbores/WellboreFormationColumn' export * from './components/Wellbores/WellboreLabel' +export * from './components/Wellbores/WellboreRibbon' export * from './components/Wellbores/Wells' export * from './contexts/DataContext' export * from './contexts/DataContextProvider' export * from './contexts/GeneratorsContext' export * from './contexts/GeneratorsContextProvider' +export * from './contexts/GlyphsContext' +export * from './contexts/GlyphsContextProvider' export * from './events/camera-events' export * from './events/depth-events' diff --git a/src/sdk/data/helpers/picks-helpers.ts b/src/sdk/data/helpers/picks-helpers.ts index 6054c6c..9f0faad 100644 --- a/src/sdk/data/helpers/picks-helpers.ts +++ b/src/sdk/data/helpers/picks-helpers.ts @@ -105,20 +105,15 @@ export async function getUnitPicks( } else { wellborePicks = await limit(() => store.get('picks', header.id)) if (wellborePicks) { + wellborePicks = wellborePicks.filter(d => d.mdMsl <= bottom && d.mdMsl >= top).sort((a, b) => b.mdMsl - a.mdMsl); picksFromPrev = wellborePicks } else { return } } - const picks = wellborePicks.filter( - (d) => d.mdMsl <= bottom && d.mdMsl >= top - ) - - picks.sort((a, b) => b.mdMsl - a.mdMsl) - - for (let i = 0; i < picks.length; i++) { - const pick = picks[i] + for (let i = 0; i < wellborePicks.length; i++) { + const pick = wellborePicks[i] if (pick.mdMsl <= md && !added.has(pick.id)) { const unit = unitsMap.get(pick.pickIdentifier) @@ -267,15 +262,14 @@ export function mergeFormationIntervals( for (let i = 1; i < items.length; i++) { const a = items[i] - if (a.entry.mdMsl > depth) { - const limit = a.entry.mdMsl + const lim = a.entry.mdMsl - while (stack.length && depth < limit) { + while (stack.length && depth < lim) { const b = stack.pop()! if (b.exit.mdMsl > depth) { - const bottom = Math.min(b.exit.mdMsl, limit) + const bottom = a.unit.level >= b.unit.level ? Math.min(b.exit.mdMsl, lim) : b.exit.mdMsl merged.push({ mdMslTop: depth, @@ -285,13 +279,13 @@ export function mergeFormationIntervals( depth = bottom - if (b.exit.mdMsl > limit) { + if (b.exit.mdMsl > depth) { stack.push(b) } } } - depth = limit + depth = Math.max(lim, depth) } stack.push(a) diff --git a/src/sdk/geometries/curve/curve-3d.ts b/src/sdk/geometries/curve/curve-3d.ts index 7393e14..67024b3 100644 --- a/src/sdk/geometries/curve/curve-3d.ts +++ b/src/sdk/geometries/curve/curve-3d.ts @@ -132,4 +132,22 @@ export function calculateFrenetFrames(curve: Curve3D, curvePositions: number[]) } return frames -} \ No newline at end of file +} + +export function getCurveSegments(curve: Curve3D, segmentsPerMeter: number, from: number = 0, to: number = 1, useTopAsReference = true): number[] { + const segments: number[] = [Math.max(from, 0)] + const curveLength = curve.length + const deltaPos = useTopAsReference ? 1 : to - from + const segmentLength = deltaPos * curveLength + const nSegments = Math.floor(segmentsPerMeter * segmentLength) + const stepSize = deltaPos / nSegments + for (let i = 0; i <= nSegments; i++) { + const position = i * stepSize + if (position > from && position < to) { + segments.push(position) + } + } + segments.push(Math.min(to, 1)) + + return segments +} diff --git a/src/sdk/geometries/curve/ribbon-geometry.stories.tsx b/src/sdk/geometries/curve/ribbon-geometry.stories.tsx deleted file mode 100644 index db76cc2..0000000 --- a/src/sdk/geometries/curve/ribbon-geometry.stories.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react' -import { useEffect, useState } from 'react' -import { BufferGeometry, DoubleSide, RepeatWrapping, TextureLoader } from 'three' -import { Canvas3dDecorator } from '../../../storybook/decorators/canvas-3d-decorator' -import { RibbonMaterial } from '../../materials/ribbon-material' -import { Vec3 } from '../../types/common' -import { getSplineCurve } from './curve-3d' -import { createRibbonGeometry, RibbonGeometryOptions } from './ribbon-geometry' -import { createTubeGeometry, TubeGeometryOptions } from './tube-geometry' - - -// const points = [ -// [1.2,-3,1],[2,-5,5],[10,-5,4.5],[12,-5,7.5], -// ] - -//const points = [[-10, 0, 0], [0, 2, 0], [10, 0, 0]] -const loader = new TextureLoader() - -const uvMap = loader.load('uv_grid.jpg') -uvMap.repeat.set(1, 50) -uvMap.wrapT = RepeatWrapping -uvMap.wrapS = RepeatWrapping - - -const points = [ - [0, 10, 0], - [1, -1, 1], - [1, -2, 2], - [1.2, -3, 1], - [2, -5, 5], - [10, -5, 4.5], - [12, -5, 7.5], - [10, -4.8, 10], -] - -const curve = getSplineCurve(points as Vec3[], false) - -type Props = { - offset: number, - width: number, - angle: number, - showWireframe: boolean, - ignoreLight: boolean, - segmentsPerMeter: number, - from: number, - to: number, -} - -const material = new RibbonMaterial({ side: DoubleSide, color: 'white', map: uvMap }) -material.ignoreLight = true - -const tubeOptions: TubeGeometryOptions = { - computeNormals: true, - endCap: true, - startCap: true, - radius: 0.05, - radialSegments: 16, -} - -const DemoComponent = ({ - offset, - angle, - width, - showWireframe, - ignoreLight, - segmentsPerMeter, - to, - from, -}: Props) => { - const [geometry, setGeometry] = useState(null) - const [tubeGeometry, setTubeGeometry] = useState(null) - // const targetRef = useRef(null) - // const bnrmRef = useRef(null) - // const cnrmRef = useRef(null) - - - // useFrame(({ camera }) => { - // let det = 0 - // let dist = Infinity - - // camera.getWorldDirection(viewDirection) - // if (geometry) { - // for (let i = 0; i < geometry.attributes.position.array.length; i += 6) { - // worldPosition.fromArray(geometry.attributes.position.array, i) - // sp.copy(worldPosition).project(camera) - // if (Math.abs(sp.x) < 0.5 && Math.abs(sp.y) < 0.5 && sp.z > 0) { - // const rank = sp.z - // if (rank < dist) { - // dist = rank - // directionToVertex.copy(worldPosition).sub(camera.position) - // tan.fromArray(geometry.attributes.tangent.array, i) - // bnrm.fromArray(geometry.attributes.binormal.array, i) - // cnrm.copy(tan).cross(viewDirection) - - // det = tan.dot(v.copy(cnrm).cross(bnrm)) - - // const angle = Math.atan2(det, bnrm.dot(cnrm)) - - // material.angle = angle - // if (targetRef.current) { - // targetRef.current.position.copy(worldPosition) - // } - // if (bnrmRef.current) { - // bnrmRef.current.position.copy(worldPosition) - // bnrmRef.current.setDirection(bnrm) - // } - // if (cnrmRef.current) { - // cnrmRef.current.position.copy(worldPosition) - // cnrmRef.current.setDirection(cnrm) - // } - - // } - // } - - // } - // } - - // }) - - useEffect(() => { - material.width = width - material.wireframe = showWireframe - material.angle = angle - material.offset = offset - material.ignoreLight = ignoreLight - }, [width, angle, offset, showWireframe, ignoreLight]) - - useEffect(() => { - - if (curve) { - const options: RibbonGeometryOptions = { - from, - to, - segmentsPerMeter, - } - - const geometry = createRibbonGeometry(curve, options) - const tubeGeometry = createTubeGeometry(curve, { - ...tubeOptions, - segmentsPerMeter - }) - setGeometry(prev => { - if (prev) { - prev.dispose() - } - return geometry - }) - setTubeGeometry(prev => { - if (prev) { - prev.dispose() - } - return tubeGeometry - }) - } - }, [from, to, segmentsPerMeter]) - - - if (!geometry || !tubeGeometry) return null - - return ( - <> - - - - - {/* - - - - - - */} - - - ) -} - -const meta = { - title: 'SDK/ribbon-geometry', - component: DemoComponent, -} satisfies Meta - -export default meta -type Story = StoryObj - - -export const Default: Story = { - args: { - angle: 2, - width: 0.5, - offset: 0, - segmentsPerMeter: 1, - from: 0, - to: 1, - ignoreLight: true, - showWireframe: false, - }, - argTypes: { - angle: { - control: { type: 'range', min: 0, max: 6, step: 0.1 } - }, - offset: { - control: { type: 'range', min: -6, max: 6, step: 0.1 } - }, - width: { - control: { type: 'range', min: 0.01, max: 1, step: 0.01 } - }, - segmentsPerMeter: { - control: { type: 'range', min: 1, max: 50, step: 1 } - }, - from: { - control: { type: 'range', min: 0, max: 1, step: 0.000001 } - }, - to: { - control: { type: 'range', min: 0, max: 1, step: 0.000001 } - }, - }, - decorators: [ - //PerformanceDecorator, - Canvas3dDecorator - ], - parameters: { - scale: 2, - autoClear: true, - cameraTarget: points[2] - } -} diff --git a/src/sdk/geometries/curve/ribbon-geometry.ts b/src/sdk/geometries/curve/ribbon-geometry.ts deleted file mode 100644 index af34626..0000000 --- a/src/sdk/geometries/curve/ribbon-geometry.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { BufferAttribute, BufferGeometry } from 'three' -import { clamp } from '../../utils/numbers' -import { calculateFrenetFrames, Curve3D, FrenetFrame } from './curve-3d' - -export type RibbonGeometryOptions = { - from?: number - to?: number - segmentsPerMeter?: number -} - -function calculateSegments(curve: Curve3D, from: number, to: number, segmentsPerMeter: number) { - const segments: number[] = [] - - const curveLength = curve.length - const deltaPos = to - from - const segmentLength = deltaPos * curveLength - const nSegments = Math.floor(segmentsPerMeter * segmentLength) - const stepSize = deltaPos / nSegments - - for (let i = 0; i <= nSegments; i++) { - const curvePosition = from + i * stepSize - segments.push(curvePosition) - } - - const frenetFrames = calculateFrenetFrames(curve, segments) - - return frenetFrames -} - - -function generateRibbon( - segments: FrenetFrame[] -) { - - let vertexCount = 0 - let indexCount = 0 - - const vertices = new Float32Array(segments.length * 2 * 3) - const tangents = new Float32Array(vertices.length) - const normals = new Float32Array(vertices.length) - const binormals = new Float32Array(vertices.length) - const uvs = new Float32Array(segments.length * 2 * 2) - const indices = new Uint32Array((segments.length - 1) * 6) - - let idx = 0 - - const dist = 0.0 - - const generateRibbonSegment = (segment: FrenetFrame) => { - - const vi = idx * 3 * 2 - const ii = (idx - 1) * 6 - const ui = idx * 2 * 2 - - // side 1 - vertices[vi] = segment.position[0] + dist * segment.normal[0] - vertices[vi + 1] = segment.position[1] + dist * segment.normal[1] - vertices[vi + 2] = segment.position[2] + dist * segment.normal[2] - - tangents[vi] = segment.tangent[0] - tangents[vi + 1] = segment.tangent[1] - tangents[vi + 2] = segment.tangent[2] - - normals[vi] = segment.normal[0] - normals[vi + 1] = segment.normal[1] - normals[vi + 2] = segment.normal[2] - - binormals[vi] = segment.binormal[0] - binormals[vi + 1] = segment.binormal[1] - binormals[vi + 2] = segment.binormal[2] - - uvs[ui] = 1 - uvs[ui + 1] = 1 - segment.curvePosition - - - // side 2 - vertices[vi + 3] = segment.position[0] - dist * segment.normal[0] - vertices[vi + 4] = segment.position[1] - dist * segment.normal[1] - vertices[vi + 5] = segment.position[2] - dist * segment.normal[2] - - tangents[vi + 3] = segment.tangent[0] - tangents[vi + 4] = segment.tangent[1] - tangents[vi + 5] = segment.tangent[2] - - normals[vi + 3] = segment.normal[0] - normals[vi + 4] = segment.normal[1] - normals[vi + 5] = segment.normal[2] - - binormals[vi + 3] = -segment.binormal[0] - binormals[vi + 4] = -segment.binormal[1] - binormals[vi + 5] = -segment.binormal[2] - - uvs[ui + 2] = 0 - uvs[ui + 3] = 1 - segment.curvePosition - - if (idx > 0) { - indices[ii] = vertexCount - 2 - indices[ii + 1] = vertexCount - indices[ii + 2] = vertexCount - 1 - - indices[ii + 3] = vertexCount - 1 - indices[ii + 4] = vertexCount - indices[ii + 5] = vertexCount + 1 - indexCount += 6 - } - - vertexCount += 2 - idx++ - } - - for (let i = 0; i < segments.length; i++) { - generateRibbonSegment(segments[i]) - } - - return { - indexCount, - vertexCount, - vertices, - tangents, - normals, - binormals, - uvs, - indices, - } -} - -/** - * experimental - may be removed or replaced - */ -export function createRibbonGeometry( - curve: Curve3D, - options: RibbonGeometryOptions = {} -) { - const from = clamp(options.from || 0, 0, 1) - const to = clamp(options.to || 1) - const segmentsPerMeter = options.segmentsPerMeter || 0.1 - - if (to < from) - throw Error('Value of "from" must be less than the value of "to"!') - - const geometry = new BufferGeometry() - - const segments = calculateSegments(curve, from, to, segmentsPerMeter) - - const attributes = generateRibbon(segments) - - geometry.setAttribute('position', new BufferAttribute(attributes.vertices, 3)) - geometry.setAttribute('tangent', new BufferAttribute(attributes.tangents, 3)) - geometry.setAttribute('normal', new BufferAttribute(attributes.normals, 3)) - geometry.setAttribute('binormal', new BufferAttribute(attributes.binormals, 3)) - geometry.setAttribute('uv', new BufferAttribute(attributes.uvs, 2)) - geometry.setIndex(new BufferAttribute(attributes.indices, 1)) - - return geometry -} diff --git a/src/sdk/geometries/packing.ts b/src/sdk/geometries/packing.ts index ca579ef..8513610 100644 --- a/src/sdk/geometries/packing.ts +++ b/src/sdk/geometries/packing.ts @@ -1,5 +1,5 @@ import { BufferAttribute, BufferGeometry, TypedArray } from 'three' - +// TODO: Add support for InterleavedBufferAttribute and InstancedBufferGeometry export type BufferAttributeDrawRange = { start: number, count: number, diff --git a/src/sdk/index.ts b/src/sdk/index.ts index c6c6586..c740a40 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -8,7 +8,6 @@ export * from './data/Store' export * from './data/types' export * from './geometries/curve/curve-3d' -export * from './geometries/curve/ribbon-geometry' export * from './geometries/curve/tube-geometry' export * from './geometries/delatin' export * from './geometries/packing' @@ -27,6 +26,7 @@ export * from './utils/conversions' export * from './utils/depth-buffer' export * from './utils/depth-reader' export * from './utils/elevation-map' +export * from './utils/glyphs' export * from './utils/irapbin-parser' export * from './utils/limiter' export * from './utils/num-array' diff --git a/src/sdk/materials/shaderLib/colors.glsl b/src/sdk/materials/shaderLib/colors.glsl new file mode 100644 index 0000000..2b9aeec --- /dev/null +++ b/src/sdk/materials/shaderLib/colors.glsl @@ -0,0 +1,8 @@ +const vec3 BLACK = vec3(0.0); +const vec3 WHITE = vec3(1.0); +const vec3 GRAY = vec3(0.5); +const vec3 LIGHTGRAY = vec3(0.75); + +const vec3 RED = vec3(0.8, 0.0, 0.0); +const vec3 GREEN = vec3(0.0, 0.8, 0.0); +const vec3 BLUE = vec3(0.0, 0.0, 0.8); \ No newline at end of file diff --git a/src/sdk/materials/shaderLib/glyphs.glsl b/src/sdk/materials/shaderLib/glyphs.glsl new file mode 100644 index 0000000..142a1aa --- /dev/null +++ b/src/sdk/materials/shaderLib/glyphs.glsl @@ -0,0 +1,79 @@ +uniform vec2 size; +uniform sampler2D glyphAtlas; + +uniform float in_bias; +uniform float out_bias; + +uniform GlyphData { + vec4 glyphPosition[GLYPHS_LENGTH]; + vec3 glyphOffset[GLYPHS_LENGTH]; + vec2 glyphTextureSize; + float glyphFontSize; + float glyphPixelRange; + float glyphLineHeight; + float glyphBaseLine; +}; + +varying vec2 vUv; + +struct GlyphParams { + vec2 position; + uint index; +}; + +uint _numDigits(float number) { + float log10 = 0.4342944819032518 * log(number); + return uint(max(trunc(log10), 0.0) + 1.0); +} + +uint _getDigit(float number, uint position) { + return uint(trunc(mod(number / pow(10.0, float(position - 1u)), 10.0))); + //return uint(floor(fract(number / pow(10.0, float(position))) * 10.0)); +} + +float _median(float r, float g, float b) { + return max(min(r, g), min(max(r, g), b)); +} + +vec2 _calcGlyphUv(vec2 texPos) { + vec2 glyphUv = vec2(texPos.x / glyphTextureSize.x, (glyphTextureSize.y - texPos.y) / glyphTextureSize.y); + + return clamp(glyphUv, 0.0, 1.0); +} + +float _calculateGlyphVerticalOffset(float vAlign) { + float pxRangeOffset = floor(glyphPixelRange / 2.0); + float lineHightOffset = glyphLineHeight / 2.0; + float vAlignOffset = (glyphFontSize / 2.0) * vAlign; + //float vAlignOffset = vAlign != 0.0 ? mix(mix(BOTTOM, 0.0, vAlign + 1.0), mix(0.0, TOP, vAlign), step(0.0, vAlign)) : 0.0; + return lineHightOffset + pxRangeOffset + vAlignOffset; +} + +float _screenPixelRange(float scale) { + vec2 scaledSize = size * scale; + vec2 screenPxRange = glyphPixelRange / fwidth(vUv * scaledSize); + return max(min(screenPxRange.x, screenPxRange.y), 1.0); +} + +float _sdfGlyph(vec2 p, uint glyphId) { + vec2 offset = vec2(p.x - glyphOffset[glyphId].x, p.y - glyphOffset[glyphId].y); + vec2 uv = glyphPosition[glyphId].xy + offset; + float sigDist = -0.5; + + if(offset.x >= 0.0 && offset.y >= 0.0 && offset.x <= glyphPosition[glyphId].z && offset.y <= glyphPosition[glyphId].w) { + vec2 TexCoord = _calcGlyphUv(uv); + vec3 mdf = texture2D(glyphAtlas, TexCoord).rgb; + sigDist = _median(mdf.r, mdf.g, mdf.b); + } + return sigDist; +} + +void renderGlyph(out vec3 outColor, vec2 position, uint glyphId, vec3 glyphColor, float pxRange) { + float dist = _sdfGlyph(position, glyphId); + float e = pxRange * (dist - 0.5 + in_bias) + 0.5 + out_bias; + + float contour = clamp(e, 0.0, 1.0); + + outColor = mix(outColor, glyphColor, contour); +} + diff --git a/src/sdk/materials/shaderLib/remap.glsl b/src/sdk/materials/shaderLib/remap.glsl index 483acc8..9f438b9 100644 --- a/src/sdk/materials/shaderLib/remap.glsl +++ b/src/sdk/materials/shaderLib/remap.glsl @@ -5,4 +5,9 @@ float inverseLerp(float v, float minValue, float maxValue) { float remap(float v, float inMin, float inMax, float outMin, float outMax) { float t = inverseLerp(v, inMin, inMax); return mix(outMin, outMax, t); +} + +float inverseSmoothstep(float y) +{ + return 0.5 - sin(asin(1.0- 2.0 * y) / 3.0); } \ No newline at end of file diff --git a/src/sdk/materials/shaderLib/render-number.glsl b/src/sdk/materials/shaderLib/render-number.glsl new file mode 100644 index 0000000..68b814b --- /dev/null +++ b/src/sdk/materials/shaderLib/render-number.glsl @@ -0,0 +1,74 @@ +uniform uint digits[12]; + +float renderNumber( + out vec3 outColor, + vec2 position, + float number, + uint decimals, + float verticalAlign, + float horizontalAlign, + vec3 textColor, + float spacing, + float scale +) { + + float width = 0.0; + float totalWidth = 0.0; + float offset = 0.0; + + // store glyphId and width during calculation of total width so we don't need to re-calculate these values + uvec2 temp[30]; + + uint glyphId; + uint nDigits; + + vec2 pos = position.xy; + uint c = 0u; + + if (number < 0.0) { + glyphId = digits[11]; + width = glyphOffset[glyphId].z + spacing; + offset = width; + temp[c++] = uvec2(glyphId, width); + number = -number; + } + + float intPart; + float fractPart = modf(number, intPart); + fractPart *= pow(10.0, float(decimals)); + nDigits = _numDigits(intPart); + + for(uint n = 0u; n < nDigits; n++) { + glyphId = digits[_getDigit(intPart, nDigits - n)]; + width = glyphOffset[glyphId].z + spacing; + temp[c++] = uvec2(glyphId, width); + totalWidth += width; + } + + if(decimals > 0u) { + glyphId = digits[10]; + width = glyphOffset[glyphId].z + spacing; + temp[c++] = uvec2(glyphId, width); + totalWidth += width; + + nDigits = _numDigits(fractPart); + for(uint n = 0u; n < decimals; n++) { + glyphId = digits[_getDigit(fractPart, nDigits - n)]; + width = glyphOffset[glyphId].z + spacing; + temp[c++] = uvec2(glyphId, width); + totalWidth += width; + } + } + + if(c > 0u) { + pos.x += (totalWidth - spacing) * horizontalAlign + offset; + pos.y += _calculateGlyphVerticalOffset(verticalAlign); + + uint n = 0u; + + while (n < c && pos.x > float(temp[n].y)) pos.x -= float(temp[n++].y); + if (n < c) renderGlyph(outColor, pos, temp[n].x, textColor, _screenPixelRange(scale)); + } + + return totalWidth; +} \ No newline at end of file diff --git a/src/sdk/materials/shaderLib/render-text.glsl b/src/sdk/materials/shaderLib/render-text.glsl new file mode 100644 index 0000000..d76ac91 --- /dev/null +++ b/src/sdk/materials/shaderLib/render-text.glsl @@ -0,0 +1,67 @@ +uniform usampler2D textTexture; +uniform uint textPointersCount; +uniform uint textPointersOffset; + +uint _readGlyphIdFromTexture(uint index) { + uint value = texelFetch(textTexture, ivec2(index, 0), 0).r; + return value; +} + +GlyphParams _findGlyph(vec2 pixelCoords, uvec3 textPointer, float spacing) { + + uint id = _readGlyphIdFromTexture(textPointer.x); + float width = glyphOffset[id].z + spacing; + vec2 position = pixelCoords.xy; + + uint i = textPointer.x; + + while(position.x >= width && i++ < textPointer.y - 1u) { + position.x -= width; + uint j = _readGlyphIdFromTexture(i); + + id = j; + width = glyphOffset[id].z + spacing; + }; + + return GlyphParams(position, id); +} + +uvec3 readTextPointerFromTexture(uint index) { + uvec3 pointer = uvec3(0u); + uint pos = (index * 3u) + textPointersOffset; + pointer.x = texelFetch(textTexture, ivec2(pos, 0), 0).r; + pointer.y = texelFetch(textTexture, ivec2(pos + 1u, 0), 0).r; + pointer.z = texelFetch(textTexture, ivec2(pos + 2u, 0), 0).r; + + return pointer; +} + +void renderText( + out vec3 outColor, + vec2 position, + uvec3 textPointer, + float verticalAlign, + float horizontalAlign, + vec3 textColor, + float spacing, + float scale +) { + + // do nothing if the text widht is 0 + if(textPointer.z == 0u) + return; + + float spacingWidth = spacing * float(textPointer.y - textPointer.x - 1u); + position.x += (float(textPointer.z) + spacingWidth) * horizontalAlign; + + //if (position.y > glyphLineHeight / 2.0 || position.y < -glyphLineHeight / 2.0) return; + + vec2 pos = position; + pos.y += _calculateGlyphVerticalOffset(verticalAlign); + + if(pos.x < 0.0) + return; + + GlyphParams params = _findGlyph(pos, textPointer, spacing); + renderGlyph(outColor, params.position, params.index, textColor, _screenPixelRange(scale)); +} \ No newline at end of file diff --git a/src/sdk/materials/shaderLib/rotation.glsl b/src/sdk/materials/shaderLib/rotation.glsl new file mode 100644 index 0000000..d9431b1 --- /dev/null +++ b/src/sdk/materials/shaderLib/rotation.glsl @@ -0,0 +1,19 @@ +mat2 rotation2d(float angle) { + float s = sin(angle); + float c = cos(angle); + return mat2(c, -s, s, c); +} + +mat4 rotation3d(vec3 axis, float angle) { + axis = normalize(axis); + float s = sin(angle); + float c = cos(angle); + float oc = 1.0 - c; + + return mat4( + oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0, + oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0, + oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0, + 0.0, 0.0, 0.0, 1.0 + ); +} \ No newline at end of file diff --git a/src/sdk/materials/shaderLib/sdf-functions.glsl b/src/sdk/materials/shaderLib/sdf-functions.glsl new file mode 100644 index 0000000..295f66f --- /dev/null +++ b/src/sdk/materials/shaderLib/sdf-functions.glsl @@ -0,0 +1,18 @@ +// SDF functions + +float sdfLine(vec2 p, vec2 a, vec2 b) { + vec2 pa = p - a; + vec2 ba = b - a; + float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); + + return length(pa - ba * h); +} + +float sdfBox(vec2 p, vec2 b) { + vec2 d = abs(p) - b; + return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); +} + +float sdfCircle(vec2 p, float r) { + return length(p) - r; +} \ No newline at end of file diff --git a/src/sdk/utils/glyphs.ts b/src/sdk/utils/glyphs.ts new file mode 100644 index 0000000..32d7e0c --- /dev/null +++ b/src/sdk/utils/glyphs.ts @@ -0,0 +1,169 @@ +import { + DataTexture, + RedIntegerFormat, + Uniform, + UniformsGroup, + UnsignedShortType, + Vector2, + Vector3, + Vector4 +} from 'three' +import { Vec3 } from '../types/common' + +export type Glyph = { + position: [number, number] + dimension: [number, number] + offset: [number, number] + spacing: number +} + +export type EncodedTextSegment = { + indices: number[] + width: number +} + +export type EncodedTextTexture = { + texture: DataTexture + textPointersOffset: number + textPointersCount: number +} + +export type GlyphConfig = { + glyphsCount: number + glyphData: UniformsGroup + encodeText: (text: string) => EncodedTextSegment + encodeTextTexture: (textSegments: string[] | string) => EncodedTextTexture + dispose: () => void +} + +export interface MsdfFontJson { + info: { + size: number + } + common: { + scaleW: number + scaleH: number + lineHeight: number + base: number + } + chars: { + id: number + index: number + char: string + width: number + height: number + xoffset: number + yoffset: number + xadvance: number + x: number + y: number + }[] + distanceField: { + distanceRange: number + } +} + +export function createConfig(json: MsdfFontJson): GlyphConfig { + const charMap = new Map( + json.chars.map((c, i) => [c.id, { index: i, spacing: c.xadvance }]) + ) + const missingChar = 32 + + const encodeText = (text: string): EncodedTextSegment => { + const indices: number[] = [] + let width = 0 + + for (let i = 0; i < text.length; i++) { + let asciiCode = text.charCodeAt(i) + if (!charMap.has(asciiCode)) { + asciiCode = missingChar + } + const character = charMap.get(asciiCode) + if (character) { + indices.push(character.index) + width += character.spacing + } + } + return { + indices, + width, + } + } + + const createTextTexture = (textSegments: string[] | string) => { + + textSegments = Array.isArray(textSegments) ? textSegments : [textSegments] + const pointers: Vec3[] = [] + + const buffer: number[] = [] + for (let s = 0; s < textSegments.length; s++) { + const text = textSegments[s] + const encoded = encodeText(text) + const segmentStart = buffer.length + buffer.push(...encoded.indices) + pointers.push([ + segmentStart, + buffer.length, + encoded.width, + ]) + } + + if (!buffer.length) { + buffer.push(0) + } + + const textPointersOffset = buffer.length + + pointers.forEach(p => buffer.push(...p)) + + const texture = new DataTexture( + new Uint16Array(buffer), + buffer.length, + 1, + RedIntegerFormat, + UnsignedShortType + ) + //console.log(createUniformBuffer(new Uint8Array(buffer))) + texture.needsUpdate = true + + return { texture, textPointersOffset, textPointersCount: pointers.length } + } + + const glyphData = new UniformsGroup() + glyphData.setName('GlyphData') + + const positions: Uniform[] = [] + const offsets: Uniform[] = [] + + json.chars.forEach((char) => { + positions.push( + new Uniform(new Vector4(char.x, char.y, char.width, char.height)) + ) + offsets.push( + new Uniform(new Vector3(char.xoffset, char.yoffset, char.xadvance)) + ) + }) + glyphData.add(positions) + glyphData.add(offsets) + glyphData.add( + new Uniform(new Vector2(json.common.scaleW, json.common.scaleH)) + ) + glyphData.add(new Uniform(json.info.size)) + glyphData.add(new Uniform(json.distanceField.distanceRange)) + glyphData.add(new Uniform(json.common.lineHeight)) + glyphData.add(new Uniform(json.common.base)) + + const dispose = () => { + glyphData.dispose() + } + + const config: GlyphConfig = { + glyphsCount: json.chars.length, + glyphData, + encodeText, + encodeTextTexture: createTextTexture, + dispose, + } + + return config +} diff --git a/src/storybook/decorators/canvas-3d-decorator.tsx b/src/storybook/decorators/canvas-3d-decorator.tsx index 0ba3063..bace431 100644 --- a/src/storybook/decorators/canvas-3d-decorator.tsx +++ b/src/storybook/decorators/canvas-3d-decorator.tsx @@ -26,7 +26,7 @@ export const Canvas3dDecorator = (Story: any, { parameters }: any) => { position: parameters.cameraPosition || [-1 * scale, 1 * scale, -1 * scale], fov: 30, }} - gl={{ logarithmicDepthBuffer: true, autoClear: !!parameters.autoClear, stencil: false }} + gl={{ logarithmicDepthBuffer: true, autoClear: !!parameters.autoClear, stencil: false, pixelRatio: parameters.pixelRatio || devicePixelRatio }} style={{ backgroundColor: parameters.background || '#000', position: 'absolute', diff --git a/src/storybook/decorators/perf.css b/src/storybook/decorators/perf.css new file mode 100644 index 0000000..778ceb8 --- /dev/null +++ b/src/storybook/decorators/perf.css @@ -0,0 +1,4 @@ +.perf-stats { + position: static !important; + float: right; +} \ No newline at end of file diff --git a/src/storybook/decorators/performance-decorator.tsx b/src/storybook/decorators/performance-decorator.tsx index 48e85ac..36d9193 100644 --- a/src/storybook/decorators/performance-decorator.tsx +++ b/src/storybook/decorators/performance-decorator.tsx @@ -1,8 +1,9 @@ -// import { Perf } from 'r3f-perf' +import { Stats } from '@react-three/drei' +import './perf.css' -// export const PerformanceDecorator = (Story: any) => ( -// <> -// -// -// -// ) \ No newline at end of file +export const PerformanceDecorator = (Story: any) => ( + <> + + + +) \ No newline at end of file diff --git a/src/storybook/dependencies/api.ts b/src/storybook/dependencies/api.ts index 3d1b65a..b6c54e2 100644 --- a/src/storybook/dependencies/api.ts +++ b/src/storybook/dependencies/api.ts @@ -1,5 +1,5 @@ export async function get(url: string): Promise { - + //console.log(self) // use correct path when not running locally if (!self.location.origin.startsWith('http://')) { url = '/videx-3d' + url diff --git a/src/storybook/examples/Wellbore.example.stories.tsx b/src/storybook/examples/Wellbore.example.stories.tsx index bd656c7..d7fc5d3 100644 --- a/src/storybook/examples/Wellbore.example.stories.tsx +++ b/src/storybook/examples/Wellbore.example.stories.tsx @@ -108,7 +108,7 @@ export const Default: Story = { useEffect(() => { dispatchEvent(new WellboreSelectedEvent({ id: args.id })) }, [args.id]) - console.log(args.id) + return ( <> diff --git a/tests/picks-helper.test.ts b/tests/picks-helper.test.ts index 7fa14ff..34ed548 100644 --- a/tests/picks-helper.test.ts +++ b/tests/picks-helper.test.ts @@ -34,7 +34,7 @@ describe("picks-helper", () => { picks = await getUnitPicks('c', 'a', store, true, 1000) expect(picks?.unmatched.length).toBe(1) - expect(picks?.matched.length).toBe(16) + expect(picks?.matched.length).toBe(17) }) test('createFormationIntervals', async () => {