wip: add plane renderer

This commit is contained in:
Wlad Meixner
2023-07-28 17:10:09 +02:00
parent 32e25d32cd
commit 26fefffd19
13 changed files with 1106 additions and 235 deletions
+3 -1
View File
@@ -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"
}
}
+196 -1
View File
@@ -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==}
+5 -4
View File
@@ -1,15 +1,16 @@
<script lang="ts">
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
import { Theme, appSettingsStore, toggleThemeMode, updateTheme } from '../store/SettingsStore';
import { Theme, toggleThemeMode, useSettingsStore } from '../store/SettingsStore';
let className: string | undefined = '';
export { className as class };
let theme: Theme;
let store = useSettingsStore();
onMount(() => {
appSettingsStore.subscribe((value) => {
store.subscribe((value) => {
theme = value.theme;
});
});
@@ -29,7 +30,7 @@
<span class="relative inline-block w-10 h-6 transition bg-gray-300 rounded-full">
<span
class="absolute top-0 left-0 w-6 h-6 transition transform bg-white rounded-full shadow-md"
style="transform: translateX({$appSettingsStore.theme === Theme.Dark
style="transform: translateX({$store.theme === Theme.Dark
? 'calc(100% - 0.75rem)'
: '0'})"
/>
+86 -181
View File
@@ -6,40 +6,31 @@
Scene,
PerspectiveCamera,
WebGLRenderer,
BoxGeometry,
MeshBasicMaterial,
Mesh,
Color,
AmbientLight,
MeshLambertMaterial,
MeshPhongMaterial,
DirectionalLight,
Group,
Light,
PointLight,
Vector2,
Vector3,
OrthographicCamera,
Camera,
MeshStandardMaterial,
Object3D,
LineBasicMaterial,
BufferGeometry,
BufferAttribute,
Line,
DoubleSide,
Raycaster,
ShaderMaterial,
type Intersection
Raycaster
} from 'three';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass';
import { MeshLine, MeshLineMaterial } from 'three.meshline';
import { AxisRenderer } from '$lib/rendering/AxisRenderer';
import { BarRenderer } from '$lib/rendering/BarRenderer';
import { PlainRenderer } from '$lib/rendering/PlainRenderer';
export let data: Array<Array<number>> = [[]];
export let onHover: (position: Vector2, object?: Object3D) => void = () => {};
export let data: Array<Array<Array<number>>> = [[[]]];
// let displatFilter: ;
let containerElement: HTMLDivElement;
let statsElement: HTMLDivElement;
@@ -53,13 +44,14 @@
let composer: EffectComposer;
let stats: Stats;
let barGroup: Group | undefined = undefined;
let dataRenderer: PlainRenderer;
let axisRenderer: AxisRenderer;
function setupControls() {
controls = new OrbitControls(camera, renderer.domElement);
controls.rotateSpeed = 0.4;
controls.zoomSpeed = 0.3;
controls.panSpeed = 0;
controls.rotateSpeed = 1;
controls.zoomSpeed = 1;
controls.panSpeed = 1;
controls.enableDamping = true;
controls.dampingFactor = 0.1;
@@ -71,16 +63,18 @@
scene.add(new AmbientLight(0xffffff, 0.5));
const cameraField = Math.max(containerElement.clientWidth, containerElement.clientHeight) * 2;
camera = new OrthographicCamera(
containerElement.clientWidth / -2,
containerElement.clientWidth / 2,
containerElement.clientHeight / 2,
containerElement.clientHeight / -2,
-1000,
1000
-cameraField,
cameraField
);
camera.position.z = 500;
camera.position.z = 1000;
// Add directional light pointing from camera
const light = new DirectionalLight(0xffffff, 1);
@@ -92,10 +86,27 @@
scene.add(camera);
}
function windowResizeHandler(evt: UIEvent) {
if (camera instanceof OrthographicCamera) {
camera.left = containerElement.clientWidth / -2;
camera.right = containerElement.clientWidth / 2;
camera.top = containerElement.clientHeight / 2;
camera.bottom = containerElement.clientHeight / -2;
camera.updateProjectionMatrix();
} else if (camera instanceof PerspectiveCamera) {
camera.aspect = containerElement.clientWidth / containerElement.clientHeight;
camera.updateProjectionMatrix();
}
renderer.setSize(containerElement.clientWidth, containerElement.clientHeight);
composer.setSize(containerElement.clientWidth, containerElement.clientHeight);
}
onMount(() => {
if (!browser) {
return;
}
setupScene();
renderer = new WebGLRenderer({ antialias: true, alpha: true });
@@ -104,6 +115,9 @@
renderer.setSize(containerElement.clientWidth, containerElement.clientHeight);
containerElement.appendChild(renderer.domElement);
const width = containerElement.clientWidth;
const height = containerElement.clientHeight;
composer = new EffectComposer(renderer);
// Add default render pass
@@ -126,21 +140,29 @@
setupControls();
raycaster = new Raycaster();
dataRenderer = new PlainRenderer(scene, camera);
dataRenderer.setScale(new Vector3(width, width, width));
dataRenderer.updateWithData({ data });
axisRenderer = new AxisRenderer(scene, {
size: new Vector3(width, height, width),
labelScale: width / 25
});
axisRenderer.render();
createBarChart();
raycaster = new Raycaster();
stats = new Stats();
stats.showPanel(1);
statsElement.appendChild(stats.dom);
const size = width * 2;
const divisions = (width * 2) / 100;
const gridHelper = new THREE.GridHelper(size, divisions);
scene.add(gridHelper);
// Animation loop
const animate = () => {
if (!barGroup) {
createBarChart();
return;
}
controls.update();
stats.begin();
@@ -148,12 +170,14 @@
// Handle selection
raycaster.setFromCamera(mousePosition, camera);
const intersections = raycaster.intersectObjects(barGroup.children, true);
const intersections = dataRenderer.getIntersections(raycaster);
if (intersections.length === 0) {
outlinePass.selectedObjects = [];
onHover(mouseClientPosition, undefined);
} else {
outlinePass.selectedObjects = [intersections[0].object];
onHover(mouseClientPosition, intersections[0].object);
}
}
@@ -162,20 +186,36 @@
requestAnimationFrame(animate);
};
// Setup resize handler
window.addEventListener('resize', windowResizeHandler);
animate();
});
onDestroy(() => {
if (!browser) {
return;
}
window.removeEventListener('resize', windowResizeHandler);
});
beforeUpdate(() => {
console.log('beforeUpdate');
// Clear the scene before updating
// clearScene();
clearScene();
});
afterUpdate(() => {
console.log('afterUpdate');
console.log('data', data);
// Re-create the bar chart when data is updated
createBarChart();
dataRenderer.updateWithData({
data: data
});
});
onDestroy(() => {
@@ -189,10 +229,15 @@
});
let mousePosition: Vector2 = new Vector2(0, 0);
let mouseClientPosition: Vector2 = new Vector2(0, 0);
function onHover(event: MouseEvent) {
mousePosition.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1;
mousePosition.y = -(event.clientY / renderer.domElement.clientHeight) * 2 + 1.0;
function handleHover(event: MouseEvent) {
const bounds = containerElement.getBoundingClientRect();
mouseClientPosition.x = event.clientX - bounds.left;
mouseClientPosition.y = event.clientY - bounds.top;
mousePosition.x = (mouseClientPosition.x / bounds.width) * 2 - 1;
mousePosition.y = -(mouseClientPosition.y / bounds.height) * 2 + 1.0;
}
function clearScene() {
@@ -200,154 +245,14 @@
if (!scene) {
return;
}
if (barGroup) {
barGroup.clear();
scene.remove(barGroup);
barGroup = undefined;
}
}
const barGap = 15;
// Define your colors
let color1 = new Color('#F0F624');
let color2 = new Color('#C5407D');
let color3 = new Color('#15078A');
function createBarChart() {
// Remove the previous bar chart
clearScene();
let maxBarHeight = 0;
let barWidth = (containerElement.clientWidth * 0.4) / data.length - barGap * 2;
let barHeightScale = 10;
const group = new Group();
let positionZ = 0;
let positionX = 0;
// compute max values for scaling
for (let x = 0; x < data.length; x++) {
maxBarHeight = Math.max(
maxBarHeight,
data[x].reduce((a, b) => a + b, 0)
);
}
// TODO: adjust data for now only mock
for (let z = 0; z < data.length; z++) {
positionX = 0;
for (let x = 0; x < data.length; x++) {
let currentBarHeight = 0;
const xProgress = x / data.length;
const baseColor = color1.clone().lerp(color2, xProgress);
for (let y = 0; y < data[x].length; y++) {
const barHeight = data[x][y] * barHeightScale;
const geometry = new BoxGeometry(barWidth, barHeight, barWidth);
const material = new MeshLambertMaterial({
color: baseColor.clone().lerp(color3, currentBarHeight / maxBarHeight),
transparent: true
});
if (currentBarHeight > 100) {
material.opacity = 0.05;
}
const bar = new Mesh(geometry, material);
bar.userData = {
data: data[x][y],
x: x,
y: y,
z: z
};
bar.position.x = positionX;
bar.position.y = currentBarHeight + barHeight / 2 + 2;
bar.position.z = positionZ;
currentBarHeight += barHeight;
group.add(bar);
}
maxBarHeight = Math.max(maxBarHeight, currentBarHeight);
positionX += barWidth + barGap;
}
positionZ += barWidth + barGap;
}
// Move group to center
group.position.x = -positionX / 2;
group.position.y = -maxBarHeight / 2;
group.position.z = -positionZ / 2;
scene.add(group);
barGroup = group;
// Draw axis indicator
const axisIndicator = createAxisIndicator(Math.max(positionX, positionZ) * 1.5);
axisIndicator.position.set(
group.position.x - barWidth / 2 - 5 / 2,
group.position.y,
group.position.z - barWidth / 2 - 5 / 2
);
scene.add(axisIndicator);
}
// Create the axis indicator
function createAxisIndicator(size: number, lineWidth: number = 5) {
const xAxisGeometry = new BufferGeometry().setFromPoints([
new Vector3(0, 0, 0),
new Vector3(size, 0, 0)
]);
const yAxisGeometry = new BufferGeometry().setFromPoints([
new Vector3(0, 0, 0),
new Vector3(0, size, 0)
]);
const zAxisGeometry = new BufferGeometry().setFromPoints([
new Vector3(0, 0, 0),
new Vector3(0, 0, size)
]);
const xAxisMaterial = new MeshLineMaterial({ color: 0xeeeeee, lineWidth });
const yAxisMaterial = new MeshLineMaterial({
color: 0xeeeeee,
lineWidth
});
const zAxisMaterial = new MeshLineMaterial({
color: 0xeeeeee,
lineWidth
});
const xAxis = new MeshLine();
xAxis.setGeometry(xAxisGeometry);
const xAxisMesh = new Mesh(xAxis.geometry, xAxisMaterial);
const yAxis = new MeshLine();
yAxis.setGeometry(yAxisGeometry);
const yAxisMesh = new Mesh(yAxis.geometry, yAxisMaterial);
const zAxis = new MeshLine();
zAxis.setGeometry(zAxisGeometry);
const zAxisMesh = new Mesh(zAxis.geometry, zAxisMaterial);
const axisIndicator = new Object3D();
axisIndicator.add(xAxisMesh);
axisIndicator.add(yAxisMesh);
axisIndicator.add(zAxisMesh);
return axisIndicator;
}
</script>
<div class="relative w-full">
<div
bind:this={containerElement}
on:mousemove={onHover}
class="bar-chart-container w-full aspect-square isolate"
on:mousemove={handleHover}
class="bar-chart-container w-full aspect-square overflow-hidden isolate"
/>
<div class="stats absolute isolate top-0 left-0" bind:this={statsElement} />
</div>
+52 -24
View File
@@ -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<BarData> {
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<BarData> {
...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<BarData> {
)
);
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<BarData> {
}
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 {
+3 -1
View File
@@ -1,4 +1,4 @@
export abstract class GraphRenderer<T> {
export abstract class GraphRenderer<T = unknown> {
constructor(public scene: THREE.Scene, public camera: THREE.Camera) {}
abstract destroy(): void;
@@ -8,4 +8,6 @@ export abstract class GraphRenderer<T> {
* @param data
*/
abstract updateWithData(data: T): void;
abstract getIntersections(raycaster: THREE.Raycaster): THREE.Intersection[];
}
+117
View File
@@ -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<PlainData> {
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<PlainRendererOptions> = {}
) {
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;
}
}
@@ -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();
}
}
@@ -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<THREE.ShaderMaterialParameters, 'lights'> = {}
) {
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);
// };
}
}
+214 -17
View File
@@ -1,29 +1,94 @@
<script lang="ts">
import Papa from 'papaparse';
import type { ParseResult } from 'papaparse';
import type { PageServerData } from './$types';
import type { DataEntry, EntryDefinition, FilterEntry } from './proxy+page.server';
import Button from '$lib/components/Button.svelte';
import DropdownSelect from '$lib/components/DropdownSelect.svelte';
import BasicGraph from '$lib/components/BasicGraph.svelte';
import LoadingOverlay from '$lib/components/LoadingOverlay.svelte';
import type { Vector2 } from 'three';
import { useDataStore } from '../store/DataStore';
import { browser } from '$app/environment';
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import type { AsyncDuckDBConnection } from '@duckdb/duckdb-wasm';
export let isLoading = false;
export let isLoading = true;
export let data: PageServerData;
let numberOfElementsOptions: string[] = [];
// TODO: either generate or manually write types for csv data
type CsvData = object;
type CsvData = {
fpr: number;
k: number;
size: number;
n_threads: number;
n_partitions: number;
n_elements_build: number;
n_elements_lookup: number;
shared_elements: number;
name: string;
fixture: string;
s: number;
duration: number;
split_name: string[];
};
let selectedFilters: FilterEntry[] | undefined = [];
let selectedNumberOfElements: string | undefined = undefined;
// List of possible test sizes (e.g. 10, 100, 1000, 10000)
let filterData: number[][][] = [
[
[2, 3, 4, 5, 6, 7],
[20, 2, 3, 4, 50, 6],
[20, 2, 3, 4, 5, 6],
[3, 4, 5, 6, 7, 8],
[20, 2, 3, 4, 5, 6]
],
[
[2, 3, 4, 5, 6, 7],
[20, 2, 3, 4, 50, 6],
[20, 2, 3, 4, 5, 6],
[3, 4, 5, 6, 7, 8],
[20, 2, 3, 4, 5, 6]
],
[[], [20, 2, 3, 4, 5, 6], [20, 2, 3, 4, 5, 6], [3, 4, 5, 6, 7, 8], [20, 2, 3, 4, 5, 6]]
];
let db = useDataStore();
let dbConnection: AsyncDuckDBConnection | undefined = undefined;
onMount(() => {
if (!browser) {
return;
}
db.subscribe((dbInstance) => {
if (!dbInstance) {
dbConnection = undefined;
return;
}
dbInstance
.connect()
.then((connection) => {
console.log('Connected to DB');
dbConnection = connection;
isLoading = false;
})
.catch((e) => {
console.error('Failed to connect to DB', e);
dbConnection = undefined;
});
});
});
async function fetchAndParseCSV(csvUrl: string) {
const url = new URL(csvUrl, location.href);
console.log(url);
const promise = new Promise<Papa.ParseResult<CsvData>>((resolve, reject) => {
Papa.parse<CsvData>(url.href, {
download: true,
@@ -32,7 +97,30 @@
dynamicTyping: true,
skipEmptyLines: true,
complete: function (results) {
console.log('Finished:', results);
// TODO: cleanup into a separate parser function
if (results.errors.length > 0) {
console.error(results.errors);
reject(results.errors);
return;
}
// FIXME: use generated types
results.data.forEach((row: any) => {
// console.log(d);
row['split_name'] = row['name'].split('/')[0].split('_');
row['k'] = parseInt(row['split_name'][row['split_name'].length - 1], 10);
row['fixture'] = row['name'].split('/')[1];
row['s'] = parseFloat(row['name'].split('/')[2]) / 100;
row['n_threads'] = parseInt(row['name'].split('/')[3], 10);
row['n_partitions'] = parseInt(row['name'].split('/')[4], 10);
row['n_elements_build'] = parseInt(row['name'].split('/')[5], 10);
row['n_elements_lookup'] = parseInt(row['name'].split('/')[6], 10);
row['shared_elements'] = parseFloat(row['name'].split('/')[7]) / 100;
row['name'] = row['split_name'].slice(0, row['split_name'].length - 1).join('_');
});
// Analyze data
results.errors.length > 0 ? reject(results.errors) : resolve(results);
}
});
@@ -40,7 +128,8 @@
return promise;
}
let filterPromise: Promise<Papa.ParseResult<object>[]> | undefined = undefined;
let hoverPosition: Vector2 | undefined = undefined;
let filterPromise: Promise<Papa.ParseResult<CsvData>[]> | undefined = undefined;
// TODO: type info values
let infoPromise: Promise<any> | undefined = undefined;
@@ -59,10 +148,112 @@
f.entries.find((e) => e.name.includes(selectedNumberOfElements!))
);
console.log(csvFiles);
// console.log(csvFiles);
// let dbLoadPromise = Promise.all(
// csvFiles.map((f) =>
// dbConnection?.insertCSVFromPath(new URL(f!.dataUrl, location.href).href, {
// name: f!.filterName,
// detect: true
// })
// )
// );
// dbLoadPromise
// .then((results) => {
// console.debug('DuckDB loaded tables', results);
// isLoading = false;
// dbConnection?.getTableNames('').then((names) => {
// console.log('Table names', names);
// });
// })
// .catch((e) => {
// console.error('DB load failed:', e);
// isLoading = false;
// });
filterPromise = Promise.all(csvFiles.map((f) => fetchAndParseCSV(f!.dataUrl)));
filterPromise?.finally(() => (isLoading = false));
let tempFilterData: any;
// Prepare data for display when all CSV files are loaded
filterPromise.then((csvs) => {
console.log(csvs);
type ValueGroup = { value: number; rows: CsvData[] };
// Create a list of all unique sizes and fprs
const mbSizeGroup: ValueGroup[] = [];
const fprGroup: ValueGroup[] = [];
let maxFpr: number = 0.0;
let maxSize: number = 0;
const steps = 10;
// TODO: covnert to single pass
// Compute range of values for each size and fpr
csvs.forEach((csv) => {
csv.data.forEach((row) => {
maxFpr = Math.max(maxFpr, row.fpr);
maxSize = Math.max(maxSize, row.size);
});
});
// Create groups for each size and fpr
for (let i = 0; i < steps; i++) {
const fpr = i / steps;
const size = Math.round((i / steps) * maxSize);
fprGroup.push({ value: fpr, rows: [] });
mbSizeGroup.push({ value: size, rows: [] });
}
const indexForFpr = (fpr: number) => {
return Math.round((fpr / maxFpr) * (steps - 1));
};
// Go over all entries and place them into the correct buckets
csvs.forEach((csv) => {
csv.data.forEach((row) => {
const fprIndex = indexForFpr(row.fpr);
const sizeIndex = Math.round((row.size / maxSize) * (steps - 1));
// console.log(fprIndex, sizeIndex);
// fprGroup[fprIndex].rows.push(row);
mbSizeGroup[sizeIndex].rows.push(row);
});
});
// Construct a datamap sorting all CSV values by size and fpr\
console.log(fprGroup, mbSizeGroup);
tempFilterData = mbSizeGroup.map((size) => {
let fprArray: number[][] = [];
// Populate fprArray
for (let i = 0; i < steps; i++) {
fprArray.push(
size.rows
.filter((row) => {
const fprIndex = indexForFpr(row.fpr);
return fprIndex === i;
})
.map((row) => (row.n_elements_lookup / row.duration) * 0.0000001)
);
}
return fprArray;
});
// csvs.forEach((csv, i) => {
// const filter = selectedFilters![i];
// const entries = filter.entries.filter((e) => e.name.includes(selectedNumberOfElements!));
});
filterPromise?.finally(() => {
isLoading = false;
filterData = tempFilterData;
});
// Load info files
infoPromise = Promise.all(csvFiles.map((f) => fetch(f!.infoUrl).then((r) => r.json())));
@@ -140,15 +331,17 @@
{/if} -->
<div class="lg:flex w-full">
<div class="flex-grow flex-shrink">
<BasicGraph
data={[
[2, 3, 4, 5, 6, 7],
[20, 2, 3, 4, 5, 6],
[20, 2, 3, 4, 5, 6],
[3, 4, 5, 6, 7, 8],
[20, 2, 3, 4, 5, 6]
]}
/>
<div>
<BasicGraph
data={filterData}
onHover={(obj, pos) => {
// hoverPosition = pos;
}}
zAxisLabel="Size in Bytes"
xAxisLabel="False positive rate"
yAxisLabel="Throughput"
/>
</div>
</div>
<div class="w-96 pt-4 flex-grow-0">
<div class="mb-4 p-4 text-left rounded-2xl border-secondary-200 bg-white border shadow-xl">
@@ -195,7 +388,11 @@
{/if}
</div>
</div>
{#if hoverPosition}
<div class="absolute border-t-2 mt-6" style="left: {hoverPosition.x}; top: {hoverPosition.y}">
<h2>Hovering over {hoverPosition}</h2>
</div>
{/if}
{#if isLoading}
<LoadingOverlay isLoading={true} />
{/if}
+52
View File
@@ -0,0 +1,52 @@
import * as duckdb from '@duckdb/duckdb-wasm';
import duckdbWasm from '@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm?url';
import duckdbWorker from '@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js?worker';
import type { AsyncDuckDB } from '@duckdb/duckdb-wasm';
import { useWritable } from './utils';
import { browser } from '$app/environment';
const uniqueStoreName = 'dataStore';
let db: AsyncDuckDB | null = null;
let loading = false;
export const initDuckDB = async () => {
if (db) {
return db; // Return existing database, if any
}
// Instantiate worker
const logger = new duckdb.ConsoleLogger(duckdb.LogLevel.DEBUG);
const worker = new duckdbWorker();
// and asynchronous database
db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(duckdbWasm);
return db;
};
export const useDataStore = () =>
useWritable<AsyncDuckDB | undefined>(uniqueStoreName, undefined, (set) => {
if (loading || !browser) {
return;
}
console.debug('useDataStore');
loading = true;
initDuckDB().then((db) => {
// FIXME: remove after debug
// add db to window object for use in console
// @ts-ignore
window.db = db;
console.debug('DuckDB instantiated', db);
set(db);
});
return () => {
console.debug('useDataStore cleanup');
db?.terminate();
db = null;
};
});
+8 -6
View File
@@ -1,5 +1,5 @@
import { writable } from 'svelte/store';
import type { Writable } from 'svelte/store';
import { get } from 'svelte/store';
import { useWritable } from './utils';
export enum Theme {
Light = 'light',
@@ -17,15 +17,17 @@ const initialAppSettings: AppSettings = {
theme: Theme.Light
};
export const appSettingsStore: Writable<AppSettings> = writable(initialAppSettings);
export const useSettingsStore = () => useWritable('appSettings', initialAppSettings);
export const updateTheme = (theme: Theme) => {
appSettingsStore.update((settings) => {
const store = useSettingsStore();
store.update((settings) => {
return { ...settings, theme };
});
};
export const toggleThemeMode = () => {
console.log('toggleThemeMode', appSettingsStore);
updateTheme(appSettingsStore.theme === Theme.Dark ? Theme.Light : Theme.Dark);
const store = useSettingsStore();
const value = get(store);
updateTheme(value.theme === Theme.Dark ? Theme.Light : Theme.Dark);
};
+27
View File
@@ -0,0 +1,27 @@
import { getContext, hasContext, setContext } from 'svelte';
import { readable, writable, type StartStopNotifier } from 'svelte/store';
// Based on context handling suggested by https://dev.to/jdgamble555/the-correct-way-to-use-stores-in-sveltekit-3h6i
// context for any type of store
export const useSharedStore = <T, A>(
name: string,
fn: (value?: A, startStop?: StartStopNotifier<A>) => T,
defaultValue?: A,
startStop?: StartStopNotifier<A>
) => {
if (hasContext(name)) {
return getContext<T>(name);
}
const _value = fn(defaultValue, startStop);
setContext(name, _value);
return _value;
};
// writable store context
export const useWritable = <T>(name: string, value: T, startStop?: StartStopNotifier<T>) =>
useSharedStore(name, writable, value, startStop);
// readable store context
export const useReadable = <T>(name: string, value: T, startStop?: StartStopNotifier<T>) =>
useSharedStore(name, readable, value, startStop);