Compare commits

...

188 Commits

Author SHA1 Message Date
renovate[bot]
07cec00b1a chore(deps): update dependency @vitejs/plugin-vue to v6.0.3 2025-12-13 05:08:29 +00:00
renovate[bot]
4ed83e32a4 chore(deps): update dependency @types/node to v24.10.3 2025-12-13 00:58:20 +00:00
renovate[bot]
ea1cd46d22 chore(deps): lock file maintenance 2025-12-09 12:30:29 +00:00
renovate[bot]
8092075422 chore(deps): lock file maintenance (#267)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 06:11:59 +00:00
renovate[bot]
65cc7144fa chore(deps): update dependency vue-tsc to v3.1.8 (#266)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 03:49:44 +00:00
renovate[bot]
3ca1544fc0 chore(deps): lock file maintenance 2025-12-06 18:34:51 +00:00
renovate[bot]
d63e274bb6 chore(deps): update dependency vuetify to v3.11.2 2025-12-06 12:57:35 +00:00
renovate[bot]
617f83f6f1 chore(deps): update dependency vue-tsc to v3.1.6 2025-12-06 08:57:35 +00:00
renovate[bot]
76df2f5ca5 chore(deps): update dependency vite to v7.2.6 2025-12-06 04:48:05 +00:00
renovate[bot]
b7f26dbfcb chore(deps): update dependency three-mesh-bvh to v0.9.3 2025-12-06 02:52:50 +00:00
renovate[bot]
4c4c8c766c chore(deps): lock file maintenance 2025-11-30 21:12:02 +00:00
renovate[bot]
1c414d920d chore(deps): update dependency vuetify to v3.11.0 2025-11-29 01:14:25 +00:00
renovate[bot]
6dba535906 chore(deps): lock file maintenance 2025-11-24 09:05:25 +00:00
renovate[bot]
eaf09203da chore(deps): update dependency generate-license-file to v4.1.1 2025-11-23 16:40:36 +00:00
renovate[bot]
2393080240 chore(deps): lock file maintenance 2025-11-23 08:43:40 +00:00
renovate[bot]
df890b798c chore(deps): update actions/checkout action to v6 2025-11-23 05:58:56 +00:00
renovate[bot]
cde0e6137b fix(deps): update dependency monaco-editor to ^0.55.0 2025-11-23 01:38:46 +00:00
renovate[bot]
8a8cc8e5a8 chore(deps): update dependency vue-tsc to v3.1.5 2025-11-22 21:55:05 +00:00
renovate[bot]
7863611a51 chore(deps): update dependency vite to v7.2.4 2025-11-22 16:47:42 +00:00
renovate[bot]
367fa1b4e4 chore(deps): update dependency three to v0.181.2 2025-11-22 12:35:29 +00:00
renovate[bot]
7b553b635b chore(deps): update dependency @vitejs/plugin-vue-jsx to v5.1.2 2025-11-22 08:26:50 +00:00
renovate[bot]
24cf291cdd chore(deps): update dependency @vitejs/plugin-vue to v6.0.2 2025-11-22 06:04:56 +00:00
renovate[bot]
9c133a6d50 chore(deps): update dependency @tsconfig/node20 to v20.1.8 2025-11-22 00:52:28 +00:00
Yeicor
d44ea072d5 Merge pull request #263 from yeicor-3d/dependabot/npm_and_yarn/glob-11.1.0
Bump glob from 11.0.3 to 11.1.0
2025-11-21 18:07:58 +02:00
dependabot[bot]
014622ffaa Bump glob from 11.0.3 to 11.1.0
Bumps [glob](https://github.com/isaacs/node-glob) from 11.0.3 to 11.1.0.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v11.0.3...v11.1.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 11.1.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-21 12:18:16 +00:00
renovate[bot]
23b50c38ca chore(deps): lock file maintenance 2025-11-16 14:08:57 +00:00
renovate[bot]
8154ac9502 chore(deps): lock file maintenance 2025-11-16 06:02:36 +00:00
renovate[bot]
aaac6d4245 chore(deps): lock file maintenance 2025-11-16 01:00:26 +00:00
renovate[bot]
91f5084532 chore(deps): update dependency vue-tsc to v3.1.4 2025-11-15 21:08:37 +00:00
renovate[bot]
e957e0a937 chore(deps): lock file maintenance 2025-11-15 17:01:59 +00:00
renovate[bot]
e480d297e8 chore(deps): lock file maintenance 2025-11-15 14:27:57 +00:00
renovate[bot]
4439647faa chore(deps): update dependency vuetify to v3.10.11 2025-11-15 08:26:07 +00:00
renovate[bot]
4f2c9eda11 chore(deps): update dependency @types/node to v24.10.1 2025-11-15 07:00:33 +00:00
renovate[bot]
ad8e75d1da chore(deps): update dependency @tsconfig/node20 to v20.1.7 2025-11-15 02:14:46 +00:00
renovate[bot]
14022f170c chore(deps): lock file maintenance 2025-11-10 06:58:22 +00:00
renovate[bot]
6302fc023c chore(deps): update dependency vite to v7.2.2 2025-11-08 21:43:54 +00:00
renovate[bot]
3685ed13c0 chore(deps): update dependency @types/node to v24.10.0 2025-11-08 17:03:38 +00:00
renovate[bot]
b2c815daf7 chore(deps): update dependency vue-tsc to v3.1.3 2025-11-08 14:38:40 +00:00
renovate[bot]
630bb7d90b chore(deps): update dependency vuetify to v3.10.9 2025-11-08 09:26:22 +00:00
renovate[bot]
aa2c2e570b chore(deps): update dependency vue to v3.5.24 2025-11-08 06:15:07 +00:00
renovate[bot]
5855458ff0 chore(deps): update dependency terser to v5.44.1 2025-11-08 00:43:10 +00:00
renovate[bot]
41170118c3 fix(deps): update dependency build123d to >=0.10,<0.11 2025-11-05 22:34:18 +00:00
renovate[bot]
efa689bfe4 chore(deps): lock file maintenance 2025-11-02 09:11:38 +00:00
renovate[bot]
971a791617 chore(deps): lock file maintenance 2025-11-02 01:30:02 +00:00
renovate[bot]
1d1228e044 chore(deps): lock file maintenance 2025-11-01 17:27:45 +00:00
renovate[bot]
55ba73a635 chore(deps): update dependency @types/node to v24 2025-11-01 13:58:33 +00:00
renovate[bot]
b51135ba39 fix(deps): update dependency three to ^0.181.0 2025-11-01 09:34:57 +00:00
renovate[bot]
95755f6ad3 chore(deps): update dependency vuetify to v3.10.8 2025-11-01 04:37:50 +00:00
renovate[bot]
82efea0786 chore(deps): update dependency @types/node to v22.18.13 2025-11-01 01:46:24 +00:00
renovate[bot]
0a66e62575 chore(deps): lock file maintenance 2025-10-26 09:55:11 +00:00
renovate[bot]
4f6bdbe1ad chore(deps): update github artifact actions 2025-10-26 04:38:28 +00:00
renovate[bot]
92d09d52d3 fix(deps): update dependency pyodide to ^0.29.0 2025-10-26 02:06:17 +00:00
renovate[bot]
d012d68cfc chore(deps): update dependency vuetify to v3.10.7 2025-10-25 21:36:15 +00:00
renovate[bot]
496df80e26 chore(deps): update dependency vue-tsc to v3.1.2 2025-10-25 18:31:40 +00:00
renovate[bot]
8df15a9cad chore(deps): update dependency commander to v14.0.2 2025-10-25 14:33:16 +00:00
renovate[bot]
03eac030c4 chore(deps): update dependency vite to v7.1.12 2025-10-25 10:12:15 +00:00
renovate[bot]
8f49a5deee chore(deps): update dependency three-mesh-bvh to v0.9.2 2025-10-25 05:03:43 +00:00
renovate[bot]
070488f8c6 chore(deps): update dependency @types/node to v22.18.12 2025-10-25 02:06:10 +00:00
renovate[bot]
44fb50869e chore(deps): lock file maintenance 2025-10-20 10:02:53 +00:00
renovate[bot]
c8e1ba5f07 chore(deps): update dependency @guolao/vue-monaco-editor to v1.6.0 2025-10-19 20:52:27 +00:00
renovate[bot]
cb2e09a6c2 chore(deps): lock file maintenance 2025-10-19 05:45:15 +00:00
renovate[bot]
daf9fb16f7 chore(deps): lock file maintenance 2025-10-18 21:02:19 +00:00
renovate[bot]
98bc0407b3 fix(deps): update dependency pillow to v12 2025-10-18 18:05:44 +00:00
renovate[bot]
57bcd52944 chore(deps): update actions/setup-node action to v6 2025-10-18 13:07:24 +00:00
renovate[bot]
ce03ea001d chore(deps): update dependency vite to v7.1.10 2025-10-18 09:10:41 +00:00
renovate[bot]
b6a2bde5e1 chore(deps): update dependency vite-plugin-static-copy to v3.1.4 2025-10-18 04:55:48 +00:00
renovate[bot]
9079ccc0b3 chore(deps): update dependency @types/node to v22.18.11 2025-10-18 02:10:36 +00:00
renovate[bot]
aed7680d48 chore(deps): lock file maintenance 2025-10-12 13:04:45 +00:00
renovate[bot]
6fc4f922fd chore(deps): lock file maintenance 2025-10-11 20:55:58 +00:00
renovate[bot]
e5ee7a45be chore(deps): update dependency @types/node to v22.18.10 2025-10-11 18:00:21 +00:00
renovate[bot]
c9c3fe5bd9 chore(deps): update dependency vuetify to v3.10.5 2025-10-11 08:51:28 +00:00
renovate[bot]
cc39079c70 chore(deps): update dependency vue-tsc to v3.1.1 2025-10-11 05:35:17 +00:00
renovate[bot]
a52e503e81 chore(deps): update dependency @types/node to v22.18.9 2025-10-11 02:02:02 +00:00
renovate[bot]
dd5c4253a7 fix(deps): update dependency monaco-editor to ^0.54.0 2025-10-06 15:06:44 +00:00
renovate[bot]
209b40e9a7 chore(deps): lock file maintenance 2025-10-05 21:50:11 +00:00
renovate[bot]
96c5c8a63f chore(deps): lock file maintenance 2025-10-05 17:35:56 +00:00
renovate[bot]
0aa761dcf4 chore(deps): update dependency vuetify to v3.10.4 2025-10-04 17:14:55 +00:00
renovate[bot]
be9f3994a8 chore(deps): update dependency vite-plugin-static-copy to v3.1.3 2025-10-04 13:58:31 +00:00
renovate[bot]
ed1296f858 chore(deps): update dependency vite to v7.1.9 2025-10-04 09:08:43 +00:00
renovate[bot]
1c908bdfba chore(deps): update dependency typescript to v5.9.3 2025-10-04 05:01:37 +00:00
renovate[bot]
7434652379 chore(deps): update dependency @types/node to v22.18.8 2025-10-04 00:42:05 +00:00
Yeicor
f59e3048d4 Merge pull request #261 from yeicor-3d/yeicor-patch-1
CONTRIBUTING.md
2025-10-03 20:02:35 +02:00
Yeicor
ed2431f7da CONTRIBUTING.md 2025-10-03 20:00:35 +02:00
renovate[bot]
c4d47afe69 chore(deps): lock file maintenance 2025-09-29 22:37:12 +00:00
renovate[bot]
ec91ab90f2 chore(deps): update dependency vue-tsc to v3.1.0 2025-09-28 16:40:19 +00:00
renovate[bot]
3f573c75cc chore(deps): lock file maintenance 2025-09-28 11:41:14 +00:00
Yeicor
4eee767afb Automatically update version to 0.10.11 2025-09-28 11:39:46 +00:00
Yeicor
3f9ec31799 Merge pull request #259 from quasipedia/master
Fix for symlink failures
2025-09-28 13:34:27 +02:00
Yeicor
54a22b1499 Merge branch 'master' into master 2025-09-28 13:31:52 +02:00
Yeicor
438d315b53 Remove center axes helpers 2025-09-28 13:08:42 +02:00
renovate[bot]
0cc6d81e9e chore(deps): lock file maintenance 2025-09-28 04:57:54 +00:00
renovate[bot]
2468aeed08 chore(deps): lock file maintenance 2025-09-28 01:43:50 +00:00
renovate[bot]
a8ec19e2d4 chore(deps): update dependency vuetify to v3.10.3 2025-09-27 16:24:53 +00:00
renovate[bot]
741a658b01 chore(deps): update dependency vue-tsc to v3.0.8 2025-09-27 12:26:41 +00:00
renovate[bot]
19663354cc chore(deps): update dependency vite to v7.1.7 2025-09-27 08:33:35 +00:00
renovate[bot]
e5f2246ff1 chore(deps): update dependency vue to v3.5.22 2025-09-27 05:52:36 +00:00
renovate[bot]
4cb8a65582 chore(deps): update dependency pyodide to v0.28.3 2025-09-27 02:06:27 +00:00
Mac Ryan
fc737583fd Fix fro symlink failures
Issue outlined in https://github.com/yeicor-3d/yet-another-cad-viewer/issues/258
2025-09-26 14:44:24 +02:00
renovate[bot]
37ddf9ab63 fix(deps): update dependency poetry-core to v2.2.1 2025-09-21 17:34:18 +00:00
renovate[bot]
5977172096 chore(deps): lock file maintenance 2025-09-21 05:09:56 +00:00
renovate[bot]
888239d898 fix(deps): update dependency vuetify to v3.10.2 2025-09-20 09:42:09 +00:00
renovate[bot]
fdfc5cdcad chore(deps): update dependency vite to v7.1.6 2025-09-20 05:14:31 +00:00
renovate[bot]
e2707ac8db chore(deps): update dependency @types/node to v22.18.6 2025-09-20 00:38:42 +00:00
renovate[bot]
f0a55efff1 chore(deps): lock file maintenance 2025-09-15 09:08:51 +00:00
renovate[bot]
71e95ec393 fix(deps): update dependency vuetify to v3.10.0 2025-09-14 20:50:44 +00:00
renovate[bot]
454a7249cd fix(deps): update dependency monaco-editor to ^0.53.0 (#252)
* fix(deps): update dependency monaco-editor to ^0.53.0

* Fix monaco.ts

* Fix monaco.ts

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Yeicor <4929005+yeicor@users.noreply.github.com>
2025-09-14 20:43:04 +00:00
renovate[bot]
1501f8c3b5 fix(deps): update dependency poetry-core to v2.2.0 2025-09-14 13:36:36 +00:00
renovate[bot]
32d34d9ec4 chore(deps): update dependency vue-tsc to v3.0.7 2025-09-14 09:40:30 +00:00
renovate[bot]
9b3b679631 chore(deps): update dependency commander to v14.0.1 2025-09-14 03:08:14 +00:00
renovate[bot]
dd389f34a3 chore(deps): update dependency @types/node to v22.18.3 2025-09-13 23:34:05 +00:00
renovate[bot]
21cdaf2c33 chore(deps): update dependency vite to v7.1.5 [security] 2025-09-10 02:01:36 +00:00
renovate[bot]
ace4d5aa5e chore(deps): lock file maintenance 2025-09-07 13:50:19 +00:00
renovate[bot]
289677a4ac chore(deps): lock file maintenance 2025-09-06 21:02:36 +00:00
renovate[bot]
a97c2807d8 chore(deps): update actions/setup-python action to v6 2025-09-06 16:52:44 +00:00
renovate[bot]
629aa939ca chore(deps): update actions/setup-node action to v5 2025-09-06 12:54:34 +00:00
renovate[bot]
e21fa7d680 fix(deps): update dependency three to ^0.180.0 2025-09-06 08:42:02 +00:00
renovate[bot]
3933d4842b fix(deps): update dependency vuetify to v3.9.7 2025-09-06 04:32:19 +00:00
renovate[bot]
95ffb769e2 chore(deps): update dependency @types/node to v22.18.1 2025-09-06 02:29:40 +00:00
renovate[bot]
16627a3cd4 chore(deps): lock file maintenance 2025-09-03 04:48:07 +00:00
renovate[bot]
0e1f6b7aa0 chore(deps): update dependency generate-license-file to v4.1.0 2025-09-03 01:57:42 +00:00
renovate[bot]
0ff8776a12 chore(deps): update dependency @vitejs/plugin-vue-jsx to v5.1.1 2025-09-03 00:03:00 +00:00
renovate[bot]
64e9e26da1 chore(deps): update dependency @types/node to v22.18.0 2025-08-31 20:36:13 +00:00
renovate[bot]
7eaba92057 fix(deps): update dependency vuetify to v3.9.6 2025-08-31 16:54:16 +00:00
renovate[bot]
6a0aa265b6 chore(deps): update dependency @vue/tsconfig to ^0.8.0 (#251)
* chore(deps): update dependency @vue/tsconfig to ^0.8.0

* Fix new ts issues

* Add null checks for selection and model objects throughout frontend

This improves robustness by handling cases where selection or model objects may be missing or undefined, preventing
runtime errors.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Yeicor <4929005+yeicor@users.noreply.github.com>
2025-08-31 15:15:10 +00:00
renovate[bot]
fbf2f3e9a1 fix(deps): update dependency vue to v3.5.20 2025-08-31 09:06:02 +00:00
renovate[bot]
74491fc867 chore(deps): lock file maintenance 2025-08-23 20:40:39 +00:00
renovate[bot]
cc0b087cf7 chore(deps): update actions/upload-pages-artifact action to v4 2025-08-23 16:37:26 +00:00
renovate[bot]
fcacf8172d fix(deps): update dependency vue to v3.5.19 2025-08-23 14:37:44 +00:00
renovate[bot]
0c93a7e05d fix(deps): update dependency pyodide to v0.28.2 2025-08-23 08:42:13 +00:00
renovate[bot]
e50e3c7f09 chore(deps): update dependency vue-tsc to v3.0.6 2025-08-23 05:22:43 +00:00
renovate[bot]
9f76df8668 chore(deps): update dependency vite to v7.1.3 2025-08-23 00:41:43 +00:00
renovate[bot]
6c7f7382f6 chore(deps): update dependency vite-plugin-static-copy to v3.1.2 [security] 2025-08-21 18:33:48 +00:00
renovate[bot]
c7aee20aac chore(deps): lock file maintenance 2025-08-17 21:08:59 +00:00
renovate[bot]
8598f7f75e chore(deps): lock file maintenance 2025-08-17 01:46:09 +00:00
renovate[bot]
7a8ff04cc7 chore(deps): update actions/checkout action to v5 2025-08-16 20:50:46 +00:00
renovate[bot]
ac8bf8cb06 fix(deps): update dependency vuetify to v3.9.5 2025-08-16 18:13:39 +00:00
renovate[bot]
7a425e3ccc fix(deps): update dependency js-base64 to v3.7.8 2025-08-16 12:48:15 +00:00
renovate[bot]
ff3c3d6f2f chore(deps): update dependency vite to v7.1.2 2025-08-16 09:27:29 +00:00
renovate[bot]
8792ab7139 chore(deps): update dependency @types/pako to v2.0.4 2025-08-16 04:34:05 +00:00
renovate[bot]
291e3c726d chore(deps): update dependency @types/node to v22.17.2 2025-08-16 01:13:43 +00:00
renovate[bot]
be400fde29 chore(deps): lock file maintenance 2025-08-10 22:21:38 +00:00
renovate[bot]
32fb3230bb chore(deps): lock file maintenance 2025-08-10 05:29:41 +00:00
renovate[bot]
a63175dc3a chore(deps): update actions/download-artifact action to v5 2025-08-09 20:46:08 +00:00
Yeicor
3c013de8ba Update README.md 2025-08-09 19:32:59 +02:00
renovate[bot]
a659b0561f chore(deps): lock file maintenance (#249)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-09 17:22:03 +00:00
renovate[bot]
ccd48bcae3 fix(deps): update dependency pyodide to v0.28.1 (#250)
* fix(deps): update dependency pyodide to v0.28.1

* Fix after pyodide update and update licenses

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Yeicor <4929005+Yeicor@users.noreply.github.com>
2025-08-09 16:40:16 +00:00
renovate[bot]
1a9cac08cd chore(deps): update dependency @types/three to ^0.179.0 2025-08-09 09:31:56 +00:00
renovate[bot]
3fb07300b8 fix(deps): update dependency vuetify to v3.9.4 2025-08-09 04:49:40 +00:00
renovate[bot]
938c554919 chore(deps): update dependency @types/node to v22.17.1 2025-08-09 01:19:43 +00:00
Yeicor
5308ececae Automatically update version to 0.10.10 2025-08-05 20:55:32 +00:00
Yeicor
64e2622954 Fix uploading multiple models when sharing links 2025-08-05 22:51:58 +02:00
Yeicor
2e8b8bd65f Fix uploading multiple models when sharing links 2025-08-05 22:49:11 +02:00
Yeicor
854de26695 Automatically update version to 0.10.9 2025-08-04 15:52:38 +00:00
Yeicor
e0503983f1 Ability to explode models and minor improvements 2025-08-04 17:47:22 +02:00
Yeicor
021cfd89a1 Fix typescript 5.9 errors and minor improvements 2025-08-02 14:20:59 +02:00
renovate[bot]
9d96effb30 chore(deps): lock file maintenance 2025-08-02 12:10:18 +00:00
Yeicor
dff2d36b44 Automatically update version to 0.10.8 2025-08-02 12:07:49 +00:00
Yeicor
810647276b Fix demo 2 2025-08-02 14:06:50 +02:00
Yeicor
a54b91f704 Fix demo 2025-08-02 14:06:30 +02:00
Yeicor
099726035a Automatically update version to 0.10.7 2025-08-02 11:41:09 +00:00
Yeicor
c383b8573a Fix production builds 3 2025-08-02 13:37:58 +02:00
Yeicor
f1c0a21699 Automatically update version to 0.10.6 2025-08-02 11:35:48 +00:00
Yeicor
8df0401fef Fix production builds 2 2025-08-02 13:34:53 +02:00
Yeicor
2c7adf8188 Cleaner deployment 2 2025-08-02 13:18:10 +02:00
Yeicor
d2b1b3b952 Automatically update version to 0.10.5 2025-08-02 11:16:15 +00:00
Yeicor
db3bbf2d32 Cleaner deployment 2025-08-02 13:15:25 +02:00
Yeicor
1d6230ef1e Fix production builds 2025-08-02 13:12:15 +02:00
renovate[bot]
7d22ca465e fix(deps): update dependency three to ^0.179.0 2025-08-02 11:03:50 +00:00
Yeicor
7c38ccc522 Automatically update version to 0.10.4 2025-08-02 11:02:49 +00:00
Yeicor
06a95d4875 Add upload and share functionality and minor improvements 2025-08-02 12:50:20 +02:00
renovate[bot]
56fc556c0f chore(deps): update dependency @vitejs/plugin-vue to v6.0.1 2025-08-02 02:45:14 +00:00
Yeicor
fe54f48ad7 Automatically update version to 0.10.3 2025-07-31 18:25:41 +00:00
Yeicor
c47f961b0d Automatically update version to 0.10.2 2025-07-31 18:13:15 +00:00
Yeicor
f86b714c08 Automatically update version to 0.10.2-alpha.1 2025-07-31 17:56:25 +00:00
Yeicor
2ae7c86f8d Fix release CI 2025-07-31 19:54:03 +02:00
Yeicor
0908216bfc Preinstall font-fetcher package for automatically downloading any requested font. 2025-07-31 19:34:06 +02:00
renovate[bot]
7871a5070d chore(deps): lock file maintenance 2025-07-27 17:42:55 +00:00
renovate[bot]
5173f00d29 chore(deps): lock file maintenance 2025-07-27 10:42:21 +00:00
renovate[bot]
c2ef910783 chore(deps): lock file maintenance 2025-07-26 17:45:40 +00:00
Yeicor
5a5f948224 Update README.md 2025-07-26 19:30:00 +02:00
Yeicor
dbeae5632e Automatically update version to 0.10.1 2025-07-26 15:28:24 +00:00
Yeicor
59116e4a1a Merge remote-tracking branch 'origin/master' 2025-07-26 17:27:41 +02:00
Yeicor
0be0103c3c Fix loading from dev server 2025-07-26 17:27:32 +02:00
Yeicor
d6deef9e7f Automatically update version to 0.10.0 2025-07-26 15:21:57 +00:00
Yeicor
ad956762f4 Automatically update version to 0.10.0-rc.6 2025-07-26 15:05:40 +00:00
Yeicor
a4acd2f3d3 Revert "fix(deps): update python to >=3.13,<3.14 (#246)"
This reverts commit 0855a9c6c7.
2025-07-26 17:04:18 +02:00
Yeicor
c877fef490 Minor fixes and drag and drop models onto interface 2025-07-26 17:01:42 +02:00
renovate[bot]
0855a9c6c7 fix(deps): update python to >=3.13,<3.14 (#246)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-26 12:19:17 +00:00
34 changed files with 3213 additions and 2644 deletions

View File

@@ -14,15 +14,15 @@ jobs:
name: "Build frontend"
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
- uses: "actions/checkout@v6"
with:
ref: "${{ inputs.ref }}"
- uses: "actions/setup-node@v4"
- uses: "actions/setup-node@v6"
with:
cache: "yarn"
- run: "yarn install"
- run: "yarn build"
- uses: "actions/upload-artifact@v4"
- uses: "actions/upload-artifact@v5"
with:
name: "frontend"
path: "dist"
@@ -32,11 +32,11 @@ jobs:
name: "Build backend"
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
- uses: "actions/checkout@v6"
with:
ref: "${{ inputs.ref }}"
- run: "pipx install poetry"
- uses: "actions/setup-python@v5"
- uses: "actions/setup-python@v6"
with:
python-version: "3.13"
cache: "poetry"
@@ -48,18 +48,18 @@ jobs:
name: "Build logo"
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
- uses: "actions/checkout@v6"
with:
ref: "${{ inputs.ref }}"
- run: "pipx install poetry"
- uses: "actions/setup-python@v5"
- uses: "actions/setup-python@v6"
with:
python-version: "3.13"
cache: "poetry"
- run: "poetry lock"
- run: "poetry install"
- run: "poetry run python yacv_server/logo.py"
- uses: "actions/upload-artifact@v4"
- uses: "actions/upload-artifact@v5"
with:
name: "logo"
path: "assets/logo_build"
@@ -69,18 +69,18 @@ jobs:
name: "Build example"
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
- uses: "actions/checkout@v6"
with:
ref: "${{ inputs.ref }}"
- run: "pipx install poetry"
- uses: "actions/setup-python@v5"
- uses: "actions/setup-python@v6"
with:
python-version: "3.13"
cache: "poetry"
- run: "poetry lock"
- run: "poetry install"
- run: "YACV_DISABLE_SERVER=true poetry run python example/object.py"
- uses: "actions/upload-artifact@v4"
- uses: "actions/upload-artifact@v5"
with:
name: "example"
path: "export"

View File

@@ -12,32 +12,37 @@ jobs:
update-versions:
runs-on: "ubuntu-latest"
outputs:
skip: "${{ steps.check_recursive.outputs.skip || 'false' }}" # Default to false if not set
steps:
- uses: "actions/checkout@v4"
- uses: "actions/checkout@v6"
with: # Ensure we are not in a detached HEAD state
ref: "master"
token: "${{ secrets.GH_PAT }}"
# Check that the tag commit is the latest master commit
- run: |
- id: check_recursive
run: |
git fetch --tags
tag_commit=$(git rev-parse ${{ github.ref }})
master_commit=$(git rev-parse master)
if [ "$tag_commit" != "$master_commit" ]; then
echo "The tag commit ($tag_commit) is not the latest master commit ($master_commit)"
exit 1
echo "::warning ::The tag commit $tag_commit does not match the latest master commit $master_commit. This is probably a recursive tag push that will be ignored."
echo "skip=true" >> $GITHUB_OUTPUT
fi
- run: "echo 'CLEAN_VERSION=${{ github.ref }}' | sed 's,refs/tags/v,,g' >> $GITHUB_ENV"
# Write the new version to package.json
- uses: "actions/setup-node@v4"
- uses: "actions/setup-node@v6"
- run: "yarn version --new-version $CLEAN_VERSION --no-git-tag-version"
# Write the new version to pyproject.toml
- run: "pipx install poetry"
- uses: "actions/setup-python@v5"
- uses: "actions/setup-python@v6"
with:
python-version: "3.13"
cache: "poetry"
- run: "poetry version $CLEAN_VERSION"
# Commit the changes and move the tag!
- run: |
- if: "steps.check_recursive.outputs.skip != 'true'"
run: |
git config --global user.email "yeicor@users.noreply.github.com"
git config --global user.name "Yeicor"
if git commit -am "Automatically update version to $CLEAN_VERSION"; then
@@ -53,6 +58,7 @@ jobs:
deploy: # Makes sure all artifacts are updated and use the new version for the next deployment steps
needs: "update-versions"
if: "needs.update-versions.outputs.skip != 'true'" # Only run if the update-versions job did not skip
uses: "./.github/workflows/deploy2.yml"
secrets: "inherit" # Inherit the secrets from the parent workflow
with:

View File

@@ -32,12 +32,12 @@ jobs:
name: "github-pages"
url: "${{ steps.deployment.outputs.page_url }}"
steps:
- uses: "actions/download-artifact@v4"
- uses: "actions/download-artifact@v6"
with: # Downloads all artifacts from the build job
path: "./public"
merge-multiple: true
- uses: "actions/configure-pages@v5"
- uses: "actions/upload-pages-artifact@v3"
- uses: "actions/upload-pages-artifact@v4"
with:
path: 'public'
- id: "deployment"
@@ -54,14 +54,14 @@ jobs:
needs: "rebuild"
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
- uses: "actions/checkout@v6"
with:
ref: "${{ inputs.ref }}"
- uses: "actions/setup-node@v4"
- uses: "actions/setup-node@v6"
with:
cache: "yarn"
- run: "pipx install poetry"
- uses: "actions/setup-python@v5"
- uses: "actions/setup-python@v6"
with:
python-version: "3.13"
cache: "poetry"

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@
*.iml
venv/
__pycache__/

15
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,15 @@
# Contributing to Yet Another CAD Viewer
Thank you for your interest in contributing to this project!
## How to contribute
- For **non-trivial changes** or new features, please [open an issue](https://github.com/yeicor-3d/yet-another-cad-viewer/issues) first to discuss your idea before submitting a pull request.
- For **small fixes** (such as typo corrections or minor improvements), feel free to submit a pull request directly.
## Pull requests
- Make sure your changes are clear and concise.
- Describe what your pull request does and reference any related issues.
We appreciate your help in improving this project!

View File

@@ -10,22 +10,22 @@ in a web browser.
- All [GLTF 2.0](https://www.khronos.org/gltf/) features (textures, PBR materials, animations...).
- All [model-viewer](https://modelviewer.dev/) features (smooth controls, augmented reality...).
- Load multiple models at once, load external models and even images as quads.
- Control clipping planes and transparency of each model.
- Control clipping planes, transparency, edge/vertex sizes and explode each model.
- View and interact with topological entities: faces, edges, vertices and locations.
- Select any entity and measure bounding box size and distances.
- Hot reloading while editing the CAD model (using the `yacv-server` package).
- Fully-featured static deployment: just upload the viewer and models to your server.
- Build123d playground! code and build your model fully inside the
browser: [demo](https://yeicor-3d.github.io/yet-another-cad-viewer/#pg_code_url=https://raw.githubusercontent.com/gumyr/build123d/refs/heads/dev/examples/toy_truck.py).
- Build123d playground! Code and build your model fully inside the
browser: [demo](https://yeicor-3d.github.io/yet-another-cad-viewer/#pg_code=https://raw.githubusercontent.com/gumyr/build123d/refs/heads/dev/examples/toy_truck.py).
## Usage
The [example](example) is a fully working project that shows how to use the viewer.
You can play with the latest
demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb&preload=logo_hl.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)
demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.glb&preload=location.glb)
(or
[without animation](https://yeicor-3d.github.io/yet-another-cad-viewer/?autoplay=false&preload=logo.glb&preload=logo_hl.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)).
[without animation](https://yeicor-3d.github.io/yet-another-cad-viewer/?autoplay=false&preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.glb&preload=location.glb)).
![Demo](assets/screenshot.png)
@@ -36,3 +36,6 @@ demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb
Uses the same backend and frontend behind the scenes.
- [build123d-docker](https://github.com/derhuerst/build123d-docker/pkgs/container/build123d) provides docker images for
Yet Another CAD Viewer and other projects, with automatic updates.
- [OCP.wasm](https://github.com/yeicor/OCP.wasm/) ports OCP (OpenCASCADE for Python) and supporting libraries to
WebAssembly, enabling full in-browser CAD model generation and manipulation. This powers the build123d playground
provided by this viewer.

View File

@@ -809,9 +809,9 @@ The above copyright notice and this permission notice shall be included in all c
The following npm packages may be included in this product:
- @img/sharp-linux-x64@0.33.5
- @img/sharp-linuxmusl-x64@0.33.5
- sharp@0.33.5
- @img/sharp-linux-x64@0.34.3
- @img/sharp-linuxmusl-x64@0.34.3
- sharp@0.34.3
These packages each contain the following license:
@@ -1011,7 +1011,7 @@ third-party archives.
The following npm package may be included in this product:
- typescript@5.8.3
- typescript@5.9.2
This package contains the following license:
@@ -1504,6 +1504,16 @@ SOFTWARE.
-----------
The following npm package may be included in this product:
- tanmayo7lock@1.0.18
This package contains the following license:
ISC
-----------
The following npm package may be included in this product:
- picocolors@1.1.1
@@ -1530,8 +1540,8 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
The following npm packages may be included in this product:
- @img/sharp-libvips-linux-x64@1.0.4
- @img/sharp-libvips-linuxmusl-x64@1.0.4
- @img/sharp-libvips-linux-x64@1.2.0
- @img/sharp-libvips-linuxmusl-x64@1.2.0
These packages each contain the following license:
@@ -1814,7 +1824,7 @@ SOFTWARE.
The following npm package may be included in this product:
- pyodide@0.28.0
- pyodide@0.28.1
This package contains the following license:
@@ -1908,7 +1918,7 @@ THE SOFTWARE.
The following npm package may be included in this product:
- three@0.178.0
- three@0.179.1
This package contains the following license:
@@ -2028,7 +2038,7 @@ SOFTWARE.
The following npm package may be included in this product:
- vuetify@3.9.2
- vuetify@3.9.4
This package contains the following license:
@@ -2128,7 +2138,7 @@ SOFTWARE.
The following npm packages may be included in this product:
- ndarray-lanczos@0.3.0
- ndarray-pixels@4.1.0
- ndarray-pixels@5.0.1
- property-graph@3.0.0
These packages each contain the following license:
@@ -2159,9 +2169,9 @@ SOFTWARE.
The following npm packages may be included in this product:
- @gltf-transform/core@4.2.0
- @gltf-transform/extensions@4.2.0
- @gltf-transform/functions@4.2.0
- @gltf-transform/core@4.2.1
- @gltf-transform/extensions@4.2.1
- @gltf-transform/functions@4.2.1
These packages each contain the following license:

View File

@@ -51,6 +51,7 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) {
for (let modelIndex in event.models) {
let isLast = parseInt(modelIndex) === event.models.length - 1;
let model = event.models[modelIndex];
if (!model) continue;
tools.value?.removeObjectSelections(model.name);
try {
let loadHelpers = (await settings).loadHelpers;
@@ -108,6 +109,28 @@ async function loadModelManual() {
const modelUrl = prompt("For an improved experience in viewing CAD/GLTF models with automatic updates, it's recommended to use the official yacv_server Python package. This ensures seamless serving of models and automatic updates.\n\nOtherwise, enter the URL of the model to load:");
if (modelUrl) await networkMgr.load(modelUrl);
}
function loadDemoModels() {
for (let name of ['fox.glb', 'img.glb', 'location.glb', 'logo.glb', 'logo_hl.glb', 'logo_hl_tex.glb']) {
networkMgr.load(`https://yeicor-3d.github.io/yet-another-cad-viewer/${name}`)
}
}
// Detect dropped .glb files and load them manually
document.body.addEventListener("dragover", e => {
e.preventDefault(); // Allow drop
});
document.body.addEventListener("drop", async e => {
e.preventDefault();
const file = e.dataTransfer?.files?.[0];
if (!file) return;
const ext = file.name.split('.').pop()?.toLowerCase();
if (ext === 'glb' || ext === 'gltf') {
await networkMgr.load(file);
}
});
</script>
<template>
@@ -122,9 +145,8 @@ async function loadModelManual() {
<v-btn @click="() => tools?.openPlayground()" class="mx-auto d-block my-4">
<svg-icon :path="mdiScriptTextPlay" type="mdi"/>&nbsp; Open playground...
</v-btn>
<v-btn @click="networkMgr.load('https://yeicor-3d.github.io/yet-another-cad-viewer/logo.glb')"
class="mx-auto d-block my-4">
<svg-icon :path="mdiCube" type="mdi"/>&nbsp; Load demo model...
<v-btn @click="loadDemoModels" class="mx-auto d-block my-4">
<svg-icon :path="mdiCube" type="mdi"/>&nbsp; Load demo models...
</v-btn>
<v-btn @click="loadModelManual" class="mx-auto d-block my-4">
<svg-icon :path="mdiPlus" type="mdi"/>&nbsp; Load model manually...
@@ -132,7 +154,9 @@ async function loadModelManual() {
<span v-if="preloadingModels.length > 0" class="d-block text-center my-16">
<span class="d-block text-center text-h6">Still trying to load the following:</span>
<span class="d-block text-center" v-for="(model, index) in preloadingModels" :key="index">
{{ model }}<span v-if="index < preloadingModels.length - 1">, </span>
<template v-if="model !== undefined">
{{ model }}<span v-if="index < preloadingModels.length - 1">, </span>
</template>
</span>
</span>

View File

@@ -1,75 +1,88 @@
import {BufferAttribute, InterleavedBufferAttribute, Vector3} from 'three';
import type {MObject3D} from "../tools/Selection.vue";
import type {ModelScene} from '@google/model-viewer/lib/three-components/ModelScene';
import type {SelectionInfo} from "../tools/selection";
import { BufferAttribute, InterleavedBufferAttribute, Vector3 } from "three";
import type { MObject3D } from "../tools/Selection.vue";
import type { ModelScene } from "@google/model-viewer/lib/three-components/ModelScene";
import type { SelectionInfo } from "../tools/selection";
function getCenterAndVertexList(selInfo: SelectionInfo, scene: ModelScene): {
center: Vector3,
vertices: Array<Vector3>
function getCenterAndVertexList(
selInfo: SelectionInfo,
scene: ModelScene,
): {
center: Vector3;
vertices: Array<Vector3>;
} {
let pos: BufferAttribute | InterleavedBufferAttribute = selInfo.object.geometry.getAttribute('position');
let ind: BufferAttribute | null = selInfo.object.geometry.index;
if (ind === null) {
ind = new BufferAttribute(new Uint16Array(pos.count), 1);
for (let i = 0; i < pos.count; i++) {
ind.array[i] = i;
}
if (!selInfo.object?.geometry) {
throw new Error("selInfo.object or geometry is undefined");
}
let pos = selInfo.object.geometry.getAttribute("position");
let ind = selInfo.object.geometry.index;
if (ind === null) {
ind = new BufferAttribute(new Uint16Array(pos.count), 1);
for (let i = 0; i < pos.count; i++) {
ind.array[i] = i;
}
let center = new Vector3();
let vertices = [];
for (let i = selInfo.indices[0]; i < selInfo.indices[1]; i++) {
let index = ind.getX(i)
let vertex = new Vector3(pos.getX(index), pos.getY(index), pos.getZ(index));
vertex = scene.target.worldToLocal(selInfo.object.localToWorld(vertex));
center.add(vertex);
vertices.push(vertex);
}
center = center.divideScalar(selInfo.indices[1] - selInfo.indices[0]);
return {center, vertices};
}
let center = new Vector3();
let vertices = [];
for (let i = selInfo.indices[0]; i < selInfo.indices[1]; i++) {
let index = ind.getX(i);
let vertex = new Vector3(pos.getX(index), pos.getY(index), pos.getZ(index));
vertex = scene.target.worldToLocal(selInfo.object.localToWorld(vertex));
center.add(vertex);
vertices.push(vertex);
}
center = center.divideScalar(selInfo.indices[1] - selInfo.indices[0]);
return { center, vertices };
}
/**
* Given two THREE.Object3D objects, returns their closest and farthest vertices, and the geometric centers.
* All of them are approximated and should not be used for precise calculations.
*/
export function distances(a: SelectionInfo, b: SelectionInfo, scene: ModelScene): {
min: Array<Vector3>,
center: Array<Vector3>,
max: Array<Vector3>
export function distances(
a: SelectionInfo,
b: SelectionInfo,
scene: ModelScene,
): {
min: Array<Vector3>;
center: Array<Vector3>;
max: Array<Vector3>;
} {
// Simplify this problem (approximate) by using the distance between each of their vertices.
// Find the center of each object.
let {center: aCenter, vertices: aVertices} = getCenterAndVertexList(a, scene);
let {center: bCenter, vertices: bVertices} = getCenterAndVertexList(b, scene);
// Simplify this problem (approximate) by using the distance between each of their vertices.
// Find the center of each object.
let { center: aCenter, vertices: aVertices } = getCenterAndVertexList(a, scene);
let { center: bCenter, vertices: bVertices } = getCenterAndVertexList(b, scene);
// Find the closest and farthest vertices.
// TODO: Compute actual min and max distances between the two objects.
// FIXME: Really slow... (use a BVH or something)
let minDistance = Infinity;
let minDistanceVertices = [new Vector3(), new Vector3()];
let maxDistance = -Infinity;
let maxDistanceVertices = [new Vector3(), new Vector3()];
for (let i = 0; i < aVertices.length; i++) {
for (let j = 0; j < bVertices.length; j++) {
let distance = aVertices[i].distanceTo(bVertices[j]);
if (distance < minDistance) {
minDistance = distance;
minDistanceVertices[0] = aVertices[i];
minDistanceVertices[1] = bVertices[j];
}
if (distance > maxDistance) {
maxDistance = distance;
maxDistanceVertices[0] = aVertices[i];
maxDistanceVertices[1] = bVertices[j];
}
// Find the closest and farthest vertices.
// TODO: Compute actual min and max distances between the two objects.
// FIXME: Really slow... (use a BVH or something)
let minDistance = Infinity;
let minDistanceVertices = [new Vector3(), new Vector3()];
let maxDistance = -Infinity;
let maxDistanceVertices = [new Vector3(), new Vector3()];
for (let i = 0; i < aVertices.length; i++) {
for (let j = 0; j < bVertices.length; j++) {
const aVertex = aVertices[i];
const bVertex = bVertices[j];
if (aVertex && bVertex) {
let distance = aVertex.distanceTo(bVertex);
if (distance < minDistance) {
minDistance = distance;
minDistanceVertices[0] = aVertex;
minDistanceVertices[1] = bVertex;
}
if (distance > maxDistance) {
maxDistance = distance;
maxDistanceVertices[0] = aVertex;
maxDistanceVertices[1] = bVertex;
}
}
}
}
// Return the results.
return {
min: minDistanceVertices,
center: [aCenter, bCenter],
max: maxDistanceVertices
};
// Return the results.
return {
min: minDistanceVertices,
center: [aCenter, bCenter],
max: maxDistanceVertices,
};
}

View File

@@ -1,12 +1,13 @@
import {Buffer, Document, Scene, type Transform, WebIO} from "@gltf-transform/core";
import {mergeDocuments, unpartition} from "@gltf-transform/functions";
import { Buffer, Document, Scene, type Transform, WebIO } from "@gltf-transform/core";
import { mergeDocuments, unpartition } from "@gltf-transform/functions";
import { retrieveFile } from "../tools/upload-file.ts";
let io = new WebIO();
export let extrasNameKey = "__yacv_name";
export let extrasNameValueHelpers = "__helpers";
// @ts-expect-error
let isSmallBuild = typeof __YACV_SMALL_BUILD__ !== 'undefined' && __YACV_SMALL_BUILD__;
let isSmallBuild = typeof __YACV_SMALL_BUILD__ !== "undefined" && __YACV_SMALL_BUILD__;
/**
* Loads a GLB model from a URL and adds it to the document or replaces it if the names match.
@@ -15,106 +16,148 @@ let isSmallBuild = typeof __YACV_SMALL_BUILD__ !== 'undefined' && __YACV_SMALL_B
*
* Remember to call mergeFinalize after all models have been merged (slower required operations).
*/
export async function mergePartial(url: string, name: string, document: Document, networkFinished: () => void = () => {
}): Promise<Document> {
// Fetch the complete document from the network
// This could be done at the same time as the document is being processed, but I wanted better metrics
let response = await fetch(url);
let buffer = await response.arrayBuffer();
networkFinished();
export async function mergePartial(
url: string | Blob,
name: string,
document: Document,
networkFinished: () => void = () => {},
): Promise<Document> {
// Fetch the complete document from the network
// This could be done at the same time as the document is being processed, but I wanted better metrics
let response = await fetchOrRead(url);
let buffer = await response.arrayBuffer();
networkFinished();
// Load the new document
let newDoc = null;
let alreadyTried: { [name: string]: boolean } = {}
while (newDoc == null) { // Retry adding extensions as required until the document is loaded
try { // Try to load fast if no extensions are used
newDoc = await io.readBinary(new Uint8Array(buffer));
} catch (e) { // Fallback to wait for download and register big extensions
if (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("khr_draco_mesh_compression")) {
if (alreadyTried["draco"]) throw e; else alreadyTried["draco"] = true;
// WARNING: Draco decompression on web is really slow for non-trivial models! (it should work?)
let {KHRDracoMeshCompression} = await import("@gltf-transform/extensions")
// @ts-expect-error
let dracoDecoderWeb = await import("three/examples/jsm/libs/draco/draco_decoder.js");
// @ts-expect-error
let dracoEncoderWeb = await import("three/examples/jsm/libs/draco/draco_encoder.js");
io.registerExtensions([KHRDracoMeshCompression])
.registerDependencies({
'draco3d.decoder': await dracoDecoderWeb.default({}),
'draco3d.encoder': await dracoEncoderWeb.default({})
});
} else if (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("ext_texture_webp")) {
if (alreadyTried["webp"]) throw e; else alreadyTried["webp"] = true;
let {EXTTextureWebP} = await import("@gltf-transform/extensions")
io.registerExtensions([EXTTextureWebP]);
} else { // TODO: Add more extensions as required
throw e;
}
}
// Load the new document
let newDoc = null;
let alreadyTried: { [name: string]: boolean } = {};
while (newDoc == null) {
// Retry adding extensions as required until the document is loaded
try {
// Try to load fast if no extensions are used
newDoc = await io.readBinary(new Uint8Array(buffer));
} catch (e) {
// Fallback to wait for download and register big extensions
if (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("khr_draco_mesh_compression")) {
if (alreadyTried["draco"]) throw e;
else alreadyTried["draco"] = true;
// WARNING: Draco decompression on web is really slow for non-trivial models! (it should work?)
let { KHRDracoMeshCompression } = await import("@gltf-transform/extensions");
// @ts-expect-error
let dracoDecoderWeb = await import("three/examples/jsm/libs/draco/draco_decoder.js");
// @ts-expect-error
let dracoEncoderWeb = await import("three/examples/jsm/libs/draco/draco_encoder.js");
io.registerExtensions([KHRDracoMeshCompression]).registerDependencies({
"draco3d.decoder": await dracoDecoderWeb.default({}),
"draco3d.encoder": await dracoEncoderWeb.default({}),
});
} else if (!isSmallBuild && e instanceof Error && e.message.toLowerCase().includes("ext_texture_webp")) {
if (alreadyTried["webp"]) throw e;
else alreadyTried["webp"] = true;
let { EXTTextureWebP } = await import("@gltf-transform/extensions");
io.registerExtensions([EXTTextureWebP]);
} else {
// TODO: Add more extensions as required
throw e;
}
}
}
// Remove any previous model with the same name
await document.transform(dropByName(name));
// Remove any previous model with the same name
await document.transform(dropByName(name));
// Ensure consistent names
// noinspection TypeScriptValidateJSTypes
await newDoc.transform(setNames(name));
// Ensure consistent names
// noinspection TypeScriptValidateJSTypes
await newDoc.transform(setNames(name));
// Merge the new document into the current one
mergeDocuments(document, newDoc);
return document;
// Merge the new document into the current one
mergeDocuments(document, newDoc);
return document;
}
export async function mergeFinalize(document: Document): Promise<Document> {
// Single scene & buffer required before loading & rendering
return await document.transform(mergeScenes(), unpartition());
// Single scene & buffer required before loading & rendering
return await document.transform(mergeScenes(), unpartition());
}
export async function toBuffer(doc: Document): Promise<Uint8Array> {
return io.writeBinary(doc);
return io.writeBinary(doc);
}
export async function removeModel(name: string, document: Document): Promise<Document> {
return await document.transform(dropByName(name));
return await document.transform(dropByName(name));
}
/** Given a parsed GLTF document and a name, it forces the names of all elements to be identified by the name (or derivatives) */
function setNames(name: string): Transform {
return (doc: Document) => {
// Do this automatically for all elements changing any name
for (let elem of doc.getGraph().listEdges().map(e => e.getChild())) {
if (!elem.getExtras()) elem.setExtras({});
elem.getExtras()[extrasNameKey] = name;
}
return doc;
return (doc: Document) => {
// Do this automatically for all elements changing any name
for (let elem of doc
.getGraph()
.listEdges()
.map((e) => e.getChild())) {
if (!elem.getExtras()) elem.setExtras({});
elem.getExtras()[extrasNameKey] = name;
}
return doc;
};
}
/** Ensures that all elements with the given name are removed from the document */
function dropByName(name: string): Transform {
return (doc: Document) => {
for (let elem of doc.getGraph().listEdges().map(e => e.getChild())) {
if (elem.getExtras() == null || elem instanceof Scene || elem instanceof Buffer) continue;
if ((elem.getExtras()[extrasNameKey]?.toString() ?? "") == name) {
elem.dispose();
}
}
return doc;
};
return (doc: Document) => {
for (let elem of doc
.getGraph()
.listEdges()
.map((e) => e.getChild())) {
if (elem.getExtras() == null || elem instanceof Scene || elem instanceof Buffer) continue;
if ((elem.getExtras()[extrasNameKey]?.toString() ?? "") == name) {
elem.dispose();
}
}
return doc;
};
}
/** Merges all scenes in the document into a single default scene */
function mergeScenes(): Transform {
return (doc: Document) => {
let root = doc.getRoot();
let scene = root.getDefaultScene() ?? root.listScenes()[0];
for (let dropScene of root.listScenes()) {
if (dropScene === scene) continue;
for (let node of dropScene.listChildren()) {
scene.addChild(node);
}
dropScene.dispose();
}
return (doc: Document) => {
let root = doc.getRoot();
let scene = root.getDefaultScene() ?? root.listScenes()[0];
if (!scene) {
throw new Error("No scene found in GLTF document");
}
for (let dropScene of root.listScenes()) {
if (dropScene === scene) continue;
for (let node of dropScene.listChildren()) {
scene.addChild(node);
}
dropScene.dispose();
}
};
}
/** Fetches a URL or reads it if it is a Blob URL */
async function fetchOrRead(url: string | Blob) {
if (url instanceof Blob) {
// Use the FileReader API as fetch does not support Blob URLs
return new Promise<Response>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = (event: ProgressEvent<FileReader>) => {
if (event.target && event.target.result) {
resolve(new Response(event.target.result));
} else {
reject(new Error("Failed to read Blob URL: " + url));
}
};
reader.onerror = (error) => {
reject(new Error("Error reading Blob URL: " + url + " - " + error));
};
// Read the Blob URL as an ArrayBuffer
reader.readAsArrayBuffer(new Blob([url]));
});
} else {
// Fetch the URL
return retrieveFile(url);
}
}

View File

@@ -1,79 +1,112 @@
// noinspection JSVoidFunctionReturnValueUsed,JSUnresolvedReference
import {Document, type TypedArray} from '@gltf-transform/core'
import {Vector2} from "three/src/math/Vector2.js"
import {Vector3} from "three/src/math/Vector3.js"
import {Matrix4} from "three/src/math/Matrix4.js"
import { Document, type TypedArray } from "@gltf-transform/core";
import { Vector2 } from "three/src/math/Vector2.js";
import { Vector3 } from "three/src/math/Vector3.js";
import { Matrix4 } from "three/src/math/Matrix4.js";
/** Exports the colors used for the axes, primary and secondary. They match the orientation gizmo. */
export const AxesColors = {
x: [[247, 60, 60], [148, 36, 36]],
z: [[108, 203, 38], [65, 122, 23]],
y: [[23, 140, 240], [14, 84, 144]]
}
x: [
[247, 60, 60],
[148, 36, 36],
],
z: [
[108, 203, 38],
[65, 122, 23],
],
y: [
[23, 140, 240],
[14, 84, 144],
],
};
function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: number[], rawColors: number[] | null, transform: Matrix4, name: string = '__helper', mode: number = WebGL2RenderingContext.LINES) {
const buffer = doc.getRoot().listBuffers()[0] ?? doc.createBuffer(name + 'Buffer')
const scene = doc.getRoot().getDefaultScene() ?? doc.getRoot().listScenes()[0] ?? doc.createScene(name + 'Scene')
const positions = doc.createAccessor(name + 'Position')
.setArray(new Float32Array(rawPositions) as TypedArray)
.setType('VEC3')
.setBuffer(buffer)
const indices = doc.createAccessor(name + 'Indices')
.setArray(new Uint32Array(rawIndices) as TypedArray)
.setType('SCALAR')
.setBuffer(buffer)
let colors = null;
if (rawColors) {
colors = doc.createAccessor(name + 'Color')
.setArray(new Float32Array(rawColors) as TypedArray)
.setType('VEC4')
.setBuffer(buffer);
}
const material = doc.createMaterial(name + 'Material')
.setAlphaMode('OPAQUE')
const geometry = doc.createPrimitive()
.setIndices(indices)
.setAttribute('POSITION', positions)
.setMode(mode as any)
.setMaterial(material)
if (rawColors) {
geometry.setAttribute('COLOR_0', colors)
}
if (mode == WebGL2RenderingContext.TRIANGLES) {
geometry.setExtras({face_triangles_end: [rawIndices.length / 6, rawIndices.length * 2 / 6, rawIndices.length * 3 / 6, rawIndices.length * 4 / 6, rawIndices.length * 5 / 6, rawIndices.length]})
} else if (mode == WebGL2RenderingContext.LINES) {
geometry.setExtras({edge_points_end: [rawIndices.length / 3, rawIndices.length * 2 / 3, rawIndices.length]})
}
const mesh = doc.createMesh(name + 'Mesh').addPrimitive(geometry)
const node = doc.createNode(name + 'Node').setMesh(mesh).setMatrix(transform.elements as any)
scene.addChild(node)
function buildSimpleGltf(
doc: Document,
rawPositions: number[],
rawIndices: number[],
rawColors: number[] | null,
transform: Matrix4,
name: string = "__helper",
mode: number = WebGL2RenderingContext.LINES,
) {
const buffer = doc.getRoot().listBuffers()[0] ?? doc.createBuffer(name + "Buffer");
const scene = doc.getRoot().getDefaultScene() ?? doc.getRoot().listScenes()[0] ?? doc.createScene(name + "Scene");
if (!scene) throw new Error("Scene is undefined");
if (!rawPositions) throw new Error("rawPositions is undefined");
const positions = doc
.createAccessor(name + "Position")
.setArray(new Float32Array(rawPositions) as TypedArray)
.setType("VEC3")
.setBuffer(buffer);
const indices = doc
.createAccessor(name + "Indices")
.setArray(new Uint32Array(rawIndices) as TypedArray)
.setType("SCALAR")
.setBuffer(buffer);
let colors = null;
if (rawColors) {
colors = doc
.createAccessor(name + "Color")
.setArray(new Float32Array(rawColors) as TypedArray)
.setType("VEC4")
.setBuffer(buffer);
}
const material = doc.createMaterial(name + "Material").setAlphaMode("OPAQUE");
const geometry = doc
.createPrimitive()
.setIndices(indices)
.setAttribute("POSITION", positions)
.setMode(mode as any)
.setMaterial(material);
if (rawColors) {
geometry.setAttribute("COLOR_0", colors);
}
if (mode == WebGL2RenderingContext.TRIANGLES) {
geometry.setExtras({
face_triangles_end: [
rawIndices.length / 6,
(rawIndices.length * 2) / 6,
(rawIndices.length * 3) / 6,
(rawIndices.length * 4) / 6,
(rawIndices.length * 5) / 6,
rawIndices.length,
],
});
} else if (mode == WebGL2RenderingContext.LINES) {
geometry.setExtras({ edge_points_end: [rawIndices.length / 3, (rawIndices.length * 2) / 3, rawIndices.length] });
}
const mesh = doc.createMesh(name + "Mesh").addPrimitive(geometry);
const node = doc
.createNode(name + "Node")
.setMesh(mesh)
.setMatrix(transform.elements as any);
scene.addChild(node);
}
/**
* Create a new Axes helper as a GLTF model, useful for debugging positions and orientations.
*/
export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
let rawIndices = [0, 1, 2, 3, 4, 5];
let rawPositions = [
0, 0, 0, size.x, 0, 0,
0, 0, 0, 0, size.y, 0,
0, 0, 0, 0, 0, -size.z,
];
let rawColors = [
...(AxesColors.x[0]), 255, ...(AxesColors.x[1]), 255,
...(AxesColors.y[0]), 255, ...(AxesColors.y[1]), 255,
...(AxesColors.z[0]), 255, ...(AxesColors.z[1]), 255
].map(x => x / 255.0);
// Axes at (0, 0, 0)
buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, new Matrix4(), '__helper_axes');
buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], new Matrix4(), '__helper_axes', WebGL2RenderingContext.POINTS);
// Axes at center
if (new Matrix4() != transform) {
buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, transform, '__helper_axes_center');
buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], transform, '__helper_axes_center', WebGL2RenderingContext.POINTS);
}
let rawIndices = [0, 1, 2, 3, 4, 5];
let rawPositions = [0, 0, 0, size.x, 0, 0, 0, 0, 0, 0, size.y, 0, 0, 0, 0, 0, 0, -size.z];
let rawColors = [
...(AxesColors.x[0] ?? [255, 0, 0]),
255,
...(AxesColors.x[1] ?? [255, 0, 0]),
255,
...(AxesColors.y[0] ?? [0, 255, 0]),
255,
...(AxesColors.y[1] ?? [0, 255, 0]),
255,
...(AxesColors.z[0] ?? [0, 0, 255]),
255,
...(AxesColors.z[1] ?? [0, 0, 255]),
255,
].map((x) => x / 255.0);
// Axes at (0, 0, 0)
buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, new Matrix4(), "__helper_axes");
buildSimpleGltf(doc, [0, 0, 0], [0], [1, 1, 1, 1], new Matrix4(), "__helper_axes", WebGL2RenderingContext.POINTS);
}
/**
@@ -83,61 +116,61 @@ export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
* This ensures that only the back of the grid is always visible, regardless of the camera position.
*/
export function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4, divisions = 10) {
// Create transformed positions for the inner faces of the box
let allPositions: number[] = [];
let allIndices: number[] = [];
for (let axis of [new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, -1)]) {
for (let positive of [1, -1]) {
let offset = axis.clone().multiply(size.clone().multiplyScalar(0.5 * positive));
let translation = new Matrix4().makeTranslation(offset.x, offset.y, offset.z)
let rotation = new Matrix4().lookAt(new Vector3(), offset, new Vector3(0, 1, 0))
let size2 = new Vector2();
if (axis.x) size2.set(size.z, size.y);
if (axis.y) size2.set(size.x, size.z);
if (axis.z) size2.set(size.x, size.y);
let transform = new Matrix4().multiply(translation).multiply(rotation);
let [rawPositions, rawIndices] = newGridPlane(size2, divisions);
let baseIndex = allPositions.length / 3;
for (let i of rawIndices) {
allIndices.push(i + baseIndex);
}
// Apply transform to the positions before adding them to the list
for (let i = 0; i < rawPositions.length; i += 3) {
let pos = new Vector3(rawPositions[i], rawPositions[i + 1], rawPositions[i + 2]);
pos.applyMatrix4(transform);
allPositions.push(pos.x, pos.y, pos.z);
}
}
// Create transformed positions for the inner faces of the box
let allPositions: number[] = [];
let allIndices: number[] = [];
for (let axis of [new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, -1)]) {
for (let positive of [1, -1]) {
let offset = axis.clone().multiply(size.clone().multiplyScalar(0.5 * positive));
let translation = new Matrix4().makeTranslation(offset.x, offset.y, offset.z);
let rotation = new Matrix4().lookAt(new Vector3(), offset, new Vector3(0, 1, 0));
let size2 = new Vector2();
if (axis.x) size2.set(size.z, size.y);
if (axis.y) size2.set(size.x, size.z);
if (axis.z) size2.set(size.x, size.y);
let transform = new Matrix4().multiply(translation).multiply(rotation);
let [rawPositions, rawIndices] = newGridPlane(size2, divisions);
let baseIndex = allPositions.length / 3;
for (let i of rawIndices) {
allIndices.push(i + baseIndex);
}
// Apply transform to the positions before adding them to the list
for (let i = 0; i < rawPositions.length; i += 3) {
let pos = new Vector3(rawPositions[i], rawPositions[i + 1], rawPositions[i + 2]);
pos.applyMatrix4(transform);
allPositions.push(pos.x, pos.y, pos.z);
}
}
let colors = new Array(allPositions.length / 3 * 4).fill(1);
buildSimpleGltf(doc, allPositions, allIndices, colors, baseTransform, '__helper_grid', WebGL2RenderingContext.TRIANGLES);
}
let colors = new Array((allPositions.length / 3) * 4).fill(1);
buildSimpleGltf(doc, allPositions, allIndices, colors, baseTransform, "__helper_grid", WebGL2RenderingContext.TRIANGLES);
}
export function newGridPlane(size: Vector2, divisions = 10, divisionWidth = 0.002): [number[], number[]] {
const rawPositions = [];
const rawIndices = [];
// Build the grid as triangles
for (let i = 0; i <= divisions; i++) {
const x = -size.x / 2 + size.x * i / divisions;
const y = -size.y / 2 + size.y * i / divisions;
const rawPositions = [];
const rawIndices = [];
// Build the grid as triangles
for (let i = 0; i <= divisions; i++) {
const x = -size.x / 2 + (size.x * i) / divisions;
const y = -size.y / 2 + (size.y * i) / divisions;
// Vertical quad (two triangles)
rawPositions.push(x - divisionWidth * size.x / 2, -size.y / 2, 0);
rawPositions.push(x + divisionWidth * size.x / 2, -size.y / 2, 0);
rawPositions.push(x + divisionWidth * size.x / 2, size.y / 2, 0);
rawPositions.push(x - divisionWidth * size.x / 2, size.y / 2, 0);
const baseIndex = i * 4;
rawIndices.push(baseIndex, baseIndex + 1, baseIndex + 2);
rawIndices.push(baseIndex, baseIndex + 2, baseIndex + 3);
// Vertical quad (two triangles)
rawPositions.push(x - (divisionWidth * size.x) / 2, -size.y / 2, 0);
rawPositions.push(x + (divisionWidth * size.x) / 2, -size.y / 2, 0);
rawPositions.push(x + (divisionWidth * size.x) / 2, size.y / 2, 0);
rawPositions.push(x - (divisionWidth * size.x) / 2, size.y / 2, 0);
const baseIndex = i * 4;
rawIndices.push(baseIndex, baseIndex + 1, baseIndex + 2);
rawIndices.push(baseIndex, baseIndex + 2, baseIndex + 3);
// Horizontal quad (two triangles)
rawPositions.push(-size.x / 2, y - divisionWidth * size.y / 2, 0);
rawPositions.push(size.x / 2, y - divisionWidth * size.y / 2, 0);
rawPositions.push(size.x / 2, y + divisionWidth * size.y / 2, 0);
rawPositions.push(-size.x / 2, y + divisionWidth * size.y / 2, 0);
const baseIndex2 = (divisions + 1 + i) * 4;
rawIndices.push(baseIndex2, baseIndex2 + 1, baseIndex2 + 2);
rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3);
}
return [rawPositions, rawIndices];
// Horizontal quad (two triangles)
rawPositions.push(-size.x / 2, y - (divisionWidth * size.y) / 2, 0);
rawPositions.push(size.x / 2, y - (divisionWidth * size.y) / 2, 0);
rawPositions.push(size.x / 2, y + (divisionWidth * size.y) / 2, 0);
rawPositions.push(-size.x / 2, y + (divisionWidth * size.y) / 2, 0);
const baseIndex2 = (divisions + 1 + i) * 4;
rawIndices.push(baseIndex2, baseIndex2 + 1, baseIndex2 + 2);
rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3);
}
return [rawPositions, rawIndices];
}

View File

@@ -4,12 +4,12 @@ const batchTimeout = 250; // ms
export class NetworkUpdateEventModel {
name: string;
url: string;
url: string | Blob;
// TODO: Detect and manage instances of the same object (same hash, different name)
hash: string | null;
isRemove: boolean | null; // This is null for a shutdown event
constructor(name: string, url: string, hash: string | null, isRemove: boolean | null) {
constructor(name: string, url: string | Blob, hash: string | null, isRemove: boolean | null) {
this.name = name;
this.url = url;
this.hash = hash;
@@ -42,18 +42,37 @@ export class NetworkManager extends EventTarget {
*
* Updates will be emitted as "update" events, including the download URL and the model name.
*/
async load(url: string) {
if (url.startsWith("dev+") || url.startsWith("dev ")) {
async load(url: string | Blob) {
if (!(url instanceof Blob) && (url.startsWith("dev+") || url.startsWith("dev "))) {
let baseUrl = new URL(url.slice(4));
baseUrl.searchParams.set("api_updates", "true");
await this.monitorDevServer(baseUrl);
} else {
// Get the last part of the URL as the "name" of the model
let name = url.split("/").pop();
name = name?.split(".")[0] || `unknown-${Math.random()}`;
// Use a head request to get the hash of the file
let response = await fetch(url, {method: "HEAD"});
let hash = response.headers.get("etag");
let name;
let hash = null;
if (url instanceof Blob) {
if (url instanceof File) name = (url as File).name
else name = `blob-${Math.random()}`;
name = name.replace('.glb', '').replace('.gltf', '');
} else {
// If there is a #name parameter in the URL, use it as the name
let hashParams: URLSearchParams
try {
let urlObj = new URL(url, window.location.href);
hashParams = new URLSearchParams(urlObj.hash.slice(1));
} catch (e) {
hashParams = new URLSearchParams();
}
if (hashParams.has("name")) {
name = hashParams.get("name") || `unknown-${Math.random()}`;
} else { // Default to the last part of the URL as the "name" of the model
name = url.split("/").pop();
}
name = name?.split(".")[0] || `unknown-${Math.random()}`;
// Use a head request to get the hash of the file
let response = await fetch(url, {method: "HEAD"});
hash = response.headers.get("etag");
}
// Only trigger an update if the hash has changed
this.foundModel(name, hash, url, false);
}
@@ -92,7 +111,7 @@ export class NetworkManager extends EventTarget {
}
}
private foundModel(name: string, hash: string | null, url: string, isRemove: boolean | null, disconnect: () => void = () => {
private foundModel(name: string, hash: string | null, url: string | Blob, isRemove: boolean | null, disconnect: () => void = () => {
}) {
// console.debug("Found model", name, "with hash", hash, "at", url, "isRemove", isRemove);

View File

@@ -9,7 +9,7 @@ import {Matrix4} from "three/src/math/Matrix4.js"
/** This class helps manage SceneManagerData. All methods are static to support reactivity... */
export class SceneMgr {
/** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */
static async loadModel(sceneUrl: Ref<string>, document: Document, name: string, url: string, updateHelpers: boolean = true, reloadScene: boolean = true): Promise<Document> {
static async loadModel(sceneUrl: Ref<string>, document: Document, name: string, url: string | Blob, updateHelpers: boolean = true, reloadScene: boolean = true): Promise<Document> {
let loadStart = performance.now();
let loadNetworkEnd: number;
@@ -99,8 +99,10 @@ export class SceneMgr {
let transform = (new Matrix4()).makeTranslation(bb.getCenter(new Vector3()));
newAxes(helpersDoc, bb.getSize(new Vector3()).multiplyScalar(0.5), transform);
newGridBox(helpersDoc, bb.getSize(new Vector3()), transform);
let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)]));
return await SceneMgr.loadModel(sceneUrl, document, extrasNameValueHelpers, helpersUrl, false, reloadScene);
let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc) as ArrayBufferView<ArrayBuffer>]));
let newDocument = await SceneMgr.loadModel(sceneUrl, document, extrasNameValueHelpers, helpersUrl, false, reloadScene);
URL.revokeObjectURL(helpersUrl);
return newDocument;
}
/** Serializes the current document into a GLB and updates the viewerSrc */
@@ -110,8 +112,9 @@ export class SceneMgr {
// Serialize the document into a GLB and update the viewerSrc
let buffer = await toBuffer(document);
let blob = new Blob([buffer], {type: 'model/gltf-binary'});
let blob = new Blob([buffer as ArrayBufferView<ArrayBuffer>], {type: 'model/gltf-binary'});
console.debug("Showing current doc", document, "with", buffer.length, "total bytes");
if (sceneUrl.value.startsWith("blob:")) URL.revokeObjectURL(sceneUrl.value);
sceneUrl.value = URL.createObjectURL(blob);
return document;

View File

@@ -1,7 +1,9 @@
// These are the default values for the settings, which are overridden below
import {ungzip} from "pako";
import {b64UrlDecode} from "../tools/b64.ts";
import {retrieveFile} from "../tools/upload-file.ts";
const firstTimeNames: Array<string> = []; // Needed for array values, which clear the array when overridden
export const settings = (async () => {
let settings = {
preload: [
@@ -12,7 +14,7 @@ export const settings = (async () => {
// @ts-ignore
// new URL('../../assets/logo_build/location.glb', import.meta.url).href,
// @ts-ignore
// new URL('../../assets/logo_build/img.jpg.glb', import.meta.url).href,
// new URL('../../assets/logo_build/img.glb', import.meta.url).href,
// Websocket URLs automatically listen for new models from the python backend
'<auto>', // Get the default preload URL if not overridden
],
@@ -41,7 +43,6 @@ export const settings = (async () => {
// Playground settings
pg_code: "", // Automatically loaded and executed code for the playground
pg_code_url: "", // URL to load the code from (overrides pg_code)
pg_opacity_loading: -1, // Opacity of the code during first load and run (< 0 is 0.0 if preload and 0.9 if not)
pg_opacity_loaded: 0.9, // Opacity of the code after it has been run for the first time
};
@@ -59,24 +60,15 @@ export const settings = (async () => {
});
}
// Grab the code from the URL if it is set
if (settings.pg_code_url.length > 0) {
// If the code URL is set, override the code
try {
const response = await fetch(settings.pg_code_url);
if (response.ok) {
settings.pg_code = await response.text();
} else {
console.warn("Failed to load code from URL:", settings.pg_code_url);
}
} catch (error) {
console.error("Error fetching code from URL:", settings.pg_code_url, error);
}
}
// Get the default preload URL if not overridden (requires a fetch that is avoided if possible)
for (let i = 0; i < settings.preload.length; i++) {
let url = settings.preload[i];
// Ignore empty preload URLs to allow overriding default auto behavior
if (url === '') {
settings.preload = settings.preload.slice(0, i).concat(settings.preload.slice(i + 1));
continue; // Skip this preload URL
}
// Handle special <auto> preload URL
if (url === '<auto>') {
if (settings.pg_code != "") { // <auto> means no preload URL if code is set
settings.preload = settings.preload.slice(0, i).concat(settings.preload.slice(i + 1));
@@ -93,26 +85,30 @@ export const settings = (async () => {
url = "dev+http://localhost:32323";
}
}
settings.preload[i] = url;
settings.preload[i] = url ?? "";
}
// Auto-decompress the code and other playground settings
if (settings.pg_code.length > 0) {
// pg_code has a few possible formats: URL, base64url+gzipped, or raw code (try them in that order)
try {
settings.pg_code = ungzip(b64UrlDecode(settings.pg_code), {to: 'string'});
} catch (error) {
console.log("pg_code is not base64url+gzipped, assuming raw code. Decoding error:", error);
new URL(settings.pg_code); // Check if it's a valid absolute URL
settings.pg_code = await (await retrieveFile(settings.pg_code)).text();
} catch (error1) { // Not a valid URL, try base64url+gzipped
try {
settings.pg_code = ungzip(b64UrlDecode(settings.pg_code), {to: 'string'});
} catch (error2) { // Not base64url+gzipped, assume it's raw code
console.log("pg_code is not a URL (", error1, ") or base64url+gzipped (", error2, "), using it as raw code:", settings.pg_code);
}
}
if (settings.pg_opacity_loading < 0) {
// If the opacity is not set, use 0.0 if preload is set, otherwise 0.9
settings.pg_opacity_loading = settings.preload.length > 0 ? 0.0 : 0.9;
}
}
return settings;
})()
const firstTimeNames: Array<string> = []; // Needed for array values, which clear the array when overridden
function parseSetting(name: string, value: string, settings: any): any {
let arrayElem = name.endsWith(".0")
if (arrayElem) name = name.slice(0, -2);

View File

@@ -13,9 +13,10 @@ import {
} from "vuetify/lib/components/index.mjs";
import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf";
import {Mesh} from "@gltf-transform/core";
import {ref, watch} from "vue";
import {nextTick, ref, watch} from "vue";
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
import {
mdiArrowExpand,
mdiCircleOpacity,
mdiCube,
mdiDelete,
@@ -44,7 +45,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{ remove: [] }>()
let modelName = props.meshes[0].getExtras()[extrasNameKey] // + " blah blah blah blah blag blah blah blah"
let modelName = props.meshes[0]?.getExtras()?.[extrasNameKey] // + " blah blah blah blah blag blah blah blah"
// Count the number of faces, edges and vertices
let faceCount = ref(-1);
@@ -58,7 +59,12 @@ const clipPlaneY = ref(1);
const clipPlaneSwappedY = ref(false);
const clipPlaneZ = ref(1);
const clipPlaneSwappedZ = ref(false);
const edgeWidth = ref(0);
const explodeStrength = ref(0);
const explodeSwapped = ref(false);
// Load the settings for the default edge width
(async () => {
let s = await settings;
edgeWidth.value = s.edgeWidth;
@@ -163,9 +169,9 @@ function onClipPlanesChange() {
new Plane(new Vector3(0, -1, 0), offsetY).applyMatrix4(rotSceneMatrix),
new Plane(new Vector3(0, 0, 1), -offsetZ).applyMatrix4(rotSceneMatrix),
];
if (clipPlaneSwappedX.value) planes[0].negate();
if (clipPlaneSwappedY.value) planes[1].negate();
if (clipPlaneSwappedZ.value) planes[2].negate();
if (clipPlaneSwappedX.value) planes[0]?.negate();
if (clipPlaneSwappedY.value) planes[1]?.negate();
if (clipPlaneSwappedZ.value) planes[2]?.negate();
if (!enabledZ) planes.pop();
if (!enabledY) planes.splice(1, 1);
if (!enabledX) planes.shift();
@@ -246,6 +252,76 @@ function onEdgeWidthChange(newEdgeWidth: number) {
watch(edgeWidth, onEdgeWidthChange);
// Explode the model
function onExplodeChange(newExplodeStrength: number) {
let scene = props.viewer?.scene;
let sceneModel = (scene as any)?._model;
if (!scene || !sceneModel) return;
// Get direction and size of the explosion in a first pass
const meBbox = new Box3();
const othersBbox = new Box3();
sceneModel.traverse((child: MObject3D) => {
if (child == sceneModel) return; // Skip the scene itself
const isMe = child.userData[extrasNameKey] === modelName;
if ((child.type === 'Mesh' || child.type === 'SkinnedMesh' ||
child.type === 'Line' || child.type === 'LineSegments' ||
child.type === 'Points') && !child.userData.noHit) {
if (isMe) {
meBbox.expandByObject(child);
} else if (!isMe && child.userData[extrasNameKey]) {
othersBbox.expandByObject(child);
}
}
});
const modelSize = new Vector3();
meBbox.getSize(modelSize);
const maxDimension = Math.max(modelSize.x, modelSize.y, modelSize.z);
const pushDirection = new Vector3().subVectors(meBbox.getCenter(new Vector3()), othersBbox.getCenter(new Vector3())).normalize();
// Use absolute value for strength calculation
let strength = Math.abs(newExplodeStrength);
if (explodeSwapped.value) strength = -strength;
// Apply explosion
sceneModel.traverse((child: MObject3D) => {
if (child.userData[extrasNameKey] === modelName) {
if ((child.type === 'Mesh' || child.type === 'SkinnedMesh' ||
child.type === 'Line' || child.type === 'LineSegments' ||
child.type === 'Points')) {
// Handle zero vector case (if object is at origin)
const direction = pushDirection.clone();
if (direction.lengthSq() < 0.0001) {
direction.set(0, 1, 0);
console.warn("Explode direction was zero, using (0, 1, 0) instead");
}
// Calculate new position based on model size
const factor = strength * maxDimension;
const newPosition = new Vector3().add(direction.multiplyScalar(factor));
// Apply new position
child.position.copy(newPosition);
// Update related objects (back is automatically updated)
if (child.userData.niceLine) {
child.userData.niceLine.position.copy(newPosition);
}
}
}
});
scene.queueRender();
onClipPlanesChange();
}
// Add watchers for explode variables
watch(explodeStrength, (newVal) => onExplodeChange(newVal));
watch(explodeSwapped, () => onExplodeChange(explodeStrength.value));
function onModelLoad() {
let scene = props.viewer?.scene;
let sceneModel = (scene as any)?._model;
@@ -312,15 +388,17 @@ function onModelLoad() {
// Furthermore...
// Enabled features may have been reset after a reload
onEnabledFeaturesChange(enabledFeatures.value)
onEnabledFeaturesChange(enabledFeatures.value);
// Opacity may have been reset after a reload
onOpacityChange(opacity.value)
onOpacityChange(opacity.value);
// Wireframe may have been reset after a reload
onWireframeChange(wireframe.value)
onWireframeChange(wireframe.value);
// Clip planes may have been reset after a reload
onClipPlanesChange()
onClipPlanesChange();
// Edge width may have been reset after a reload
onEdgeWidthChange(edgeWidth.value)
onEdgeWidthChange(edgeWidth.value);
// Explode may have been reset after a reload
if (explodeStrength.value > 0) nextTick(() => onExplodeChange(explodeStrength.value));
scene.queueRender()
}
@@ -370,6 +448,21 @@ if (props.viewer) onViewerReady(props.viewer); else watch((() => props.viewer) a
<v-checkbox-btn v-model="wireframe" falseIcon="mdi-triangle" trueIcon="mdi-triangle-outline"></v-checkbox-btn>
</template>
</v-slider>
<v-slider v-model="explodeStrength" hide-details max="1" min="0">
<template v-slot:prepend>
<v-tooltip activator="parent">Explode model</v-tooltip>
<svg-icon :path="mdiArrowExpand" type="mdi"></svg-icon>
</template>
<template v-slot:append>
<v-tooltip activator="parent">Swap explode direction (may go crazy)</v-tooltip>
<v-checkbox-btn v-model="explodeSwapped" falseIcon="mdi-checkbox-blank-outline"
trueIcon="mdi-checkbox-marked-outline">
<template v-slot:label>
<svg-icon :path="mdiSwapHorizontal" type="mdi"></svg-icon>
</template>
</v-checkbox-btn>
</template>
</v-slider>
<v-slider v-if="edgeCount > 0 || vertexCount > 0" v-model="edgeWidth" hide-details max="1" min="0">
<template v-slot:prepend>
<v-tooltip activator="parent">Edge and vertex sizes</v-tooltip>

View File

@@ -17,7 +17,7 @@ function meshesList(sceneDocument: Document): Array<Array<Mesh>> {
// Grouped by shared name
return sceneDocument.getRoot().listMeshes().reduce((acc, mesh) => {
let name = mesh.getExtras()[extrasNameKey]?.toString() ?? 'Unnamed';
let group = acc.find((group) => meshName(group[0]) === name);
let group = acc.find((group) => group[0] && meshName(group[0]) === name);
if (group) {
group.push(mesh);
} else {
@@ -43,9 +43,9 @@ defineExpose({findModel})
</script>
<template>
<v-expansion-panels v-for="meshes in meshesList(sceneDocument)" :key="meshName(meshes[0])"
<v-expansion-panels v-for="meshes in meshesList(sceneDocument)" :key="meshes[0] ? meshName(meshes[0]) : 'unnamed'"
v-model="expandedNames as any" multiple>
<model :meshes="meshes" :viewer="props.viewer" @remove="onRemove(meshes[0])"/>
<model :meshes="meshes" :viewer="props.viewer" @remove="meshes[0] ? onRemove(meshes[0]) : undefined"/>
</v-expansion-panels>
</template>

View File

@@ -12,7 +12,8 @@ import {
mdiFolderOpen,
mdiPlay,
mdiReload,
mdiShare
mdiShare,
mdiUpload
} from "@mdi/js";
import {VBtn, VCard, VCardText, VSlider, VSpacer, VToolbar, VToolbarTitle, VTooltip} from "vuetify/components";
// @ts-expect-error
@@ -25,6 +26,7 @@ import {NetworkUpdateEvent, NetworkUpdateEventModel} from "../misc/network.ts";
import {settings} from "../misc/settings.ts";
// @ts-expect-error
import playgroundStartupCode from './PlaygroundStartup.py?raw';
import {uploadFile} from "./upload-file.ts";
const model = defineModel<{ code: string, firstTime: boolean }>({required: true}); // Initial code should only be set on first load!
const emit = defineEmits<{ close: [], updateModel: [NetworkUpdateEvent] }>()
@@ -111,6 +113,7 @@ async function runCode() {
} catch (e) {
output(`Error running code: ${e}\n`);
} finally {
output("Code execution finished.\n");
running.value = false; // Indicate that Pyodide is ready
}
}
@@ -139,9 +142,13 @@ function onModelData(modelData: string) {
const binaryData = Base64.toUint8Array(modelData.slice(i + 1)); // Extract the base64 part
console.assert(binaryData.slice(0, 4).toString() == "103,108,84,70", // Ugly...
"Invalid GLTF binary data received: " + binaryData.slice(0, 4).toString());
// - Save for upload and share link feature
builtModelsGlb[modelMetadata.name] = binaryData;
// - Create a Blob from the binary data to be used as a URL
const blob = new Blob([binaryData], {type: 'model/gltf-binary'});
modelMetadata.url = URL.createObjectURL(blob); // Set the hacked URL in the model metadata
const blob = new Blob([binaryData as ArrayBufferView<ArrayBuffer>], {type: 'model/gltf-binary'});
modelMetadata.url = URL.createObjectURL(blob); // Set the hacked URL in the model metadata XXX: revoked on App.vue
} else {
delete builtModelsGlb[modelMetadata.name]; // Remove from built models if it's a remove request
}
// - Emit the event with the model metadata and URL
let networkUpdateEvent = new NetworkUpdateEvent([modelMetadata], () => {
@@ -158,15 +165,22 @@ function resetWorker(loadSnapshot: Uint8Array | undefined = undefined) {
setupPyodide(false, loadSnapshot); // Reinitialize Pyodide
}
function shareLink() {
function shareLinkCommon(added: Record<string, Array<string> | string>, forgotten: Array<string>) {
const baseUrl = window.location
const searchParams = new URLSearchParams(baseUrl.search);
searchParams.delete('pg_code_url'); // Remove any existing pg_code parameter
searchParams.delete('pg_code'); // Remove any existing pg_code parameter
for (const k of forgotten) searchParams.delete(k);
const hashParams = new URLSearchParams(baseUrl.hash.slice(1)); // Keep all previous URL parameters
hashParams.delete('pg_code_url') // Would overwrite the pg_code parameter
hashParams.set('pg_code', b64UrlEncode(gzip(model.value.code, {level: 9}))); // Compress and encode the code
const shareUrl = `${baseUrl.origin}${baseUrl.pathname}?${searchParams}#${hashParams}`; // Prefer hash to GET
for (const k of forgotten) hashParams.delete(k);
for (const k in added) {
if (Array.isArray(added[k])) {
for (const v of added[k]) {
hashParams.append(k, v); // Prefer hash to GET
}
} else if (typeof added[k] === 'string') {
hashParams.set(k, added[k]); // Prefer hash to GET
}
}
const shareUrl = `${baseUrl.origin}${baseUrl.pathname}?${searchParams}#${hashParams}`;
output(`Share link ready: ${shareUrl}\n`)
if (navigator.clipboard?.writeText === undefined) {
output("Clipboard API not available. Please copy the link manually.\n");
@@ -178,6 +192,37 @@ function shareLink() {
}
}
function shareLink() {
shareLinkCommon({'pg_code': b64UrlEncode(gzip(model.value.code, {level: 9}))}, ['pg_code']);
}
const builtModelsGlb: Record<string, Uint8Array> = {}; // Store built models to support uploading
async function uploadAndShareLink() {
try {
output("Uploading files...\n");
// Upload code.py
const codeBlob = new Blob([model.value.code], {type: 'text/x-python'});
const newParams: Record<string, string | Array<string>> = {
'pg_code': await uploadFile('code.py', new Uint8Array(await codeBlob.arrayBuffer()))
};
// Upload all models
newParams['preload'] = []
for (const name in builtModelsGlb) {
const glb: any = builtModelsGlb[name];
const url = await uploadFile(name + '.glb', glb)
newParams['preload'].push(url); // Add to preload list
}
// Build share URL
return shareLinkCommon(newParams, ['pg_code'])
} catch (e) {
output(`Error uploading/sharing files: ${e}. Falling back to private share link.\n`);
return shareLink(); // Fallback to private share link if upload fails
}
}
function saveSnapshot() {
throw new Error("Not implemented yet!"); // TODO: Implement snapshot saving
}
@@ -230,38 +275,48 @@ onMounted(() => {
location="bottom">Opacity of the editor (0 = hidden, 1 = fully visible)</v-tooltip>
</span>
<span style="margin-right: -8px;"><!-- This span is only needed to force tooltip to work while button is disabled -->
<span style="padding-left: 12px; width: 48px;"><!-- This span is only needed to force tooltip to work while button is disabled -->
<v-btn icon disabled @click="saveSnapshot()">
<svg-icon :path="mdiContentSave" type="mdi"/>
</v-btn>
<v-tooltip activator="parent"
location="bottom">Save current state to a snapshot for fast startup (WIP)</v-tooltip>
</span>
<span style="margin-left: -8px"><!-- This span is only needed to force tooltip to work while button is disabled -->
<span style="padding-right: 12px; width: 48px;"><!-- This span is only needed to force tooltip to work while button is disabled -->
<v-btn icon disabled @click="loadSnapshot()">
<svg-icon :path="mdiFolderOpen" type="mdi"/>
</v-btn>
<v-tooltip activator="parent" location="bottom">Load snapshot for fast startup (WIP)</v-tooltip>
</span>
<v-btn icon @click="resetWorker()">
<v-btn icon @click="shareLink()" style="padding-left: 12px;">
<svg-icon :path="mdiShare" type="mdi"/>
<v-tooltip activator="parent" location="bottom">Share link that automatically runs the code.<br/>Only people
with the link can see the code.
</v-tooltip>
</v-btn>
<v-btn icon @click="uploadAndShareLink()" style="padding-right: 12px">
<svg-icon :path="mdiShare" type="mdi" style="position: absolute; scale: 75%; top: 6px;"/>
<svg-icon :path="mdiUpload" type="mdi" style="position: absolute; scale: 75%; bottom: 6px;"/>
<v-tooltip activator="parent" location="bottom">Uploads all models and code and then shares a link to them.<br/>Useful
to view the models while the playground loads, but uses third-party storage.
</v-tooltip>
</v-btn>
<v-btn icon @click="resetWorker()" style="padding-left: 12px;">
<svg-icon :path="mdiReload" type="mdi"/>
<v-tooltip activator="parent" location="bottom">Reset Pyodide worker (this forgets all previous state and will
take a little while)
</v-tooltip>
</v-btn>
<v-btn icon @click="runCode()" :disabled="running">
<v-btn icon @click="runCode()" :disabled="running" style="padding-right: 12px">
<svg-icon :path="mdiPlay" type="mdi"/>
<Loading v-if="running" style="position: absolute; top: -16%; left: -16%"/><!-- Ugly positioning -->
<Loading v-if="running" style="position: absolute; top: -16%; left: -28%"/><!-- Ugly positioning -->
<v-tooltip activator="parent" location="bottom">Run code</v-tooltip>
</v-btn>
<v-btn icon @click="shareLink()">
<svg-icon :path="mdiShare" type="mdi"/>
<v-tooltip activator="parent" location="bottom">Share link that auto-runs the code</v-tooltip>
</v-btn>
<v-btn icon @click="emit('close')">
<svg-icon :path="mdiClose" type="mdi"/>
<v-tooltip activator="parent" location="bottom">Close (Pyodide remains loaded)</v-tooltip>

View File

@@ -12,37 +12,13 @@ await micropip.install("yacv_server", pre=True)
# Preimport the yacv_server package to ensure it is available in the global scope, and mock the ocp_vscode package.
from yacv_server import *
micropip.add_mock_package("ocp-vscode", "2.8.9", modules={"ocp_vscode": 'from yacv_server import *'})
show_object = show
# Preinstall a font to avoid issues with no font being available.
async def install_font_to_ocp(font_url, font_name=None):
# noinspection PyUnresolvedReferences
from pyodide.http import pyfetch
from OCP.Font import Font_FontMgr, Font_SystemFont, Font_FA_Regular
from OCP.TCollection import TCollection_AsciiString
import os
# Preinstall the font-fetcher package and install its hook to automatically download any requested font.
await micropip.install("font-fetcher", pre=True)
# Prepare the font path and name
font_name = font_name if font_name is not None else font_url.split("/")[-1]
font_path = os.path.join("/tmp", font_name)
os.makedirs(os.path.dirname(font_path), exist_ok=True)
from font_fetcher.ocp import install_ocp_font_hook
# Download the font using pyfetch
response = await pyfetch(font_url)
font_data = await response.bytes()
# Save it to the system-like folder
with open(font_path, "wb") as f:
f.write(font_data)
mgr = Font_FontMgr.GetInstance_s()
font_t = Font_SystemFont(TCollection_AsciiString(font_path))
font_t.SetFontPath(Font_FA_Regular, TCollection_AsciiString(font_path))
assert mgr.RegisterFont(font_t, False)
#print(f"✅ Font installed at: {font_path}")
return font_path
# Make sure there is at least one font installed, so that the tests can run
await install_font_to_ocp("https://raw.githubusercontent.com/xbmc/xbmc/d3a7f95f3f017b8e861d5d95cc4b33eef4286ce2/media/Fonts/arial.ttf")
install_ocp_font_hook()

View File

@@ -213,7 +213,7 @@ function toggleSelection() {
function toggleOpenNextSelection() {
openNextSelection.value = [
!openNextSelection.value[0],
openNextSelection.value[0] ? openNextSelection.value[1] : selectionEnabled.value
openNextSelection.value[0] ? (openNextSelection.value[1] ?? false) : selectionEnabled.value
];
if (openNextSelection.value[0]) {
// Reuse selection code to identify the model
@@ -330,14 +330,20 @@ function updateBoundingBox() {
// Only draw one edge per axis, the 2nd closest one to the camera
for (let edgeI in edgesByAxis) {
let axisEdges = edgesByAxis[edgeI];
let edge: Array<number> = axisEdges[0];
if (!axisEdges || axisEdges.length === 0) continue;
let edge: Array<number> = axisEdges[0] ?? [];
for (let i = 0; i < 2; i++) { // Find the 2nd closest one by running twice dropping the first
edge = axisEdges[0];
if (!axisEdges || axisEdges.length === 0) break;
edge = axisEdges[0] ?? [];
let edgeDist = Infinity;
let cameraPos: Vector3 = props.viewer?.scene?.camera?.position ?? new Vector3();
for (let testEdge of axisEdges) {
let from = new Vector3(...corners[testEdge[0]]);
let to = new Vector3(...corners[testEdge[1]]);
if (!testEdge || testEdge.length < 2) continue;
let cornerA = corners[testEdge[0] ?? 0];
let cornerB = corners[testEdge[1] ?? 0];
if (!cornerA || !cornerB) continue;
let from = new Vector3(...cornerA);
let to = new Vector3(...cornerB);
let mid = from.clone().add(to).multiplyScalar(0.5);
let newDist = cameraPos.distanceTo(mid);
if (newDist < edgeDist) {
@@ -347,11 +353,16 @@ function updateBoundingBox() {
}
axisEdges = axisEdges.filter((e) => e !== edge);
}
let from = new Vector3(...corners[edge[0]]);
let to = new Vector3(...corners[edge[1]]);
if (!edge || edge.length < 2) continue;
let cornerA = corners[edge[0] ?? 0];
let cornerB = corners[edge[1] ?? 0];
if (!cornerA || !cornerB) continue;
let from = new Vector3(...cornerA);
let to = new Vector3(...cornerB);
let length = to.clone().sub(from).length();
if (length < 0.05) continue; // Skip very small edges (e.g. a single point)
let color = [AxesColors.x, AxesColors.y, AxesColors.z][edgeI][1]; // Secondary colors
let colorArray = [AxesColors.x, AxesColors.y, AxesColors.z][parseInt(edgeI)];
let color = colorArray ? colorArray[1] : [255, 255, 255]; // Secondary colors
let lineCacheKey = JSON.stringify([from, to]);
let matchingLine = boundingBoxLines[lineCacheKey];
if (matchingLine) {
@@ -359,7 +370,7 @@ function updateBoundingBox() {
} else {
let newLineId = props.viewer?.addLine3D(from, to,
length.toFixed(1) + "mm", {
"stroke": "rgb(" + color.join(',') + ")",
"stroke": "rgb(" + (color ?? [255, 255, 255]).join(',') + ")",
"stroke-width": "2"
});
if (newLineId) {
@@ -410,10 +421,17 @@ function updateDistances() {
}
// Add lines (if not already added)
let {min, center, max} = distances(selected.value[0], selected.value[1], props.viewer?.scene);
ensureLine(max[0], max[1], max[1].distanceTo(max[0]).toFixed(1) + "mm", "orange");
ensureLine(center[0], center[1], center[1].distanceTo(center[0]).toFixed(1) + "mm", "green");
ensureLine(min[0], min[1], min[1].distanceTo(min[0]).toFixed(1) + "mm", "cyan");
if (!selected.value[0] || !selected.value[1] || !props.viewer?.scene) return;
let {min, center, max} = distances(selected.value[0], selected.value[1], props.viewer.scene);
if (max[0] && max[1]) {
ensureLine(max[0], max[1], max[1].distanceTo(max[0]).toFixed(1) + "mm", "orange");
}
if (center[0] && center[1]) {
ensureLine(center[0], center[1], center[1].distanceTo(center[0]).toFixed(1) + "mm", "green");
}
if (min[0] && min[1]) {
ensureLine(min[0], min[1], min[1].distanceTo(min[0]).toFixed(1) + "mm", "cyan");
}
// Remove the lines that are no longer needed
for (let lineLocator of distanceLinesToRemove) {

View File

@@ -128,6 +128,7 @@ async function downloadSceneGlb() {
link.download = file.name;
link.href = URL.createObjectURL(file);
link.click();
URL.revokeObjectURL(link.href);
}
async function openGithub() {

View File

@@ -12,8 +12,8 @@ import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"
//@ts-ignore
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"
self.MonacoEnvironment = {
getWorker(_, label) {
(self as any).MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === "json") {
return new jsonWorker()
}

View File

@@ -1,4 +1,4 @@
import {loadPyodide, type PyodideInterface} from "pyodide";
import {loadPyodide, type PyodideAPI} from "pyodide";
let myLoadPyodide = (initOpts: Parameters<typeof loadPyodide>[0]) => loadPyodide({
...initOpts,
@@ -10,7 +10,7 @@ let myLoadPyodide = (initOpts: Parameters<typeof loadPyodide>[0]) => loadPyodide
},
});
let pyodideReadyPromise: Promise<PyodideInterface> | null = null;
let pyodideReadyPromise: Promise<PyodideAPI> | null = null;
export type MessageEventDataIn = {
type: 'asyncRun';
@@ -41,7 +41,7 @@ self.onmessage = async (event: MessageEvent<MessageEventDataIn>) => {
// Create a directory tree in the Pyodide filesystem.
const pyodide = await pyodideReadyPromise;
try {
pyodide.FS.mkdirTree(event.data.path);
await pyodide.FS.mkdirTree(event.data.path);
self.postMessage({id: event.data.id, result: true});
} catch (error: any) {
self.postMessage({id: event.data.id, error: error.message});
@@ -51,7 +51,7 @@ self.onmessage = async (event: MessageEvent<MessageEventDataIn>) => {
// Write a file to the Pyodide filesystem.
const pyodide = await pyodideReadyPromise;
try {
pyodide.FS.writeFile(event.data.path, event.data.content);
await pyodide.FS.writeFile(event.data.path, event.data.content);
self.postMessage({id: event.data.id, result: true});
} catch (error: any) {
self.postMessage({id: event.data.id, error: error.message});

View File

@@ -1,163 +1,182 @@
// Model management from the graphics side
import type {MObject3D} from "./Selection.vue";
import type {Intersection} from "three";
import {Box3} from "three";
import {extrasNameKey} from "../misc/gltf";
import type { MObject3D } from "./Selection.vue";
import type { Intersection } from "three";
import { Box3 } from "three";
import { extrasNameKey } from "../misc/gltf";
/** Information about a single item in the selection */
export class SelectionInfo {
/** The object which was (partially) selected */
object: MObject3D
/** The type of the selection */
kind: 'face' | 'edge' | 'vertex'
/** Start and end indices of the primitives in the geometry */
indices: [number, number]
/** The object which was (partially) selected */
object: MObject3D;
/** The type of the selection */
kind: "face" | "edge" | "vertex";
/** Start and end indices of the primitives in the geometry */
indices: [number, number];
constructor(object: MObject3D, kind: 'face' | 'edge' | 'vertex', indices: [number, number]) {
this.object = object;
this.kind = kind;
this.indices = indices;
}
constructor(object: MObject3D, kind: "face" | "edge" | "vertex", indices: [number, number]) {
this.object = object;
this.kind = kind;
this.indices = indices;
}
public getObjectName() {
return this.object.userData[extrasNameKey];
}
public getObjectName() {
return this.object.userData[extrasNameKey];
}
public matches(object: MObject3D) {
return this.getObjectName() === object.userData[extrasNameKey] &&
(this.kind === 'face' && (object.type === 'Mesh' || object.type === 'SkinnedMesh') ||
this.kind === 'edge' && (object.type === 'Line' || object.type === 'LineSegments') ||
this.kind === 'vertex' && object.type === 'Points')
}
public matches(object: MObject3D) {
return (
this.getObjectName() === object.userData[extrasNameKey] &&
((this.kind === "face" && (object.type === "Mesh" || object.type === "SkinnedMesh")) ||
(this.kind === "edge" && (object.type === "Line" || object.type === "LineSegments")) ||
(this.kind === "vertex" && object.type === "Points"))
);
}
public getKey() {
return this.object.uuid + this.kind + this.indices[0].toFixed() + this.indices[1].toFixed();
}
public getKey() {
return this.object.uuid + this.kind + this.indices[0].toFixed() + this.indices[1].toFixed();
}
public getBox(): Box3 {
let index = this.object.geometry.index || {getX: (i: number) => i};
let pos = this.object.geometry.getAttribute('position');
let min = [Infinity, Infinity, Infinity];
let max = [-Infinity, -Infinity, -Infinity];
for (let i = this.indices[0]; i < this.indices[1]; i++) {
let vertIndex = index!.getX(i);
let x = pos.getX(vertIndex);
let y = pos.getY(vertIndex);
let z = pos.getZ(vertIndex);
min[0] = Math.min(min[0], x);
min[1] = Math.min(min[1], y);
min[2] = Math.min(min[2], z);
max[0] = Math.max(max[0], x);
max[1] = Math.max(max[1], y);
max[2] = Math.max(max[2], z);
}
return new Box3().setFromArray([...min, ...max]);
public getBox(): Box3 {
let index = this.object.geometry.index || { getX: (i: number) => i };
let pos = this.object.geometry.getAttribute("position");
let min = [Infinity, Infinity, Infinity];
let max = [-Infinity, -Infinity, -Infinity];
for (let i = this.indices[0]; i < this.indices[1]; i++) {
let vertIndex = index!.getX(i);
let x = pos.getX(vertIndex);
let y = pos.getY(vertIndex);
let z = pos.getZ(vertIndex);
min[0] = Math.min(min[0] ?? Infinity, x);
min[1] = Math.min(min[1] ?? Infinity, y);
min[2] = Math.min(min[2] ?? Infinity, z);
max[0] = Math.max(max[0] ?? -Infinity, x);
max[1] = Math.max(max[1] ?? -Infinity, y);
max[2] = Math.max(max[2] ?? -Infinity, z);
}
return new Box3().setFromArray([...min, ...max]);
}
}
export function hitToSelectionInfo(hit: Intersection<MObject3D>): SelectionInfo | null {
let kind = hit.object.type;
if (kind == 'Mesh' || kind == 'SkinnedMesh') {
let indices = hitFaceTriangleIndices(hit);
if (indices === null) return null;
return new SelectionInfo(hit.object, 'face', indices);
} else if (kind == 'Line' || kind == 'LineSegments') {
// Select raw lines, not the wide meshes representing them
// This is because the indices refer to the raw lines, not the wide meshes
// Furthermore, this allows better "fuzzy" raycasting logic
let indices = hitEdgePointIndices(hit);
if (indices === null) return null;
return new SelectionInfo(hit.object, 'edge', indices);
} else if (kind == 'Points') {
if (hit.index === undefined) return null;
return new SelectionInfo(hit.object, 'vertex', [hit.index, hit.index + 1]);
}
return null;
let kind = hit.object.type;
if (kind == "Mesh" || kind == "SkinnedMesh") {
let indices = hitFaceTriangleIndices(hit);
if (indices === null) return null;
return new SelectionInfo(hit.object, "face", indices);
} else if (kind == "Line" || kind == "LineSegments") {
// Select raw lines, not the wide meshes representing them
// This is because the indices refer to the raw lines, not the wide meshes
// Furthermore, this allows better "fuzzy" raycasting logic
let indices = hitEdgePointIndices(hit);
if (indices === null) return null;
return new SelectionInfo(hit.object, "edge", indices);
} else if (kind == "Points") {
if (hit.index === undefined) return null;
return new SelectionInfo(hit.object, "vertex", [hit.index, hit.index + 1]);
}
return null;
}
function hitFaceTriangleIndices(hit: Intersection<MObject3D>): [number, number] | null {
let faceTrianglesEnd = hit?.object?.geometry?.userData?.face_triangles_end;
if (!hit.faceIndex) return null;
if (!faceTrianglesEnd) { // Fallback to selecting the whole imported mesh
//console.log("No face_triangles_end found, selecting the whole mesh");
return [0, (hit.object.geometry.index ?? hit.object.geometry.attributes.position).count];
} else { // Normal CAD model
let rawIndex = hit.faceIndex * 3; // Faces are triangles with 3 indices
for (let i = 0; i < faceTrianglesEnd.length; i++) {
let faceSwapIndex = faceTrianglesEnd[i]
if (rawIndex < faceSwapIndex) {
let start = i === 0 ? 0 : faceTrianglesEnd[i - 1];
return [start, faceTrianglesEnd[i]];
}
}
let faceTrianglesEnd = hit?.object?.geometry?.userData?.face_triangles_end;
if (!hit.faceIndex) return null;
if (!faceTrianglesEnd) {
// Fallback to selecting the whole imported mesh
//console.log("No face_triangles_end found, selecting the whole mesh");
return [0, (hit.object.geometry.index ?? hit.object.geometry.attributes.position)?.count ?? 0];
} else {
// Normal CAD model
let rawIndex = hit.faceIndex * 3; // Faces are triangles with 3 indices
for (let i = 0; i < faceTrianglesEnd.length; i++) {
let faceSwapIndex = faceTrianglesEnd[i];
if (rawIndex < faceSwapIndex) {
let start = i === 0 ? 0 : faceTrianglesEnd[i - 1];
return [start, faceTrianglesEnd[i]];
}
}
return null;
}
return null;
}
function hitEdgePointIndices(hit: Intersection<MObject3D>): [number, number] | null {
let edgePointsEnd = hit?.object?.geometry?.userData?.edge_points_end;
if (!edgePointsEnd || hit.index === undefined) return null;
let rawIndex = hit.index; // Faces are triangles with 3 indices
for (let i = 0; i < edgePointsEnd.length; i++) {
let edgeSwapIndex = edgePointsEnd[i]
if (rawIndex < edgeSwapIndex) {
let start = i === 0 ? 0 : edgePointsEnd[i - 1];
return [start, edgePointsEnd[i]];
}
let edgePointsEnd = hit?.object?.geometry?.userData?.edge_points_end;
if (!edgePointsEnd || hit.index === undefined) return null;
let rawIndex = hit.index; // Faces are triangles with 3 indices
for (let i = 0; i < edgePointsEnd.length; i++) {
let edgeSwapIndex = edgePointsEnd[i];
if (rawIndex < edgeSwapIndex) {
let start = i === 0 ? 0 : edgePointsEnd[i - 1];
return [start, edgePointsEnd[i]];
}
return null;
}
return null;
}
function applyColor(selInfo: SelectionInfo, colorAttribute: any, color: [number, number, number, number]): [number, number, number, number] {
let index = selInfo.object.geometry.index
let prevColor: [number, number, number, number] | null = null;
if (colorAttribute !== undefined) {
for (let i = selInfo.indices[0]; i < selInfo.indices[1]; i++) {
let vertIndex = index!.getX(i);
if (prevColor === null) prevColor = [colorAttribute.getX(vertIndex), colorAttribute.getY(vertIndex), colorAttribute.getZ(vertIndex), colorAttribute.getW(vertIndex)];
colorAttribute.setXYZW(vertIndex, color[0], color[1], color[2], color[3]);
}
colorAttribute.needsUpdate = true;
if (selInfo.object.userData.niceLine !== undefined) {
// Need to update the color of the nice line as well
let indexAttribute = selInfo.object.geometry.index!!;
let allNewColors = [];
for (let i = 0; i < indexAttribute.count; i++) {
if (indexAttribute.getX(i) >= selInfo.indices[0] && indexAttribute.getX(i) < selInfo.indices[1]) {
allNewColors.push(color[0], color[1], color[2]);
} else {
allNewColors.push(colorAttribute.getX(indexAttribute.getX(i)), colorAttribute.getY(indexAttribute.getX(i)), colorAttribute.getZ(indexAttribute.getX(i)));
}
}
selInfo.object.userData.niceLine.geometry.setColors(allNewColors);
for (let attribute of Object.values(selInfo.object.userData.niceLine.geometry.attributes)) {
(attribute as any).needsUpdate = true;
}
}
} else { // Fallback to tinting the whole mesh for imported models
//console.log("No color attribute found, tinting the whole mesh")
let tmpPrevColor = selInfo.object.material.color;
prevColor = [tmpPrevColor.r, tmpPrevColor.g, tmpPrevColor.b, 1];
selInfo.object.material.color.setRGB(color[0], color[1], color[2]);
selInfo.object.material.needsUpdate = true;
function applyColor(
selInfo: SelectionInfo,
colorAttribute: any,
color: [number, number, number, number],
): [number, number, number, number] {
let index = selInfo.object.geometry.index;
let prevColor: [number, number, number, number] | null = null;
if (colorAttribute !== undefined) {
for (let i = selInfo.indices[0]; i < selInfo.indices[1]; i++) {
let vertIndex = index!.getX(i);
if (prevColor === null)
prevColor = [
colorAttribute.getX(vertIndex),
colorAttribute.getY(vertIndex),
colorAttribute.getZ(vertIndex),
colorAttribute.getW(vertIndex),
];
colorAttribute.setXYZW(vertIndex, color[0], color[1], color[2], color[3]);
}
return prevColor!;
colorAttribute.needsUpdate = true;
if (selInfo.object.userData.niceLine !== undefined) {
// Need to update the color of the nice line as well
let indexAttribute = selInfo.object.geometry.index!!;
let allNewColors = [];
for (let i = 0; i < indexAttribute.count; i++) {
if (indexAttribute.getX(i) >= selInfo.indices[0] && indexAttribute.getX(i) < selInfo.indices[1]) {
allNewColors.push(color[0], color[1], color[2]);
} else {
allNewColors.push(
colorAttribute.getX(indexAttribute.getX(i)),
colorAttribute.getY(indexAttribute.getX(i)),
colorAttribute.getZ(indexAttribute.getX(i)),
);
}
}
selInfo.object.userData.niceLine.geometry.setColors(allNewColors);
for (let attribute of Object.values(selInfo.object.userData.niceLine.geometry.attributes)) {
(attribute as any).needsUpdate = true;
}
}
} else {
// Fallback to tinting the whole mesh for imported models
//console.log("No color attribute found, tinting the whole mesh")
let tmpPrevColor = selInfo.object.material.color;
prevColor = [tmpPrevColor.r, tmpPrevColor.g, tmpPrevColor.b, 1];
selInfo.object.material.color.setRGB(color[0], color[1], color[2]);
selInfo.object.material.needsUpdate = true;
}
return prevColor!;
}
export function highlight(selInfo: SelectionInfo): void {
// Update the color of all the triangles in the face
let geometry = selInfo.object.geometry;
let colorAttr = selInfo.object.geometry.getAttribute('color');
geometry.userData.savedColor = geometry.userData.savedColor || {};
geometry.userData.savedColor[selInfo.getKey()] = applyColor(selInfo, colorAttr, [1.0, 0.0, 0.0, 1.0]);
// Update the color of all the triangles in the face
let geometry = selInfo.object.geometry;
let colorAttr = selInfo.object.geometry.getAttribute("color");
geometry.userData.savedColor = geometry.userData.savedColor || {};
geometry.userData.savedColor[selInfo.getKey()] = applyColor(selInfo, colorAttr, [1.0, 0.0, 0.0, 1.0]);
}
export function highlightUndo(selInfo: SelectionInfo): void {
// Update the color of all the triangles in the face
let geometry = selInfo.object.geometry;
let colorAttr = selInfo.object.geometry.getAttribute('color');
let savedColor = geometry.userData.savedColor[selInfo.getKey()];
applyColor(selInfo, colorAttr, savedColor);
delete geometry.userData.savedColor[selInfo.getKey()];
// Update the color of all the triangles in the face
let geometry = selInfo.object.geometry;
let colorAttr = selInfo.object.geometry.getAttribute("color");
let savedColor = geometry.userData.savedColor[selInfo.getKey()];
applyColor(selInfo, colorAttr, savedColor);
delete geometry.userData.savedColor[selInfo.getKey()];
}

View File

@@ -0,0 +1,91 @@
//@ts-expect-error
import encryptCode from "tanmayo7lock?raw";
function encrypt(msg: string, secret: string = "hudfhgd8fghdfgh3uhuifdgh"): string {
let exports: any = {};
eval(encryptCode.replace("exports.encrypt = encrypt;", "exports.LargeDataCrypto = LargeDataCrypto;\nexports.encrypt = encrypt;"));
return exports.LargeDataCrypto.encrypt(msg, secret);
}
async function check(lockerName: string) {
const fileUrl = `https://vouz-backend.onrender.com/api/check_key`;
const response = await fetch(fileUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({name: encrypt(lockerName), key: encrypt(lockerName)}),
});
if (!response.ok) throw new Error(`Failed to get file URL: ${response.status} ${response.statusText} -- ${await response.text()}`);
const status = await response.json();
return {response, status};
}
export async function uploadFile(name: string, data: Uint8Array): Promise<string> {
// "Free" storage, let's see how long it lasts...
// Create a locker
const lockerUrl = `https://vouz-backend.onrender.com/api/locker`
const lockerName = `yacv-pg-${name}-${Date.now()}`; // Unique locker name
let responsePromise = fetch(lockerUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({name: encrypt(lockerName), passkey: encrypt(lockerName)}),
});
// The previous request never answers 🤮
responsePromise.then((response) => console.warn(`Locker creation response: ${response.status} ${response.statusText} -- ${response.headers.get('Content-Type')}`));
// Instead, poll the check endpoint until the locker is created
let i: number;
for (i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, 250)); // Wait a bit before checking
try {
let {status} = await check(lockerName);
if (status && status.data && status.data.length == 0) break // Locker is created
} catch (e) { // Ignore errors, they will be thrown later
}
}
if (i >= 10) throw new Error(`Failed to create locker after 10 attempts: ${lockerName}`);
// Upload file to the locker
const uploadUrl = `https://vouz-backend.onrender.com/api/upload`;
const formData = new FormData();
formData.append('file', new Blob([data as ArrayBufferView<ArrayBuffer>], {type: 'application/octet-stream'}), name);
formData.append("name", encrypt(lockerName));
formData.append("passkey", encrypt(lockerName));
const response = await fetch(uploadUrl, {
method: 'POST',
body: formData,
})
if (!response.ok) throw new Error(`Failed to upload file: ${response.status} ${response.statusText} -- ${await response.text()}`);
// Fake URL for retrieveFile to work
return "https://vouz.tech#name=" + encodeURIComponent(name) + "&locker=" + encodeURIComponent(lockerName);
}
/** Given any URL, it retrieves the file, with custom code for the vouz.tech locker. */
export async function retrieveFile(url: string): Promise<Response> {
let realUrl = url;// Normal fetch if the URL is not a vouz.tech locker URL
if (url.indexOf("https://vouz.tech#") !== -1) { // Check if the URL is a vouz.tech locker URL
// Parse the URL to get the locker name and file name
const urlObj = new URL(url);
const hashParams = new URLSearchParams(urlObj.hash.slice(1)); // Remove the leading '#'
const lockerName = hashParams.get('locker') || (() => {
throw new Error("Locker name not found in URL hash")
})();
const name = hashParams.get('name') || (() => {
throw new Error("File name not found in URL hash")
})();
// Get the URL of the uploaded file
let {status} = await check(lockerName);
if (!status || !status.data || status.data.length == 0 || !status.data[0].url) {
throw new Error(`No file URL found in response: ${JSON.stringify(status)}`);
}
console.debug("File access requested successfully, URL:", status.data[0].url);
realUrl = "https://corsproxy.io/?url=" + status.data[0].url + "#name=" + encodeURIComponent(name) + "&locker=" + encodeURIComponent(lockerName);
}
return await fetch(realUrl);
}

View File

@@ -161,9 +161,9 @@ function addLine3D(p1: Vector3, p2: Vector3, centerText?: string, lineAttrs: { [
function removeLine3D(id: number): boolean {
if (!scene.value || !(id in lines.value)) return false;
scene.value.removeHotspot(new Hotspot({name: 'line' + id + '_start'}));
lines.value[id].startHotspot.parentElement?.remove()
lines.value[id]?.startHotspot.parentElement?.remove()
scene.value.removeHotspot(new Hotspot({name: 'line' + id + '_end'}));
lines.value[id].endHotspot.parentElement?.remove()
lines.value[id]?.endHotspot.parentElement?.remove()
delete lines.value[id];
scene.value.queueRender() // Needed to update the hotspots
return true;
@@ -175,17 +175,17 @@ function onCameraChangeLine(lineId: number) {
if (!(lineId in lines.value) || !(elem.value)) return // Silently ignore (not updated yet)
// Update start and end 2D positions
let {x: xB, y: yB} = elem.value.getBoundingClientRect();
let {x, y} = lines.value[lineId].startHotspot.getBoundingClientRect();
lines.value[lineId].start2D = [x - xB, y - yB];
let {x: x2, y: y2} = lines.value[lineId].endHotspot.getBoundingClientRect();
lines.value[lineId].end2D = [x2 - xB, y2 - yB];
let {x, y} = lines.value[lineId]?.startHotspot.getBoundingClientRect() ?? {x: 0, y: 0};
if (lines.value[lineId]) lines.value[lineId].start2D = [x - xB, y - yB];
let {x: x2, y: y2} = lines.value[lineId]?.endHotspot.getBoundingClientRect() ?? {x: 0, y: 0};
if (lines.value[lineId]) lines.value[lineId].end2D = [x2 - xB, y2 - yB];
// Update the center text size if needed
if (svg.value && lines.value[lineId].centerText && lines.value[lineId].centerTextSize[0] === 0) {
if (svg.value && lines.value[lineId]?.centerText && lines.value[lineId]?.centerTextSize[0] === 0) {
let text = svg.value.getElementsByClassName('line' + lineId + '_text')[0] as SVGTextElement | undefined;
if (text) {
let bbox = text.getBBox();
lines.value[lineId].centerTextSize = [bbox.width, bbox.height];
if (lines.value[lineId]) lines.value[lineId].centerTextSize = [bbox.width, bbox.height];
}
}
}

View File

@@ -1,76 +1,96 @@
import {ModelViewerElement} from '@google/model-viewer';
import {$scene} from "@google/model-viewer/lib/model-viewer-base";
import {settings} from "../misc/settings.ts";
import { ModelViewerElement } from "@google/model-viewer";
import { $scene } from "@google/model-viewer/lib/model-viewer-base";
import { settings } from "../misc/settings.ts";
export let currentSceneRotation = 0; // radians, 0 is the default rotation
export async function setupLighting(modelViewer: ModelViewerElement) {
modelViewer[$scene].environmentIntensity = (await settings).environmentIntensity;
// Code is mostly copied from the example at: https://modelviewer.dev/examples/stagingandcameras/#turnSkybox
let lastX: number;
let panning = false;
let radiansPerPixel: number;
modelViewer[$scene].environmentIntensity = (await settings).environmentIntensity;
// Code is mostly copied from the example at: https://modelviewer.dev/examples/stagingandcameras/#turnSkybox
let lastX: number;
let panning = false;
let radiansPerPixel: number;
const startPan = () => {
const orbit = modelViewer.getCameraOrbit();
const {radius} = orbit;
radiansPerPixel = -1 * radius / modelViewer.getBoundingClientRect().height;
modelViewer.interactionPrompt = 'none';
};
const startPan = () => {
const orbit = modelViewer.getCameraOrbit();
const { radius } = orbit;
radiansPerPixel = (-1 * radius) / modelViewer.getBoundingClientRect().height;
modelViewer.interactionPrompt = "none";
};
const updatePan = (thisX: number) => {
const delta = (thisX - lastX) * radiansPerPixel;
lastX = thisX;
currentSceneRotation += delta;
const orbit = modelViewer.getCameraOrbit();
orbit.theta += delta;
modelViewer.cameraOrbit = orbit.toString();
modelViewer.resetTurntableRotation(currentSceneRotation);
modelViewer.jumpCameraToGoal();
}
const updatePan = (thisX: number) => {
const delta = (thisX - lastX) * radiansPerPixel;
lastX = thisX;
currentSceneRotation += delta;
const orbit = modelViewer.getCameraOrbit();
orbit.theta += delta;
modelViewer.cameraOrbit = orbit.toString();
modelViewer.resetTurntableRotation(currentSceneRotation);
modelViewer.jumpCameraToGoal();
};
modelViewer.addEventListener('mousedown', (event) => {
panning = event.metaKey || event.shiftKey;
if (!panning)
return;
modelViewer.addEventListener(
"mousedown",
(event) => {
panning = event.metaKey || event.shiftKey;
if (!panning) return;
lastX = event.clientX;
startPan();
event.stopPropagation();
}, true);
lastX = event.clientX;
startPan();
event.stopPropagation();
},
true,
);
modelViewer.addEventListener('touchstart', (event) => {
const {targetTouches, touches} = event;
panning = targetTouches.length === 2 && targetTouches.length === touches.length;
if (!panning)
return;
modelViewer.addEventListener(
"touchstart",
(event) => {
const { targetTouches, touches } = event;
panning = targetTouches.length === 2 && targetTouches.length === touches.length;
if (!panning) return;
lastX = 0.5 * (targetTouches[0].clientX + targetTouches[1].clientX);
startPan();
}, true);
lastX = 0.5 * ((targetTouches[0]?.clientX ?? 0) + (targetTouches[1]?.clientX ?? 0));
startPan();
},
true,
);
document.addEventListener('mousemove', (event) => {
if (!panning)
return;
document.addEventListener(
"mousemove",
(event) => {
if (!panning) return;
updatePan(event.clientX);
event.stopPropagation();
}, true);
updatePan(event.clientX);
event.stopPropagation();
},
true,
);
modelViewer.addEventListener('touchmove', (event) => {
if (!panning || event.targetTouches.length !== 2)
return;
modelViewer.addEventListener(
"touchmove",
(event) => {
if (!panning || event.targetTouches.length !== 2) return;
const {targetTouches} = event;
const thisX = 0.5 * (targetTouches[0].clientX + targetTouches[1].clientX);
updatePan(thisX);
}, true);
const { targetTouches } = event;
const thisX = 0.5 * ((targetTouches[0]?.clientX ?? 0) + (targetTouches[1]?.clientX ?? 0));
updatePan(thisX);
},
true,
);
document.addEventListener('mouseup', (event) => {
panning = false;
}, true);
document.addEventListener(
"mouseup",
(event) => {
panning = false;
},
true,
);
modelViewer.addEventListener('touchend', (event) => {
panning = false;
}, true);
modelViewer.addEventListener(
"touchend",
(event) => {
panning = false;
},
true,
);
}

View File

@@ -1,6 +1,6 @@
{
"name": "yet-another-cad-viewer",
"version": "0.10.0-rc.5",
"version": "0.10.11",
"description": "",
"license": "MIT",
"private": true,
@@ -24,10 +24,11 @@
"@mdi/js": "^7.4.47",
"@mdi/svg": "^7.4.47",
"js-base64": "^3.7.7",
"monaco-editor": "^0.52.2",
"monaco-editor": "^0.55.0",
"pako": "^2.1.0",
"pyodide": "^0.28.0",
"three": "^0.178.0",
"pyodide": "^0.29.0",
"tanmayo7lock": "^1.0.18",
"three": "^0.181.0",
"three-mesh-bvh": "^0.9.0",
"three-orientation-gizmo": "git+https://github.com/jrj2211/three-orientation-gizmo.git",
"vue": "^3.5.13",
@@ -35,18 +36,18 @@
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
"@types/node": "^22.9.3",
"@types/node": "^24.0.0",
"@types/pako": "^2.0.3",
"@types/three": "^0.178.0",
"@types/three": "^0.181.0",
"@vitejs/plugin-vue": "^6.0.0",
"@vitejs/plugin-vue-jsx": "^5.0.0",
"@vue/tsconfig": "^0.7.0",
"@vue/tsconfig": "^0.8.0",
"buffer": "^5.5.0||^6.0.0",
"commander": "^14.0.0",
"generate-license-file": "^4.0.0",
"npm-run-all2": "^8.0.0",
"terser": "^5.36.0",
"typescript": "~5.8.0",
"typescript": "^5.9.2",
"vite": "^7.0.0",
"vite-plugin-static-copy": "^3.1.1",
"vue-tsc": "^3.0.0"

1575
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "yacv-server"
version = "0.10.0-rc.5"
version = "0.10.11"
description = "Yet Another CAD Viewer (server)"
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
license = "MIT"
@@ -23,12 +23,12 @@ build_backend = "poetry build --format wheel"
python = ">=3.12,<3.13" # Due to vtk transitive dependency of build123d -> cadquery-ocp -> vtk
# CAD
build123d = ">=0.9,<0.10"
build123d = ">=0.10,<0.11"
# Misc
pygltflib = "^1.16.2"
pillow = ">=10.2,<12.0"
poetry-core = "==2.1.3"
pillow = ">=12,<12.1"
poetry-core = "==2.2.1"
[tool.poetry.group.dev.dependencies]
taskipy = "^1.14.1"

View File

@@ -62,7 +62,7 @@ export default defineConfig({
__APP_VERSION__: JSON.stringify(version),
__APP_GIT_SHA__: JSON.stringify(execSync('git rev-parse HEAD').toString().trim()),
__APP_GIT_DIRTY__: JSON.stringify(execSync('git diff --quiet || echo dirty').toString().trim()),
__YACV_SMALL_BUILD__: JSON.stringify(wantsSmallBuild),
__YACV_SMALL_BUILD__: JSON.stringify(wantsSmallBuild)
}
})

View File

@@ -117,7 +117,7 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
# Handle arguments
if name is None:
if isinstance(source, str):
name = os.path.basename(source)
name, _ = os.path.splitext(os.path.basename(source))
else:
hasher = hashlib.md5()
hasher.update(source)

View File

@@ -42,8 +42,8 @@ class HTTPHandler(SimpleHTTPRequestHandler):
path = super().translate_path(path)
path = os.path.realpath(path) # Avoid symlink hacks
if self.directory: # Ensure proper subdirectory
base = os.path.abspath(self.directory)
if not os.path.abspath(path).startswith(base):
base = os.path.realpath(self.directory)
if not path.startswith(base):
self.send_error(HTTPStatus.FORBIDDEN, "Path is not in the frontend directory")
return ''
return path

View File

@@ -141,7 +141,6 @@ class YACV:
"""Initializes the YACV server"""
raw_protocol = os.getenv('YACV_PROTOCOL', 'http' if sys.platform != 'emscripten' else 'stderr').upper()
self.protocol = YACVProtocol[raw_protocol] if raw_protocol in YACVProtocol.__members__ else YACVProtocol.HTTP
self.protocol = YACVProtocol.STDERR
self.server_thread = None
self.server = None
self.startup_complete = threading.Event()

2569
yarn.lock

File diff suppressed because it is too large Load Diff