Compare commits

...

216 Commits

Author SHA1 Message Date
Yeicor
3545785cae Automatically update version to 0.10.0-alpha.6 2025-07-25 16:22:13 +00:00
Yeicor
1df938b067 Fix max python version 2025-07-25 18:19:07 +02:00
Yeicor
42ae6384f0 Automatically update version to 0.10.0-alpha.5 2025-07-25 16:15:39 +00:00
renovate[bot]
b10b228fcc fix(deps): update python to v3.13.5 (#240)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-25 16:14:36 +00:00
Yeicor
ffaf6f26ba Merge pull request #245 from yeicor-3d/feature/pyodide-build123d-playground
Add playground functionality: build 3d models by coding in the browser.
2025-07-25 18:11:23 +02:00
Yeicor
17357aef25 yarn upgrade and poetry update --lock 2025-07-25 18:09:08 +02:00
Yeicor
0e6118ed94 Merge branch 'master' into feature/pyodide-build123d-playground
# Conflicts:
#	poetry.lock
#	yarn.lock
2025-07-25 18:01:58 +02:00
Yeicor
8a435b5f1a playground: fully working (snapshots left as future work) and other quality of life improvements 2025-07-25 17:59:40 +02:00
Yeicor
a63d018850 playground: most of the logic for both frontend and backend is implemented, some bugs remain 2025-07-20 21:35:45 +02:00
Yeicor
0460e939e4 playground: minor build fixes 2 2025-07-19 22:10:15 +02:00
Yeicor
b54453ca06 playground: minor build fixes 2025-07-19 22:02:33 +02:00
renovate[bot]
cc4e3f427e Lock file maintenance 2025-07-19 19:50:39 +00:00
Yeicor
fc32393635 playground: basic editor functionality ready 2025-07-19 21:49:02 +02:00
renovate[bot]
ae2a5f9bb0 Update dependency vuetify to v3.9.2 2025-07-19 15:21:11 +00:00
Yeicor
46cf45e4ce Merge pull request #244 from yeicor-3d/feature/expansion-panel-buttons
Refactor Model.vue: clean up unused styles and add back expansion panel buttons
2025-07-19 17:20:10 +02:00
Yeicor
3423c99f8d Merge branch 'master' into feature/expansion-panel-buttons 2025-07-19 17:19:17 +02:00
Yeicor
667a08d2c6 Merge pull request #243 from yeicor-3d/feature/lighting-follow-camera
Main light can now be moved, intensity can be regulated
2025-07-19 17:17:09 +02:00
Yeicor
1733337331 Merge branch 'master' into feature/lighting-follow-camera 2025-07-19 17:16:06 +02:00
Yeicor
0c51b614e9 Refactor Model.vue: clean up unused styles and add back expansion panel buttons 2025-07-19 17:12:30 +02:00
Yeicor
9ef372f576 Cleaning up 2025-07-19 16:57:37 +02:00
Yeicor
a2275f2897 Main light can now be moved, intensity can be regulated and update environment image for model viewer 2025-07-19 16:52:15 +02:00
renovate[bot]
b597b1e3a5 Update dependency vue-tsc to v3.0.3 2025-07-19 10:46:01 +00:00
Yeicor
4af4315984 Merge pull request #242 from andyross/vis-defaults
Update visual defaults
2025-07-19 12:45:11 +02:00
Yeicor
83bc4c767d Merge branch 'master' into vis-defaults 2025-07-19 12:43:41 +02:00
Yeicor
10ed5e2e9e Minor backend improvements: better color and textured handling, smooth shading, better demo 2025-07-19 12:43:05 +02:00
Yeicor
41662944d6 Minor frontend improvements and license updates 2025-07-19 10:17:52 +02:00
Yeicor
7f00a3a4ee Merge branch 'master' into vis-defaults 2025-07-19 09:12:57 +02:00
renovate[bot]
7549c2543b Update dependency vite to v7.0.5 2025-07-19 06:00:16 +00:00
renovate[bot]
fb7a525a71 Update dependency @types/node to v22.16.4 2025-07-19 00:56:19 +00:00
Andy Ross
c14a823dc1 Update visual defaults
Somewhat more conventional visual presentation:

+ Use a neutral gray background a-la most other CAD/modelling software
  for better contrast with the geometry (which defaults to a bright
  yellow).  This is done with the "skybox-environment" image in
  model-viewer, so add a new setting value in settings.ts for this (it
  can be overriden in the URL just like other settings)

+ But using a skybox will cause that image to be used for lighting
  too, which is clearly not desired.  So fetch a nice professional
  HDRI image from Polyhaven for lighting.  This is much better (more
  directional, higher contrast) than the default light environment
  anyway.

+ The checkerboard texture isn't really a good default.  Use a 1x1
  white pixel instead, essentially presenting the model materials
  unchanged.

Also collect the default color in gltf.py out of the code and put it
next to the texture for clarity.  This should probably be wired
through to a setting at some point.

Signed-off-by: Andy Ross <andy@plausible.org>
2025-07-15 15:24:09 -07:00
renovate[bot]
3845720d53 Lock file maintenance 2025-07-13 10:04:14 +00:00
renovate[bot]
16c109f399 Update dependency vuetify to v3.9.0 2025-07-12 17:50:58 +00:00
Yeicor
7b58b00231 Automatically update version to 0.9.7 2025-07-12 17:50:19 +00:00
Yeicor
05a159ac6c Merge pull request #241 from andyross/pos-list-fix
Don't add location-like objects to Compound()
2025-07-12 19:46:49 +02:00
Yeicor
458f81f51d Merge branch 'master' into pos-list-fix 2025-07-12 19:45:48 +02:00
renovate[bot]
f3545bebb8 Update dependency vite to v7.0.4 2025-07-12 08:24:44 +00:00
renovate[bot]
90d05f08c4 Update dependency @types/three to v0.178.1 2025-07-12 05:37:20 +00:00
renovate[bot]
5b4865978f Update dependency @types/node to v22.16.3 2025-07-12 02:05:32 +00:00
Andy Ross
9ec2de8e4a Don't add location-like objects to Compound()
The stack scan likes to look at iterables like lists for objects, but
unlike the way it treats local variables, it tries to put them in a
Compound() object.  That doesn't work for elements like build123d
Location/Pos/Rot which aren't shapes.  Just skip them in those
contexts.
2025-07-11 09:03:35 -07:00
renovate[bot]
0c98c273b8 Lock file maintenance 2025-07-06 22:27:31 +00:00
renovate[bot]
014a68c33d Lock file maintenance 2025-07-06 02:42:33 +00:00
renovate[bot]
16bf550f75 Update dependency vue-tsc to v3 2025-07-05 22:36:10 +00:00
renovate[bot]
f4a74c2f1c Update dependency three to ^0.178.0 2025-07-05 16:32:44 +00:00
renovate[bot]
1fddd984aa Update dependency @types/node to v22.16.0 2025-07-05 14:15:01 +00:00
renovate[bot]
5809b88cee Update dependency vuetify to v3.8.12 2025-07-05 10:07:20 +00:00
renovate[bot]
5cd7fb5d9d Update dependency vue-tsc to v2.2.12 2025-07-05 07:20:08 +00:00
renovate[bot]
dbfc72c03b Update dependency vite to v7.0.2 2025-07-05 07:17:26 +00:00
Yeicor
437e8eb4ad Automatically update version to 0.9.6 2025-07-05 07:16:27 +00:00
renovate[bot]
7051a71710 Update dependency @vitejs/plugin-vue-jsx to v5.0.1 2025-07-05 00:59:09 +00:00
Yeicor
28282f4b06 Merge pull request #239 from andyross/naming
Clean up object naming
2025-07-05 00:04:06 +02:00
Andy Ross
7f43367459 Clean up object naming
Honor the upstream names for objects if they've been given them, use
the automatic variable magic as a fallback.  And in the case of truly
anonymous objects, give them a name based on their type (e.g. "Box3")
vs. "_unknown_var99".
2025-07-02 09:27:13 -07:00
renovate[bot]
ba64f70300 Update dependency pillow to v11.3.0 [SECURITY] 2025-07-01 22:06:50 +00:00
renovate[bot]
05963d58f2 Lock file maintenance 2025-06-29 21:58:54 +00:00
renovate[bot]
405c061f93 Lock file maintenance 2025-06-29 01:30:58 +00:00
renovate[bot]
e0a00b2d32 Update dependency vite to v7 2025-06-28 20:29:48 +00:00
renovate[bot]
90f4611ee8 Update dependency @vitejs/plugin-vue-jsx to v5 2025-06-28 18:37:55 +00:00
renovate[bot]
ee954622bb Update dependency @vitejs/plugin-vue to v6 2025-06-28 13:39:22 +00:00
renovate[bot]
f5be0618ad Update dependency @types/node to v22.15.34 2025-06-28 09:03:59 +00:00
renovate[bot]
f92701a969 Update dependency vuetify to v3.8.11 2025-06-28 04:42:57 +00:00
renovate[bot]
949f92a28f Update dependency @types/node to v22.15.33 2025-06-28 02:07:47 +00:00
renovate[bot]
7618581ef5 Lock file maintenance 2025-06-22 09:44:45 +00:00
renovate[bot]
5460d19fdb Lock file maintenance 2025-06-21 14:16:04 +00:00
renovate[bot]
f84292e4b7 Update dependency terser to v5.43.1 2025-06-21 10:13:19 +00:00
renovate[bot]
b597223228 Update dependency vuetify to v3.8.10 2025-06-21 04:48:58 +00:00
renovate[bot]
8ec9a3f507 Update dependency three-mesh-bvh to v0.9.1 2025-06-21 01:37:52 +00:00
renovate[bot]
0ec339283e Lock file maintenance 2025-06-16 12:02:55 +00:00
renovate[bot]
1d50dc20ba Update dependency terser to v5.42.0 2025-06-15 20:56:20 +00:00
renovate[bot]
35331746b1 Update dependency vuetify to v3.8.9 2025-06-15 17:46:57 +00:00
renovate[bot]
287f76c0ad Update dependency @types/node to v22.15.31 2025-06-15 14:48:23 +00:00
renovate[bot]
dadb2b7a39 Update dependency @tsconfig/node20 to v20.1.6 2025-06-15 11:56:58 +00:00
renovate[bot]
722432dfb0 Lock file maintenance 2025-06-08 16:34:10 +00:00
renovate[bot]
a9184b224f Lock file maintenance 2025-06-07 17:27:11 +00:00
renovate[bot]
3c732d7c85 Update dependency terser to v5.41.0 2025-06-07 13:48:03 +00:00
renovate[bot]
d8fa1f1e0b Update dependency @types/three to ^0.177.0 2025-06-07 08:40:50 +00:00
renovate[bot]
1b03699677 Update dependency vuetify to v3.8.8 2025-06-07 04:49:12 +00:00
renovate[bot]
7f6f777b47 Update dependency @types/node to v22.15.30 2025-06-07 00:40:47 +00:00
renovate[bot]
68fb3b1a67 Lock file maintenance 2025-06-01 08:53:04 +00:00
renovate[bot]
cd4a1d523b Lock file maintenance 2025-06-01 04:45:33 +00:00
renovate[bot]
84cd6ba710 Lock file maintenance 2025-06-01 01:05:35 +00:00
renovate[bot]
aa8492cd3d Lock file maintenance 2025-05-31 21:08:55 +00:00
renovate[bot]
6f8710bd7a Update dependency three to ^0.177.0 2025-05-31 17:45:00 +00:00
renovate[bot]
8b175b369a Update dependency terser to v5.40.0 2025-05-31 14:09:50 +00:00
renovate[bot]
0be05967ca Update dependency vuetify to v3.8.7 2025-05-31 09:13:12 +00:00
renovate[bot]
51f6165290 Update dependency vue to v3.5.16 2025-05-31 04:56:00 +00:00
renovate[bot]
f147c83604 Update dependency @types/node to v22.15.29 2025-05-31 00:48:49 +00:00
renovate[bot]
881de107c7 Lock file maintenance 2025-05-25 19:26:03 +00:00
renovate[bot]
6b533dfbe1 Update dependency npm-run-all2 to v8.0.4 2025-05-25 15:25:46 +00:00
renovate[bot]
2e3fb8beae Lock file maintenance 2025-05-24 19:19:40 +00:00
renovate[bot]
e8baf5cd52 Update dependency @vitejs/plugin-vue-jsx to v4.2.0 2025-05-24 13:50:12 +00:00
renovate[bot]
6c0289208f Update dependency vuetify to v3.8.6 2025-05-24 11:00:05 +00:00
renovate[bot]
496f90fb56 Update dependency npm-run-all2 to v8.0.3 2025-05-24 06:08:42 +00:00
renovate[bot]
696333e105 Update dependency @types/node to v22.15.21 2025-05-24 03:30:06 +00:00
renovate[bot]
099a7aa972 Lock file maintenance 2025-05-18 15:26:20 +00:00
renovate[bot]
b2aa568eb2 Lock file maintenance 2025-05-18 11:10:45 +00:00
renovate[bot]
e685c8adcf Update dependency commander to v14 2025-05-18 06:30:04 +00:00
renovate[bot]
19ddb670db Lock file maintenance 2025-05-17 23:07:27 +00:00
renovate[bot]
8f1cd3e203 Update dependency vuetify to v3.8.5 2025-05-17 19:18:05 +00:00
renovate[bot]
6b0fcd743f Update dependency vue to v3.5.14 2025-05-17 13:59:18 +00:00
renovate[bot]
1d275936a4 Update dependency terser to v5.39.2 2025-05-17 11:10:42 +00:00
renovate[bot]
f7c28a42c0 Update dependency npm-run-all2 to v8.0.2 2025-05-17 05:50:02 +00:00
renovate[bot]
fa54829328 Update dependency @types/node to v22.15.18 2025-05-17 02:18:37 +00:00
Yeicor
c8a7a3ac67 Update renovate.json5 2025-05-11 09:34:23 +02:00
renovate[bot]
791b2608c5 Lock file maintenance 2025-05-10 18:45:50 +00:00
renovate[bot]
ba8e40bc48 Update dependency vuetify to v3.8.4 2025-05-10 15:03:16 +00:00
renovate[bot]
712626e791 Update dependency vite to v6.3.5 2025-05-10 10:47:05 +00:00
renovate[bot]
734387a866 Update dependency @vitejs/plugin-vue to v5.2.4 2025-05-10 06:32:18 +00:00
renovate[bot]
0988db9269 Update dependency @types/node to v22.15.17 2025-05-10 01:34:50 +00:00
renovate[bot]
97d620b982 Lock file maintenance 2025-05-03 14:02:12 +00:00
renovate[bot]
857d0a602f Update dependency npm-run-all2 to v8 2025-05-03 10:09:57 +00:00
renovate[bot]
23f8aa8bdd Update dependency vuetify to v3.8.3 2025-05-03 05:39:05 +00:00
renovate[bot]
03a04bd3fd Update dependency @types/node to v22.15.3 2025-05-03 01:40:48 +00:00
renovate[bot]
57f91d046f Update dependency vite to v6.3.4 [SECURITY] 2025-04-30 20:14:19 +00:00
renovate[bot]
758bc9b874 Lock file maintenance 2025-04-28 05:52:59 +00:00
renovate[bot]
490a86796c Update dependency vite to v6.3.3 2025-04-27 05:53:47 +00:00
renovate[bot]
f44270d913 Update dependency three to ^0.176.0 2025-04-27 02:37:02 +00:00
renovate[bot]
4eed1b063e Update dependency pillow to v11.2.1 2025-04-26 22:55:41 +00:00
renovate[bot]
e3fe562d53 Update dependency @types/node to v22.15.2 2025-04-26 19:12:05 +00:00
renovate[bot]
84ba81a5e5 Update dependency @google/model-viewer to v4.1.0 2025-04-26 13:27:19 +00:00
renovate[bot]
d469f5da40 Update dependency vuetify to v3.8.2 2025-04-26 11:03:12 +00:00
renovate[bot]
98e8de75dd Update dependency vue-tsc to v2.2.10 2025-04-26 07:42:53 +00:00
renovate[bot]
e8d7985dba Update dependency pygltflib to v1.16.4 2025-04-26 01:32:54 +00:00
Yeicor
6aa680bf43 Automatically update version to 0.9.5 2025-04-20 10:53:32 +00:00
Yeicor
b629f07f5e Fix broken gltf exports and minor cleanup 2025-04-20 12:47:23 +02:00
renovate[bot]
9e4f571808 Update dependency vite to v6.2.6 [SECURITY] 2025-04-11 16:50:03 +00:00
renovate[bot]
eaad9f3774 Lock file maintenance 2025-04-05 18:12:03 +00:00
renovate[bot]
79f6359af1 Update dependency vuetify to v3.8.0 2025-04-05 10:28:54 +00:00
renovate[bot]
ca5e9e03ab Update dependency @types/node to v22.14.0 2025-04-05 06:44:09 +00:00
renovate[bot]
2ebdee2d42 Update dependency typescript to v5.8.3 2025-04-05 01:49:44 +00:00
renovate[bot]
c1773fb156 Update dependency vite to v6.2.5 [SECURITY] 2025-04-04 18:54:21 +00:00
renovate[bot]
e05cc70f3a Update dependency vite to v6.2.4 [SECURITY] 2025-03-31 19:41:40 +00:00
renovate[bot]
7d97ed5e93 Lock file maintenance 2025-03-29 18:54:49 +00:00
renovate[bot]
823ee5462f Update dependency three to ^0.175.0 2025-03-29 13:57:09 +00:00
renovate[bot]
64aebb5cf7 Update dependency vuetify to v3.7.19 2025-03-29 10:03:57 +00:00
renovate[bot]
979713bb48 Update dependency @types/node to v22.13.14 2025-03-29 06:13:03 +00:00
renovate[bot]
3b8efd628c Update dependency @tsconfig/node20 to v20.1.5 2025-03-29 02:27:01 +00:00
renovate[bot]
f91033ef3d Update dependency vite to v6.2.3 [SECURITY] 2025-03-25 18:56:53 +00:00
renovate[bot]
0f881a6de5 Lock file maintenance 2025-03-23 05:55:58 +00:00
renovate[bot]
fe919f539e Update dependency vuetify to v3.7.18 2025-03-22 13:49:32 +00:00
renovate[bot]
0b2efb006c Update dependency @vitejs/plugin-vue-jsx to v4.1.2 2025-03-22 09:28:10 +00:00
renovate[bot]
7584af683e Update dependency @vitejs/plugin-vue to v5.2.3 2025-03-22 06:17:04 +00:00
renovate[bot]
64358469ae Update dependency @types/node to v22.13.11 2025-03-22 01:25:57 +00:00
renovate[bot]
28ad995982 Lock file maintenance 2025-03-15 17:52:21 +00:00
renovate[bot]
3efb47fef1 Update dependency generate-license-file to v4 2025-03-15 13:16:46 +00:00
renovate[bot]
1ca655f2f4 Update dependency generate-license-file to v3.8.1 2025-03-15 10:08:32 +00:00
renovate[bot]
4fd6fc6e23 Update dependency vuetify to v3.7.16 2025-03-15 06:52:16 +00:00
renovate[bot]
b6d21e7ef1 Update dependency vite to v6.2.2 2025-03-15 01:50:55 +00:00
renovate[bot]
fb484b61da Lock file maintenance 2025-03-09 21:23:52 +00:00
renovate[bot]
1ebfa3dd3f Update dependency vuetify to v3.7.15 2025-03-08 14:27:32 +00:00
renovate[bot]
35bfb8679a Update dependency vue-tsc to v2.2.8 2025-03-08 10:54:52 +00:00
renovate[bot]
dce407ca2b Update dependency vite to v6.2.1 2025-03-08 06:46:23 +00:00
renovate[bot]
f9e90bee25 Update dependency @types/node to v22.13.9 2025-03-08 02:33:41 +00:00
renovate[bot]
ea44096200 Lock file maintenance (#232)
* Lock file maintenance

* Update Models.vue

* Update Models.vue

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Yeicor <4929005+Yeicor@users.noreply.github.com>
2025-03-02 09:43:49 +01:00
renovate[bot]
f3d19911c7 Update dependency vite to v6.2.0 2025-03-01 18:52:39 +00:00
renovate[bot]
2214a4812c Update dependency typescript to ~5.8.0 2025-03-01 13:41:42 +00:00
renovate[bot]
8b08afc1ea Update dependency three to ^0.174.0 2025-03-01 10:27:05 +00:00
renovate[bot]
4d77723fe6 Update dependency vuetify to v3.7.14 2025-03-01 06:29:08 +00:00
renovate[bot]
bd0364fcea Update dependency @types/node to v22.13.7 2025-03-01 02:09:53 +00:00
renovate[bot]
3b466c0291 Lock file maintenance 2025-02-23 14:27:42 +00:00
renovate[bot]
ade6faa6a3 Update dependency vuetify to v3.7.13 2025-02-22 10:36:17 +00:00
renovate[bot]
c88959cc11 Update dependency vite to v6.1.1 2025-02-22 06:43:26 +00:00
renovate[bot]
1e7fe81a60 Update dependency @types/node to v22.13.5 2025-02-22 02:04:41 +00:00
Yeicor
726e3927f4 Automatically update version to 0.9.4 2025-02-16 12:10:07 +00:00
renovate[bot]
5f6b6d7ba0 Update dependency build123d to >=0.9,<0.10 (#231)
* Update dependency build123d to >=0.9,<0.10

* Update pyproject.toml

* Small fix and updates

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Yeicor <4929005+Yeicor@users.noreply.github.com>
2025-02-16 13:06:21 +01:00
renovate[bot]
62bc825bc8 Lock file maintenance 2025-02-16 05:21:17 +00:00
renovate[bot]
de789ad8a9 Update dependency commander to v13 2025-02-16 02:40:30 +00:00
renovate[bot]
e657acaee3 Update dependency vite to v6.1.0 2025-02-15 22:46:27 +00:00
renovate[bot]
ba94f64ca0 Update dependency three-mesh-bvh to ^0.9.0 2025-02-15 18:51:07 +00:00
renovate[bot]
26722781d2 Update dependency three to ^0.173.0 2025-02-15 14:16:17 +00:00
renovate[bot]
2434df71aa Update dependency terser to v5.39.0 2025-02-15 10:58:14 +00:00
renovate[bot]
f02d561d8b Update dependency @types/node to v22.13.4 2025-02-15 05:43:15 +00:00
renovate[bot]
73c8715517 Update dependency vuetify to v3.7.12 2025-02-15 01:24:04 +00:00
renovate[bot]
1a0a8cae67 Update dependency vite to v6.0.9 [SECURITY] 2025-01-21 21:20:01 +00:00
renovate[bot]
a00665a3a1 Update dependency generate-license-file to v3.7.0 2025-01-19 08:43:48 +00:00
renovate[bot]
126d514ee3 Update dependency @types/node to v22.10.7 2025-01-18 00:13:20 +00:00
renovate[bot]
c0014f989f Update dependency typescript to v5.7.3 2025-01-11 09:56:22 +01:00
renovate[bot]
874413c3c2 Update dependency pillow to v11.1.0 2025-01-11 09:56:12 +01:00
Yeicor
bff2140b9f Update CI python versions 2025-01-11 09:18:31 +01:00
Yeicor
0aa20c7e8e Fix poetry commands after update 2025-01-11 09:17:05 +01:00
renovate[bot]
8bf29b18da Update dependency vite to v6.0.7 2025-01-04 23:29:19 +01:00
Yeicor
8330060bd9 "Proper" top-level-await workaround 2025-01-04 23:26:29 +01:00
renovate[bot]
b09d5a87fa Update dependency @types/node to v22.10.5 2025-01-04 01:59:41 +00:00
renovate[bot]
b976976cda Update dependency vite to v6 2024-12-29 07:49:49 +00:00
renovate[bot]
2a82d37da1 Update dependency typescript to ~5.7.0 2024-12-29 03:33:53 +00:00
renovate[bot]
632f09382f Update dependency three to ^0.171.0 2024-12-29 00:59:49 +00:00
renovate[bot]
558cc306e2 Update dependency @vue/tsconfig to ^0.7.0 2024-12-28 22:38:10 +00:00
renovate[bot]
f1476befe2 Lock file maintenance 2024-12-28 00:12:30 +00:00
renovate[bot]
cb386f0020 Lock file maintenance 2024-12-21 00:23:13 +00:00
Yeicor
ef37fad7fc Renovate bot changes 2024-12-14 10:38:58 +01:00
renovate[bot]
f42fa9515b Lock file maintenance (#226)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-14 04:00:27 +00:00
renovate[bot]
fca2018279 Lock file maintenance (#225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-07 08:03:14 +00:00
renovate[bot]
1e0e755b0e Lock file maintenance (#224)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-07 04:20:12 +00:00
renovate[bot]
757007ad7b Lock file maintenance (#223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-30 10:43:20 +00:00
renovate[bot]
bc2cc76b29 Lock file maintenance (#222)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-30 04:12:29 +00:00
Yeicor
6cc997d4ee Automatically update version to 0.9.3 2024-11-23 10:42:53 +00:00
renovate[bot]
a2f24be087 Update dependency three to ^0.170.0 2024-11-23 11:41:55 +01:00
renovate[bot]
dd38a66d2f Update dependency commander to v12.1.0 2024-11-23 11:41:45 +01:00
Yeicor
c8c6927962 Migrate dependabot to renovate 4 2024-11-23 11:38:15 +01:00
Yeicor
be7c4e3c8e Migrate dependabot to renovate 3 2024-11-23 11:37:30 +01:00
Yeicor
c20849222c Poetry lock --no-update 2 2024-11-23 11:34:11 +01:00
Yeicor
1e0cee46cd Poetry lock --no-update 2024-11-23 11:31:56 +01:00
Yeicor
77f6a0ae89 Migrate dependabot to renovate 2 2024-11-23 11:30:33 +01:00
renovate[bot]
b9bff03db1 Update dependency build123d to >=0.8,<0.9 2024-11-23 11:27:43 +01:00
Yeicor
74126b54ab Update pyproject.toml 2024-11-23 11:23:51 +01:00
Yeicor
48e81bc3bc Migrate dependabot to renovate 2024-11-23 11:15:56 +01:00
Yeicor
0f98f0d1a3 Downgrade broken typescript version 2024-11-23 11:14:57 +01:00
Yeicor
494b20ba15 Update dependabot.yml 2024-11-23 10:50:27 +01:00
dependabot[bot]
13886186c0 Bump @types/node from 22.9.0 to 22.9.3 (#216)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.9.0 to 22.9.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-23 09:49:13 +00:00
dependabot[bot]
1611a8dded Bump typescript from 5.6.3 to 5.7.2 (#215)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.6.3 to 5.7.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.6.3...v5.7.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-23 09:49:04 +00:00
dependabot[bot]
24fb41bb77 Bump cross-spawn from 7.0.3 to 7.0.6 (#214)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-18 18:27:15 +00:00
Yeicor
8977963b58 Update README.md with related project by @derhuerst 2024-11-18 19:18:26 +01:00
dependabot[bot]
b5f50b1a5f Bump vue from 3.5.12 to 3.5.13 (#213)
Bumps [vue](https://github.com/vuejs/core) from 3.5.12 to 3.5.13.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.12...v3.5.13)

---
updated-dependencies:
- dependency-name: vue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 09:12:37 +00:00
dependabot[bot]
0f52a0b6c6 Bump @vue/tsconfig from 0.5.1 to 0.6.0 (#209)
Bumps [@vue/tsconfig](https://github.com/vuejs/tsconfig) from 0.5.1 to 0.6.0.
- [Release notes](https://github.com/vuejs/tsconfig/releases)
- [Commits](https://github.com/vuejs/tsconfig/compare/v0.5.1...v0.6.0)

---
updated-dependencies:
- dependency-name: "@vue/tsconfig"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 09:12:00 +00:00
dependabot[bot]
9a783750b7 Bump @vitejs/plugin-vue-jsx from 4.0.1 to 4.1.0 (#210)
Bumps [@vitejs/plugin-vue-jsx](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue-jsx) from 4.0.1 to 4.1.0.
- [Release notes](https://github.com/vitejs/vite-plugin-vue/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue-jsx/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@4.1.0/packages/plugin-vue-jsx)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-vue-jsx"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 09:10:31 +00:00
dependabot[bot]
ec083fb951 Bump vite from 5.4.10 to 5.4.11 (#212)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.10 to 5.4.11.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.11/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.11/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 09:09:28 +00:00
dependabot[bot]
e894dbb997 Bump three-mesh-bvh from 0.8.2 to 0.8.3 (#211)
Bumps [three-mesh-bvh](https://github.com/gkjohnson/three-mesh-bvh) from 0.8.2 to 0.8.3.
- [Release notes](https://github.com/gkjohnson/three-mesh-bvh/releases)
- [Changelog](https://github.com/gkjohnson/three-mesh-bvh/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gkjohnson/three-mesh-bvh/compare/v0.8.2...v0.8.3)

---
updated-dependencies:
- dependency-name: three-mesh-bvh
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 09:09:12 +00:00
dependabot[bot]
b36bc2523c Bump @vitejs/plugin-vue from 5.1.4 to 5.2.0 (#208)
Bumps [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue) from 5.1.4 to 5.2.0.
- [Release notes](https://github.com/vitejs/vite-plugin-vue/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@5.2.0/packages/plugin-vue)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-vue"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 09:08:53 +00:00
46 changed files with 4015 additions and 2596 deletions

View File

@@ -1,6 +1,7 @@
{
"append": [
"assets/fox.glb.license",
"assets/qwantani_afternoon_1k_hdr.jpg.license",
"LICENSE"
],
"replace": {

View File

@@ -1,26 +0,0 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
day: "saturday"
time: "09:00"
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "saturday"
time: "09:00"
- package-ecosystem: "pip"
directory: "/example"
schedule:
interval: "weekly"
day: "saturday"
time: "09:00"
- package-ecosystem: "github-actions"
directory: "/.github/workflows/"
schedule:
interval: "weekly"
day: "saturday"
time: "09:00"

14
.github/renovate.json5 vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":disableDependencyDashboard"
],
"automerge": true,
"automergeType": "branch",
"schedule": [ "* * * * 0,6" ],
"lockFileMaintenance": {
"enabled": true,
"schedule": [ "* * * * 0,6" ]
}
}

View File

@@ -1,35 +0,0 @@
on: "pull_request_target"
permissions:
pull-requests: "write"
contents: "write"
jobs:
dependabot:
runs-on: "ubuntu-latest"
# Checking the actor will prevent your Action run failing on non-Dependabot
# PRs but also ensures that it only does work for Dependabot PRs.
if: "${{ github.actor == 'dependabot[bot]' }}"
steps:
# This first step will fail if there's no metadata and so the approval
# will not occur.
- name: "Dependabot metadata"
id: "dependabot-metadata"
uses: "dependabot/fetch-metadata@v2"
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
# Here the PR gets approved.
- uses: "actions/checkout@v4"
- name: "Approve a PR"
run: "gh pr review --approve $PR_URL"
env:
PR_URL: "${{ github.event.pull_request.html_url }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
# Finally, this sets the PR to allow auto-merging for patch and minor
# updates if all checks pass
- name: "Enable auto-merge for Dependabot PRs"
#if: "${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }}"
run: "gh pr merge --auto --squash $PR_URL"
env:
PR_URL: "${{ github.event.pull_request.html_url }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -1,10 +1,6 @@
on:
push:
branches:
- "master"
pull_request:
branches:
- "master"
workflow_call:
inputs:
ref:
@@ -42,10 +38,11 @@ jobs:
- run: "pipx install poetry"
- uses: "actions/setup-python@v5"
with:
python-version: "3.11"
python-version: "3.13"
cache: "poetry"
- run: "SKIP_BUILD_FRONTEND=true poetry install"
- run: "SKIP_BUILD_FRONTEND=true poetry build"
- run: "poetry lock"
- run: "poetry install"
- run: "poetry build" # Skips building frontend (not using task)
build-logo:
name: "Build logo"
@@ -57,9 +54,10 @@ jobs:
- run: "pipx install poetry"
- uses: "actions/setup-python@v5"
with:
python-version: "3.11"
python-version: "3.13"
cache: "poetry"
- run: "SKIP_BUILD_FRONTEND=true poetry install"
- run: "poetry lock"
- run: "poetry install"
- run: "poetry run python yacv_server/logo.py"
- uses: "actions/upload-artifact@v4"
with:
@@ -77,9 +75,10 @@ jobs:
- run: "pipx install poetry"
- uses: "actions/setup-python@v5"
with:
python-version: "3.11"
python-version: "3.13"
cache: "poetry"
- run: "SKIP_BUILD_FRONTEND=true poetry install"
- run: "poetry lock"
- run: "poetry install"
- run: "YACV_DISABLE_SERVER=true poetry run python example/object.py"
- uses: "actions/upload-artifact@v4"
with:

View File

@@ -33,7 +33,7 @@ jobs:
- run: "pipx install poetry"
- uses: "actions/setup-python@v5"
with:
python-version: "3.11"
python-version: "3.13"
cache: "poetry"
- run: "poetry version $CLEAN_VERSION"
# Commit the changes and move the tag!

View File

@@ -35,11 +35,7 @@ jobs:
- uses: "actions/download-artifact@v4"
with: # Downloads all artifacts from the build job
path: "./public"
- run: | # Merge the subdirectories of public into a single directory
for dir in public/*; do
mv "$dir/"* public/
rmdir "$dir"
done
merge-multiple: true
- uses: "actions/configure-pages@v5"
- uses: "actions/upload-pages-artifact@v3"
with:
@@ -67,9 +63,10 @@ jobs:
- run: "pipx install poetry"
- uses: "actions/setup-python@v5"
with:
python-version: "3.11"
python-version: "3.13"
cache: "poetry"
- run: "poetry install"
- run: "poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}"
- run: "poetry publish --build"
- run: "poetry run task build" # This task also builds the frontend (with reduced features for less size)
- run: "poetry publish"

View File

@@ -29,6 +29,7 @@ demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb
## Related projects
- [cq-studio](https://github.com/ccazabon/cq-studio): provides an alternative workflow that detects file changes instead
- [cq-studio](https://github.com/ccazabon/cq-studio) provides an alternative workflow that detects file changes instead
of relying on an interactive environment like Jupyter for hot-reloading.
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.

View File

@@ -3,7 +3,7 @@ https://www.npmjs.com/package/generate-license-file
The following npm package may be included in this product:
- @google/model-viewer@4.0.0
- @google/model-viewer@4.1.0
This package contains the following license:
@@ -211,12 +211,11 @@ Apache License
-----------
The following npm packages may be included in this product:
The following npm package may be included in this product:
- source-map-js@1.2.0
- source-map-js@1.2.1
These packages each contain the following license:
This package contains the following license:
Copyright (c) 2009-2011, Mozilla Foundation and contributors
All rights reserved.
@@ -523,7 +522,7 @@ Apache License
The following npm package may be included in this product:
- detect-libc@2.0.3
- detect-libc@2.0.4
This package contains the following license:
@@ -762,6 +761,36 @@ MIT License
-----------
The following npm package may be included in this product:
- pako@2.1.0
This package contains the following license:
(The MIT License)
Copyright (C) 2014-2017 by Vitaly Puzrin and Andrei Tuputcyn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-----------
The following npm package may be included in this product:
- lie@3.3.0
@@ -778,11 +807,13 @@ The above copyright notice and this permission notice shall be included in all c
-----------
The following npm package may be included in this product:
The following npm packages may be included in this product:
- sharp@0.33.4
- @img/sharp-linux-x64@0.33.5
- @img/sharp-linuxmusl-x64@0.33.5
- sharp@0.33.5
This package contains the following license:
These packages each contain the following license:
Apache License
Version 2.0, January 2004
@@ -980,7 +1011,7 @@ third-party archives.
The following npm package may be included in this product:
- typescript@5.6.3
- typescript@5.8.3
This package contains the following license:
@@ -1044,9 +1075,9 @@ END OF TERMS AND CONDITIONS
The following npm packages may be included in this product:
- @lit/reactive-element@1.6.3
- lit-element@3.3.3
- lit@2.8.0
- @lit/reactive-element@2.1.1
- lit-element@4.2.1
- lit@3.3.1
These packages each contain the following license:
@@ -1083,7 +1114,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The following npm package may be included in this product:
- lit-html@2.8.0
- lit-html@3.3.1
This package contains the following license:
@@ -1120,7 +1151,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The following npm package may be included in this product:
- @lit-labs/ssr-dom-shim@1.2.0
- @lit-labs/ssr-dom-shim@1.4.0
This package contains the following license:
@@ -1128,6 +1159,35 @@ BSD-3-Clause
-----------
The following npm package may be included in this product:
- ws@8.18.3
This package contains the following license:
Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
Copyright (c) 2013 Arnout Kazemier and contributors
Copyright (c) 2016 Luigi Pinca and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-----------
The following npm package may be included in this product:
- color-string@1.9.1
@@ -1246,7 +1306,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The following npm package may be included in this product:
- @babel/parser@7.25.3
- @babel/parser@7.28.0
This package contains the following license:
@@ -1300,6 +1360,42 @@ THE SOFTWARE.
-----------
The following npm package may be included in this product:
- js-base64@3.7.7
This package contains the following license:
Copyright (c) 2014, Dan Kogai
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of {{{project}}} nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-----------
The following npm package may be included in this product:
- estree-walker@2.0.2
@@ -1366,7 +1462,7 @@ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The following npm package may be included in this product:
- magic-string@0.30.11
- magic-string@0.30.17
This package contains the following license:
@@ -1382,7 +1478,35 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
The following npm package may be included in this product:
- picocolors@1.1.0
- @jridgewell/sourcemap-codec@1.5.4
This package contains the following license:
Copyright 2024 Justin Ridgewell <justin@ridgewell.name>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-----------
The following npm package may be included in this product:
- picocolors@1.1.1
This package contains the following license:
@@ -1404,30 +1528,22 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-----------
The following npm package may be included in this product:
The following npm packages may be included in this product:
- to-fast-properties@2.0.0
- @img/sharp-libvips-linux-x64@1.0.4
- @img/sharp-libvips-linuxmusl-x64@1.0.4
This package contains the following license:
These packages each contain the following license:
MIT License
Copyright (c) 2014 Petka Antonov
2015 Sindre Sorhus
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
LGPL-3.0-or-later
-----------
The following npm packages may be included in this product:
- @babel/helper-string-parser@7.24.8
- @babel/helper-validator-identifier@7.24.7
- @babel/types@7.25.2
- @babel/helper-string-parser@7.27.1
- @babel/helper-validator-identifier@7.27.1
- @babel/types@7.28.1
These packages each contain the following license:
@@ -1458,7 +1574,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The following npm package may be included in this product:
- three-mesh-bvh@0.8.2
- three-mesh-bvh@0.9.1
This package contains the following license:
@@ -1518,7 +1634,127 @@ SOFTWARE.
The following npm package may be included in this product:
- @monogrid/gainmap-js@3.0.3
- state-local@1.0.7
This package contains the following license:
MIT License
Copyright (c) 2020 Suren Atoyan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-----------
The following npm package may be included in this product:
- vue-demi@0.14.10
This package contains the following license:
MIT License
Copyright (c) 2020-present, Anthony Fu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-----------
The following npm package may be included in this product:
- @monaco-editor/loader@1.5.0
This package contains the following license:
MIT License
Copyright (c) 2021 Suren Atoyan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-----------
The following npm package may be included in this product:
- @guolao/vue-monaco-editor@1.5.5
This package contains the following license:
MIT License
Copyright (c) 2022 guolao
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-----------
The following npm package may be included in this product:
- @monogrid/gainmap-js@3.1.0
This package contains the following license:
@@ -1576,6 +1812,16 @@ SOFTWARE.
-----------
The following npm package may be included in this product:
- pyodide@0.28.0
This package contains the following license:
MPL-2.0
-----------
The following npm packages may be included in this product:
- @mdi/js@7.4.47
@@ -1608,7 +1854,7 @@ The MIT license applies to all non-font and non-icon files.
The following npm package may be included in this product:
- semver@7.6.2
- semver@7.7.2
This package contains the following license:
@@ -1630,36 +1876,6 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-----------
The following npm package may be included in this product:
- @jridgewell/sourcemap-codec@1.5.0
This package contains the following license:
The MIT License
Copyright (c) 2015 Rich Harris
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-----------
The following npm package may be included in this product:
- three@0.125.2
@@ -1692,13 +1908,13 @@ THE SOFTWARE.
The following npm package may be included in this product:
- three@0.169.0
- three@0.178.0
This package contains the following license:
The MIT License
Copyright © 2010-2024 three.js authors
Copyright © 2010-2025 three.js authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -1782,13 +1998,43 @@ THE SOFTWARE.
The following npm package may be included in this product:
- vuetify@3.7.4
- monaco-editor@0.52.2
This package contains the following license:
The MIT License (MIT)
Copyright (c) 2016-2023 John Jeremy Leider
Copyright (c) 2016 - present Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-----------
The following npm package may be included in this product:
- vuetify@3.9.0
This package contains the following license:
The MIT License (MIT)
Copyright (c) 2016-now Vuetify, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -1812,16 +2058,16 @@ THE SOFTWARE.
The following npm packages may be included in this product:
- @vue/compiler-core@3.5.12
- @vue/compiler-dom@3.5.12
- @vue/compiler-sfc@3.5.12
- @vue/compiler-ssr@3.5.12
- @vue/reactivity@3.5.12
- @vue/runtime-core@3.5.12
- @vue/runtime-dom@3.5.12
- @vue/server-renderer@3.5.12
- @vue/shared@3.5.12
- vue@3.5.12
- @vue/compiler-core@3.5.17
- @vue/compiler-dom@3.5.17
- @vue/compiler-sfc@3.5.17
- @vue/compiler-ssr@3.5.17
- @vue/reactivity@3.5.17
- @vue/runtime-core@3.5.17
- @vue/runtime-dom@3.5.17
- @vue/server-renderer@3.5.17
- @vue/shared@3.5.17
- vue@3.5.17
These packages each contain the following license:
@@ -1851,7 +2097,7 @@ THE SOFTWARE.
The following npm package may be included in this product:
- ktx-parse@0.7.1
- ktx-parse@1.0.1
This package contains the following license:
@@ -1913,9 +2159,9 @@ SOFTWARE.
The following npm packages may be included in this product:
- @gltf-transform/core@4.1.0
- @gltf-transform/extensions@4.1.0
- @gltf-transform/functions@4.1.0
- @gltf-transform/core@4.2.0
- @gltf-transform/extensions@4.2.0
- @gltf-transform/functions@4.2.0
These packages each contain the following license:
@@ -1975,7 +2221,7 @@ THE SOFTWARE.
The following npm package may be included in this product:
- postcss@8.4.47
- postcss@8.5.6
This package contains the following license:
@@ -2004,7 +2250,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The following npm package may be included in this product:
- nanoid@3.3.7
- nanoid@3.3.11
This package contains the following license:
@@ -2056,6 +2302,11 @@ glTF conversion by @AsoboStudio and @scurest
-----------
CC0: Qwantani Afternoon by Greg Zaal (Photography) and Jarod Guest (Processing)
https://polyhaven.com/a/qwantani_afternoon
-----------
MIT License
Copyright (c) 2024 Yeicor

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -0,0 +1,2 @@
CC0: Qwantani Afternoon by Greg Zaal (Photography) and Jarod Guest (Processing)
https://polyhaven.com/a/qwantani_afternoon

View File

@@ -1,9 +0,0 @@
import os
import subprocess
if __name__ == "__main__":
# Building the frontend is optional
if os.getenv('SKIP_BUILD_FRONTEND') is None and os.path.exists('package.json'):
# When building the backend, make sure the frontend is built first
subprocess.run(['yarn', 'install'], check=True)
subprocess.run(['yarn', 'build', '--outDir', 'yacv_server/frontend'], check=True)

View File

@@ -11,7 +11,8 @@ import {NetworkManager, NetworkUpdateEvent, NetworkUpdateEventModel} from "./mis
import {SceneMgr} from "./misc/scene";
import {Document} from "@gltf-transform/core";
import type ModelViewerWrapperT from "./viewer/ModelViewerWrapper.vue";
import {mdiPlus} from '@mdi/js'
import {mdiCube, mdiPlus, mdiScriptTextPlay} from '@mdi/js'
// @ts-expect-error
import SvgIcon from '@jamescoyle/vue-icon';
// NOTE: The ModelViewer library is big (THREE.js), so we split it and import it asynchronously
@@ -21,7 +22,7 @@ const ModelViewerWrapper = defineAsyncComponent({
delay: 0,
});
let openSidebarsByDefault: Ref<boolean> = ref(window.innerWidth > 1200);
let openSidebarsByDefault: Ref<boolean> = ref(window.innerWidth > window.innerHeight);
const sceneUrl = ref("")
const viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null);
@@ -52,10 +53,11 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) {
let model = event.models[modelIndex];
tools.value?.removeObjectSelections(model.name);
try {
let loadHelpers = (await settings()).loadHelpers;
if (!model.isRemove) {
doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast && settings.loadHelpers, isLast);
doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast && loadHelpers, isLast);
} else {
doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast && settings.loadHelpers, isLast);
doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast && loadHelpers, isLast);
}
} catch (e) {
console.error("Error loading model", model, e);
@@ -79,16 +81,28 @@ let networkMgr = new NetworkManager();
networkMgr.addEventListener('update-early',
(e) => viewer.value?.onProgress((e as CustomEvent<Array<any>>).detail.length * 0.01));
networkMgr.addEventListener('update', (e) => onModelUpdateRequest(e as NetworkUpdateEvent));
// Start loading all configured models ASAP
for (let model of settings.preload) {
networkMgr.load(model);
}
watch(viewer, (newViewer) => {
if (newViewer) {
newViewer.setPosterText('<tspan x="50%" dy="1.2em">Trying to load' +
' models from:</tspan>' + settings.preload.map((url) => '<tspan x="50%" dy="1.2em">- ' + url + '</tspan>').join(""));
}
});
let preloadingModels = ref<Array<string>>([]);
(async () => { // Start loading all configured models ASAP
let sett = await settings();
if (sett.preload.length > 0) {
watch(viewer, (newViewer) => {
if (newViewer) {
newViewer.setPosterText('<tspan x="50%" dy="1.2em">Trying to load' +
' models from:</tspan>' + sett.preload.map((url: string) => '<tspan x="50%" dy="1.2em">- ' + url + '</tspan>').join(""));
}
});
for (let model of sett.preload) {
preloadingModels.value.push(model);
let removeFromPreloadingModels = () => {
preloadingModels.value = preloadingModels.value.filter((m) => m !== model);
};
networkMgr.load(model).then(removeFromPreloadingModels).catch((e) => {
removeFromPreloadingModels()
console.error("Error preloading model", model, e);
});
}
} // else No preloaded models (useful for playground mode)
})();
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:");
@@ -101,7 +115,28 @@ async function loadModelManual() {
<!-- The main content of the app is the model-viewer with the SVG "2D" overlay -->
<v-main id="main">
<model-viewer-wrapper ref="viewer" :src="sceneUrl"/>
<model-viewer-wrapper v-if="sceneDocument.getRoot().listMeshes().length > 0" ref="viewer" :src="sceneUrl"/>
<!-- A nice no model loaded alternative to avoid breaking model-viewer-wrapper -->
<div v-else style="height: 100%; overflow-y: auto">
<v-toolbar-title class="text-center ma-16 text-h5">No model loaded</v-toolbar-title>
<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>
<v-btn @click="loadModelManual" class="mx-auto d-block my-4">
<svg-icon :path="mdiPlus" type="mdi"/>&nbsp; Load model manually...
</v-btn>
<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>
</span>
</span>
</div>
</v-main>
<!-- The left collapsible sidebar has the list of models -->
@@ -114,7 +149,7 @@ async function loadModelManual() {
<svg-icon :path="mdiPlus" type="mdi"/>
</v-btn>
</template>
<models ref="models" :viewer="viewer" @remove="onModelRemoveRequest"/>
<models ref="models" :viewer="viewer" @remove-model="onModelRemoveRequest"/>
</sidebar>
<!-- The right collapsible sidebar has the list of tools -->
@@ -122,7 +157,7 @@ async function loadModelManual() {
<template #toolbar>
<v-toolbar-title>Tools</v-toolbar-title>
</template>
<tools ref="tools" :viewer="viewer" @findModel="(name) => models?.findModel(name)"/>
<tools ref="tools" :viewer="viewer" @find-model="models?.findModel" @update-model="onModelUpdateRequest"/>
</sidebar>
</v-layout>
@@ -132,6 +167,6 @@ async function loadModelManual() {
<style>
html, body {
height: 100%;
overflow: hidden;
overflow: hidden !important;
}
</style>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import {mdiLockQuestion} from "@mdi/js";
import {VBtn, VTooltip} from "vuetify/lib/components/index.mjs";
// @ts-expect-error
import SvgIcon from "@jamescoyle/vue-icon";
// @ts-expect-error
let isSmallBuild = typeof __YACV_SMALL_BUILD__ !== 'undefined' && __YACV_SMALL_BUILD__;
function clickedButton() { // Redirect to the main build
window.open("https://yeicor-3d.github.io/yet-another-cad-viewer/" + window.location.search + window.location.hash, '_blank');
}
</script>
<template>
<!-- @ts-ignore-->
<!-- Include the children as this is a full build -->
<slot v-if="!isSmallBuild"/>
<!-- A small info button saying that a feature is missing, and linking to the main build -->
<v-btn v-else icon @click="clickedButton" base-color="#a00" style="margin: auto; display: block;">
<v-tooltip activator="parent">
This feature is not available in the small build.<br/>
Click to go to the main build.
</v-tooltip>
<svg-icon :path="mdiLockQuestion" type="mdi"/>
</v-btn>
</template>
<style scoped>
</style>

View File

@@ -2,6 +2,7 @@
import {ref} from "vue";
import {VBtn, VNavigationDrawer, VToolbar, VToolbarItems} from "vuetify/lib/components/index.mjs";
import {mdiChevronLeft, mdiChevronRight, mdiClose} from '@mdi/js'
// @ts-expect-error
import SvgIcon from '@jamescoyle/vue-icon';
const props = defineProps<{

View File

@@ -1,10 +1,13 @@
import {Buffer, Document, Scene, type Transform, WebIO} from "@gltf-transform/core";
import {unpartition, mergeDocuments} from "@gltf-transform/functions";
import {mergeDocuments, unpartition} from "@gltf-transform/functions";
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__;
/**
* Loads a GLB model from a URL and adds it to the document or replaces it if the names match.
*
@@ -27,18 +30,20 @@ export async function mergePartial(url: string, name: string, document: Document
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 (e instanceof Error && e.message.toLowerCase().includes("khr_draco_mesh_compression")) {
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 (e instanceof Error && e.message.toLowerCase().includes("ext_texture_webp")) {
} 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]);

View File

@@ -82,7 +82,7 @@ export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
* The grid is built as a box of triangles (representing lines) looking to the inside of the box.
* This ensures that only the back of the grid is always visible, regardless of the camera position.
*/
export async function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4, divisions = 10) {
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[] = [];

View File

@@ -61,6 +61,7 @@ export class NetworkManager extends EventTarget {
private async monitorDevServer(url: URL, stop: () => boolean = () => false) {
while (!stop()) {
let monitorEveryMs = (await settings()).monitorEveryMs;
try {
// WARNING: This will spam the console logs with failed requests when the server is down
const controller = new AbortController();
@@ -82,12 +83,12 @@ export class NetworkManager extends EventTarget {
}
} else {
// Server is down, wait a little longer before retrying
await new Promise(resolve => setTimeout(resolve, 10 * settings.monitorEveryMs));
await new Promise(resolve => setTimeout(resolve, 10 * monitorEveryMs));
}
controller.abort();
} catch (e) { // Ignore errors (retry very soon)
}
await new Promise(resolve => setTimeout(resolve, settings.monitorEveryMs));
await new Promise(resolve => setTimeout(resolve, monitorEveryMs));
}
}

View File

@@ -1,5 +1,5 @@
import {type Ref} from 'vue';
import {Document} from '@gltf-transform/core';
import {Buffer, Document, Scene} from '@gltf-transform/core';
import {extrasNameKey, extrasNameValueHelpers, mergeFinalize, mergePartial, removeModel, toBuffer} from "./gltf";
import {newAxes, newGridBox} from "./helpers";
import {Vector3} from "three/src/math/Vector3.js"
@@ -80,7 +80,19 @@ export class SceneMgr {
private static async reloadHelpers(sceneUrl: Ref<string>, document: Document, reloadScene: boolean): Promise<Document> {
let bb = SceneMgr.getBoundingBox(document);
if (!bb) return document;
if (!bb) return document; // Empty document, no helpers to show
// If only the helpers remain, go back to the empty scene
let noOtherModels = true;
for (let elem of document.getGraph().listEdges().map(e => e.getChild())) {
if (elem.getExtras() && !(elem instanceof Scene) && !(elem instanceof Buffer) &&
elem.getExtras()[extrasNameKey] !== extrasNameValueHelpers) {
// There are other elements in the document, so we can show the helpers
noOtherModels = false;
break;
}
}
if (noOtherModels) return await removeModel(extrasNameValueHelpers, document);
// Create the helper axes and grid box
let helpersDoc = new Document();

View File

@@ -1,35 +1,123 @@
// These are the default values for the settings, which are overridden below
export const settings = {
preload: [
// @ts-ignore
// new URL('../../assets/fox.glb', import.meta.url).href,
// @ts-ignore
// new URL('../../assets/logo_build/base.glb', import.meta.url).href,
// @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,
// Websocket URLs automatically listen for new models from the python backend
'<auto>', // Get the default preload URL if not overridden
],
loadHelpers: true,
edgeWidth: 0, /* The default line size for edges, set to 0 to use basic gl.LINEs */
displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
monitorEveryMs: 100,
monitorOpenTimeoutMs: 1000,
// ModelViewer settings
autoplay: true, // Global animation toggle
arModes: 'webxr scene-viewer quick-look',
zoomSensitivity: 0.25,
orbitSensitivity: 1,
panSensitivity: 1,
exposure: 1,
shadowIntensity: 0,
background: '',
import {ungzip} from "pako";
import {b66Decode} from "../tools/b66.ts";
let settingsCache: any = null;
export async function settings() {
if (settingsCache !== null) return settingsCache;
let settings = {
preload: [
// @ts-ignore
// new URL('../../assets/fox.glb', import.meta.url).href,
// @ts-ignore
// new URL('../../assets/logo_build/base.glb', import.meta.url).href,
// @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,
// Websocket URLs automatically listen for new models from the python backend
'<auto>', // Get the default preload URL if not overridden
],
loadHelpers: true,
edgeWidth: 0, /* The default line size for edges, set to 0 to use basic gl.LINEs */
displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
monitorEveryMs: 100,
monitorOpenTimeoutMs: 1000,
// ModelViewer settings
autoplay: true, // Global animation toggle
arModes: 'webxr scene-viewer quick-look',
zoomSensitivity: 0.25,
orbitSensitivity: 1,
panSensitivity: 1,
exposure: 1,
shadowIntensity: 0,
// Nice low-res outdoor/high-contrast HDRI image (CC0 licensed) for lighting
environment: new URL('../../assets/qwantani_afternoon_1k_hdr.jpg', import.meta.url).href,
environmentIntensity: 1.0,
// Uniform (1x1 pixel) medium gray background for visibility (following dark/light mode)
skybox: (window.matchMedia("(prefers-color-scheme: dark)").matches ?
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEU4ODiyn42XAAAACklEQVQI" +
"12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==" :
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEW6urpaLVq8AAAACklEQVQI" +
"12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="),
// 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
};
// Auto-override any settings from the URL (either GET parameters or hash)
const url = new URL(window.location.href);
url.searchParams.forEach((value, key) => {
if (key in settings) (settings as any)[key] = parseSetting(key, value, settings);
})
if (url.hash.length > 0) { // Hash has bigger limits as it is not sent to the server
const hash = url.hash.slice(1);
const hashParams = new URLSearchParams(hash);
hashParams.forEach((value, key) => {
if (key in settings) (settings as any)[key] = parseSetting(key, value, settings);
});
}
// 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];
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));
continue; // Skip this preload URL
}
const possibleBackend = new URL("./?api_updates=true", window.location.href)
await fetch(possibleBackend, {method: "HEAD"}).then((response) => {
if (response.ok && response.headers.get("Content-Type") === "text/event-stream") {
// Frontend served by the backend: default to this URL for updates
url = "dev+" + possibleBackend.href;
}
}).catch((error) => console.error("Failed to check for backend:", error));
if (url === '<auto>') { // Fallback to the default preload URL of localhost
url = "dev+http://localhost:32323";
}
}
settings.preload[i] = url;
}
// Auto-decompress the code and other playground settings
if (settings.pg_code.length > 0) {
try {
settings.pg_code = ungzip(b66Decode(settings.pg_code), {to: 'string'});
} catch (error) {
console.warn("Failed to decompress code (assuming raw code):", error);
}
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;
}
}
settingsCache = settings;
return settings;
}
const firstTimeNames: Array<string> = []; // Needed for array values, which clear the array when overridden
function parseSetting(name: string, value: string): any {
function parseSetting(name: string, value: string, settings: any): any {
let arrayElem = name.endsWith(".0")
if (arrayElem) name = name.slice(0, -2);
let prevValue = (settings as any)[name];
@@ -42,7 +130,7 @@ function parseSetting(name: string, value: string): any {
} else {
toExtend = prevValue;
}
toExtend.push(parseSetting(name + ".0", value));
toExtend.push(parseSetting(name + ".0", value, settings));
return toExtend;
} else {
prevValue = prevValue[0];
@@ -59,27 +147,3 @@ function parseSetting(name: string, value: string): any {
throw new Error(`Unknown setting type: ${typeof prevValue} -- ${prevValue}`);
}
}
// Auto-override any settings from the URL
const url = new URL(window.location.href);
url.searchParams.forEach((value, key) => {
if (key in settings) (settings as any)[key] = parseSetting(key, value);
})
// 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];
if (url === '<auto>') {
const possibleBackend = new URL("./?api_updates=true", window.location.href)
await fetch(possibleBackend, {method: "HEAD"}).then((response) => {
if (response.ok && response.headers.get("Content-Type") === "text/event-stream") {
// Frontend served by the backend: default to this URL for updates
url = "dev+" + possibleBackend.href;
}
}).catch((error) => console.error("Failed to check for backend:", error));
if (url === '<auto>') { // Fallback to the default preload URL of localhost
url = "dev+http://localhost:32323";
}
}
settings.preload[i] = url;
}

View File

@@ -25,6 +25,7 @@ import {
mdiVectorLine,
mdiVectorRectangle
} from '@mdi/js'
// @ts-expect-error
import SvgIcon from '@jamescoyle/vue-icon';
import {BackSide, FrontSide} from "three/src/constants.js";
import {Box3} from "three/src/math/Box3.js";
@@ -34,6 +35,8 @@ import {Vector3} from "three/src/math/Vector3.js";
import type {MObject3D} from "../tools/Selection.vue";
import {toLineSegments} from "../misc/lines.js";
import {settings} from "../misc/settings.js"
import {currentSceneRotation} from "../viewer/lighting.ts";
import {Matrix4} from "three/src/math/Matrix4.js";
const props = defineProps<{
meshes: Array<Mesh>,
@@ -55,7 +58,11 @@ const clipPlaneY = ref(1);
const clipPlaneSwappedY = ref(false);
const clipPlaneZ = ref(1);
const clipPlaneSwappedZ = ref(false);
const edgeWidth = ref(settings.edgeWidth);
const edgeWidth = ref(0);
(async () => {
let s = await settings();
edgeWidth.value = s.edgeWidth;
})();
// Misc properties
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
@@ -111,7 +118,8 @@ function onWireframeChange(newWireframe: boolean) {
if (!scene || !sceneModel) return;
sceneModel.traverse((child: MObject3D) => {
if (child.userData[extrasNameKey] === modelName) {
if (child.material && child.material.wireframe !== newWireframe) {
let childIsFace = child.type == 'Mesh' || child.type == 'SkinnedMesh'
if (child.material && child.material.wireframe !== newWireframe && childIsFace) {
child.material.wireframe = newWireframe;
child.material.needsUpdate = true;
}
@@ -149,10 +157,11 @@ function onClipPlanesChange() {
let offsetX = bbox.min.x + clipPlaneX.value * (bbox.max.x - bbox.min.x);
let offsetY = bbox.min.y + clipPlaneY.value * (bbox.max.y - bbox.min.y);
let offsetZ = bbox.min.z + (1 - clipPlaneZ.value) * (bbox.max.z - bbox.min.z);
let rotSceneMatrix = new Matrix4().makeRotationY(currentSceneRotation);
let planes = [
new Plane(new Vector3(-1, 0, 0), offsetX),
new Plane(new Vector3(0, -1, 0), offsetY),
new Plane(new Vector3(0, 0, 1), -offsetZ),
new Plane(new Vector3(-1, 0, 0), offsetX).applyMatrix4(rotSceneMatrix),
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();
@@ -223,9 +232,10 @@ function onEdgeWidthChange(newEdgeWidth: number) {
line.userData.niceLine = line2;
// line.parent!.remove(line); // Keep it for better raycast and selection!
line2.userData.noHit = true;
line2.visible = enabledFeatures.value.includes(1);
edgeWidthChangeCleanup.push(() => {
line2.parent!.remove(line2);
line.visible = true;
line.visible = enabledFeatures.value.includes(1);
props.viewer!!.onElemReady((elem) => {
elem.removeEventListener('resize', () => resizeListener(elem));
});
@@ -327,7 +337,7 @@ if (props.viewer) onViewerReady(props.viewer); else watch((() => props.viewer) a
<template>
<v-expansion-panel :value="modelName">
<v-expansion-panel-title collapse-icon="hide-this-icon" expand-icon="hide-this-icon">
<v-expansion-panel-title>
<v-btn-toggle v-model="enabledFeatures" color="surface-light" multiple @click.stop>
<v-btn icon>
<v-tooltip activator="parent">Toggle Faces ({{ faceCount }})</v-tooltip>
@@ -426,10 +436,6 @@ if (props.viewer) onViewerReady(props.viewer); else watch((() => props.viewer) a
}
/* More compact accordions */
.v-expansion-panel {
margin-top: 0 !important;
}
.v-expansion-panel-title {
padding: 0;
}
@@ -440,11 +446,11 @@ if (props.viewer) onViewerReady(props.viewer); else watch((() => props.viewer) a
}
.v-btn {
--v-btn-height: 16px;
--v-btn-height: 12px;
}
.model-name {
width: 179px;
width: 172px;
font-size: 110%;
overflow-x: clip;
overflow-y: visible; /* HACK: bottom of text is lost otherwise (due to buggy -webkit-box bounds?) */
@@ -461,10 +467,6 @@ if (props.viewer) onViewerReady(props.viewer); else watch((() => props.viewer) a
padding: 0 !important;
}
.hide-this-icon {
display: none !important;
}
.mdi-checkbox-blank-outline { /* HACK: mdi is not fully imported, only required icons... */
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M19,5V19H5V5H19Z"/></svg>');
}

View File

@@ -7,11 +7,11 @@ import Model from "./Model.vue";
import {inject, ref, type Ref} from "vue";
const props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> | null }>();
const emit = defineEmits<{ remove: [string] }>()
const emit = defineEmits<{ removeModel: [string] }>()
let {sceneDocument} = inject<{ sceneDocument: Ref<Document> }>('sceneDocument')!!;
let expandedNames = ref<Array<string>>([]);
const expandedNames = ref<Array<string>>([]);
function meshesList(sceneDocument: Document): Array<Array<Mesh>> {
// Grouped by shared name
@@ -32,7 +32,7 @@ function meshName(mesh: Mesh) {
}
function onRemove(mesh: Mesh) {
emit('remove', meshName(mesh))
emit('removeModel', meshName(mesh))
}
function findModel(name: string) {
@@ -44,7 +44,7 @@ defineExpose({findModel})
<template>
<v-expansion-panels v-for="meshes in meshesList(sceneDocument)" :key="meshName(meshes[0])"
v-model="expandedNames" multiple>
v-model="expandedNames as any" multiple>
<model :meshes="meshes" :viewer="props.viewer" @remove="onRemove(meshes[0])"/>
</v-expansion-panels>
</template>
@@ -61,4 +61,4 @@ defineExpose({findModel})
.v-overlay--active > .v-overlay__content {
display: block !important; /* HACK: Fix buggy tooltips not showing? */
}
</style>
</style>

5
frontend/shims.d.ts vendored
View File

@@ -1,5 +0,0 @@
// Avoids typescript error when importing some files
declare module '@jamescoyle/vue-icon'
declare module 'three-orientation-gizmo/src/OrientationGizmo'
declare module 'three/examples/jsm/libs/draco/draco_decoder.js'
declare module 'three/examples/jsm/libs/draco/draco_encoder.js'

View File

@@ -1,12 +1,15 @@
<script lang="ts" setup>
import {onMounted, onUpdated, ref} from "vue";
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
// @ts-expect-error
import * as OrientationGizmoRaw from "three-orientation-gizmo/src/OrientationGizmo";
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
import {currentSceneRotation} from "../viewer/lighting.ts";
// Optimized minimal dependencies from three
import {Vector3} from "three/src/math/Vector3.js";
import {Matrix4} from "three/src/math/Matrix4.js";
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
import {Euler} from "three/src/math/Euler.js";
(globalThis as any).THREE = {Vector3, Matrix4} as any // HACK: Required for the gizmo to work
@@ -48,7 +51,7 @@ function createGizmo(expectedParent: HTMLElement, scene: ModelScene): HTMLElemen
axis.direction.y = -axis.direction.y;
axis.direction.z = -axis.direction.z;
}
wantedTheta = Math.atan2(axis.direction.x, axis.direction.z);
wantedTheta = Math.atan2(axis.direction.x, axis.direction.z) + currentSceneRotation;
wantedPhi = Math.asin(-axis.direction.y) + Math.PI / 2;
attempt++;
}
@@ -66,7 +69,12 @@ let gizmo: HTMLElement & { update: () => void }
function updateGizmo() {
if (gizmo.isConnected) {
// HACK: Update camera temporarily to match skybox rotation before updating the gizmo and go back
let prevRot = ((gizmo as any).camera).rotation.clone() as Euler;
let thetaMat = new Matrix4().makeRotationY(-currentSceneRotation);
((gizmo as any).camera).rotation.setFromRotationMatrix(thetaMat.multiply(new Matrix4().makeRotationFromEuler(prevRot)));
gizmo.update();
((gizmo as any).camera).rotation.set(prevRot.x, prevRot.y, prevRot.z);
requestIdleCallback(updateGizmo, {timeout: 250});
}
}

View File

@@ -0,0 +1,341 @@
<script setup lang="ts">
import {setupMonaco} from "./monaco.ts";
import {VueMonacoEditor} from '@guolao/vue-monaco-editor'
import {nextTick, onMounted, ref, shallowRef} from "vue";
import Loading from "../misc/Loading.vue";
import {newPyodideWorker} from "./pyodide-worker-api.ts";
import {mdiCircleOpacity, mdiClose, mdiContentSave, mdiFolderOpen, mdiPlay, mdiReload, mdiShare} from "@mdi/js";
import {VBtn, VCard, VCardText, VSlider, VSpacer, VToolbar, VToolbarTitle, VTooltip} from "vuetify/components";
// @ts-expect-error
import SvgIcon from '@jamescoyle/vue-icon';
import {version as pyodideVersion} from "pyodide";
import {gzip} from 'pako';
import {b66Encode} from "./b66.ts";
import {Base64} from 'js-base64'; // More compatible with binary data from python...
import {NetworkUpdateEvent, NetworkUpdateEventModel} from "../misc/network.ts";
import {settings} from "../misc/settings.ts";
const props = defineProps<{ initialCode: string }>();
const emit = defineEmits<{ close: [], updateModel: [NetworkUpdateEvent] }>()
// ============ LOAD MONACO EDITOR ============
setupMonaco() // Must be called before using the editor
const code = ref(props.initialCode); // TODO: Default code as input (and autorun!)
const outputText = ref(``);
function output(text: string) {
outputText.value += text; // Append to output
// Avoid too much output, keep it reasonable
let max_output = 10000; // 10k characters
if (outputText.value.length > max_output) {
outputText.value = outputText.value.slice(-max_output); // Keep only the last 10k characters
}
console.log(text); // Also log to console
nextTick(() => { // Scroll to bottom
const consoleElement = document.querySelector('.playground-console');
if (consoleElement) {
consoleElement.scrollTop = consoleElement.scrollHeight;
}
})
}
const MONACO_EDITOR_OPTIONS = {
automaticLayout: true,
formatOnType: true,
formatOnPaste: true,
}
const editorTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? `vs-dark` : `vs`
const editor = shallowRef()
const handleMount = (editorInstance: typeof VueMonacoEditor) => (editor.value = editorInstance)
const opacity = ref(0.9); // Opacity for the editor
// ============ LOAD PYODIDE (ASYNC) ============
let pyodideWorker: ReturnType<typeof newPyodideWorker> | null = (import.meta as any).hot?.data?.pyodideWorker || null;
const running = ref(true);
async function setupPyodide() {
running.value = true;
if (opacity.value == 0.0) opacity.value = 0.9; // User doesn't know how to show code again, reset after reopening
if (pyodideWorker === null) {
output("Creating new Pyodide worker...\n");
pyodideWorker = newPyodideWorker({
indexURL: `https://cdn.jsdelivr.net/pyodide/v${pyodideVersion}/full/`, // FIXME: Local deployment?
packages: ["micropip", "sqlite3"], // Faster load if done here
});
if ((import.meta as any).hot) (import.meta as any).hot.data.pyodideWorker = pyodideWorker
} else {
output("Reusing existing Pyodide instance...\n");
}
output("Preloading packages...\n");
await pyodideWorker.asyncRun(`import micropip, asyncio
micropip.set_index_urls(["https://yeicor.github.io/OCP.wasm", "https://pypi.org/simple"])
await (micropip.install("lib3mf"))
micropip.add_mock_package("py-lib3mf", "2.4.1", modules={"py_lib3mf": 'from lib3mf import *'})
await (micropip.install(["https://files.pythonhosted.org/packages/67/25/80be117f39ff5652a4fdbd761b061123c5425e379f4b0a5ece4353215d86/yacv_server-0.10.0a4-py3-none-any.whl"]))
from yacv_server import *
micropip.add_mock_package("ocp-vscode", "2.8.9", modules={"ocp_vscode": 'from yacv_server import *'})`, output, output); // Also import yacv_server and mock ocp_vscode here for faster custom code execution
running.value = false; // Indicate that Pyodide is ready
output("Pyodide worker initialized.\n");
}
async function runCode() {
if (pyodideWorker === null) {
output("Pyodide worker is not initialized. Please wait...\n");
return;
}
if (running.value) {
output("Pyodide is already running. Please wait...\n");
return;
}
output("Running code...\n");
try {
running.value = true;
await pyodideWorker.asyncRun(code.value, output, (msg: string) => {
// Detect models printed to console (since http server is not available in pyodide)
if (msg.startsWith(yacvServerModelPrefix)) {
const modelData = msg.slice(yacvServerModelPrefix.length);
onModelData(modelData);
} else {
output(msg); // Print other messages directly
}
});
} catch (e) {
output(`Error running initial code: ${e}\n`);
} finally {
running.value = false; // Indicate that Pyodide is ready
}
}
const yacvServerModelPrefix = "yacv_server://model/";
function onModelData(modelData: string) {
output(`Model data detected... ${modelData.length}B\n`);
// Decode the model data and emit the event for the interface to handle
// - Start by finding the end of the initial json object by looking for brackets.
let i = 0;
let openBrackets = 0;
for (; i < modelData.length; i++) {
if (modelData[i] === '{') openBrackets++;
else if (modelData[i] === '}') openBrackets--;
if (openBrackets === 0) break; // Found the end of the JSON object
}
if (openBrackets !== 0) throw `Error: Invalid model data received: ${modelData}\n`
const jsonData = modelData.slice(0, i + 1); // Extract the JSON part and parse it into the proper class
let modelMetadataRaw = JSON.parse(jsonData);
const modelMetadata: any = new NetworkUpdateEventModel(modelMetadataRaw.name, "", modelMetadataRaw.hash, modelMetadataRaw.is_remove)
// console.debug(`Model metadata:`, modelMetadata);
output(`Model metadata: ${JSON.stringify(modelMetadata)}\n`);
// - Now decode the rest of the model data which is a single base64 encoded glb file (or an empty string)
if (!modelMetadata.isRemove) {
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());
// - 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
}
// - Emit the event with the model metadata and URL
let networkUpdateEvent = new NetworkUpdateEvent([modelMetadata], () => {
});
emit('updateModel', networkUpdateEvent);
}
function resetWorker() {
if (pyodideWorker) {
pyodideWorker.terminate(); // Terminate existing worker
pyodideWorker = null; // Reset worker reference
}
outputText.value = ``; // Clear output text
setupPyodide(); // Reinitialize Pyodide
}
function shareLink() {
const baseUrl = window.location
const urlParams = new URLSearchParams(baseUrl.hash.slice(1)); // Keep all previous URL parameters
urlParams.set('pg_code', b66Encode(gzip(code.value, {level: 9}))); // Compress and encode the code
const shareUrl = `${baseUrl.origin}${baseUrl.pathname}${baseUrl.search}#${urlParams.toString()}`; // Prefer hash to GET (bigger limits)
output(`Share link ready: ${shareUrl}\n`)
if (!navigator.clipboard) {
output("Clipboard API not available. Please copy the link manually.\n");
return;
} else {
navigator.clipboard.writeText(shareUrl)
.then(() => output("Link copied to clipboard!\n"))
.catch(err => output(`Failed to copy link: ${err}\n`));
}
}
function saveSnapshot() {
throw new Error("Not implemented yet!"); // TODO: Implement snapshot saving
}
function loadSnapshot() {
throw new Error("Not implemented yet!"); // TODO: Implement snapshot loading
}
const reused = (import.meta as any).hot?.data?.pyodideWorker !== undefined;
(async () => {
const sett = await settings()
if (!reused) opacity.value = sett.pg_opacity_loading
await setupPyodide()
if (props.initialCode != "" && !reused) await runCode();
if (!reused) opacity.value = sett.pg_opacity_loaded
})()
// Add keyboard shortcuts
const editorRef = ref<HTMLElement | null>(null);
onMounted(() => {
if (editorRef.value) {
console.log(editorRef.value)
editorRef.value.addEventListener('keydown', (event: Event) => {
if (!(event instanceof KeyboardEvent)) return; // Ensure event is a KeyboardEvent
if (event.key === 'Enter' && event.ctrlKey) {
event.preventDefault(); // Prevent default behavior of Enter key
runCode(); // Run code on Ctrl+Enter
} else if (event.key === 'Escape') {
emit('close'); // Close on Escape key
}
});
}
});
</script>
<template>
<v-card class="popup-card"
:style="opacity == 0 ? `position: absolute; top: calc(-50vh + 24px); width: calc(100vw - 64px);` : ``">
<v-toolbar class="popup">
<v-toolbar-title style="flex: 0 1 auto">Playground</v-toolbar-title>
<v-spacer></v-spacer>
<span style="display: inline-flex; margin-right: 16px;">
<svg-icon :path="mdiCircleOpacity" type="mdi" style="height: 32px"></svg-icon>
<v-slider v-model="opacity" :max="1" :min="0" :step="0.1"
style="width: 100px; height: 32px">
</v-slider>
<v-tooltip activator="parent"
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 -->
<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 -->
<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()">
<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">
<svg-icon :path="mdiPlay" type="mdi"/>
<Loading v-if="running" style="position: absolute; top: -16%; left: -16%"/><!-- 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>
</v-btn>
</v-toolbar>
<v-card-text class="popup-card-text" :style="opacity == 0 ? `display: none` : ``">
<!-- Only show content if opacity is greater than 0 -->
<div class="playground-container">
<div class="playground-editor" ref="editorRef">
<VueMonacoEditor v-model:value="code" :theme="editorTheme" :options="MONACO_EDITOR_OPTIONS"
language="python" @mount="handleMount"/>
</div>
<div class="playground-console">
<h3>Console Output</h3>
<pre>{{ outputText }}</pre> <!-- Placeholder for console output -->
<Loading v-if="running"/>
</div>
</div>
</v-card-text>
</v-card>
</template>
<style scoped>
.popup-card {
background-color: #00000000; /* Transparent background */
}
.v-toolbar.popup > * {
overflow-x: auto;
}
.popup-card-text {
background-color: #1e1e1e; /* Matches the Monaco editor background */
opacity: v-bind(opacity);
}
.playground-container {
display: flex;
flex-direction: row;
}
.playground-editor {
flex: 1;
height: calc(100vh - 150px);
}
.playground-console {
flex: 0.5;
padding: 10px;
overflow-y: auto;
min-width: 100px;
height: calc(100vh - 150px);
}
.playground-console pre {
margin: 0;
white-space: pre-wrap;
word-break: break-all;
}
@media (min-height: 100vw) {
/* Adjust layout for vertical space */
.playground-container {
flex-direction: column;
}
.playground-editor {
flex: 1;
min-height: 60vh;
}
.playground-editor > * {
min-height: 60vh;
}
.playground-console {
max-height: calc(40vh - 150px);
}
}
/* TODO: Adjust more colors on bright mode */
</style>
<style>
/* https://stackoverflow.com/questions/47017753/monaco-editor-dynamically-resizable/71876526#71876526 */
.monaco-editor {
position: absolute !important;
}
</style>

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
import {defineModel, inject, ref, type ShallowRef, watch} from "vue";
import {inject, ref, type ShallowRef, watch} from "vue";
import {VBtn, VSelect, VTooltip} from "vuetify/lib/components/index.mjs";
// @ts-expect-error
import SvgIcon from '@jamescoyle/vue-icon';
import type {ModelViewerElement} from '@google/model-viewer';
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
@@ -28,7 +29,7 @@ let emit = defineEmits<{ findModel: [string] }>();
let {setDisableTap} = inject<{ setDisableTap: (arg0: boolean) => void }>('disableTap')!!;
let selectionEnabled = ref(false);
let selected = defineModel<Array<SelectionInfo>>({default: []});
let highlightNextSelection = ref([false, false]); // Second is whether selection was enabled before
let openNextSelection = ref([false, false]); // Second is whether selection was enabled before
let showBoundingBox = ref<Boolean>(false); // Enabled automatically on start
let showDistances = ref<Boolean>(true);
@@ -149,7 +150,7 @@ let mouseUpListener = (event: MouseEvent) => {
// Return the best hit
[0] as Intersection<MObject3D> | undefined;
if (!highlightNextSelection.value[0]) {
if (!openNextSelection.value[0]) {
// If we are selecting, toggle the selection or deselect all if no hit
let selInfo: SelectionInfo | null = null;
if (hit) selInfo = hitToSelectionInfo(hit);
@@ -171,7 +172,7 @@ let mouseUpListener = (event: MouseEvent) => {
// Otherwise, highlight the model that owns the hit
emit('findModel', hit.object.userData[extrasNameKey])
// And reset the selection mode
toggleHighlightNextSelection()
toggleOpenNextSelection()
}
scene.queueRender() // Force rerender of model-viewer
}
@@ -209,17 +210,17 @@ function toggleSelection() {
setDisableTap(selectionEnabled.value);
}
function toggleHighlightNextSelection() {
highlightNextSelection.value = [
!highlightNextSelection.value[0],
highlightNextSelection.value[0] ? highlightNextSelection.value[1] : selectionEnabled.value
function toggleOpenNextSelection() {
openNextSelection.value = [
!openNextSelection.value[0],
openNextSelection.value[0] ? openNextSelection.value[1] : selectionEnabled.value
];
if (highlightNextSelection.value[0]) {
if (openNextSelection.value[0]) {
// Reuse selection code to identify the model
if (!selectionEnabled.value) toggleSelection()
} else {
if (selectionEnabled.value !== highlightNextSelection.value[1]) toggleSelection()
highlightNextSelection.value = [false, false];
if (selectionEnabled.value !== openNextSelection.value[1]) toggleSelection()
openNextSelection.value = [false, false];
}
}
@@ -333,7 +334,7 @@ function updateBoundingBox() {
for (let i = 0; i < 2; i++) { // Find the 2nd closest one by running twice dropping the first
edge = axisEdges[0];
let edgeDist = Infinity;
let cameraPos: Vector3 = props.viewer?.scene.camera.position;
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]]);
@@ -427,6 +428,10 @@ defineExpose({deselect, updateBoundingBox, updateDistances});
// Add keyboard shortcuts
window.addEventListener('keydown', (event) => {
if ((event.target as any)?.tagName && ((event.target as any).tagName === 'INPUT' || (event.target as any).tagName === 'TEXTAREA')) {
// Ignore key events when an input is focused, except for text inputs
return;
}
if (event.key === 's') {
if (selectFilter.value == 'Any (S)') toggleSelection();
else {
@@ -455,8 +460,8 @@ window.addEventListener('keydown', (event) => {
toggleShowBoundingBox();
} else if (event.key === 'd') {
toggleShowDistances();
} else if (event.key === 'h') {
toggleHighlightNextSelection();
} else if (event.key === 'o') {
toggleOpenNextSelection();
}
});
</script>
@@ -474,8 +479,8 @@ window.addEventListener('keydown', (event) => {
variant="underlined"/>
</template>
</v-tooltip>
<v-btn :color="highlightNextSelection[0] ? 'surface-light' : ''" icon @click="toggleHighlightNextSelection">
<v-tooltip activator="parent">(H)ighlight the next clicked element in the models list</v-tooltip>
<v-btn :color="openNextSelection[0] ? 'surface-light' : ''" icon @click="toggleOpenNextSelection">
<v-tooltip activator="parent">(O)pen the next clicked element in the models list</v-tooltip>
<svg-icon :path="mdiFeatureSearch" type="mdi"/>
</v-btn>
<v-btn :color="showBoundingBox ? 'surface-light' : ''" icon @click="toggleShowBoundingBox">

View File

@@ -13,13 +13,26 @@ import {
import OrientationGizmo from "./OrientationGizmo.vue";
import type {PerspectiveCamera} from "three/src/cameras/PerspectiveCamera.js";
import {OrthographicCamera} from "three/src/cameras/OrthographicCamera.js";
import {mdiClose, mdiCrosshairsGps, mdiDownload, mdiGithub, mdiLicense, mdiProjector} from '@mdi/js'
import {
mdiClose,
mdiCrosshairsGps,
mdiDownload,
mdiGithub,
mdiLicense,
mdiLightbulb,
mdiProjector,
mdiScriptTextPlay
} from '@mdi/js'
// @ts-expect-error
import SvgIcon from '@jamescoyle/vue-icon';
import type {ModelViewerElement} from '@google/model-viewer';
import Loading from "../misc/Loading.vue";
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
import {defineAsyncComponent, ref, type Ref} from "vue";
import type {SelectionInfo} from "./selection";
import {settings} from "../misc/settings.ts";
import type {NetworkUpdateEvent} from "../misc/network.ts";
import IfNotSmallBuild from "../misc/IfNotSmallBuild.vue";
const SelectionComponent = defineAsyncComponent({
loader: () => import("./Selection.vue"),
@@ -34,9 +47,22 @@ const LicensesDialogContent = defineAsyncComponent({
delay: 0,
});
const PlaygroundDialogContent = defineAsyncComponent({
loader: () => import("./PlaygroundDialogContent.vue"),
loadingComponent: Loading,
delay: 0,
});
let props = defineProps<{ viewer: InstanceType<typeof ModelViewerWrapper> | null }>();
const emit = defineEmits<{ findModel: [string] }>()
const emit = defineEmits<{ findModel: [string], updateModel: [NetworkUpdateEvent] }>()
const sett = ref<any | null>(null);
const showPlaygroundDialog = ref(false);
(async () => {
sett.value = await settings();
showPlaygroundDialog.value = sett.value.pg_code != "";
})();
let selection: Ref<Array<SelectionInfo>> = ref([]);
let selectionFaceCount = () => selection.value.filter((s) => s.kind == 'face').length
@@ -114,10 +140,14 @@ function removeObjectSelections(objName: string) {
selectionComp.value?.updateDistances();
}
defineExpose({removeObjectSelections});
defineExpose({removeObjectSelections, openPlayground: () => showPlaygroundDialog.value = true});
// Add keyboard shortcuts
window.addEventListener('keydown', (event) => {
document.addEventListener('keydown', (event) => {
if ((event.target as any)?.tagName && ((event.target as any).tagName === 'INPUT' || (event.target as any).tagName === 'TEXTAREA')) {
// Ignore key events when an input is focused, except for text inputs
return;
}
if (event.key === 'p') toggleProjection();
else if (event.key === 'c') centerCamera();
else if (event.key === 'd') downloadSceneGlb();
@@ -139,6 +169,13 @@ window.addEventListener('keydown', (event) => {
<v-tooltip activator="parent">Re(c)enter Camera</v-tooltip>
<svg-icon :path="mdiCrosshairsGps" type="mdi"/>
</v-btn>
<span>
<v-tooltip activator="parent">To rotate the light hold shift and drag the mouse or use two fingers<br/>
Note that this breaks slightly clipping planes for now... (restart to fix)</v-tooltip>
<v-btn icon disabled style="background: black;">
<svg-icon :path="mdiLightbulb" type="mdi"/>
</v-btn>
</span>
<v-divider/>
<h5>Selection ({{ selectionFaceCount() }}F {{ selectionEdgeCount() }}E {{ selectionVertexCount() }}V)</h5>
<selection-component ref="selectionComp" v-model="selection" :viewer="props.viewer as any"
@@ -146,11 +183,26 @@ window.addEventListener('keydown', (event) => {
<v-divider/>
<v-spacer></v-spacer>
<h5>Extras</h5>
<v-dialog v-model="showPlaygroundDialog" persistent :scrim="false" attach="body">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" style="width: 100%">
<v-tooltip activator="parent">Open a python editor and build models directly in the browser!</v-tooltip>
<svg-icon :path="mdiScriptTextPlay" type="mdi"/>
&nbsp;Sandbox
</v-btn>
</template>
<template v-slot:default="{ isActive }">
<if-not-small-build>
<playground-dialog-content v-if="sett != null" :initial-code="sett.pg_code" @close="isActive.value = false"
@update-model="(event: NetworkUpdateEvent) => emit('updateModel', event)"/>
</if-not-small-build>
</template>
</v-dialog>
<v-btn icon @click="downloadSceneGlb">
<v-tooltip activator="parent">(D)ownload Scene</v-tooltip>
<svg-icon :path="mdiDownload" type="mdi"/>
</v-btn>
<v-dialog id="licenses-dialog" fullscreen>
<v-dialog>
<template v-slot:activator="{ props }">
<v-btn icon v-bind="props">
<v-tooltip activator="parent">Show Licenses</v-tooltip>
@@ -158,11 +210,10 @@ window.addEventListener('keydown', (event) => {
</v-btn>
</template>
<template v-slot:default="{ isActive }">
<v-card>
<v-toolbar>
<v-card style="height: 90vh">
<v-toolbar class="popup">
<v-toolbar-title>Licenses</v-toolbar-title>
<v-spacer>
</v-spacer>
<v-spacer></v-spacer>
<v-btn icon @click="isActive.value = false">
<svg-icon :path="mdiClose" type="mdi"/>
</v-btn>
@@ -199,4 +250,17 @@ window.addEventListener('keydown', (event) => {
h5 {
font-size: 14px;
}
.v-toolbar {
position: sticky !important;
top: 0;
}
.v-toolbar.popup {
height: 32px;
}
.v-toolbar.popup > div {
height: 32px !important;
}
</style>

54
frontend/tools/b66.ts Normal file
View File

@@ -0,0 +1,54 @@
// B66 encoding and decoding functions for compact url query parameter values. https://gist.github.com/danneu/6755394
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-_~";
export function b66Encode(data: Uint8Array): string {
let result = "";
let bits = 0;
let value = 0;
for (let byte of data) {
value = (value << 8) | byte;
bits += 8;
while (bits >= 6) {
bits -= 6;
result += alphabet[(value >> bits) & 0x3F];
}
}
if (bits > 0) {
result += alphabet[(value << (6 - bits)) & 0x3F];
}
return result;
}
export function b66Decode(encoded: string): Uint8Array {
let result = [];
let bits = 0;
let value = 0;
for (let char of encoded) {
const index = alphabet.indexOf(char);
if (index === -1) {
throw new Error(`Invalid character '${char}' in B66 encoded string.`);
}
value = (value << 6) | index;
bits += 6;
while (bits >= 8) {
bits -= 8;
result.push((value >> bits) & 0xFF);
}
}
if (bits > 0) {
// If there are leftover bits, they should not be present in a valid B66 encoding.
if (value << (8 - bits)) {
throw new Error("Invalid B66 encoding: leftover bits.");
}
}
return new Uint8Array(result);
}

35
frontend/tools/monaco.ts Normal file
View File

@@ -0,0 +1,35 @@
import {loader} from "@guolao/vue-monaco-editor"
import * as monaco from "monaco-editor"
//@ts-ignore
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"
//@ts-ignore
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"
//@ts-ignore
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"
//@ts-ignore
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) {
if (label === "json") {
return new jsonWorker()
}
if (label === "css" || label === "scss" || label === "less") {
return new cssWorker()
}
if (label === "html" || label === "handlebars" || label === "razor") {
return new htmlWorker()
}
if (label === "typescript" || label === "javascript") {
return new tsWorker()
}
return new editorWorker()
}
}
export function setupMonaco() {
loader.config({monaco})
}

View File

@@ -0,0 +1,31 @@
import type {loadPyodide} from "pyodide";
/** Simple API for the Pyodide worker. */
export function newPyodideWorker(initOpts: Parameters<typeof loadPyodide>[0]) {
let worker = new Worker(new URL('./pyodide-worker.ts', import.meta.url), {type: "module"});
worker.postMessage(initOpts);
return {
asyncRun: (code: String, stdout: (msg: string) => void, stderr: (msg: string) => void) => new Promise((resolve, reject) => {
worker.addEventListener("message", function listener(event) {
if (event.data?.stdout) {
stdout(event.data.stdout);
return;
}
if (event.data?.stderr) {
stderr(event.data.stderr);
return;
}
// Result or error.
worker.removeEventListener("message", listener);
if (event.data?.error) {
reject(event.data.error);
} else {
resolve(event.data?.result);
}
});
worker.postMessage(code);
}),
terminate: () => worker.terminate()
}
}

View File

@@ -0,0 +1,33 @@
import {loadPyodide, type PyodideInterface} from "pyodide";
let myLoadPyodide = (initOpts: Parameters<typeof loadPyodide>[0]) => loadPyodide({
...initOpts,
stdout: (msg) => self.postMessage({stdout: msg + "\n"}), // Add newline for better readability
stderr: (msg) => self.postMessage({stderr: msg + "\n"}), // Add newline for better readability
stdin: () => {
console.warn("Input requested by Python code, but stdin is not supported in this playground.");
return "";
},
});
let pyodideReadyPromise: Promise<PyodideInterface> | null = null;
self.onmessage = async (event: MessageEvent<any>) => {
if (!pyodideReadyPromise) { // First message is always the init message
// If we haven't loaded Pyodide yet, do so now.
// This is a singleton, so we only load it once.
pyodideReadyPromise = myLoadPyodide(event.data as Parameters<typeof loadPyodide>[0]);
return;
}
// All other messages are code to run.
let code = event.data as string;
// make sure loading is done
const pyodide = await pyodideReadyPromise;
// Now load any packages we need, run the code, and send the result back.
await pyodide.loadPackagesFromImports(code);
try {
self.postMessage({result: await pyodide.runPythonAsync(code)});
} catch (error: any) {
self.postMessage({error: error.message});
}
};

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import {settings} from "../misc/settings";
import {inject, onMounted, type Ref, ref, watch} from "vue";
import {inject, onUpdated, type Ref, ref, watch} from "vue";
import {$renderer, $scene} from "@google/model-viewer/lib/model-viewer-base";
import {$controls} from '@google/model-viewer/lib/features/controls.js';
import {type SmoothControls} from '@google/model-viewer/lib/three-components/SmoothControls';
@@ -11,6 +11,7 @@ import type {Renderer} from "@google/model-viewer/lib/three-components/Renderer"
import type {Vector3} from "three";
import {BufferGeometry, Mesh} from "three";
import {acceleratedRaycast, computeBoundsTree, disposeBoundsTree} from 'three-mesh-bvh';
import {setupLighting} from "./lighting.ts";
ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking
//@ts-ignore
@@ -27,14 +28,19 @@ const scene = ref<ModelScene | null>(null);
const renderer = ref<Renderer | null>(null);
const controls = ref<SmoothControls | null>(null);
const sett = ref<any | null>(null);
(async () => sett.value = await settings())();
let lastCameraTargetPosition: Vector3 | undefined = undefined;
let lastCameraZoom: number | undefined = undefined;
let lastCameraUrl = props.src.toString();
onMounted(() => {
if (!elem.value) return;
let initialized = false
onUpdated(() => {
if (!elem.value) return; // Not ready yet
if (initialized) return; // Already initialized
initialized = true;
elem.value.addEventListener('before-render', () => {
if (!elem.value) return;
if (!elem.value) return
// Extract internals of model-viewer in order to hack unsupported features
scene.value = elem.value[$scene] as ModelScene;
renderer.value = elem.value[$renderer] as Renderer;
@@ -62,6 +68,7 @@ onMounted(() => {
});
elem.value.addEventListener('camera-change', onCameraChange);
elem.value.addEventListener('progress', (ev) => onProgress((ev as any).detail.totalProgress));
setupLighting(elem.value);
});
function onCameraChange() {
@@ -207,12 +214,12 @@ watch(disableTap, (newDisableTap) => {
<template>
<!-- The main 3D model viewer -->
<model-viewer ref="elem" :ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :autoplay="settings.autoplay"
:environment-image="settings.background" :exposure="settings.exposure"
:orbit-sensitivity="settings.orbitSensitivity" :pan-sensitivity="settings.panSensitivity"
:poster="poster" :shadow-intensity="settings.shadowIntensity" :skybox-image="settings.background"
:src="props.src" :zoom-sensitivity="settings.zoomSensitivity" alt="The 3D model(s)" camera-controls
camera-orbit="30deg 75deg auto" interaction-prompt="none" max-camera-orbit="Infinity 180deg auto"
<model-viewer ref="elem" v-if="sett != null" :ar="sett.arModes.length > 0" :ar-modes="sett.arModes"
:environment-image="sett.environment" :exposure="sett.exposure" :autoplay="sett.autoplay"
:orbit-sensitivity="sett.orbitSensitivity" :pan-sensitivity="sett.panSensitivity"
:poster="poster" :shadow-intensity="sett.shadowIntensity" :skybox-image="sett.skybox"
:src="props.src" :zoom-sensitivity="sett.zoomSensitivity" alt="The 3D model(s)" camera-controls
camera-orbit="45deg 45deg auto" interaction-prompt="none" max-camera-orbit="Infinity 180deg auto"
min-camera-orbit="-Infinity 0deg 5%" style="width: 100%; height: 100%">
<slot></slot>
<!-- Add a progress bar to the top of the model viewer -->
@@ -297,4 +304,4 @@ watch(disableTap, (newDisableTap) => {
float: left;
transition: width 0.3s;
}
</style>
</style>

View File

@@ -0,0 +1,76 @@
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;
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();
}
modelViewer.addEventListener('mousedown', (event) => {
panning = event.metaKey || event.shiftKey;
if (!panning)
return;
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;
lastX = 0.5 * (targetTouches[0].clientX + targetTouches[1].clientX);
startPan();
}, true);
document.addEventListener('mousemove', (event) => {
if (!panning)
return;
updatePan(event.clientX);
event.stopPropagation();
}, true);
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);
document.addEventListener('mouseup', (event) => {
panning = false;
}, true);
modelViewer.addEventListener('touchend', (event) => {
panning = false;
}, true);
}

View File

@@ -1,6 +1,6 @@
{
"name": "yet-another-cad-viewer",
"version": "0.9.2",
"version": "0.10.0-alpha.6",
"description": "",
"license": "MIT",
"private": true,
@@ -19,30 +19,36 @@
"@gltf-transform/extensions": "^4.1.0",
"@gltf-transform/functions": "^4.1.0",
"@google/model-viewer": "^4.0.0",
"@guolao/vue-monaco-editor": "^1.5.5",
"@jamescoyle/vue-icon": "^0.1.2",
"@mdi/js": "^7.4.47",
"@mdi/svg": "^7.4.47",
"three": "^0.169.0",
"three-mesh-bvh": "^0.8.2",
"three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo",
"vue": "^3.5.12",
"js-base64": "^3.7.7",
"monaco-editor": "^0.52.2",
"pako": "^2.1.0",
"pyodide": "^0.28.0",
"three": "^0.178.0",
"three-mesh-bvh": "^0.9.0",
"three-orientation-gizmo": "git+https://github.com/jrj2211/three-orientation-gizmo.git",
"vue": "^3.5.13",
"vuetify": "^3.7.4"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
"@types/node": "^22.9.0",
"@types/three": "^0.169.0",
"@vitejs/plugin-vue": "^5.1.4",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"@vue/tsconfig": "^0.5.1",
"@types/node": "^22.9.3",
"@types/pako": "^2.0.3",
"@types/three": "^0.178.0",
"@vitejs/plugin-vue": "^6.0.0",
"@vitejs/plugin-vue-jsx": "^5.0.0",
"@vue/tsconfig": "^0.7.0",
"buffer": "^5.5.0||^6.0.0",
"commander": "^12.0.0",
"generate-license-file": "^3.6.0",
"npm-run-all2": "^7.0.1",
"commander": "^14.0.0",
"generate-license-file": "^4.0.0",
"npm-run-all2": "^8.0.0",
"terser": "^5.36.0",
"typescript": "~5.6.3",
"vite": "^5.4.10",
"vite-plugin-top-level-await": "^1.4.4",
"vue-tsc": "^2.1.10"
"typescript": "~5.8.0",
"vite": "^7.0.0",
"vite-plugin-static-copy": "^3.1.1",
"vue-tsc": "^3.0.0"
}
}

1488
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,34 @@
[build-system]
requires = ["poetry-core==2.1.3", "taskipy==1.14.1"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "yacv-server"
version = "0.9.2"
version = "0.10.0-alpha.6"
description = "Yet Another CAD Viewer (server)"
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
license = "MIT"
readme = "README.md"
include = [
{ path = 'yacv_server/frontend/*', format = 'wheel' },
{ path = 'yacv_server/frontend/*', format = 'sdist' },
{ path = 'yacv_server/frontend/**/*', format = 'wheel' },
{ path = 'yacv_server/frontend/**/*', format = 'sdist' },
]
[tool.taskipy.tasks]
build = "task build_frontend && task build_backend"
build_frontend = "rm -rf yacv_server/frontend || true && yarn install && YACV_SMALL_BUILD=true yarn build --outDir yacv_server/frontend"
build_backend = "poetry build --format wheel"
[tool.poetry.dependencies]
python = "^3.9"
python = ">=3.12,<3.13" # Due to vtk transitive dependency of build123d -> cadquery-ocp -> vtk
# CAD
build123d = ">=0.5,<0.8"
build123d = ">=0.9,<0.10"
# Misc
pygltflib = "^1.16.2"
pillow = ">=10.2,<12.0"
poetry-core = "==2.1.3"
[tool.poetry.build]
generate-setup-file = false
script = "build.py"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.group.dev.dependencies]
taskipy = "^1.14.1"

View File

@@ -1,11 +1,17 @@
import {fileURLToPath, URL} from 'node:url'
import {defineConfig} from 'vite'
// @ts-ignore
import vue from '@vitejs/plugin-vue'
// @ts-ignore
import vueJsx from '@vitejs/plugin-vue-jsx'
import {name, version} from './package.json'
import {execSync} from 'child_process'
import topLevelAwait from "vite-plugin-top-level-await";
import {viteStaticCopy} from "vite-plugin-static-copy";
import {dirname, join} from "path";
import {version as pyodideVersion} from "pyodide";
let wantsSmallBuild = process.env.YACV_SMALL_BUILD == "true";
// https://vitejs.dev/config/
export default defineConfig({
@@ -14,18 +20,14 @@ export default defineConfig({
vue({
template: {
compilerOptions: {
isCustomElement: tag => tag == 'model-viewer'
isCustomElement: (tag: string) => tag == 'model-viewer'
}
}
}),
vueJsx(),
topLevelAwait({
// The export name of top-level await promise for each chunk module
promiseExportName: "__tla",
// The function to generate import names of top-level await promise in each chunk module
promiseImportName: i => `__tla_${i}`
}),
viteStaticCopyPyodide(),
],
optimizeDeps: {exclude: ["pyodide"]},
resolve: {
alias: {
// @ts-ignore
@@ -35,13 +37,51 @@ export default defineConfig({
build: {
assetsDir: '.', // Support deploying to a subdirectory using relative URLs
cssCodeSplit: false, // Small enough to inline
chunkSizeWarningLimit: 550, // Three.js is big. Draco is even bigger but not likely to be used.
chunkSizeWarningLimit: 1024, // KB. Three.js is big. Draco is even bigger but not likely to be used.
sourcemap: true, // For debugging production
rollupOptions: {
output: {
experimentalMinChunkSize: 512000, // 512KB (avoid too many small chunks)
},
external: wantsSmallBuild ? [
// Exclude some large optional dependencies if small build is requested (for embedding in python package)
"pyodide",
/.*\/pyodide-worker.*/,
"monaco-editor",
/monaco-editor\/.*/,
"@guolao/vue-monaco-editor",
/three\/examples\/jsm\/libs\/draco\/draco_(en|de)coder\.js/,
] : [],
},
},
worker: {
format: 'es', // Use ES modules for workers (IIFE is not supported with code-splitting)
},
define: {
__APP_NAME__: JSON.stringify(name),
__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),
}
})
function viteStaticCopyPyodide() {
const PYODIDE_EXCLUDE = [
"!**/*.{md,html}",
"!**/*.d.ts",
"!**/*.whl",
"!**/node_modules",
];
// @ts-ignore
const pyodideDir = dirname(fileURLToPath(import.meta.resolve("pyodide")));
return viteStaticCopy({
targets: wantsSmallBuild ? [] : [
{
src: [join(pyodideDir, "*")].concat(PYODIDE_EXCLUDE),
dest: "pyodide-v" + pyodideVersion, // It would be better to use hashed names instead of folder...
},
],
});
}

View File

@@ -10,7 +10,7 @@ from OCP.TopExp import TopExp
from OCP.TopLoc import TopLoc_Location
from OCP.TopTools import TopTools_IndexedMapOfShape
from OCP.TopoDS import TopoDS_Shape
from build123d import Compound, Shape, Color
from build123d import Compound, Color
from yacv_server.gltf import GLTFMgr
@@ -77,8 +77,9 @@ def get_shape(obj: CADLike, error: bool = True) -> Optional[CADCoreLike]:
if len(shapes_raw_filtered) > 0: # Continue if we found at least one shape
# Sorting is required to improve hashcode consistency
shapes_raw_filtered_sorted = sorted(shapes_raw_filtered, key=lambda x: _hashcode(x))
# Build a single compound shape
shapes_bd = [Shape(shape) for shape in shapes_raw_filtered_sorted if shape is not None]
# Build a single compound shape (skip locations/axes here, they can't be in a Compound)
shapes_bd = [Compound(shape) for shape in shapes_raw_filtered_sorted if
shape is not None and not isinstance(shape, TopLoc_Location)]
return get_shape(Compound(shapes_bd), error)
except TypeError:
pass
@@ -121,11 +122,11 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
hasher = hashlib.md5()
hasher.update(source)
name = 'image_' + hasher.hexdigest()
format: str
_format: str
if save_mime == 'image/jpeg':
format = 'JPEG'
_format = 'JPEG'
elif save_mime == 'image/png':
format = 'PNG'
_format = 'PNG'
else:
raise ValueError(f'Unsupported save MIME type (for GLTF files): {save_mime}')
@@ -154,7 +155,7 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
img = img.resize((new_width, new_height))
# Save the image to a buffer
img.save(img_buf, format=format)
img.save(img_buf, format=_format)
img_buf = img_buf.getvalue()
# Convert coordinates system as a last step (gltf is Y-up instead of Z-up)
@@ -168,7 +169,7 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
vert(plane.origin + plane.x_dir * width / 2 + plane.y_dir * height / 2),
vert(plane.origin + plane.x_dir * width / 2 - plane.y_dir * height / 2),
vert(plane.origin - plane.x_dir * width / 2 - plane.y_dir * height / 2),
], [
], [vert(plane.z_dir)] * 4, [
(0, 2, 1),
(0, 3, 2),
], [

View File

@@ -4,9 +4,6 @@ import numpy as np
from build123d import Location, Plane, Vector
from pygltflib import *
_checkerboard_image_bytes = base64.decodebytes(
b'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVQI12N49OjR////Gf'
b'/////48WMATwULS8tcyj8AAAAASUVORK5CYII=')
def get_version() -> str:
try:
@@ -24,6 +21,7 @@ class GLTFMgr:
# - Face data
face_indices: List[int] # 3 indices per triangle
face_positions: List[float] # x, y, z
face_normals: List[float] # x, y, z
face_tex_coords: List[float] # u, v
face_colors: List[float] # r, g, b, a
image: Optional[Tuple[bytes, str]] # image/png
@@ -36,7 +34,7 @@ class GLTFMgr:
vertex_positions: List[float] # x, y, z
vertex_colors: List[float] # r, g, b, a
def __init__(self, image: Optional[Tuple[bytes, str]] = (_checkerboard_image_bytes, 'image/png')):
def __init__(self, image: Optional[Tuple[bytes, str]] = None):
self.gltf = GLTF2(
asset=Asset(generator=f"yacv_server@{get_version()}"),
scene=0,
@@ -54,6 +52,7 @@ class GLTFMgr:
)
self.face_indices = []
self.face_positions = []
self.face_normals = []
self.face_tex_coords = []
self.face_colors = []
self.image = image
@@ -76,24 +75,23 @@ class GLTFMgr:
def _vertices_primitive(self) -> Primitive:
return [p for p in self.gltf.meshes[0].primitives if p.mode == POINTS][0]
def add_face(self, vertices_raw: List[Vector], indices_raw: List[Tuple[int, int, int]],
tex_coord_raw: List[Tuple[float, float]], color: Optional[Tuple[float, float, float, float]] = None):
def add_face(self, vertices_raw: List[Vector], normals: List[Vector], indices_raw: List[Tuple[int, int, int]],
tex_coord_raw: List[Tuple[float, float]], color: Tuple[float, float, float, float]):
"""Add a face to the GLTF mesh"""
if color is None: color = (1.0, 0.75, 0.0, 1.0)
# assert len(vertices_raw) == len(tex_coord_raw), f"Vertices and texture coordinates have different lengths"
# assert min([i for t in indices_raw for i in t]) == 0, f"Face indices start at {min(indices_raw)}"
# assert max([e for t in indices_raw for e in t]) < len(vertices_raw), f"Indices have non-existing vertices"
base_index = len(self.face_positions) // 3 # All the new indices reference the new vertices
self.face_indices.extend([base_index + i for t in indices_raw for i in t])
self.face_positions.extend([v for t in vertices_raw for v in t])
self.face_normals.extend([n for t in normals for n in t])
self.face_tex_coords.extend([c for t in tex_coord_raw for c in t])
self.face_colors.extend([col for _ in range(len(vertices_raw)) for col in color])
self._faces_primitive.extras["face_triangles_end"].append(len(self.face_indices))
def add_edge(self, vertices_raw: List[Tuple[Tuple[float, float, float], Tuple[float, float, float]]],
color: Optional[Tuple[float, float, float, float]] = None):
color: Tuple[float, float, float, float]):
"""Add an edge to the GLTF mesh"""
if color is None: color = (0.1, 0.1, 1.0, 1.0)
vertices_flat = [v for t in vertices_raw for v in t] # Line from 0 to 1, 2 to 3, 4 to 5, etc.
base_index = len(self.edge_positions) // 3
self.edge_indices.extend([base_index + i for i in range(len(vertices_flat))])
@@ -101,9 +99,8 @@ class GLTFMgr:
self.edge_colors.extend([col for _ in range(len(vertices_flat)) for col in color])
self._edges_primitive.extras["edge_points_end"].append(len(self.edge_indices))
def add_vertex(self, vertex: Tuple[float, float, float], color: Optional[Tuple[float, float, float, float]] = None):
def add_vertex(self, vertex: Tuple[float, float, float], color: Tuple[float, float, float, float]):
"""Add a vertex to the GLTF mesh"""
if color is None: color = (0.1, 0.1, 0.1, 1.0)
base_index = len(self.vertex_positions) // 3
self.vertex_indices.append(base_index)
self.vertex_positions.extend(vertex)
@@ -117,10 +114,11 @@ class GLTFMgr:
return v.X, v.Y, v.Z
# Add 1 origin vertex and 3 edges with custom colors to identify the X, Y and Z axis
self.add_vertex(vert(pl.origin))
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.x_dir))], color=(0.97, 0.24, 0.24, 1))
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.y_dir))], color=(0.42, 0.8, 0.15, 1))
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.z_dir))], color=(0.09, 0.55, 0.94, 1))
# The colors are hardcoded. You can add vertices and edges manually to change them.
self.add_vertex(vert(pl.origin), color=(0.1, 0.1, 0.1, 1.0))
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.x_dir))], color=(0.97, 0.24, 0.24, 1.0))
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.y_dir))], color=(0.42, 0.8, 0.15, 1.0))
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.z_dir))], color=(0.09, 0.55, 0.94, 1.0))
def build(self) -> GLTF2:
"""Merge the intermediate data into the GLTF object and return it"""
@@ -131,6 +129,8 @@ class GLTFMgr:
buffers_list.append(_gen_buffer_metadata(self.face_indices, 1))
self._faces_primitive.attributes.POSITION = len(buffers_list)
buffers_list.append(_gen_buffer_metadata(self.face_positions, 3))
self._faces_primitive.attributes.NORMAL = len(buffers_list)
buffers_list.append(_gen_buffer_metadata(self.face_normals, 3))
self._faces_primitive.attributes.TEXCOORD_0 = len(buffers_list)
buffers_list.append(_gen_buffer_metadata(self.face_tex_coords, 2))
self._faces_primitive.attributes.COLOR_0 = len(buffers_list)
@@ -169,6 +169,7 @@ class GLTFMgr:
self.gltf.images = [Image(bufferView=len(buffers_list), mimeType=self.image[1])]
self.gltf.textures = [Texture(source=0, sampler=0)]
self.gltf.samplers = [Sampler(magFilter=NEAREST)]
# noinspection PyPep8Naming
self.gltf.materials[0].pbrMetallicRoughness.baseColorTexture = TextureInfo(index=0)
buffers_list.append((Accessor(), BufferView(), self.image[0]))

View File

@@ -6,24 +6,30 @@ from build123d import *
ASSETS_DIR = os.getenv('ASSETS_DIR', os.path.join(os.path.dirname(__file__), '..', 'assets'))
def build_logo(text: bool = True) -> Dict[str, Union[Part, Location, str]]:
def build_logo(text: bool = True) -> Dict[str, Union[Compound, Location, str]]:
"""Builds the CAD part of the logo"""
with BuildPart(Plane.XY.offset(50)) as logo_obj:
Box(22, 40, 30)
fillet(edges().filter_by(Axis.Y).group_by(Axis.Z)[-1], 10)
offset(solid(), 2, openings=faces().group_by(Axis.Z)[0] + faces().filter_by(Plane.XZ))
if text:
text_at_plane = Plane.YZ
text_at_plane.origin = faces().group_by(Axis.X)[-1].face().center()
with BuildSketch(text_at_plane.location):
with BuildSketch(Plane.YZ.move(Pos(faces().group_by(Axis.X)[-1].face().center()))):
Text('Yet Another\nCAD Viewer', 6, font_path='/usr/share/fonts/TTF/Hack-Regular.ttf')
extrude(amount=1)
logo_face_curved_front = faces().filter_by(GeomType.CYLINDER).group_by(Axis.X)[-1].face()
# Highlight text edges with a custom color
to_highlight = logo_obj.edges().group_by(Axis.X)[-1]
logo_obj_hl = Compound(to_highlight).translate((1e-3, 0, 0)) # To avoid z-fighting
logo_obj_hl.color = (0, 0.3, 0.3, 1)
# Highlight face with custom texture
logo_face_curved_front.yacv_texture = \
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVQI12N49OjR////Gf' \
'/////48WMATwULS8tcyj8AAAAASUVORK5CYII='
logo_face_curved_front.color = (0, 0.5, 0.0, 1)
logo_obj = Compound(logo_obj.faces() - ShapeList([logo_face_curved_front])) # Remove face from the main object
# Add a logo image to the CAD part
logo_img_location = logo_obj.faces().group_by(Axis.X)[0].face().center_location
logo_img_location *= Location((0, 0, 4e-2), (0, 0, 90)) # Avoid overlapping and adjust placement
@@ -33,7 +39,8 @@ def build_logo(text: bool = True) -> Dict[str, Union[Part, Location, str]]:
# Add an animated fox to the CAD part
fox_glb_bytes = open(os.path.join(ASSETS_DIR, 'fox.glb'), 'rb').read()
return {'fox': fox_glb_bytes, 'logo': logo_obj, 'logo_hl': logo_obj_hl, 'location': logo_img_location, img_name: img_glb_bytes}
return {'fox': fox_glb_bytes, 'logo': logo_obj, 'logo_hl': logo_obj_hl, 'logo_hl_tex': logo_face_curved_front,
'location': logo_img_location, img_name: img_glb_bytes}
if __name__ == "__main__":

View File

@@ -1,6 +0,0 @@
from os import system
if __name__ == '__main__':
# Just a reminder that a hot-reloading server can be started with the following command:
# Need to disable auto-start to avoid conflicts with the hot-reloading server
system('YACV_DISABLE_SERVER=true aiohttp-devtools runserver __init__.py --port 32323 --app-factory _get_app')

View File

@@ -138,3 +138,4 @@ class HTTPHandler(SimpleHTTPRequestHandler):
self.send_header('E-Tag', f'"{_hash}"')
self.end_headers()
self.wfile.write(exported_glb)
return None

View File

@@ -1,8 +1,6 @@
import queue
import queue
import threading
from typing import List, TypeVar, \
Generic, Generator
from typing import List, TypeVar, Generic, Generator
from yacv_server.mylogger import logger
@@ -56,9 +54,9 @@ class BufferedPubSub(Generic[T]):
self._subscribers.remove(q)
logger.debug(f"Unsubscribed from %s (%d subscribers)", self, len(self._subscribers))
def subscribe(self, include_buffered: bool = True, include_future: bool = True, yield_timeout: float = 0.0) -> \
Generator[T, None, None]:
"""Subscribes to events as an generator that yields events and automatically unsubscribes"""
def subscribe(self, include_buffered: bool = True, include_future: bool = True,
yield_timeout: float | None = 0.0) -> Generator[T, None, None]:
"""Subscribes to events as a generator that yields events and automatically unsubscribes"""
q = self._subscribe(include_buffered, include_future)
try:
while True:

View File

@@ -3,9 +3,11 @@ from typing import List, Dict, Tuple, Optional
from OCP.BRep import BRep_Tool
from OCP.BRepAdaptor import BRepAdaptor_Curve
from OCP.GCPnts import GCPnts_TangentialDeflection
from OCP.BRepLib import BRepLib_ToolTriangulatedShape
from OCP.TopAbs import TopAbs_Orientation
from OCP.TopLoc import TopLoc_Location
from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape, TopoDS_Vertex
from build123d import Shape, Vertex, Face, Location
from build123d import Vertex, Face, Location, Compound, Vector
from pygltflib import GLTF2
from yacv_server.cad import CADCoreLike, ColorTuple
@@ -14,14 +16,9 @@ from yacv_server.mylogger import logger
def tessellate(
cad_like: CADCoreLike,
tolerance: float = 0.1,
angular_tolerance: float = 0.1,
faces: bool = True,
edges: bool = True,
vertices: bool = True,
obj_color: Optional[ColorTuple] = None,
texture: Optional[Tuple[bytes, str]] = None,
cad_like: CADCoreLike, color_faces: ColorTuple, color_edges: ColorTuple, color_vertices: ColorTuple,
color_obj: Optional[ColorTuple] = None, tolerance: float = 0.1, angular_tolerance: float = 0.1,
faces: bool = True, edges: bool = True, vertices: bool = True, texture: Optional[Tuple[bytes, str]] = None,
) -> GLTF2:
"""Tessellate a whole shape into a list of triangle vertices and a list of triangle indices."""
if texture is None:
@@ -33,31 +30,35 @@ def tessellate(
mgr.add_location(Location(cad_like))
elif isinstance(cad_like, TopoDS_Shape):
shape = Shape(cad_like)
shape = Compound(cad_like)
# Perform tessellation tasks
edge_to_faces: Dict[str, List[TopoDS_Face]] = {}
vertex_to_faces: Dict[str, List[TopoDS_Face]] = {}
if faces:
if faces and hasattr(shape, 'faces'):
shape_faces = shape.faces()
for face in shape_faces:
_tessellate_face(mgr, face.wrapped, tolerance, angular_tolerance, obj_color)
_tessellate_face(mgr, face.wrapped, color_obj or color_faces, tolerance, angular_tolerance)
if edges:
for edge in face.edges():
edge_to_faces[edge.wrapped] = edge_to_faces.get(edge.wrapped, []) + [face.wrapped]
if vertices:
for vertex in face.vertices():
vertex_to_faces[vertex.wrapped] = vertex_to_faces.get(vertex.wrapped, []) + [face.wrapped]
if len(shape_faces) > 0: obj_color = None # Don't color edges/vertices if faces are colored
if edges:
if len(shape_faces) > 0: color_obj = None # Don't color edges/vertices if faces are colored
if edges and hasattr(shape, 'edges'):
shape_edges = shape.edges()
for edge in shape_edges:
_tessellate_edge(mgr, edge.wrapped, edge_to_faces.get(edge.wrapped, []), angular_tolerance,
angular_tolerance, obj_color)
if len(shape_edges) > 0: obj_color = None # Don't color vertices if edges are colored
if vertices:
_tessellate_edge(mgr, edge.wrapped, edge_to_faces.get(edge.wrapped, []), color_obj or color_edges,
angular_tolerance, angular_tolerance)
if len(shape_edges) > 0: color_obj = None # Don't color vertices if edges are colored
if vertices and hasattr(shape, 'vertices'):
for vertex in shape.vertices():
_tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(vertex.wrapped, []), obj_color)
_tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(vertex.wrapped, []),
color_obj or color_vertices)
else:
raise TypeError(f"Unsupported type: {type(cad_like)}: {cad_like}")
return mgr.build()
@@ -65,18 +66,27 @@ def tessellate(
def _tessellate_face(
mgr: GLTFMgr,
ocp_face: TopoDS_Face,
color: ColorTuple,
tolerance: float = 1e-3,
angular_tolerance: float = 0.1,
color: Optional[ColorTuple] = None,
):
face = Shape(ocp_face)
face = Compound(ocp_face)
# face.mesh(tolerance, angular_tolerance)
tri_mesh = face.tessellate(tolerance, angular_tolerance)
# noinspection PyArgumentList
poly = BRep_Tool.Triangulation_s(face.wrapped, TopLoc_Location())
if poly is None:
logger.warn("No triangulation found for face")
return GLTF2()
# Get the normal for each vertex (for smooth instead of flat shading!)
BRepLib_ToolTriangulatedShape.ComputeNormals_s(face.wrapped, poly)
reversed_face = face.wrapped.Orientation() == TopAbs_Orientation.TopAbs_REVERSED
normals = [
-Vector(v) if reversed_face else Vector(v)
for v in (poly.Normal(i) for i in range(1, poly.NbNodes() + 1))
]
# Get UV of each face from the parameters
uv = [
(v.X(), v.Y())
@@ -85,7 +95,8 @@ def _tessellate_face(
vertices = tri_mesh[0]
indices = tri_mesh[1]
mgr.add_face(vertices, indices, uv, color)
mgr.add_face(vertices, normals, indices, uv, color)
return None
def _push_point(v: Tuple[float, float, float], faces: List[TopoDS_Face]) -> Tuple[float, float, float]:
@@ -108,9 +119,9 @@ def _tessellate_edge(
mgr: GLTFMgr,
ocp_edge: TopoDS_Edge,
faces: List[TopoDS_Face],
color: ColorTuple,
angular_deflection: float = 0.1,
curvature_deflection: float = 0.1,
color: Optional[ColorTuple] = None,
):
# Use a curve discretizer to get the vertices
curve = BRepAdaptor_Curve(ocp_edge)
@@ -131,9 +142,6 @@ def _tessellate_edge(
mgr.add_edge(vertices, color)
def _tessellate_vertex(mgr: GLTFMgr, ocp_vertex: TopoDS_Vertex, faces: List[TopoDS_Face],
color: Optional[ColorTuple] = None):
def _tessellate_vertex(mgr: GLTFMgr, ocp_vertex: TopoDS_Vertex, faces: List[TopoDS_Face], color: ColorTuple):
c = Vertex(ocp_vertex).center()
mgr.add_vertex(_push_point((c.X, c.Y, c.Z), faces), color)

View File

@@ -8,19 +8,20 @@ import sys
import threading
import time
from dataclasses import dataclass
from enum import Enum, auto
from http.server import ThreadingHTTPServer
from io import BytesIO
from threading import Thread
from typing import Optional, Dict, Union, Callable, List, Tuple
from OCP.TopLoc import TopLoc_Location
from OCP.TopoDS import TopoDS_Shape
# noinspection PyProtectedMember
from build123d import Shape, Axis, Location, Vector, Color
from dataclasses_json import dataclass_json
from PIL import Image
from io import BytesIO
# noinspection PyProtectedMember
from build123d import Shape, Axis, Location, Vector
from dataclasses_json import dataclass_json
from yacv_server.cad import _hashcode, ColorTuple, get_color
from yacv_server.cad import _hashcode, get_color, ColorTuple
from yacv_server.cad import get_shape, grab_all_cad, CADCoreLike, CADLike
from yacv_server.gltf import get_version
from yacv_server.myhttp import HTTPHandler
@@ -64,10 +65,20 @@ class UpdatesApiFullData(UpdatesApiData):
return super().to_json()
class YACVProtocol(Enum):
"""Enum of communication protocols supported by the server"""
HTTP = auto()
"""The recommended protocol for any platform that can run a web server."""
STDERR = auto()
"""Prints the updates one by one to stderr (first metadata, then base64 of glb file) using a special prefix. Required for Pyodide support."""
class YACV:
"""The main yacv_server class, which manages the web server and the CAD objects."""
# Startup
protocol: YACVProtocol
"""The protocol used by the server. Defaults to HTTP, but can be set to STDERR for Pyodide support."""
server_thread: Optional[Thread]
"""The main thread running the server (will spawn other threads for each request)"""
server: Optional[ThreadingHTTPServer]
@@ -93,13 +104,44 @@ class YACV:
texture: Optional[Tuple[bytes, str]]
"""Default texture to use for model faces, in (data, mimetype) format.
If left as None, a default checkerboard texture will be used.
If left as None, no texture will be used.
It can be set with the YACV_BASE_TEXTURE=<uri> and overriden by `show(..., texture="<uri>")`.
It can be set with the YACV_TEXTURE=<uri> and overridden by the custom `yacv_texture` attribute of an object.
The <uri> can be file:<path> or data:<mime>;base64,<data> where <mime> is the mime type and
<data> is the base64 encoded image."""
color_faces: Optional[ColorTuple]
"""Overrides the default color to use for model faces. Applies even if a texture is used.
You can use `show(..., color_faces=...)` or the standard way of setting colors for build123d/cadquery objects to
override this color.
It can be set with the YACV_COLOR_FACES=<color> environment variable, where <color> is a color
in the hexadecimal format #RRGGBB or #RRGGBBAA."""
color_edges: Optional[ColorTuple]
"""Overrides the default color to use for model edges.
You can use `show(..., color_edges=...) or the standard way of setting colors for build123d/cadquery objects to
override this color.
It can be set with the YACV_COLOR_EDGES=<color> environment variable, where <color> is a color
in the hexadecimal format #RRGGBB or #RRGGBBAA."""
color_vertices: Optional[ColorTuple]
"""Overrides the default color to use for model vertices.
You can use `show(..., color_vertices=...)` or the standard way of setting colors for build123d/cadquery objects to
override this color.
It can be set with the YACV_COLOR_VERTICES=<color> environment variable, where <color> is a color
in the hexadecimal format #RRGGBB or #RRGGBBAA."""
def __init__(self):
"""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()
@@ -109,11 +151,15 @@ class YACV:
self.at_least_one_client = threading.Event()
self.shutting_down = threading.Event()
self.frontend_lock = RWLock()
self.texture = _read_texture_uri(os.getenv("YACV_BASE_TEXTURE"))
self.texture = _read_texture_uri(os.getenv("YACV_TEXTURE"))
self.color_faces = _read_color(os.getenv("YACV_COLOR_FACES", "#ffbf00")) # Default yellow
self.color_edges = _read_color(os.getenv("YACV_COLOR_EDGES", "#1a1aff")) # Default blue
self.color_vertices = _read_color(os.getenv("YACV_COLOR_VERTICES", "#1a1a1a")) # Default dark gray
logger.info('Using yacv-server v%s', get_version())
def start(self):
"""Starts the web server in the background"""
if self.protocol == YACVProtocol.STDERR: return # No server to start, just print to stderr
assert self.server_thread is None, "Server currently running, cannot start another one"
assert self.startup_complete.is_set() is False, "Server already started"
# Start the server in a separate daemon thread
@@ -130,6 +176,8 @@ class YACV:
# noinspection PyUnusedLocal
def stop(self, *args):
"""Stops the web server"""
if self.protocol == YACVProtocol.STDERR: return # No server to stop, just print to stderr
# The remainder is for the HTTP protocol only
if self.server_thread is None:
logger.error('Cannot stop server because it is not running')
return
@@ -137,7 +185,7 @@ class YACV:
# Inform the server that we are shutting down
self.shutting_down.set()
# noinspection PyTypeChecker
self.show_events.publish(UpdatesApiFullData(name='__shutdown', _hash='', is_remove=None, obj=None))
self._show_event(UpdatesApiFullData(name='__shutdown', _hash='', is_remove=None, obj=None))
# If we were too fast, ensure that at least one client has connected
graceful_secs_connect = float(os.getenv('YACV_GRACEFUL_SECS_CONNECT', 12.0))
@@ -165,9 +213,11 @@ class YACV:
if len(args) >= 1 and args[0] in (signal.SIGINT, signal.SIGTERM):
sys.exit(0) # Exit with success
_yacvServerModelPrefix = "yacv_server://model/"
def _run_server(self):
"""Runs the web server"""
logger.info('Starting server...')
logger.info('Starting server in %s mode...', self.protocol.name)
self.server = ThreadingHTTPServer(
(os.getenv('YACV_HOST', 'localhost'), int(os.getenv('YACV_PORT', 32323))),
lambda a, b, c: HTTPHandler(a, b, c, yacv=self))
@@ -176,6 +226,22 @@ class YACV:
self.startup_complete.set()
self.server.serve_forever()
def _show_event(self, event: UpdatesApiFullData):
"""Handles a show event by publishing it to the show events buffer (and special handling for stderr protocol)."""
self.show_events.publish(event)
# If the protocol is STDERR, we need to print the event to stderr
if self.protocol == YACVProtocol.STDERR:
msg = f'{self._yacvServerModelPrefix}{event.to_json()}'
if not event.is_remove:
# Always build the object even if the interface already has it (optimization disabled for Pyodide)
glb_and_hash = self.export(event.name)
if glb_and_hash is None:
logger.warning('Object %s not found, ignoring it...', event.name)
return
glb = glb_and_hash[0]
msg += f'{base64.b64encode(glb).decode("utf-8")}'
print(msg, file=sys.stderr, flush=True)
def show(self, *objs: List[YACVSupported], names: Optional[Union[str, List[str]]] = None, **kwargs):
"""
Shows the given CAD objects in the frontend. The objects will be tessellated and converted to GLTF. Optionally,
@@ -201,8 +267,9 @@ class YACV:
if isinstance(names, str):
names = [names]
assert len(names) == len(objs), 'Number of names must match the number of objects'
if 'color' in kwargs:
kwargs['color'] = get_color(kwargs['color'])
for color_name in ('color_faces', 'color_edges', 'color_vertices'):
if color_name in kwargs:
kwargs[color_name] = get_color(kwargs[color_name]) or _read_color(kwargs[color_name])
# Handle auto clearing of previous objects
if kwargs.get('auto_clear', True):
@@ -218,14 +285,16 @@ class YACV:
# Publish the show event
for obj, name in zip(objs, names):
obj_color = get_color(obj)
# Some properties may be lost in preprocessing, so save them in kwargs
_kwargs = kwargs.copy()
if obj_color is not None:
kwargs = kwargs.copy()
kwargs['color'] = obj_color
_kwargs['color_obj'] = obj_color # Only applies to highest-dimensional objects
_kwargs['texture'] = _read_texture_uri(getattr(obj, 'yacv_texture', None) or kwargs.get('texture', None))
if not isinstance(obj, bytes):
obj = _preprocess_cad(obj, **kwargs)
_hash = _hashcode(obj, **kwargs)
event = UpdatesApiFullData(name=name, _hash=_hash, obj=obj, kwargs=kwargs or {})
self.show_events.publish(event)
obj = _preprocess_cad(obj, **_kwargs)
_hash = _hashcode(obj, **_kwargs)
event = UpdatesApiFullData(name=name, _hash=_hash, obj=obj, kwargs=_kwargs or {})
self._show_event(event)
logger.info('show %s took %.3f seconds', names, time.time() - start)
@@ -250,7 +319,7 @@ class YACV:
# Publish the remove event
show_event = copy.copy(show_events[-1])
show_event.is_remove = True
self.show_events.publish(show_event)
self._show_event(show_event)
def clear(self, except_names: List[str] = None):
"""Clears all previously-shown objects from the scene"""
@@ -309,17 +378,17 @@ class YACV:
if isinstance(event.obj, bytes): # Already a GLTF
publish_to.publish(event.obj)
else: # CAD object to tessellate and convert to GLTF
texture_override_uri = event.kwargs.get('texture', None)
texture_override = None
if isinstance(texture_override_uri, str):
texture_override = _read_texture_uri(texture_override_uri)
gltf = tessellate(event.obj, tolerance=event.kwargs.get('tolerance', 0.1),
angular_tolerance=event.kwargs.get('angular_tolerance', 0.1),
faces=event.kwargs.get('faces', True),
edges=event.kwargs.get('edges', True),
vertices=event.kwargs.get('vertices', True),
obj_color=event.kwargs.get('color', None),
texture=texture_override or self.texture)
gltf = tessellate(
event.obj,
color_faces=event.kwargs.get('color_faces', self.color_faces),
color_edges=event.kwargs.get('color_edges', self.color_edges),
color_vertices=event.kwargs.get('color_vertices', self.color_vertices),
color_obj=event.kwargs.get('color_obj', None),
tolerance=event.kwargs.get('tolerance', 0.1),
angular_tolerance=event.kwargs.get('angular_tolerance', 0.1),
faces=event.kwargs.get('faces', True), edges=event.kwargs.get('edges', True),
vertices=event.kwargs.get('vertices', True),
texture=event.kwargs.get('texture', self.texture))
glb_list_of_bytes = gltf.save_to_bytes()
glb_bytes = b''.join(glb_list_of_bytes)
publish_to.publish(glb_bytes)
@@ -331,6 +400,7 @@ class YACV:
try:
return next(subscription), event.hash
finally:
# noinspection PyInconsistentReturns
subscription.close()
def export_all(self, folder: str,
@@ -354,7 +424,7 @@ def _read_texture_uri(uri: str) -> Optional[Tuple[bytes, str]]:
img = Image.open(buf)
mtype = img.get_format_mimetype()
return data, mtype
if uri.startswith("data:"): # https://en.wikipedia.org/wiki/Data_URI_scheme#Syntax (limited)
if uri.startswith("data:"): # https://en.wikipedia.org/wiki/Data_URI_scheme#Syntax (limited)
mtype_and_data = uri[len("data:"):]
mtype = mtype_and_data.split(";", 1)[0]
data_str = mtype_and_data.split(",", 1)[1]
@@ -362,6 +432,20 @@ def _read_texture_uri(uri: str) -> Optional[Tuple[bytes, str]]:
return data, mtype
return None
def _read_color(color: str) -> Optional[ColorTuple]:
"""Reads a color from a string in the format #RRGGBB or #RRGGBBAA"""
if color is None:
return None
if not color.startswith('#') or len(color) not in (7, 9):
raise ValueError(f'Invalid color format: {color}')
r = float(int(color[1:3], 16)) / 255.0
g = float(int(color[3:5], 16)) / 255.0
b = float(int(color[5:7], 16)) / 255.0
a = float(int(color[7:9], 16)) / 255.0 if len(color) == 9 else 1.0
return r, g, b, a
# noinspection PyUnusedLocal
def _preprocess_cad(obj: CADLike, **kwargs) -> CADCoreLike:
# Get the shape of a CAD-like object
@@ -381,19 +465,31 @@ def _preprocess_cad(obj: CADLike, **kwargs) -> CADCoreLike:
return obj
_find_var_name_count = 0
_obj_name_counts = {}
def _find_var_name(obj: any, avoid_levels: int = 2) -> str:
"""A hacky way to get a stable name for an object that may change over time"""
global _find_var_name_count
# Build123d objects have a "label" property, CadQuery Assembly's have "name"
for f in ('label', 'name'):
if hasattr(obj, f):
v = getattr(obj, f)
if v != '':
return v;
# Otherwise walk up our stack to see if there's a local variable that points to it
obj_shape = get_shape(obj, error=False) or obj
for frame in inspect.stack()[avoid_levels:]:
for key, value in frame.frame.f_locals.items():
if get_shape(value, error=False) is obj_shape:
return key
_find_var_name_count += 1
return 'unknown_var_' + str(_find_var_name_count)
# Last resort, name it for its type with a disambiguating number
global _obj_name_counts
t = obj.__class__.__name__
_obj_name_counts[t] = 1 if t not in _obj_name_counts else _obj_name_counts[t] + 1
return t + str(_obj_name_counts[t])
def sizeof_fmt(num, suffix="B"):

3051
yarn.lock

File diff suppressed because it is too large Load Diff