diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8cfa869dcee6..3484e3894b2e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -22,6 +22,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_REPO: ${{ github.repository }} GITHUB_SHA: ${{ github.sha }} + BASE_URL: /cesium/${{ github.ref_name }}/ + DEPLOYED_URL: https://ci-builds.cesium.com/cesium/${{ github.ref_name }}/ steps: - uses: actions/checkout@v4 - name: install node 20 @@ -40,6 +42,8 @@ jobs: run: npm pack --workspaces &> /dev/null - name: build apps run: npm run build-apps + - name: build sandcastle v2 + run: npm run build-ci -w packages/sandcastle -- -l warn - uses: ./.github/actions/verify-package - name: deploy to s3 if: ${{ env.AWS_ACCESS_KEY_ID != '' }} diff --git a/.gitignore b/.gitignore index 2356a17e08cf..056370a688f2 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ Thumbs.db /Apps/Sandcastle/jsHintOptions.js /Apps/Sandcastle/gallery/gallery-index.js /Apps/Sandcastle/templates/bucket.css +/Apps/Sandcastle2 /Source/Assets/ /Source/**/*.d.ts @@ -43,4 +44,4 @@ yarn.lock .idea/shelf # Used in the CLA checking GitHub workflow -GoogleConfig.json \ No newline at end of file +GoogleConfig.json diff --git a/.markdownlintignore b/.markdownlintignore index 834910312616..2732a1a6ed23 100644 --- a/.markdownlintignore +++ b/.markdownlintignore @@ -1,4 +1,5 @@ /node_modules +packages/sandcastle/node_modules /ThirdParty /Tools/** diff --git a/.prettierignore b/.prettierignore index 82962ae0ee9c..f79e4c14cdc4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -19,6 +19,7 @@ !**/*.html !**/*.md !**/*.ts +!**/*.tsx # Re-ignore a few things caught above @@ -34,6 +35,9 @@ packages/widgets/Build/** packages/widgets/index.js packages/widgets/Source/ThirdParty/** +packages/sandcastle/node_modules/** +Apps/Sandcastle2/** + Specs/jasmine/** Apps/Sandcastle/ThirdParty diff --git a/eslint.config.js b/eslint.config.js index 39649c64b28a..48d57253b32f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,6 +1,9 @@ import globals from "globals"; import html from "eslint-plugin-html"; import configCesium from "@cesium/eslint-config"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; export default [ { @@ -15,6 +18,8 @@ export default [ "Apps/HelloWorld.html", "Apps/Sandcastle/jsHintOptions.js", "Apps/Sandcastle/gallery/gallery-index.js", + "Apps/Sandcastle2/", + "packages/sandcastle/public/", "packages/engine/Source/Scene/GltfPipeline/**/*", "packages/engine/Source/Shaders/**/*", "Specs/jasmine/*", @@ -74,6 +79,31 @@ export default [ sourceType: "module", }, }, + ...[...tseslint.configs.recommended].map((config) => ({ + // This is needed to restrict to a specific path unless using the tseslint.config function + // https://typescript-eslint.io/packages/typescript-eslint#config + ...config, + files: ["packages/sandcastle/**/*.{ts,tsx}"], + })), + { + // This config came from the vite project generation + files: ["packages/sandcastle/**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + }, + }, { files: ["Specs/**/*", "packages/**/Specs/**/*"], languageOptions: { diff --git a/gulpfile.js b/gulpfile.js index b1518b0d6258..ebc01b459649 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -43,8 +43,18 @@ if (/\.0$/.test(version)) { version = version.substring(0, version.length - 2); } const karmaConfigFile = resolve("./Specs/karma.conf.cjs"); +function getWorkspaces(onlyDependencies = false) { + const dependencies = Object.keys(packageJson.dependencies); + return onlyDependencies + ? packageJson.workspaces.filter((workspace) => { + return dependencies.includes( + workspace.replace("packages", `@${scope}`), + ); + }) + : packageJson.workspaces; +} -const devDeployUrl = "https://ci-builds.cesium.com/cesium/"; +const devDeployUrl = process.env.DEPLOYED_URL; const isProduction = process.env.PROD; //Gulp doesn't seem to have a way to get the currently running tasks for setting @@ -247,7 +257,7 @@ export async function buildTs() { } else if (argv.workspace) { workspaces = argv.workspace; } else { - workspaces = packageJson.workspaces; + workspaces = getWorkspaces(true); } // Generate types for passed packages in order. @@ -396,7 +406,7 @@ export async function buildDocs() { stdio: "inherit", env: Object.assign({}, process.env, { CESIUM_VERSION: version, - CESIUM_PACKAGES: packageJson.workspaces, + CESIUM_PACKAGES: getWorkspaces(true), }), }, ); @@ -694,12 +704,10 @@ export async function deployStatus() { const status = argv.status; const message = argv.message; - const deployUrl = `${devDeployUrl + process.env.BRANCH}/`; + const deployUrl = `${devDeployUrl}`; const zipUrl = `${deployUrl}Cesium-${version}.zip`; const npmUrl = `${deployUrl}cesium-${version}.tgz`; - const coverageUrl = `${ - devDeployUrl + process.env.BRANCH - }/Build/Coverage/index.html`; + const coverageUrl = `${devDeployUrl}Build/Coverage/index.html`; return Promise.all([ setStatus(status, deployUrl, message, "deployment"), @@ -1484,8 +1492,8 @@ async function getLicenseDataFromThirdPartyExtra(path, discoveredDependencies) { return result; } - // Resursively check the workspaces - for (const workspace of packageJson.workspaces) { + // Recursively check the workspaces + for (const workspace of getWorkspaces(true)) { const workspacePackageJson = require(`./${workspace}/package.json`); result = await getLicenseDataFromPackage( workspacePackageJson, diff --git a/index.html b/index.html index 37c59ab3a7f4..9c90d4314c53 100644 --- a/index.html +++ b/index.html @@ -33,7 +33,12 @@
  • Sandcastle - (built version) +
  • =18.18.0" }, "lint-staged": { - "*.{js,cjs,mjs,css,html}": [ + "*.{js,cjs,mjs,ts,tsx,css,html}": [ "eslint --cache --quiet", "prettier --write" ], @@ -157,6 +161,7 @@ }, "workspaces": [ "packages/engine", - "packages/widgets" + "packages/widgets", + "packages/sandcastle" ] } diff --git a/packages/sandcastle/.gitignore b/packages/sandcastle/.gitignore new file mode 100644 index 000000000000..acc2a2f6cbda --- /dev/null +++ b/packages/sandcastle/.gitignore @@ -0,0 +1,14 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + diff --git a/packages/sandcastle/README.md b/packages/sandcastle/README.md new file mode 100644 index 000000000000..29a3c232442e --- /dev/null +++ b/packages/sandcastle/README.md @@ -0,0 +1,60 @@ +# CesiumJS Sandcastle + +This package is the application for Sandcastle. + +## Running/Building + +- `npm run dev`: run the development server +- `npm run build`: alias for `npm run build-app` +- `npm run build-app`: build to static files in `/Apps/Sandcastle2` for hosting/access from the root cesium dev server +- `npm run build-ci`: build to static files in `/Apps/Sandcastle2` and configure paths as needed for CI deployment + +Linting and style is managed under the project root's scripts. + +## Expanding the ESLint configuration + + + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config({ + extends: [ + // Remove ...tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + ], + languageOptions: { + // other options... + parserOptions: { + project: ["./tsconfig.node.json", "./tsconfig.app.json"], + tsconfigRootDir: import.meta.dirname, + }, + }, +}); +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from "eslint-plugin-react-x"; +import reactDom from "eslint-plugin-react-dom"; + +export default tseslint.config({ + plugins: { + // Add the react-x and react-dom plugins + "react-x": reactX, + "react-dom": reactDom, + }, + rules: { + // other rules... + // Enable its recommended typescript rules + ...reactX.configs["recommended-typescript"].rules, + ...reactDom.configs.recommended.rules, + }, +}); +``` diff --git a/packages/sandcastle/index.html b/packages/sandcastle/index.html new file mode 100644 index 000000000000..b3a80d2f3bb1 --- /dev/null +++ b/packages/sandcastle/index.html @@ -0,0 +1,31 @@ + + + + + + + Sandcastle Reborn + + + +
    + + + diff --git a/packages/sandcastle/package.json b/packages/sandcastle/package.json new file mode 100644 index 000000000000..81ff809aa96a --- /dev/null +++ b/packages/sandcastle/package.json @@ -0,0 +1,30 @@ +{ + "name": "@cesium/sandcastle", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite --config vite.config.dev.ts", + "build": "npm run build-app", + "build-app": "tsc -b && vite build --config vite.config.app.ts", + "build-ci": "tsc -b && vite build --config vite.config.ci.ts" + }, + "dependencies": { + "@itwin/itwinui-react": "^5.0.0-alpha.14", + "@monaco-editor/react": "^4.7.0", + "monaco-editor": "^0.52.2", + "pako": "^2.1.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/pako": "^2.0.3", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "globals": "^15.15.0", + "typescript": "~5.7.2", + "vite": "^6.2.0", + "vite-plugin-static-copy": "^2.3.1" + } +} diff --git a/packages/sandcastle/public/Sandcastle-client.js b/packages/sandcastle/public/Sandcastle-client.js new file mode 100644 index 000000000000..5acd572c626e --- /dev/null +++ b/packages/sandcastle/public/Sandcastle-client.js @@ -0,0 +1,203 @@ +(function () { + "use strict"; + window.parent.postMessage("reload", "*"); + + function defined(value) { + return value !== undefined; + } + + function print(value) { + if (value === null) { + return "null"; + } else if (defined(value)) { + return value.toString(); + } + return "undefined"; + } + + console.originalLog = console.log; + console.log = function (d1) { + console.originalLog.apply(console, arguments); + window.parent.postMessage( + { + log: print(d1), + }, + "*", + ); + }; + + console.originalWarn = console.warn; + console.warn = function (d1) { + console.originalWarn.apply(console, arguments); + window.parent.postMessage( + { + warn: defined(d1) ? d1.toString() : "undefined", + }, + "*", + ); + }; + + console.originalError = console.error; + console.error = function (d1) { + console.originalError.apply(console, arguments); + if (!defined(d1)) { + window.parent.postMessage( + { + error: "undefined", + }, + "*", + ); + return; + } + + // Look for d1.stack, "bucket.html:line:char" + let lineNumber = -1; + const errorMsg = d1.toString(); + if (typeof d1.stack === "string") { + const stack = d1.stack; + let pos = stack.indexOf(Sandcastle.bucket); + if (pos < 0) { + pos = stack.indexOf(""); + } + if (pos >= 0) { + const lineStart = stack.indexOf(":", pos); + if (lineStart > pos) { + let lineEnd1 = stack.indexOf(":", lineStart + 1); + const lineEnd2 = stack.indexOf("\n", lineStart + 1); + if ( + lineEnd2 > lineStart && + (lineEnd2 < lineEnd1 || lineEnd1 < lineStart) + ) { + lineEnd1 = lineEnd2; + } + if (lineEnd1 > lineStart) { + /*eslint-disable no-empty*/ + try { + lineNumber = parseInt( + stack.substring(lineStart + 1, lineEnd1), + 10, + ); + } catch (ex) {} + /*eslint-enable no-empty*/ + } + } + } + } + + if (lineNumber >= 0) { + window.parent.postMessage( + { + error: errorMsg, + lineNumber: lineNumber, + }, + "*", + ); + } else { + window.parent.postMessage( + { + error: errorMsg, + }, + "*", + ); + } + }; + + window.onerror = function (errorMsg, url, lineNumber) { + if (defined(lineNumber)) { + if (defined(url) && url.indexOf(Sandcastle.bucket) > -1) { + // if the URL is the bucket itself, ignore it + url = ""; + } + if (lineNumber < 1) { + // Change lineNumber to the local one for highlighting. + /*eslint-disable no-empty*/ + try { + let pos = errorMsg.indexOf(`${Sandcastle.bucket}:`); + if (pos < 0) { + pos = errorMsg.indexOf(""); + } + if (pos >= 0) { + pos += 12; + lineNumber = parseInt(errorMsg.substring(pos), 10); + } + } catch (ex) {} + /*eslint-enable no-empty*/ + } + window.parent.postMessage( + { + error: errorMsg, + url: url, + lineNumber: lineNumber, + }, + "*", + ); + } else { + window.parent.postMessage( + { + error: errorMsg, + url: url, + }, + "*", + ); + } + console.originalError.apply(console, [errorMsg]); + return false; + }; + + Sandcastle.declare = function (obj) { + /*eslint-disable no-empty*/ + try { + //Browsers such as IE don't have a stack property until you actually throw the error. + let stack = ""; + try { + throw new Error(); + } catch (ex) { + stack = ex.stack.toString(); + } + let needle = `${Sandcastle.bucket}:`; // Firefox + let pos = stack.indexOf(needle); + if (pos < 0) { + needle = " (:"; // Chrome + pos = stack.indexOf(needle); + } + if (pos < 0) { + needle = " (Unknown script code:"; // IE 11 + pos = stack.indexOf(needle); + } + if (pos >= 0) { + pos += needle.length; + const lineNumber = parseInt(stack.substring(pos), 10); + Sandcastle.registered.push({ + obj: obj, + lineNumber: lineNumber, + }); + } + } catch (ex) {} + /*eslint-enable no-empty*/ + }; + + Sandcastle.highlight = function (obj) { + if (typeof obj !== "undefined") { + for (let i = 0, len = Sandcastle.registered.length; i < len; ++i) { + if ( + obj === Sandcastle.registered[i].obj || + obj.primitive === Sandcastle.registered[i].obj + ) { + window.parent.postMessage( + { + highlight: Sandcastle.registered[i].lineNumber, + }, + "*", + ); + return; + } + } + } + window.parent.postMessage( + { + highlight: 0, + }, + "*", + ); + }; +})(); diff --git a/packages/sandcastle/public/Sandcastle-header.js b/packages/sandcastle/public/Sandcastle-header.js new file mode 100644 index 000000000000..9ab0530ec2aa --- /dev/null +++ b/packages/sandcastle/public/Sandcastle-header.js @@ -0,0 +1,111 @@ +(function () { + "use strict"; + + let defaultAction; + let bucket = window.location.href; + const pos = bucket.lastIndexOf("/"); + if (pos > 0 && pos < bucket.length - 1) { + bucket = bucket.substring(pos + 1); + } + + window.Sandcastle = { + bucket: bucket, + declare: function () {}, + highlight: function () {}, + registered: [], + finishedLoading: function () { + window.Sandcastle.reset(); + + if (defaultAction) { + window.Sandcastle.highlight(defaultAction); + defaultAction(); + defaultAction = undefined; + } + + document.body.className = document.body.className.replace( + /(?:\s|^)sandcastle-loading(?:\s|$)/, + " ", + ); + }, + addToggleButton: function (text, checked, onchange, toolbarID) { + window.Sandcastle.declare(onchange); + const input = document.createElement("input"); + input.checked = checked; + input.type = "checkbox"; + input.style.pointerEvents = "none"; + const label = document.createElement("label"); + label.appendChild(input); + label.appendChild(document.createTextNode(text)); + label.style.pointerEvents = "none"; + const button = document.createElement("button"); + button.type = "button"; + button.className = "cesium-button"; + button.appendChild(label); + + button.onclick = function () { + window.Sandcastle.reset(); + window.Sandcastle.highlight(onchange); + input.checked = !input.checked; + onchange(input.checked); + }; + + document.getElementById(toolbarID || "toolbar").appendChild(button); + }, + addToolbarButton: function (text, onclick, toolbarID) { + window.Sandcastle.declare(onclick); + const button = document.createElement("button"); + button.type = "button"; + button.className = "cesium-button"; + button.onclick = function () { + window.Sandcastle.reset(); + window.Sandcastle.highlight(onclick); + onclick(); + }; + button.textContent = text; + document.getElementById(toolbarID || "toolbar").appendChild(button); + }, + addDefaultToolbarButton: function (text, onclick, toolbarID) { + window.Sandcastle.addToolbarButton(text, onclick, toolbarID); + defaultAction = onclick; + }, + addDefaultToolbarMenu: function (options, toolbarID) { + window.Sandcastle.addToolbarMenu(options, toolbarID); + defaultAction = options[0].onselect; + }, + addToolbarMenu: function (options, toolbarID) { + const menu = document.createElement("select"); + menu.className = "cesium-button"; + menu.onchange = function () { + window.Sandcastle.reset(); + const item = options[menu.selectedIndex]; + if (item && typeof item.onselect === "function") { + item.onselect(); + } + }; + document.getElementById(toolbarID || "toolbar").appendChild(menu); + + if (!defaultAction && typeof options[0].onselect === "function") { + defaultAction = options[0].onselect; + } + + for (let i = 0, len = options.length; i < len; ++i) { + const option = document.createElement("option"); + option.textContent = options[i].text; + option.value = options[i].value; + menu.appendChild(option); + } + }, + reset: function () {}, + }; + + if (window.location.protocol === "file:") { + if ( + window.confirm( + "You must host this app on a web server.\nSee contributor's guide for more info?", + ) + ) { + window.location = + "https://github.com/CesiumGS/cesium/blob/main/Documentation/Contributors/BuildGuide/README.md#quickstart"; + } + } +})(); diff --git a/packages/sandcastle/public/Sandcastle-helpers.js b/packages/sandcastle/public/Sandcastle-helpers.js new file mode 100644 index 000000000000..06d1834429ed --- /dev/null +++ b/packages/sandcastle/public/Sandcastle-helpers.js @@ -0,0 +1,46 @@ +(function () { + "use strict"; + + window.embedInSandcastleTemplate = function (code, addExtraLine) { + return ( + `${ + "window.startup = async function (Cesium) {\n" + + " 'use strict';\n" + + "//Sandcastle_Begin\n" + }${addExtraLine ? "\n" : ""}${code}//Sandcastle_End\n` + + ` Sandcastle.finishedLoading();\n` + + `};\n` + + `if (typeof Cesium !== 'undefined') {\n` + + ` window.startupCalled = true;\n` + + ` window.startup(Cesium).catch((error) => {\n` + + ` "use strict";\n` + + ` console.error(error);\n` + + ` });\n` + + `}\n` + ); + }; + window.decodeBase64Data = function (base64String, pako) { + // data stored in the hash as: + // Base64 encoded, raw DEFLATE compressed JSON array where index 0 is code, index 1 is html + // restore padding + while (base64String.length % 4 !== 0) { + base64String += "="; + } + let jsonString = pako.inflate(atob(base64String), { + raw: true, + to: "string", + }); + // we save a few bytes by omitting the leading [" and trailing "] since they are always the same + jsonString = `["${jsonString}"]`; + const json = JSON.parse(jsonString); + // index 0 is code, index 1 is html + const code = json[0]; + const html = json[1]; + const baseHref = json[2]; + return { + code: code, + html: html, + baseHref: baseHref, + }; + }; +})(); diff --git a/packages/sandcastle/public/favicon.ico b/packages/sandcastle/public/favicon.ico new file mode 100644 index 000000000000..0b05a3cda16b Binary files /dev/null and b/packages/sandcastle/public/favicon.ico differ diff --git a/packages/sandcastle/public/fonts/InterVariable-Italic.woff2 b/packages/sandcastle/public/fonts/InterVariable-Italic.woff2 new file mode 100644 index 000000000000..b3530f3f5269 Binary files /dev/null and b/packages/sandcastle/public/fonts/InterVariable-Italic.woff2 differ diff --git a/packages/sandcastle/public/fonts/InterVariable.woff2 b/packages/sandcastle/public/fonts/InterVariable.woff2 new file mode 100644 index 000000000000..5a8d3e72ad7f Binary files /dev/null and b/packages/sandcastle/public/fonts/InterVariable.woff2 differ diff --git a/packages/sandcastle/public/gallery/3D Models.html b/packages/sandcastle/public/gallery/3D Models.html new file mode 100644 index 000000000000..b1f1d7d47c8c --- /dev/null +++ b/packages/sandcastle/public/gallery/3D Models.html @@ -0,0 +1,183 @@ + + + + + + + + + Cesium Demo + + + + + + +
    +

    Loading...

    +
    + + + diff --git a/packages/sandcastle/public/gallery/3D Models.jpg b/packages/sandcastle/public/gallery/3D Models.jpg new file mode 100644 index 000000000000..b06301c8a0a4 Binary files /dev/null and b/packages/sandcastle/public/gallery/3D Models.jpg differ diff --git a/packages/sandcastle/public/gallery/Billboards.html b/packages/sandcastle/public/gallery/Billboards.html new file mode 100644 index 000000000000..d92b112c9fb7 --- /dev/null +++ b/packages/sandcastle/public/gallery/Billboards.html @@ -0,0 +1,381 @@ + + + + + + + + + Cesium Demo + + + + + + +
    +

    Loading...

    +
    + + + diff --git a/packages/sandcastle/public/gallery/Billboards.jpg b/packages/sandcastle/public/gallery/Billboards.jpg new file mode 100644 index 000000000000..edaddea07046 Binary files /dev/null and b/packages/sandcastle/public/gallery/Billboards.jpg differ diff --git a/packages/sandcastle/public/gallery/Moon.html b/packages/sandcastle/public/gallery/Moon.html new file mode 100644 index 000000000000..42f20e7a7e3f --- /dev/null +++ b/packages/sandcastle/public/gallery/Moon.html @@ -0,0 +1,381 @@ + + + + + + + + + Cesium Moon Terrain + + + + + + +
    +

    Loading...

    +
    + + + diff --git a/packages/sandcastle/public/gallery/Moon.jpg b/packages/sandcastle/public/gallery/Moon.jpg new file mode 100644 index 000000000000..c9d8f75fe2b6 Binary files /dev/null and b/packages/sandcastle/public/gallery/Moon.jpg differ diff --git a/packages/sandcastle/public/gallery/Terrain Exaggeration.html b/packages/sandcastle/public/gallery/Terrain Exaggeration.html new file mode 100644 index 000000000000..121feacece09 --- /dev/null +++ b/packages/sandcastle/public/gallery/Terrain Exaggeration.html @@ -0,0 +1,206 @@ + + + + + + + + + Cesium Demo + + + + + + +
    +

    Loading...

    +
    + + + + + + + + + + + +
    Exaggeration + + +
    Relative Height + + +
    +
    + + + diff --git a/packages/sandcastle/public/gallery/Terrain Exaggeration.jpg b/packages/sandcastle/public/gallery/Terrain Exaggeration.jpg new file mode 100644 index 000000000000..d3d9ed3a5bf4 Binary files /dev/null and b/packages/sandcastle/public/gallery/Terrain Exaggeration.jpg differ diff --git a/packages/sandcastle/public/images/Cesium_Logo_Color.jpg b/packages/sandcastle/public/images/Cesium_Logo_Color.jpg new file mode 100644 index 000000000000..26d622e522bd Binary files /dev/null and b/packages/sandcastle/public/images/Cesium_Logo_Color.jpg differ diff --git a/packages/sandcastle/public/images/Cesium_Logo_Color_Overlay.png b/packages/sandcastle/public/images/Cesium_Logo_Color_Overlay.png new file mode 100644 index 000000000000..2cad188a786e Binary files /dev/null and b/packages/sandcastle/public/images/Cesium_Logo_Color_Overlay.png differ diff --git a/packages/sandcastle/public/images/Cesium_Logo_ETC1S.ktx2 b/packages/sandcastle/public/images/Cesium_Logo_ETC1S.ktx2 new file mode 100644 index 000000000000..1c44a7e3925b Binary files /dev/null and b/packages/sandcastle/public/images/Cesium_Logo_ETC1S.ktx2 differ diff --git a/packages/sandcastle/public/images/Cesium_Logo_UASTC.ktx2 b/packages/sandcastle/public/images/Cesium_Logo_UASTC.ktx2 new file mode 100644 index 000000000000..53ccb13613e1 Binary files /dev/null and b/packages/sandcastle/public/images/Cesium_Logo_UASTC.ktx2 differ diff --git a/packages/sandcastle/public/images/Cesium_Logo_overlay.png b/packages/sandcastle/public/images/Cesium_Logo_overlay.png new file mode 100644 index 000000000000..42a43316bd27 Binary files /dev/null and b/packages/sandcastle/public/images/Cesium_Logo_overlay.png differ diff --git a/packages/sandcastle/public/images/bumpmap.png b/packages/sandcastle/public/images/bumpmap.png new file mode 100644 index 000000000000..eeaf43a60bdb Binary files /dev/null and b/packages/sandcastle/public/images/bumpmap.png differ diff --git a/packages/sandcastle/public/images/cesium-logomark-192.png b/packages/sandcastle/public/images/cesium-logomark-192.png new file mode 100644 index 000000000000..6b9663a5b732 Binary files /dev/null and b/packages/sandcastle/public/images/cesium-logomark-192.png differ diff --git a/packages/sandcastle/public/images/cesium-logomark.svg b/packages/sandcastle/public/images/cesium-logomark.svg new file mode 100644 index 000000000000..344203be5592 --- /dev/null +++ b/packages/sandcastle/public/images/cesium-logomark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color.jpg b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color.jpg new file mode 100644 index 000000000000..3cb934494e97 Binary files /dev/null and b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color.jpg differ diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color.jpg.aux.xml b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color.jpg.aux.xml new file mode 100644 index 000000000000..fe4d8205d59d --- /dev/null +++ b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color.jpg.aux.xml @@ -0,0 +1,9 @@ + + -1.2000000000000000e+02, 5.0462573591253154e-02, 0.0000000000000000e+00, 4.0000000000000000e+01, 0.0000000000000000e+00, -6.6666666666666666e-02 + + JPEG + PIXEL + YCbCr + + + diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/0/0/0.png b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/0/0/0.png new file mode 100644 index 000000000000..9a8874e19912 Binary files /dev/null and b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/0/0/0.png differ diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/1/0/1.png b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/1/0/1.png new file mode 100644 index 000000000000..8fbc7a62be02 Binary files /dev/null and b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/1/0/1.png differ diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/2/0/2.png b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/2/0/2.png new file mode 100644 index 000000000000..ab9cdec454c9 Binary files /dev/null and b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/2/0/2.png differ diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/2/1/2.png b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/2/1/2.png new file mode 100644 index 000000000000..5d8426918b82 Binary files /dev/null and b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/2/1/2.png differ diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/3/1/4.png b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/3/1/4.png new file mode 100644 index 000000000000..5f793794e35b Binary files /dev/null and b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/3/1/4.png differ diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/3/2/4.png b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/3/2/4.png new file mode 100644 index 000000000000..ce57afc9d9d9 Binary files /dev/null and b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/3/2/4.png differ diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/2/8.png b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/2/8.png new file mode 100644 index 000000000000..cbefe0f1da24 Binary files /dev/null and b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/2/8.png differ diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/2/9.png b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/2/9.png new file mode 100644 index 000000000000..4a2860eb5d61 Binary files /dev/null and b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/2/9.png differ diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/3/8.png b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/3/8.png new file mode 100644 index 000000000000..da9def538d87 Binary files /dev/null and b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/3/8.png differ diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/3/9.png b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/3/9.png new file mode 100644 index 000000000000..e051fd101a72 Binary files /dev/null and b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/3/9.png differ diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/4/8.png b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/4/8.png new file mode 100644 index 000000000000..da9def538d87 Binary files /dev/null and b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/4/8.png differ diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/4/9.png b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/4/9.png new file mode 100644 index 000000000000..b8dbb887ade2 Binary files /dev/null and b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/4/9.png differ diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/5/8.png b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/5/8.png new file mode 100644 index 000000000000..1243871b5e60 Binary files /dev/null and b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/5/8.png differ diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/5/9.png b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/5/9.png new file mode 100644 index 000000000000..25f75f80322b Binary files /dev/null and b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/4/5/9.png differ diff --git a/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/tilemapresource.xml b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/tilemapresource.xml new file mode 100644 index 000000000000..e6ac2fe366db --- /dev/null +++ b/packages/sandcastle/public/images/cesium_maptiler/Cesium_Logo_Color/tilemapresource.xml @@ -0,0 +1,17 @@ + + + Cesium_Logo_Color.jpg + + EPSG:900913 + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/sandcastle/public/images/checkerboard.png b/packages/sandcastle/public/images/checkerboard.png new file mode 100644 index 000000000000..c5e1709225e7 Binary files /dev/null and b/packages/sandcastle/public/images/checkerboard.png differ diff --git a/packages/sandcastle/public/images/earthbump1k.jpg b/packages/sandcastle/public/images/earthbump1k.jpg new file mode 100644 index 000000000000..7b1a98907145 Binary files /dev/null and b/packages/sandcastle/public/images/earthbump1k.jpg differ diff --git a/packages/sandcastle/public/images/earthspec1k.jpg b/packages/sandcastle/public/images/earthspec1k.jpg new file mode 100644 index 000000000000..1d716469017f Binary files /dev/null and b/packages/sandcastle/public/images/earthspec1k.jpg differ diff --git a/packages/sandcastle/public/images/facility.gif b/packages/sandcastle/public/images/facility.gif new file mode 100644 index 000000000000..a4f809310306 Binary files /dev/null and b/packages/sandcastle/public/images/facility.gif differ diff --git a/packages/sandcastle/public/images/gitHub16px.png b/packages/sandcastle/public/images/gitHub16px.png new file mode 100644 index 000000000000..4e39f42103e7 Binary files /dev/null and b/packages/sandcastle/public/images/gitHub16px.png differ diff --git a/packages/sandcastle/public/images/normalmap.png b/packages/sandcastle/public/images/normalmap.png new file mode 100644 index 000000000000..c84ba99235ff Binary files /dev/null and b/packages/sandcastle/public/images/normalmap.png differ diff --git a/packages/sandcastle/public/images/share16px.png b/packages/sandcastle/public/images/share16px.png new file mode 100644 index 000000000000..edd9bef00b9b Binary files /dev/null and b/packages/sandcastle/public/images/share16px.png differ diff --git a/packages/sandcastle/public/images/whiteShapes.png b/packages/sandcastle/public/images/whiteShapes.png new file mode 100644 index 000000000000..0e41785600cd Binary files /dev/null and b/packages/sandcastle/public/images/whiteShapes.png differ diff --git a/packages/sandcastle/public/templates/bucket.css b/packages/sandcastle/public/templates/bucket.css new file mode 100644 index 000000000000..63c27d7f2a01 --- /dev/null +++ b/packages/sandcastle/public/templates/bucket.css @@ -0,0 +1,66 @@ +/* @import url(/Build/CesiumUnminified/Widgets/widgets.css); +@import url(/Build/CesiumUnminified/Widgets/lighter.css); */ + +html { + height: 100%; +} + +body { + background: #000; + color: #eee; + font-family: sans-serif; + font-size: 9pt; + padding: 0; + margin: 0; + width: 100%; + height: 100%; + overflow: hidden; +} + +.fullSize { + display: block; + position: absolute; + top: 0; + left: 0; + border: none; + width: 100%; + height: 100%; +} + +#loadingOverlay { + position: absolute; + top: 0; + left: 0; + opacity: 0.9; + width: 100%; + height: 100%; + display: none; +} + +#loadingOverlay h1 { + text-align: center; + position: relative; + top: 50%; + margin-top: -0.5em; +} + +.sandcastle-loading #loadingOverlay { + display: block; +} + +.sandcastle-loading #toolbar { + display: none; +} + +#toolbar { + margin: 5px; + padding: 2px 5px; + position: absolute; +} + +.infoPanel { + background: rgba(42, 42, 42, 0.8); + padding: 4px; + border: 1px solid #444; + border-radius: 4px; +} diff --git a/packages/sandcastle/public/templates/bucketRaw.css b/packages/sandcastle/public/templates/bucketRaw.css new file mode 100644 index 000000000000..58ee8d49ac3b --- /dev/null +++ b/packages/sandcastle/public/templates/bucketRaw.css @@ -0,0 +1,66 @@ +@import url(../../../Source/Widgets/widgets.css); +@import url(../../../Source/Widgets/lighter.css); + +html { + height: 100%; +} + +body { + background: #000; + color: #eee; + font-family: sans-serif; + font-size: 9pt; + padding: 0; + margin: 0; + width: 100%; + height: 100%; + overflow: hidden; +} + +.fullSize { + display: block; + position: absolute; + top: 0; + left: 0; + border: none; + width: 100%; + height: 100%; +} + +#loadingOverlay { + position: absolute; + top: 0; + left: 0; + opacity: 0.9; + width: 100%; + height: 100%; + display: none; +} + +#loadingOverlay h1 { + text-align: center; + position: relative; + top: 50%; + margin-top: -0.5em; +} + +.sandcastle-loading #loadingOverlay { + display: block; +} + +.sandcastle-loading #toolbar { + display: none; +} + +#toolbar { + margin: 5px; + padding: 2px 5px; + position: absolute; +} + +.infoPanel { + background: rgba(42, 42, 42, 0.8); + padding: 4px; + border: 1px solid #444; + border-radius: 4px; +} diff --git a/packages/sandcastle/src/App.css b/packages/sandcastle/src/App.css new file mode 100644 index 000000000000..e52c6c7aad88 --- /dev/null +++ b/packages/sandcastle/src/App.css @@ -0,0 +1,63 @@ +#root { + width: 100vw; + height: 100vh; + display: grid; + grid-template: + "toolbar toolbar" + "code viewer" + "gallery gallery"; + grid-template-columns: 50% 50%; + grid-template-rows: max-content auto 150px; + /* TODO: this shouldn't be needed but it works for hacky code */ + overflow: hidden; +} + +.toolbar { + grid-area: toolbar; + display: flex; + gap: 0.5rem; + padding: 0.5rem; + background: var(--ids-color-bg-page-base); + + .spacer { + flex-grow: 10; + } +} + +.editor-container { + grid-area: code; + + section { + border-top: 1px solid grey; + } +} + +.viewer-bucket { + grid-area: viewer; + width: 50vw; + background-color: white; + background-image: var(--_rings), var(--_gradient); + --_rings: repeating-radial-gradient( + circle at center, + var(--ids-color-border-neutral-muted) 1px, + var(--ids-color-border-neutral-muted) 3px, + transparent 3px, + transparent 10px + ); + --_gradient: linear-gradient( + 180deg, + var(--ids-color-bg-page-base) 0%, + var(--ids-color-bg-page-depth) 100% + ); + + .fullFrame { + width: 100%; + height: 100%; + } +} + +.gallery { + grid-area: gallery; + border-top: 1px solid grey; + background: var(--ids-color-bg-page-depth); +} diff --git a/packages/sandcastle/src/App.tsx b/packages/sandcastle/src/App.tsx new file mode 100644 index 000000000000..410e25a2dab0 --- /dev/null +++ b/packages/sandcastle/src/App.tsx @@ -0,0 +1,741 @@ +import { useEffect, useRef, useState } from "react"; +import "./App.css"; + +import Editor, { Monaco } from "@monaco-editor/react"; +import { editor, KeyCode } from "monaco-editor"; +import pako from "pako"; +import Gallery, { GalleryDemo } from "./Gallery.js"; +import gallery_demos from "./gallery-index.ts"; +import { Button, Root } from "@itwin/itwinui-react/bricks"; + +const local = { + docTypes: [], + headers: "", + bucketName: "starter bucket", + emptyBucket: "", +}; + +const defaultJsCode = 'const viewer = new Cesium.Viewer("cesiumContainer");\n'; +const defaultHtmlCode = ` +
    +

    Loading...

    +
    +`; + +function embedInSandcastleTemplate(code: string, addExtraLine: boolean) { + console.log("embedSandcastle"); + return ( + `${ + "window.startup = async function (Cesium) {\n" + + " 'use strict';\n" + + "//Sandcastle_Begin\n" + }${addExtraLine ? "\n" : ""}${code}//Sandcastle_End\n` + + ` Sandcastle.finishedLoading();\n` + + `};\n` + + `if (typeof Cesium !== 'undefined') {\n` + + ` window.startupCalled = true;\n` + + ` window.startup(Cesium).catch((error) => {\n` + + ` "use strict";\n` + + ` console.error(error);\n` + + ` });\n` + + `}\n` + ); +} + +function activateBucketScripts( + bucketDoc: Document, + bucketFrame: HTMLIFrameElement, + jsEditor: editor.IStandaloneCodeEditor, + htmlEditor: editor.IStandaloneCodeEditor, +) { + console.log("activateBucketScripts"); + const headNodes = bucketDoc.head.childNodes; + let node; + const nodes: HTMLScriptElement[] = []; + let i, len; + for (i = 0, len = headNodes.length; i < len; ++i) { + node = headNodes[i]; + // header is included in blank frame. + if ( + node instanceof HTMLScriptElement && + node.src.indexOf("Sandcastle-header.js") < 0 && + node.src.indexOf("Cesium.js") < 0 + ) { + nodes.push(node); + } + } + + for (i = 0, len = nodes.length; i < len; ++i) { + bucketDoc.head.removeChild(nodes[i]); + } + + // Apply user HTML to bucket. + const htmlElement = bucketDoc.createElement("div"); + htmlElement.innerHTML = htmlEditor.getValue(); + bucketDoc.body.appendChild(htmlElement); + + const onScriptTagError = function () { + if (bucketFrame.contentDocument === bucketDoc) { + // @ts-expect-error this has type any because it's from anywhere inside the bucket + appendConsole("consoleError", `Error loading ${this.src}`, true); + appendConsole( + "consoleError", + "Make sure Cesium is built, see the Contributor's Guide for details.", + true, + ); + } + }; + + console.log("nodes", nodes); + + // Load each script after the previous one has loaded. + const loadScript = function () { + if (bucketFrame.contentDocument !== bucketDoc) { + // A newer reload has happened, abort this. + return; + } + if (nodes.length > 0) { + while (nodes.length > 0) { + node = nodes.shift(); + if (!node) { + continue; + } + const scriptElement = bucketDoc.createElement("script"); + let hasSrc = false; + for (let j = 0, numAttrs = node.attributes.length; j < numAttrs; ++j) { + const name = node.attributes[j].name; + const val = node.attributes[j].value; + scriptElement.setAttribute(name, val); + if (name === "src" && val) { + hasSrc = true; + } + } + scriptElement.innerHTML = node.innerHTML; + if (hasSrc) { + scriptElement.onload = loadScript; + scriptElement.onerror = onScriptTagError; + bucketDoc.head.appendChild(scriptElement); + } else { + bucketDoc.head.appendChild(scriptElement); + loadScript(); + } + } + } else { + // Apply user JS to bucket + const element = bucketDoc.createElement("script"); + + // Firefox line numbers are zero-based, not one-based. + const isFirefox = navigator.userAgent.indexOf("Firefox/") >= 0; + + element.textContent = embedInSandcastleTemplate( + jsEditor.getValue(), + isFirefox, + ); + bucketDoc.body.appendChild(element); + } + }; + + loadScript(); +} + +function appendConsole( + type: "consoleError" | "", + message: string, + focusPanel: boolean, +) { + // TODO: + if (type === "consoleError") { + console.error(message); + return; + } + console.log(message); + if (focusPanel) { + // TODO: + } +} + +// let bucketWaiting = false; + +function applyBucket( + bucketFrame: HTMLIFrameElement, + jsEditor: editor.IStandaloneCodeEditor, + htmlEditor: editor.IStandaloneCodeEditor, +) { + console.log("applyBucket"); + if ( + // local.emptyBucket && + // local.bucketName && + // typeof bucketTypes[local.bucketName] === "string" + // eslint-disable-next-line no-constant-condition + true + ) { + // bucketWaiting = false; + const bucketDoc = bucketFrame.contentDocument; + if (!bucketDoc) { + console.warn( + "tried to applyBucket before the bucket content document existed", + ); + return; + } + if ( + local.headers.substring(0, local.emptyBucket.length) !== local.emptyBucket + ) { + appendConsole( + "consoleError", + `Error, first part of ${local.bucketName} must match first part of bucket.html exactly.`, + true, + ); + } else { + const bodyAttributes = local.headers.match(/]*?)>/)?.[1] ?? ""; + const attributeRegex = /([-a-z_]+)\s*="([^"]*?)"/gi; + //group 1 attribute name, group 2 attribute value. Assumes double-quoted attributes. + let attributeMatch; + while ((attributeMatch = attributeRegex.exec(bodyAttributes)) !== null) { + const attributeName = attributeMatch[1]; + const attributeValue = attributeMatch[2]; + if (attributeName === "class") { + bucketDoc.body.className = attributeValue; + } else { + bucketDoc.body.setAttribute(attributeName, attributeValue); + } + } + + const pos = local.headers.indexOf(""); + const extraHeaders = local.headers.substring( + local.emptyBucket.length, + pos, + ); + bucketDoc.head.innerHTML += extraHeaders; + activateBucketScripts(bucketDoc, bucketFrame, jsEditor, htmlEditor); + } + } else { + // bucketWaiting = true; + } +} + +// window.addEventListener( +// "message", +// function (e) { +// let line; +// // The iframe (bucket.html) sends this message on load. +// // This triggers the code to be injected into the iframe. +// if (e.data === "reload") { +// console.log("message reload"); +// const bucketDoc = bucketFrame.contentDocument; +// if (!local.bucketName) { +// // Reload fired, bucket not specified yet. +// return; +// } +// if (bucketDoc.body.getAttribute("data-sandcastle-loaded") !== "yes") { +// bucketDoc.body.setAttribute("data-sandcastle-loaded", "yes"); +// logOutput.innerHTML = ""; +// numberOfNewConsoleMessages = 0; +// registry.byId("logContainer").set("title", "Console"); +// // This happens after a Run (F8) reloads bucket.html, to inject the editor code +// // into the iframe, causing the demo to run there. +// applyBucket(); +// // if (docError) { +// // appendConsole( +// // "consoleError", +// // 'Documentation not available. Please run the "build-docs" build script to generate Cesium documentation.', +// // true, +// // ); +// // // showGallery(); +// // } +// // if (galleryError) { +// // appendConsole( +// // "consoleError", +// // "Error loading gallery, please run the build script.", +// // true, +// // ); +// // } +// // if (deferredLoadError) { +// // appendConsole( +// // "consoleLog", +// // `Unable to load demo named ${queryObject.src.replace( +// // ".html", +// // "", +// // )}. Redirecting to HelloWorld.\n`, +// // true, +// // ); +// // } +// } +// // } else if (defined(e.data.log)) { +// // // Console log messages from the iframe display in Sandcastle. +// // appendConsole("consoleLog", e.data.log, false); +// // } else if (defined(e.data.error)) { +// // // Console error messages from the iframe display in Sandcastle +// // let errorMsg = e.data.error; +// // let lineNumber = e.data.lineNumber; +// // if (defined(lineNumber)) { +// // errorMsg += " (on line "; + +// // if (e.data.url) { +// // errorMsg += `${lineNumber} of ${e.data.url})`; +// // } else { +// // lineNumber = scriptLineToEditorLine(lineNumber); +// // errorMsg += `${lineNumber + 1})`; +// // line = jsEditor.setGutterMarker( +// // lineNumber, +// // "errorGutter", +// // makeLineLabel(e.data.error, "errorMarker"), +// // ); +// // jsEditor.addLineClass(line, "text", "errorLine"); +// // errorLines.push(line); +// // scrollToLine(lineNumber); +// // } +// // } +// // appendConsole("consoleError", errorMsg, true); +// // } else if (defined(e.data.warn)) { +// // // Console warning messages from the iframe display in Sandcastle. +// // appendConsole("consoleWarn", e.data.warn, true); +// // } else if (defined(e.data.highlight)) { +// // // Hovering objects in the embedded Cesium window. +// // highlightLine(e.data.highlight); +// } +// }, +// true, +// ); + +const TYPES_URL = `${__PAGE_BASE_URL__}Source/Cesium.d.ts`; + +// function appendCode(code, run = true) { +// const codeMirror = getJsCodeMirror(); +// codeMirror.setValue(`${codeMirror.getValue()}\n${code}`); +// if (run) { +// runCesium(); +// } +// } + +// function appendCodeOnce(code, run = true) { +// const codeMirror = getJsCodeMirror(); +// if (!codeMirror.getValue().includes(code)) { +// appendCode(code, run); +// } +// } + +// function prependCode(code, run = true) { +// const codeMirror = getJsCodeMirror(); +// codeMirror.setValue(`${code}\n${codeMirror.getValue()}`); +// if (run) { +// runCesium(); +// } +// } + +// function prependCodeOnce(code, run = true) { +// const codeMirror = getJsCodeMirror(); +// if (!codeMirror.getValue().includes(code)) { +// prependCode(code, run); +// } +// } + +type SandcastleSaveData = { + code: string; + html: string; + baseHref?: string; +}; + +function makeCompressedBase64String(data: [code: string, html: string]) { + // data stored in the hash as: + // Base64 encoded, raw DEFLATE compressed JSON array where index 0 is code, index 1 is html + let jsonString = JSON.stringify(data); + // we save a few bytes by omitting the leading [" and trailing "] since they are always the same + jsonString = jsonString.slice(2, 2 + jsonString.length - 4); + const pakoString = pako.deflate(jsonString, { + raw: true, + level: 9, + }); + let base64String = btoa( + // TODO: not 100% sure why I have to do this conversion manually anymore but it works + // https://stackoverflow.com/questions/12710001/how-to-convert-uint8-array-to-base64-encoded-string + String.fromCharCode(...new Uint8Array(pakoString)), + ); + base64String = base64String.replace(/=+$/, ""); // remove padding + + return base64String; +} + +function decodeBase64Data(base64String: string): SandcastleSaveData { + // data stored in the hash as: + // Base64 encoded, raw DEFLATE compressed JSON array where index 0 is code, index 1 is html + // restore padding + while (base64String.length % 4 !== 0) { + base64String += "="; + } + // https://stackoverflow.com/questions/12710001/how-to-convert-uint8-array-to-base64-encoded-string + const dataArray = new Uint8Array( + atob(base64String) + .split("") + .map(function (c) { + return c.charCodeAt(0); + }), + ); + let jsonString = pako.inflate(dataArray, { + raw: true, + to: "string", + }); + // we save a few bytes by omitting the leading [" and trailing "] since they are always the same + jsonString = `["${jsonString}"]`; + const json = JSON.parse(jsonString); + // index 0 is code, index 1 is html + const code = json[0]; + const html = json[1]; + const baseHref = json[2]; + return { + code: code, + html: html, + baseHref: baseHref, + }; +} + +function App() { + const jsEditorRef = useRef(null); + const htmlEditorRef = useRef(null); + const bucket = useRef(null); + + function loadFromUrl() { + // TODO: I don't think this is the "correct" way to do on mount/load logic but it's working + if ( + window.location.hash.indexOf("#c=") === 0 && + jsEditorRef.current && + htmlEditorRef.current + ) { + const base64String = window.location.hash.substr(3); + const data = decodeBase64Data(base64String); + + jsEditorRef.current.setValue(data.code); + htmlEditorRef.current.setValue(data.html); + // applyLoadedDemo(code, html); + console.log("loadFromUrl", data); + } + } + + /** + * @param {IStandaloneCodeEditor} editor + */ + function handleEditorDidMount( + editor: editor.IStandaloneCodeEditor, + monaco: Monaco, + ) { + jsEditorRef.current = editor; + + monaco.editor.addCommand({ + id: "run-cesium", + run: () => { + runCode(); + }, + }); + + // Remove some default keybindings that get in the way + // https://github.com/microsoft/monaco-editor/issues/102 + monaco.editor.addKeybindingRules([ + { + // disable show command center + keybinding: KeyCode.F1, + command: null, + }, + { + // disable show error command + keybinding: KeyCode.F8, + command: null, + }, + { + // disable toggle debugger breakpoint + keybinding: KeyCode.F9, + command: null, + }, + { + // disable go to definition to allow opening dev console + keybinding: KeyCode.F12, + command: null, + }, + { + keybinding: KeyCode.F8, + command: "run-cesium", + }, + ]); + + loadFromUrl(); + } + + function handleEditorWillMount(monaco: Monaco) { + // here is the monaco instance + // do something before editor is mounted + + monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ + // TODO: pick what target we want, probably newer than ES2020 but TS was upset with that + target: monaco.languages.typescript.ScriptTarget.ES2020, + allowNonTsExtensions: true, + }); + + setTypes(monaco); + } + + function runCode() { + if (!bucket.current || !bucket.current.contentWindow) { + return; + } + + // Check for a race condition in some browsers where the iframe hasn't loaded yet. + if (bucket.current.contentWindow.location.href.indexOf("bucket.html") > 0) { + bucket.current.contentWindow.location.reload(); + } + // applyBucket(bucket.current, jsEditorRef.current, htmlEditorRef.current); + } + + async function setTypes(monaco: Monaco) { + console.log("setTypes"); + // https://microsoft.github.io/monaco-editor/playground.html?source=v0.52.2#example-extending-language-services-configure-javascript-defaults + const cesiumTypes = await (await fetch(TYPES_URL)).text(); + // TODO: I don't know 100% why this declaration works for global module variable but it "does" + const cesiumTypesSource = `${cesiumTypes}\nvar Cesium: typeof import('cesium');`; + const cesiumTypesUri = "ts:filename/cesium.d.ts"; + + monaco.languages.typescript.javascriptDefaults.addExtraLib( + cesiumTypesSource, + cesiumTypesUri, + ); + + monaco.editor.createModel( + cesiumTypesSource, + "typescript", + monaco.Uri.parse(cesiumTypesUri), + ); + } + + useEffect(() => { + window.addEventListener("message", function (e) { + // The iframe (bucket.html) sends this message on load. + // This triggers the code to be injected into the iframe. + if (e.data === "reload") { + console.log("message reload"); + if (!local.bucketName || !bucket.current) { + // Reload fired, bucket not specified yet. + return; + } + const bucketDoc = bucket.current.contentDocument; + if (!bucketDoc) { + // TODO: this whole handler probably needs to be set up better for things like this + console.warn("bucket not set up yet"); + return; + } + if (!jsEditorRef.current || !htmlEditorRef.current) { + console.warn("editors not set up yet"); + return; + } + if (bucketDoc.body.getAttribute("data-sandcastle-loaded") !== "yes") { + bucketDoc.body.setAttribute("data-sandcastle-loaded", "yes"); + // logOutput.innerHTML = ""; + // numberOfNewConsoleMessages = 0; + // registry.byId("logContainer").set("title", "Console"); + // This happens after a Run (F8) reloads bucket.html, to inject the editor code + // into the iframe, causing the demo to run there. + applyBucket( + bucket.current, + jsEditorRef.current, + htmlEditorRef.current, + ); + // if (docError) { + // appendConsole( + // "consoleError", + // 'Documentation not available. Please run the "build-docs" build script to generate Cesium documentation.', + // true, + // ); + // // showGallery(); + // } + // if (galleryError) { + // appendConsole( + // "consoleError", + // "Error loading gallery, please run the build script.", + // true, + // ); + // } + // if (deferredLoadError) { + // appendConsole( + // "consoleLog", + // `Unable to load demo named ${queryObject.src.replace( + // ".html", + // "", + // )}. Redirecting to HelloWorld.\n`, + // true, + // ); + // } + } + } + }); + }, []); + + function formatJs() { + jsEditorRef.current?.getAction("editor.action.formatDocument")?.run(); + } + + function nextHighestVariableName(name: string) { + if (!jsEditorRef.current) { + // can't find next highest if there's no code yet + return; + } + const codeMirror = jsEditorRef.current; + const code = codeMirror.getValue(); + const otherDeclarations = [ + ...code.matchAll(new RegExp(`(const|let|var)\\s+${name}\\d*\\s=`, "g")), + ].length; + const variableName = `${name}${otherDeclarations + 1}`; + return variableName; + } + + function appendCode(code: string, run = false) { + if (!jsEditorRef.current) { + // can't append if there's no editor + return; + } + jsEditorRef.current.setValue(`${jsEditorRef.current.getValue()}\n${code}`); + if (run) { + runCode(); + } + } + + function addButton() { + appendCode( + ` +Sandcastle.addToolbarButton("New Button", function () { + // your code here +});`, + false, + ); + } + + function addToggle() { + const variableName = nextHighestVariableName("toggleValue"); + + appendCode( + ` +let ${variableName} = true; +Sandcastle.addToggleButton("Toggle", ${variableName}, function (checked) { + ${variableName} = checked; +});`, + false, + ); + } + + function addMenu() { + const variableName = nextHighestVariableName("options"); + + appendCode( + ` +const ${variableName} = [ + { + text: "Option 1", + onselect: function () { + // your code here, the first option is always run at load + }, + }, +]; +Sandcastle.addToolbarMenu(${variableName});`, + false, + ); + } + + function setCode(js: string, html: string) { + if (!jsEditorRef.current || !htmlEditorRef.current) { + // can't find next highest if there's no code yet + return; + } + jsEditorRef.current.setValue(js); + htmlEditorRef.current.setValue(html); + + runCode(); + } + + function resetCode() { + if (!jsEditorRef.current || !htmlEditorRef.current) { + // can't find next highest if there's no code yet + return; + } + jsEditorRef.current.setValue(defaultJsCode); + htmlEditorRef.current.setValue(defaultHtmlCode); + + window.history.replaceState({}, "", "/"); + runCode(); + } + + function share() { + if (!jsEditorRef.current || !htmlEditorRef.current) { + // can't find next highest if there's no code yet + return; + } + const code = jsEditorRef.current.getValue(); + const html = htmlEditorRef.current.getValue(); + console.log([code, html]); + + const base64String = makeCompressedBase64String([code, html]); + + // const shareUrl = `${getBaseUrl()}#c=${base64String}`; + const shareUrl = `#c=${base64String}`; + window.history.replaceState({}, "", shareUrl); + } + + function loadDemo(demo: GalleryDemo) { + // do stuff + setCode(demo.js ?? defaultJsCode, demo.html ?? defaultHtmlCode); + + // format to account for any bad template strings, not ideal but better than not doing it + formatJs(); + + // TODO: this is not the right way to save these, should be able to reference by name but this works for the demo + share(); + } + + const [darkTheme, setDarkTheme] = useState(false); + + return ( + +
    + + + + + + + +
    + +
    +
    + + { + htmlEditorRef.current = editor; + loadFromUrl(); + }} + /> +
    +
    + +
    +
    + loadDemo(demo)} /> +
    +
    + ); +} + +export default App; diff --git a/packages/sandcastle/src/Gallery.css b/packages/sandcastle/src/Gallery.css new file mode 100644 index 000000000000..3cd4104fdb03 --- /dev/null +++ b/packages/sandcastle/src/Gallery.css @@ -0,0 +1,19 @@ +.gallery { + grid-area: gallery; + display: flex; + gap: 1rem; + padding: 0.5rem; + + .card { + display: flex; + flex-direction: column; + background: var(--ids-color-bg-info-muted); + padding: 0.5rem; + border-radius: 5px; + cursor: pointer; + + img { + height: 80%; + } + } +} diff --git a/packages/sandcastle/src/Gallery.tsx b/packages/sandcastle/src/Gallery.tsx new file mode 100644 index 000000000000..16f568f14a6d --- /dev/null +++ b/packages/sandcastle/src/Gallery.tsx @@ -0,0 +1,47 @@ +import { MouseEventHandler } from "react"; +import "./Gallery.css"; + +export type GalleryDemo = { + name: string; + isNew: boolean; + img: string; + js: string; + html?: string; +}; + +type GalleryCardProps = { + demo: GalleryDemo; + cardClickHandler: MouseEventHandler; +}; + +function GalleryCard({ demo, cardClickHandler }: GalleryCardProps) { + return ( +
    +
    {demo.name}
    + +
    + ); +} + +type GalleryProps = { + demos: GalleryDemo[]; + loadDemo: (demo: GalleryDemo) => void; +}; + +function Gallery({ demos, loadDemo }: GalleryProps) { + return ( + <> + {demos.map((demo) => { + return ( + loadDemo(demo)} + > + ); + })} + + ); +} + +export default Gallery; diff --git a/packages/sandcastle/src/gallery-index.ts b/packages/sandcastle/src/gallery-index.ts new file mode 100644 index 000000000000..8c64627d53ef --- /dev/null +++ b/packages/sandcastle/src/gallery-index.ts @@ -0,0 +1,921 @@ +import { GalleryDemo } from "./Gallery"; + +const gallery_demos: GalleryDemo[] = [ + { + name: "3D Models", + isNew: false, + img: "3D Models.jpg", + js: `const viewer = new Cesium.Viewer("cesiumContainer", { + infoBox: false, + selectionIndicator: false, + shadows: true, + shouldAnimate: true, +}); + +function createModel(url, height) { + viewer.entities.removeAll(); + + const position = Cesium.Cartesian3.fromDegrees( + -123.0744619, + 44.0503706, + height, + ); + const heading = Cesium.Math.toRadians(135); + const pitch = 0; + const roll = 0; + const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll); + const orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr); + + const entity = viewer.entities.add({ + name: url, + position: position, + orientation: orientation, + model: { + uri: url, + minimumPixelSize: 128, + maximumScale: 20000, + }, + }); + viewer.trackedEntity = entity; +} + +const options = [ + { + text: "Aircraft", + onselect: function () { + createModel("../../SampleData/models/CesiumAir/Cesium_Air.glb", 5000.0); + }, + }, + { + text: "Drone", + onselect: function () { + createModel("../../SampleData/models/CesiumDrone/CesiumDrone.glb", 150.0); + }, + }, + { + text: "Ground Vehicle", + onselect: function () { + createModel("../../SampleData/models/GroundVehicle/GroundVehicle.glb", 0); + }, + }, + { + text: "Hot Air Balloon", + onselect: function () { + createModel( + "../../SampleData/models/CesiumBalloon/CesiumBalloon.glb", + 1000.0, + ); + }, + }, + { + text: "Milk Truck", + onselect: function () { + createModel( + "../../SampleData/models/CesiumMilkTruck/CesiumMilkTruck.glb", + 0, + ); + }, + }, + { + text: "Skinned Character", + onselect: function () { + createModel("../../SampleData/models/CesiumMan/Cesium_Man.glb", 0); + }, + }, + { + text: "Unlit Box", + onselect: function () { + createModel("../../SampleData/models/BoxUnlit/BoxUnlit.gltf", 10.0); + }, + }, + { + text: "Draco Compressed Model", + onselect: function () { + createModel( + "../../SampleData/models/DracoCompressed/CesiumMilkTruck.gltf", + 0, + ); + }, + }, + { + text: "KTX2 Compressed Balloon", + onselect: function () { + if (!Cesium.FeatureDetection.supportsBasis(viewer.scene)) { + window.alert( + "This browser does not support Basis Universal compressed textures", + ); + } + createModel( + "../../SampleData/models/CesiumBalloonKTX2/CesiumBalloonKTX2.glb", + 1000.0, + ); + }, + }, + { + text: "Instanced Box", + onselect: function () { + createModel("../../SampleData/models/BoxInstanced/BoxInstanced.gltf", 15); + }, + }, +]; + +Sandcastle.addToolbarMenu(options);`, + }, + { + name: "Billboards", + isNew: false, + img: "Billboards.jpg", + js: `const viewer = new Cesium.Viewer("cesiumContainer"); + +function addBillboard() { + Sandcastle.declare(addBillboard); + + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883), + billboard: { + image: "../images/Cesium_Logo_overlay.png", + }, + }); +} + +function setBillboardProperties() { + Sandcastle.declare(setBillboardProperties); + + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883), + billboard: { + image: "../images/Cesium_Logo_overlay.png", // default: undefined + show: true, // default + pixelOffset: new Cesium.Cartesian2(0, -50), // default: (0, 0) + eyeOffset: new Cesium.Cartesian3(0.0, 0.0, 0.0), // default + horizontalOrigin: Cesium.HorizontalOrigin.CENTER, // default + verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // default: CENTER + scale: 2.0, // default: 1.0 + color: Cesium.Color.LIME, // default: WHITE + rotation: Cesium.Math.PI_OVER_FOUR, // default: 0.0 + alignedAxis: Cesium.Cartesian3.ZERO, // default + width: 100, // default: undefined + height: 25, // default: undefined + }, + }); +} + +function changeBillboardProperties() { + Sandcastle.declare(changeBillboardProperties); + + const entity = viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883, 300000.0), + billboard: { + image: "../images/Cesium_Logo_overlay.png", + }, + }); + + const billboard = entity.billboard; + billboard.scale = 3.0; + billboard.color = Cesium.Color.WHITE.withAlpha(0.25); +} + +function sizeBillboardInMeters() { + Sandcastle.declare(sizeBillboardInMeters); + + const entity = viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883), + billboard: { + image: "../images/Cesium_Logo_overlay.png", + sizeInMeters: true, + }, + }); + + viewer.zoomTo(entity); +} + +function addMultipleBillboards() { + Sandcastle.declare(addMultipleBillboards); + + const logoUrl = "../images/Cesium_Logo_overlay.png"; + const facilityUrl = "../images/facility.gif"; + + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883), + billboard: { + image: logoUrl, + }, + }); + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-80.5, 35.14), + billboard: { + image: facilityUrl, + }, + }); + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-80.12, 25.46), + billboard: { + image: facilityUrl, + }, + }); +} + +function scaleByDistance() { + Sandcastle.declare(scaleByDistance); + + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883), + billboard: { + image: "../images/facility.gif", + scaleByDistance: new Cesium.NearFarScalar(1.5e2, 2.0, 1.5e7, 0.5), + }, + }); +} + +function fadeByDistance() { + Sandcastle.declare(fadeByDistance); + + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883), + billboard: { + image: "../images/Cesium_Logo_overlay.png", + translucencyByDistance: new Cesium.NearFarScalar(1.5e2, 2.0, 1.5e7, 0.5), + }, + }); +} + +function offsetByDistance() { + Sandcastle.declare(offsetByDistance); + Promise.all([ + Cesium.Resource.fetchImage("../images/Cesium_Logo_overlay.png"), + Cesium.Resource.fetchImage("../images/facility.gif"), + ]).then(function (images) { + // As viewer zooms closer to facility billboard, + // increase pixelOffset on CesiumLogo billboard to this height + const facilityHeight = images[1].height; + + // colocated billboards, separate as viewer gets closer + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883), + billboard: { + image: images[1], + horizontalOrigin: Cesium.HorizontalOrigin.CENTER, + verticalOrigin: Cesium.VerticalOrigin.BOTTOM, + }, + }); + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883), + billboard: { + image: images[0], + horizontalOrigin: Cesium.HorizontalOrigin.CENTER, + verticalOrigin: Cesium.VerticalOrigin.BOTTOM, + pixelOffset: new Cesium.Cartesian2(0.0, -facilityHeight), + pixelOffsetScaleByDistance: new Cesium.NearFarScalar( + 1.0e3, + 1.0, + 1.5e6, + 0.0, + ), + translucencyByDistance: new Cesium.NearFarScalar(1.0e3, 1.0, 1.5e6, 0.1), + }, + }); + }); +} + +function addMarkerBillboards() { + Sandcastle.declare(addMarkerBillboards); + + // Add several billboards based on the above image in the atlas. + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883), + billboard: { + image: "../images/whiteShapes.png", + imageSubRegion: new Cesium.BoundingRectangle(49, 43, 18, 18), + color: Cesium.Color.LIME, + }, + }); + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-84.0, 39.0), + billboard: { + image: "../images/whiteShapes.png", + imageSubRegion: new Cesium.BoundingRectangle(61, 23, 18, 18), + color: new Cesium.Color(0, 0.5, 1.0, 1.0), + }, + }); + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-70.0, 41.0), + billboard: { + image: "../images/whiteShapes.png", + imageSubRegion: new Cesium.BoundingRectangle(67, 80, 14, 14), + color: new Cesium.Color(0.5, 0.9, 1.0, 1.0), + }, + }); + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-73.0, 37.0), + billboard: { + image: "../images/whiteShapes.png", + imageSubRegion: new Cesium.BoundingRectangle(27, 103, 22, 22), + color: Cesium.Color.RED, + }, + }); + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-79.0, 35.0), + billboard: { + image: "../images/whiteShapes.png", + imageSubRegion: new Cesium.BoundingRectangle(105, 105, 18, 18), + color: Cesium.Color.YELLOW, + }, + }); +} + +async function disableDepthTest() { + Sandcastle.declare(disableDepthTest); + + viewer.scene.globe.depthTestAgainstTerrain = true; + + try { + const worldTerrainProvider = await Cesium.createWorldTerrainAsync(); + + // Return early in case a different option has been selected in the meantime + if (!viewer.scene.globe.depthTestAgainstTerrain) { + return; + } + + viewer.terrainProvider = worldTerrainProvider; + } catch (error) { + window.alert(\`Failed to load terrain. \${error}\`); + } + + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(-122.1958, 46.1915), + billboard: { + image: "../images/facility.gif", + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, + disableDepthTestDistance: Number.POSITIVE_INFINITY, + }, + }); + viewer.scene.camera.setView({ + destination: new Cesium.Cartesian3( + -2357576.243142461, + -3744417.5604860787, + 4581807.855903771, + ), + orientation: new Cesium.HeadingPitchRoll( + 5.9920811504170475, + -0.6032820429886212, + 6.28201303164098, + ), + }); +} + +Sandcastle.addToolbarMenu([ + { + text: "Add billboard", + onselect: function () { + addBillboard(); + Sandcastle.highlight(addBillboard); + }, + }, + { + text: "Set billboard properties at creation", + onselect: function () { + setBillboardProperties(); + Sandcastle.highlight(setBillboardProperties); + }, + }, + { + text: "Change billboard properties", + onselect: function () { + changeBillboardProperties(); + Sandcastle.highlight(changeBillboardProperties); + }, + }, + { + text: "Size billboard in meters", + onselect: function () { + sizeBillboardInMeters(); + Sandcastle.highlight(sizeBillboardInMeters); + }, + }, + { + text: "Add multiple billboards", + onselect: function () { + addMultipleBillboards(); + Sandcastle.highlight(addMultipleBillboards); + }, + }, + { + text: "Scale by viewer distance", + onselect: function () { + scaleByDistance(); + Sandcastle.highlight(scaleByDistance); + }, + }, + { + text: "Fade by viewer distance", + onselect: function () { + fadeByDistance(); + Sandcastle.highlight(fadeByDistance); + }, + }, + { + text: "Offset by viewer distance", + onselect: function () { + offsetByDistance(); + Sandcastle.highlight(offsetByDistance); + }, + }, + { + text: "Add marker billboards", + onselect: function () { + addMarkerBillboards(); + Sandcastle.highlight(addMarkerBillboards); + }, + }, + { + text: "Disable the depth test when clamped to ground", + onselect: function () { + disableDepthTest(); + Sandcastle.highlight(disableDepthTest); + }, + }, +]); + +Sandcastle.reset = async function () { + viewer.camera.flyHome(0); + viewer.entities.removeAll(); + viewer.scene.terrainProvider = new Cesium.EllipsoidTerrainProvider(); + viewer.scene.globe.depthTestAgainstTerrain = false; +}; +`, + }, + { + name: "Moon", + isNew: false, + img: "Moon.jpg", + js: `// Set the ellipsoid to be the moon before creating the viewer +Cesium.Ellipsoid.default = Cesium.Ellipsoid.MOON; + +const viewer = new Cesium.Viewer("cesiumContainer", { + terrainProvider: false, + baseLayer: false, + timeline: false, + animation: false, + baseLayerPicker: false, + geocoder: false, + shadows: true, +}); + +const scene = viewer.scene; + +// Add Moon Terrain 3D Tiles +try { + const tileset1 = await Cesium.Cesium3DTileset.fromIonAssetId(2684829, { + // Allow clamp to 3D Tiles + enableCollision: true, + }); + viewer.scene.primitives.add(tileset1); +} catch (error) { + console.log(\`Error loading tileset: \${error}\`); +} + +// Boundary data from https://wms.lroc.asu.edu/lroc/view_rdr/SHAPEFILE_LROC_GLOBAL_MARE +const boundariesResource = await Cesium.IonResource.fromAssetId(2683530); +const boundarySource = await Cesium.GeoJsonDataSource.load(boundariesResource, { + clampToGround: true, + fill: Cesium.Color.fromBytes(26, 106, 113).withAlpha(0.6), +}); +boundarySource.show = false; +viewer.dataSources.add(boundarySource); + +// Possible Artemis 3 landing locations. data from https://files.actgate.com/lunar/A3_Named_regions.geojson +const artemis3resource = await Cesium.IonResource.fromAssetId(2683531); +const artemis3Source = await Cesium.GeoJsonDataSource.load(artemis3resource, { + clampToGround: true, + fill: Cesium.Color.fromBytes(243, 242, 99).withAlpha(0.6), +}); +artemis3Source.show = false; +viewer.dataSources.add(artemis3Source); + +// Positions courtesy of https://www.sciencedirect.com/science/article/abs/pii/S0019103516301518?via%3Dihub +const pointsOfInterest = [ + { + text: "Apollo 11", + latitude: 0.67416, + longitude: 23.47315, + }, + { + text: "Apollo 14", + latitude: -3.64417, + longitude: 342.52135, + }, + { + text: "Apollo 15", + latitude: 26.13341, + longitude: 3.6285, + }, + { + text: "Lunokhod 1", + latitude: 38.2378, + longitude: -35.0017, + }, + { + text: "Lunokhod 2", + latitude: 25.83232, + longitude: 30.92215, + }, +]; + +for (const poi of pointsOfInterest) { + viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(poi.longitude, poi.latitude), + label: { + text: poi.text, + font: "14pt Verdana", + outlineColor: Cesium.Color.DARKSLATEGREY, + outlineWidth: 2, + style: Cesium.LabelStyle.FILL_AND_OUTLINE, + pixelOffset: new Cesium.Cartesian2(0, -22), + scaleByDistance: new Cesium.NearFarScalar(1.5e2, 1.0, 1.5e7, 0.5), + translucencyByDistance: new Cesium.NearFarScalar(2.5e7, 1.0, 4.0e7, 0.0), + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, + disableDepthTestDistance: new Cesium.CallbackProperty(() => { + return Cesium.Cartesian3.magnitude(scene.camera.positionWC); + }, false), + }, + point: { + pixelSize: 10, + color: Cesium.Color.fromBytes(243, 242, 99), + outlineColor: Cesium.Color.fromBytes(219, 218, 111), + outlineWidth: 2, + scaleByDistance: new Cesium.NearFarScalar(1.5e3, 1.0, 4.0e7, 0.1), + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, + disableDepthTestDistance: new Cesium.CallbackProperty(() => { + return Cesium.Cartesian3.magnitude(scene.camera.positionWC); + }, false), + }, + }); +} + +const seaOfTranquility = { + destination: new Cesium.Cartesian3( + 2134594.9298812235, + 1256488.0678322134, + 379606.9284823841, + ), + orientation: { + direction: new Cesium.Cartesian3( + -0.8518395698371783, + -0.5014189063342804, + -0.1514873843927112, + ), + up: new Cesium.Cartesian3( + -0.13054959630640847, + -0.07684549781463353, + 0.9884591910493093, + ), + }, + easingFunction: Cesium.EasingFunction.LINEAR_NONE, +}; + +const apollo11 = { + destination: new Cesium.Cartesian3( + 1609100.311044896, + 733266.0643925276, + 53608.976740262646, + ), + orientation: { + direction: new Cesium.Cartesian3( + -0.41704286323660256, + -0.7222280712427744, + -0.5517806297183315, + ), + up: new Cesium.Cartesian3( + 0.8621189850799429, + -0.12210806245903304, + -0.49177278965720556, + ), + }, + easingFunction: Cesium.EasingFunction.LINEAR_NONE, +}; + +const copernicus = { + destination: new Cesium.Cartesian3( + 1613572.8201475781, + -677039.3827805589, + 339559.7958496013, + ), + orientation: { + direction: new Cesium.Cartesian3( + -0.10007925201262617, + 0.8771366500325052, + -0.4696971795597116, + ), + up: new Cesium.Cartesian3( + 0.9948921707513932, + 0.08196514973381885, + -0.058917593354560566, + ), + }, + easingFunction: Cesium.EasingFunction.LINEAR_NONE, +}; + +const tycho = { + destination: new Cesium.Cartesian3( + 1368413.3560818078, + -166198.00035620513, + -1203576.7397013502, + ), + orientation: { + direction: new Cesium.Cartesian3( + -0.8601315724135887, + -0.5073902275496569, + 0.05223825345888711, + ), + up: new Cesium.Cartesian3( + 0.2639103814694499, + -0.5303301783281616, + -0.8056681776681204, + ), + }, + easingFunction: Cesium.EasingFunction.LINEAR_NONE, +}; + +const shackleton = { + destination: Cesium.Rectangle.fromBoundingSphere( + new Cesium.BoundingSphere( + new Cesium.Cartesian3( + -17505.087036391753, + 38147.40236305639, + -1769721.5748224584, + ), + 40000.0, + ), + ), + orientation: { + direction: new Cesium.Cartesian3( + 0.2568703591904826, + -0.6405212914728244, + 0.7237058060699372, + ), + up: new Cesium.Cartesian3( + 0.26770932874967773, + -0.6723714327527822, + -0.6901075073627064, + ), + }, + easingFunction: Cesium.EasingFunction.LINEAR_NONE, +}; + +const camera = viewer.scene.camera; +const rotationSpeed = Cesium.Math.toRadians(0.1); +const removeRotation = viewer.scene.postRender.addEventListener( + function (scene, time) { + viewer.scene.camera.rotateRight(rotationSpeed); + }, +); + +const options1 = [ + { + text: "Fly to...", + onselect: () => {}, + }, + { + text: "Sea of Tranquility", + onselect: function () { + removeRotation(); + scene.camera.flyTo(seaOfTranquility); + artemis3Source.show = false; + }, + }, + { + text: "Apollo 11 Landing Site", + onselect: () => { + removeRotation(); + scene.camera.flyTo(apollo11); + artemis3Source.show = false; + }, + }, + { + text: "Copernicus Crater", + onselect: () => { + removeRotation(); + scene.camera.flyTo(copernicus); + artemis3Source.show = false; + }, + }, + { + text: "Tycho Crater", + onselect: () => { + removeRotation(); + scene.camera.flyTo(tycho); + artemis3Source.show = false; + }, + }, + { + text: "Shackleton Crater (South Pole) and Artemis 3 landing options", + onselect: () => { + removeRotation(); + scene.camera.flyTo(shackleton); + artemis3Source.show = true; + }, + }, +]; +Sandcastle.addToolbarMenu(options1); + +Sandcastle.addToggleButton("Show Mare Boundaries", false, function (checked) { + boundarySource.show = checked; +}); + +// Spin the moon on first load but disable the spinning upon any input +const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); +handler.setInputAction( + () => removeRotation(), + Cesium.ScreenSpaceEventType.LEFT_DOWN, +); +handler.setInputAction( + () => removeRotation(), + Cesium.ScreenSpaceEventType.RIGHT_DOWN, +); +handler.setInputAction( + () => removeRotation(), + Cesium.ScreenSpaceEventType.MIDDLE_DOWN, +); +handler.setInputAction(() => removeRotation(), Cesium.ScreenSpaceEventType.WHEEL); + +`, + html: ` +
    +

    Loading...

    +
    +`, + }, + { + name: "Terrain Exaggeration", + isNew: false, + img: "Terrain Exaggeration.jpg", + js: `const viewer = new Cesium.Viewer("cesiumContainer", { + terrain: Cesium.Terrain.fromWorldTerrain(), +}); + +const scene = viewer.scene; +const globe = scene.globe; +scene.verticalExaggeration = 2.0; +scene.verticalExaggerationRelativeHeight = 2400.0; + +scene.camera.setView({ + destination: new Cesium.Cartesian3( + 336567.0354790703, + 5664688.047602498, + 2923204.3566963132, + ), + orientation: new Cesium.HeadingPitchRoll( + 1.2273281382639265, + -0.32239612370237514, + 0.0027207329018610338, + ), +}); + +viewer.entities.add({ + position: new Cesium.Cartesian3( + 314557.3531714575, + 5659723.771882165, + 2923538.5417330978, + ), + ellipsoid: { + radii: new Cesium.Cartesian3(400.0, 400.0, 400.0), + material: Cesium.Color.RED, + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, + }, +}); + +let visualizeRelativeHeight = true; + +function updateMaterial() { + if (visualizeRelativeHeight) { + const height = scene.verticalExaggerationRelativeHeight; + const exaggeration = scene.verticalExaggeration; + const alpha = Math.min(1.0, exaggeration * 0.25); + const layer = { + extendUpwards: true, + extendDownwards: true, + entries: [ + { + height: height + 100.0, + color: new Cesium.Color(0.0, 1.0, 0.0, alpha * 0.25), + }, + { + height: height + 50.0, + color: new Cesium.Color(1.0, 1.0, 1.0, alpha * 0.5), + }, + { + height: height, + color: new Cesium.Color(1.0, 1.0, 1.0, alpha), + }, + { + height: height - 50.0, + color: new Cesium.Color(1.0, 1.0, 1.0, alpha * 0.5), + }, + { + height: height - 100.0, + color: new Cesium.Color(1.0, 0.0, 0.0, alpha * 0.25), + }, + ], + }; + scene.globe.material = Cesium.createElevationBandMaterial({ + scene: scene, + layers: [layer], + }); + } else { + scene.globe.material = undefined; + } +} +updateMaterial(); + +const viewModel = { + exaggeration: scene.verticalExaggeration, + relativeHeight: scene.verticalExaggerationRelativeHeight, +}; + +function updateExaggeration() { + scene.verticalExaggeration = Number(viewModel.exaggeration); + scene.verticalExaggerationRelativeHeight = Number(viewModel.relativeHeight); + updateMaterial(); +} + +Cesium.knockout.track(viewModel); +const toolbar = document.getElementById("toolbar"); +Cesium.knockout.applyBindings(viewModel, toolbar); +for (const name in viewModel) { + if (viewModel.hasOwnProperty(name)) { + Cesium.knockout.getObservable(viewModel, name).subscribe(updateExaggeration); + } +} + +Sandcastle.addToggleButton( + "Visualize Relative Height", + visualizeRelativeHeight, + function (checked) { + visualizeRelativeHeight = checked; + updateMaterial(); + }, +); + +Sandcastle.addToolbarButton("Remove Exaggeration", function () { + viewModel.exaggeration = 1.0; + viewModel.relativeHeight = 0.0; +}); +`, + html: ` +
    +

    Loading...

    +
    + + + + + + + + + + + +
    Exaggeration + + +
    Relative Height + + +
    +
    +`, + }, +]; + +export default gallery_demos; diff --git a/packages/sandcastle/src/main.tsx b/packages/sandcastle/src/main.tsx new file mode 100644 index 000000000000..37a25dd3daff --- /dev/null +++ b/packages/sandcastle/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./reset.css"; // TODO: this may not be needed with itwin-ui +import App from "./App.tsx"; + +createRoot(document.getElementById("app-container")!).render( + + + , +); diff --git a/packages/sandcastle/src/reset.css b/packages/sandcastle/src/reset.css new file mode 100644 index 000000000000..1d4abc0d621c --- /dev/null +++ b/packages/sandcastle/src/reset.css @@ -0,0 +1,75 @@ +/* https://www.joshwcomeau.com/css/custom-css-reset/ */ + +/* 1. Use a more-intuitive box-sizing model */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +/* 2. Remove default margin */ +* { + margin: 0; +} + +/* 3. Enable keyword animations */ +html { + interpolate-size: allow-keywords; +} + +body { + /* 4. Add accessible line-height */ + line-height: 1.5; + /* 5. Improve text rendering */ + -webkit-font-smoothing: antialiased; +} + +/* 6. Improve media defaults */ +img, +picture, +video, +canvas, +svg { + display: block; + max-width: 100%; +} + +/* 7. Inherit fonts for form controls */ +input, +button, +textarea, +select { + font: inherit; +} + +/* 8. Avoid text overflows */ +p, +h1, +h2, +h3, +h4, +h5, +h6 { + overflow-wrap: break-word; +} + +/* 9. Improve line wrapping */ +p { + text-wrap: pretty; +} +h1, +h2, +h3, +h4, +h5, +h6 { + text-wrap: balance; +} + +/* + 10. Create a root stacking context +*/ +#root, +#__next { + isolation: isolate; +} diff --git a/packages/sandcastle/src/vite-env.d.ts b/packages/sandcastle/src/vite-env.d.ts new file mode 100644 index 000000000000..11f02fe2a006 --- /dev/null +++ b/packages/sandcastle/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/sandcastle/templates/bucket.html b/packages/sandcastle/templates/bucket.html new file mode 100644 index 000000000000..cc01a443d6a5 --- /dev/null +++ b/packages/sandcastle/templates/bucket.html @@ -0,0 +1,38 @@ + + + + + + + Cesium Demo + + + + + + + + + + diff --git a/packages/sandcastle/tsconfig.app.json b/packages/sandcastle/tsconfig.app.json new file mode 100644 index 000000000000..af80c2a448d7 --- /dev/null +++ b/packages/sandcastle/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src", "vite-env.d.ts"] +} diff --git a/packages/sandcastle/tsconfig.json b/packages/sandcastle/tsconfig.json new file mode 100644 index 000000000000..1ffef600d959 --- /dev/null +++ b/packages/sandcastle/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/packages/sandcastle/tsconfig.node.json b/packages/sandcastle/tsconfig.node.json new file mode 100644 index 000000000000..db0becc8b033 --- /dev/null +++ b/packages/sandcastle/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/sandcastle/vite-env.d.ts b/packages/sandcastle/vite-env.d.ts new file mode 100644 index 000000000000..0b9672547d5a --- /dev/null +++ b/packages/sandcastle/vite-env.d.ts @@ -0,0 +1,3 @@ +// + +const __PAGE_BASE_URL__: string; diff --git a/packages/sandcastle/vite.config.app.ts b/packages/sandcastle/vite.config.app.ts new file mode 100644 index 000000000000..3fc9c38a1b4a --- /dev/null +++ b/packages/sandcastle/vite.config.app.ts @@ -0,0 +1,27 @@ +import { defineConfig, UserConfig } from "vite"; + +import baseConfig, { cesiumPathReplace } from "./vite.config.ts"; + +export default defineConfig(() => { + const cesiumBaseUrl = "/Build/CesiumUnminified"; + + const config: UserConfig = baseConfig; + // This will make the built files point to routes in the correct nested path + // for the normal local server.js to work correctly + config.base = "/Apps/Sandcastle2"; + + config.build = { + ...config.build, + outDir: "../../Apps/Sandcastle2", + }; + + config.define = { + ...config.define, + __PAGE_BASE_URL__: JSON.stringify("/"), + }; + + const plugins = config.plugins ?? []; + config.plugins = [...plugins, cesiumPathReplace(cesiumBaseUrl)]; + + return config; +}); diff --git a/packages/sandcastle/vite.config.ci.ts b/packages/sandcastle/vite.config.ci.ts new file mode 100644 index 000000000000..491ed2feb606 --- /dev/null +++ b/packages/sandcastle/vite.config.ci.ts @@ -0,0 +1,29 @@ +import { defineConfig, UserConfig } from "vite"; + +import baseConfig, { cesiumPathReplace } from "./vite.config.ts"; + +export default defineConfig(() => { + const cesiumBaseUrl = `${process.env.BASE_URL}Build/CesiumUnminified`; + + console.log("Building Sandcastle with base url:", cesiumBaseUrl); + + const config: UserConfig = baseConfig; + // This will make the built files point to routes in the correct nested path + // based on the ci branch path + config.base = `${process.env.BASE_URL}Apps/Sandcastle2`; + + config.define = { + ...config.define, + __PAGE_BASE_URL__: JSON.stringify(process.env.BASE_URL), + }; + + config.build = { + ...config.build, + outDir: "../../Apps/Sandcastle2", + }; + + const plugins = config.plugins ?? []; + config.plugins = [...plugins, cesiumPathReplace(cesiumBaseUrl)]; + + return config; +}); diff --git a/packages/sandcastle/vite.config.dev.ts b/packages/sandcastle/vite.config.dev.ts new file mode 100644 index 000000000000..6160d4b24246 --- /dev/null +++ b/packages/sandcastle/vite.config.dev.ts @@ -0,0 +1,37 @@ +import { defineConfig, UserConfig } from "vite"; +import { viteStaticCopy } from "vite-plugin-static-copy"; +import baseConfig, { cesiumPathReplace } from "./vite.config.ts"; + +export default defineConfig(() => { + const config: UserConfig = baseConfig; + + const cesiumSource = "../../Build/CesiumUnminified"; + const cesiumBaseUrl = "Build/CesiumUnminified"; + + const copyPlugin = viteStaticCopy({ + targets: [ + { src: `${cesiumSource}/ThirdParty`, dest: cesiumBaseUrl }, + { src: `${cesiumSource}/Workers`, dest: cesiumBaseUrl }, + { src: `${cesiumSource}/Assets`, dest: cesiumBaseUrl }, + { src: `${cesiumSource}/Widgets`, dest: cesiumBaseUrl }, + { src: `${cesiumSource}/Cesium.js`, dest: cesiumBaseUrl }, + { src: `../../Source/Cesium.d.ts`, dest: "Source" }, + { src: "../../Apps/SampleData", dest: "Apps" }, + { src: "../../Apps/SampleData", dest: "" }, + ], + }); + + config.define = { + ...config.define, + __PAGE_BASE_URL__: JSON.stringify("/"), + }; + + const plugins = config.plugins ?? []; + config.plugins = [ + ...plugins, + copyPlugin, + cesiumPathReplace(`/${cesiumBaseUrl}`), + ]; + + return config; +}); diff --git a/packages/sandcastle/vite.config.ts b/packages/sandcastle/vite.config.ts new file mode 100644 index 000000000000..3f9207a59f3b --- /dev/null +++ b/packages/sandcastle/vite.config.ts @@ -0,0 +1,45 @@ +import { fileURLToPath } from "url"; +import react from "@vitejs/plugin-react"; +import { PluginOption, UserConfig } from "vite"; + +// https://vite.dev/config/ +const baseConfig: UserConfig = { + plugins: [react()], + server: { + // Given the nature of loading and constructing a CesiumJS Viewer on startup HMR can get memory intensive + // The state of the editor could also be lost when developing if the page refreshes unexpectedly + hmr: false, + }, + build: { + // "the outDir may not be inside project root and will not be emptied without this setting + emptyOutDir: true, + rollupOptions: { + input: { + index: fileURLToPath(new URL("./index.html", import.meta.url)), + bucket: fileURLToPath( + new URL("./templates/bucket.html", import.meta.url), + ), + }, + }, + }, +}; +export default baseConfig; + +/** + * Replace path values in + * @param cesiumBaseUrl Path to use for replacement + */ +export const cesiumPathReplace = (cesiumBaseUrl: string): PluginOption => { + return { + name: "custom-cesium-path-plugin", + config(config) { + config.define = { + ...config.define, + __CESIUM_BASE_URL__: JSON.stringify(cesiumBaseUrl), + }; + }, + transformIndexHtml(html) { + return html.replaceAll("__CESIUM_BASE_URL__", `${cesiumBaseUrl}`); + }, + }; +};