diff --git a/LICENSE b/LICENSE index ee566ac..4e32002 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 ShapeDiver +Copyright (c) 2025 ShapeDiver Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..b0cde4a --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# Stargate Web Client Example + +This repository contains an minimal React app acting as a "desktop client application" for ShapeDiver. Read more about connecting client applications to ShapeDiver [here](https://help.shapediver.com/doc/shapediver-desktop-clients). + +## How to use this + +The code provided in this repository makes it as easy as possible to integrate web applications implemented using React with ShapeDiver. Easily send data to and from ShapeDiver. This allows to extend web applications using the power of Grasshopper models running on ShapeDiver. ShapeDiver provides the most secure, scalable, reliable, and performant infrastructure to turn Grasshopper models into cloud applications. + +If your web application is not implemented using React, you should still be able to reuse lots of the provided code. + +See the bottom of this README for details on how to use the code. + +## Getting started + +### Create a ShapeDiver account + +Create an account on the [ShapeDiver platform](https://www.shapediver.com/app/). You need a paid subscription to use +the desktop client feature. It's possible to register for a free trial. + +### Upload the example Grasshopper model + +Upload the [example Grasshopper model](StargateWebClientExampleRhino8.ghx) contained in this repository. The Grasshopper model contains three [import components](https://help.shapediver.com/doc/import-components), and three corresponding [export components](https://help.shapediver.com/doc/download-export). The imported data is piped directly from the import components to the export components. Also, the imported geometry is fed into [display components](https://help.shapediver.com/doc/gltf-2-0-display) for rendering it in the 3D view. + +![Screenshot of example Grasshopper model](screenshots/grasshoppermodel.png) + +Once model checking has completed, click "Save" on the model edit page. Now click "Open App". + +![Open App button](screenshots/openapp.png) + +You should see an empty 3D view, because the example model doesn't output geometry by default. +The opened App shows the following default user interface, corresponding to the import and export components: + +![Opened App - Parameters - No client connected](screenshots/app-notconnected-parameters.png) + +Note the "No active client found" message, telling us that no client application is currently connected. + +### Start the local React application + +> [!WARNING] +> Local testing is not allowed when using the productive ShapeDiver platform. +> However, you can use a service like https://ngrok.com/ to provide a public URL for your localhost. +> Your public URL will need to be whitelisted by ShapeDiver. Write to us at contact@shapediver.com +> and ask us about it. +> As an alternative, you can test a deployed version of this example [here](https://appbuilder.shapediver.com/stargate/v1/main/development/). + +As a prerequisite install [Node.js 20](https://nodejs.org/en/about/previous-releases) and [pnpm](https://pnpm.io/). + +Run `pnpm i` to install the required dependencies. + +Now run `pnpm start`. A browser tab should open and show this: + +![Start ShapeDiver Authentication](screenshots/startshapediverauth.png) + +Clicking "Start ShapeDiver Auth" will redirect you to the ShapeDiver Platform, which will ask you to authorize the example application: + +![Authorize the App](screenshots/authorize-the-app.png) + +Clicking "Yes" will take you back to the locally running example application. You should see this: + +![Authenticated example React app](screenshots/authenticated-react-app.png) + +### Test the connection + +Go back to the opened App, select the "Desktop clients" tab, and click the refresh icon. +The dropdown should show the "Stargate Web Client". Select it. + +![Client selection](screenshots/client-selection.png) + +Now select the "Parameters" tab. You should see that the parameter components have been enabled. +Clicking one of the orange buttons will trigger a file import from the local React application. +Give it a try by clicking at least one of the buttons. + +![Import from client](screenshots/import-from-client.png) + +Once the import and the computation of the Grasshopper +model have finished, navigate to the "Exports" tab. Clicking the corresponding "Export to client" button triggers a download +of the file exported from Grasshopper in the local React application. + +![Export to client](screenshots/export-to-client.png) + +## How to get started with the code + +Most of the code is contained in hooks that can easily be reused in other React apps. + + * [useShapeDiverAuth](src/hooks/useShapeDiverAuth.ts) - Hook to manage authentication with ShapeDiver via OAuth2 Authorization Code Flow with PKCE. + * [useShapeDiverStargate](src/hooks/useShapeDiverStargate.ts) - Hook providing a ShapeDiver Stargate Client implementation. The ShapeDiver Stargate service is used to communicate between ShapeDiver Apps and client applications by relaying event messages via websocket connections. + +Code specific to this example: + + * [useStargateHandlers](src/hooks/useStargateHandlers.ts) - Example handlers for the "GET DATA" and "EXPORT FILE" commands, showing how to send data to [import components](https://help.shapediver.com/doc/import-components) and how to get data from [export components](https://help.shapediver.com/doc/download-export). + * [HomePage](src/pages/HomePage.tsx) - Example page, showing how to make use of the hooks. + +## ShapeDiver SDKs used + + * [Platform SDK](https://www.npmjs.com/package/@shapediver/sdk.platform-api-sdk-v1) + * [Geometry SDK](https://www.npmjs.com/package/@shapediver/sdk.geometry-api-sdk-v2) + * [Stargate SDK](https://www.npmjs.com/package/@shapediver/sdk.stargate-sdk-v1) + + +## Planned extensions + + * Extend the "GET DATA" handler by an example on how to send data to [structured inputs](https://help.shapediver.com/doc/inputs-and-outputs#Defininginputs-Usefloatingparametersasstructuredinputs). + * Implement an example "BAKE DATA" handler, allowing to get structured data from [output components](https://help.shapediver.com/doc/shapediver-output#ShapeDiverOutput-clientUsagewithdesktopclients). + +## Related links + +You can find a similar example for connecting .NET applications to ShapeDiver [here](https://github.com/shapediver/StargateDotNetClientExample). + + diff --git a/StargateWebClientExampleRhino8.ghx b/StargateWebClientExampleRhino8.ghx new file mode 100644 index 0000000..458e108 --- /dev/null +++ b/StargateWebClientExampleRhino8.ghx @@ -0,0 +1,3104 @@ + + + + + + + + 0 + 2 + 2 + + + + + + + 1 + 0 + 8 + + + + + + d8e301ff-26fb-4246-b5cf-16f9a9d6a566 + Shaded + 1 + + 100;150;0;0 + + + 100;0;150;0 + + + + + + 638863513631962980 + + true + AppBuilderStargateTestRhino8.ghx + + + + + 0 + + + + + + 493 + -950 + + 1.2861298 + + + + + 0 + + + + + + + 0 + + + + + 0 + 0 + 0 + 0b0b9a70-81f1-4e36-8620-acb481be85db + Gltf2 + 1.0.0007 + 0 + 7.38.24338.17001 + 0 + 1.27.0.0 + 1.27.0.0 + + + + + 2 + + + + + Robert McNeel & Associates + 00000000-0000-0000-0000-000000000000 + Grasshopper + 8.23.25251.13001 + + + + + ShapeDiverForGrasshopper, Version=1.29.0.0, Culture=neutral, PublicKeyToken=null + 1.29.0.0 + ShapeDiver + b9116316-c87d-4212-a382-bc40035939bb + ShapeDiverForGrasshopper + 1.29.0-beta.3 + + + + + + + 26 + + + + + 40352d2e-fe0a-4205-89a6-1afc0d2c871e + b9116316-c87d-4212-a382-bc40035939bb + Download Export + + + + + Let users download a file from the ShapeDiver viewer. +CAUTION: The exported data will be publicly available from the ShapeDiver viewer. Use the email export if you want the exported data to be kept private. + true + badf662f-57ce-4df1-915b-063e129eac86 + Download Export + SG_Download 3dm + + + + + + 1117 + 1037 + 46 + 64 + + + 1149 + 1069 + + + + + + 2 + Data (geometry, text, bitmap or document) to be exported. + cfa7511e-5f55-4c0f-8d76-dbb90a8e463b + Data + D + false + af9cdd9b-35a8-4bdc-b688-fedc044c3085 + 1 + + + + + + 1119 + 1039 + 15 + 20 + + + 1128 + 1049 + + + + + + + + 2 + Format and export options for the exported file. + ed3456c6-003f-4d57-aa06-b127ac0f28c4 + Options + O + true + d8e00fc1-17b3-4804-ace9-ee265990e0aa + 1 + + + + + + 1119 + 1059 + 15 + 20 + + + 1128 + 1069 + + + + + + + + 2 + Name of the exported file. + 8fff16d0-5d9b-499d-8d8b-36b856e92212 + Name + N + true + ce0e6255-64e2-4867-9c5a-ef34a23d14b2 + 1 + + + + + + 1119 + 1079 + 15 + 20 + + + 1128 + 1089 + + + + + + + + + + + + b64e8d69-28f8-48fd-a8d0-70c1be15d25d + b9116316-c87d-4212-a382-bc40035939bb + 3dm Export Options + + + + + Export to a 3dm file and configure which export options to use. + true + 33e6c86b-92d2-4df8-a8e7-10724ac27a5c + 3dm Export Options + Export3dm + + + + + + 984 + 1056 + 66 + 28 + + + 1015 + 1070 + + + + + + Rhino version for the 3dm export. + c29c81da-e9d6-41d4-a0f0-1066653b0f91 + Version + V + true + 0 + + + + + + 986 + 1058 + 14 + 24 + + + 994.5 + 1070 + + + + + + 1 + + + + + 1 + {0} + + + + + 7 + + + + + + + + + + + User defined export options + d8e00fc1-17b3-4804-ace9-ee265990e0aa + Options + O + false + 0 + + + + + + 1030 + 1058 + 18 + 24 + + + 1039 + 1070 + + + + + + + + + + + + a42d8653-ac54-4352-ba32-ec7effb2f647 + b9116316-c87d-4212-a382-bc40035939bb + Import Geometry + + + + + Let users upload a geometry file from the ShapeDiver viewer (uses headless documents, requires Rhino 7). + 624d465c-d46a-46d4-bc37-172998f4cc2d + Import Geometry + SG_Import DWG + 52428800 + application/dwg + + + + + + 221 + 1292 + 69 + 64 + + + 253 + 1324 + + + + + + 2 + The default URI to read from if no file is set. + 6d21ef8e-5f6b-4525-9a58-13f87a00d1b4 + URI + U + true + 0 + + + + + + 223 + 1294 + 15 + 60 + + + 232 + 1324 + + + + + + + + 1 + The imported object. + 59dce463-f9d8-4633-b48b-288a843e61b1 + Object + O + false + 0 + + + + + + 268 + 1294 + 20 + 20 + + + 278 + 1304 + + + + + + + + 1 + The imported material definition. + 38e62cd1-7826-4f69-b91c-95cf65d8d070 + Material + M + false + 0 + + + + + + 268 + 1314 + 20 + 20 + + + 278 + 1324 + + + + + + + + Name of the file + 7605f91f-1f37-4ed6-83db-3179bf41b96d + Filename + F + false + 0 + + + + + + 268 + 1334 + 20 + 20 + + + 278 + 1344 + + + + + + + + + + + + 4681a95e-ea17-4d21-83ed-f558c41eb67f + b9116316-c87d-4212-a382-bc40035939bb + Import Text + + + + + Let users upload a text file from the ShapeDiver viewer. + 0f20e01a-94a1-47b4-b6d7-ac1d64096e7a + Import Text + SG_Import JSON + 52428800 + application/json + + + + + + 228 + 1179 + 65 + 44 + + + 260 + 1201 + + + + + + 2 + The default URI to read from if no file is set. + 4c62cd7a-cbe4-4c59-9c03-a76e494e94ac + URI + U + true + 0 + + + + + + 230 + 1181 + 15 + 40 + + + 239 + 1201 + + + + + + + + Imported text + d340df04-0b2a-4d14-b754-9670b578b895 + Text + T + false + 0 + + + + + + 275 + 1181 + 16 + 20 + + + 283 + 1191 + + + + + + + + Name of the file + cab01277-6933-4a38-b4bd-c63625ae48af + Filename + F + false + 0 + + + + + + 275 + 1201 + 16 + 20 + + + 283 + 1211 + + + + + + + + + + + + 40352d2e-fe0a-4205-89a6-1afc0d2c871e + b9116316-c87d-4212-a382-bc40035939bb + Download Export + + + + + Let users download a file from the ShapeDiver viewer. +CAUTION: The exported data will be publicly available from the ShapeDiver viewer. Use the email export if you want the exported data to be kept private. + true + 2bfcb393-ca83-40b5-82fe-08d05335832d + Download Export + SG_Download json + + + + + + 1117 + 1176 + 46 + 64 + + + 1149 + 1208 + + + + + + 2 + Data (geometry, text, bitmap or document) to be exported. + 5f78cd21-8c8c-4c3e-aa3c-47f13383a5d5 + Data + D + false + d340df04-0b2a-4d14-b754-9670b578b895 + 1 + + + + + + 1119 + 1178 + 15 + 20 + + + 1128 + 1188 + + + + + + + + 2 + Format and export options for the exported file. + 4278ad87-75a9-45f6-9314-723d4e1075bf + Options + O + true + 33bf7804-068e-4b71-aa79-1428a8c6bc40 + 1 + + + + + + 1119 + 1198 + 15 + 20 + + + 1128 + 1208 + + + + + + + + 2 + Name of the exported file. + 84176cae-4077-4882-88f1-c293490555b0 + Name + N + true + 27f45e6e-6246-4759-aa21-ba355367ace8 + 1 + + + + + + 1119 + 1218 + 15 + 20 + + + 1128 + 1228 + + + + + + + + + + + + 969f41a1-e380-43be-8c99-1e2aedc15124 + b9116316-c87d-4212-a382-bc40035939bb + Text Export Options + + + + + Export to a text file and configure which export options to use. + e3697ad2-e393-4cdd-8c1f-d72594e18f9a + false + Text Export Options + ExportTxt + + + + + + 869 + 1202 + 76 + 64 + + + 910 + 1234 + + + + + + Export file extension. + 4cf416c6-d3bf-4917-9943-f8b81b760788 + Extension + Ext + true + 0 + + + + + + 871 + 1204 + 24 + 20 + + + 884.5 + 1214 + + + + + + 1 + + + + + 1 + {0} + + + + + false + json + + + + + + + + + + + Character encoding of line endings. + 06b4c5c6-abb8-4db4-9222-95cd328af1c4 + End of Line + EoL + true + 0 + + + + + + 871 + 1224 + 24 + 20 + + + 884.5 + 1234 + + + + + + 1 + + + + + 1 + {0} + + + + + false + CRLF + + + + + + + + + + + File encoding (ASCII or UTF-8). + b1a2bcea-ac7c-489d-9a2f-b7262b22fa7b + Encoding + E + true + 0 + + + + + + 871 + 1244 + 24 + 20 + + + 884.5 + 1254 + + + + + + 1 + + + + + 1 + {0} + + + + + false + UTF_8 + + + + + + + + + + + User defined export options + 33bf7804-068e-4b71-aa79-1428a8c6bc40 + Options + O + false + 0 + + + + + + 925 + 1204 + 18 + 60 + + + 934 + 1234 + + + + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + 27f45e6e-6246-4759-aa21-ba355367ace8 + Panel + + false + 0 + 0 + test.json + + + + + + 961 + 1277 + 82 + 20 + + 0 + 0 + 0 + + 961.8005 + 1277.733 + + + + + + + 255;255;250;90 + + true + true + true + false + false + true + + + + + + + + + 40352d2e-fe0a-4205-89a6-1afc0d2c871e + b9116316-c87d-4212-a382-bc40035939bb + Download Export + + + + + Let users download a file from the ShapeDiver viewer. +CAUTION: The exported data will be publicly available from the ShapeDiver viewer. Use the email export if you want the exported data to be kept private. + true + e8a338f3-a13c-4fde-bd0e-0e8566498266 + Download Export + SG_Download dwg + + + + + + 1108 + 1341 + 46 + 64 + + + 1140 + 1373 + + + + + + 2 + Data (geometry, text, bitmap or document) to be exported. + 2cbf0361-65c7-4583-8c07-3eb7f4666719 + Data + D + false + 59dce463-f9d8-4633-b48b-288a843e61b1 + 1 + + + + + + 1110 + 1343 + 15 + 20 + + + 1119 + 1353 + + + + + + + + 2 + Format and export options for the exported file. + 2dadfd22-b414-42c4-8ae5-a48db2988120 + Options + O + true + cefc5e58-92f9-44b6-bb1b-cf6957ce90bc + 1 + + + + + + 1110 + 1363 + 15 + 20 + + + 1119 + 1373 + + + + + + + + 2 + Name of the exported file. + 29e2852e-8fa9-4b9e-8159-5cbd6e404543 + Name + N + true + 4f5b6a27-7902-4fa8-bfee-98378ea4b571 + 1 + + + + + + 1110 + 1383 + 15 + 20 + + + 1119 + 1393 + + + + + + + + + + + + 21a63243-f490-4621-aaa1-52246deb2745 + b9116316-c87d-4212-a382-bc40035939bb + Drawing Export Options + + + + + Export to Autodesk drawing file formats and configure which export options to use. + 868f9dd5-7c67-42eb-ba48-d75f511e2823 + false + Drawing Export Options + ExportDwgDxf + + + + + + 944 + 1584 + 76 + 384 + + + 985 + 1776 + + + + + + Format of the exported file (either dxf of dwg). + 455a336a-d80a-42fc-9685-45792388bacf + Format + F + true + 53169a57-1a3b-43b8-8bcc-011319f9cf5e + 1 + + + + + + 946 + 1586 + 24 + 20 + + + 959.5 + 1596 + + + + + + 1 + + + + + 1 + {0} + + + + + false + dxf + + + + + + + + + + + Export surfaces as curves, meshes or solids. + 5bbc0a39-7a3c-499c-a91d-4d58c0671f91 + Save Surfaces As + Ss + true + 4bebac4f-3c5f-431f-b323-6e83ca7004ee + 1 + + + + + + 946 + 1606 + 24 + 20 + + + 959.5 + 1616 + + + + + + 1 + + + + + 1 + {0} + + + + + false + Curves + + + + + + + + + + + Export meshes as meshes or 3D Faces. + f08413ff-8864-4a97-bba8-c3f43f330b5b + Save Meshes As + Sm + true + d44bf763-b66b-4855-aefa-1a47a7a1028b + 1 + + + + + + 946 + 1626 + 24 + 20 + + + 959.5 + 1636 + + + + + + 1 + + + + + 1 + {0} + + + + + false + Meshes + + + + + + + + + + + Project geometry to the World XY plane. + 9ed5638a-72b0-4f93-8351-6f9fe15f054f + Project + P + true + 0 + + + + + + 946 + 1646 + 24 + 20 + + + 959.5 + 1656 + + + + + + 1 + + + + + 1 + {0} + + + + + false + + + + + + + + + + + Use full layer paths (Parent$Child) or layer names only (Child). + 299ce199-bfdc-476b-8ab8-be5a8ff3a25c + Full Layer Paths + Fl + true + 0 + + + + + + 946 + 1666 + 24 + 20 + + + 959.5 + 1676 + + + + + + 1 + + + + + 1 + {0} + + + + + false + + + + + + + + + + + Color by RGB or the AutoCAD Index. + 6bcc2fe9-167a-44e2-9f9a-e7479173df85 + Color Method + C + true + 6e8a72ec-9578-4962-af43-8d53cb8ea1aa + 1 + + + + + + 946 + 1686 + 24 + 20 + + + 959.5 + 1696 + + + + + + 1 + + + + + 1 + {0} + + + + + false + RGB + + + + + + + + + + + Preserve Arc normals or flip them to +Z. + 65d1ed7c-c89e-4acb-a85a-fd2439cfeb94 + Arc Normals + An + true + 1e8cd2f5-8342-410f-a341-75abf6356eaa + 1 + + + + + + 946 + 1706 + 24 + 20 + + + 959.5 + 1716 + + + + + + 1 + + + + + 1 + {0} + + + + + false + Preserve + + + + + + + + + + + Use LWPolylines when possible. + e07e2d4b-89e8-4879-a058-89bc9b323e91 + LWPolylines + Lp + true + 0 + + + + + + 946 + 1726 + 24 + 20 + + + 959.5 + 1736 + + + + + + 1 + + + + + 1 + {0} + + + + + false + + + + + + + + + + + How to export Lines. + 0a884296-3771-4632-8c81-a0873ad1f773 + Export Lines As + El + true + fca7a863-6ebe-4c44-b8ba-a1ad92cc916e + 1 + + + + + + 946 + 1746 + 24 + 20 + + + 959.5 + 1756 + + + + + + 1 + + + + + 1 + {0} + + + + + false + Lines + + + + + + + + + + + How to export Arcs. + 679d0981-ff53-49e6-9880-d731a5f66b2f + Export Arcs As + Ea + true + 43fdcec0-7729-4d04-91fc-10d5cd06b929 + 1 + + + + + + 946 + 1766 + 24 + 20 + + + 959.5 + 1776 + + + + + + 1 + + + + + 1 + {0} + + + + + false + Arcs + + + + + + + + + + + How to export Polylines. + 63edf650-4201-40b1-9178-a01cb58a3f54 + Export Polylines As + Epl + true + 1cfe0225-0f63-4aae-82a5-9007399f4e44 + 1 + + + + + + 946 + 1786 + 24 + 20 + + + 959.5 + 1796 + + + + + + 1 + + + + + 1 + {0} + + + + + false + Polylines + + + + + + + + + + + How to export Curves. + 0a30fd66-5e57-4cb9-b821-ab0d201e871d + Export Curves As + Ec + false + 465709ae-f6e2-415c-b00b-0315effbb792 + 1 + + + + + + 946 + 1806 + 24 + 20 + + + 959.5 + 1816 + + + + + + 1 + + + + + 1 + {0} + + + + + false + Splines + + + + + + + + + + + How to export Polycurves. + daf93c8f-f15e-4270-8d21-e5aeca808cb5 + Export Polycurves As + Epc + true + 75fcb542-93ab-4248-92bc-b71d37976a58 + 1 + + + + + + 946 + 1826 + 24 + 20 + + + 959.5 + 1836 + + + + + + 1 + + + + + 1 + {0} + + + + + false + Splines + + + + + + + + + + + Max angle to use for curve tessellation. +Leave empty to not use a max angle. + 7e563035-1370-4161-b639-aa69ef314912 + Max angle + M + true + 0 + + + + + + 946 + 1846 + 24 + 20 + + + 959.5 + 1856 + + + + + + + + Chord height to use for curve tessellation. +Leave empty to not use a chord height. + 55a3e217-cfbd-4374-8c49-0cbee6092211 + Chord Height + Ch + true + 0 + + + + + + 946 + 1866 + 24 + 20 + + + 959.5 + 1876 + + + + + + + + Segment length to use for curve tessellation. +Leave empty to not use a segment length. + 9dd99e71-89ff-4c2d-a57e-ad8dbab989ac + Segment Length + Sl + true + 0 + + + + + + 946 + 1886 + 24 + 20 + + + 959.5 + 1896 + + + + + + + + Explode Polycurves. + f90702d8-d58e-47de-8740-d1aa7bf33d1b + Explode Polycurves + Ep + true + 0 + + + + + + 946 + 1906 + 24 + 20 + + + 959.5 + 1916 + + + + + + 1 + + + + + 1 + {0} + + + + + false + + + + + + + + + + + Split curves at kinks. + 120dab4b-d945-4d48-8160-11dca445dfa5 + Split at Kinks + Sk + true + 0 + + + + + + 946 + 1926 + 24 + 20 + + + 959.5 + 1936 + + + + + + 1 + + + + + 1 + {0} + + + + + false + + + + + + + + + + + Tolerance for simplifying lines and arcs. +Leave empty to not simplify lines and arcs. + ec418266-6c37-4053-a66f-67b7c9c1fd2f + Simplify Tolerance + St + true + 0 + + + + + + 946 + 1946 + 24 + 20 + + + 959.5 + 1956 + + + + + + + + User defined export options + cefc5e58-92f9-44b6-bb1b-cf6957ce90bc + Options + O + false + 0 + + + + + + 1000 + 1586 + 18 + 380 + + + 1009 + 1776 + + + + + + + + + + + + 00027467-0d24-4fa7-b178-8dc0ac5f42ec + Value List + + + + + Provides a list of preset values to choose from + 53169a57-1a3b-43b8-8bcc-011319f9cf5e + 2 + 1 + Value List + Format + false + 0 + + + + + "dxf" + dxf + false + + + + + "dwg" + dwg + true + + + + + + 652 + 1567 + 167 + 22 + + + 738 + 1567 + + + + + + + + + + 00027467-0d24-4fa7-b178-8dc0ac5f42ec + Value List + + + + + Provides a list of preset values to choose from + 4bebac4f-3c5f-431f-b323-6e83ca7004ee + 3 + 1 + Value List + Save Surfaces As + false + 0 + + + + + "Curves" + Curves + true + + + + + "Meshes" + Meshes + false + + + + + "Solids" + Solids + false + + + + + + 434 + 1589 + 292 + 22 + + + 613 + 1589 + + + + + + + + + + 00027467-0d24-4fa7-b178-8dc0ac5f42ec + Value List + + + + + Provides a list of preset values to choose from + d44bf763-b66b-4855-aefa-1a47a7a1028b + 2 + 1 + Value List + Save Meshes As + false + 0 + + + + + "Meshes" + Meshes + true + + + + + "3Dfaces" + 3Dfaces + false + + + + + + 445 + 1611 + 288 + 22 + + + 617 + 1611 + + + + + + + + + + 00027467-0d24-4fa7-b178-8dc0ac5f42ec + Value List + + + + + Provides a list of preset values to choose from + 6e8a72ec-9578-4962-af43-8d53cb8ea1aa + 2 + 1 + Value List + Color Method + false + 0 + + + + + "RGB" + RGB + true + + + + + "ACI" + ACI + false + + + + + + 519 + 1677 + 233 + 22 + + + 672 + 1677 + + + + + + + + + + 00027467-0d24-4fa7-b178-8dc0ac5f42ec + Value List + + + + + Provides a list of preset values to choose from + 1e8cd2f5-8342-410f-a341-75abf6356eaa + 2 + 1 + Value List + Arc Normals + false + 0 + + + + + "Preserve" + Preserve + true + + + + + "Flip" + Flip + false + + + + + + 510 + 1699 + 258 + 22 + + + 647 + 1699 + + + + + + + + + + 00027467-0d24-4fa7-b178-8dc0ac5f42ec + Value List + + + + + Provides a list of preset values to choose from + fca7a863-6ebe-4c44-b8ba-a1ad92cc916e + 4 + 1 + Value List + Export Lines As + false + 0 + + + + + "Lines" + Lines + true + + + + + "Polylines" + Polylines + false + + + + + "3Dpolylines" + 3Dpolylines + false + + + + + "Splines" + Splines + false + + + + + + 424 + 1743 + 317 + 22 + + + 588 + 1743 + + + + + + + + + + 00027467-0d24-4fa7-b178-8dc0ac5f42ec + Value List + + + + + Provides a list of preset values to choose from + 43fdcec0-7729-4d04-91fc-10d5cd06b929 + 6 + 1 + Value List + Export Arcs As + false + 0 + + + + + "Arcs" + Arcs + true + + + + + "Lines" + Lines + false + + + + + "Polylines" + Polylines + false + + + + + "Polybulges" + Polybulges + false + + + + + "3Dpolylines" + 3Dpolylines + false + + + + + "Splines" + Splines + false + + + + + + 438 + 1765 + 310 + 22 + + + 595 + 1765 + + + + + + + + + + 00027467-0d24-4fa7-b178-8dc0ac5f42ec + Value List + + + + + Provides a list of preset values to choose from + 1cfe0225-0f63-4aae-82a5-9007399f4e44 + 4 + 1 + Value List + Export Polylines As + false + 0 + + + + + "Polylines" + Polylines + true + + + + + "Lines" + Lines + false + + + + + "3Dpolylines" + 3Dpolylines + false + + + + + "Splines" + Splines + false + + + + + + 352 + 1787 + 353 + 22 + + + 552 + 1787 + + + + + + + + + + 00027467-0d24-4fa7-b178-8dc0ac5f42ec + Value List + + + + + Provides a list of preset values to choose from + 465709ae-f6e2-415c-b00b-0315effbb792 + 4 + 1 + Value List + Export Curves As + false + 0 + + + + + "Splines" + Splines + true + + + + + "Lines" + Lines + false + + + + + "3Dpolylines" + 3Dpolylines + false + + + + + "Polylines" + Polylines + false + + + + + + 392 + 1809 + 333 + 22 + + + 572 + 1809 + + + + + + + + + + 00027467-0d24-4fa7-b178-8dc0ac5f42ec + Value List + + + + + Provides a list of preset values to choose from + 75fcb542-93ab-4248-92bc-b71d37976a58 + 6 + 1 + Value List + Export Polycurves As + false + 0 + + + + + "Splines" + Splines + true + + + + + "Lines" + Lines + false + + + + + "Arcs" + Arcs + false + + + + + "Polylines" + Polylines + false + + + + + "Polybulges" + Polybulges + false + + + + + "3Dpolylines" + 3Dpolylines + false + + + + + + 316 + 1831 + 371 + 22 + + + 534 + 1831 + + + + + + + + + + c552a431-af5b-46a9-a8a4-0fcbc27ef596 + Group + + + + + 1 + + 150;170;135;255 + + A group of Grasshopper objects + 53169a57-1a3b-43b8-8bcc-011319f9cf5e + 4bebac4f-3c5f-431f-b323-6e83ca7004ee + d44bf763-b66b-4855-aefa-1a47a7a1028b + 6e8a72ec-9578-4962-af43-8d53cb8ea1aa + 1e8cd2f5-8342-410f-a341-75abf6356eaa + fca7a863-6ebe-4c44-b8ba-a1ad92cc916e + 43fdcec0-7729-4d04-91fc-10d5cd06b929 + 1cfe0225-0f63-4aae-82a5-9007399f4e44 + 465709ae-f6e2-415c-b00b-0315effbb792 + 75fcb542-93ab-4248-92bc-b71d37976a58 + 10 + 35986252-b6b0-47ad-95ec-228472699985 + Group + ignore + + + + + + + + + + 59e0b89a-e487-49f8-bab8-b5bab16be14c + Panel + + + + + A panel for custom notes and text values + 4f5b6a27-7902-4fa8-bfee-98378ea4b571 + Panel + + false + 0 + 0 + test.dwg + + + + + + 903 + 1392 + 76 + 20 + + 0 + 0 + 0 + + 903 + 1392.5 + + + + + + + 255;255;250;90 + + true + true + true + false + false + true + + + + + + + + + a42d8653-ac54-4352-ba32-ec7effb2f647 + b9116316-c87d-4212-a382-bc40035939bb + Import Geometry + + + + + Let users upload a geometry file from the ShapeDiver viewer (uses headless documents, requires Rhino 7). + 4ea44cc5-5183-490d-8351-48e4ac8847d2 + Import Geometry + SG_Import Geometry + 52428800 + application/3dm,application/3ds,application/ai,application/amf,application/dgn,application/dwg,application/dxf,application/fbx,application/off,application/pdf,application/ply,application/postscript,application/skp,application/sla,application/slc,application/step,application/vda,application/wavefront-obj,image/svg+xml,model/3mf,model/iges,model/stl,model/gltf-binary + + + + + + 224 + 1061 + 69 + 64 + + + 256 + 1093 + + + + + + 2 + The default URI to read from if no file is set. + 53269117-1892-46e5-b9a5-85957eb1a419 + URI + U + true + 0 + + + + + + 226 + 1063 + 15 + 60 + + + 235 + 1093 + + + + + + + + 1 + The imported object. + af9cdd9b-35a8-4bdc-b688-fedc044c3085 + Object + O + false + 0 + + + + + + 271 + 1063 + 20 + 20 + + + 281 + 1073 + + + + + + + + 1 + The imported material definition. + 75bc53b6-9b1d-4af6-a8e6-949d06799963 + Material + M + false + 0 + + + + + + 271 + 1083 + 20 + 20 + + + 281 + 1093 + + + + + + + + Name of the file + ce0e6255-64e2-4867-9c5a-ef34a23d14b2 + Filename + F + false + 0 + + + + + + 271 + 1103 + 20 + 20 + + + 281 + 1113 + + + + + + + + + + + + 8e260f04-39c4-48a1-9b8c-bbec04a7088b + b9116316-c87d-4212-a382-bc40035939bb + ​​​​​​​glTF 2.0 Display + + + + + Display geometry in ShapeDiver's viewer using glTF 2.0 materials. + +Change the nickname of this component to create a named output, which can be used in ShapeDiver's Viewer API. + 7b922621-a940-403f-8ced-5fd9632da257 + ​​​​​​​glTF 2.0 Display + Import DWG + + + + + + 676 + 1302 + 48 + 44 + + + 710 + 1324 + + + + + + 2 + All of the geometry coming in here will be converted to meshes, polylines, or points and included in a single glTF asset to be displayed in ShapeDiver's Viewer. + 731676ac-b51d-485a-8165-3c4e03881548 + Geometry + G + false + 59dce463-f9d8-4633-b48b-288a843e61b1 + 1 + + + + + + 678 + 1304 + 17 + 20 + + + 688 + 1314 + + + + + + + + 2 + List of materials to be assigned to branches of geometry. + 92ed6cba-989e-4f9f-8f20-e7ad9126781f + Material + M + true + 38e62cd1-7826-4f69-b91c-95cf65d8d070 + 1 + + + + + + 678 + 1324 + 17 + 20 + + + 688 + 1334 + + + + + + + + + + + + 8e260f04-39c4-48a1-9b8c-bbec04a7088b + b9116316-c87d-4212-a382-bc40035939bb + ​​​​​​​glTF 2.0 Display + + + + + Display geometry in ShapeDiver's viewer using glTF 2.0 materials. + +Change the nickname of this component to create a named output, which can be used in ShapeDiver's Viewer API. + bb853f86-4adc-42e2-b4f0-d0fcddde924f + ​​​​​​​glTF 2.0 Display + Import Geometry + + + + + + 785 + 938 + 48 + 44 + + + 819 + 960 + + + + + + 2 + All of the geometry coming in here will be converted to meshes, polylines, or points and included in a single glTF asset to be displayed in ShapeDiver's Viewer. + 2ac05ea6-aace-43dd-ac1c-f6b1a45f05a8 + Geometry + G + false + af9cdd9b-35a8-4bdc-b688-fedc044c3085 + 1 + + + + + + 787 + 940 + 17 + 20 + + + 797 + 950 + + + + + + + + 2 + List of materials to be assigned to branches of geometry. + f74fe398-0beb-4d36-85f6-feef0aec867e + Material + M + true + 75bc53b6-9b1d-4af6-a8e6-949d06799963 + 1 + + + + + + 787 + 960 + 17 + 20 + + + 797 + 970 + + + + + + + + + + + + 7f5c6c55-f846-4a08-9c9a-cfdc285cc6fe + Scribble + + + + + true + + 93.51926 + 907.35925 + + + 619.86194 + 907.35925 + + + 619.86194 + 982.9443 + + + 93.51926 + 982.9443 + + A quick note + Microsoft Sans Serif + 1f15073d-09c6-441c-ae84-0245bd397691 + false + Scribble + Scribble + 25 + Prefix the names of the "Import" components +with "SG_" to enable the use of Stargate +clients for them + + + + + + + 88.51926 + 902.35925 + 536.34265 + 85.58502 + + + 93.51926 + 907.35925 + + + + + + + + + + 7f5c6c55-f846-4a08-9c9a-cfdc285cc6fe + Scribble + + + + + true + + 1024.8881 + 915.8042 + + + 1552.6835 + 915.8042 + + + 1552.6835 + 991.39014 + + + 1024.8881 + 991.39014 + + A quick note + Microsoft Sans Serif + 2137e834-32e2-440d-b373-31348dbef132 + false + Scribble + Scribble + 25 + Prefix the names of the "Export" components +with "SG_" to enable the use of Stargate +clients for them + + + + + + + 1019.88806 + 910.8042 + 537.7954 + 85.58594 + + + 1024.8881 + 915.8042 + + + + + + + + + + + + + + + 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 + + + + + \ No newline at end of file diff --git a/deploy-test.sh b/deploy-test.sh new file mode 100755 index 0000000..66a1914 --- /dev/null +++ b/deploy-test.sh @@ -0,0 +1,92 @@ +#!/bin/bash +MAIN_TARGET="main" + +# Function to build and deploy a test page for the Stargate Web Client example +# $1: prefix - the prefix for the deployment, "stargate/v1/$MAIN_TARGET" +# $2: version - the version of the deployment (branch name or npm version) +# $3: deploying_branch - 1 if we are deploying a branch, 0 if we are deploying a version +build_and_deploy() { + prefix=$1 + version=$2 + deploying_branch=$3 + + echo "Building Stargate Web Client example version $version with prefix $prefix" + vite build --base=$prefix/$version/ + if [ $? -ne 0 ]; then + echo "Build failed." + exit 1 + fi + + if [ -z "$APPBUILDER_BUCKET" ]; then + echo "APPBUILDER_BUCKET environment variable is not set." + exit 1 + fi + + cachecontrol="public, max-age=31536000, immutable" + # if we are deploying a branch, we need to change the cache control, compared to a version + if [ $deploying_branch -eq 1 ]; then + cachecontrol="public, max-age=0, s-maxage=86400" + fi + + echo "Deploying to version $version with prefix $prefix to bucket $APPBUILDER_BUCKET" + + # depending on the prefix, we need to deploy to different locations + if [ $prefix == "stargate/v1/$MAIN_TARGET" ]; then + aws s3 sync ./dist s3://$APPBUILDER_BUCKET/appbuilder/$prefix/$version/ --region us-east-1 --cache-control "$cachecontrol" + touch empty + aws s3 cp empty s3://$APPBUILDER_BUCKET/appbuilder/$prefix/$version --region us-east-1 \ + --website-redirect https://appbuilder.shapediver.com/$prefix/$version/ --cache-control "$cachecontrol" + aws s3 cp empty s3://$APPBUILDER_BUCKET/appbuilder/$prefix/.invalidate --region us-east-1 \ + --cache-control "$cachecontrol" + rm empty + else + echo "Unsupported prefix for deployment." + exit 1 + fi +} + +# load environment variables from .env file +if [ -f .env ]; then + export $(grep -v '^#' .env | sed 's/#.*//' | sed 's/^ *//;s/ *$//' | xargs) +fi + +# where should we deploy? +prefix=$1 +if [ -z "$prefix" ]; then + echo "Please specify a prefix." + exit 1 +fi + +# check for git changes +if [[ -n $(git status --porcelain) ]]; then + echo "There are uncommitted changes." + exit 1 +fi + +# Get the current branch +branch=$(git rev-parse --abbrev-ref HEAD) + +# npm version +npm_version=$(node -p "require('./package.json').version") +echo "Current npm version: $npm_version" + +deploying_branch=1 + +# If the branch is "development", "staging" or starts with "task/", we use the branch name as the version +if [ "$branch" == "development" ] || [ "$branch" == "staging" ]; then + deploying_branch=1 + version=$branch + + # And we create a new tag with the name "WordPressPlugin@branch" + git tag -fa "StargateWebClient@$branch" -m "Release of branch $branch" + git push origin "StargateWebClient@$branch" --force +elif [[ $branch == task/* ]]; then + deploying_branch=1 + # In this case we have to remove the "task/" prefix + version=${branch#task/} +else + echo "Unsupported branch name." + exit 1 +fi + +build_and_deploy $prefix $version $deploying_branch diff --git a/package.json b/package.json index 378a996..d5be19f 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,10 @@ "type": "module", "homepage": "./", "dependencies": { + "@shapediver/sdk.geometry-api-sdk-v2": "^2.9.1", + "@shapediver/sdk.platform-api-sdk-v1": "^2.28.7", + "@shapediver/sdk.stargate-sdk-v1": "^1.6.1", + "@shapediver/viewer.utils.mime-type": "^1.1.0", "globals": "^16.4.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -16,6 +20,7 @@ "build": "tsc && vite build", "optimize": "tsc && vite optimize --force", "preview": "vite preview", + "deploy-test": "./deploy-test.sh stargate/v1/main", "eslint": "eslint .", "prettier": "prettier --write ./src/ ./*.ts ./eslint.config.mjs ./vite.config.ts ./tsconfig.json ./package.json", "test": "jest" @@ -82,4 +87,4 @@ "node" ] } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index faa71ad..3760121 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,18 @@ settings: excludeLinksFromLockfile: false dependencies: + '@shapediver/sdk.geometry-api-sdk-v2': + specifier: ^2.9.1 + version: 2.9.1 + '@shapediver/sdk.platform-api-sdk-v1': + specifier: ^2.28.7 + version: 2.28.7 + '@shapediver/sdk.stargate-sdk-v1': + specifier: ^1.6.1 + version: 1.6.1 + '@shapediver/viewer.utils.mime-type': + specifier: ^1.1.0 + version: 1.1.0 globals: specifier: ^16.4.0 version: 16.4.0 @@ -2864,6 +2876,60 @@ packages: resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==} dev: false + /@shapediver/api.platform-api-dto-v1@2.28.7: + resolution: {integrity: sha512-uS2a+aV27ZVuB6qneBmKlF3JV9sI4az1ci7uqNWGUxY995AJiclG4Dgkf1cSRL/rQondM9FELhssXqRWQiBLkg==} + dependencies: + '@types/q': 1.5.8 + chargebee-typescript: 2.16.0 + dev: false + + /@shapediver/sdk.geometry-api-sdk-v2@2.9.1: + resolution: {integrity: sha512-D9DBSrCgOtgLCzLyqDnDl08vcCf3e+PSLg1xDlXYvE5Z8JzHfVtgSYT/qoAj/5nCq3+YdFpv62cR09E1fSh7xQ==} + dependencies: + axios: 1.10.0 + transitivePeerDependencies: + - debug + dev: false + + /@shapediver/sdk.platform-api-sdk-v1@2.28.7: + resolution: {integrity: sha512-3F6jm5T5jEjrrSTXgX0hw44e5JVBNOH5i3QFK3ssXLny6Fy8zAIJH2vmNpKgnLfFYd5sXUqTyOts+qW8Q5MHzw==} + dependencies: + '@shapediver/api.platform-api-dto-v1': 2.28.7 + '@shapediver/sdk.geometry-api-sdk-v2': 2.9.1 + axios: 1.12.2 + jwt-decode: 3.1.2 + transitivePeerDependencies: + - debug + dev: false + + /@shapediver/sdk.stargate-sdk-core@1.3.1: + resolution: {integrity: sha512-94Zto7YO7mA1xGPEjMNK6y6eL6iONdFnb7NQYnhq2sz9H06Rs9/RHIXpUjSLOtBbT9L2i1fmUNxharQngG1E6w==} + dependencies: + uuid: 9.0.1 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /@shapediver/sdk.stargate-sdk-v1@1.6.1: + resolution: {integrity: sha512-qM/kOWmLcXrK6uirZH78D41bbNkh1EYtKvkvSM+z/fIsGKZ9fzINw3R5C62IAlDEeM/m5at5S3SrYv+u4DHKrg==} + dependencies: + '@shapediver/sdk.stargate-sdk-core': 1.3.1 + '@types/node': 20.19.17 + '@types/uuid': 9.0.8 + ajv: 8.17.1 + jsonschema: 1.5.0 + uuid: 9.0.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /@shapediver/viewer.utils.mime-type@1.1.0: + resolution: {integrity: sha512-KssQEsaUYiLkMCtbW/vd0CxjmP/8REAUejKL+fvjOrp1Z98MED31wYPeVbOXstFOtrCOdhMGM6mFIN2I4DMzfw==} + dev: false + /@sinclair/typebox@0.24.51: resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} dev: false @@ -3467,6 +3533,10 @@ packages: resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} dev: false + /@types/uuid@9.0.8: + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + dev: false + /@types/ws@8.18.1: resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} dependencies: @@ -4266,6 +4336,26 @@ packages: engines: {node: '>=4'} dev: false + /axios@1.10.0: + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + + /axios@1.12.2: + resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -4692,6 +4782,13 @@ packages: engines: {node: '>=12.20'} dev: false + /chargebee-typescript@2.16.0: + resolution: {integrity: sha512-w0xFnZ3GjMeHlne99tb/EXEEGDyORlMtdS1z7Hmz0g+emsANGI8zWrOXp1q5zqW2FWU+XdeguDgj3OCkmraOAA==} + engines: {node: '>=8.0.0'} + dependencies: + q: 1.5.1 + dev: false + /check-types@11.2.3: resolution: {integrity: sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==} dev: false @@ -6509,6 +6606,17 @@ packages: mime-types: 2.1.35 dev: false + /form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + dev: false + /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -8455,6 +8563,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /jsonschema@1.5.0: + resolution: {integrity: sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==} + dev: false + /jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -8464,6 +8576,10 @@ packages: object.assign: 4.1.7 object.values: 1.2.1 + /jwt-decode@3.1.2: + resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==} + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -10077,6 +10193,10 @@ packages: ipaddr.js: 1.9.1 dev: false + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} dependencies: @@ -11844,6 +11964,11 @@ packages: hasBin: true dev: false + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + /v8-to-istanbul@8.1.1: resolution: {integrity: sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==} engines: {node: '>=10.12.0'} diff --git a/public/test.3dm b/public/test.3dm new file mode 100644 index 0000000..e59d267 Binary files /dev/null and b/public/test.3dm differ diff --git a/public/test.dwg b/public/test.dwg new file mode 100644 index 0000000..9ce1845 Binary files /dev/null and b/public/test.dwg differ diff --git a/public/test.json b/public/test.json new file mode 100644 index 0000000..d02fab0 --- /dev/null +++ b/public/test.json @@ -0,0 +1,3 @@ +{ + "hello": "world" +} \ No newline at end of file diff --git a/screenshots/app-notconnected-parameters.png b/screenshots/app-notconnected-parameters.png new file mode 100644 index 0000000..a16ca1a Binary files /dev/null and b/screenshots/app-notconnected-parameters.png differ diff --git a/screenshots/authenticated-react-app.png b/screenshots/authenticated-react-app.png new file mode 100644 index 0000000..924f038 Binary files /dev/null and b/screenshots/authenticated-react-app.png differ diff --git a/screenshots/authorize-the-app.png b/screenshots/authorize-the-app.png new file mode 100644 index 0000000..95409bb Binary files /dev/null and b/screenshots/authorize-the-app.png differ diff --git a/screenshots/client-selection.png b/screenshots/client-selection.png new file mode 100644 index 0000000..eff3b7d Binary files /dev/null and b/screenshots/client-selection.png differ diff --git a/screenshots/export-to-client.png b/screenshots/export-to-client.png new file mode 100644 index 0000000..515a5bc Binary files /dev/null and b/screenshots/export-to-client.png differ diff --git a/screenshots/grasshoppermodel.png b/screenshots/grasshoppermodel.png new file mode 100644 index 0000000..3e98c2b Binary files /dev/null and b/screenshots/grasshoppermodel.png differ diff --git a/screenshots/import-from-client.png b/screenshots/import-from-client.png new file mode 100644 index 0000000..451f511 Binary files /dev/null and b/screenshots/import-from-client.png differ diff --git a/screenshots/openapp.png b/screenshots/openapp.png new file mode 100644 index 0000000..bec8567 Binary files /dev/null and b/screenshots/openapp.png differ diff --git a/screenshots/startshapediverauth.png b/screenshots/startshapediverauth.png new file mode 100644 index 0000000..c54fb8a Binary files /dev/null and b/screenshots/startshapediverauth.png differ diff --git a/src/hooks/useShapeDiverAuth.ts b/src/hooks/useShapeDiverAuth.ts new file mode 100644 index 0000000..6a74518 --- /dev/null +++ b/src/hooks/useShapeDiverAuth.ts @@ -0,0 +1,313 @@ +import { + create, + isPBInvalidGrantOAuthResponseError, + isPBInvalidRequestOAuthResponseError, + isPBOAuthResponseError, + SdPlatformSdk, +} from "@shapediver/sdk.platform-api-sdk-v1"; +import {useCallback, useEffect, useState} from "react"; + +const refreshTokenKey = "shapediver_refresh_token"; +const codeVerifierKey = "shapediver_code_verifier"; +const oauthStateKey = "shapediver_oauth_state"; +// Note: Local testing is not allowed when using the productive ShapeDiver platform. +// However, you can use a service like https://ngrok.com/ to provide a public URL for your localhost. +// Your public URL will need to be whitelisted by ShapeDiver. Write to us at contact@shapediver.com +const authBaseUrl = + window.location.hostname === "localhost" + ? "https://staging-wwwcdn.us-east-1.shapediver.com" + : "https://www.shapediver.com"; +const authEndPoint = `${authBaseUrl}/oauth/authorize`; +const clientId = "A085FCC5-6EEB-46A6-A381-ADCEDB6E59D6"; // "660310c8-50f4-4f47-bd78-9c7ede8e659b"; + +async function sha256(buffer: Uint8Array): Promise { + return await crypto.subtle.digest("SHA-256", buffer); +} + +function base64UrlEncode(buffer: ArrayBuffer) { + const bytes = new Uint8Array(buffer); + let binary = ""; + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + // see https://developer.mozilla.org/en-US/docs/Glossary/Base64#url_and_filename_safe_base64 + return btoa(binary) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); +} + +function generateRandomString(length: number) { + const charset = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const values = new Uint8Array(length); + crypto.getRandomValues(values); + let result = ""; + for (let i = 0; i < length; i++) { + result += charset[values[i] % charset.length]; + } + return result; +} + +function clearBrowserStorage() { + window.localStorage.removeItem(refreshTokenKey); + window.localStorage.removeItem(codeVerifierKey); + window.localStorage.removeItem(oauthStateKey); +} + +/** Get the URL to which the ShapeDiver Platform should redirect back after authentication */ +function getRedirectUri() { + return window.location.origin + window.location.pathname; +} + +interface Props { + /** Log in automatically if a refresh token is available. */ + autoLogin?: boolean; +} + +type ShapeDiverAuthStateType = + | "not_authenticated" + | "refresh_token_present" + | "authenticated" + | "authentication_in_progress"; + +/** + * Hook to manage authentication with ShapeDiver via OAuth2 Authorization Code Flow with PKCE. + * @returns + */ +export default function useShapeDiverAuth(props?: Props): { + /** The access token. */ + accessToken: string | undefined; + /** Callback for initiating authorization code flow via the ShapeDiver platform. */ + initiateShapeDiverAuth: () => Promise; + /** Error type, if any. */ + error: string | null; + /** Error description, if any. */ + errorDescription: string | null; + /** Authenticate using the current refresh token. No need to call this if autoLogin is true. */ + authUsingRefreshToken: () => Promise; + /** Authentication state. */ + authState: ShapeDiverAuthStateType; + /** ShapeDiver Platform SDK, authenticated or anonymous depending on auth state. */ + platformSdk: SdPlatformSdk; +} { + const {autoLogin = false} = props || {}; + + // check for error and error description in URL parameters + const params = new URLSearchParams(window.location.search); + const [error, setError] = useState(params.get("error")); + const [errorDescription, setErrorDescription] = useState( + params.get("error_description"), + ); + const [codeData, setCodeData] = useState<{ + code: string; + verifier: string; + } | null>(null); + const [accessToken, setAccessToken] = useState(); + const [platformSdk] = useState( + create({clientId, baseUrl: authBaseUrl}), + ); + // try to get refresh token from local storage + const [refreshToken, setRefreshToken_] = useState( + window.localStorage.getItem(refreshTokenKey), + ); + + const setRefreshToken = useCallback((token: string | null) => { + if (token) { + window.localStorage.setItem(refreshTokenKey, token); + } else { + window.localStorage.removeItem(refreshTokenKey); + } + setRefreshToken_(token); + }, []); + + // determine initial auth state + let authState_: ShapeDiverAuthStateType = refreshToken + ? autoLogin + ? "authentication_in_progress" + : "refresh_token_present" + : "not_authenticated"; + + if (error) { + // if there is an error, clear the local storage + clearBrowserStorage(); + } else { + // check if we got a code and state in the URL parameters + const code = params.get("code"); + const state = params.get("state"); + if (state && code) { + // remove code and state from URL to avoid re-processing + params.delete("code"); + params.delete("state"); + const url = new URL(window.location.href); + url.searchParams.delete("code"); + url.searchParams.delete("state"); + window.history.replaceState({}, document.title, url.toString()); + // verify state + const storedState = window.localStorage.getItem(oauthStateKey); + const storedVerifier = window.localStorage.getItem(codeVerifierKey); + if (storedState === null) { + setError("missing stored state"); + setErrorDescription( + "No stored state found, please initiate the authentication flow again.", + ); + } else if (storedVerifier === null) { + setError("missing stored verifier"); + setErrorDescription( + "No stored code verifier found, please initiate the authentication flow again.", + ); + } else if (state === window.localStorage.getItem(oauthStateKey)) { + // state is valid, now exchange the code for a token (handled in useEffect below) + setCodeData({code, verifier: storedVerifier}); + authState_ = "authentication_in_progress"; + } else { + // state is invalid, clear local storage and return error + setError("state mismatch"); + setErrorDescription( + "The returned state does not match the stored state.", + ); + } + window.localStorage.removeItem(oauthStateKey); + window.localStorage.removeItem(codeVerifierKey); + } + } + + // define auth state + const [authState, setAuthState] = + useState(authState_); + + // exchange code for token + useEffect(() => { + if (codeData) { + const getToken = async () => { + try { + const data = + await platformSdk.authorization.authorizationCodePkce( + codeData.code, + codeData.verifier, + getRedirectUri(), + ); + setAccessToken(data.access_token); + setRefreshToken(data.refresh_token ?? null); + setAuthState("authenticated"); + } catch (error) { + if (isPBOAuthResponseError(error)) { + setError(error.error ?? null); + setErrorDescription(error.error_description ?? null); + } else { + setError("Unknown token exchange error"); + setErrorDescription( + error && + typeof error === "object" && + "message" in error && + typeof error.message === "string" + ? error.message + : "Unknown token exchange error", + ); + } + setAccessToken(undefined); + setRefreshToken(null); + setAuthState("not_authenticated"); + } + }; + setCodeData(null); + setAuthState("authentication_in_progress"); + getToken(); + } + }, [codeData, platformSdk]); + + // callback for auth using refresh token + const authUsingRefreshToken = useCallback(async () => { + if (refreshToken) { + // reset state + setError(null); + setErrorDescription(null); + setCodeData(null); + try { + setAuthState("authentication_in_progress"); + const data = + await platformSdk.authorization.refreshToken(refreshToken); + setAccessToken(data.access_token); + setRefreshToken(data.refresh_token ?? null); + setAuthState("authenticated"); + } catch (error) { + if ( + isPBInvalidRequestOAuthResponseError(error) || // <-- thrown if the refresh token is not valid anymore or there is none + isPBInvalidGrantOAuthResponseError(error) // <-- thrown if the refresh token is generally invalid + ) { + setAccessToken(undefined); + setRefreshToken(null); + setAuthState("not_authenticated"); + setError("invalid refresh token"); + setErrorDescription( + "The stored refresh token is invalid, please log in again.", + ); + throw error; + } else { + setAccessToken(undefined); + setRefreshToken(null); + setAuthState("not_authenticated"); + setError("refresh token login failed"); + setErrorDescription( + "The refresh token login failed, please log in again.", + ); + throw error; + } + } + } + }, [refreshToken, platformSdk]); + + // optionally automatically log in + useEffect(() => { + if (autoLogin && refreshToken && !accessToken) authUsingRefreshToken(); + }, [autoLogin, refreshToken, accessToken]); + + // callback for initiating the authorization code flow via the ShapeDiver platform + const initiateShapeDiverAuth = useCallback(async () => { + // reset state + setError(null); + setErrorDescription(null); + setCodeData(null); + setAccessToken(undefined); + setRefreshToken(null); + setAuthState("authentication_in_progress"); + // clear previous tokens and state + clearBrowserStorage(); + // Create a 64 character random string (from characters a-zA-Z0-9), we call this the secret code verifier. + const codeVerifier = generateRandomString(64); + window.localStorage.setItem(codeVerifierKey, codeVerifier); + // get unix timestamp in seconds + const timestamp = Math.floor(Date.now() / 1000); + // create state + const _state = `${codeVerifier}:${authEndPoint}:${clientId}:${timestamp}`; + const encoder = new TextEncoder(); + const state = base64UrlEncode(await sha256(encoder.encode(_state))); + window.localStorage.setItem(oauthStateKey, state); + const code_challenge = base64UrlEncode( + await sha256(encoder.encode(codeVerifier)), + ); + + // construct the redirection URL + const params = new URLSearchParams(); + params.append("state", state); + params.append("response_type", "code"); + params.append("client_id", clientId); + params.append("code_challenge", code_challenge); + params.append("code_challenge_method", "S256"); + params.append("redirect_uri", getRedirectUri()); + const redirectUrl = `${authEndPoint}?${params.toString()}`; + + // redirect to the authorization endpoint + window.location.href = redirectUrl; + }, []); + + return { + accessToken, + initiateShapeDiverAuth, + error, + errorDescription, + authUsingRefreshToken, + authState, + platformSdk, + }; +} diff --git a/src/hooks/useShapeDiverStargate.ts b/src/hooks/useShapeDiverStargate.ts new file mode 100644 index 0000000..730625e --- /dev/null +++ b/src/hooks/useShapeDiverStargate.ts @@ -0,0 +1,367 @@ +import { + Configuration, + ResCreateSessionByTicket, + SessionApi, +} from "@shapediver/sdk.geometry-api-sdk-v2"; +import { + SdPlatformModelGetEmbeddableFields, + SdPlatformSdk, +} from "@shapediver/sdk.platform-api-sdk-v1"; +import { + createSdk, + ISdStargateBakeDataCommandDto, + ISdStargateBakeDataReplyDto, + ISdStargateBakeDataResultEnum, + ISdStargateExportFileCommandDto, + ISdStargateExportFileReplyDto, + ISdStargateExportFileResultEnum, + ISdStargateGetDataCommandDto, + ISdStargateGetDataReplyDto, + ISdStargateGetDataResultEnum, + ISdStargateGetSupportedDataReplyDto, + ISdStargatePrepareModelCommandDto, + ISdStargatePrepareModelReplyDto, + ISdStargatePrepareModelResultEnum, + ISdStargateSdk, + ISdStargateStatusReplyDto, + SdStargateBakeDataCommand, + SdStargateExportFileCommand, + SdStargateGetDataCommand, + SdStargateGetSupportedDataCommand, + SdStargatePrepareModelCommand, + SdStargateStatusCommand, +} from "@shapediver/sdk.stargate-sdk-v1"; +import {useCallback, useEffect, useRef, useState} from "react"; +import packagejson from "../../package.json"; + +const firstActivity = Math.floor(Date.now() / 1000); + +export type SessionData = { + config: Configuration; + session: ResCreateSessionByTicket; +}; + +type ModelIdSessionMapType = { + [key: string]: Promise; +}; + +interface Props { + /** The access token to use. */ + accessToken: string | undefined; + /** The platform SDK to use. */ + platformSdk: SdPlatformSdk; + /** + * Supported data. Use this to specify which ShapeDiver parameter types, + * file endings, and content types are supported by the handlers. + */ + supportedData: Partial; + /** + * Handler for command messages for which no handler is registered. + * Typically there is no need to provide this handler. + * The default handler logs to the browser console. + */ + serverCommandHandler?: (payload: unknown) => void; + /** + * Handler for connection errors, called if an error message has been + * received from the Stargate server. + * The default handler logs to the browser console. + */ + connectionErrorHandler?: (msg: string) => void; + /** + * Handler called when the established connection is closed by the Stargate server + * or other external circumstances. + * The default handler logs to the browser console. + */ + disconnectHandler?: (msg: string) => void; + /** + * Handler for the GET DATA command. The GET DATA command handler + * is called whenever the user requests data for an input (for a parameter) + * from a client application, by clicking on the corresponding UI element. + * @see https://help.shapediver.com/doc/inputs-and-outputs + * + * This extends the default handler by providing session data, + * which can be used to interact with the corresponding + * ShapeDiver model via the Geometry API. + * @param data + * @param sessionData + * @returns + */ + getDataCommandHandler?: ( + data: ISdStargateGetDataCommandDto, + sessionData: SessionData, + ) => Promise; + /** + * Handler for the BAKE DATA command. The BAKE DATA command handler + * is called whenever the user requests baking of data from an output + * to a client application, by clicking on the corresponding UI element. + * @see https://help.shapediver.com/doc/shapediver-output#ShapeDiverOutput-clientUsagewithdesktopclients + * + * This extends the default handler by providing session data, + * which can be used to interact with the corresponding + * ShapeDiver model via the Geometry API. + * @param data + * @param sessionData + * @returns + */ + bakeDataCommandHandler?: ( + data: ISdStargateBakeDataCommandDto, + sessionData: SessionData, + ) => Promise; + /** + * Handler for the EXPORT FILE command. The EXPORT FILE command handler + * is called whenever the user requests the export of a file + * via a client application, by clicking on the corresponding UI element. + * + * This extends the default handler by providing session data, + * which can be used to interact with the corresponding + * ShapeDiver model via the Geometry API. + * @param data + * @param sessionData + * @returns + */ + exportFileCommandHandler?: ( + data: ISdStargateExportFileCommandDto, + sessionData: SessionData, + ) => Promise; +} + +/** + * Hook providing a ShapeDiver Stargate Client implementation. + * @returns + */ +export default function useShapeDiverStargate(props: Props): { + /** + * The Stargate SDK. Typically there is no need to register further + * command handlers, as this is already taken care of by this hook. + */ + stargateSdk: ISdStargateSdk | null; + /** + * True if this Stargate client is currently being used by the user + * from a ShapeDiver App. This is based on the STATUS command, + * which is sent by the App every 30 seconds. If no STATUS command + * has been received for 35 seconds, this property will be set to false. + */ + isActive: boolean; +} { + const { + accessToken, + platformSdk, + supportedData, + serverCommandHandler, + connectionErrorHandler, + disconnectHandler, + getDataCommandHandler, + bakeDataCommandHandler, + exportFileCommandHandler, + } = props; + + const [stargateSdk, setStargateSdk] = useState(null); + const [isActive, setIsActive_] = useState(false); + const timeoutRef = useRef(); + const setIsActive = useCallback(() => { + if (typeof timeoutRef.current === "number") + clearTimeout(timeoutRef.current); + setIsActive_(true); + timeoutRef.current = window.setTimeout( + () => setIsActive_(false), + 35000, + ); + }, []); + const modelIdSessionMapRef = useRef({}); + + const getSessionDataForModelId = useCallback( + async (modelId: string) => { + const get = async (modelId: string) => { + const model = ( + await platformSdk.models.get(modelId, [ + SdPlatformModelGetEmbeddableFields.BackendSystem, + SdPlatformModelGetEmbeddableFields.Ticket, + SdPlatformModelGetEmbeddableFields.TokenExport, + ]) + ).data; + const config = new Configuration({ + accessToken: model.access_token, + basePath: model.backend_system!.model_view_url, + }); + const session = ( + await new SessionApi(config).createSessionByTicket( + model.ticket!.ticket!, + ) + ).data; + + return {config, session}; + }; + + // create a session for the model if none exists yet + if (!modelIdSessionMapRef.current[modelId]) { + modelIdSessionMapRef.current[modelId] = get(modelId); + } + + return modelIdSessionMapRef.current[modelId]; + }, + [platformSdk], + ); + + useEffect(() => { + const init = async (jwt: string, platformSdk: SdPlatformSdk) => { + // get Stargate endpoint to use + const endpoints = (await platformSdk.stargate.getConfig())?.data + .endpoint; + const endpoint = endpoints + ? endpoints[Object.keys(endpoints)[0]] + : "prod-sg.eu-central-1.shapediver.com"; + // create and configure the SDK + const sdk = await createSdk() + .setBaseUrl(endpoint) + .setServerCommandHandler( + serverCommandHandler ?? + ((payload: unknown) => { + console.log("Received Stargate command:", payload); + }), + ) + .setConnectionErrorHandler( + connectionErrorHandler ?? + ((msg: string) => + console.error(`Stargate connection error: ${msg}`)), + ) + .setDisconnectHandler( + disconnectHandler ?? + ((msg: string) => + console.error(`Stargate disconnected: ${msg}`)), + ) + .build(); + // register the client + await sdk.register( + jwt, + "Stargate Web Client", + packagejson.version, + navigator.platform || "", + window.location.hostname, + "", + ); + // register a handler for the status command + new SdStargateStatusCommand(sdk).registerHandler( + async (): Promise => { + setIsActive(); + return { + firstActivity, + latestActivity: Math.floor(Date.now() / 1000), + }; + }, + ); + // register a handler for the get supported data command + new SdStargateGetSupportedDataCommand(sdk).registerHandler( + async (): Promise => ({ + parameterTypes: [], + typeHints: [], + contentTypes: [], + fileExtensions: [], + ...supportedData, + }), + ); + // register a handler for the prepare model command + new SdStargatePrepareModelCommand(sdk).registerHandler( + async ( + data: ISdStargatePrepareModelCommandDto, + ): Promise => { + await getSessionDataForModelId(data.model.id); + + return { + info: { + result: ISdStargatePrepareModelResultEnum.SUCCESS, + }, + }; + }, + ); + // register a handler for the get data command + new SdStargateGetDataCommand(sdk).registerHandler( + async ( + data: ISdStargateGetDataCommandDto, + ): Promise => { + const sessionData = await getSessionDataForModelId( + data.model.id, + ); + if (getDataCommandHandler) { + return getDataCommandHandler(data, sessionData); + } + console.warn( + "Received get data command, but no handler is registered.", + data, + ); + return { + info: { + message: "No handler registered.", + result: ISdStargateGetDataResultEnum.NOTHING, + count: 0, + }, + }; + }, + ); + // register a handler for the bake data command + new SdStargateBakeDataCommand(sdk).registerHandler( + async ( + data: ISdStargateBakeDataCommandDto, + ): Promise => { + const sessionData = await getSessionDataForModelId( + data.model.id, + ); + if (bakeDataCommandHandler) { + return bakeDataCommandHandler(data, sessionData); + } + console.warn( + "Received bake data command, but no handler is registered.", + data, + ); + return { + info: { + message: "No handler registered.", + result: ISdStargateBakeDataResultEnum.NOTHING, + count: 0, + }, + }; + }, + ); + // register a handler for the export file command + new SdStargateExportFileCommand(sdk).registerHandler( + async ( + data: ISdStargateExportFileCommandDto, + ): Promise => { + const sessionData = await getSessionDataForModelId( + data.model.id, + ); + if (exportFileCommandHandler) { + return exportFileCommandHandler(data, sessionData); + } + console.warn( + "Received export file command, but no handler is registered.", + data, + ); + return { + info: { + message: "No handler registered.", + result: ISdStargateExportFileResultEnum.NOTHING, + }, + }; + }, + ); + // store the SDK in state + setStargateSdk(sdk); + }; + if (accessToken && platformSdk) init(accessToken, platformSdk); + }, [ + accessToken, + platformSdk, + supportedData, + serverCommandHandler, + connectionErrorHandler, + disconnectHandler, + getDataCommandHandler, + bakeDataCommandHandler, + exportFileCommandHandler, + ]); + + return { + stargateSdk, + isActive, + }; +} diff --git a/src/hooks/useStargateHandlers.ts b/src/hooks/useStargateHandlers.ts new file mode 100644 index 0000000..9b56284 --- /dev/null +++ b/src/hooks/useStargateHandlers.ts @@ -0,0 +1,212 @@ +import { + ExportApi, + FileApi, + ResComputationStatus, + ResExport, + ResExportDefinitionType, + UtilsApi, +} from "@shapediver/sdk.geometry-api-sdk-v2"; +import { + ISdStargateBakeDataCommandDto, + ISdStargateBakeDataReplyDto, + ISdStargateExportFileCommandDto, + ISdStargateExportFileReplyDto, + ISdStargateExportFileResultEnum, + ISdStargateGetDataCommandDto, + ISdStargateGetDataReplyDto, + ISdStargateGetDataResultEnum, + ISdStargateGetSupportedDataReplyDto, +} from "@shapediver/sdk.stargate-sdk-v1"; +import {fetchFileWithToken} from "@shapediver/viewer.utils.mime-type"; +import {useCallback} from "react"; +import {SessionData} from "./useShapeDiverStargate"; + +/** + * Example files that can be used by the handlers. + */ +const exampleFiles: {filename: string; contentType: string; href: string}[] = [ + { + filename: "test.json", + contentType: "application/json", + href: window.location.origin + window.location.pathname + "test.json", + }, + { + filename: "test.3dm", + contentType: "model/vnd.3dm", + href: window.location.origin + window.location.pathname + "test.3dm", + }, + { + filename: "test.dwg", + contentType: "application/dwg", + href: window.location.origin + window.location.pathname + "test.dwg", + }, +]; + +/** + * Which content types, file extensions, and parameter types are supported by the handlers. + */ +const supportedData: Partial = { + contentTypes: ["application/json", "application/dwg", "model/vnd.3dm"], + fileExtensions: ["json", "3dm", "dwg"], + parameterTypes: ["File"], +}; + +/** + * Hook providing example Stargate handlers. + * @returns + */ +export default function useStargateHandlers(): { + /** + * Types of parameters, content types, and file endings supported by the handlers. + */ + supportedData: Partial; + /** + * Handler for the GET DATA command. + * @param data + * @param sessionData + * @returns + */ + getDataCommandHandler?: ( + data: ISdStargateGetDataCommandDto, + sessionData: SessionData, + ) => Promise; + /** + * Handler for the BAKE DATA command. + * @param data + * @param sessionData + * @returns + */ + bakeDataCommandHandler?: ( + data: ISdStargateBakeDataCommandDto, + sessionData: SessionData, + ) => Promise; + /** + * Handler for the EXPORT FILE command. + * @param data + * @param sessionData + * @returns + */ + exportFileCommandHandler?: ( + data: ISdStargateExportFileCommandDto, + sessionData: SessionData, + ) => Promise; +} { + // example handler for the GET DATA command + const getDataCommandHandler = useCallback( + async ( + {parameter: {id: parameterId}}: ISdStargateGetDataCommandDto, + {config, session}: SessionData, + ): Promise => { + // get the definition of the parameter for which data is requested + const paramDef = session.parameters![parameterId]; + // in this example we handle "File" parameters with "application/json" format + if (paramDef.type === "File") { + // try to find a suitable test file + const testFile = exampleFiles.find((f) => + paramDef.format?.includes(f.contentType), + ); + if (testFile) { + // download the file + const downloadedFile = await new UtilsApi(config).download( + testFile.href, + {responseType: "arraybuffer"}, + ); + // request a file upload URL from the Geometry API + const response = await new FileApi(config).uploadFile( + session.sessionId, + { + [parameterId]: { + size: ( + downloadedFile.data as unknown as ArrayBuffer + ).byteLength, + filename: testFile.filename, + format: testFile.contentType, + }, + }, + ); + // extract the data for the file to be uploaded + const fileData = response.data.asset.file[parameterId]; + // upload the file to the URL provided by the Geometry API + await new UtilsApi(config).upload( + fileData.href, + downloadedFile.data, + testFile.contentType, + testFile.filename, + ); + // return the id of the uploaded file + return { + info: { + message: "File uploaded successfully.", + result: ISdStargateGetDataResultEnum.SUCCESS, + count: 1, + }, + asset: { + id: fileData.id, + }, + }; + } + } + // if we cannot handle the request, return a "nothing" response + return { + info: { + message: "No data available.", + result: ISdStargateGetDataResultEnum.NOTHING, + count: 0, + }, + }; + }, + [], + ); + + // example handler for the EXPORT FILE command + const exportFileCommandHandler = useCallback( + async ( + {parameters, export: {id, index}}: ISdStargateExportFileCommandDto, + {config, session}: SessionData, + ): Promise => { + // get the definition of the export which should be downloaded + const exportDef = session.exports![id]; + if (exportDef.type !== ResExportDefinitionType.DOWNLOAD) + return { + info: { + message: "Export is not of type DOWNLOAD.", + result: ISdStargateExportFileResultEnum.NOTHING, + }, + }; + // request the export + const { + data: {exports: exportResults}, + } = await new ExportApi(config).computeExports(session.sessionId, { + parameters, + exports: [id], + }); + const exportResult = exportResults![id] as ResExport; + if ( + exportResult.status_collect !== ResComputationStatus.SUCCESS || + exportResult.status_computation !== ResComputationStatus.SUCCESS + ) + return { + info: { + message: "Export computation was not successful.", + result: ISdStargateExportFileResultEnum.NOTHING, + }, + }; + // fetch the exported file + const {href, size} = exportResult.content![index]; + await fetchFileWithToken( + href, + exportResult.filename!, + config.accessToken as string, + ); + return { + info: { + message: `File ${exportResult.filename} downloaded successfully (${size} bytes).`, + result: ISdStargateExportFileResultEnum.SUCCESS, + }, + }; + }, + [], + ); + + return {getDataCommandHandler, exportFileCommandHandler, supportedData}; +} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 17b0ed0..fd88756 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,5 +1,52 @@ import React from "react"; +import useShapeDiverAuth from "~/hooks/useShapeDiverAuth"; +import useShapeDiverStargate from "~/hooks/useShapeDiverStargate"; +import useStargateHandlers from "~/hooks/useStargateHandlers"; export default function HomePage() { - return <>; + // call hook used to manage authentication with ShapeDiver via OAuth2 Authorization Code Flow with PKCE + const { + error, + errorDescription, + accessToken, + initiateShapeDiverAuth, + authUsingRefreshToken, + authState, + platformSdk, + } = useShapeDiverAuth({ autoLogin: true }); + + // example handlers for the ShapeDiver Stargate service + const handlers = useStargateHandlers(); + + // call hook used to register as a client for the ShapeDiver Stargate service, using the example handlers + const { stargateSdk, isActive } = useShapeDiverStargate({ + accessToken, + platformSdk, + ...handlers, + }); + + return ( + <> + {error &&

Error: {error}

} + {errorDescription &&

Error description: {errorDescription}

} +

Authentication state: {authState}

+ {stargateSdk && ( +

+ Stargate SDK initialized {isActive ? "(active)" : undefined} +

+ )} +

+ {authState === "not_authenticated" ? ( + + ) : undefined} + {authState === "refresh_token_present" ? ( + + ) : undefined} +

+ + ); }