Rebuilding Sandcastle: Technical Implementation
This issue will track the technical implementation of a new version of Sandcastle for CesiumJS. Design and features will primarily be worked out and discussed elsewhere. If you want to provide feedback on features you'd like to see please add them to our post on the community forum
High level plan
We plan to create a new workspace/package within our mono-repo to keep the new Sandcastle project separated but co-located with CesiumJS.
-
itwin-ui: We expect to use the newest version of iTwin/Bentley UI library for both components and styling as much as possible. We may contribute improvements or components back to it as necessary. - React: This is a given if we want to make use of iTwinUI.
- Monaco: We'll want to balance robust feature set with usability for users who prefer a more minimal code editing experience. This should greatly increase the DX of the editor bringing familiarity for any already used to using VSCode
- TypeScript: Unless there is a reason not to. This will be isolated to it's own package so it's separate from the rest of CesiumJS
- Build tools: We'll likely get started with the Vite plugin for react, and if needed we can use esbuild like the CesiumJS library itself
As we build out the new version we plan to have a beta/dev deployment for people to test against. More info on this as build systems and CI get set up
CC @ggetz
This will be a very welcomed update. Sandcastle has had the same look and feel for many years and could definitely use a refresh.
Some running TODO's which have come up in discussion like the forum and @jjspace's initial PR.
[Moved to the top level post]
Build structure
A big complication with this process is the actual build configuration. This is more complicated because locally we want to point at all the locally built files instead of copying them into the vite build statically. I wanted to share this breakdown I've been referencing a lot during my work on #12574. This is also not helped by the fact that when we deploy to prod we copy everything to the top level domain and basically all routes need to be adjusted somewhat to account for that. This will need to be kept in mind as we discuss how we want to change the import process into Sandcastles.
Local cesium server: npm start
localhost:8080/Apps/Sandcastle/index.html
/standalone.html (CESIUM_BASE_URL = '../../../Build/CesiumUnminified)
/templates/bucket.html (CESIUM_BASE_URL = '../../../Build/CesiumUnminified') - look at load-es6.js
/bucket.css (Accessed by '../templates/bucket.css')
/gallery/Example.html (CESIUM_BASE_URL = '../../../Build/CesiumUnminified')
/gallery/development/Example.html (still run in bucket.html or have custom `<base>` in standalone)
/images/... (Accessed by '../images/...')
/Apps/SampleData/... (Accessed by '../../SampleData/...')
/Build/CesiumUnminified/Cesium.js ('../../Build/CesiumUnminified/index.js' from '../load-es6.js')
/index.js
/Assets/...
/Widgets/...
/Source/Widgets/*.css (Accessed by '../../../Source/Widgets/...')
Production (check gulpfile buildSancastle for when isProduction)
sandcastle.cesium.com/index.html
/standalone.html (CESIUM_BASE_URL = 'CesiumUnminified/')
/templates/bucket.html (CESIUM_BASE_URL = '../CesiumUnminified/')
/bucket.css (Accessed by '../templates/bucket.css')
/gallery/Example.html (CESIUM_BASE_URL = '../CesiumUnminified/')
/images/... (Accessed by '../images/...)
/SampleData/... (Accessed by '../SampleData/...')
/CesiumUnminified/Cesium.js (Accessed by '../CesiumUnminified/...')
/Assets/...
/Widgets/...
CI deployment
ci-build.cesium.com/cesium/\[branch\]/Apps/Sandcastle/index.html
/templates/bucket.html (CESIUM_BASE_URL = '../../../Build/CesiumUnminified/')
/bucket.css
/gallery/Example.html (CESIUM_BASE_URL = '../../../Build/CesiumUnminified/')
/images/... (Accessed by '../images/...')
/Apps/SampleData/... (Accessed by '../../SampleData/...')
/Build/CesiumUnminified/index.js
/Assets/...
/Widgets/...
Local vite dev: npm run dev
localhost:5173/index.html
/templates/bucket.html (CESIUM_BASE_URL = '/Build/CesiumUnminified')
/bucket.css
/images/... (Accessed by '../images/...')
/SampleData/... (Accessed by '../../SampleData/...')
/Build/CesiumUnminified/Cesium.js
/Assets/...
/Widgets/...
Local vite build for server: npm run build-app -> Apps/Sandcastle2 -> npm start
localhost:8080/Apps/Sandcastle2/index.html
/templates/bucket.html (CESIUM_BASE_URL = '/Build/CesiumUnminified')
/bucket.css
/images/... (Accessed by '../images/...')
/Apps/SampleData/... (Accessed by '../../SampleData/...')
/Build/CesiumUnminified/Cesium.js (Accessed by '/Build/CesiumUnminified')
/Assets/...
/Widgets/...
/Source/Cesium.d.ts
There are basically 3 main ways a specific sandcastle can be loaded. They all need to work. More specifically routes to imported JS or CSS and sample data need to work for all of these routes regardless of the environment outlined above.
- Directly by navigating to the gallery file
sandcastle.cesium.com/gallery/3D Models.html -
bucket.htmlwhich is the iframe used in the main sandcastle UIsandcastle.cesium.com/templates/bucket.html-
bucket-requirejs.htmlwhich is the iframe location that usesload-cesium-es6.jsto load the esm module instead of the iife. This is only used locally and gets replaced when deployed - Note that
bucket.htmlis "nested" inside thetemplatesdirectory which lets paths from it mimic those in the gallery directory, this is important for them to both work the same
-
-
standalone.htmlwhich is the full standalone page that is capable of loading a sandcastle from the URL stringhttps://sandcastle.cesium.com/standalone.html#c=bY...
Gallery files
Another big topic we still need to settle on is the structure of an actual, saved, gallery file. Right now they're all html files that work on their own. The code is extracted from them and loaded into the code editors when they're loaded in Sandcastle. Metadata like the title, tags and description is included in the <head> tags.
Open questions:
- Should these remain as singular
htmlfiles? or should they be broken apart into html + js files? - Should every sandcastle example have a
@import(bucket.css)statement or can this be moved to thebucket.htmland gallery files themselves? - Is there a better way we can extract information to avoid loading every single gallery item on every page load?
- How do we manage more metadata?
- There's been discussion of at the very least longer descriptions
- I also think more integrated "explainers" for lines or chunks of code could be nice but I don't know the best way to integrate those
- Is there anything more we should include in the wrapper for sandcastle examples that could help the application as a whole work better?
- For example forcing the iframe window to have a
cesiumViewervariable accessible so the surrounding app can access it
- For example forcing the iframe window to have a
CC @ggetz there's more beyond these to talk about but hopefully this can help kickstart our discussions
@jjspace Re: Gallery files
Before we get into the technical aspects of the gallery file (like the JS/CSS/HTML breakdown, import styles, etc), I think it would be helpful to decouple what we'd like out of a gallery example and how to organize them from implementation itself.
To start what makes up a gallery example? I think defining this will help guide (3) and is directly involved with (4).
- Currently, sandcastle examples consist of the following, correct?
- The code
- A thumbnail
- A title
- A description
- Labels (or tags)
- In addition to tags, of which there can be multiple, would it be helpful to organize the examples into categories (or even subcategories if appropriate)? That could help with organization and user discoverability because each category can be more intentional or curated than the current deluge of examples in each tag.
- I do like the idea of having more digestible "explainer" content to augment a sandcastle. IMO this should be separate from the
descriptionas that should be more concise. The explainer could potentially be a text property. But maybe it could be something with a bit more expressiveness such as markdown. - Are there any other metadata fields which we would like to maintain?
Then going one level deeper, how do we ideally want to store that metadata (4)? I'm putting aside backwards compatibility for a moment to think about how it might make sense if starting from scratch.
- I think it's a bit cumbersome to manage this all in an HTML file directly. Contributors often forget to update descriptions and tags because they're easy to miss.
- Ideally, a more intentional approach to the metadata could help us avoid loading each entire sandcastle example on page load (3).
- Maybe we can use yaml for capturing gallery example metadata. YAML is common in a lot of documentation tools and static site generators, so I don't think it should be a barrier to entry for contributors. For instance:
title: Adjust 3D Tileset Height
thumbnail: "./3d-tiles-adjust-height.jpg"
description: Adjust the relative height of a tileset to better align it with terrain.
labels:
- 3D Tiles
- Tutorial
category: Working with 3D Tiles
context: Each instance of a 3D tileset has a modelMatrix property that, when set, modifies the entire tileset's position, rotation, and scale. This transformations are applied in worldspace before the tileset's root tile transform.
Or, if we want more formatting, that yaml can be ported to a markdown file's front matter:
---
title: Adjust 3D Tileset Height
thumbnail: "./3d-tiles-adjust-height.jpg"
description: Adjust the relative height of a tileset to better align it with terrain.
labels:
- 3D Tiles
- Tutorial
category: Working with 3D Tiles
---
Each instance of a 3D tileset has a `modelMatrix` property that, when set, modifies the entire tileset's position, rotation, and scale. This transformations are applied in worldspace before the tileset's [root tile transform](https://github.com/CesiumGS/3d-tiles/tree/main/specification#transforms).
For (5), I'm having trouble picturing a use case, but it sounds like you have some in mind. Could you share what you were thinking about?
Let me know your thoughts on all that, and then we can talk through the details of how we'd like to store the code and what this may mean for backwards compatability.
@ggetz sorry my response got a little lost but here's my thoughts
To start what makes up a gallery example?
Correct, that is the current list, even if not all of those are displayed promenantly (description).
I think it could be helpful to include a general category but I'm not sure how different it would be than tags. It would definitely help to categorize in whatever UI we pick but I could also see it being an issue that certain sandcastles should belong to multiple categories and then we're right back to tags. Maybe we just pick a set of tags that are "important" that we can use to categorize in the UI but still only store them as tags?
how do we ideally want to store that metadata?
I think it's a bit cumbersome to manage this all in an HTML file directly. Contributors often forget to update descriptions and tags because they're easy to miss.
I agree it may get cumbersome to all be in the same html file. I also think the more we do in there the harder it may make our lives to try and parse/process it when we want to load examples. That said, a separate file may end up being even easier to miss and forget to update but I think that's going to be true for any solution we come up with.
Ideally, a more intentional approach to the metadata could help us avoid loading each entire sandcastle example on page load (3).
I agree, even if we stuck to a single file I would want to pre-process it more so we can load only the metadata on page load instead of the full example.
Maybe we can use yaml for capturing gallery example metadata.
I do really like the idea of using YAML, it's common and easy to use and can be pretty extensible. Definitely easier to write and read than JSON. The main question will be where does it live.
This is all pointing to having one or more "sidecar files" for a given sandcastle.
We have also discussed adding a larger "explainer" type block of text or MD so that needs considered as well. Maybe that could be where we use Frontmatter for the metadata and the rest of the file for any explainer if it exists? If a sandcastle doesn't need that maybe it's just an adjacent [sandcastle name].yaml that has the data?
One big question I think we need to answer is still: Should people be able to load gallery examples directly? If the answer is "yes" then I think we should keep what we can inside the html file itself. If the answer is "no" then we can do more with the "save format". Maybe even separating into dedicated JS and HTML files. or creating subdirectories for each sandcastle and use the file structure itself to encapsulate the metadata sidecar files and the thumbnail and the code etc.
For (5), I'm having trouble picturing a use case, but it sounds like you have some in mind. Could you share what you were thinking about?
The primary usecase I like is just having access to the active Viewer from the console in the outer window context. It's also really useful if we want to add any sort of helper buttons to the Sandcastle UI itself to add code like "Save the current view" which I've found very helpful. Without a consistent way to reach into the iframe or for the iframe to reach out it's impossible to do things like that.
Interestingly I found that Babylon's playground requires that the code creates a createScene function in order to run. Maybe we should do the same with viewer?
Categories
Maybe we just pick a set of tags that are "important" that we can use to categorize in the UI but still only store them as tags?
I agree this is valid from an implementation standpoint. From a usability perspective, just displaying the tags as "categories" is a bit overwhelming due to the number of them, and I think we would like to be more intentional about which ones we present and in which order.
I agree it may get cumbersome to all be in the same html file. I also think the more we do in there the harder it may make our lives to try and parse/process it when we want to load examples. That said, a separate file may end up being even easier to miss and forget to update but I think that's going to be true for any solution we come up with.
One big question I think we need to answer is still: Should people be able to load gallery examples directly?
Good point. Just for clarity here, you're talking about the ability to take the HTML file and load it outside of the context of Sandcastle as more of a standalone example, correct? Or are you looking more to address "saving" a sandcastle to a file and being able to bring it back in?
Overall, I'm personally favoring a gallery example being a directory of files for the flexibility and simplicity from the point of view of the user. Theoretically, this wouldn't prevent the example from working as a standalone example. And we could save as a zip file if the user wants to download, while an example could be brought back in by unzipping in browser (for example).
- [Optional]
README.mdfor the explainer text so we can use markup, link to other resources, etc. This also gives us the benefit of having the explainer info be discoverable and browsable in the GitHub repo as well as Sandcastle. - [Optional] Thumbnail image
- HTML
- JS
- [Optional] Standalone CSS
-
yml(or frontmatter in theREADME.md) specifying all the needed metadata:-
title(string), -
description(string), -
tags(string[]), -
html(string, relative path), -
js(string, relative path), -
css(string, relative path, optional) -
thumbnail(string, relative path, optional),
-
Potentially, we could include things like version info, author, last updated, or search embeddings if they prove useful.
I really am leaning towards the yml data, as it will be easy to generate a gallery list with all the needed info for displaying the example in the Sandcastle UI without having to load to content itself.
Talking offline, with @jjspace– To facilitate different directory structures, we'll consider path aliasing for things like SampleData. Also we should probably move (or duplicate) images and othered referenced data to the SampleData directory.
@ggetz I've updated this main post with a tasklist that I will endeavor to keep updated with everything on my mind, big and small. Some of the bigger topics might get broken out into their own issues for a better place to discuss but I will do that as needed
Awesome, thanks @jjspace! Are these listed in any particular priority order? If not, are you OK with edits to the ordering?
@ggetz I only tried to roughly group them by topic or focus but didn't order by importance or size. Feel free to make edits
@angrycat9000 Had a great suggestion about potentially localizing Sandcastle. And @Mizuki0522 has some resources we could potentially use for localizing materials for Japanese.
I think that there is actually not toooo much in the Sandcastle itself that would require I18N. But certainly in the area of documentation and examples.
I've previously been advocating for more fine-grained Sandcastles to build "Learning paths". For example, the current 'Materials' Sandcastle stands at a whopping 560 lines of code, with zero comments. I think that it could make sense to break this down into 10 "Learning"-Sandcastles, each with elaborate documentation, e.g.
Title: How to apply a texture to a rectangle
const viewer = new Cesium.Viewer("cesiumContainer");
// Create a rectangle [bla, bla]
const rectangle = new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
// The Geometry is [bla, bla]
geometry: new Cesium.RectangleGeometry({
rectangle: Cesium.Rectangle.fromDegrees(-120.0, 20.0, -60.0, 40.0),
// This must be used because [bla, bla] (see below)
vertexFormat: Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT,
}),
}),
appearance: new Cesium.EllipsoidSurfaceAppearance({
aboveGround: false,
}),
});
// Now define the material...
const material = new Cesium.Material({
fabric: {
type: "DiffuseMap",
// This is [bla, bla]...
uniforms: {
image: "../images/Cesium_Logo_Color.jpg",
},
},
});
// Assign the material ...
rectangle.appearance.material = material;
// Add the rectangle...
viewer.scene.primitives.add(rectangle);
(And this could be beneficial regardless of the I18N...)
But... when there should be localization, then it raises the question of where that translation from
// Create a rectangle [bla, bla]
to
// 四角形を作成する [bla, bla]
is going to happen.
@ggetz Just a bump on reviewing PRs 🙏 Threw together a small git diagram to show the branches to help dictate merge order (since even I was starting to lose track 😆 )
---
config:
gitGraph:
mainBranchName: sandcastle-v2
showCommitLabel: false
---
gitGraph
commit
commit
branch nested-gallery
commit
branch sandcastle-react-structure
commit
commit
branch monaco-prettier
commit
branch sandcastle-resize
commit
checkout sandcastle-react-structure
commit
branch sandcastle-snippets
commit
checkout sandcastle-react-structure
commit
branch sandcastle-dev-deployment
commit
checkout nested-gallery
commit
checkout sandcastle-v2
commit
-
nested-gallery: https://github.com/CesiumGS/cesium/pull/12631 -
sandcastle-react-structure: https://github.com/CesiumGS/cesium/pull/12639 -
monaco-prettier: https://github.com/CesiumGS/cesium/pull/12640 -
sandcastle-resize: https://github.com/CesiumGS/cesium/pull/12672 -
sandcastle-snippets: https://github.com/CesiumGS/cesium/pull/12679 -
sandcastle-dev-deployment: https://github.com/CesiumGS/cesium/pull/12680
Only the nested gallery and react PRs are sizable, the others are small, I just separated them to try and help focus reviews.
Small progress update. I've been working on implementing the new UI structure/design that we have mostly settled on. Still fairly rough visually but the panels and structures are all pretty much there now. Work is in the stratakit-ui branch
https://github.com/user-attachments/assets/0e3a4fb9-b6c4-405c-8dee-600da25c9176
CC @ggetz
Closing in favor of a new task list to keep things a bit cleaner https://github.com/CesiumGS/cesium/issues/12894 All remaining tasks from this issue that we still want to address have been moved there