@johnmorrisdotca/uchusen-rockets 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/LICENSES/CC-BY-4.0.txt +396 -0
- package/LICENSES/MIT.txt +21 -0
- package/NOTICE.md +66 -0
- package/README.md +171 -2
- package/dist/assets/dragon-dragon-1-apart.painted.svg +1 -0
- package/dist/assets/dragon-dragon-1-apart.svg +1 -0
- package/dist/assets/dragon-dragon-1-apart.thick.svg +1 -0
- package/dist/assets/dragon-dragon-1-capsule.glb +0 -0
- package/dist/assets/dragon-dragon-1-capsule.painted.svg +1 -0
- package/dist/assets/dragon-dragon-1-capsule.svg +1 -0
- package/dist/assets/dragon-dragon-1-capsule.thick.svg +1 -0
- package/dist/assets/dragon-dragon-1-trunk.glb +0 -0
- package/dist/assets/dragon-dragon-1-trunk.painted.svg +1 -0
- package/dist/assets/dragon-dragon-1-trunk.svg +1 -0
- package/dist/assets/dragon-dragon-1-trunk.thick.svg +1 -0
- package/dist/assets/dragon-dragon-1.glb +0 -0
- package/dist/assets/dragon-dragon-1.painted.svg +1 -0
- package/dist/assets/dragon-dragon-1.svg +1 -0
- package/dist/assets/dragon-dragon-1.thick.svg +1 -0
- package/dist/assets/dragon-dragon-2-apart.painted.svg +1 -0
- package/dist/assets/dragon-dragon-2-apart.svg +1 -0
- package/dist/assets/dragon-dragon-2-apart.thick.svg +1 -0
- package/dist/assets/dragon-dragon-2-capsule.glb +0 -0
- package/dist/assets/dragon-dragon-2-capsule.painted.svg +1 -0
- package/dist/assets/dragon-dragon-2-capsule.svg +1 -0
- package/dist/assets/dragon-dragon-2-capsule.thick.svg +1 -0
- package/dist/assets/dragon-dragon-2-trunk.glb +0 -0
- package/dist/assets/dragon-dragon-2-trunk.painted.svg +1 -0
- package/dist/assets/dragon-dragon-2-trunk.svg +1 -0
- package/dist/assets/dragon-dragon-2-trunk.thick.svg +1 -0
- package/dist/assets/dragon-dragon-2.glb +0 -0
- package/dist/assets/dragon-dragon-2.painted.svg +1 -0
- package/dist/assets/dragon-dragon-2.svg +1 -0
- package/dist/assets/dragon-dragon-2.thick.svg +1 -0
- package/dist/assets/falcon-9-block-5-apart.painted.svg +1 -0
- package/dist/assets/falcon-9-block-5-apart.svg +1 -0
- package/dist/assets/falcon-9-block-5-apart.thick.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2-apart.painted.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2-apart.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2-apart.thick.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2-capsule.glb +0 -0
- package/dist/assets/falcon-9-block-5-dragon-2-capsule.painted.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2-capsule.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2-capsule.thick.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2-first-stage.glb +0 -0
- package/dist/assets/falcon-9-block-5-dragon-2-first-stage.painted.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2-first-stage.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2-first-stage.thick.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2-second-stage.glb +0 -0
- package/dist/assets/falcon-9-block-5-dragon-2-second-stage.painted.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2-second-stage.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2-second-stage.thick.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2-trunk.glb +0 -0
- package/dist/assets/falcon-9-block-5-dragon-2-trunk.painted.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2-trunk.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2-trunk.thick.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2.glb +0 -0
- package/dist/assets/falcon-9-block-5-dragon-2.painted.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2.svg +1 -0
- package/dist/assets/falcon-9-block-5-dragon-2.thick.svg +1 -0
- package/dist/assets/falcon-9-block-5-fairing.glb +0 -0
- package/dist/assets/falcon-9-block-5-fairing.painted.svg +1 -0
- package/dist/assets/falcon-9-block-5-fairing.svg +1 -0
- package/dist/assets/falcon-9-block-5-fairing.thick.svg +1 -0
- package/dist/assets/falcon-9-block-5-first-stage.glb +0 -0
- package/dist/assets/falcon-9-block-5-first-stage.painted.svg +1 -0
- package/dist/assets/falcon-9-block-5-first-stage.svg +1 -0
- package/dist/assets/falcon-9-block-5-first-stage.thick.svg +1 -0
- package/dist/assets/falcon-9-block-5-second-stage.glb +0 -0
- package/dist/assets/falcon-9-block-5-second-stage.painted.svg +1 -0
- package/dist/assets/falcon-9-block-5-second-stage.svg +1 -0
- package/dist/assets/falcon-9-block-5-second-stage.thick.svg +1 -0
- package/dist/assets/falcon-9-block-5.glb +0 -0
- package/dist/assets/falcon-9-block-5.painted.svg +1 -0
- package/dist/assets/falcon-9-block-5.svg +1 -0
- package/dist/assets/falcon-9-block-5.thick.svg +1 -0
- package/dist/assets/falcon-9-ft-apart.painted.svg +1 -0
- package/dist/assets/falcon-9-ft-apart.svg +1 -0
- package/dist/assets/falcon-9-ft-apart.thick.svg +1 -0
- package/dist/assets/falcon-9-ft-fairing.glb +0 -0
- package/dist/assets/falcon-9-ft-fairing.painted.svg +1 -0
- package/dist/assets/falcon-9-ft-fairing.svg +1 -0
- package/dist/assets/falcon-9-ft-fairing.thick.svg +1 -0
- package/dist/assets/falcon-9-ft-first-stage.glb +0 -0
- package/dist/assets/falcon-9-ft-first-stage.painted.svg +1 -0
- package/dist/assets/falcon-9-ft-first-stage.svg +1 -0
- package/dist/assets/falcon-9-ft-first-stage.thick.svg +1 -0
- package/dist/assets/falcon-9-ft-second-stage.glb +0 -0
- package/dist/assets/falcon-9-ft-second-stage.painted.svg +1 -0
- package/dist/assets/falcon-9-ft-second-stage.svg +1 -0
- package/dist/assets/falcon-9-ft-second-stage.thick.svg +1 -0
- package/dist/assets/falcon-9-ft.glb +0 -0
- package/dist/assets/falcon-9-ft.painted.svg +1 -0
- package/dist/assets/falcon-9-ft.svg +1 -0
- package/dist/assets/falcon-9-ft.thick.svg +1 -0
- package/dist/assets/falcon-9-v1.0-apart.painted.svg +1 -0
- package/dist/assets/falcon-9-v1.0-apart.svg +1 -0
- package/dist/assets/falcon-9-v1.0-apart.thick.svg +1 -0
- package/dist/assets/falcon-9-v1.0-fairing.glb +0 -0
- package/dist/assets/falcon-9-v1.0-fairing.painted.svg +1 -0
- package/dist/assets/falcon-9-v1.0-fairing.svg +1 -0
- package/dist/assets/falcon-9-v1.0-fairing.thick.svg +1 -0
- package/dist/assets/falcon-9-v1.0-first-stage.glb +0 -0
- package/dist/assets/falcon-9-v1.0-first-stage.painted.svg +1 -0
- package/dist/assets/falcon-9-v1.0-first-stage.svg +1 -0
- package/dist/assets/falcon-9-v1.0-first-stage.thick.svg +1 -0
- package/dist/assets/falcon-9-v1.0-second-stage.glb +0 -0
- package/dist/assets/falcon-9-v1.0-second-stage.painted.svg +1 -0
- package/dist/assets/falcon-9-v1.0-second-stage.svg +1 -0
- package/dist/assets/falcon-9-v1.0-second-stage.thick.svg +1 -0
- package/dist/assets/falcon-9-v1.0.glb +0 -0
- package/dist/assets/falcon-9-v1.0.painted.svg +1 -0
- package/dist/assets/falcon-9-v1.0.svg +1 -0
- package/dist/assets/falcon-9-v1.0.thick.svg +1 -0
- package/dist/assets/falcon-9-v1.1-apart.painted.svg +1 -0
- package/dist/assets/falcon-9-v1.1-apart.svg +1 -0
- package/dist/assets/falcon-9-v1.1-apart.thick.svg +1 -0
- package/dist/assets/falcon-9-v1.1-fairing.glb +0 -0
- package/dist/assets/falcon-9-v1.1-fairing.painted.svg +1 -0
- package/dist/assets/falcon-9-v1.1-fairing.svg +1 -0
- package/dist/assets/falcon-9-v1.1-fairing.thick.svg +1 -0
- package/dist/assets/falcon-9-v1.1-first-stage.glb +0 -0
- package/dist/assets/falcon-9-v1.1-first-stage.painted.svg +1 -0
- package/dist/assets/falcon-9-v1.1-first-stage.svg +1 -0
- package/dist/assets/falcon-9-v1.1-first-stage.thick.svg +1 -0
- package/dist/assets/falcon-9-v1.1-second-stage.glb +0 -0
- package/dist/assets/falcon-9-v1.1-second-stage.painted.svg +1 -0
- package/dist/assets/falcon-9-v1.1-second-stage.svg +1 -0
- package/dist/assets/falcon-9-v1.1-second-stage.thick.svg +1 -0
- package/dist/assets/falcon-9-v1.1.glb +0 -0
- package/dist/assets/falcon-9-v1.1.painted.svg +1 -0
- package/dist/assets/falcon-9-v1.1.svg +1 -0
- package/dist/assets/falcon-9-v1.1.thick.svg +1 -0
- package/dist/assets/falcon-heavy-falcon-heavy-apart.painted.svg +1 -0
- package/dist/assets/falcon-heavy-falcon-heavy-apart.svg +1 -0
- package/dist/assets/falcon-heavy-falcon-heavy-apart.thick.svg +1 -0
- package/dist/assets/falcon-heavy-falcon-heavy-centre.glb +0 -0
- package/dist/assets/falcon-heavy-falcon-heavy-centre.painted.svg +1 -0
- package/dist/assets/falcon-heavy-falcon-heavy-centre.svg +1 -0
- package/dist/assets/falcon-heavy-falcon-heavy-centre.thick.svg +1 -0
- package/dist/assets/falcon-heavy-falcon-heavy-side-left.glb +0 -0
- package/dist/assets/falcon-heavy-falcon-heavy-side-left.painted.svg +1 -0
- package/dist/assets/falcon-heavy-falcon-heavy-side-left.svg +1 -0
- package/dist/assets/falcon-heavy-falcon-heavy-side-left.thick.svg +1 -0
- package/dist/assets/falcon-heavy-falcon-heavy-side-right.glb +0 -0
- package/dist/assets/falcon-heavy-falcon-heavy-side-right.painted.svg +1 -0
- package/dist/assets/falcon-heavy-falcon-heavy-side-right.svg +1 -0
- package/dist/assets/falcon-heavy-falcon-heavy-side-right.thick.svg +1 -0
- package/dist/assets/falcon-heavy-falcon-heavy.glb +0 -0
- package/dist/assets/falcon-heavy-falcon-heavy.painted.svg +1 -0
- package/dist/assets/falcon-heavy-falcon-heavy.svg +1 -0
- package/dist/assets/falcon-heavy-falcon-heavy.thick.svg +1 -0
- package/dist/assets/grasshopper-grasshopper.glb +0 -0
- package/dist/assets/grasshopper-grasshopper.painted.svg +1 -0
- package/dist/assets/grasshopper-grasshopper.svg +1 -0
- package/dist/assets/grasshopper-grasshopper.thick.svg +1 -0
- package/dist/assets/models/dragon-dragon-1-capsule.glb +0 -0
- package/dist/assets/models/dragon-dragon-1-trunk.glb +0 -0
- package/dist/assets/models/dragon-dragon-2-capsule.glb +0 -0
- package/dist/assets/models/dragon-dragon-2-trunk.glb +0 -0
- package/dist/assets/models/falcon-9-block-5-fairing.glb +0 -0
- package/dist/assets/models/falcon-9-block-5-first-stage.glb +0 -0
- package/dist/assets/models/falcon-9-block-5-second-stage.glb +0 -0
- package/dist/assets/models/falcon-heavy-falcon-heavy-centre.glb +0 -0
- package/dist/assets/models/falcon-heavy-falcon-heavy-side-left.glb +0 -0
- package/dist/assets/models/falcon-heavy-falcon-heavy-side-right.glb +0 -0
- package/dist/assets/models/grasshopper-grasshopper-vehicle.glb +0 -0
- package/dist/assets/models/starship-2019-ship.glb +0 -0
- package/dist/assets/models/starship-v1-flight1-booster.glb +0 -0
- package/dist/assets/models/starship-v1-flight1-ship.glb +0 -0
- package/dist/assets/models/starship-v3-booster.glb +0 -0
- package/dist/assets/models/starship-v3-ship.glb +0 -0
- package/dist/assets/parts/flap-aft.glb +0 -0
- package/dist/assets/parts/flap-aft.painted.svg +1 -0
- package/dist/assets/parts/flap-aft.svg +1 -0
- package/dist/assets/parts/flap-aft.thick.svg +1 -0
- package/dist/assets/parts/flap-forward.glb +0 -0
- package/dist/assets/parts/flap-forward.painted.svg +1 -0
- package/dist/assets/parts/flap-forward.svg +1 -0
- package/dist/assets/parts/flap-forward.thick.svg +1 -0
- package/dist/assets/parts/grid-fin-four.glb +0 -0
- package/dist/assets/parts/grid-fin-four.painted.svg +1 -0
- package/dist/assets/parts/grid-fin-four.svg +1 -0
- package/dist/assets/parts/grid-fin-four.thick.svg +1 -0
- package/dist/assets/parts/grid-fin-three.glb +0 -0
- package/dist/assets/parts/grid-fin-three.painted.svg +1 -0
- package/dist/assets/parts/grid-fin-three.svg +1 -0
- package/dist/assets/parts/grid-fin-three.thick.svg +1 -0
- package/dist/assets/parts/merlin-sea-level.glb +0 -0
- package/dist/assets/parts/merlin-sea-level.painted.svg +1 -0
- package/dist/assets/parts/merlin-sea-level.svg +1 -0
- package/dist/assets/parts/merlin-sea-level.thick.svg +1 -0
- package/dist/assets/parts/raptor-sea-level.glb +0 -0
- package/dist/assets/parts/raptor-sea-level.painted.svg +1 -0
- package/dist/assets/parts/raptor-sea-level.svg +1 -0
- package/dist/assets/parts/raptor-sea-level.thick.svg +1 -0
- package/dist/assets/parts/raptor-vacuum.glb +0 -0
- package/dist/assets/parts/raptor-vacuum.painted.svg +1 -0
- package/dist/assets/parts/raptor-vacuum.svg +1 -0
- package/dist/assets/parts/raptor-vacuum.thick.svg +1 -0
- package/dist/assets/starship-2019.glb +0 -0
- package/dist/assets/starship-2019.painted.svg +1 -0
- package/dist/assets/starship-2019.svg +1 -0
- package/dist/assets/starship-2019.thick.svg +1 -0
- package/dist/assets/starship-v1-apart.painted.svg +1 -0
- package/dist/assets/starship-v1-apart.svg +1 -0
- package/dist/assets/starship-v1-apart.thick.svg +1 -0
- package/dist/assets/starship-v1-booster.glb +0 -0
- package/dist/assets/starship-v1-booster.painted.svg +1 -0
- package/dist/assets/starship-v1-booster.svg +1 -0
- package/dist/assets/starship-v1-booster.thick.svg +1 -0
- package/dist/assets/starship-v1-flight1-apart.painted.svg +1 -0
- package/dist/assets/starship-v1-flight1-apart.svg +1 -0
- package/dist/assets/starship-v1-flight1-apart.thick.svg +1 -0
- package/dist/assets/starship-v1-flight1-booster.glb +0 -0
- package/dist/assets/starship-v1-flight1-booster.painted.svg +1 -0
- package/dist/assets/starship-v1-flight1-booster.svg +1 -0
- package/dist/assets/starship-v1-flight1-booster.thick.svg +1 -0
- package/dist/assets/starship-v1-flight1-ship.glb +0 -0
- package/dist/assets/starship-v1-flight1-ship.painted.svg +1 -0
- package/dist/assets/starship-v1-flight1-ship.svg +1 -0
- package/dist/assets/starship-v1-flight1-ship.thick.svg +1 -0
- package/dist/assets/starship-v1-flight1.glb +0 -0
- package/dist/assets/starship-v1-flight1.painted.svg +1 -0
- package/dist/assets/starship-v1-flight1.svg +1 -0
- package/dist/assets/starship-v1-flight1.thick.svg +1 -0
- package/dist/assets/starship-v1-ship.glb +0 -0
- package/dist/assets/starship-v1-ship.painted.svg +1 -0
- package/dist/assets/starship-v1-ship.svg +1 -0
- package/dist/assets/starship-v1-ship.thick.svg +1 -0
- package/dist/assets/starship-v1.glb +0 -0
- package/dist/assets/starship-v1.painted.svg +1 -0
- package/dist/assets/starship-v1.svg +1 -0
- package/dist/assets/starship-v1.thick.svg +1 -0
- package/dist/assets/starship-v2-apart.painted.svg +1 -0
- package/dist/assets/starship-v2-apart.svg +1 -0
- package/dist/assets/starship-v2-apart.thick.svg +1 -0
- package/dist/assets/starship-v2-booster.glb +0 -0
- package/dist/assets/starship-v2-booster.painted.svg +1 -0
- package/dist/assets/starship-v2-booster.svg +1 -0
- package/dist/assets/starship-v2-booster.thick.svg +1 -0
- package/dist/assets/starship-v2-ship.glb +0 -0
- package/dist/assets/starship-v2-ship.painted.svg +1 -0
- package/dist/assets/starship-v2-ship.svg +1 -0
- package/dist/assets/starship-v2-ship.thick.svg +1 -0
- package/dist/assets/starship-v2.glb +0 -0
- package/dist/assets/starship-v2.painted.svg +1 -0
- package/dist/assets/starship-v2.svg +1 -0
- package/dist/assets/starship-v2.thick.svg +1 -0
- package/dist/assets/starship-v3-apart.painted.svg +1 -0
- package/dist/assets/starship-v3-apart.svg +1 -0
- package/dist/assets/starship-v3-apart.thick.svg +1 -0
- package/dist/assets/starship-v3-booster.glb +0 -0
- package/dist/assets/starship-v3-booster.painted.svg +1 -0
- package/dist/assets/starship-v3-booster.svg +1 -0
- package/dist/assets/starship-v3-booster.thick.svg +1 -0
- package/dist/assets/starship-v3-ship.glb +0 -0
- package/dist/assets/starship-v3-ship.painted.svg +1 -0
- package/dist/assets/starship-v3-ship.svg +1 -0
- package/dist/assets/starship-v3-ship.thick.svg +1 -0
- package/dist/assets/starship-v3.glb +0 -0
- package/dist/assets/starship-v3.painted.svg +1 -0
- package/dist/assets/starship-v3.svg +1 -0
- package/dist/assets/starship-v3.thick.svg +1 -0
- package/dist/assets/starship-v4-apart.painted.svg +1 -0
- package/dist/assets/starship-v4-apart.svg +1 -0
- package/dist/assets/starship-v4-apart.thick.svg +1 -0
- package/dist/assets/starship-v4-booster.glb +0 -0
- package/dist/assets/starship-v4-booster.painted.svg +1 -0
- package/dist/assets/starship-v4-booster.svg +1 -0
- package/dist/assets/starship-v4-booster.thick.svg +1 -0
- package/dist/assets/starship-v4-ship.glb +0 -0
- package/dist/assets/starship-v4-ship.painted.svg +1 -0
- package/dist/assets/starship-v4-ship.svg +1 -0
- package/dist/assets/starship-v4-ship.thick.svg +1 -0
- package/dist/assets/starship-v4.glb +0 -0
- package/dist/assets/starship-v4.painted.svg +1 -0
- package/dist/assets/starship-v4.svg +1 -0
- package/dist/assets/starship-v4.thick.svg +1 -0
- package/dist/build/glb.d.ts +12 -0
- package/dist/build/glb.js +157 -0
- package/dist/build/index.d.ts +59 -0
- package/dist/build/index.js +118 -0
- package/dist/build/lineup.d.ts +40 -0
- package/dist/build/lineup.js +97 -0
- package/dist/crossfade.d.ts +6 -0
- package/dist/crossfade.js +11 -0
- package/dist/data/credits.d.ts +87 -0
- package/dist/data/credits.js +82 -0
- package/dist/data/dragon.d.ts +2 -0
- package/dist/data/dragon.js +45 -0
- package/dist/data/falcon.d.ts +3 -0
- package/dist/data/falcon.js +121 -0
- package/dist/data/grasshopper.d.ts +2 -0
- package/dist/data/grasshopper.js +21 -0
- package/dist/data/sources.d.ts +173 -0
- package/dist/data/sources.js +25 -0
- package/dist/data/starship.d.ts +2 -0
- package/dist/data/starship.js +183 -0
- package/dist/data/types.d.ts +48 -0
- package/dist/data/types.js +8 -0
- package/dist/draw/svg.d.ts +26 -0
- package/dist/draw/svg.js +262 -0
- package/dist/element/paint.d.ts +9 -0
- package/dist/element/paint.js +138 -0
- package/dist/element/scene.d.ts +104 -0
- package/dist/element/scene.js +758 -0
- package/dist/element.d.ts +74 -0
- package/dist/element.js +488 -0
- package/dist/fit.d.ts +43 -0
- package/dist/fit.js +93 -0
- package/dist/framing.d.ts +32 -0
- package/dist/framing.js +52 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +13 -0
- package/dist/layout.d.ts +17 -0
- package/dist/layout.js +33 -0
- package/dist/looks.d.ts +25 -0
- package/dist/looks.js +36 -0
- package/dist/model/falcon.d.ts +48 -0
- package/dist/model/falcon.js +206 -0
- package/dist/model/materials.d.ts +113 -0
- package/dist/model/materials.js +19 -0
- package/dist/model/pieces.d.ts +91 -0
- package/dist/model/pieces.js +223 -0
- package/dist/model/registry.d.ts +8 -0
- package/dist/model/registry.js +104 -0
- package/dist/model/starship.d.ts +67 -0
- package/dist/model/starship.js +503 -0
- package/dist/parts.d.ts +44 -0
- package/dist/parts.js +1 -0
- package/dist/react.d.ts +314 -0
- package/dist/react.js +8 -0
- package/dist/rockets.d.ts +27 -0
- package/dist/rockets.js +36 -0
- package/dist/tilt.d.ts +21 -0
- package/dist/tilt.js +23 -0
- package/package.json +65 -4
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
// The 3D view (three.js, MIT), loaded only when the element comes on screen. One canvas with one rocket, drawn three ways: the line drawing, the thick lines, and painted with true materials. Switching look
|
|
2
|
+
// changes materials and never the camera, so the angle stays. Moving between together, apart and one part alone is an animated move of each part, and under reduced motion it is a cut. It draws only while
|
|
3
|
+
// something moves, never while out of view or the tab is hidden, and settles after a while with no one touching it.
|
|
4
|
+
import { ACESFilmicToneMapping, BackSide, CanvasTexture, Color, DirectionalLight, DoubleSide, Group, HemisphereLight, LineSegments, LinearMipmapLinearFilter, MathUtils, Mesh, MeshBasicMaterial, MeshStandardMaterial, PerspectiveCamera, PlaneGeometry, PMREMGenerator, RepeatWrapping, Scene, ShaderMaterial, SphereGeometry, Spherical, SRGBColorSpace, Timer, Vector2, Vector3, WebGLRenderer, } from "three";
|
|
5
|
+
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
6
|
+
import { LineMaterial } from "three/addons/lines/LineMaterial.js";
|
|
7
|
+
import { LineSegments2 } from "three/addons/lines/LineSegments2.js";
|
|
8
|
+
import { LineSegmentsGeometry } from "three/addons/lines/LineSegmentsGeometry.js";
|
|
9
|
+
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
10
|
+
import { FOV_DEG, frameFor, PICTURE_H } from "../framing.js";
|
|
11
|
+
import { LOOKS } from "../looks.js";
|
|
12
|
+
import { MATERIALS } from "../model/materials.js";
|
|
13
|
+
import { paintGlow, paintSky, paintSteel, paintTiles } from "./paint.js";
|
|
14
|
+
const IDLE_STOP_MS = 90_000;
|
|
15
|
+
const MOVE_MS = 1400;
|
|
16
|
+
const VIEW_MS = 700;
|
|
17
|
+
const autoRotateOf = (spin) => (spin.seconds <= 0 ? 0 : (60 / spin.seconds) * (spin.way === "cw" ? -1 : 1));
|
|
18
|
+
const LINE_VERTEX = /* glsl */ `
|
|
19
|
+
varying vec3 vNormal;
|
|
20
|
+
varying vec2 vUv;
|
|
21
|
+
void main() {
|
|
22
|
+
vNormal = normalize(normalMatrix * normal);
|
|
23
|
+
vUv = uv;
|
|
24
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
25
|
+
}`;
|
|
26
|
+
const LINE_FRAGMENT = /* glsl */ `
|
|
27
|
+
uniform vec3 edge;
|
|
28
|
+
uniform vec3 middle;
|
|
29
|
+
uniform vec3 hexColor;
|
|
30
|
+
uniform float hexMetres;
|
|
31
|
+
uniform float hexOn;
|
|
32
|
+
varying vec3 vNormal;
|
|
33
|
+
varying vec2 vUv;
|
|
34
|
+
float hexLine(vec2 p) {
|
|
35
|
+
vec2 s = vec2(1.0, 1.7320508);
|
|
36
|
+
vec2 a = mod(p, s) - s * 0.5;
|
|
37
|
+
vec2 b = mod(p + s * 0.5, s) - s * 0.5;
|
|
38
|
+
vec2 g = dot(a, a) < dot(b, b) ? a : b;
|
|
39
|
+
vec2 q = abs(g);
|
|
40
|
+
return max(dot(q, s * 0.5), q.x);
|
|
41
|
+
}
|
|
42
|
+
void main() {
|
|
43
|
+
vec3 n = normalize(vNormal);
|
|
44
|
+
float facing = pow(abs(n.z), 1.4);
|
|
45
|
+
vec3 c = mix(edge, middle, facing);
|
|
46
|
+
if (hexOn > 0.5) {
|
|
47
|
+
vec2 metres = vec2(vUv.x * 28.27, vUv.y * 100.0) / hexMetres;
|
|
48
|
+
float d = hexLine(metres);
|
|
49
|
+
float w = fwidth(d) * 1.1;
|
|
50
|
+
float line = 1.0 - smoothstep(0.5 - w, 0.5, d);
|
|
51
|
+
c = mix(c, hexColor, (1.0 - line) * 0.34);
|
|
52
|
+
}
|
|
53
|
+
gl_FragColor = vec4(c, 1.0);
|
|
54
|
+
#include <colorspace_fragment>
|
|
55
|
+
}`;
|
|
56
|
+
const OUTLINE_VERTEX = /* glsl */ `
|
|
57
|
+
uniform float width;
|
|
58
|
+
uniform vec2 size;
|
|
59
|
+
void main() {
|
|
60
|
+
vec4 clip = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
61
|
+
vec3 n = normalize(normalMatrix * normal);
|
|
62
|
+
vec2 away = normalize(n.xy + vec2(1e-5));
|
|
63
|
+
clip.xy += away * width * 2.0 / size * clip.w;
|
|
64
|
+
gl_Position = clip;
|
|
65
|
+
}`;
|
|
66
|
+
const OUTLINE_FRAGMENT = /* glsl */ `
|
|
67
|
+
uniform vec3 color;
|
|
68
|
+
void main() {
|
|
69
|
+
gl_FragColor = vec4(color, 1.0);
|
|
70
|
+
#include <colorspace_fragment>
|
|
71
|
+
}`;
|
|
72
|
+
const asVec = (v) => [v.x, v.y, v.z];
|
|
73
|
+
export class RocketViewer {
|
|
74
|
+
canvas;
|
|
75
|
+
frames = 0;
|
|
76
|
+
holder;
|
|
77
|
+
options;
|
|
78
|
+
renderer;
|
|
79
|
+
environment;
|
|
80
|
+
scene = new Scene();
|
|
81
|
+
camera = new PerspectiveCamera(FOV_DEG, 3 / 4, 1, 4000);
|
|
82
|
+
root = new Group();
|
|
83
|
+
parts = [];
|
|
84
|
+
built = null;
|
|
85
|
+
look = "line";
|
|
86
|
+
show = "together";
|
|
87
|
+
generation = 0;
|
|
88
|
+
materialCache = new Map();
|
|
89
|
+
outlineMaterial;
|
|
90
|
+
textures = [];
|
|
91
|
+
steelMap = null;
|
|
92
|
+
tileMap = null;
|
|
93
|
+
glow = null;
|
|
94
|
+
controls;
|
|
95
|
+
clock = new Timer();
|
|
96
|
+
frame = 0;
|
|
97
|
+
visible = false;
|
|
98
|
+
lastActive = performance.now();
|
|
99
|
+
needsDraw = true;
|
|
100
|
+
pose = null;
|
|
101
|
+
tilt = { azimuth: 0, polar: 0, applied: { azimuth: 0, polar: 0 } };
|
|
102
|
+
resize;
|
|
103
|
+
disposed = false;
|
|
104
|
+
constructor(holder, options) {
|
|
105
|
+
this.holder = holder;
|
|
106
|
+
this.options = options;
|
|
107
|
+
this.renderer = new WebGLRenderer({ antialias: true, alpha: true, powerPreference: "low-power" });
|
|
108
|
+
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
109
|
+
this.renderer.outputColorSpace = SRGBColorSpace;
|
|
110
|
+
this.renderer.toneMapping = ACESFilmicToneMapping;
|
|
111
|
+
this.renderer.toneMappingExposure = 1;
|
|
112
|
+
this.canvas = this.renderer.domElement;
|
|
113
|
+
this.canvas.className = "canvas";
|
|
114
|
+
this.canvas.setAttribute("part", "canvas");
|
|
115
|
+
this.canvas.tabIndex = 0;
|
|
116
|
+
this.canvas.setAttribute("role", "img");
|
|
117
|
+
this.canvas.setAttribute("aria-roledescription", "3D view");
|
|
118
|
+
this.setLabel(options.label);
|
|
119
|
+
this.canvas.addEventListener("webglcontextlost", (event) => {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
if (!this.disposed)
|
|
122
|
+
this.options.onLost();
|
|
123
|
+
});
|
|
124
|
+
holder.appendChild(this.canvas);
|
|
125
|
+
const pmrem = new PMREMGenerator(this.renderer);
|
|
126
|
+
const skyTexture = new CanvasTexture(paintSky());
|
|
127
|
+
skyTexture.colorSpace = SRGBColorSpace;
|
|
128
|
+
const skyScene = new Scene();
|
|
129
|
+
const sky = new Mesh(new SphereGeometry(50, 48, 24), new MeshBasicMaterial({ map: skyTexture, side: BackSide }));
|
|
130
|
+
skyScene.add(sky);
|
|
131
|
+
this.environment = pmrem.fromScene(skyScene, 0.012).texture;
|
|
132
|
+
pmrem.dispose();
|
|
133
|
+
sky.geometry.dispose();
|
|
134
|
+
sky.material.dispose();
|
|
135
|
+
skyTexture.dispose();
|
|
136
|
+
this.scene.environment = this.environment;
|
|
137
|
+
this.scene.environmentIntensity = 0.95;
|
|
138
|
+
const key = new DirectionalLight("#fff6e6", 1.6);
|
|
139
|
+
key.position.set(3, 5, 4);
|
|
140
|
+
const rim = new DirectionalLight("#cfe2ff", 1.1);
|
|
141
|
+
rim.position.set(-4, 1, -5);
|
|
142
|
+
const low = new DirectionalLight("#ffffff", 0.35);
|
|
143
|
+
low.position.set(1, -4, 2);
|
|
144
|
+
this.scene.add(key, rim, low, new HemisphereLight("#d7e6ff", "#22252b", 0.35), this.root);
|
|
145
|
+
this.outlineMaterial = new ShaderMaterial({
|
|
146
|
+
vertexShader: OUTLINE_VERTEX,
|
|
147
|
+
fragmentShader: OUTLINE_FRAGMENT,
|
|
148
|
+
side: BackSide,
|
|
149
|
+
uniforms: { width: { value: 1 }, size: { value: new Vector2(1, 1) }, color: { value: new Color("#8fa1b8") } },
|
|
150
|
+
});
|
|
151
|
+
this.controls = new OrbitControls(this.camera, this.canvas);
|
|
152
|
+
this.controls.enableDamping = true;
|
|
153
|
+
this.controls.dampingFactor = 0.08;
|
|
154
|
+
this.controls.enablePan = false;
|
|
155
|
+
this.controls.enableZoom = false;
|
|
156
|
+
this.controls.rotateSpeed = 0.7;
|
|
157
|
+
this.controls.minPolarAngle = 0.04;
|
|
158
|
+
this.controls.maxPolarAngle = Math.PI - 0.04;
|
|
159
|
+
this.canvas.style.touchAction = "pan-y";
|
|
160
|
+
this.controls.addEventListener("start", () => this.touched());
|
|
161
|
+
this.controls.addEventListener("change", () => this.wake());
|
|
162
|
+
this.canvas.addEventListener("focus", () => (this.controls.enableZoom = true));
|
|
163
|
+
this.canvas.addEventListener("blur", () => (this.controls.enableZoom = false));
|
|
164
|
+
this.canvas.addEventListener("pointerdown", () => this.canvas.focus({ preventScroll: true }));
|
|
165
|
+
this.canvas.addEventListener("dblclick", () => this.setAngle(this.homeAngle, true));
|
|
166
|
+
this.canvas.addEventListener("keydown", (event) => this.key(event));
|
|
167
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
168
|
+
this.resize = new ResizeObserver(() => this.fit());
|
|
169
|
+
this.resize.observe(holder);
|
|
170
|
+
this.fit();
|
|
171
|
+
}
|
|
172
|
+
homeAngle = { az: 32, el: 9 };
|
|
173
|
+
setLabel(label) {
|
|
174
|
+
this.canvas.setAttribute("aria-label", `3D view of ${label}. Drag or use the arrow keys to turn it; double-click or Home puts it back.`);
|
|
175
|
+
}
|
|
176
|
+
/** Loads a rocket and shows it. The camera's angle stays when a rocket is already showing. */
|
|
177
|
+
async load(built, show, look, angle) {
|
|
178
|
+
const generation = ++this.generation;
|
|
179
|
+
const bytes = built.glb().bytes;
|
|
180
|
+
const gltf = await new GLTFLoader().parseAsync(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), "");
|
|
181
|
+
if (this.disposed || generation !== this.generation)
|
|
182
|
+
return;
|
|
183
|
+
for (const part of this.parts)
|
|
184
|
+
part.holder.removeFromParent();
|
|
185
|
+
this.disposeTree(this.root);
|
|
186
|
+
this.root.clear();
|
|
187
|
+
this.parts = [];
|
|
188
|
+
this.built = built;
|
|
189
|
+
this.show = show;
|
|
190
|
+
const places = built.placesOf(show);
|
|
191
|
+
for (const part of built.parts) {
|
|
192
|
+
const source = gltf.scene.getObjectByName(part.slot);
|
|
193
|
+
if (!source)
|
|
194
|
+
continue;
|
|
195
|
+
const holder = new Group();
|
|
196
|
+
holder.name = part.slot;
|
|
197
|
+
const code = new Group();
|
|
198
|
+
holder.add(code);
|
|
199
|
+
const view = {
|
|
200
|
+
slot: part.slot,
|
|
201
|
+
holder,
|
|
202
|
+
code,
|
|
203
|
+
model: null,
|
|
204
|
+
modelFile: part.record.painted?.file ?? null,
|
|
205
|
+
modelState: "none",
|
|
206
|
+
faces: [],
|
|
207
|
+
outlines: [],
|
|
208
|
+
feature: [],
|
|
209
|
+
weld: [],
|
|
210
|
+
from: [0, 0, 0],
|
|
211
|
+
to: [0, 0, 0],
|
|
212
|
+
hideAtEnd: false,
|
|
213
|
+
};
|
|
214
|
+
this.takeMeshes(source, code, view);
|
|
215
|
+
const place = places.find((p) => p.slot === part.slot);
|
|
216
|
+
holder.position.set(...(place?.at ?? [0, 0, 0]));
|
|
217
|
+
holder.visible = place?.shown ?? false;
|
|
218
|
+
this.root.add(holder);
|
|
219
|
+
this.parts.push(view);
|
|
220
|
+
}
|
|
221
|
+
this.addGlow();
|
|
222
|
+
this.setLook(look);
|
|
223
|
+
this.frameTo(angle ?? this.currentAngle(), false);
|
|
224
|
+
this.canvas.dataset.ready = "true";
|
|
225
|
+
this.canvas.dataset.parts = this.parts.map((part) => part.slot).join(" ");
|
|
226
|
+
this.wake();
|
|
227
|
+
}
|
|
228
|
+
/** Moves the parts from the packed nodes into `into`, keeping each mesh's own placement. */
|
|
229
|
+
takeMeshes(source, into, view) {
|
|
230
|
+
const box = source.children[0];
|
|
231
|
+
if (!box)
|
|
232
|
+
return;
|
|
233
|
+
const carrier = new Group();
|
|
234
|
+
carrier.position.copy(box.position);
|
|
235
|
+
carrier.scale.copy(box.scale);
|
|
236
|
+
into.add(carrier);
|
|
237
|
+
const meshes = [];
|
|
238
|
+
box.traverse((node) => {
|
|
239
|
+
if (node instanceof Mesh || node instanceof LineSegments)
|
|
240
|
+
meshes.push(node);
|
|
241
|
+
});
|
|
242
|
+
for (const node of meshes) {
|
|
243
|
+
if (node instanceof LineSegments) {
|
|
244
|
+
const named = node.material.name;
|
|
245
|
+
const fat = this.fatLines(node);
|
|
246
|
+
carrier.add(fat);
|
|
247
|
+
(named === "line-feature" ? view.feature : view.weld).push(fat);
|
|
248
|
+
}
|
|
249
|
+
else if (node instanceof Mesh) {
|
|
250
|
+
const mesh = new Mesh(node.geometry);
|
|
251
|
+
const name = node.material.name;
|
|
252
|
+
mesh.name = name;
|
|
253
|
+
carrier.add(mesh);
|
|
254
|
+
view.faces.push({ mesh, name, hasUv: Boolean(node.geometry.getAttribute("uv")) });
|
|
255
|
+
const outline = new Mesh(node.geometry, this.outlineMaterial);
|
|
256
|
+
outline.name = "outline";
|
|
257
|
+
carrier.add(outline);
|
|
258
|
+
view.outlines.push(outline);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** Lines of a set width in pixels, whatever the distance. */
|
|
263
|
+
fatLines(node) {
|
|
264
|
+
const position = node.geometry.getAttribute("position");
|
|
265
|
+
const index = node.geometry.getIndex();
|
|
266
|
+
const pairs = new Float32Array((index ? index.count : position.count) * 3);
|
|
267
|
+
for (let i = 0; i < (index ? index.count : position.count); i++) {
|
|
268
|
+
const v = index ? index.getX(i) : i;
|
|
269
|
+
pairs[i * 3] = position.getX(v);
|
|
270
|
+
pairs[i * 3 + 1] = position.getY(v);
|
|
271
|
+
pairs[i * 3 + 2] = position.getZ(v);
|
|
272
|
+
}
|
|
273
|
+
const geometry = new LineSegmentsGeometry();
|
|
274
|
+
geometry.setPositions(pairs);
|
|
275
|
+
const fat = new LineSegments2(geometry, new LineMaterial({ color: 0x8fa1b8, linewidth: 1, transparent: true }));
|
|
276
|
+
fat.computeLineDistances();
|
|
277
|
+
return fat;
|
|
278
|
+
}
|
|
279
|
+
/** The drawing's own colours for a material in a line look: a body that darkens to its edges, as the drawing's gradient does. */
|
|
280
|
+
lineMaterial(name, look, hasUv) {
|
|
281
|
+
const key = `${look}/${name}/${hasUv}`;
|
|
282
|
+
const known = this.materialCache.get(key);
|
|
283
|
+
if (known)
|
|
284
|
+
return known;
|
|
285
|
+
const fill = (name in MATERIALS ? MATERIALS[name].fill : "metal");
|
|
286
|
+
const [edge, middle] = LOOKS[look].fills[fill];
|
|
287
|
+
const made = new ShaderMaterial({
|
|
288
|
+
vertexShader: LINE_VERTEX,
|
|
289
|
+
fragmentShader: LINE_FRAGMENT,
|
|
290
|
+
side: DoubleSide,
|
|
291
|
+
polygonOffset: true,
|
|
292
|
+
polygonOffsetFactor: 1.5,
|
|
293
|
+
polygonOffsetUnits: 2,
|
|
294
|
+
uniforms: {
|
|
295
|
+
edge: { value: new Color(edge) },
|
|
296
|
+
middle: { value: new Color(middle) },
|
|
297
|
+
hexColor: { value: new Color(LOOKS[look].line) },
|
|
298
|
+
hexOn: { value: name === "tile" && hasUv ? 1 : 0 },
|
|
299
|
+
hexMetres: { value: 1 },
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
this.materialCache.set(key, made);
|
|
303
|
+
return made;
|
|
304
|
+
}
|
|
305
|
+
/** True materials: a stainless hull with its panels, black tiles, dark bells. */
|
|
306
|
+
paintedMaterial(name, hasUv) {
|
|
307
|
+
const key = `painted/${name}/${hasUv}`;
|
|
308
|
+
const known = this.materialCache.get(key);
|
|
309
|
+
if (known)
|
|
310
|
+
return known;
|
|
311
|
+
const spec = name in MATERIALS ? MATERIALS[name] : MATERIALS.steel;
|
|
312
|
+
const made = new MeshStandardMaterial({ color: spec.color, metalness: spec.metal, roughness: spec.rough, side: DoubleSide, polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1 });
|
|
313
|
+
if (name === "steel" && hasUv) {
|
|
314
|
+
this.steelMap ??= this.repeating(paintSteel(8, 8), 2, 100 / (8 * 1.83));
|
|
315
|
+
made.map = this.steelMap;
|
|
316
|
+
made.color.set("#dfe2e6");
|
|
317
|
+
made.roughness = 0.33;
|
|
318
|
+
}
|
|
319
|
+
if (name === "tile" && hasUv) {
|
|
320
|
+
// the mosaic repeats every 28 flats, round the hull and up it
|
|
321
|
+
this.tileMap ??= this.repeating(paintTiles(28), 2, 100 / 14.14);
|
|
322
|
+
made.map = this.tileMap;
|
|
323
|
+
made.color.set("#ffffff");
|
|
324
|
+
}
|
|
325
|
+
this.dullTile(made, name);
|
|
326
|
+
this.materialCache.set(key, made);
|
|
327
|
+
return made;
|
|
328
|
+
}
|
|
329
|
+
/** Black tiles are matt: without this a bright sky washes them grey. */
|
|
330
|
+
dullTile(material, name) {
|
|
331
|
+
if (name !== "tile" && name !== "tile-plain")
|
|
332
|
+
return;
|
|
333
|
+
material.envMap = this.environment;
|
|
334
|
+
material.envMapIntensity = 0.28;
|
|
335
|
+
material.roughness = Math.max(material.roughness, 0.72);
|
|
336
|
+
}
|
|
337
|
+
repeating(picture, u, v) {
|
|
338
|
+
const texture = new CanvasTexture(picture);
|
|
339
|
+
texture.colorSpace = SRGBColorSpace;
|
|
340
|
+
texture.wrapS = texture.wrapT = RepeatWrapping;
|
|
341
|
+
texture.repeat.set(u, v);
|
|
342
|
+
texture.minFilter = LinearMipmapLinearFilter;
|
|
343
|
+
texture.anisotropy = this.renderer.capabilities.getMaxAnisotropy();
|
|
344
|
+
this.textures.push(texture);
|
|
345
|
+
return texture;
|
|
346
|
+
}
|
|
347
|
+
/** One of the three looks. The camera does not move. */
|
|
348
|
+
setLook(look) {
|
|
349
|
+
this.look = look;
|
|
350
|
+
const spec = LOOKS[look];
|
|
351
|
+
const painted = look === "painted";
|
|
352
|
+
this.outlineMaterial.uniforms.color.value.set(spec.line);
|
|
353
|
+
this.outlineMaterial.uniforms.width.value = spec.outlinePx * this.renderer.getPixelRatio();
|
|
354
|
+
for (const part of this.parts) {
|
|
355
|
+
for (const { mesh, name, hasUv } of part.faces)
|
|
356
|
+
mesh.material = painted ? this.paintedMaterial(name, hasUv) : this.lineMaterial(name, look, hasUv);
|
|
357
|
+
for (const outline of part.outlines)
|
|
358
|
+
outline.visible = !painted;
|
|
359
|
+
for (const lines of part.feature) {
|
|
360
|
+
const material = lines.material;
|
|
361
|
+
material.color.set(painted ? "#2b2e33" : spec.line);
|
|
362
|
+
material.linewidth = spec.featurePx;
|
|
363
|
+
material.opacity = painted ? 0.5 : 1;
|
|
364
|
+
material.alphaToCoverage = false;
|
|
365
|
+
}
|
|
366
|
+
for (const lines of part.weld) {
|
|
367
|
+
const material = lines.material;
|
|
368
|
+
material.color.set(painted ? "#2b2e33" : spec.line);
|
|
369
|
+
material.linewidth = spec.weldPx;
|
|
370
|
+
material.opacity = spec.weldOpacity;
|
|
371
|
+
}
|
|
372
|
+
this.useModel(part, painted);
|
|
373
|
+
}
|
|
374
|
+
if (this.glow)
|
|
375
|
+
this.glow.visible = !painted;
|
|
376
|
+
this.canvas.dataset.look = look;
|
|
377
|
+
this.reportPainted();
|
|
378
|
+
this.wake();
|
|
379
|
+
}
|
|
380
|
+
/** Which painted parts are in: "model" when all come from a licensed model, "code" when this package's own paint, "loading" while one is on its way. */
|
|
381
|
+
reportPainted() {
|
|
382
|
+
if (this.look !== "painted") {
|
|
383
|
+
this.canvas.dataset.painted = "";
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const states = this.parts.map((part) => (part.modelFile ? part.modelState : "code"));
|
|
387
|
+
const state = states.some((s) => s === "loading" || s === "none") ? "loading" : states.every((s) => s === "ready") ? "model" : states.includes("ready") ? "mixed" : "code";
|
|
388
|
+
this.canvas.dataset.painted = state;
|
|
389
|
+
this.options.onPainted(state);
|
|
390
|
+
}
|
|
391
|
+
/** The licensed model of a part, fetched the first time the painted look asks for it. Until it is in, and if it fails, this package's own paint shows. */
|
|
392
|
+
useModel(part, painted) {
|
|
393
|
+
if (!part.modelFile)
|
|
394
|
+
return;
|
|
395
|
+
if (part.model) {
|
|
396
|
+
part.model.visible = painted;
|
|
397
|
+
part.code.visible = !painted;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
part.code.visible = true;
|
|
401
|
+
if (!painted || part.modelState !== "none")
|
|
402
|
+
return;
|
|
403
|
+
part.modelState = "loading";
|
|
404
|
+
const generation = this.generation;
|
|
405
|
+
new GLTFLoader()
|
|
406
|
+
.loadAsync(`${this.options.assets}models/${part.modelFile}`)
|
|
407
|
+
.then((gltf) => {
|
|
408
|
+
if (this.disposed || generation !== this.generation)
|
|
409
|
+
return;
|
|
410
|
+
part.model = gltf.scene;
|
|
411
|
+
// the same sheen as this package's own steel, whichever author's hull it is
|
|
412
|
+
gltf.scene.traverse((node) => {
|
|
413
|
+
if (!(node instanceof Mesh))
|
|
414
|
+
return;
|
|
415
|
+
const material = node.material;
|
|
416
|
+
if (material.name === "steel")
|
|
417
|
+
material.roughness = 0.46;
|
|
418
|
+
this.dullTile(material, material.name);
|
|
419
|
+
if (material.name === "tile")
|
|
420
|
+
material.color.setScalar(0.42);
|
|
421
|
+
});
|
|
422
|
+
part.holder.add(gltf.scene);
|
|
423
|
+
part.modelState = "ready";
|
|
424
|
+
this.useModel(part, this.look === "painted");
|
|
425
|
+
this.reportPainted();
|
|
426
|
+
this.wake();
|
|
427
|
+
})
|
|
428
|
+
.catch(() => {
|
|
429
|
+
part.modelState = "failed";
|
|
430
|
+
this.reportPainted();
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
/** Together, apart, or one part alone: an animated move, or a cut under reduced motion. */
|
|
434
|
+
setShow(show) {
|
|
435
|
+
if (!this.built || show === this.show)
|
|
436
|
+
return;
|
|
437
|
+
this.show = show;
|
|
438
|
+
const places = this.built.placesOf(show);
|
|
439
|
+
const fromAngle = this.currentAngle();
|
|
440
|
+
const framing = this.framingFor(fromAngle);
|
|
441
|
+
if (this.options.reducedMotion) {
|
|
442
|
+
for (const part of this.parts) {
|
|
443
|
+
const place = places.find((p) => p.slot === part.slot);
|
|
444
|
+
if (!place)
|
|
445
|
+
continue;
|
|
446
|
+
part.holder.position.set(...place.at);
|
|
447
|
+
part.holder.visible = place.shown;
|
|
448
|
+
}
|
|
449
|
+
this.pose = null;
|
|
450
|
+
this.applyFraming(framing);
|
|
451
|
+
this.wake();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
for (const part of this.parts) {
|
|
455
|
+
const place = places.find((p) => p.slot === part.slot);
|
|
456
|
+
if (!place)
|
|
457
|
+
continue;
|
|
458
|
+
part.from = asVec(part.holder.position);
|
|
459
|
+
const height = this.built.parts.find((p) => p.slot === part.slot)?.record.height ?? 50;
|
|
460
|
+
// a part leaving goes off upwards, or downwards if it is the lower one; a part arriving comes from where it left
|
|
461
|
+
if (!place.shown) {
|
|
462
|
+
part.to = part.holder.visible ? [part.from[0], part.from[1] + (part.slot === this.parts[0].slot ? -height * 1.6 : height * 1.6), part.from[2]] : part.from;
|
|
463
|
+
part.hideAtEnd = true;
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
if (!part.holder.visible) {
|
|
467
|
+
const away = part.slot === this.parts[0].slot ? -height * 1.6 : height * 1.6;
|
|
468
|
+
part.from = [place.at[0], place.at[1] + away, place.at[2]];
|
|
469
|
+
part.holder.position.set(...part.from);
|
|
470
|
+
part.holder.visible = true;
|
|
471
|
+
}
|
|
472
|
+
part.to = place.at;
|
|
473
|
+
part.hideAtEnd = false;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
this.startPose(framing, MOVE_MS, true);
|
|
477
|
+
}
|
|
478
|
+
framingFor(angle) {
|
|
479
|
+
if (!this.built)
|
|
480
|
+
throw new Error("nothing is loaded");
|
|
481
|
+
const { min, max } = this.built.boundsOf(this.show);
|
|
482
|
+
return frameFor(min, max, angle, this.camera.aspect);
|
|
483
|
+
}
|
|
484
|
+
applyFraming(framing) {
|
|
485
|
+
this.camera.position.set(...framing.position);
|
|
486
|
+
this.controls.target.set(...framing.target);
|
|
487
|
+
this.limits(framing.distance);
|
|
488
|
+
this.controls.update();
|
|
489
|
+
}
|
|
490
|
+
limits(distance) {
|
|
491
|
+
this.fitGlow();
|
|
492
|
+
this.camera.near = Math.max(0.2, distance / 40);
|
|
493
|
+
this.camera.far = distance * 8;
|
|
494
|
+
this.camera.updateProjectionMatrix();
|
|
495
|
+
this.controls.minDistance = distance * 0.12;
|
|
496
|
+
this.controls.maxDistance = distance * 1.8;
|
|
497
|
+
this.updateHexSize(distance);
|
|
498
|
+
this.syncSizes();
|
|
499
|
+
}
|
|
500
|
+
/** The tile mosaic is drawn at a constant size on screen, as the drawing's is. */
|
|
501
|
+
updateHexSize(distance) {
|
|
502
|
+
const metresPerPicturePx = (2 * distance * Math.tan(MathUtils.degToRad(FOV_DEG / 2))) / PICTURE_H;
|
|
503
|
+
const value = Math.max(0.3, metresPerPicturePx * 9);
|
|
504
|
+
for (const material of this.materialCache.values())
|
|
505
|
+
if (material instanceof ShaderMaterial && "hexMetres" in material.uniforms)
|
|
506
|
+
material.uniforms.hexMetres.value = value;
|
|
507
|
+
}
|
|
508
|
+
startPose(framing, ms, moveParts) {
|
|
509
|
+
// said when it starts, not when a frame happens to be drawn: a slow machine may draw none in the middle of a move
|
|
510
|
+
this.canvas.dataset.moving = "true";
|
|
511
|
+
if (moveParts)
|
|
512
|
+
this.canvas.dataset.moves = String(Number(this.canvas.dataset.moves ?? 0) + 1);
|
|
513
|
+
this.pose = {
|
|
514
|
+
from: { target: this.controls.target.clone(), offset: this.camera.position.clone().sub(this.controls.target) },
|
|
515
|
+
to: { target: new Vector3(...framing.target), offset: new Vector3(...framing.position).sub(new Vector3(...framing.target)) },
|
|
516
|
+
start: performance.now(),
|
|
517
|
+
ms,
|
|
518
|
+
moveParts,
|
|
519
|
+
};
|
|
520
|
+
this.limits(Math.max(framing.distance, this.camera.position.distanceTo(this.controls.target)));
|
|
521
|
+
this.wake();
|
|
522
|
+
}
|
|
523
|
+
finishParts() {
|
|
524
|
+
for (const part of this.parts) {
|
|
525
|
+
if (part.hideAtEnd)
|
|
526
|
+
part.holder.visible = false;
|
|
527
|
+
part.hideAtEnd = false;
|
|
528
|
+
}
|
|
529
|
+
if (this.built)
|
|
530
|
+
this.limits(this.framingFor(this.currentAngle()).distance);
|
|
531
|
+
}
|
|
532
|
+
/** Where the camera is looking from. */
|
|
533
|
+
currentAngle() {
|
|
534
|
+
const offset = this.camera.position.clone().sub(this.controls.target);
|
|
535
|
+
if (offset.lengthSq() === 0)
|
|
536
|
+
return this.homeAngle;
|
|
537
|
+
const s = new Spherical().setFromVector3(offset);
|
|
538
|
+
return { az: MathUtils.radToDeg(s.theta), el: 90 - MathUtils.radToDeg(s.phi) };
|
|
539
|
+
}
|
|
540
|
+
/** Turns to look from an angle. */
|
|
541
|
+
setAngle(angle, animate) {
|
|
542
|
+
if (!this.built)
|
|
543
|
+
return;
|
|
544
|
+
this.controls.autoRotate = false;
|
|
545
|
+
const framing = this.framingFor(angle);
|
|
546
|
+
if (animate && !this.options.reducedMotion)
|
|
547
|
+
this.startPose(framing, VIEW_MS, false);
|
|
548
|
+
else
|
|
549
|
+
this.applyFraming(framing);
|
|
550
|
+
this.wake();
|
|
551
|
+
}
|
|
552
|
+
frameTo(angle, animate) {
|
|
553
|
+
this.homeAngle = angle;
|
|
554
|
+
this.setAngleKeepingSpin(angle, animate);
|
|
555
|
+
}
|
|
556
|
+
setAngleKeepingSpin(angle, animate) {
|
|
557
|
+
if (!this.built)
|
|
558
|
+
return;
|
|
559
|
+
const framing = this.framingFor(angle);
|
|
560
|
+
if (animate && !this.options.reducedMotion)
|
|
561
|
+
this.startPose(framing, VIEW_MS, false);
|
|
562
|
+
else
|
|
563
|
+
this.applyFraming(framing);
|
|
564
|
+
}
|
|
565
|
+
setHome(angle) {
|
|
566
|
+
this.homeAngle = angle;
|
|
567
|
+
}
|
|
568
|
+
setVisible(visible) {
|
|
569
|
+
this.visible = visible;
|
|
570
|
+
if (visible)
|
|
571
|
+
this.wake();
|
|
572
|
+
}
|
|
573
|
+
setSpin(spin) {
|
|
574
|
+
this.controls.autoRotateSpeed = autoRotateOf(spin);
|
|
575
|
+
this.controls.autoRotate = !this.options.reducedMotion && spin.seconds > 0;
|
|
576
|
+
this.canvas.dataset.spin = `${spin.seconds}${spin.way}`;
|
|
577
|
+
this.wake();
|
|
578
|
+
}
|
|
579
|
+
setTilt(azimuthDeg, polarDeg) {
|
|
580
|
+
this.tilt.azimuth = MathUtils.degToRad(MathUtils.clamp(azimuthDeg, -25, 25));
|
|
581
|
+
this.tilt.polar = MathUtils.degToRad(MathUtils.clamp(polarDeg, -12, 12));
|
|
582
|
+
this.wake();
|
|
583
|
+
}
|
|
584
|
+
dispose() {
|
|
585
|
+
this.disposed = true;
|
|
586
|
+
cancelAnimationFrame(this.frame);
|
|
587
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
588
|
+
this.resize.disconnect();
|
|
589
|
+
this.controls.dispose();
|
|
590
|
+
this.disposeTree(this.root);
|
|
591
|
+
for (const material of this.materialCache.values())
|
|
592
|
+
material.dispose();
|
|
593
|
+
this.outlineMaterial.dispose();
|
|
594
|
+
for (const texture of this.textures)
|
|
595
|
+
texture.dispose();
|
|
596
|
+
this.environment.dispose();
|
|
597
|
+
this.renderer.dispose();
|
|
598
|
+
this.renderer.forceContextLoss();
|
|
599
|
+
this.canvas.remove();
|
|
600
|
+
}
|
|
601
|
+
disposeTree(tree) {
|
|
602
|
+
tree.traverse((node) => {
|
|
603
|
+
const geometry = node.geometry;
|
|
604
|
+
geometry?.dispose();
|
|
605
|
+
if (node instanceof LineSegments2)
|
|
606
|
+
node.material.dispose();
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
addGlow() {
|
|
610
|
+
if (!this.built || this.glow)
|
|
611
|
+
return;
|
|
612
|
+
const texture = new CanvasTexture(paintGlow("rgba(108,208,255,0.16)", "rgba(108,208,255,0)"));
|
|
613
|
+
this.textures.push(texture);
|
|
614
|
+
const plane = new Mesh(new PlaneGeometry(1, 1), new MeshBasicMaterial({ map: texture, transparent: true, depthWrite: false }));
|
|
615
|
+
plane.rotation.x = -Math.PI / 2;
|
|
616
|
+
plane.position.y = -0.02;
|
|
617
|
+
this.glow = plane;
|
|
618
|
+
this.scene.add(plane);
|
|
619
|
+
this.fitGlow();
|
|
620
|
+
}
|
|
621
|
+
/** The pool of light is as wide as what stands in it. */
|
|
622
|
+
fitGlow() {
|
|
623
|
+
if (!this.built || !this.glow)
|
|
624
|
+
return;
|
|
625
|
+
const { min, max } = this.built.boundsOf(this.show);
|
|
626
|
+
const span = Math.max(max[0] - min[0], max[2] - min[2]);
|
|
627
|
+
this.glow.scale.setScalar(span * 3);
|
|
628
|
+
this.glow.position.x = (min[0] + max[0]) / 2;
|
|
629
|
+
this.glow.position.z = (min[2] + max[2]) / 2;
|
|
630
|
+
}
|
|
631
|
+
onVisibility = () => {
|
|
632
|
+
if (!document.hidden)
|
|
633
|
+
this.wake();
|
|
634
|
+
};
|
|
635
|
+
touched() {
|
|
636
|
+
this.controls.autoRotate = false;
|
|
637
|
+
this.pose = null;
|
|
638
|
+
this.wake();
|
|
639
|
+
}
|
|
640
|
+
wake() {
|
|
641
|
+
this.lastActive = performance.now();
|
|
642
|
+
this.needsDraw = true;
|
|
643
|
+
if (!this.frame && this.visible && !document.hidden && !this.disposed)
|
|
644
|
+
this.frame = requestAnimationFrame(this.tick);
|
|
645
|
+
}
|
|
646
|
+
tick = (now) => {
|
|
647
|
+
this.frame = 0;
|
|
648
|
+
if (!this.visible || document.hidden || this.disposed)
|
|
649
|
+
return;
|
|
650
|
+
this.clock.update(now);
|
|
651
|
+
const dt = Math.min(this.clock.getDelta(), 0.1);
|
|
652
|
+
const ambient = !this.options.reducedMotion && now - this.lastActive < IDLE_STOP_MS;
|
|
653
|
+
const t = this.tilt;
|
|
654
|
+
const dA = (t.azimuth - t.applied.azimuth) * 0.12;
|
|
655
|
+
const dP = (t.polar - t.applied.polar) * 0.12;
|
|
656
|
+
if (Math.abs(dA) > 1e-5 || Math.abs(dP) > 1e-5) {
|
|
657
|
+
this.orbit(dA, dP);
|
|
658
|
+
t.applied.azimuth += dA;
|
|
659
|
+
t.applied.polar += dP;
|
|
660
|
+
this.needsDraw = true;
|
|
661
|
+
}
|
|
662
|
+
if (this.pose) {
|
|
663
|
+
const { from, to, start, ms, moveParts } = this.pose;
|
|
664
|
+
const k = Math.min(1, (performance.now() - start) / ms);
|
|
665
|
+
const e = k < 0.5 ? 4 * k * k * k : 1 - (-2 * k + 2) ** 3 / 2;
|
|
666
|
+
this.controls.target.lerpVectors(from.target, to.target, e);
|
|
667
|
+
// round the circle, not through the rocket
|
|
668
|
+
const a = new Spherical().setFromVector3(from.offset);
|
|
669
|
+
const b = new Spherical().setFromVector3(to.offset);
|
|
670
|
+
let dTheta = b.theta - a.theta;
|
|
671
|
+
while (dTheta > Math.PI)
|
|
672
|
+
dTheta -= 2 * Math.PI;
|
|
673
|
+
while (dTheta < -Math.PI)
|
|
674
|
+
dTheta += 2 * Math.PI;
|
|
675
|
+
const s = new Spherical(MathUtils.lerp(a.radius, b.radius, e), MathUtils.lerp(a.phi, b.phi, e), a.theta + dTheta * e);
|
|
676
|
+
this.camera.position.copy(this.controls.target).add(new Vector3().setFromSpherical(s));
|
|
677
|
+
if (moveParts)
|
|
678
|
+
for (const part of this.parts) {
|
|
679
|
+
// lifts clear first, then across, then down: a stage does not slide through another
|
|
680
|
+
const across = Math.min(1, Math.max(0, (e - 0.15) / 0.7));
|
|
681
|
+
part.holder.position.set(MathUtils.lerp(part.from[0], part.to[0], across), MathUtils.lerp(part.from[1], part.to[1], e), MathUtils.lerp(part.from[2], part.to[2], across));
|
|
682
|
+
}
|
|
683
|
+
this.needsDraw = true;
|
|
684
|
+
if (k >= 1) {
|
|
685
|
+
const done = this.pose.moveParts;
|
|
686
|
+
this.pose = null;
|
|
687
|
+
if (done)
|
|
688
|
+
this.finishParts();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (!ambient)
|
|
692
|
+
this.controls.autoRotate = false;
|
|
693
|
+
const changed = this.controls.update(dt);
|
|
694
|
+
if (this.needsDraw || changed || this.pose) {
|
|
695
|
+
this.syncSizes();
|
|
696
|
+
this.renderer.render(this.scene, this.camera);
|
|
697
|
+
this.needsDraw = false;
|
|
698
|
+
this.frames++;
|
|
699
|
+
this.canvas.dataset.frames = String(this.frames);
|
|
700
|
+
const angle = this.currentAngle();
|
|
701
|
+
this.canvas.dataset.azimuth = angle.az.toFixed(2);
|
|
702
|
+
this.canvas.dataset.elevation = angle.el.toFixed(2);
|
|
703
|
+
this.canvas.dataset.moving = this.pose ? "true" : "false";
|
|
704
|
+
if (this.glow)
|
|
705
|
+
this.glow.visible = this.look !== "painted" && this.camera.position.y > 0;
|
|
706
|
+
}
|
|
707
|
+
if ((ambient && this.controls.autoRotate) || changed || this.pose || this.needsDraw || Math.abs(dA) > 1e-5)
|
|
708
|
+
this.frame = requestAnimationFrame(this.tick);
|
|
709
|
+
};
|
|
710
|
+
syncSizes() {
|
|
711
|
+
const size = this.renderer.getDrawingBufferSize(new Vector2());
|
|
712
|
+
this.outlineMaterial.uniforms.size.value.copy(size);
|
|
713
|
+
}
|
|
714
|
+
fit() {
|
|
715
|
+
const w = Math.max(1, this.holder.clientWidth);
|
|
716
|
+
const h = Math.max(1, this.holder.clientHeight);
|
|
717
|
+
this.renderer.setSize(w, h, false);
|
|
718
|
+
this.camera.aspect = w / h;
|
|
719
|
+
this.camera.updateProjectionMatrix();
|
|
720
|
+
this.syncSizes();
|
|
721
|
+
if (this.built && !this.pose)
|
|
722
|
+
this.applyFraming(this.framingFor(this.currentAngle()));
|
|
723
|
+
this.wake();
|
|
724
|
+
}
|
|
725
|
+
orbit(dAzimuth, dPolar) {
|
|
726
|
+
const offset = this.camera.position.clone().sub(this.controls.target);
|
|
727
|
+
const s = new Spherical().setFromVector3(offset);
|
|
728
|
+
s.theta += dAzimuth;
|
|
729
|
+
s.phi = MathUtils.clamp(s.phi + dPolar, this.controls.minPolarAngle, this.controls.maxPolarAngle);
|
|
730
|
+
this.camera.position.copy(this.controls.target).add(new Vector3().setFromSpherical(s));
|
|
731
|
+
}
|
|
732
|
+
key(event) {
|
|
733
|
+
const step = MathUtils.degToRad(15);
|
|
734
|
+
const moves = {
|
|
735
|
+
ArrowLeft: () => this.orbit(-step, 0),
|
|
736
|
+
ArrowRight: () => this.orbit(step, 0),
|
|
737
|
+
ArrowUp: () => this.orbit(0, -step * 0.66),
|
|
738
|
+
ArrowDown: () => this.orbit(0, step * 0.66),
|
|
739
|
+
"+": () => this.zoom(0.85),
|
|
740
|
+
"=": () => this.zoom(0.85),
|
|
741
|
+
"-": () => this.zoom(1 / 0.85),
|
|
742
|
+
Home: () => this.setAngle(this.homeAngle, true),
|
|
743
|
+
"0": () => this.setAngle(this.homeAngle, true),
|
|
744
|
+
};
|
|
745
|
+
const move = moves[event.key];
|
|
746
|
+
if (!move)
|
|
747
|
+
return;
|
|
748
|
+
event.preventDefault();
|
|
749
|
+
this.touched();
|
|
750
|
+
move();
|
|
751
|
+
this.wake();
|
|
752
|
+
}
|
|
753
|
+
zoom(factor) {
|
|
754
|
+
const offset = this.camera.position.clone().sub(this.controls.target);
|
|
755
|
+
const length = MathUtils.clamp(offset.length() * factor, this.controls.minDistance, this.controls.maxDistance);
|
|
756
|
+
this.camera.position.copy(this.controls.target).add(offset.setLength(length));
|
|
757
|
+
}
|
|
758
|
+
}
|