From 26fefffd1946b0526f5dffff1becb140e07b7d99 Mon Sep 17 00:00:00 2001 From: Wlad Meixner Date: Fri, 28 Jul 2023 17:10:09 +0200 Subject: [PATCH] wip: add plane renderer --- package.json | 4 +- pnpm-lock.yaml | 197 ++++++++++++- src/lib/Sidebar.svelte | 9 +- src/lib/components/BasicGraph.svelte | 267 ++++++------------ src/lib/rendering/BarRenderer.ts | 76 +++-- src/lib/rendering/GraphRenderer.ts | 4 +- src/lib/rendering/PlainRenderer.ts | 117 ++++++++ .../rendering/geometry/DataPlaneGeometry.ts | 228 +++++++++++++++ .../rendering/materials/DataPlaneMaterial.ts | 115 ++++++++ src/routes/+page.svelte | 231 +++++++++++++-- src/store/DataStore.ts | 52 ++++ src/store/SettingsStore.ts | 14 +- src/store/utils.ts | 27 ++ 13 files changed, 1106 insertions(+), 235 deletions(-) create mode 100644 src/lib/rendering/PlainRenderer.ts create mode 100644 src/lib/rendering/geometry/DataPlaneGeometry.ts create mode 100644 src/lib/rendering/materials/DataPlaneMaterial.ts create mode 100644 src/store/DataStore.ts create mode 100644 src/store/utils.ts diff --git a/package.json b/package.json index 3ccc126..d5eb4ee 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ }, "type": "module", "dependencies": { + "@duckdb/duckdb-wasm": "^1.27.0", "@sveltejs/adapter-static": "^2.0.2", "@types/papaparse": "^5.3.7", "@types/three": "^0.152.1", @@ -45,6 +46,7 @@ "svelte-feather-icons": "^4.0.1", "tailwindcss": "^3.3.2", "three": "^0.154.0", - "three.meshline": "^1.4.0" + "three.meshline": "^1.4.0", + "web-worker": "^1.2.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b892009..9cb7bc0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@duckdb/duckdb-wasm': + specifier: ^1.27.0 + version: 1.27.0 '@sveltejs/adapter-static': specifier: ^2.0.2 version: 2.0.2(@sveltejs/kit@1.20.5) @@ -44,6 +47,9 @@ dependencies: three.meshline: specifier: ^1.4.0 version: 1.4.0 + web-worker: + specifier: ^1.2.0 + version: 1.2.0 devDependencies: '@playwright/test': @@ -102,6 +108,12 @@ packages: engines: {node: '>=10'} dev: false + /@duckdb/duckdb-wasm@1.27.0: + resolution: {integrity: sha512-qx0OjbS1NWAWZSU6hJb6bbyhN48LQn5kO54XsGeBdXbtJYDpPWGfTksP6meWsH8DKCUJIwAXLLImuGvIcY0l0A==} + dependencies: + apache-arrow: 11.0.0 + dev: false + /@esbuild/android-arm64@0.17.19: resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} engines: {node: '>=12'} @@ -488,16 +500,36 @@ packages: resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==} dev: true + /@types/command-line-args@5.2.0: + resolution: {integrity: sha512-UuKzKpJJ/Ief6ufIaIzr3A/0XnluX7RvFgwkV89Yzvm77wCh1kFaFmqN8XEnGcN62EuHdedQjEMb8mYxFLGPyA==} + dev: false + + /@types/command-line-usage@5.0.2: + resolution: {integrity: sha512-n7RlEEJ+4x4TS7ZQddTmNSxP+zziEG0TNsMfiRIxcIVXt71ENJ9ojeXmGO3wPoTdn7pJcU2xc3CJYMktNT6DPg==} + dev: false + /@types/cookie@0.5.1: resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==} + /@types/flatbuffers@1.10.0: + resolution: {integrity: sha512-7btbphLrKvo5yl/5CC2OCxUSMx1wV1wvGT1qDXkSt7yi00/YW7E8k6qzXqJHsp+WU0eoG7r6MTQQXI9lIvd0qA==} + dev: false + /@types/json-schema@7.0.12: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true + /@types/node@18.7.23: + resolution: {integrity: sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg==} + dev: false + /@types/node@20.3.1: resolution: {integrity: sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==} + /@types/pad-left@2.1.1: + resolution: {integrity: sha512-Xd22WCRBydkGSApl5Bw0PhAOHKSVjNL3E3AwzKaps96IMraPqy5BvZIsBVK6JLwdybUzjHnuWVwpDd0JjTfHXA==} + dev: false + /@types/papaparse@5.3.7: resolution: {integrity: sha512-f2HKmlnPdCvS0WI33WtCs5GD7X1cxzzS/aduaxSu3I7TbhWlENjSPs6z5TaB9K0J+BH1jbmqTaM+ja5puis4wg==} dependencies: @@ -693,6 +725,13 @@ packages: engines: {node: '>=8'} dev: true + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: false + /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -711,6 +750,23 @@ packages: normalize-path: 3.0.0 picomatch: 2.3.1 + /apache-arrow@11.0.0: + resolution: {integrity: sha512-M8J4y+DimIyS44w2KOmVfzNHbTroR1oDpBKK6BYnlu8xVB41lxTz0yLmapo8/WJVAt5XcinAxMm14M771dm/rA==} + hasBin: true + dependencies: + '@types/command-line-args': 5.2.0 + '@types/command-line-usage': 5.0.2 + '@types/flatbuffers': 1.10.0 + '@types/node': 18.7.23 + '@types/pad-left': 2.1.1 + command-line-args: 5.2.1 + command-line-usage: 6.1.3 + flatbuffers: 2.0.4 + json-bignum: 0.0.3 + pad-left: 2.1.0 + tslib: 2.5.3 + dev: false + /arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} dev: false @@ -719,6 +775,16 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true + /array-back@3.1.0: + resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} + engines: {node: '>=6'} + dev: false + + /array-back@4.0.2: + resolution: {integrity: sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==} + engines: {node: '>=8'} + dev: false + /array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -811,6 +877,15 @@ packages: type-detect: 4.0.8 dev: true + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: false + /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -841,6 +916,12 @@ packages: resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} dev: false + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: false + /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -848,10 +929,34 @@ packages: color-name: 1.1.4 dev: true + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: false + /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /command-line-args@5.2.1: + resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} + engines: {node: '>=4.0.0'} + dependencies: + array-back: 3.1.0 + find-replace: 3.0.0 + lodash.camelcase: 4.3.0 + typical: 4.0.0 + dev: false + + /command-line-usage@6.1.3: + resolution: {integrity: sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==} + engines: {node: '>=8.0.0'} + dependencies: + array-back: 4.0.2 + chalk: 2.4.2 + table-layout: 1.0.2 + typical: 5.2.0 + dev: false + /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -902,6 +1007,11 @@ packages: type-detect: 4.0.8 dev: true + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: false + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -982,6 +1092,11 @@ packages: engines: {node: '>=6'} dev: false + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: false + /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1167,6 +1282,13 @@ packages: dependencies: to-regex-range: 5.0.1 + /find-replace@3.0.0: + resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} + engines: {node: '>=4.0.0'} + dependencies: + array-back: 3.1.0 + dev: false + /find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1183,6 +1305,10 @@ packages: rimraf: 3.0.2 dev: true + /flatbuffers@2.0.4: + resolution: {integrity: sha512-4rUFVDPjSoP0tOII34oQf+72NKU7E088U5oX7kwICahft0UB2kOQ9wUzzCp+OHxByERIfxRDCgX5mP8Pjkfl0g==} + dev: false + /flatted@3.2.7: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} dev: true @@ -1274,6 +1400,11 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: false + /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1367,6 +1498,11 @@ packages: argparse: 2.0.1 dev: true + /json-bignum@0.0.3: + resolution: {integrity: sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==} + engines: {node: '>=0.8'} + dev: false + /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true @@ -1412,6 +1548,10 @@ packages: p-locate: 5.0.0 dev: true + /lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: false + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true @@ -1565,6 +1705,13 @@ packages: p-limit: 3.1.0 dev: true + /pad-left@2.1.0: + resolution: {integrity: sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA==} + engines: {node: '>=0.10.0'} + dependencies: + repeat-string: 1.6.1 + dev: false + /papaparse@5.4.1: resolution: {integrity: sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==} dev: false @@ -1736,6 +1883,16 @@ packages: dependencies: picomatch: 2.3.1 + /reduce-flatten@2.0.0: + resolution: {integrity: sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==} + engines: {node: '>=6'} + dev: false + + /repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + dev: false + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1906,6 +2063,13 @@ packages: ts-interface-checker: 0.1.13 dev: false + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: false + /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2012,6 +2176,16 @@ packages: resolution: {integrity: sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==} engines: {node: '>= 8'} + /table-layout@1.0.2: + resolution: {integrity: sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==} + engines: {node: '>=8.0.0'} + dependencies: + array-back: 4.0.2 + deep-extend: 0.6.0 + typical: 5.2.0 + wordwrapjs: 4.0.1 + dev: false + /tailwindcss@3.3.2: resolution: {integrity: sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==} engines: {node: '>=14.0.0'} @@ -2103,7 +2277,6 @@ packages: /tslib@2.5.3: resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==} - dev: true /tsutils@3.21.0(typescript@5.1.3): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} @@ -2138,6 +2311,16 @@ packages: hasBin: true dev: true + /typical@4.0.0: + resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} + engines: {node: '>=8'} + dev: false + + /typical@5.2.0: + resolution: {integrity: sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==} + engines: {node: '>=8'} + dev: false + /undici@5.22.1: resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==} engines: {node: '>=14.0'} @@ -2253,6 +2436,10 @@ packages: - terser dev: true + /web-worker@1.2.0: + resolution: {integrity: sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==} + dev: false + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2266,6 +2453,14 @@ packages: engines: {node: '>=0.10.0'} dev: true + /wordwrapjs@4.0.1: + resolution: {integrity: sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==} + engines: {node: '>=8.0.0'} + dependencies: + reduce-flatten: 2.0.0 + typical: 5.2.0 + dev: false + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} diff --git a/src/lib/Sidebar.svelte b/src/lib/Sidebar.svelte index 68d5ab0..44bd824 100644 --- a/src/lib/Sidebar.svelte +++ b/src/lib/Sidebar.svelte @@ -1,15 +1,16 @@
diff --git a/src/lib/rendering/BarRenderer.ts b/src/lib/rendering/BarRenderer.ts index 669675c..1e2d5b0 100644 --- a/src/lib/rendering/BarRenderer.ts +++ b/src/lib/rendering/BarRenderer.ts @@ -10,12 +10,19 @@ interface BarRendererOptions { barWidth: number; barDepth: number; barGap: number; + + // TODO: think about naming + cols: number; + rows: number; } const defaultBarRendererOptions: BarRendererOptions = { - barWidth: 1, - barDepth: 1, - barGap: 0.1 + barWidth: 0.15, + barDepth: 0.15, + barGap: 0.05, + + cols: 10, + rows: 10 }; // Define your colors @@ -23,16 +30,21 @@ const color1 = new THREE.Color('#F0F624'); const color2 = new THREE.Color('#C5407D'); const color3 = new THREE.Color('#15078A'); -const dataColorizer = (value: number, dataIndex: Vector3, axisProgress: Vector3): THREE.Color => { +const dataColorizer = (dataIndex: Vector3, axisProgress: Vector3): THREE.Color => { const xColor = color1.clone().lerp(color2, axisProgress.x); return xColor.lerp(color3, axisProgress.y); }; export class BarRenderer extends GraphRenderer { - private barGroup: THREE.Group; + private barGroup?: THREE.Group; private options: BarRendererOptions; private size: THREE.Vector3 = new THREE.Vector3(0, 0, 0); + // Getter for all bar blocks managed by the renderer + get bars(): THREE.Object3D[] { + return this.barGroup?.children ?? []; + } + constructor( public scene: THREE.Scene, public camera: THREE.Camera, @@ -44,23 +56,28 @@ export class BarRenderer extends GraphRenderer { ...defaultBarRendererOptions, ...options }; - - this.barGroup = new THREE.Group(); - this.scene.add(this.barGroup); } destroy(): void { - this.scene.remove(this.barGroup); + if (this.barGroup) { + this.scene.remove(this.barGroup); + } } setScale(scale: THREE.Vector3): void { this.size = scale; - this.barGroup.scale.copy(scale); + this.barGroup?.scale.copy(scale); + } + + getIntersections(raycaster: THREE.Raycaster): THREE.Intersection[] { + return raycaster.intersectObjects(this.bars, true); } updateWithData(data: BarData) { - // Clear group - this.barGroup.children = []; + if (this.barGroup) { + this.scene.remove(this.barGroup); + } + const barGroup = new THREE.Group(); // Compute min and max values let maxBarHeight = -Infinity; @@ -76,10 +93,23 @@ export class BarRenderer extends GraphRenderer { ) ); + console.log(maxBarHeight); + + const relBarSize = Math.min( + (1 - this.options.barGap) / this.options.cols, + (1 - this.options.barGap) / this.options.rows + ); + + const relGapSize = Math.min( + this.options.barGap / this.options.cols, + this.options.barGap / this.options.rows + ); + // Iterate over input data and create bars for (let z = 0; z < data.data.length; z++) { for (let x = 0; x < data.data[z].length; x++) { let currentBarHeight = 0; + for (let y = 0; y < data.data[z][x].length; y++) { const value = data.data[z][x][y]; @@ -89,45 +119,43 @@ export class BarRenderer extends GraphRenderer { } const color = dataColorizer( - value, new THREE.Vector3(x, y, z), - new THREE.Vector3( - x / data.data[z][x].length, - y / data.data[z].length, - z / data.data.length - ) + new THREE.Vector3(x / this.options.rows, value / maxBarHeight, z / this.options.cols) ); const barSegmentHeight = value / maxBarHeight; // Create bar const bar = this.createBarSegment( + relBarSize, // Scale bar to 0 to 1 range barSegmentHeight, - this.options.barWidth, - this.options.barDepth, + relBarSize, color ); // Position bar bar.position.set( - this.options.barWidth / 2 + x * (this.options.barWidth + this.options.barGap), + relBarSize / 2 + x * (relBarSize + relGapSize), currentBarHeight + barSegmentHeight / 2, - this.options.barDepth / 2 + z * (this.options.barWidth + this.options.barGap) + relBarSize / 2 + z * (relBarSize + relGapSize) ); currentBarHeight += barSegmentHeight; // Add bar to group - this.barGroup.add(bar); + barGroup.add(bar); } } } + this.barGroup = barGroup; + this.barGroup.scale.copy(this.size); + this.scene.add(barGroup); } private createBarSegment( - height: number, width: number, + height: number, depth: number, color: THREE.Color ): THREE.Mesh { diff --git a/src/lib/rendering/GraphRenderer.ts b/src/lib/rendering/GraphRenderer.ts index fd6d9fa..c70cd0f 100644 --- a/src/lib/rendering/GraphRenderer.ts +++ b/src/lib/rendering/GraphRenderer.ts @@ -1,4 +1,4 @@ -export abstract class GraphRenderer { +export abstract class GraphRenderer { constructor(public scene: THREE.Scene, public camera: THREE.Camera) {} abstract destroy(): void; @@ -8,4 +8,6 @@ export abstract class GraphRenderer { * @param data */ abstract updateWithData(data: T): void; + + abstract getIntersections(raycaster: THREE.Raycaster): THREE.Intersection[]; } diff --git a/src/lib/rendering/PlainRenderer.ts b/src/lib/rendering/PlainRenderer.ts new file mode 100644 index 0000000..76b2e99 --- /dev/null +++ b/src/lib/rendering/PlainRenderer.ts @@ -0,0 +1,117 @@ +import * as THREE from 'three'; +import { GraphRenderer } from './GraphRenderer'; +import { DataPlaneShapeMaterial } from './materials/DataPlaneMaterial'; +import { DataPlaneShapeGeometry } from './geometry/DataPlaneGeometry'; + +interface PlainData { + data: number[][][]; +} + +interface PlainRendererOptions { + // TODO: think about naming + cols: number; + rows: number; +} + +const defaultRendererOptions: PlainRendererOptions = { + cols: 10, + rows: 10 +}; + +export class PlainRenderer extends GraphRenderer { + private group?: THREE.Group; + private options: PlainRendererOptions; + private size: THREE.Vector3 = new THREE.Vector3(0, 0, 0); + + // Getter for all bar blocks managed by the renderer + get bars(): THREE.Object3D[] { + return this.group?.children ?? []; + } + + constructor( + public scene: THREE.Scene, + public camera: THREE.Camera, + options: Partial = {} + ) { + super(scene, camera); + + this.options = { + ...defaultRendererOptions, + ...options + }; + } + + destroy(): void { + if (this.group) { + this.scene.remove(this.group); + } + } + + setScale(scale: THREE.Vector3): void { + this.size = scale; + this.group?.scale.copy(scale); + } + + getIntersections(raycaster: THREE.Raycaster): THREE.Intersection[] { + return raycaster.intersectObjects(this.bars, true); + } + + updateWithData(data: PlainData) { + if (this.group) { + this.scene.remove(this.group); + } + const group = new THREE.Group(); + + // Construct a list of all highest values + const highestValues = Array.from({ length: data.data.length }, () => + Array.from({ length: data.data.length > 0 ? data.data[0].length : 0 }, () => 0) + ); + + let maxValue = -0.0; + + // Compute maxBar height to correctly scale bars + data.data.forEach((z, zIdx) => + z.forEach((x, xIdx) => { + // FIXME: this is really slow, just use for initial rendering setup + let height = Math.max(...x); + if (!Number.isFinite(height)) { + height = 0; + } + highestValues[zIdx][xIdx] = height; + maxValue = Math.max(maxValue, height); + }) + ); + + const mockupData = highestValues.map((row, z) => row.map((_, x) => highestValues[z][x] * 0.5)); + + const geometry = new DataPlaneShapeGeometry(highestValues, undefined); + const geometry2 = new DataPlaneShapeGeometry(mockupData, undefined, true); + + // Create a material with the custom fragment shader + const material = new DataPlaneShapeMaterial(new THREE.Color(0xff00ff)); + const material2 = new DataPlaneShapeMaterial(new THREE.Color(0x00ffff)); + + const mesh = new THREE.Mesh(geometry, material); + const mesh2 = new THREE.Mesh(geometry2, material2); + group.add(mesh); + // group.add(mesh2); + + this.group = group; + this.group.position.z = -0.5; + this.group.scale.copy(this.size).multiplyScalar(0.5); + this.scene.add(group); + } + + private createBarSegment( + width: number, + height: number, + depth: number, + color: THREE.Color + ): THREE.Mesh { + const geometry = new THREE.BoxGeometry(width, height, depth); + const material = new THREE.MeshPhongMaterial({ color, transparent: true }); + + const bar = new THREE.Mesh(geometry, material); + return bar; + } +} diff --git a/src/lib/rendering/geometry/DataPlaneGeometry.ts b/src/lib/rendering/geometry/DataPlaneGeometry.ts new file mode 100644 index 0000000..722681d --- /dev/null +++ b/src/lib/rendering/geometry/DataPlaneGeometry.ts @@ -0,0 +1,228 @@ +import * as THREE from 'three'; + +export class DataPlaneShapeGeometry extends THREE.BufferGeometry { + static readonly pointComponentSize = 3; + + private normalizedData: number[][]; + private previousNormalizedData: number[][] | undefined = undefined; + private width: number; + private depth: number; + + private indexBuffer: Uint32Array | undefined = undefined; + private pointBuffer: Float32Array | undefined = undefined; + + private get pointsPerPlane() { + return this.width * this.depth; + } + + private get numPolygonsPerPlane() { + return (this.width - 1) * (this.depth - 1) * 2; + } + + constructor( + data: number[][], + previousData: number[][] | undefined = undefined, + normalized = false + ) { + super(); + + if (normalized) { + this.normalizedData = data; + } else { + // Find max value and normalize all values to be between 0 and 1 + let maxValue = -Infinity; + + data.forEach((row) => { + row.forEach((value) => { + maxValue = Math.max(maxValue, value); + }); + }); + + this.normalizedData = data.map((row) => row.map((value) => value / maxValue)); + + // If previous data is provided, normalize it as well + if (previousData) { + this.previousNormalizedData = previousData.map((row) => + row.map((value) => value / maxValue) + ); + } + } + + this.width = this.normalizedData[0].length; + this.depth = this.normalizedData.length; + + if (previousData) { + if (previousData.length !== this.depth || previousData[0].length !== this.width) { + throw new Error('Previous data has different dimensions'); + } + } + + this.computeGeometry(); + } + + /** + * Setup buffers for the geometry + * - we are using indexed geometry to avoid duplicating vertices + */ + private setupBuffers() { + const { pointsPerPlane, width, depth } = this; + // Total number of floats in the buffer + // - Each point has DataPlaneShapeGeometry.pointComponentSize components + // - Each plane has pointsPerPlane points and we have 2 planes + const bufferElementSize = pointsPerPlane * 2 * DataPlaneShapeGeometry.pointComponentSize; + + if (!this.pointBuffer) { + this.pointBuffer = new Float32Array(bufferElementSize); + } else if (this.pointBuffer.length !== pointsPerPlane * bufferElementSize * 2) { + // Recreate buffer if size changed + delete this.pointBuffer; + this.pointBuffer = new Float32Array(bufferElementSize); + } + // All indices for geometry sides (front,back,left,right) + const numEdgeIndices = (width - 1 + (depth - 1)) * 2 * 3 * 2; + // All indices for geometry top and bottom (top, bottom) + const numPlaneIndices = this.numPolygonsPerPlane * 3 * 2; + const numIndices = numEdgeIndices + numPlaneIndices; + if (!this.indexBuffer) { + this.indexBuffer = new Uint32Array(numIndices); + } else if (this.indexBuffer.length !== numIndices) { + // Recreate buffer if size changed + delete this.indexBuffer; + this.indexBuffer = new Uint32Array(numIndices); + } + } + + private computeGeometry() { + const { normalizedData, width, depth, pointsPerPlane } = this; + + this.setupBuffers(); + + // Sanity check + if (this.pointBuffer === undefined || this.indexBuffer === undefined) { + throw new Error('Buffers not initialized'); + } + + const vertices = this.pointBuffer; + const indices = this.indexBuffer; + + const bufferElementSize = 3; + const numPolygons = (width - 1) * (depth - 1) * 2; + + const hasBottomLayer = this.previousNormalizedData !== undefined; + + // Add all points to the geometry + for (let z = 0; z < depth; z++) { + for (let x = 0; x < width; x++) { + const pointIdx = z * width + x; + const vertexIdx = pointIdx * bufferElementSize; + const indexIdx = (z * width + x) * 6; + + // Add top plane coordinates + vertices[vertexIdx] = (x / width) * 2.0 - 1.0; //x + vertices[vertexIdx + 1] = normalizedData[z][x]; //y + vertices[vertexIdx + 2] = (z / depth) * 2.0 - 1.0; //z + + // Add bottom plane coordinates + vertices[vertexIdx + pointsPerPlane * bufferElementSize] = (x / width) * 2.0 - 1.0; //x + vertices[vertexIdx + pointsPerPlane * bufferElementSize + 1] = hasBottomLayer + ? this.previousNormalizedData?.[z][x] ?? 0 // syntax enforced by strict null checks (should never happen) + : 0; + vertices[vertexIdx + pointsPerPlane * bufferElementSize + 2] = (z / depth) * 2.0 - 1.0; //z + + if (z === depth - 1 || x === width - 1) { + continue; + } + + // Skip if value is 0 + // if ( + // highestValues[z][x] === 0 && + // highestValues[z][x + 1] === 0 && + // highestValues[z + 1][x] === 0 && + // highestValues[z + 1][x + 1] === 0 + // ) { + // continue; + // } + + // Add top plane indices + indices[indexIdx] = pointIdx + width; + indices[indexIdx + 1] = pointIdx + 1; + indices[indexIdx + 2] = pointIdx; + + indices[indexIdx + 3] = pointIdx + width; + indices[indexIdx + 4] = pointIdx + width + 1; + indices[indexIdx + 5] = pointIdx + 1; + + // Add bottom plane indices + indices[indexIdx + numPolygons * 3] = pointIdx + pointsPerPlane; + indices[indexIdx + numPolygons * 3 + 1] = pointIdx + 1 + pointsPerPlane; + indices[indexIdx + numPolygons * 3 + 2] = pointIdx + width + pointsPerPlane; + + indices[indexIdx + numPolygons * 3 + 3] = pointIdx + 1 + pointsPerPlane; + indices[indexIdx + numPolygons * 3 + 4] = pointIdx + width + 1 + pointsPerPlane; + indices[indexIdx + numPolygons * 3 + 5] = pointIdx + width + pointsPerPlane; + } + } + + // Draw walls after top and bottom planes + const wallIdxOffset = numPolygons * 3 * 2; + + // Draw front & back polygons + for (let x = 0; x < width - 1; x++) { + const indexIdx = wallIdxOffset + x * 6; + + // Draw back wall + indices[indexIdx] = x; + indices[indexIdx + 1] = x + 1; + indices[indexIdx + 2] = pointsPerPlane + x; + + indices[indexIdx + 3] = x + 1; + indices[indexIdx + 4] = pointsPerPlane + x + 1; + indices[indexIdx + 5] = pointsPerPlane + x; + + const frontWallIdx = wallIdxOffset + (width - 1 + x) * 6; + + // Draw front wall + indices[frontWallIdx] = pointsPerPlane * 2 - width + x; + indices[frontWallIdx + 1] = x + 1 + pointsPerPlane - width; + indices[frontWallIdx + 2] = x + pointsPerPlane - width; + + indices[frontWallIdx + 3] = pointsPerPlane * 2 - width + x; + indices[frontWallIdx + 4] = pointsPerPlane * 2 - width + x + 1; + indices[frontWallIdx + 5] = x + 1 + pointsPerPlane - width; + } + + const sideWallIdxOffset = wallIdxOffset + (width - 1) * 6 * 2; + + // Draw side walls + for (let z = 0; z < depth - 1; z++) { + const indexIdx = sideWallIdxOffset + z * 6; + + // Draw left wall + indices[indexIdx] = pointsPerPlane + z * width; + indices[indexIdx + 1] = z * width + width; + indices[indexIdx * 2] = z * width; + + console.log({ x: indices[indexIdx], y: indices[indexIdx + 1], z: indices[indexIdx + 2] }); + + // indices[indexIdx + 3] = z * width + width; + // indices[indexIdx + 4] = pointsPerPlane + z * width + width; + // indices[indexIdx + 5] = pointsPerPlane + z * width; + + // const rightWallIdx = sideWallIdxOffset + (depth - 1 + z) * 6; + // indices[rightWallIdx] = z * width + width - 1; + // indices[rightWallIdx + 1] = z * width + width - 1 + width; + // indices[rightWallIdx + 2] = pointsPerPlane + z * width + width - 1; + + // indices[rightWallIdx + 3] = z * width + width - 1 + width; + // indices[rightWallIdx + 4] = pointsPerPlane + z * width + width - 1 + width; + // indices[rightWallIdx + 5] = pointsPerPlane + z * width + width - 1; + } + + this.setAttribute( + 'position', + new THREE.BufferAttribute(vertices, DataPlaneShapeGeometry.pointComponentSize) + ); + this.setIndex(new THREE.Uint32BufferAttribute(indices, 1)); + this.computeVertexNormals(); + } +} diff --git a/src/lib/rendering/materials/DataPlaneMaterial.ts b/src/lib/rendering/materials/DataPlaneMaterial.ts new file mode 100644 index 0000000..b3c0dc7 --- /dev/null +++ b/src/lib/rendering/materials/DataPlaneMaterial.ts @@ -0,0 +1,115 @@ +import * as THREE from 'three'; + +// Define your colors +const color1 = new THREE.Color('#F0F624'); +const color2 = new THREE.Color('#C5407D'); +const color3 = new THREE.Color('#15078A'); + +export class DataPlaneShapeMaterial extends THREE.ShaderMaterial { + static vertexShader = ` + varying float y; + varying vec3 vNormal; + varying vec3 vViewPosition; + + #if NUM_DIR_LIGHTS > 0 + + struct DirectionalLight { + vec3 direction; + vec3 color; + }; + + uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ]; + + void applyDirectionalLight(int index, vec3 normal, vec3 viewDir) { + DirectionalLight light = directionalLights[ index ]; + // ... apply lighting based on light direction and color + } + +#endif + void main() { + y = position.y; + vNormal = normalize( normalMatrix * normal ); + vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); + vViewPosition = -mvPosition.xyz; + gl_Position = projectionMatrix * mvPosition; + } + `; + + // Define the fragment shader that uses the y-coordinate to color + static fragmentShader = ` + uniform float time; + uniform vec3 colorA; + uniform vec3 colorB; + uniform vec3 lightPosition; + + varying vec3 vNormal; + varying float y; + varying vec3 vViewPosition; + + void main() { + // Use the y-coordinate to determine the color + vec3 color = mix(colorA, colorB, y); + + // // ambient + // float ambientStrength = 0.95; + // vec3 ambient = ambientStrength * color; + + // // diffuse + // vec3 norm = normalize(vNormal); + // vec3 lightDir = normalize(lightPosition - vViewPosition); + // float diff = max(dot(norm, lightDir), 0.0); + // vec3 diffuse = diff * color; + + // // specular + // float specularStrength = 0.05; + // vec3 viewDir = normalize(-vViewPosition); + // vec3 reflectDir = reflect(-lightDir, norm); + // float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0); + // vec3 specular = specularStrength * spec * color; + + // vec3 result = (ambient + diffuse + specular); + gl_FragColor = vec4(color, 1.0); + }`; + + constructor( + colorA: THREE.Color = color2, + colorB: THREE.Color = color3, + options: Omit = {} + ) { + super({ + ...options, + vertexShader: DataPlaneShapeMaterial.vertexShader, + fragmentShader: DataPlaneShapeMaterial.fragmentShader, + // lights: true, + // wireframe: true, + side: THREE.DoubleSide, + uniforms: THREE.UniformsUtils.merge([ + THREE.UniformsLib['common'], + THREE.UniformsLib['lights'], + + { + colorA: { value: colorA }, + colorB: { value: colorB }, + time: { value: 0 } + } + ]) + }); + // super.onBeforeCompile = function (shader) { + // // Add custom uniforms if needed + // shader.uniforms.customColor1 = { value: new THREE.Color(0xff0000) }; // Red + // shader.uniforms.customColor2 = { value: new THREE.Color(0x0000ff) }; // Blue + + // // Replace gl_FragColor with your own color logic + // shader.fragmentShader = shader.fragmentShader.replace( + // 'gl_FragColor = vec4( outgoingLight, diffuseColor.a );', + // [ + // 'float gradientFactor = (normal.y + 1.0) / 2.0;', // Make it range from 0 to 1 + // 'vec3 gradientColor = mix(customColor1, customColor2, gradientFactor);', + // 'gl_FragColor = vec4( gradientColor, diffuseColor.a );' + // ].join('\n') + // ); + + // console.log(shader.fragmentShader); + // }; + } +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 4844ce8..8ee4da0 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,29 +1,94 @@