Export
Register the export lifecycle hook, and the platform will invoke your generator to retrieve the resulting file whenever the user clicks "Export" or "Open in Studio." The return value is always { filename, blob }, and any file format is supported (images, SVG, 3D models, PDF, etc.).
Placing the Export Button
You place the export button yourself, inside your app—add an element with the data-atomm-export-button attribute anywhere on the page, and the SDK will render it in place as a platform-controlled Export button (Download / Open in Studio + credit indicator). Clicks are always routed through the platform:
<div data-atomm-export-button></div>
Clicking it opens a dropdown with two items that behave differently. Download hands the files straight to the user with no dialog at all. Open in Studio opens an export settings dialog first: the user picks Download or Open in Studio, chooses machine / processing mode / material, and assigns a processing type to each SVG colour group (see SVG Export Color Spec). Your export hook still receives intent, fired once per tab, so returning different artefacts per action keeps working.
- Position and outer layout are entirely up to you (e.g.,
position: fixedto pin it to a corner); you can place multiple instances on one page, each hydrating independently. - The button renders inside a Shadow DOM for style isolation; theming is done exclusively through the documented
--atomm-export-*CSS variables (set them on the element itself or any ancestor—they're automatically inherited by the button):
[data-atomm-export-button] {
--atomm-export-bg: #d32f2f; /* Button background color */
--atomm-export-radius: 0; /* Border radius */
--atomm-export-width: 200px; /* Width: px, or 100% to fill the container */
--atomm-export-height: 40px; /* Height */
}
| Variable | Default | Controls |
|---|---|---|
--atomm-export-bg / -bg-hover / -bg-active | #070b10 / #252c36 / #1c2129 | Button background for the default / hover / active states |
--atomm-export-color | #fff | Button text color |
--atomm-export-width | auto (fits content) | Button width (px, or 100% to fill the container; min 24px) |
--atomm-export-height | 32px | Button height (px or 100%; min 24px) |
--atomm-export-radius | 8px | Border radius |
--atomm-export-font / -font-size | Inter, system-ui, sans-serif / 14px | Font family / font size |
--atomm-export-menu-bg / -menu-color / -menu-hover-bg | #fff / #111 / #f4f4f5 | Dropdown menu colors |
--atomm-export-menu-radius / -menu-shadow | 8px / 0 4px 16px rgba(16,24,40,.12) | Menu border radius / shadow |
--atomm-export-z | 1000 | Dropdown overlay z-index |
Only the
--atomm-export-*variables listed above are supported customization points; internal button class names may change at any time, so don't rely on them.
The export button—and its Download / Open in Studio / credit indicator—is driven by the atomm platform, and only works when your generator runs inside the atomm environment: the live platform, or the dev tool's local preview (?local=).
If you open your generator standalone, outside atomm (e.g. opening the HTML directly), the button may still render, but clicking it won't produce a file and no billing status will show—this is by design, not a bug. Always verify export via local preview or the live platform.
Free-Use Count / Credit Indicator on the Button
The button automatically displays the current billing status (pushed by the platform—no action needed on your part):
| State | Display | Meaning |
|---|---|---|
| Free window | 30s countdown | A short grace period granted after a successful export; exports within this window are free |
| Free count | Free 3/3 | Remaining / total free exports |
| Credit cost | Credit icon + number | Credits consumed per export once free uses are exhausted |
In local debugging (
?local=), clicking export never actually deducts credits; refreshing the page resets it.
Registering the export Hook
atomm.lifecycle.on('export', async ({ intent }) => {
// intent tells you whether the user clicked Download or Open in Studio; the handler reads the current generation result itself and produces a Blob
const blob = await exportCurrentResultAsBlob()
if (!blob) {
throw new Error('请先生成作品后再下载')
}
return {
filename: 'my-generator.glb', // Must include an extension; this determines the downloaded filename and the asset type used by Open in Studio
blob, // A Blob of any format; MIME type comes from blob.type
}
})
The intent Argument: Telling Download from Open in Studio
Download and Open in Studio share this one hook. intent tells you which dropdown item the user picked, so you can return different output per action.
Each item triggers your hook once; return the same output for both if the distinction does not apply.
| intent | When it fires | What you typically return |
|---|---|---|
'download' | The Download tab | Every file the user should get on disk (the platform zips multiple files) |
'openInStudio' | The Open in Studio tab | Only what Studio should open, typically a single editable vector file |
A processing type can only be written onto an SVG element as an attribute, and a standalone bitmap file has nowhere to carry one. So when the user picks Open in Studio, the platform wraps any standalone bitmap in your output (PNG / JPEG / WebP / GIF / BMP) in an SVG <image> — that is what lets it show up under the "Bitmap" group in the dialog, receive a processing type, and reach Studio with it.
Physical size is decided like this: if the image carries a resolution (PNG pHYs, JPEG EXIF / JFIF, BMP pixels-per-metre) it is converted to millimetres from that; otherwise the SVG/CSS 96 PPI default is used. If the size matters, write resolution metadata into the image you export.
Download always delivers your bytes untouched — no wrapping, no attributes. If you told the user you export a PNG, a PNG is what they get.
atomm.lifecycle.on('export', async ({ intent }) => {
// Open in Studio: the file will be edited further in Studio, so send one vector file
if (intent === 'openInStudio') {
return { filename: 'design.svg', blob: await buildSvg() }
}
// Download: send every file; the platform bundles them into a single zip
return [
{ filename: 'cut.svg', blob: cutBlob },
{ filename: 'engrave.svg', blob: engraveBlob },
{ filename: 'score.svg', blob: scoreBlob },
{ filename: 'readme.txt', blob: readmeBlob },
]
})
If the distinction doesn't apply to you, return the same thing for both; ignoring
intententirely (async () => { ... }) keeps working.
Return Value Fields
| Field | Type | Description |
|---|---|---|
| filename | string | The downloaded filename; must include an extension (e.g., design.glb) and must not contain path separators / \ |
| blob | Blob | File content of any format; each file must be ≤ 100MB; MIME type comes from blob.type, falling back to the filename extension when empty |
Multi-File Export (Optional)
The handler can also return an array of files, and the platform will bundle them into a single zip download:
atomm.lifecycle.on('export', async () => [
{ filename: 'model.glb', blob: glbBlob },
{ filename: 'preview.png', blob: pngBlob },
])
| Rule | Description |
|---|---|
| zip filename | Derived from the base name of the first file in the array (model.glb → model.zip) |
| Duplicate names | Automatically deduplicated to name (1).ext |
| Size limit | The combined size of all files must be ≤ 100MB; exceeding this fails the entire export |
| Array of length 1 | Downloads the file directly, without wrapping it in a zip |
| Open in Studio | Supports multiple files—all returned files are handed to Studio to open, with no format restriction; formats Studio doesn't support are flagged by Studio itself. Multi-file import requires xTool Studio 1.8+; single-file export remains compatible with older versions |
Single files still use
{ filename, blob }; use an array for multiple files. If any item in the returned array is invalid (not a Blob, filename missing an extension, etc.), the entire result is considered invalid.
Getting a Blob
Almost any export source can be converted to a Blob in a single line:
| What you have | Convert to Blob |
|---|---|
| Canvas (2D drawing) | canvas.toBlob(cb, 'image/png') |
| SVG string / text / JSON | new Blob([str], { type: 'image/svg+xml' }) |
| ArrayBuffer / 3D mesh (glb/stl, etc.) | new Blob([buffer], { type: 'model/gltf-binary' }) |
| Existing data URL / remote URL | await (await fetch(x)).blob() |
File from <input type=file> | Use it directly (a File is already a Blob) |
Export data is always carried as a Blob, so any format is supported — a Blob preserves the binary content of any file type.
filename must include an extension and must not contain path separators; if a file exceeds 100MB or the return value isn't a valid { filename, blob }, the platform treats it as invalid, the export fails, and the user is notified.
Downloads support saving any format to disk. "Open in Studio" hands the file off to xTool Studio to open—the platform makes no assumptions about format, and Studio itself will flag any format it doesn't support.
The export button is available to every generator (it appears as soon as you add data-atomm-export-button to your app). If a user clicks export and you haven't registered the export hook, the platform receives "the app provides no download method"—and your submission will be rejected in review as a result. Any generator that supports export must register this hook; you can test it locally by clicking "Export" in the preview to confirm a file is produced correctly.