diff --git a/README.md b/README.md index 518effe..625f104 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,12 @@ npm install npm run dev ``` +Then in a new terminal run the server: + +```bash +npm run server +``` + Open your browser at to view the app. ### Build for Production @@ -31,3 +37,13 @@ npm run build ``` The built files will be in the `dist/` folder. + +## Structure + +### The Potree Build + +This application is built using Potree as a package, meaning that the potree build is used as a base layer for UI and functionality and built onto by our own code. The add-ons may use, move or manipulate objects from the potree build, referencing the objects by classname or id. + +### Updating Potree version + +To update the version of Potree that this application uses you must make a build from the official potree app and replace the build folder here with the new one. There is no guarantee that Molloy Explorer is compatible with other versions of potree. Make sure that everything works on the development server before applying this to the production server. diff --git a/index.html b/index.html index 165c847..2f98510 100644 --- a/index.html +++ b/index.html @@ -38,6 +38,7 @@ rel="stylesheet" href="/src/MeasurementControl/measurementsPanel.css" /> + diff --git a/package-lock.json b/package-lock.json index 26d9905..1bb01a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,761 +7,92 @@ "": { "name": "molloyexplorer", "version": "0.0.0", - "devDependencies": { - "cypress": "^15.4.0", - "prettier": "^3.6.2", - "vite": "^7.1.2" - } - }, - "node_modules/@cypress/request": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.9.tgz", - "integrity": "sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~4.0.4", - "http-signature": "~1.4.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "performance-now": "^2.1.0", - "qs": "6.14.0", - "safe-buffer": "^5.1.2", - "tough-cookie": "^5.0.0", - "tunnel-agent": "^0.6.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@cypress/xvfb": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", - "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.1.0", - "lodash.once": "^4.1.1" - } - }, - "node_modules/@cypress/xvfb/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz", - "integrity": "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.2.tgz", - "integrity": "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.2.tgz", - "integrity": "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.2.tgz", - "integrity": "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.2.tgz", - "integrity": "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.2.tgz", - "integrity": "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.2.tgz", - "integrity": "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.2.tgz", - "integrity": "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.2.tgz", - "integrity": "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.2.tgz", - "integrity": "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.50.2.tgz", - "integrity": "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.2.tgz", - "integrity": "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.2.tgz", - "integrity": "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "cors": "^2.8.5", + "express": "^4.21.1" + }, + "devDependencies": { + "cypress": "^15.4.0", + "prettier": "^3.6.2", + "vite": "^7.1.2" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.2.tgz", - "integrity": "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==", - "cpu": [ - "riscv64" - ], + "node_modules/@cypress/request": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.9.tgz", + "integrity": "sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.4", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.14.0", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.2.tgz", - "integrity": "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==", - "cpu": [ - "s390x" - ], + "node_modules/@cypress/request/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz", - "integrity": "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==", - "cpu": [ - "x64" - ], + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.2.tgz", - "integrity": "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==", - "cpu": [ - "x64" - ], + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "ms": "^2.1.1" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.2.tgz", - "integrity": "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==", - "cpu": [ - "arm64" - ], + "node_modules/@cypress/xvfb/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] + "license": "MIT" }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.2.tgz", - "integrity": "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", "cpu": [ "arm64" ], @@ -769,48 +100,33 @@ "license": "MIT", "optional": true, "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.2.tgz", - "integrity": "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==", - "cpu": [ - "ia32" + "darwin" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { + "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.2.tgz", - "integrity": "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ] }, "node_modules/@types/estree": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { - "version": "24.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", - "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", + "version": "24.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz", + "integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==", "dev": true, "license": "MIT", "optional": true, @@ -850,6 +166,17 @@ "@types/node": "*" } }, + "node_modules/accepts": { + "version": "1.3.8", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -950,6 +277,10 @@ ], "license": "MIT" }, + "node_modules/array-flatten": { + "version": "1.1.1", + "license": "MIT" + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -1059,6 +390,28 @@ "dev": true, "license": "MIT" }, + "node_modules/body-parser": { + "version": "1.20.3", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1094,6 +447,13 @@ "node": "*" } }, + "node_modules/bytes": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cachedir": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", @@ -1106,9 +466,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1120,9 +477,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -1315,6 +669,34 @@ "node": ">=4.0.0" } }, + "node_modules/content-disposition": { + "version": "0.5.4", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "license": "MIT" + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -1322,6 +704,17 @@ "dev": true, "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1338,9 +731,9 @@ } }, "node_modules/cypress": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.4.0.tgz", - "integrity": "sha512-+GC/Y/LXAcaMCzfuM7vRx5okRmonceZbr0ORUAoOrZt/5n2eGK8yh04bok1bWSjZ32wRHrZESqkswQ6biArN5w==", + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.5.0.tgz", + "integrity": "sha512-7jXBsh5hTfjxr9QQONC2IbdTj0nxSyU8x4eiarMZBzXzCj3pedKviUx8JnLcE4vL8e0TsOzp70WSLRORjEssRA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1396,6 +789,31 @@ "node": "^20.1.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/cypress/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/cypress/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -1414,24 +832,13 @@ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" } }, "node_modules/delayed-stream": { @@ -1444,11 +851,23 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -1470,6 +889,10 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1477,6 +900,13 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -1503,9 +933,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1513,9 +940,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1523,9 +947,6 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -1552,8 +973,6 @@ }, "node_modules/esbuild": { "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1592,6 +1011,10 @@ "@esbuild/win32-x64": "0.25.9" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -1602,6 +1025,13 @@ "node": ">=0.8.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/eventemitter2": { "version": "6.4.7", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", @@ -1646,6 +1076,50 @@ "node": ">=4" } }, + "node_modules/express": { + "version": "4.21.2", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -1674,6 +1148,31 @@ "@types/yauzl": "^2.9.1" } }, + "node_modules/extract-zip/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -1696,8 +1195,6 @@ }, "node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -1728,6 +1225,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/finalhandler": { + "version": "1.3.1", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -1755,6 +1268,20 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -1773,10 +1300,7 @@ }, "node_modules/fsevents": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -1788,9 +1312,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1798,9 +1319,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -1823,9 +1341,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -1879,9 +1394,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1909,9 +1421,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1955,9 +1464,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1966,6 +1472,20 @@ "node": ">= 0.4" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-signature": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", @@ -1991,6 +1511,16 @@ "node": ">=8.12.0" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2022,6 +1552,10 @@ "node": ">=8" } }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, "node_modules/ini": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", @@ -2032,6 +1566,13 @@ "node": ">=10" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2279,14 +1820,25 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -2294,11 +1846,25 @@ "dev": true, "license": "MIT" }, + "node_modules/methods": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2306,9 +1872,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -2338,16 +1901,11 @@ } }, "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, + "version": "2.0.0", "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -2363,6 +1921,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "0.6.3", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -2376,11 +1941,15 @@ "node": ">=8" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2389,6 +1958,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2438,6 +2017,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2448,6 +2034,10 @@ "node": ">=8" } }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "license": "MIT" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -2464,15 +2054,11 @@ }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -2494,8 +2080,6 @@ }, "node_modules/postcss": { "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -2523,8 +2107,6 @@ }, "node_modules/prettier": { "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { @@ -2560,6 +2142,17 @@ "node": ">= 0.6.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", @@ -2579,13 +2172,10 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, + "version": "6.13.0", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -2594,6 +2184,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/request-progress": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", @@ -2627,8 +2237,6 @@ }, "node_modules/rollup": { "version": "4.50.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz", - "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", "dev": true, "license": "MIT", "dependencies": { @@ -2678,9 +2286,6 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -2699,9 +2304,6 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/semver": { @@ -2717,6 +2319,56 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "0.19.0", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2742,9 +2394,6 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2762,9 +2411,6 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2779,9 +2425,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -2798,9 +2441,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -2840,8 +2480,6 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -2874,6 +2512,13 @@ "node": ">=0.10.0" } }, + "node_modules/statuses": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2974,8 +2619,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3019,6 +2662,13 @@ "node": ">=14.14" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tough-cookie": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", @@ -3079,6 +2729,17 @@ "node": ">=8" } }, + "node_modules/type-is": { + "version": "1.6.18", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/undici-types": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", @@ -3097,6 +2758,13 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -3107,6 +2775,13 @@ "node": ">=8" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -3117,6 +2792,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/vary": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -3134,8 +2816,6 @@ }, "node_modules/vite": { "version": "7.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", - "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index eeb6c34..8540e47 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,16 @@ "type": "module", "scripts": { "dev": "vite", + "server": "node server.js", "build": "vite build", "preview": "vite preview", "format": "prettier --write .", "format:check": "prettier --check ." }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.21.1" + }, "devDependencies": { "cypress": "^15.4.0", "prettier": "^3.6.2", diff --git a/public/annotations/annotations.json b/public/annotations/annotations.json new file mode 100644 index 0000000..c58be02 --- /dev/null +++ b/public/annotations/annotations.json @@ -0,0 +1,4 @@ +{ + "version": 1, + "folders": {} +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..d11befc --- /dev/null +++ b/server.js @@ -0,0 +1,48 @@ +import express from 'express' +import cors from 'cors' +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +const app = express() +const PORT = process.env.PORT || 5174 + +app.use(cors()) +app.use(express.json({ limit: '2mb' })) + +// Directory where annotations are stored alongside Vite's public folder +const annotationsDir = path.join(dirname, 'public', 'annotations') +const annotationsFile = path.join(annotationsDir, 'annotations.json') + +// Ensure directory exists +function ensureDirSync(dir) { + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }) +} + +// Save annotations JSON +app.post('/api/annotations', (req, res) => { + try { + const body = req.body + if (!body || typeof body !== 'object') { + return res.status(400).json({ error: 'Invalid JSON body' }) + } + if (!body.folders || typeof body.folders !== 'object') { + return res.status(400).json({ error: 'Missing folders in payload' }) + } + + ensureDirSync(annotationsDir) + const data = JSON.stringify(body, null, 2) + fs.writeFileSync(annotationsFile, data, 'utf8') + return res.json({ saved: true, path: '/annotations/annotations.json' }) + } catch (err) { + console.error('Failed to save annotations:', err) + return res.status(500).json({ error: 'Failed to save annotations' }) + } +}) + +app.listen(PORT, () => { + console.log(`API server listening on http://localhost:${PORT}`) +}) diff --git a/src/AnnotationControl/annotationPanel.css b/src/AnnotationControl/annotationPanel.css new file mode 100644 index 0000000..13a3b2e --- /dev/null +++ b/src/AnnotationControl/annotationPanel.css @@ -0,0 +1,386 @@ +/* Hide Potree annotation icon specifically */ +/* This targets the that references annotation.svg. */ +img.button-icon[src$='/annotation.svg'] { + display: none !important; + visibility: hidden !important; + width: 0 !important; + height: 0 !important; + margin: 0 !important; + padding: 0 !important; +} + +.annotation-desc { + font-size: 0.85em; + margin-left: 8px; +} + +.annotation-info { + font-size: 0.8em; + margin-left: 8px; +} + +.annotation-edit-textarea { + width: 100%; + box-sizing: border-box; + max-width: 100%; + display: block; + padding: 8px; + border-radius: 4px; + border: 1px solid #404a50; + background: #2f383d; + color: #cfd5d8; + font-family: inherit; + font-size: 12px; + line-height: 1.3; + white-space: pre-wrap; + overflow: auto; + /* Only allow vertical resizing so user can't drag the box horizontally + out of the gray container. */ + resize: vertical; + max-height: 40vh; +} + +.annotation-row .annotation-edit-textarea { + margin-top: 6px; +} + +.annotation-add-button { + margin: 10px 0; +} + +.annotation-empty { + opacity: 0.6; + padding: 10px; + text-align: center; +} + +.annotation-row { + display: flex; + flex-direction: column; /* stack header above body */ + gap: 6px; + padding: 6px 8px; + margin: 10px 2px; + border-radius: 6px; + background: #2c3539; + border: 1px solid transparent; + transition: + background 0.15s, + border-color 0.15s; + color: #d9e2e6; +} + +.annotation-row .annotation-label { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.annotation-header { + display: flex; + align-items: center; + gap: 8px; + width: 100%; +} +.annotation-body { + width: 100%; + margin-top: 6px; +} + +/* By default hide the body (details); show when row has open */ +.annotation-body { + display: none; +} +.annotation-row.open .annotation-body { + display: block; +} + +.annotation-row .toggle-triangle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + font-size: 12px; + color: #8fb9c9; + vertical-align: middle; + cursor: pointer; +} +.annotation-row .toggle-triangle::after { + content: '▸'; +} +.annotation-row.open .toggle-triangle::after { + content: '▾'; +} + +/* Jump button */ +.annotation-row .jump-btn { + width: 28px; + height: 28px; + border-radius: 50%; + background: transparent; + color: #7fbcd3; + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 8px; + border: 1px solid rgba(127, 188, 211, 0.28); + cursor: pointer; + transition: + transform 0.12s ease, + background 0.18s ease, + box-shadow 0.18s ease, + border-color 0.18s ease, + color 0.12s ease; + box-shadow: none; +} +.annotation-row .jump-btn::after { + content: '→'; + font-weight: 700; + font-size: 13px; + line-height: 1; +} +.annotation-row .jump-btn:hover { + color: #7fbcd3; + border-color: rgba(14, 166, 217, 0.44); + box-shadow: + 0 8px 18px rgba(14, 166, 217, 0.14), + 0 0 0 3px rgba(14, 166, 217, 0.06); + background: transparent; +} +.annotation-row .jump-btn:focus { + outline: none; + border-color: rgba(14, 166, 217, 0.44); + box-shadow: + 0 8px 22px rgba(14, 166, 217, 0.18), + 0 0 0 4px rgba(14, 166, 217, 0.06); +} +.annotation-row .jump-btn:active, +.annotation-row .jump-btn[aria-pressed='true'] { + transform: translateY(1px); + background: linear-gradient(180deg, #28c1ff 0%, #0ea6d9 100%); + color: #fff; + border-color: transparent; + box-shadow: + 0 6px 20px rgba(14, 166, 217, 0.26), + inset 0 1px 0 rgba(255, 255, 255, 0.14); +} + +.annotation-row .jump-btn.recently-pressed { + transform: translateY(1px); + background: linear-gradient(180deg, #28c1ff 0%, #0ea6d9 100%); + color: #fff; + border-color: transparent; + box-shadow: + 0 6px 20px rgba(14, 166, 217, 0.26), + inset 0 1px 0 rgba(255, 255, 255, 0.14); + transition: + background 1.4s cubic-bezier(0.2, 0.9, 0.2, 1), + box-shadow 1.4s cubic-bezier(0.2, 0.9, 0.2, 1), + transform 0.12s ease, + color 1s ease, + opacity 1.4s ease; +} + +.annotation-row .jump-btn.jump-disabled, +.annotation-row .jump-btn:disabled { + opacity: 0.44; + color: #9fbfcf; + border-color: rgba(127, 188, 211, 0.12); + box-shadow: none; + cursor: default; + pointer-events: none; +} + +.annotation-row .jump-btn.jump-disabled:hover, +.annotation-row .jump-btn.jump-disabled:focus, +.annotation-row .jump-btn:disabled:hover, +.annotation-row .jump-btn:disabled:focus { + transform: none; + box-shadow: none; + background: transparent; +} + +.annotation-row .del-btn { + background: #3b2626; + border: 1px solid #5a3a3a; + color: #ff9a9a; + font-weight: 600; + font-size: 11px; + line-height: 1; + padding: 4px 6px; + border-radius: 4px; + cursor: pointer; + transition: + background 0.15s, + color 0.15s; + margin-left: 8px; +} +.annotation-row .del-btn:hover { + background: #5a2d2d; + color: #fff; +} + +.annotation-row .edit-btn { + width: 28px; + height: 28px; + border-radius: 50%; + background: transparent; + color: #d0d6da; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(208, 214, 218, 0.28); + cursor: pointer; + transition: + transform 0.12s ease, + background 0.18s ease, + box-shadow 0.18s ease, + border-color 0.18s ease, + color 0.12s ease; + box-shadow: none; +} +.annotation-row .edit-btn:hover, +.annotation-row .edit-btn:focus { + color: #ffffff; + border-color: rgba(208, 214, 218, 0.44); + box-shadow: + 0 8px 18px rgba(140, 150, 160, 0.14), + 0 0 0 3px rgba(140, 150, 160, 0.06); + background: transparent; + outline: none; +} +.annotation-row .edit-btn:active { + transform: translateY(1px); +} + +.annotation-row:hover { + background: #354045; + border-color: #425056; +} +.annotation-row.active { + background: #1f4b63; + border-color: #2f6b8c; + box-shadow: 0 0 0 1px #2f6b8c66; +} + +.annotation-row .annotation-desc, +.annotation-row .annotation-info { + background: #2f383d; + padding: 8px; + border: 1px solid #404a50; + border-radius: 4px; + color: #cfd5d8; + font-family: inherit; + font-size: 12px; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +/* make room for the pencil inside the description box */ +.annotation-row .annotation-desc { + position: relative; + padding-right: 36px; /* space for inner pencil */ +} + +.annotation-row .annotation-desc .edit-desc-btn { + position: absolute; + top: 6px; + right: 6px; + width: 22px; + height: 22px; + border-radius: 50%; + background: transparent; + color: #d0d6da; + border: 1px solid rgba(208, 214, 218, 0.28); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; + line-height: 1; + padding: 0; +} +.annotation-row .annotation-desc .edit-desc-btn:hover, +.annotation-row .annotation-desc .edit-desc-btn:focus { + color: #ffffff; + border-color: rgba(208, 214, 218, 0.44); + box-shadow: + 0 6px 14px rgba(140, 150, 160, 0.16), + 0 0 0 2px rgba(140, 150, 160, 0.06); + outline: none; +} + +.annotation-row .annotation-desc, +.annotation-row .annotation-info { + display: none; +} +.annotation-row.open .annotation-desc, +.annotation-row.open .annotation-info { + display: block; + margin-top: 6px; +} + +.annotation-header .annotation-label { + margin-right: 8px; +} + +.annotation-header .controls { + margin-left: auto; + display: flex; + gap: 8px; + align-items: center; + flex: 0 0 auto; +} + +.annotation-header input[type='text'], +.annotation-header .annotation-label input[type='text'] { + width: 100%; + max-width: 100%; + box-sizing: border-box; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Make the caret/triangle take up fixed space so it doesn't shift */ +.annotation-row .toggle-triangle { + flex: 0 0 18px; +} + +/* Add button */ +.annotation-add-button { + background: linear-gradient(180deg, #f6f6f6 0%, #e9e9e9 100%); + color: #222; + padding: 8px 16px; + min-width: 140px; + height: 38px; + display: block; + margin: 12px auto; + border-radius: 6px; + font-size: 13px; + font-weight: 700; + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.6) inset; + border: 1px solid #cfcfcf; + cursor: pointer; + text-align: center; +} +.annotation-add-button .add-label { + color: #222; + font-weight: 700; +} +.annotation-add-button:hover { + background: linear-gradient(180deg, #f3f3f3 0%, #e2e2e2 100%); + border-color: #bfbfbf; +} +.annotation-add-button:active { + transform: translateY(1px); + background: linear-gradient(180deg, #e9e9e9 0%, #dbdbdb 100%); + box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.06); +} +.annotation-add-button:focus { + outline: 2px solid rgba(100, 100, 100, 0.12); + outline-offset: 2px; +} diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js new file mode 100644 index 0000000..4bdd4ec --- /dev/null +++ b/src/AnnotationControl/annotationPanel.js @@ -0,0 +1,1025 @@ +import { initAnnotationPersistence } from './persistence' + +/** + * Initialize and inject the Annotations sidebar panel, used for managing saved camera views. + * + * Creates (or re-uses) a container with id `annotations_list`, renders the + * saved annotation entries from the project's jsTree, wires UI controls + * (jump, delete, toggle, inline editing), and hooks into Potree's + * annotation events to keep the list in sync. + * + * Side effects: + * - Mutates DOM by injecting a panel and a "Add a location" button. + * - Registers event listeners on viewer.scene.annotations to refresh the list. + * - Attaches click/dblclick handlers and inline edit inputs that commit + * edits back to jsTree and the live annotation objects. + * + * @param {Object} viewer - Potree viewer instance (used to read camera/pivot, + * jump the view, and access viewer.scene.annotations). + */ +export function initAnnotationsPanel(viewer) { + // Container management + const existingListContainer = document.getElementById('annotations_list') + let targetContainer = existingListContainer + if (!targetContainer) { + const menu = document.getElementById('potree_menu') + if (menu) { + const header = document.createElement('h3') + header.id = 'menu_camera_annotations' + const headerSpan = document.createElement('span') + headerSpan.textContent = 'Saved Locations' + header.appendChild(headerSpan) + + const panel = document.createElement('div') + panel.className = 'pv-menu-list annotations-panel' + + const listContainerDiv = document.createElement('div') + listContainerDiv.id = 'annotations_list' + listContainerDiv.className = 'auto' + panel.appendChild(listContainerDiv) + + // Insert after measurement panel but before tools, or at end if not found + const measurements = document.querySelector('.measurements-panel') + if (measurements) { + menu.insertBefore(panel, measurements.nextSibling) + menu.insertBefore(header, panel) + } else { + menu.appendChild(header) + menu.appendChild(panel) + } + + if (window.jQuery && window.jQuery(menu).accordion) { + try { + window.jQuery(menu).accordion('refresh') + } catch (e) {} + } + + // Toggle collapse + header.addEventListener('click', () => { + if ($(menu).accordion && $(menu).data('uiAccordion')) return + if (window.jQuery) { + const $p = window.jQuery(panel) + $p.is(':visible') ? $p.slideUp(350) : $p.slideDown(350) + return + } + }) + targetContainer = panel.querySelector('#annotations_list') + } + } + if (!targetContainer) { + console.warn( + 'Annotations list container not found and dynamic injection failed' + ) + return + } + + /** + * Normalize a vector-like input into an [x,y,z] array. (necessary for handling both Three.js Vector3 and serialized data stored in the jsTree) + * + * Accepts: + * - Array [x,y,z] + * - Three.js Vector3 with toArray() + * - Plain object {x,y,z} + * + * Returns null for invalid input. + * + * @param {*} v - value to normalize + * @returns {Array|null} an [x,y,z] array or null + */ + function vecToArray(v) { + if (!v) return null + if (Array.isArray(v)) return v + if (typeof v.toArray === 'function') return v.toArray() + if (v.x != null && v.y != null && v.z != null) return [v.x, v.y, v.z] + return null + } + + const _uuidToIndex = new Map() + let _nextIndex = 1 + + /** + * Return a monotonic index number for a UUID (Universally Unique Identifier). + * + * The function assigns an incrementing integer to each UUID the first time + * it is seen and never reuses numbers. Useful to produce human-readable + * default names (e.g. "Annotation #3") when nodes are missing titles. + * + * @param {string} uuid - UUID string for the annotation + * @returns {number|null} index assigned to the UUID, or null if uuid falsy + */ + function _ensureIndexForUUID(uuid) { + if (!uuid) return null + if (!_uuidToIndex.has(uuid)) { + _uuidToIndex.set(uuid, _nextIndex++) + } + return _uuidToIndex.get(uuid) + } + + // Helper: safe accessors and utilities for jsTree and annotation descriptions + function _getJSTree() { + try { + if (!window.jQuery) return null + return $('#jstree_scene').jstree && $('#jstree_scene').jstree() + } catch (e) { + return null + } + } + + function _getJSTreeInstance() { + try { + if (!window.jQuery || !window.jQuery.jstree) return null + return ( + ($('#jstree_scene').jstree && $('#jstree_scene').jstree(true)) || + (typeof $.jstree !== 'undefined' && + $.jstree.reference && + $.jstree.reference('#jstree_scene')) || + null + ) + } catch (e) { + return null + } + } + + /** + * Find the live annotation object in the viewer.scene by UUID. + * + * Returns the live annotation or null. + * + * @param {string} uuid - annotation UUID + * @returns {Object|null} live annotation object or null if not found + */ + function _getAnnotationsRoot() { + const t = _getJSTree() + try { + return t ? t.get_json('annotations') : null + } catch (e) { + return null + } + } + + function _findNodeInAnnotationsRootByUUID(root, uuid) { + if (!root || !root.children || !uuid) return null + return root.children.find((c) => (c.data && c.data.uuid) === uuid) || null + } + + function _getDescriptionForUUID(uuid, nodeData) { + // Prefer live annotation description, then node data (desc/description) + if (!uuid && !nodeData) return '' + try { + const live = _findLiveAnnotationByUUID(uuid) + if (live) { + if (live.data && typeof live.data.description !== 'undefined') + return String(live.data.description || '') + if (typeof live.description !== 'undefined') + return String(live.description || '') + } + } catch (e) {} + const ann = nodeData || {} + return ( + (ann.description && String(ann.description)) || + (ann.desc && String(ann.desc)) || + '' + ) + } + + function _renameJSTreeNode(nodeId, text) { + try { + if (window.$ && $.jstree) { + if ($.jstree.reference) { + const ref = $.jstree.reference(nodeId) + if (ref && typeof ref.rename_node === 'function') { + ref.rename_node(nodeId, text) + return + } + } + // fallback to global selector + if ($('#jstree_scene') && $('#jstree_scene').jstree) { + try { + $('#jstree_scene').jstree('rename_node', nodeId, text) + return + } catch (e) {} + } + } + } catch (e) { + // ignore rename failures + } + } + + function _findLiveAnnotationByUUID(uuid) { + if (!uuid || !viewer || !viewer.scene || !viewer.scene.annotations) + return null + try { + const coll = + (viewer.scene.annotations.flatten && + viewer.scene.annotations.flatten()) || + (viewer.scene.annotations.descendants && + viewer.scene.annotations.descendants()) || + viewer.scene.annotations.children || + [] + for (const a of coll) { + if (!a) continue + if (a.uuid === uuid || (a.data && a.data.uuid === uuid)) return a + } + } catch (e) { + // fallback + try { + const coll2 = viewer.scene.annotations.children || [] + for (const a of coll2) + if (a && (a.uuid === uuid || (a.data && a.data.uuid === uuid))) + return a + } catch (e) {} + } + return null + } + + /** + * Rebuild the annotation list UI from the project's jsTree data. + * + * This function: + * - Clears and re-populates the `targetContainer` with a row per annotation. + * - Ensures default labels for unnamed annotations (using monotonic numbering). + * - Renders header, toggle triangle, edit label, jump/delete controls, description, + * and read-only camera/point info. + * - Wires events for inline editing, jump, delete, and header click-to-toggle. + * + * Side effects: + * - Mutates DOM inside `targetContainer`. + * - May call `_renameJSTreeNode` to rename nodes when needed. + * - Uses `_findLiveAnnotationByUUID` to prefer live object positions over serialized values. + * - Event listeners stop propagation where necessary so double-click and button + * behaviour are preserved. + */ + function updateAnnotationsList() { + // Implementation for listing annotations + targetContainer.innerHTML = '' + const annotationsTree = _getJSTree() + if (!annotationsTree) return + let annotationsRoot = _getAnnotationsRoot() + if ( + !annotationsRoot || + !annotationsRoot.children || + annotationsRoot.children.length === 0 + ) { + const empty = document.createElement('div') + empty.className = 'annotation-empty' + empty.textContent = 'No saved locations yet' + targetContainer.appendChild(empty) + return + } + // Assign monotonic indices for any node UUIDs encountered and rename unlabeled nodes + try { + const items = (annotationsRoot.children || []).map((n) => ({ + data: n.data || {}, + node: n + })) + for (const it of items) { + const uuid = (it.data && it.data.uuid) || null + if (!uuid) continue + const idx = _ensureIndexForUUID(uuid) + const text = (it.node && it.node.text) || '' + const shouldRename = + !text || + text.trim() === '' || + text === 'Unnamed' || + text === 'Annotation Title' + if (shouldRename && idx != null) { + const newName = `Annotation #${idx}` + _renameJSTreeNode(it.node.id, newName) + it.node.text = newName + // update live annotation object's title/name so in-scene label matches + try { + const live = _findLiveAnnotationByUUID(it.data.uuid) + if (live) { + if (typeof live.title !== 'undefined') live.title = newName + if (typeof live.name !== 'undefined') live.name = newName + if (live.data) live.data.title = newName + } + } catch (e) {} + } + } + } catch (e) { + // ignore failures + } + + // Build list entries for each annotation node + annotationsRoot.children.forEach((node) => { + const row = document.createElement('div') + row.className = 'annotation-row' + + // header and body structure + const header = document.createElement('div') + header.className = 'annotation-header' + const body = document.createElement('div') + body.className = 'annotation-body' + + const label = document.createElement('span') + label.textContent = node.text || 'Unnamed' + label.className = 'annotation-label' + // attach uuid for editing + try { + const uuid = (node.data && node.data.uuid) || null + if (uuid) label.dataset.uuid = uuid + } catch (e) {} + // double-click label to edit + label.addEventListener('dblclick', (ev) => { + ev.stopPropagation() + const uuid = label.dataset.uuid + if (uuid) startInlineEditForUUID(uuid) + }) + + // Small pencil button to enter title edit mode + const editBtn = document.createElement('button') + editBtn.className = 'edit-btn' + editBtn.title = 'Edit title' + editBtn.setAttribute('aria-label', 'Edit title') + editBtn.textContent = '✎' + editBtn.addEventListener('click', (ev) => { + ev.stopPropagation() + const uuid = label.dataset.uuid + if (uuid) startInlineEditForUUID(uuid) + }) + + // triangular toggle (collapsed/open) + const toggle = document.createElement('span') + toggle.className = 'toggle-triangle' + toggle.title = 'Toggle details' + + // Jump button + const jumpBtn = document.createElement('button') + jumpBtn.className = 'jump-btn' + jumpBtn.title = 'Move to this position' + jumpBtn.setAttribute('aria-label', 'Jump to saved view') + // Move the viewer to the saved camera position/pivot for this annotation (if present) + jumpBtn.onclick = () => { + const ann = node.data || {} + const camPos = vecToArray( + ann.cameraPosition || ann.camera_position || ann.cameraPos + ) + const annPos = vecToArray( + ann.position || ann.annotationPosition || ann.pos + ) + + if ( + camPos && + viewer && + viewer.scene && + viewer.scene.view && + typeof viewer.scene.view.setView === 'function' + ) { + const target = annPos || null + viewer.scene.view.setView(camPos, target, 1000) // animation duration in ms + + // Transient visual feedback: mark button as recently pressed so CSS can + // show the filled gradient and glow, then fade it back automatically. + try { + jumpBtn.setAttribute('aria-pressed', 'true') + jumpBtn.classList.add('recently-pressed') + window.setTimeout(() => { + try { + jumpBtn.classList.remove('recently-pressed') + jumpBtn.setAttribute('aria-pressed', 'false') + } catch (e) {} + }, 200) + } catch (e) {} + } + } + + // Delete button + const delBtn = document.createElement('button') + delBtn.className = 'del-btn' + delBtn.textContent = '✖' + delBtn.title = 'Delete saved position' + // Delete an annotation: remove annotation from the renderer scene (live annotation), then remove the tree node + delBtn.onclick = () => { + const annData = node.data || {} + const uuid = annData.uuid + + if (uuid && viewer && viewer.scene && viewer.scene.annotations) { + // Find the live annotation instance by UUID + let candidates = [] + try { + candidates = + (viewer.scene.annotations.flatten && + viewer.scene.annotations.flatten()) || + (viewer.scene.annotations.descendants && + viewer.scene.annotations.descendants()) || + viewer.scene.annotations.children || + [] + } catch (e) { + candidates = viewer.scene.annotations.children || [] + } + + let live = null + if (Array.isArray(candidates)) { + for (let a of candidates) { + if (!a) continue + if (a.uuid === uuid || (a.data && a.data.uuid === uuid)) { + live = a + break + } + } + } + + if (live) { + if (typeof live.removeHandles === 'function') + live.removeHandles(viewer) + if (typeof viewer.scene.removeAnnotation === 'function') { + viewer.scene.removeAnnotation(live) + } else if ( + viewer.scene.annotations && + typeof viewer.scene.annotations.remove === 'function' + ) { + viewer.scene.annotations.remove(live) + } + } + } + + // remove from sidebar/tree + try { + if (window.jQuery && window.jQuery.fn.jstree) { + window.jQuery('#jstree_scene').jstree('delete_node', node.id) + } + } catch (e) { + try { + if ( + window.jQuery && + window.jQuery.jstree && + window.jQuery.jstree.reference + ) { + window.jQuery.jstree.reference(node.id).delete_node(node.id) + } + } catch (_) {} + } + updateAnnotationsList() + } + + // Append elements into header: toggle, label, then controls (jump/delete) + const controls = document.createElement('div') + controls.className = 'controls' + controls.appendChild(editBtn) + controls.appendChild(jumpBtn) + controls.appendChild(delBtn) + + header.appendChild(toggle) + header.appendChild(label) + header.appendChild(controls) + row.appendChild(header) + + // Allow clicking anywhere on the header to toggle the details, but + // ignore clicks that originate from interactive controls (jump/delete) + // so those buttons keep their original behavior. + try { + header.addEventListener('click', (ev) => { + try { + // If the click was inside the controls area (jump/delete), do nothing + if ( + ev.target && + ev.target.closest && + ev.target.closest('.controls') + ) { + return + } + + // If the click was on an interactive element (button, input, textarea, a), ignore + const interactive = [ + 'BUTTON', + 'INPUT', + 'TEXTAREA', + 'A', + 'SELECT', + 'LABEL' + ] + if ( + ev.target && + ev.target.tagName && + interactive.indexOf(ev.target.tagName) >= 0 + ) { + return + } + + // Otherwise toggle the row + row.classList.toggle('open') + } catch (e) { + // fail silently + } + }) + } catch (e) {} + + // Wire toggle to show/hide details + try { + toggle.addEventListener('click', (ev) => { + ev.stopPropagation() + row.classList.toggle('open') + }) + } catch (e) {} + // Description view,supports dblclick edit + try { + const ann = node.data || {} + const descText = _getDescriptionForUUID(ann.uuid, ann) + const display = descText.trim() ? descText : 'Annotation Description' + const desc = document.createElement('div') + desc.className = 'annotation-desc' + desc.textContent = display + desc.dataset.uuid = (ann && ann.uuid) || '' + desc.dataset.raw = descText.trim() + desc.addEventListener('dblclick', (ev) => { + ev.stopPropagation() + const u = desc.dataset.uuid + if (u) startInlineDescriptionEditForUUID(u) + }) + // Add a pencil inside the description box + const innerDescBtn = document.createElement('button') + innerDescBtn.className = 'edit-desc-btn' + innerDescBtn.title = 'Edit description' + innerDescBtn.setAttribute('aria-label', 'Edit description') + innerDescBtn.textContent = '✎' + innerDescBtn.addEventListener('click', (ev) => { + ev.stopPropagation() + try { + row.classList.add('open') + } catch (e) {} + const u = desc.dataset.uuid + if (u) startInlineDescriptionEditForUUID(u) + }) + desc.appendChild(innerDescBtn) + body.appendChild(desc) + } catch (e) { + // ignore description rendering errors + } + + // show saved camera and point info + try { + const ann = node.data || {} + + const cam = vecToArray( + ann.cameraPosition || ann.camera_position || ann.cameraPos + ) + + // Start with serialized position, then prefer the live object's current position + let pointPos = vecToArray( + ann.position || ann.annotationPosition || ann.pos + ) + try { + const live = _findLiveAnnotationByUUID(ann.uuid) + if (live) { + const livePos = vecToArray( + live.position || (live.data && live.data.position) + ) + if (livePos) pointPos = livePos + } + } catch (e) {} + + // Hide Potree's default placeholder coordinates until the annotation is actually placed + function approxEqual(a, b, eps = 1e-3) { + if (!a || !b || a.length !== b.length) return false + for (let i = 0; i < a.length; i++) + if (Math.abs(Number(a[i]) - Number(b[i])) > eps) return false + return true + } + const PLACEHOLDER_POS = [589748.27, 231444.54, 753.675] + if (pointPos && approxEqual(pointPos, PLACEHOLDER_POS)) pointPos = null + + // Format the camera and point positions for display + if (cam || pointPos) { + const info = document.createElement('div') + info.className = 'annotation-info' + const fmt = (v) => + v ? v.map((c) => Number(c).toFixed(3)).join(', ') : '—' + const fmtPoint = fmt(pointPos) + ? String(fmt(pointPos)).replace(/,\s*/g, ',
') + : '—' + const fmtCam = fmt(cam) + ? String(fmt(cam)).replace(/,\s*/g, ',
') + : '—' + info.innerHTML = `Saved Point coordinates:
${fmtPoint}

Camera coordinates:
${fmtCam}` + body.appendChild(info) + + // Enable or disable jump button depending on whether we have a saved point position + try { + if (pointPos && Array.isArray(pointPos) && pointPos.length === 3) { + jumpBtn.disabled = false + jumpBtn.classList.remove('jump-disabled') + } else { + jumpBtn.disabled = true + jumpBtn.classList.add('jump-disabled') + } + } catch (e) {} + } + } catch (e) { + // ignore formatting errors + } + // Append body last so it's hidden until opened + row.appendChild(body) + targetContainer.appendChild(row) + }) + } + + /** + * Start inline editing of an annotation title (opens an input in the sidebar). + * + * Replaces the `.annotation-label` with a text input and handles commit/abort: + * - On commit, updates the label in the sidebar, updates the jsTree node text, + * and updates the live annotation object's title (via `_commitEditedName`). + * - On abort, restores the original label without committing. + * + * Special handling: + * - If the label contains a decorative edit-hint span, the hint text is not + * included in the input value and is re-attached to the restored label. + * + * @param {string} uuid - annotation UUID to edit + */ + function startInlineEditForUUID(uuid) { + if (!uuid) return + const labelEl = targetContainer.querySelector( + `.annotation-label[data-uuid="${uuid}"]` + ) + if (!labelEl) return + // Compute the visible title text without the edit-hint glyph. + // The label contains a text node (the title) and a child .annotation-edit-hint span. + let oldText = '' + try { + for (const node of Array.from(labelEl.childNodes)) { + if (node.nodeType === Node.TEXT_NODE) { + oldText += node.nodeValue || '' + } + } + oldText = (oldText || '').trim() + } catch (e) { + oldText = labelEl.textContent || '' + } + // Capture existing hint so we can re-attach it after editing + const existingHint = + labelEl.querySelector && labelEl.querySelector('.annotation-edit-hint') + const existingHintText = existingHint ? existingHint.textContent : null + const existingHintTitle = existingHint ? existingHint.title : null + const input = document.createElement('input') + input.type = 'text' + input.value = oldText + input.className = 'annotation-edit-input' + labelEl.replaceWith(input) + input.focus() + input.select() + + function finishInlineEditForUUID(commit) { + const newText = commit ? input.value.trim() || oldText : oldText + // restore label + const newLabel = document.createElement('span') + newLabel.className = 'annotation-label' + newLabel.textContent = newText + newLabel.dataset.uuid = uuid + newLabel.addEventListener('dblclick', (ev) => { + ev.stopPropagation() + startInlineEditForUUID(uuid) + }) + // Re-attach the edit hint so the hint persists after editing + try { + const hint = document.createElement('span') + hint.className = 'annotation-edit-hint' + if (existingHintTitle) hint.title = existingHintTitle + hint.textContent = existingHintText != null ? existingHintText : '✎' + newLabel.appendChild(hint) + } catch (e) { + // ignore hint restoration errors + } + + try { + if (input.isConnected) { + input.replaceWith(newLabel) + } else { + const existing = targetContainer.querySelector( + `.annotation-label[data-uuid="${uuid}"]` + ) + if (existing) existing.textContent = newText + } + } catch (e) { + // ignore DOM replacement errors and try to update label if present + try { + const existing = targetContainer.querySelector( + `.annotation-label[data-uuid="${uuid}"]` + ) + if (existing) existing.textContent = newText + } catch (_) {} + } + // commit to jsTree and live annotation + _commitEditedName(uuid, newText) + } + + input.addEventListener('blur', () => finishInlineEditForUUID(true)) + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + finishInlineEditForUUID(true) + } else if (e.key === 'Escape') { + finishInlineEditForUUID(false) + } + }) + } + + /** + * Start inline editing of an annotation description (multiline). + * + * Replaces the `.annotation-desc` container with a textarea. Behavior mirrors + * `startInlineEditForUUID` but handles multiline input. Commit updates both + * the jsTree node data and the live annotation object's description. + * + * @param {string} uuid - annotation UUID to edit + */ + function startInlineDescriptionEditForUUID(uuid) { + if (!uuid) return + const descEl = targetContainer.querySelector( + `.annotation-desc[data-uuid="${uuid}"]` + ) + if (!descEl) return + let oldText = descEl.dataset.raw || descEl.textContent || '' + try { + const live = _findLiveAnnotationByUUID(uuid) + if (live) { + if (live.data && typeof live.data.description !== 'undefined') { + oldText = String(live.data.description || '') + } else if (typeof live.description !== 'undefined') { + oldText = String(live.description || '') + } + } + } catch (e) {} + + const ta = document.createElement('textarea') + ta.className = 'annotation-edit-textarea' + // Prefill textarea with existing description + ta.value = oldText + ta.rows = 1 + descEl.replaceWith(ta) + ta.focus() + try { + ta.select() + } catch (e) {} + + /** + * Finish editing the description textarea. + * + * Replaces the textarea with a `.annotation-desc`, re-attaches handlers, + * and persists (or discards) the change via _commitEditedDescription. + * + * @param {boolean} commit - true to save, false to cancel + */ + function finishInlineDescriptionEdit(commit) { + const newText = commit ? ta.value.trim() || '' : oldText + const displayText = newText ? newText : 'Annotation Description' + const newDesc = document.createElement('div') + newDesc.className = 'annotation-desc' + newDesc.textContent = displayText + newDesc.dataset.uuid = uuid + newDesc.dataset.raw = newText + newDesc.addEventListener('dblclick', (ev) => { + ev.stopPropagation() + startInlineDescriptionEditForUUID(uuid) + }) + // Re-add inner edit pencil + const innerDescBtn = document.createElement('button') + innerDescBtn.className = 'edit-desc-btn' + innerDescBtn.title = 'Edit description' + innerDescBtn.setAttribute('aria-label', 'Edit description') + innerDescBtn.textContent = '✎' + innerDescBtn.addEventListener('click', (ev) => { + ev.stopPropagation() + startInlineDescriptionEditForUUID(uuid) + }) + newDesc.appendChild(innerDescBtn) + + try { + if (ta.isConnected) { + ta.replaceWith(newDesc) + } else { + const existing = targetContainer.querySelector( + `.annotation-desc[data-uuid="${uuid}"]` + ) + if (existing) existing.textContent = displayText + } + } catch (e) { + try { + const existing = targetContainer.querySelector( + `.annotation-desc[data-uuid="${uuid}"]` + ) + if (existing) existing.textContent = displayText + } catch (_) {} + } + + _commitEditedDescription(uuid, newText) + } + + ta.addEventListener('blur', () => finishInlineDescriptionEdit(true)) + ta.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + // Shift+Enter: insert newline (allow default). Enter: finish and save. + if (e.shiftKey) { + // allow default -> newline inserted + return + } + e.preventDefault() + finishInlineDescriptionEdit(true) + } else if (e.key === 'Escape') { + finishInlineDescriptionEdit(false) + } + }) + } + + /** + * Commit a changed annotation description to jsTree and to the live annotation. + * + * Also updates any cached node data used by the sidebar. + * + * @param {string} uuid - annotation UUID + * @param {string} description - new description text + */ + function _commitEditedDescription(uuid, description) { + if (!uuid) return + try { + const tree = _getJSTree() + if (tree) { + const annotationsRoot = _getAnnotationsRoot() + const node = _findNodeInAnnotationsRootByUUID(annotationsRoot, uuid) + if (node) { + node.data = node.data || {} + node.data.description = description + + try { + const jsTreeInst = _getJSTreeInstance() + if (jsTreeInst) { + const jsNode = jsTreeInst.get_node(node.id) + if (jsNode) { + jsNode.original = jsNode.original || {} + jsNode.original.data = jsNode.original.data || {} + jsNode.original.data.description = description + } + } + } catch (e) { + // If we couldn't access the internal node, continue anyway + } + } + } + } catch (e) {} + + try { + const live = _findLiveAnnotationByUUID(uuid) + if (live) { + live.data = live.data || {} + live.data.description = description + if (typeof live.description !== 'undefined') + live.description = description + if (typeof live.desc !== 'undefined') live.desc = description + } + } catch (e) {} + + setTimeout(updateAnnotationsList, 0) + } + + /** + * Commit a changed annotation name to jsTree and to the live annotation object. + * + * This updates the jsTree node text (if accessible) and mutates the live + * annotation instance's title/name/data so the viewer label matches. + * + * @param {string} uuid - annotation UUID + * @param {string} name - new name to commit + */ + function _commitEditedName(uuid, name) { + if (!uuid) return + // update jsTree node text + try { + if (!window.jQuery) return + const tree = + window.jQuery('#jstree_scene').jstree && + window.jQuery('#jstree_scene').jstree() + if (tree) { + const annotationsRoot = tree.get_json('annotations') + if (annotationsRoot && annotationsRoot.children) { + const node = annotationsRoot.children.find( + (c) => (c.data && c.data.uuid) === uuid + ) + if (node) { + _renameJSTreeNode(node.id, name) + node.text = name + } + } + } + } catch (e) {} + + // update live annotation + try { + const live = _findLiveAnnotationByUUID(uuid) + if (live) { + if (typeof live.title !== 'undefined') live.title = name + if (typeof live.name !== 'undefined') live.name = name + if (live.data) live.data.title = name + } + } catch (e) {} + + // refresh sidebar to reflect changes + setTimeout(updateAnnotationsList, 0) + } + + /** + * Create and insert the "Add a location" button for annotations. + * + * The button: + * - Captures the current camera view when clicked. + * - Starts Potree's annotation insertion mode. + * - Waits for placement completion and triggers `updateAnnotationsList`. + * + * Side effects: + * - Inserts a button into the DOM near the annotations list. + */ + function createAddButton() { + const btn = document.createElement('button') + btn.className = 'annotation-add-button' + btn.setAttribute('aria-label', 'Add a new saved location') + btn.innerHTML = `Add a location` + btn.onclick = () => { + // Show this panel, if collapsed + const menu = document.getElementById('potree_menu') + const annotationHeader = document.getElementById( + 'menu_camera_annotations' + ) + if (annotationHeader && annotationHeader.nextElementSibling) { + if (window.jQuery) { + window.jQuery(annotationHeader.nextElementSibling).slideDown() + } else { + annotationHeader.nextElementSibling.style.display = '' + } + } + // Capture current camera view (position) at the moment the user clicks Add + let camPos = null + try { + if (viewer && viewer.scene && viewer.scene.view) { + camPos = + viewer.scene.view.position && + typeof viewer.scene.view.position.toArray === 'function' + ? viewer.scene.view.position.toArray() + : viewer.scene.view.position + ? [ + viewer.scene.view.position.x, + viewer.scene.view.position.y, + viewer.scene.view.position.z + ] + : null + } + } catch (e) { + console.warn('Could not read current view for annotation', e) + } + + // Start Potree annotation insertion + let annotation = viewer.annotationTool.startInsertion() + + // Wait for the actual placement (left click) before updating the sidebar. + try { + const dom = viewer && viewer.renderer && viewer.renderer.domElement + if (dom) { + const onMouseUp = (ev) => { + try { + if (ev.button === 0) { + // left click = placement finished + setTimeout(() => updateAnnotationsList(), 50) + dom.removeEventListener('mouseup', onMouseUp, true) + } + } catch (e) {} + } + dom.addEventListener('mouseup', onMouseUp, true) + } + } catch (e) { + // ignore + } + + // Wait for annotation creation, then select in jsTree + setTimeout(() => { + let tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree() + if (!tree || !annotation) return + // Persist captured camera info on the annotation object so it will be available in the jsTree node data + try { + if (camPos) annotation.cameraPosition = camPos + } catch (e) { + console.warn('Could not attach camera info to annotation', e) + } + updateAnnotationsList() + }, 200) // A short delay for Potree/jsTree to update + } + targetContainer.parentElement.insertBefore(btn, targetContainer) + } + + createAddButton() + updateAnnotationsList() + initAnnotationPersistence(viewer) + + // Listen to Potree's annotation events to auto-refresh the list + if (viewer.scene && viewer.scene.annotations) { + viewer.scene.annotations.addEventListener( + 'annotation_added', + updateAnnotationsList + ) + viewer.scene.annotations.addEventListener( + 'annotation_removed', + updateAnnotationsList + ) + viewer.scene.annotations.addEventListener( + 'annotation_changed', + updateAnnotationsList + ) + } +} diff --git a/src/AnnotationControl/persistence.js b/src/AnnotationControl/persistence.js new file mode 100644 index 0000000..f991210 --- /dev/null +++ b/src/AnnotationControl/persistence.js @@ -0,0 +1,306 @@ +export function initAnnotationPersistence(viewer, options = {}) { + const jsonUrl = options.jsonUrl || '/annotations/annotations.json' + const defaultSaveUrl = (() => { + try { + if ( + typeof window !== 'undefined' && + window.location && + window.location.port === '5173' + ) { + // Dev fallback: post directly to API server to avoid proxy issues + return 'http://localhost:5174/api/annotations' + } + } catch {} + return '/api/annotations' + })() + const saveUrl = defaultSaveUrl + const autosave = true + const folderResolver = defaultGetFolder + const folderSetter = defaultSetFolder + const STORAGE_KEY = `molloy_annotations_snapshot` + const _watchedAnnotations = new Set() + const _titleDescCache = new WeakMap() + + function defaultGetFolder(ann) { + return ann?.userData?.folder || 'General' + } + function defaultSetFolder(ann, folder) { + if (!ann.userData) ann.userData = {} + ann.userData.folder = folder || 'General' + } + + function posToArray(p) { + if (!p) return null + if (Array.isArray(p)) return p + if (typeof p.x === 'number') return [p.x, p.y, p.z] + if (typeof p.toArray === 'function') return p.toArray() + return null + } + + function serializeNode(ann) { + return { + title: ann.title || '', + description: ann.description || '', + position: posToArray( + ann?.marker && ann?.marker?.position + ? ann.marker.position + : ann.position || ann._position + ), + cameraPosition: posToArray(ann.cameraPosition) + } + } + + function serializeGrouped() { + const root = viewer.scene.annotations + const folders = {} + for (const ann of root.children || []) { + const f = folderResolver(ann) || 'General' + if (!folders[f]) folders[f] = [] + folders[f].push(serializeNode(ann)) + } + return { version: 1, folders } + } + + async function loadFromJson() { + try { + const res = await fetch(jsonUrl, { cache: 'no-store' }) + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) + const data = await res.json() + applyJsonToScene(data) + // snapshot to localStorage for quick restore + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)) + } catch {} + } catch (err) { + // fallback to local snapshot + try { + const snap = localStorage.getItem(STORAGE_KEY) + if (snap) applyJsonToScene(JSON.parse(snap)) + } catch {} + } + } + + function applyJsonToScene(json) { + console.log('Loading annotations from JSON...', json) + if (!json || !json.folders) return + const root = viewer.scene.annotations + for (const ch of [...(root.children || [])]) root.remove(ch) + + const folders = json.folders || {} + for (const folderName of Object.keys(folders)) { + const list = folders[folderName] || [] + for (const item of list) addAnnotationRec(root, item, folderName) + } + } + + function addAnnotationRec(parent, item, folderName) { + const posArr = Array.isArray(item.position) ? item.position : [0, 0, 0] + const ann = new Potree.Annotation({ + position: new THREE.Vector3(posArr[0], posArr[1], posArr[2]), + title: item.title || '' + }) + ann.description = item.description || '' + if (Array.isArray(item.cameraPosition)) + ann.cameraPosition = new THREE.Vector3(...item.cameraPosition) + + if (folderSetter) folderSetter(ann, folderName) + parent.add(ann) + + watchAnnotationDeep(ann) + // If Potree attaches a marker asynchronously, hook into it on the next frame + requestAnimationFrame(() => { + try { + if (ann.marker && ann.marker.position) { + if (typeof ann.marker.position.onChange === 'function') { + ann.marker.position.onChange(() => debouncedSnapshot()) + } + } + } catch {} + }) + for (const child of item.children || []) + addAnnotationRec(ann, child, folderName) + } + + function getAnnotationsJSON() { + return serializeGrouped() + } + + function snapshotToLocalStorage() { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(serializeGrouped())) + } catch {} + } + + async function saveToServer(payload) { + const data = payload || serializeGrouped() + try { + const headers = { 'Content-Type': 'application/json' } + const res = await fetch(saveUrl, { + method: 'POST', + headers, + body: JSON.stringify(data) + }) + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) + return await res.json() + } catch (err) { + throw err + } + } + + // Autosnapshot after add/remove + const root = viewer.scene.annotations + let snapshotTimer = null + const debouncedSnapshot = () => { + if (snapshotTimer) cancelAnimationFrame(snapshotTimer) + snapshotTimer = requestAnimationFrame(async () => { + snapshotToLocalStorage() + if (autosave) { + try { + await saveToServer() + } catch {} + } + }) + } + + // Expose minimal handle for the poller to access closure values safely + try { + window.__annPersist = { + watched: _watchedAnnotations, + cache: _titleDescCache, + debouncedSnapshot + } + } catch {} + + // Patch Potree.Annotation.prototype to watch all additions/removals in the tree + if (Potree?.Annotation && !Potree.Annotation.prototype._persistPatched) { + Potree.Annotation.prototype._persistPatched = true + const _protoAdd = Potree.Annotation.prototype.add + const _protoRemove = Potree.Annotation.prototype.remove + Potree.Annotation.prototype.add = function (...args) { + const child = args[0] + const r = _protoAdd.apply(this, args) + if (child) watchAnnotationDeep(child) + // Delay a bit so position can settle after placement + requestAnimationFrame(() => requestAnimationFrame(debouncedSnapshot)) + return r + } + Potree.Annotation.prototype.remove = function (...args) { + const r = _protoRemove.apply(this, args) + debouncedSnapshot() + return r + } + } + + // Snapshot whenever an annotation changes (e.g., placed/moved/edited) + function onAnnotationChanged() { + debouncedSnapshot() + } + try { + root.addEventListener('annotation_changed', onAnnotationChanged) + root.addEventListener('annotation_added', () => { + requestAnimationFrame(() => requestAnimationFrame(debouncedSnapshot)) + }) + root.addEventListener('annotation_removed', onAnnotationChanged) + } catch (_) {} + + function watchAnnotationDeep(ann) { + if (!ann || ann._persistWatched) return + ann._persistWatched = true + watchAnnotation(ann) + // Watch vector changes to capture post-placement updates reliably + watchVectorChanges(ann.position) + if (ann.marker && ann.marker.position) + watchVectorChanges(ann.marker.position) + _watchedAnnotations.add(ann) + // cache current title/desc + try { + _titleDescCache.set(ann, { t: ann.title, d: ann.description }) + } catch {} + ;(ann.children || []).forEach(watchAnnotationDeep) + } + + function patchSetMethod(obj, methodName) { + if (!obj || typeof obj[methodName] !== 'function') return + const original = obj[methodName] + if (original._persistPatched) return + const patched = function (...args) { + const r = original.apply(this, args) + debouncedSnapshot() + return r + } + patched._persistPatched = true + obj[methodName] = patched + } + + function watchAnnotation(ann) { + patchSetMethod(ann, 'setTitle') + patchSetMethod(ann, 'setDescription') + if (ann.cameraPosition) watchVectorChanges(ann.cameraPosition) + } + + function watchVectorChanges(vec) { + if (!vec) return + try { + if (typeof vec.onChange === 'function' && !vec._persistOnChangeHooked) { + vec._persistOnChangeHooked = true + vec.onChange(() => debouncedSnapshot()) + } else if (!vec._persistPollHooked) { + // Fallback: brief polling window to detect early changes when onChange is unavailable + vec._persistPollHooked = true + let frames = 0 + let last = `${vec.x},${vec.y},${vec.z}` + const poll = () => { + const cur = `${vec.x},${vec.y},${vec.z}` + if (cur !== last) { + last = cur + debouncedSnapshot() + } + if (frames++ < 90) requestAnimationFrame(poll) // ~1.5s at 60fps + } + requestAnimationFrame(poll) + } + } catch {} + } + + loadFromJson() + watchAnnotationDeep(root) + startTitleDescPoller() + + return { + getAnnotationsJSON, + snapshotToLocalStorage, + loadFromJson, + serializeGrouped, + saveToServer + } +} + +function startTitleDescPoller() { + // Use a module-level flag to avoid multiple pollers + if (startTitleDescPoller._started) return + startTitleDescPoller._started = true + try { + const tick = () => { + try { + const h = window.__annPersist + if (h && h.watched && h.cache) { + let changed = false + h.watched.forEach((ann) => { + const prev = h.cache.get(ann) || { t: undefined, d: undefined } + const curT = ann.title + const curD = ann.description + if (prev.t !== curT || prev.d !== curD) { + h.cache.set(ann, { t: curT, d: curD }) + changed = true + } + }) + if (changed && typeof h.debouncedSnapshot === 'function') { + h.debouncedSnapshot() + } + } + } catch {} + requestAnimationFrame(tick) + } + requestAnimationFrame(tick) + } catch {} +} diff --git a/src/potreeViewer.js b/src/potreeViewer.js index ae66b30..8614525 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -62,6 +62,7 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { } initMeasurementsPanel(viewer) + initAnnotationsPanel(viewer) }) const e = await Potree.loadPointCloud(pointcloudUrl) diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..afeed41 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:5174', + changeOrigin: true, + secure: false + } + } + } +})