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...
+
+
+
+
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...
+
+`,
+ },
+];
+
+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}`);
+ },
+ };
+};