# Research: native CesiumJS in TvT.js / Fes.js / Vite **Ticket:** [Cesium integration into Fes and Vite](../issues/06-cesium-fes-vite-integration.md) **Branch:** `research/cesium-fes-vite` **Date:** 2026-04-10 **Scope:** decision facts only. No production cockpit code. ## Decision Add the official npm package `cesium`. Load it with ESM named imports from a cockpit **map service** (not a TvT plugin). Copy Cesium’s four static directories and set `CESIUM_BASE_URL` **before** the `cesium` import, using this app’s relative `publicPath: './'` (and qiankun’s injected public path when present). Do **not** use `vite-plugin-cesium`, `vue-cesium`, CDN `Cesium.js`, `simpleGIS`, `geokit`, `3d-tiles-renderer`, or `three-tile` as the MapCanvas runtime. --- ## 1. Package and load method ### Repo fact `package.json` has no `cesium`. GIS-adjacent deps that **are** present: `three@0.180.0`, `@tresjs/core@5.2.1`, `3d-tiles-renderer@0.4.16`, `three-tile@0.11.10`, `@icegl/geokit@^2.0.3`. (`package.json`) ### Official load method CesiumJS Quickstart (Cesium 1.145 docs) gives two supported setups: CDN script, or **`npm install cesium`** for a module bundler. The npm recipe is: ```js window.CESIUM_BASE_URL = '/'; import { Cartesian3, Ion, Terrain, Viewer } from 'cesium'; import 'cesium/Build/Cesium/Widgets/widgets.css'; ``` > “The `window.CESIUM_BASE_URL` global variable must be set **before CesiumJS is imported**. It must point to the URL where those four directories are served.” > > Directories to copy and serve as static files: > - `node_modules/cesium/Build/Cesium/Workers` > - `node_modules/cesium/Build/Cesium/ThirdParty` > - `node_modules/cesium/Build/Cesium/Assets` > - `node_modules/cesium/Build/Cesium/Widgets` Source: [CesiumJS Quickstart — Install with NPM](https://cesium.com/learn/cesiumjs-learn/cesiumjs-quickstart/#install-with-npm) The first-party bundler example is Webpack, not Vite: [CesiumGS/cesium-webpack-example TUTORIAL.md](https://github.com/CesiumGS/cesium-webpack-example/blob/main/TUTORIAL.md). It copies Workers / Assets / Widgets (and later ThirdParty in the quickstart) and defines `CESIUM_BASE_URL` at compile time. ### What not to use as the primary integration | Option | Why not | | --- | --- | | CDN `Cesium.js` | Second global copy; fights `publicPath: './'`, qiankun, and federation ESM. Quickstart lists it as the non-bundler path. | | `vite-plugin-cesium` (nshen) | Community convenience wrapper, **not** first-party. Injects HTML/scripts and copies the same four dirs. High collision risk with Fes HTML plugin, `@originjs/vite-plugin-federation`, and qiankun public-path injection. Acceptable only as a last-resort adapter of the official copy recipe. [nshen/vite-plugin-cesium](https://github.com/nshen/vite-plugin-cesium) | | `vue-cesium` / Resium | Vue/React wrappers. Ticket 03 needs replacing default camera in a map service; wrappers fight that. | | `@cesium/engine` + `@cesium/widgets` split | Official quickstart uses the `cesium` meta-package. No reason to split. | | `3d-tiles-renderer` / `three-tile` / `@icegl/geokit` | Three.js GIS, not native CesiumJS. ADR 0004 already chose Cesium as the main map. | ### ESM import-order constraint ES module `import` is hoisted. `window.CESIUM_BASE_URL = x` in the same file as `import { Viewer } from 'cesium'` is **too late**. Required pattern: 1. A tiny module that **only** sets `window.CESIUM_BASE_URL` (and has no `cesium` import). 2. Map service imports that module first, then `cesium`. 3. Optionally also `define: { CESIUM_BASE_URL: JSON.stringify(...) }` like the Webpack example, but qiankun needs a **runtime** `window.CESIUM_BASE_URL` override. --- ## 2. How this repo’s Vite / Fes stack maps onto Cesium ### `publicPath: './'` → Vite `base` `.fes.js`: ```js publicPath: './', // './' 若在线部署用于生产环境 且 是使用qiankun微前端时,需要配置base为主应用地址 ``` `@fesjs/builder-vite` `getInnerCommonConfig`: ```js const publicPath = base || api.config.publicPath || '/'; return deepmerge({ base: publicPath, define: getDefine(api, publicPath), ... }, otherViteOption); ``` `@fesjs/utils` `resolveRuntimeEnv`: `env.BASE_URL = publicPath`. So in this app: - Vite `base` is `./` - `process.env.BASE_URL` (Fes convention used in `src/`) is `./` - `import.meta.env.BASE_URL` is also `./` Vite relative base: all generated URLs are relative to each file. Dynamic URLs must use `import.meta.env.BASE_URL` written exactly that way. Source: [Vite — Relative base](https://vitejs.dev/guide/build.html#relative-base) ### Hash router makes `./cesium/` safe Fes default `router.mode` is `"hash"` (`@fesjs/preset-built-in` route plugin). `.fes.js` does not override it. Document URL stays at the HTML file (`index.html`); `#/cockpit` does not change how `./cesium/Workers/...` resolves. If someone later switches to `history` mode, `./cesium/` from `/cockpit` vs `/cockpit/` would split. Do **not** silently change router mode without revisiting `CESIUM_BASE_URL`. ### Recommended `CESIUM_BASE_URL` Join, always with a trailing slash: ```text + 'cesium/' ``` Resolution order: 1. `qiankunWindow.__INJECTED_PUBLIC_PATH_BY_QIANKUN__` when `__POWERED_BY_QIANKUN__` (already handled in `src/app.jsx` `beforeRender`). 2. Else `new URL('cesium/', new URL(process.env.BASE_URL || './', document.baseURI)).href` so `./` becomes an absolute URL against the HTML document, not a JS chunk. Do **not** hardcode `CESIUM_BASE_URL = '/cesium/'`. That ignores `publicPath: './'` and qiankun. Workers, Assets, Widgets, ThirdParty are loaded from that base. They must **not** be processed by Vite’s worker plugin; copy the prebuilt files from `node_modules/cesium/Build/Cesium/*`. ### Where to copy static files Put them at dist/dev root `cesium/{Workers,ThirdParty,Assets,Widgets}/`, **not** under `public/plugins/`. Spec already assigns cockpit statics to `public/cockpit`. Cesium engine files are a build-time copy from `node_modules`, not a committed public tree. Implement the copy as an extra plugin in `.fes.js` `viteOption.plugins` (small `vite-plugin-static-copy` or a 20-line copy plugin). Must run in: | Mode | Why it matters | | --- | --- | | `yarn dev` / `yarn build` | default Vite `copyPublicDir: true` | | `yarn pre.dev` / `yarn pre.build` | same host app | | `yarn pre.dev.one` / `yarn pre.build.one` | `.fes.predev.one.js` sets `copyPublicDir: false` and uses `copySelectedPublicDirsPlugin`, which copies **all non-`plugins` public dirs**. A `public/cesium` tree would copy; a Vite plugin copy from `node_modules` is still required for dev and to avoid committing Cesium blobs. | Cockpit is a `src/pages` route, not a plugin, so `FES_APP_PLSNAME` filtering does not drop it. ### Federation `.fes.js` federation: ```js federation({ name: 'tvt-host', remotes: { home: { external: `Promise.resolve('https://dcser.icegl.cn/assets/remoteEntry.js')`, externalType: 'promise' } }, shared: { vue: {}, three: { version: '0.180.0' } } }) ``` Constraints: - Do **not** add `cesium` to `shared`. Remotes do not need it. Cesium is huge and `CESIUM_BASE_URL` is a process-wide singleton. - Keep `build.target: 'esnext'` (already set; good for Cesium ESM). - Add `cesium` as its own `manualChunks` entry next to existing `three` / `3d-tiles-renderer` splits. - Do **not** let a Cesium Vite plugin rewrite `index.html` in ways that break federation’s `__rf_placeholder__shareScope` or Fes HTML injection. ### TresJS / Three.js - `src/app.jsx` does `app.use(Tres)` globally. That only registers custom elements; it does **not** create a canvas. - Default `src/pages/index.vue` is a `TresCanvas`. MapCanvas must **not** be a `TresCanvas`. - ADR 0004 / ticket 23: Cesium is the geographic main map; Three.js is a **separate** enhancement for selected objects and must not take over the Cesium scene. They share domain state, not a WebGL context. - No need to put Three.js inside Cesium’s renderer for tickets 02/03/21. Conflicts that **do** exist if ignored: - Wrapping MapCanvas in `TresCanvas` / `OrbitControls` (simpleGIS pattern) would steal pointer events from the Cesium camera contract. - Two live WebGL contexts at once (Cesium globe + Tres overlay) is allowed by browsers but expensive; ticket 23 already says close the enhancement and return to the map. - `optimizeDeps.exclude` currently only lists `@fesjs/fes`. Cesium should be explicitly `optimizeDeps.include` (or left to Vite) — do not exclude it in a way that double-bundles workers. ### qiankun `.fes.js` enables `qiankun.micro.useDevMode: true`. `src/app.jsx` copies `__INJECTED_PUBLIC_PATH_BY_QIANKUN__` onto `window`. Comment on `publicPath` says production-as-micro must set base to the host URL (example `https://cos.icegl.cn/qiankun/tvt/`). Cesium workers will 404 unless `CESIUM_BASE_URL` uses that injected path. Cockpit is still **not** delivered as a TvT plugin; it is a first-class page **inside** this host, which itself can be a qiankun micro. --- ## 3. Existing plugin cases: reference vs forbidden runtime ADR 0007: development may look at `src/plugins`; delivery is `src/pages/cockpit` + `src/modules/cockpit`. Large plugins are not a cockpit runtime dependency. Spec: static assets in `public/cockpit`. ### Must NOT be MapCanvas runtime | Case | What it actually is | Why forbidden | | --- | --- | --- | | `src/plugins/simpleGIS` | TresJS + `3d-tiles-renderer` / `three-tile` / custom satellite map. Preview pages under `/plugins/simpleGIS/*`. | TvT plugin. Three.js globe, not Cesium Viewer. | | `simpleGIS/pages/cesiumIon.vue` + `cesiumIon3DTile.vue` | `TilesRenderer` + `CesiumIonAuthPlugin` with a **hardcoded Ion JWT** (`assetId: '354759'`). | Name says Cesium; implementation is NASA AMMOS 3d-tiles-renderer. Ticket 21 forbids hardcoded Ion tokens. | | `simpleGIS/pages/renderer3DTiles.vue` | TresCanvas + OrbitControls + `tilesMesh` (`TilesRenderer` on a CDN `tileset.json`). | Three.js tiles, OrbitControls fights ticket 03. | | `src/plugins/geokit` + `@icegl/geokit` | Marketing copy: “是否在为cesium复杂的sdk烦恼?” — a **Three.js alternative** to Cesium. | Explicitly not Cesium. | | `src/plugins/useThreeGeospatial` | Ports Cesium math into Three.js. | Algorithm reference only. | | `three-tile` examples in simpleGIS | Three.js tile map. | Wrong renderer. | ### Useful as **reference only** (copy ideas, not imports) - Relative asset habit: `process.env.BASE_URL`, Draco path `'./draco/'` in `cesiumIon3DTile.vue` — same class of bug as `CESIUM_BASE_URL` with `publicPath: './'`. - `.fes.js` `/tianditu.map` proxy — ticket 21 says 天地图 is for basemap **validation**, not the production globe source. - 3D Tiles loading / empty / error UX from `tilesMesh.vue` (`load-model`, bounding sphere) — reimplement with `Cesium3DTileset` events, do not import `PLS/simpleGIS`. - `copySelectedPublicDirsPlugin.js` — cockpit + cesium static dirs that live outside `public/plugins` survive `pre.build.one`. Do **not** `import … from 'PLS/simpleGIS'` (or geokit) from cockpit modules. --- ## 4. Camera: replacing Cesium defaults Official defaults on `ScreenSpaceCameraController` (Cesium 1.145): | Flag / event | Default | Meaning in 3D | | --- | --- | --- | | `enableInputs` | `true` | Master switch; **temporary** (flights / drag-select). Docs: to keep inputs off, use the other booleans. | | `enableRotate` | `true` | `rotateEventTypes` default `LEFT_DRAG` | | `enableZoom` | `true` | `zoomEventTypes` default `[RIGHT_DRAG, WHEEL, PINCH]` | | `enableTilt` | `true` | middle-drag / pinch / ctrl+drag | | `enableLook` | `true` | shift+left-drag | | `enableTranslate` | `true` | **2D / Columbus only** | Sources: [ScreenSpaceCameraController](https://cesium.com/learn/cesiumjs/ref-doc/ScreenSpaceCameraController.html), [Control the camera](https://cesium.com/learn/cesiumjs-learn/cesiumjs-camera/) ADR 0008 / ticket 03 want: left-drag **pan**, right-drag **orbit around pivot**, wheel **spring-arm length**. That is the inverse of Cesium 3D defaults (left = rotate, wheel/right = zoom). Spring-arm primitive already in official camera guide: ```js const transform = Cesium.Transforms.eastNorthUpToFixedFrame(center); viewer.scene.camera.lookAtTransform(transform, new Cesium.HeadingPitchRange(0, -Math.PI / 4, 2900)); ``` Then orbit with `camera.rotateRight` on tick, or (for the cockpit) a custom `ScreenSpaceEventHandler` that writes heading/pitch/range. Implementation constraint: disable `enableRotate`, `enableZoom`, `enableTilt`, `enableLook` (and do not rely on `enableInputs = false` alone). Own the handler in the map service. Do not keep Viewer chrome (geocoder, home, timeline, …) that reintroduces default navigation. --- ## 5. Constraints for implementation tickets (conclusions only) ### 02 — MapCanvas persistent mount - Host is a **plain DOM container** in `/cockpit` (`src/pages/cockpit*`), not `src/plugins/**` and not `TresCanvas`. - `new Viewer(container, { …widgets off })` once in `src/modules/cockpit` map service. Mode / `expandedPanelIds` must not `v-if` the container. - On panel expand/collapse the container size changes → `viewer.resize()`. - `viewer.destroy()` only when leaving `/cockpit`. - `cesium` import is isolated to the map service so `CESIUM_BASE_URL` is set once. ### 03 — Camera control contract - After Viewer construct, turn off default SSCC rotate/zoom/tilt/look. - Implement pan / orbit / spring-arm on `lookAtTransform` + `HeadingPitchRange` + `ScreenSpaceEventHandler` in the **same** map service. - Do not use Tres `OrbitControls` (simpleGIS) and do not use vue-cesium mouse bindings. ### 21 — 3D Tiles and terrain baseline - Load tiles with `Cesium3DTileset.fromUrl(configUrl)` (official example uses `http://localhost:8002/tilesets/Seattle/tileset.json`). Source: [Cesium3DTileset](https://cesium.com/learn/cesiumjs/ref-doc/Cesium3DTileset.html) - Load terrain with `CesiumTerrainProvider.fromUrl(configUrl)` for quantized-mesh / heightmap. No terrain → `EllipsoidTerrainProvider`. Source: [CesiumTerrainProvider.fromUrl](https://cesium.com/learn/cesiumjs/ref-doc/CesiumTerrainProvider.html#.fromUrl), [Visualizing 3D Terrain](https://cesium.com/learn/cesiumjs-learn/cesiumjs-terrain/) - **Forbidden:** `Terrain.fromWorldTerrain()`, `CesiumTerrainProvider.fromIonAssetId`, `createOsmBuildingsAsync`, `Ion.defaultAccessToken`, simpleGIS’s hardcoded Ion JWT. - URLs and any future token come from ticket 04 config injection, not source. - Loading / success / fail / empty: `fromUrl` try/catch + `tileset.allTilesLoaded` / provider `errorEvent`. Do not use `3d-tiles-renderer` events. - Ground vs pipeline view is camera + data layers on this same Viewer, not a second engine. --- ## 6. Suggested wiring (not implemented) In `.fes.js` `viteOption` (later implementation ticket): 1. Dependency: `cesium` (runtime). 2. Plugin: copy the four `Build/Cesium/{Workers,ThirdParty,Assets,Widgets}` dirs to `cesium/`. 3. `manualChunks.cesium = ['cesium']`. 4. Do not add Cesium to federation `shared`. 5. Map service: set `CESIUM_BASE_URL`, import `cesium` + `widgets.css`, construct Viewer, disable default camera, expose resize/destroy. Cockpit page: mount `
` once; pass the element to the map service. --- ## Sources ### High-trust, this repo - `package.json` — no `cesium`; Tres / three / 3d-tiles-renderer / three-tile / geokit present - `.fes.js` — `publicPath: './'`, federation shared `vue`+`three`, qiankun micro, tianditu proxy, `manualChunks` - `.fes.predev.one.js` — `copyPublicDir: false` - `src/common/copySelectedPublicDirsPlugin.js` — copies all non-`plugins` public dirs - `src/app.jsx` — `app.use(Tres)`, qiankun public path, plugin page glob - `src/pages/index.vue` — TresCanvas homepage, not cockpit - `src/plugins/simpleGIS/**` — Tres GIS demos; `cesiumIon3DTile.vue` is 3d-tiles-renderer + Ion token - `src/plugins/geokit/config.js` — positions geokit as a Cesium alternative - `node_modules/@fesjs/builder-vite/lib/common/getConfig.js` — `publicPath` → Vite `base` - `node_modules/@fesjs/preset-built-in` — default `router.mode: 'hash'` - `docs/adr/0004`, `0005`, `0006`, `0007`, `0008` - `.scratch/cockpit-shell-panel-layout/spec.md` and issues `02`, `03`, `21`, `23` ### High-trust, official docs - [CesiumJS Quickstart](https://cesium.com/learn/cesiumjs-learn/cesiumjs-quickstart/) — npm `cesium`, four static dirs, `CESIUM_BASE_URL` before import (CDN build **1.145**) - [cesium-webpack-example TUTORIAL.md](https://github.com/CesiumGS/cesium-webpack-example/blob/main/TUTORIAL.md) — first-party copy + define pattern - [Control the camera](https://cesium.com/learn/cesiumjs-learn/cesiumjs-camera/) — `lookAtTransform` / `HeadingPitchRange` / SSCC - [ScreenSpaceCameraController](https://cesium.com/learn/cesiumjs/ref-doc/ScreenSpaceCameraController.html) — default left-drag rotate, wheel/right-drag zoom - [Cesium3DTileset.fromUrl](https://cesium.com/learn/cesiumjs/ref-doc/Cesium3DTileset.html) - [CesiumTerrainProvider.fromUrl](https://cesium.com/learn/cesiumjs/ref-doc/CesiumTerrainProvider.html#.fromUrl) - [Visualizing 3D Terrain](https://cesium.com/learn/cesiumjs-learn/cesiumjs-terrain/) — `fromWorldTerrain` is Ion; local uses a terrain provider URL - [Vite relative base](https://vitejs.dev/guide/build.html#relative-base) ### Medium-trust (community, cited only as a rejected option) - [nshen/vite-plugin-cesium](https://github.com/nshen/vite-plugin-cesium)