diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a3c939..dd5ff99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + npm install + npm run build pip install -e . pip install "pytest<8" - name: Test with pytest @@ -74,6 +76,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + npm install + npm run build python -m pip install -U jupyterlab==4.1.2 jupyter-packaging~=0.12 pip install -e . diff --git a/.gitignore b/.gitignore index 4330648..d5f7359 100644 --- a/.gitignore +++ b/.gitignore @@ -153,6 +153,9 @@ npm-debug.log .vscode/* !.vscode/extensions.json +# do not upload static files +src/weas_widget/static/* + # *.pdf tests/work diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cad01df --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Xing Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 4c8051d..ec4d06e 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,16 @@ Features: Use the pip: ```console - pip install weas-widget +pip install weas-widget ``` To install the latest version from source, first clone the repository and then install using pip: ```console - $ git clone https://github.com/superstar54/weas-widget - $ pip install -e weas-widget +git clone https://github.com/superstar54/weas-widget +cd weas-widget +npm run build +pip install -e . ``` ## How to use @@ -170,8 +172,14 @@ viewer +### Save image +Save image to a path by: +```python +viewer.save_image("/home/xing/filename.png") +``` ### Download image +This will open a download panel. ```python viewer.download_image("filename.png") diff --git a/docs/source/gui.rst b/docs/source/gui.rst index f8cce5b..a10717c 100644 --- a/docs/source/gui.rst +++ b/docs/source/gui.rst @@ -69,7 +69,7 @@ Here is a example of a molecule with `index` label: Other parameters: ----------------- +------------------ - **Atom Scale**: change scale for all atoms. - **Unit Cell**: show or hide the unit cell. @@ -80,7 +80,7 @@ Other parameters: Buttons -------- +--------- There are several buttons on the top right of the GUI. They are: .. figure:: _static/images/gui-buttons.png @@ -99,7 +99,7 @@ Configuration One can use a configuration dict to specify their GUI preferences, such as enabling/disabling the GUI entirely or choosing specific components to display. Disable the GUI entirely -~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------- .. code-block:: python from weas_widget import WeasWidget @@ -108,7 +108,7 @@ Disable the GUI entirely Select specific components -~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------- .. code-block:: python diff --git a/docs/source/quick_start.ipynb b/docs/source/quick_start.ipynb index fbf3606..529efad 100644 --- a/docs/source/quick_start.ipynb +++ b/docs/source/quick_start.ipynb @@ -394,7 +394,7 @@ "viewer = WeasWidget()\n", "viewer.from_ase(trajectory)\n", "# set a vector field to show the arrow\n", - "viewer.vectorField = {\"origins\": \"positions\", \"vectors\": \"movement\", \"radius\": 0.1}\n", + "viewer.vectorField = [{\"origins\": \"positions\", \"vectors\": \"movement\", \"radius\": 0.1}]\n", "viewer" ] }, diff --git a/weas_widget/style.css b/js/widget.css similarity index 100% rename from weas_widget/style.css rename to js/widget.css diff --git a/weas_widget/index.js b/js/widget.js similarity index 83% rename from weas_widget/index.js rename to js/widget.js index f4e9c8e..10f2fa8 100644 --- a/weas_widget/index.js +++ b/js/widget.js @@ -1,5 +1,12 @@ -// use the latest version of weas from unpkg +// if we want test weas package, then use the following import +// clone the weas repo and import the weas module +// import * as weas from "../../weas/src/index.js"; +// if not, then use the release version from unpkg import * as weas from "https://unpkg.com/weas/dist/weas.mjs"; +import "./widget.css"; + + + function render({ model, el }) { let avr; // Declare avr here let viewerElement = document.createElement("div"); @@ -49,10 +56,29 @@ function render({ model, el }) { setTimeout(() => { avr = renderAtoms(); }, 10 - ); // Delay rendering by 10ms - // Listen for changes in the '_update' property - model.on("change:_drawModels", () => { - avr.drawModels(); + ); + // js task + model.on("change:js_task", () => { + const task = model.get("js_task"); + function run_task(task) { + switch (task.name) { + case "drawModels": + avr.drawModels(); + break; + case "exportImage": + const imageData = avr.tjs.exportImage(task.kwargs.resolutionScale); + model.set("imageData", imageData); + model.save_changes(); + break; + case "downloadImage": + avr.tjs.downloadImage(task.kwargs.filename); + break; + case "setCameraPosition": + avr.tjs.updateCameraAndControls(avr.atoms.getCenterOfGeometry(), task.kwargs.position); + break; + } + } + run_task(task); }); // Listen for changes in the 'atoms' property model.on("change:atoms", () => { @@ -103,7 +129,6 @@ function render({ model, el }) { model.on("change:modelPolyhedras", () => {avr.modelPolyhedras = model.get("modelPolyhedras");}); model.on("change:selectedAtomsIndices", () => {avr.selectedAtomsIndices = model.get("selectedAtomsIndices");}); model.on("change:boundary", () => {avr.boundary = model.get("boundary");}); - // volumetric data model.on("change:volumetricData", () => { const data = model.get("volumetricData"); @@ -122,20 +147,7 @@ function render({ model, el }) { avr.VFManager.drawVectorFields(); }); - // export image - model.on("change:_exportImage", () => { - const imageData = avr.tjs.exportImage(); - model.set("imageData", imageData); - model.save_changes(); - }); - // download image - model.on("change:_downloadImage", () => { - const filename = model.get("_imageFileName"); - console.log("filename: ", filename); - avr.tjs.downloadImage(filename); - }); } - function createVolumeData(data, cell=[[1, 0, 0], [0, 1, 0], [0, 0, 1]]) { // get the dimensions const dims = [data.values.length, data.values[0].length, data.values[0][0].length]; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..10832bb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,432 @@ +{ + "name": "weas-widget", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "dat.gui": "^0.7.9", + "three": "^0.161.0" + }, + "devDependencies": { + "esbuild": "^0.20.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", + "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", + "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", + "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", + "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", + "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", + "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", + "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", + "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", + "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", + "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", + "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", + "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", + "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", + "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", + "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", + "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", + "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", + "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", + "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", + "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", + "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", + "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", + "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/dat.gui": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.9.tgz", + "integrity": "sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ==" + }, + "node_modules/esbuild": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", + "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.1", + "@esbuild/android-arm": "0.20.1", + "@esbuild/android-arm64": "0.20.1", + "@esbuild/android-x64": "0.20.1", + "@esbuild/darwin-arm64": "0.20.1", + "@esbuild/darwin-x64": "0.20.1", + "@esbuild/freebsd-arm64": "0.20.1", + "@esbuild/freebsd-x64": "0.20.1", + "@esbuild/linux-arm": "0.20.1", + "@esbuild/linux-arm64": "0.20.1", + "@esbuild/linux-ia32": "0.20.1", + "@esbuild/linux-loong64": "0.20.1", + "@esbuild/linux-mips64el": "0.20.1", + "@esbuild/linux-ppc64": "0.20.1", + "@esbuild/linux-riscv64": "0.20.1", + "@esbuild/linux-s390x": "0.20.1", + "@esbuild/linux-x64": "0.20.1", + "@esbuild/netbsd-x64": "0.20.1", + "@esbuild/openbsd-x64": "0.20.1", + "@esbuild/sunos-x64": "0.20.1", + "@esbuild/win32-arm64": "0.20.1", + "@esbuild/win32-ia32": "0.20.1", + "@esbuild/win32-x64": "0.20.1" + } + }, + "node_modules/three": { + "version": "0.161.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.161.0.tgz", + "integrity": "sha512-LC28VFtjbOyEu5b93K0bNRLw1rQlMJ85lilKsYj6dgTu+7i17W+JCCEbvrpmNHF1F3NAUqDSWq50UD7w9H2xQw==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..705705c --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "scripts": { + "dev": "npm run build -- --sourcemap=inline --watch", + "build": "esbuild js/widget.js --minify --format=esm --bundle --outdir=src/weas_widget/static", + "build-watch": "esbuild js/widget.js --minify --format=esm --bundle --outdir=src/weas_widget/static --watch=forever" + }, + "dependencies": { + "dat.gui": "^0.7.9", + "three": "^0.161.0" + }, + "devDependencies": { + "esbuild": "^0.20.0" + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1b4fb2d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,74 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "weas_widget" +version = "0.0.11" +description = "A widget to visualize and interact with atomistic structures in Jupyter Notebook." +authors = [{name = "Xing Wang", email = "xingwang1991@gmail.com"}] +readme = "README.md" +license = {file = "LICENSE"} + +classifiers = [ + "Development Status :: 1 - Planning", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS :: MacOS X", + "Programming Language :: Python", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering" +] + +keywords = ["atomistic", "visualize", "edit"] +requires-python = ">=3.6" + +dependencies = ["anywidget", "ase", "requests"] + +[project.urls] +Documentation = "https://weas-widget.readthedocs.io" +Source = "https://github.com/superstart54/weas-widget" + + +[project.optional-dependencies] +docs = [ + "sphinx_rtd_theme", + "sphinx~=7.2", + "nbsphinx", +] +pre-commit = [ + "pre-commit~=2.2", + "pylint~=2.17.4", +] +tests = [ + "pytest~=7.0", + "pytest-cov~=2.7,<2.11", + "playwright", + "httpx", +] +dev = ["watchfiles", "jupyterlab"] + +# automatically add the dev feature to the default env (e.g., hatch shell) +[tool.hatch.envs.default] +features = ["dev"] + + +[tool.hatch.build] +only-packages = true +artifacts = ["src/weas_widget/static/*"] + +[tool.hatch.build.hooks.jupyter-builder] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = ["src/weas_widget/static/widget.js"] +skip-if-exists = ["src/weas_widget/static/widget.js"] +dependencies = ["hatch-jupyter-builder>=0.5.0"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] +npm = "npm" +build_cmd = "build" diff --git a/setup.py b/setup.py deleted file mode 100644 index 90a06a5..0000000 --- a/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name="weas-widget", - version="0.0.10", - packages=find_packages(), - description="A widget to visualize and interact with atomistic structures in Jupyter Notebook.", - long_description=open("README.md").read(), - long_description_content_type="text/markdown", - author="Xing Wang", - author_email="xingwang1991@gmail.com", - url="https://github.com/superstar54/weas-widget", - install_requires=[ - "anywidget", - "ipywidgets", - "ase", - ], - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - ], - package_data={ - "": ["*.js", "*.css"], - "weas_widget.datas": ["*"], - }, - include_package_data=True, # This tells setuptools to check MANIFEST.in for additional files -) diff --git a/weas_widget/__init__.py b/src/weas_widget/__init__.py similarity index 59% rename from weas_widget/__init__.py rename to src/weas_widget/__init__.py index d734aa5..198e6e9 100644 --- a/weas_widget/__init__.py +++ b/src/weas_widget/__init__.py @@ -1,16 +1,23 @@ +import importlib.metadata +import pathlib import anywidget import traitlets as tl -import os -from .utils import ASE_Adapter, Pymatgen_Adapter +from .utils import ASE_Adapter, Pymatgen_Adapter, load_online_example +import time +import threading -esm_path = os.path.join(os.path.dirname(__file__), """index.js""") -# css_path = os.path.join(os.path.dirname(__file__), """style.css""") -css_path = "https://unpkg.com/weas/dist/style.css" +try: + __version__ = importlib.metadata.version("weas_widget") +except importlib.metadata.PackageNotFoundError: + __version__ = "unknown" class WeasWidget(anywidget.AnyWidget): - _esm = esm_path - _css = css_path + _esm = pathlib.Path(__file__).parent / "static" / "widget.js" + _css = pathlib.Path(__file__).parent / "static" / "widget.css" + + # indicate if the widget is displayed and available for interaction. + ready = tl.Bool(False).tag(sync=True) # atoms can be a dictionary or a list of dictionaries atoms = tl.Union([tl.Dict({}), tl.List(tl.Dict({}))]).tag(sync=True) selectedAtomsIndices = tl.List([]).tag(sync=True) @@ -21,19 +28,17 @@ class WeasWidget(anywidget.AnyWidget): atomLabelType = tl.Unicode("None").tag(sync=True) showCell = tl.Bool(True).tag(sync=True) showBondedAtoms = tl.Bool(False).tag(sync=True) - _drawModels = tl.Bool(False).tag(sync=True) atomScales = tl.List([]).tag(sync=True) modelSticks = tl.List([]).tag(sync=True) modelPolyhedras = tl.List([]).tag(sync=True) volumetricData = tl.Dict({"values": [[[]]]}).tag(sync=True) isoSettings = tl.List([]).tag(sync=True) imageData = tl.Unicode("").tag(sync=True) - _exportImage = tl.Bool(False).tag(sync=True) - _downloadImage = tl.Bool(False).tag(sync=True) - _imageFileName = tl.Unicode("atomistic-model.png").tag(sync=True) vectorField = tl.List().tag(sync=True) showVectorField = tl.Bool(True).tag(sync=True) guiConfig = tl.Dict({}).tag(sync=True) + # task + js_task = tl.Dict({}).tag(sync=True) debug = tl.Bool(False).tag(sync=True) def __init__(self, from_ase=None, from_pymatgen=None, **kwargs): @@ -43,9 +48,18 @@ def __init__(self, from_ase=None, from_pymatgen=None, **kwargs): if from_pymatgen is not None: self.from_pymatgen(from_pymatgen) + def send_js_task(self, task): + """Send a task to the javascript side. + task is a dictionary with the following keys + - name: the name of the task + - kwargs: a dictionary of arguments + """ + self.js_task = task + self.js_task = {} + def drawModels(self): """Redraw the widget.""" - self._drawModels = not self._drawModels + self.send_js_task({"name": "drawModels"}) def set_atoms(self, atoms): self.atoms = atoms @@ -90,13 +104,16 @@ def to_pymatgen(self): return Pymatgen_Adapter.to_pymatgen(self.atoms) def load_example(self, name="tio2.cif"): - from ase.io import read - - atoms = read(os.path.join(os.path.dirname(__file__), f"datas/{name}")) + atoms = load_online_example(name) self.set_atoms(ASE_Adapter.to_weas(atoms)) - def export_image(self): - self._exportImage = not self._exportImage + def export_image(self, resolutionScale=5): + self.send_js_task( + { + "name": "exportImage", + "kwargs": {"resolutionScale": resolutionScale}, + } + ) def display_image(self): from IPython.display import display, Image @@ -114,6 +131,41 @@ def display_image(self): # Display the image return display(Image(data=image_data)) - def download_image(self, imageFileName="atomistic-model.png"): - self._imageFileName = imageFileName - self._downloadImage = not self._downloadImage + def download_image(self, filename="weas-model.png"): + self.send_js_task( + { + "name": "downloadImage", + "kwargs": {"filename": filename}, + } + ) + + def save_image( + self, filename="weas-model.png", resolutionScale=5, camera_position=None + ): + import base64 + + def _save_image(): + while not self.ready: + time.sleep(0.1) + if camera_position is not None: + self.camera_position = camera_position + self.export_image(resolutionScale) + # polling mechanism to check if the image data is available + while not self.imageData: + time.sleep(0.1) + base64_data = self.imageData.split(",")[1] + # Decode the base64 string + image_data = base64.b64decode(base64_data) + with open(filename, "wb") as f: + f.write(image_data) + + thread = threading.Thread(target=_save_image, args=(), daemon=False) + thread.start() + + @property + def camera_position(self): + return self._camera_position + + @camera_position.setter + def camera_position(self, value): + self.send_js_task({"name": "setCameraPosition", "kwargs": {"position": value}}) diff --git a/weas_widget/utils.py b/src/weas_widget/utils.py similarity index 87% rename from weas_widget/utils.py rename to src/weas_widget/utils.py index fd404b2..5a4545d 100644 --- a/weas_widget/utils.py +++ b/src/weas_widget/utils.py @@ -127,3 +127,22 @@ def generate_phonon_trajectory( new_atoms = new_atoms.repeat(repeat) trajectory.append(new_atoms) return trajectory + + +def load_online_example(name="tio2.cif"): + """Load an example from the online data.""" + from ase.io import read + import requests + from io import StringIO + + url = "https://raw.githubusercontent.com/superstar54/weas/main/demo/datas/" + name + # Download the file content + response = requests.get(url) + if response.status_code == 200: + file_content = response.text + # Use StringIO to simulate a file-like object for ASE to read from + file_like_object = StringIO(file_content) + atoms = read(file_like_object, format="cif") + return atoms + else: + raise ValueError(f"Failed to download the file {name}") diff --git a/weas_widget/datas/__init__.py b/weas_widget/datas/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/weas_widget/datas/tio2.cif b/weas_widget/datas/tio2.cif deleted file mode 100644 index f29d6c2..0000000 --- a/weas_widget/datas/tio2.cif +++ /dev/null @@ -1,31 +0,0 @@ -data_image0 -_chemical_formula_structural Ti2O4 -_chemical_formula_sum "Ti2 O4" -_cell_length_a 4.65327 -_cell_length_b 4.65327 -_cell_length_c 2.9692 -_cell_angle_alpha 90 -_cell_angle_beta 90 -_cell_angle_gamma 90 - -_space_group_name_H-M_alt "P 1" -_space_group_IT_number 1 - -loop_ - _space_group_symop_operation_xyz - 'x, y, z' - -loop_ - _atom_site_type_symbol - _atom_site_label - _atom_site_symmetry_multiplicity - _atom_site_fract_x - _atom_site_fract_y - _atom_site_fract_z - _atom_site_occupancy - Ti Ti1 1.0 0.00000 0.00000 0.00000 1.0000 - Ti Ti2 1.0 0.50000 0.50000 0.50000 1.0000 - O O1 1.0 0.19542 0.80458 0.50000 1.0000 - O O2 1.0 0.80458 0.19542 0.50000 1.0000 - O O3 1.0 0.69542 0.69542 0.00000 1.0000 - O O4 1.0 0.30458 0.30458 0.00000 1.0000