[{"data":1,"prerenderedAt":886},["ShallowReactive",2],{"docs-nav-en":3,"docs-search-sections-en":118,"doc-\u002Fen\u002Fdocs\u002F3d-preview\u002Frecipes":795,"doc-surround-\u002Fen\u002Fdocs\u002F3d-preview\u002Frecipes":881},[4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,74,78,82,86,90,94,97,100,103,107,111,115],{"path":5,"title":6,"icon":7},"\u002Fen\u002Fdocs\u002Fquickstart","Quickstart","ri-rocket-line",{"path":9,"title":10,"icon":11},"\u002Fen\u002Fdocs\u002Fdevtool","Local Debugging","ri-terminal-box-line",{"path":13,"title":14,"icon":15},"\u002Fen\u002Fdocs\u002Fatomm","atomm Object","ri-plug-line",{"path":17,"title":18,"icon":19},"\u002Fen\u002Fdocs\u002F3d-preview\u002Foverview","Overview","ri-book-open-line",{"path":21,"title":22,"icon":23},"\u002Fen\u002Fdocs\u002F3d-preview\u002Farchitecture","Architecture","ri-braces-line",{"path":25,"title":26,"icon":27},"\u002Fen\u002Fdocs\u002F3d-preview\u002Finteraction","Interaction","ri-flashlight-line",{"path":29,"title":30,"icon":31},"\u002Fen\u002Fdocs\u002F3d-preview\u002Frebuild","Rebuild on change","ri-refresh-line",{"path":33,"title":34,"icon":35},"\u002Fen\u002Fdocs\u002F3d-preview\u002Frendering","Render quality","ri-sun-line",{"path":37,"title":38,"icon":39},"\u002Fen\u002Fdocs\u002F3d-preview\u002Fdependencies","Dependencies","ri-stack-line",{"path":41,"title":42,"icon":43},"\u002Fen\u002Fdocs\u002F3d-preview\u002Frecipes","Common parts","ri-shape-line",{"path":45,"title":46,"icon":47},"\u002Fen\u002Fdocs\u002F3d-preview\u002Fchecklist","Acceptance checklist","ri-checkbox-circle-line",{"path":49,"title":50,"icon":51},"\u002Fen\u002Fdocs\u002Fexport","Export","ri-download-2-line",{"path":53,"title":54,"icon":55},"\u002Fen\u002Fdocs\u002Fexport\u002Fsvg-color-spec","SVG Export Color Spec","ri-drop-line",{"path":57,"title":58,"icon":59},"\u002Fen\u002Fdocs\u002Fpublish","Submit for Review & Publish","ri-send-plane-line",{"path":61,"title":62,"icon":63},"\u002Fen\u002Fdocs\u002Ffaq","FAQ","ri-questionnaire-line",{"path":65,"title":66,"icon":67},"\u002Fen\u002Fdocs\u002Fai\u002Fllms-txt","LLMs.txt","ri-file-text-line",{"path":69,"title":70,"icon":71},"\u002Fen\u002Fdocs\u002Fai\u002Fskills","Skills","ri-file-code-line",{"path":73,"title":18,"icon":19},"\u002Fen\u002Fdocs\u002Fdesign\u002Foverview",{"path":75,"title":76,"icon":77},"\u002Fen\u002Fdocs\u002Fdesign\u002Fplatform","Platform constraints","ri-shield-check-line",{"path":79,"title":80,"icon":81},"\u002Fen\u002Fdocs\u002Fdesign\u002Flayout","Layout","ri-layout-line",{"path":83,"title":84,"icon":85},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents","Components","ri-checkbox-multiple-line",{"path":87,"title":88,"icon":89},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor","Colour","ri-palette-line",{"path":91,"title":92,"icon":93},"\u002Fen\u002Fdocs\u002Fdesign\u002Ftypography","Typography","ri-text",{"path":95,"title":96,"icon":43},"\u002Fen\u002Fdocs\u002Fdesign\u002Fshapes","Shapes",{"path":98,"title":99,"icon":39},"\u002Fen\u002Fdocs\u002Fdesign\u002Felevation","Elevation & Depth",{"path":101,"title":102,"icon":27},"\u002Fen\u002Fdocs\u002Fdesign\u002Fmotion","Motion",{"path":104,"title":105,"icon":106},"\u002Fen\u002Fdocs\u002Fdesign\u002Ficon","Iconography","ri-apps-2-line",{"path":108,"title":109,"icon":110},"\u002Fen\u002Fdocs\u002Fdesign\u002Fdirection","Direction (RTL)","ri-global-line",{"path":112,"title":113,"icon":114},"\u002Fen\u002Fdocs\u002Fdesign\u002Faccessibility","Accessibility","ri-eye-line",{"path":116,"title":117,"icon":47},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules","Rules",[119,123,127,133,136,139,144,149,152,155,160,165,170,175,180,183,187,192,197,200,203,208,213,218,220,223,228,233,238,243,248,250,253,258,263,268,270,273,278,283,288,293,295,299,302,306,309,312,316,321,326,330,333,336,341,347,352,357,362,367,372,375,379,384,389,394,399,404,407,410,415,420,425,430,435,437,440,445,450,455,460,465,470,473,477,482,487,490,494,499,504,509,512,516,521,526,531,534,538,541,544,549,554,559,564,569,574,577,581,586,591,596,601,606,611,616,621,626,631,636,639,643,648,653,658,663,668,673,678,681,685,690,695,698,702,705,709,714,716,720,723,727,732,737,740,744,749,752,756,759,762,767,772,776,780,785,790],{"id":5,"title":6,"titles":120,"content":121,"level":122},[],"Integrate a generator built with any tech stack into Atomm: create a generator → add one line of SDK and integrate capabilities → preview and debug locally in real time → submit for review and publish. No command-line tools required.",1,{"id":124,"title":6,"titles":125,"content":126,"level":122},"\u002Fen\u002Fdocs\u002Fquickstart#quickstart",[],"Integrate a generator built with any tech stack into Atomm: create a generator → add one line of SDK and integrate capabilities → preview and debug locally in real time → submit for review and publish. No command-line tools required. Plain HTML, Vite, Vue, React — any tech stack works. Atomm communicates with your app through a browser-side SDK and doesn't constrain how you build it.",{"id":128,"title":129,"titles":130,"content":131,"level":132},"\u002Fen\u002Fdocs\u002Fquickstart#end-to-end-flow","End-to-end flow",[6],"1. Create a generator in the Developer Console Start by picking a name to serve as its public identifier (must start with a lowercase letter and contain only lowercase letters, digits, and hyphens -; it cannot be changed after creation, as it becomes part of the runtime subdomain and access path). After creation you'll land on the app detail page to continue setup. This name is used again in local preview and final publishing. 2. Add the SDK to your app and integrate capabilities Add one script tag to your page to get the global atomm object: \u003Cscript src=\"https:\u002F\u002Fstatic-res.makextool.com\u002Fscripts\u002Fjs\u002Fgenerator-sdk\u002Fplatform-sdk.js\">\u003C\u002Fscript> Then integrate platform capabilities as needed. Taking export as an example, this takes two steps — place an export button placeholder element (the SDK renders it in place as an Export button), and register an export hook to supply the resulting file: \u003C!-- Place anywhere on the page; the SDK renders it in place as an Export dropdown -->\n\u003Cdiv data-atomm-export-button>\u003C\u002Fdiv> \u002F\u002F The platform calls back into your app for the result file when the user clicks export\natomm.lifecycle.on('export', async () => ({ filename: 'design.glb', blob })) See the atomm object and export capability for details. 3. Start your local server, then preview and verify in real time in the browser Start your local server (e.g. http:\u002F\u002Flocalhost:5173), build the preview URL using the generator name from step 1, and put your local address into ?local= (only loopback addresses such as localhost \u002F 127.0.0.1 are supported): https:\u002F\u002Fwww.atomm.com\u002Fcreativetools\u002Fcommunity\u002Fgenerator\u002F\u003Cgenerator-name>?local=http:\u002F\u002Flocalhost:5173\u002F DevTool runs your local app inside the same environment it will run in live, and previews it there. The integration assistant panel on the right shows whether the SDK is loaded and lists the platform capabilities available to integrate, so you can verify in the preview that export and other capabilities are being invoked correctly. You can also open this directly from the \"Local debugging\" card on the app detail page by entering your local address. See Local debugging for details. 4. Upload the artifact, submit for review, and publish On the app detail page's configuration card, add your listing information, upload the packaged artifact, and submit for review. Once approved, it's automatically published to , and users can open and use it directly from the generator gallery — no installation required. See Submitting for review and publishing for details. html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sPWt5, html code.shiki .sPWt5{--shiki-default:#7EE787}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}",2,{"id":9,"title":10,"titles":134,"content":135,"level":122},[],"No command-line tools need to be installed. Local debugging takes only two steps: integrate the SDK, then open the online DevTool in your browser to load your local service.",{"id":137,"title":10,"titles":138,"content":135,"level":122},"\u002Fen\u002Fdocs\u002Fdevtool#local-debugging",[],{"id":140,"title":141,"titles":142,"content":143,"level":132},"\u002Fen\u002Fdocs\u002Fdevtool#_1-integrate-the-platform-sdk","1. Integrate the Platform SDK",[10],"Include this in your app page: \u003Cscript src=\"https:\u002F\u002Fstatic-res.makextool.com\u002Fscripts\u002Fjs\u002Fgenerator-sdk\u002Fplatform-sdk.js\">\u003C\u002Fscript> Once loaded, the SDK handshakes with the platform and injects a global atomm object (see atomm object).",{"id":145,"title":146,"titles":147,"content":148,"level":132},"\u002Fen\u002Fdocs\u002Fdevtool#_2-open-the-online-devtool-preview","2. Open the Online DevTool Preview",[10],"After starting your service locally (any port), open the URL below in your browser and fill your local address into ?local= (only loopback addresses such as localhost \u002F 127.0.0.1 are supported): https:\u002F\u002Fwww.atomm.com\u002Fcreativetools\u002Fcommunity\u002Fgenerator\u002F\u003Cgenerator-name>?local=http:\u002F\u002Flocalhost:5173\u002F Left: a live preview of your generator (your local service runs in the same environment it will run in live, with the same capability limits)Right: two tabs\nSimulator: currently offers \"Preview language\" — switching it changes what atomm.app.getLocale() returns and reloads your generator, so you can verify your localization (see The atomm object · Language).Integration assistant:\nSDK integration hint: shows an \"integration incomplete\" notice until platform-sdk.js is loaded, along with copyable integration code and a documentation link; it disappears automatically once the SDK handshake succeeds.Platform capability list: lists the capabilities the platform provides (export \u002F download, the atomm object, etc.); each item lets you \"view docs\" or \"copy the integration prompt\" to hand off to an AI for integration. Dev preview mode is fixed to the dev environment, incurs no charges, has all platform capabilities enabled by default, and does not depend on any backend data. On the app detail page in the Developer Console, fill in your local address in the \"Local Debugging\" card to open the preview above directly. www.atomm.com runs over HTTPS. Browsers grant a mixed-content exemption for http addresses on localhost, so a local http service loads normally; non-localhost http addresses will be blocked by the browser — use localhost or enable HTTPS for your local service. html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sPWt5, html code.shiki .sPWt5{--shiki-default:#7EE787}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":13,"title":14,"titles":150,"content":151,"level":122},[],"atomm is a global object injected into the page once the platform SDK (platform-sdk.js) loads. It exposes platform capabilities and UI utilities, and can be used directly in your generator code.",{"id":153,"title":14,"titles":154,"content":151,"level":122},"\u002Fen\u002Fdocs\u002Fatomm#atomm-object",[],{"id":156,"title":157,"titles":158,"content":159,"level":132},"\u002Fen\u002Fdocs\u002Fatomm#lifecycle-atommlifecycle","Lifecycle — atomm.lifecycle",[14],"Register platform lifecycle hooks via lifecycle.on: atomm.lifecycle.on('export', async () => { ... })",{"id":161,"title":162,"titles":163,"content":164,"level":132},"\u002Fen\u002Fdocs\u002Fatomm#toasts-atommui","Toasts — atomm.ui",[14],"Shows a platform-wide toast notification. Takes an object as its argument: type: optional, one of success \u002F warning \u002F error \u002F info, defaults to successmessage: the notification textduration: optional, how long the toast stays visible, in seconds; 0 means it won't auto-close; defaults to 3 seconds if omitted toast() returns the id of that toast; call closeToast(id) to close it manually. A persistent toast with duration: 0 must be closed this way, or it will stay on screen indefinitely. atomm.ui.toast({ type: 'success', message: '已完成' })\n\n\u002F\u002F Show for 5 seconds\natomm.ui.toast({ type: 'info', message: '正在处理…', duration: 5 })\n\n\u002F\u002F duration: 0 keeps it on screen; close it with closeToast once the task finishes\nconst id = await atomm.ui.toast({ type: 'info', message: '导出中…', duration: 0 })\n\u002F\u002F …task finishes…\nawait atomm.ui.closeToast(id) Passing a string (e.g. toast('已完成')) won't work — the platform only reads message and type off an object, so a string is treated as an empty message. The ui namespace currently provides toast (which returns the toast's id) and closeToast.",{"id":166,"title":167,"titles":168,"content":169,"level":132},"\u002Fen\u002Fdocs\u002Fatomm#language-atommapp","Language — atomm.app",[14],"The platform's current UI language, so your generator can localize along with it. \u002F\u002F Current locale code — always one of the 17 supported languages\nconst locale = await atomm.app.getLocale()\n\n\u002F\u002F All supported locales (code + display name), for building a language selector consistent with the platform\nconst locales = await atomm.app.getSupportedLocales()\n\u002F\u002F → [{ code: 'zh', name: '简体中文' }, { code: 'en', name: 'English' }, …] Calling getLocale() once at startup to pick the initial language is enough. It is a one-time read, not a subscription — changing the platform language reloads the whole page, so your app restarts and reads the new value on its own. You never need to listen for changes. getLocale() always returns one of the 17 codes below, but your app may not have translated all of them. When you get a language you have no copy for, fall back to English or your own default. CodeDisplay nameLanguagezh简体中文Simplified ChineseenEnglishEnglishzh-hant繁體中文Traditional ChinesedeDeutschGermanesEspañolSpanishfrFrançaisFrenchitItalianoItalianja日本語Japaneseko한국어KoreanruРусскийRussianukУкраїнськаUkrainianslSlovenščinaSlovenianthไทยThaiplPolskiPolishcsČeštinaCzechidBahasa IndonesiaIndonesianviTiếng ViệtVietnamese The table is in the order getSupportedLocales() returns, and \"Display name\" is exactly the name it gives you — use it directly in a language picker. The developer toolkit's Simulator has a \"Preview language\" dropdown. Switching it changes what getLocale() returns and reloads your generator, so you don't have to change the language on the real platform. See Developer toolkit.",{"id":171,"title":172,"titles":173,"content":174,"level":132},"\u002Fen\u002Fdocs\u002Fatomm#user-atommuser","User — atomm.user",[14],"\u002F\u002F Whether the user is currently logged in (returns only a boolean, no token or profile data)\nconst loggedIn = await atomm.user.isLoggedIn()\n\n\u002F\u002F If not logged in, opens the login dialog and waits for the user to finish; returns whether they ended up logged in\nif (!loggedIn) {\n  const ok = await atomm.user.login()\n  if (!ok) return \u002F\u002F User canceled or login failed\n}\n\u002F\u002F Reaching here means the user is logged in — continue with your logic login() opens a login window, so call it from within a user gesture handler (e.g. a click callback) to avoid having it blocked by the browser's popup blocker. Logging in is a long-running interaction, so the platform already extends its timeout for it (about 5 minutes) — you don't need to add your own.",{"id":176,"title":177,"titles":178,"content":179,"level":132},"\u002Fen\u002Fdocs\u002Fatomm#safe-invocation","Safe invocation",[14],"If you're not sure whether your code is running inside the platform, use optional chaining to avoid errors: window.atomm?.ui?.toast?.({ type: 'info', message: 'hi' }) html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}",{"id":17,"title":18,"titles":181,"content":182,"level":122},[],"This section covers the technical requirements for building a three.js 3D preview into a creative tool \u002F generator on the Atomm platform — architecture, interaction, render quality, and performance and stability — and closes with a pre-submission acceptance checklist.",{"id":184,"title":18,"titles":185,"content":186,"level":122},"\u002Fen\u002Fdocs\u002F3d-preview\u002Foverview#overview",[],"This section covers the technical requirements for building a three.js 3D preview into a creative tool \u002F generator on the Atomm platform — architecture, interaction, render quality, and performance and stability — and closes with a pre-submission acceptance checklist. Applies to: every generator app that ships a 3D preview view. The same requirements are also written as a single-file skill for coding agents (Download 3D Preview Skill, top right). Hand it straight to yours. The demo below is what an agent built after reading that skill: drag to orbit, scroll to zoom, and every parameter on the right feeds one geometry IR that the 2D preview, the 3D view and the exported SVG all read from.",{"id":188,"title":189,"titles":190,"content":191,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Foverview#how-to-read-the-requirement-levels","How to read the requirement levels",[18],"TermMeaningMustMandatory. Checked item by item at review.ShouldThe default. Deviate only with a good reason.OptionalImplement it if your product needs it.",{"id":193,"title":194,"titles":195,"content":196,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Foverview#do-you-need-a-3d-preview-at-all","Do you need a 3D preview at all?",[18],"A 3D preview is optional. Build one only when thickness, stacked depth or how parts fit together is core to what your tool has to show. If the output is essentially flat, use a 2D SVG with a procedural material texture instead (a feTurbulence wood-grain filter, for example) — you get the material read at a fraction of the cost. Once you decide to build one, it has to meet every acceptance criterion below. A 3D preview that falls short of the interaction and rendering baseline makes the product feel worse than no 3D at all, and will not pass review.",{"id":21,"title":22,"titles":198,"content":199,"level":122},[],"",{"id":201,"title":22,"titles":202,"content":199,"level":122},"\u002Fen\u002Fdocs\u002F3d-preview\u002Farchitecture#architecture",[],{"id":204,"title":205,"titles":206,"content":207,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Farchitecture#one-geometry-source","One geometry source",[22],"You must follow a single data flow: Config (parameter object) → pure render → geometry IR → 2D canvas \u002F 3D view \u002F exported file The 3D view must consume the same geometry IR as the 2D canvas and the exported file. Do not write a second geometry pipeline on the 3D side — the preview and the export will drift apart.",{"id":209,"title":210,"titles":211,"content":212,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Farchitecture#coordinate-systems","Coordinate systems",[22],"Each rendering space uses the conventions below, and they must be documented in one place rather than rediscovered per file: SpaceUnit \u002F origin \u002F axesIR (canonical space)mm, centred at (0,0), Y down (2D — no Z)3D (three.js)mm, Y up, Z is thickness Outlines are built in the XY plane and extruded along +Z, so the part faces the camera. Flip IR to Y-up by mirroring, not rotating — set group.scale.y = -1 once, on the content group; rotating it flat moves thickness onto another axis. Two ways the flip goes wrong: One part is upside-down and mirrored while everything around it is correct — that mesh set scale.y itself. It already inherits the group's flip, so the second one cancels it.Every face renders inside-out — the flip was applied to the geometry (geometry.scale(1, -1, 1)). three.js compensates the winding order from matrixWorld at the object level only; there is no compensation at the geometry level.",{"id":214,"title":215,"titles":216,"content":217,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Farchitecture#path-conversion","Path conversion",[22],"SVG paths for text and complex shapes must go through new SVGLoader().parse() and SVGLoader.createShapes() to become a THREE.Shape — that path applies the fill rule correctly, so holes and letter counters come out right — and then through ExtrudeGeometry. Do not write your own SVG path parser.",{"id":25,"title":26,"titles":219,"content":199,"level":122},[],{"id":221,"title":26,"titles":222,"content":199,"level":122},"\u002Fen\u002Fdocs\u002F3d-preview\u002Finteraction#interaction",[],{"id":224,"title":225,"titles":226,"content":227,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Finteraction#camera-controls","Camera controls",[26],"When you use OrbitControls, you must configure all of the following: Damping on: controls.enableDamping = true, with controls.update() called every frame.Clamped zoom: set minDistance and maxDistance. 1.2×R to 8×R (R being the model's bounding radius) is the recommended range — it keeps the camera from entering the model or drifting out to nothing.Clamped pitch: set maxPolarAngle slightly under π (0.95π works), so the camera never tips under the model.Opening shot: about 3.2×R out, tilted slightly down — the whole model visible at the default angle, and reading as a solid.",{"id":229,"title":230,"titles":231,"content":232,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Finteraction#wheel-events","Wheel events",[26],"If you take over zoom yourself, you must attach a native non-passive listener: element.addEventListener('wheel', handler, { passive: false }) and call preventDefault() in the handler. Framework-level wheel bindings are usually passive and cannot stop the page's default scroll (React's onWheel, Vue's @wheel without a passive opt-out), so zooming inside the 3D area scrolls the whole page with it. The wheel handling built into OrbitControls already satisfies this.",{"id":234,"title":235,"titles":236,"content":237,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Finteraction#ambient-motion","Ambient motion",[26],"You should add an idle float: a Lissajous sway on frequencies that are not integer multiples of each other, so there is no perceptible loop point. Keep the amplitude slight.You should drive drag through an under-damped spring rather than mapping pointer travel 1:1 onto rotation. Aim for the behaviour, not a number: releasing produces one visible overshoot and settles in roughly 0.3s. (Stiffness 70 \u002F damping 5.5 lands there in one particular per-frame integrator — the values only mean something alongside the formula you use, so tune to the behaviour.)Ambient-motion transforms must live on a persistent parent node (the rig) that geometry rebuilds never touch (see Rebuild on change), so motion stays continuous while parameters change.Every animation must honour prefers-reduced-motion and switch off entirely when it is set.",{"id":239,"title":240,"titles":241,"content":242,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Finteraction#the-frame-loop","The frame loop",[26],"Per-frame updates (motion, springs, controls.update()) must run in a rAF loop that mutates Object3D transforms directly. The UI framework re-renders only when Config changes; do not trigger framework state updates from the frame loop (React setState, Vue reactive assignment, Svelte store writes — all of them re-render 60 times a second and drop frames).Note that rAF pauses while the tab is hidden (document.visibilityState === 'hidden'), so every animated value freezes at its last frame. Anything that reads those values — an automated check, a debugging session — has to bring the tab to the foreground first, or it reads stale numbers and concludes the motion is broken.",{"id":244,"title":245,"titles":246,"content":247,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Finteraction#text-selection","Text selection",[26],"The 3D stage container must set user-select: none, so drag-to-rotate does not select page text. html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":29,"title":30,"titles":249,"content":199,"level":122},[],{"id":251,"title":30,"titles":252,"content":199,"level":122},"\u002Fen\u002Fdocs\u002F3d-preview\u002Frebuild#rebuild-on-change",[],{"id":254,"title":255,"titles":256,"content":257,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Frebuild#rebuild-strategy","Rebuild strategy",[30],"Geometry rebuilds triggered by a parameter change must be debounced (100–200ms), so dragging a slider does not rebuild per frame and drop the frame rate.A rebuild must be scoped to the content group's subtree. Camera position, OrbitControls state and the ambient-motion rig must not reset because a parameter changed.",{"id":259,"title":260,"titles":261,"content":262,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Frebuild#disposing-resources","Disposing resources",[30],"Two different lifetimes — do not collapse them into one dispose function. On every rebuild, release what this build created: every geometry.dispose()every material.dispose(), plus any texture created for this build Skip this and GPU memory climbs the whole time someone is adjusting parameters, until the page locks up. On unmount only, release what outlives a rebuild: the PMREM render target and the environment maptextures shared across rebuilds — the procedural noise texture, a CanvasTexture shared with the 2D canvas (see Render quality)controls.dispose() and renderer.dispose() Neither list tolerates items from the other. Regenerating the environment map every rebuild costs a multi-pass PMREM render per debounce tick — more expensive than the leak you were avoiding. Disposing a shared texture every rebuild is worse: the next frame renders against a dead texture and the material goes blank. Note that walking the content subtree and disposing every texture you find there hits both traps at once — a material references shared textures without owning them. Dispose what you created, not what you referenced.",{"id":264,"title":265,"titles":266,"content":267,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Frebuild#sizing-dpr-and-zero-size-mount","Sizing, DPR and zero-size mount",[30],"renderer.setPixelRatio() must be clamped — Math.min(window.devicePixelRatio, 2). Rendering at DPR 3 on a phone or a 5K display costs 2.25× the fragments of DPR 2 for no visible gain, and is the most common reason a preview that runs fine on the developer's machine drops frames on the user's.Reacting to a container resize means all three of camera.aspect, camera.updateProjectionMatrix() and renderer.setSize(). Miss updateProjectionMatrix() and the image stretches; miss setSize() and it renders at the old resolution.Your component can mount at 0×0 — inside a hidden tab or a collapsed panel. When ResizeObserver reports 0×0 you must ignore it and keep the current view state, then initialise or refit once a non-zero size arrives. That is what keeps the view intact across a tab switch.",{"id":33,"title":34,"titles":269,"content":199,"level":122},[],{"id":271,"title":34,"titles":272,"content":199,"level":122},"\u002Fen\u002Fdocs\u002F3d-preview\u002Frendering#render-quality",[],{"id":274,"title":275,"titles":276,"content":277,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Frendering#lighting","Lighting",[34],"Image-based lighting must be configured; AmbientLight alone is not acceptable. The recommended setup is the built-in procedural interior — no asset files, no network request, nothing for CSP to block:const pmrem = new THREE.PMREMGenerator(renderer)\nconst envRT = pmrem.fromScene(new RoomEnvironment()) \u002F\u002F keep envRT — it is disposed on unmount, see Rebuild on change\nscene.environment = envRT.texture\nKeep the environment dim (scene.environmentIntensity ≈ 0.4 as a reference; a lower envMapIntensity on dark parts) to hold contrast, and add one soft directional light so the lighting has a direction.",{"id":279,"title":280,"titles":281,"content":282,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Frendering#tone-mapping","Tone mapping",[34],"ACES tone mapping must be enabled: renderer.toneMapping = THREE.ACESFilmicToneMapping It makes a visible difference to how MeshStandardMaterial \u002F MeshPhysicalMaterial read.",{"id":284,"title":285,"titles":286,"content":287,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Frendering#materials","Materials",[34],"You should generate a procedural noise texture on a canvas and use it as both map and bumpMap — a cheap way to get a physical material read. Build it once and reuse it across rebuilds; Rebuild on change covers when to dispose it.ExtrudeGeometry UVs are world coordinates in mm, so texture.repeat must be derived from physical size: pick how many mm one tile of the texture should span, and set repeat = 1 \u002F tile_mm (a 45mm wood grain gives 1\u002F45). Hardcode a repeat count instead and the grain changes size whenever the part does.The procedural material SVG used by the 2D canvas can be rendered to a canvas and reused in 3D as a CanvasTexture, keeping 2D and 3D materials consistent. For the top face — assuming the model is centred on the origin, so world coordinates run from −R to +R — repeat = 1\u002F(2R) and offset = 0.5 map that span onto 0–1.You should split materials per face group, [faceMat, sideMat] (group 0 top and bottom, group 1 sides), with the sides a shade darker to read as solid.",{"id":289,"title":290,"titles":291,"content":292,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Frendering#hdri-environment-optional","HDRI environment (optional)",[34],"For a more convincing environment, ship a small CC0-licensed 1k .hdr (around 1.5MB): run it through PMREM for scene.environment, use the raw equirect texture as scene.background with a moderate backgroundBlurriness, and fall back to RoomEnvironment as an instant placeholder while it loads. Two things to watch for: A downward-tilted camera looks at the panorama's zenith or ground — the flattest part of it — and the background collapses into one blurred colour. Rotate the horizon behind the subject with backgroundRotation and environmentRotation, and start the camera at a shallower angle.Keep the background clearly darker than the subject, so the model's silhouette and its holes stay readable. html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":37,"title":38,"titles":294,"content":199,"level":122},[],{"id":296,"title":38,"titles":297,"content":298,"level":122},"\u002Fen\u002Fdocs\u002F3d-preview\u002Fdependencies#dependencies",[],"Pin your three.js version. Its API deprecates things continuously — known examples:\nthe RoomEnvironment constructor dropped its renderer argument around r150;r180 deprecated RGBELoader in favour of HDRLoader (the old class still loads, but warns);r185 deprecated PCFSoftShadowMap, which now silently falls back to PCFShadowMap;colour-space and tone-mapping defaults have changed more than once.After a three.js upgrade you must check the browser console. No deprecation warnings in the console is an acceptance criterion.",{"id":41,"title":42,"titles":300,"content":301,"level":122},[],"Recipes to reach for when the shape calls for them:",{"id":303,"title":42,"titles":304,"content":305,"level":122},"\u002Fen\u002Fdocs\u002F3d-preview\u002Frecipes#common-parts",[],"Recipes to reach for when the shape calls for them: PartHow to build itRaised marks \u002F letteringReuse the cut part's outline path → SVGLoader → ExtrudeGeometry, and place it at z = panel thickness. Add it to the content group like everything else — it inherits the group's Y flip, so do not flip it againThin rings \u002F outlined partsGenerate inner and outer outlines at radius ± stroke\u002F2 and extrude the thin ring; sample circles smoothly, flatten polygonsFold-up flapsTreat every open cut arc as one flap, with the hinge axis running between the arc's two endpoints (its chord). Extrude the flap and rotate it about that axis, in whichever direction lifts the flap's centroid toward +z. The base panel gets the matching hole punched out, while the uncut core stays solid. Cycle the fold angle through a small set of values so the result looks natural",{"id":45,"title":46,"titles":307,"content":308,"level":122},[],"Work through this before you submit. Everything has to pass.",{"id":310,"title":46,"titles":311,"content":308,"level":122},"\u002Fen\u002Fdocs\u002F3d-preview\u002Fchecklist#acceptance-checklist",[],{"id":313,"title":26,"titles":314,"content":315,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Fchecklist#interaction",[46],"OrbitControls damping is on; zoom distance and pitch are both clamped Opening shot is around 3.2×R, tilted slightly down, whole model on one screen The wheel inside the 3D area never scrolls or zooms the page Frame rate holds while a slider is dragged continuously (geometry rebuilds are debounced) Changing any parameter leaves the camera where it was and does not interrupt motion Drag springs back on release; idle has a slight float; prefers-reduced-motion disables all of it The 3D stage sets user-select: none — dragging never selects page text",{"id":317,"title":318,"titles":319,"content":320,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Fchecklist#correctness","Correctness",[46],"3D geometry, the 2D canvas and the exported file all come from one IR (change any parameter and all three follow) The Y-down → Y-up flip happens once on the content group — not per mesh, not on the geometry. No mirrored text, no holes on the wrong side, no inside-out faces Every panel parameter has a visible effect in the 3D view, or is hidden for that mode with an explanation — a parameter that is visible but does nothing is not acceptable",{"id":322,"title":323,"titles":324,"content":325,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Fchecklist#performance-and-stability","Performance and stability",[46],"setPixelRatio is clamped (DPR ≤ 2) Five minutes of continuous parameter changes with flat GPU and JS memory (everything is disposed) Hide the tab and come back: the view is intact, not blank, nothing lost Mounting at 0×0 still initialises correctly once a real size arrives A container resize updates camera.aspect, updateProjectionMatrix() and setSize() together — the view refits without stretching",{"id":327,"title":34,"titles":328,"content":329,"level":132},"\u002Fen\u002Fdocs\u002F3d-preview\u002Fchecklist#render-quality",[46],"Environment lighting is configured (RoomEnvironment or HDRI) with a directional light on top — not bare AmbientLight ACES tone mapping is on The environment map and any shared texture are built once and disposed on unmount only — not regenerated or disposed per rebuild Top and side materials are separate groups, sides a shade darker No three.js deprecation warnings in the browser console",{"id":49,"title":50,"titles":331,"content":332,"level":122},[],"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.).",{"id":334,"title":50,"titles":335,"content":332,"level":122},"\u002Fen\u002Fdocs\u002Fexport#export",[],{"id":337,"title":338,"titles":339,"content":340,"level":132},"\u002Fen\u002Fdocs\u002Fexport#placing-the-export-button","Placing the Export Button",[50],"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 \u002F Open in Studio + credit indicator). Clicks are always routed through the platform: \u003Cdiv data-atomm-export-button>\u003C\u002Fdiv> 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 \u002F processing mode \u002F 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: fixed to 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] {\n  --atomm-export-bg: #d32f2f; \u002F* Button background color *\u002F\n  --atomm-export-radius: 0; \u002F* Border radius *\u002F\n  --atomm-export-width: 200px; \u002F* Width: px, or 100% to fill the container *\u002F\n  --atomm-export-height: 40px; \u002F* Height *\u002F\n} VariableDefaultControls--atomm-export-bg \u002F -bg-hover \u002F -bg-active#070b10 \u002F #252c36 \u002F #1c2129Button background for the default \u002F hover \u002F active states--atomm-export-color#fffButton text color--atomm-export-widthauto (fits content)Button width (px, or 100% to fill the container; min 24px)--atomm-export-height32pxButton height (px or 100%; min 24px)--atomm-export-radius8pxBorder radius--atomm-export-font \u002F -font-sizeInter, system-ui, sans-serif \u002F 14pxFont family \u002F font size--atomm-export-menu-bg \u002F -menu-color \u002F -menu-hover-bg#fff \u002F #111 \u002F #f4f4f5Dropdown menu colors--atomm-export-menu-radius \u002F -menu-shadow8px \u002F 0 4px 16px rgba(16,24,40,.12)Menu border radius \u002F shadow--atomm-export-z1000Dropdown 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 \u002F Open in Studio \u002F 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.",{"id":342,"title":343,"titles":344,"content":345,"level":346},"\u002Fen\u002Fdocs\u002Fexport#free-use-count-credit-indicator-on-the-button","Free-Use Count \u002F Credit Indicator on the Button",[50,338],"The button automatically displays the current billing status (pushed by the platform—no action needed on your part): StateDisplayMeaningFree window30s countdownA short grace period granted after a successful export; exports within this window are freeFree countFree 3\u002F3Remaining \u002F total free exportsCredit costCredit icon + numberCredits consumed per export once free uses are exhausted In local debugging (?local=), clicking export never actually deducts credits; refreshing the page resets it.",3,{"id":348,"title":349,"titles":350,"content":351,"level":132},"\u002Fen\u002Fdocs\u002Fexport#registering-the-export-hook","Registering the export Hook",[50],"atomm.lifecycle.on('export', async ({ intent }) => {\n  \u002F\u002F intent tells you whether the user clicked Download or Open in Studio; the handler reads the current generation result itself and produces a Blob\n  const blob = await exportCurrentResultAsBlob()\n  if (!blob) {\n    throw new Error('请先生成作品后再下载')\n  }\n\n  return {\n    filename: 'my-generator.glb', \u002F\u002F Must include an extension; this determines the downloaded filename and the asset type used by Open in Studio\n    blob, \u002F\u002F A Blob of any format; MIME type comes from blob.type\n  }\n})",{"id":353,"title":354,"titles":355,"content":356,"level":346},"\u002Fen\u002Fdocs\u002Fexport#the-intent-argument-telling-download-from-open-in-studio","The intent Argument: Telling Download from Open in Studio",[50,349],"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.\nEach item triggers your hook once; return the same output for both if the distinction does not apply. intentWhen it firesWhat you typically return'download'The Download tabEvery file the user should get on disk (the platform zips multiple files)'openInStudio'The Open in Studio tabOnly 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 \u002F JPEG \u002F WebP \u002F GIF \u002F BMP) in an SVG \u003Cimage> — 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 \u002F JFIF, BMP pixels-per-metre) it is converted to millimetres from that; otherwise the SVG\u002FCSS 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 }) => {\n  \u002F\u002F Open in Studio: the file will be edited further in Studio, so send one vector file\n  if (intent === 'openInStudio') {\n    return { filename: 'design.svg', blob: await buildSvg() }\n  }\n\n  \u002F\u002F Download: send every file; the platform bundles them into a single zip\n  return [\n    { filename: 'cut.svg', blob: cutBlob },\n    { filename: 'engrave.svg', blob: engraveBlob },\n    { filename: 'score.svg', blob: scoreBlob },\n    { filename: 'readme.txt', blob: readmeBlob },\n  ]\n}) If the distinction doesn't apply to you, return the same thing for both; ignoring intent entirely (async () => { ... }) keeps working.",{"id":358,"title":359,"titles":360,"content":361,"level":132},"\u002Fen\u002Fdocs\u002Fexport#return-value-fields","Return Value Fields",[50],"FieldTypeDescriptionfilenamestringThe downloaded filename; must include an extension (e.g., design.glb) and must not contain path separators \u002F \\blobBlobFile content of any format; each file must be ≤ 100MB; MIME type comes from blob.type, falling back to the filename extension when empty",{"id":363,"title":364,"titles":365,"content":366,"level":132},"\u002Fen\u002Fdocs\u002Fexport#multi-file-export-optional","Multi-File Export (Optional)",[50],"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 () => [\n  { filename: 'model.glb', blob: glbBlob },\n  { filename: 'preview.png', blob: pngBlob },\n]) RuleDescriptionzip filenameDerived from the base name of the first file in the array (model.glb → model.zip)Duplicate namesAutomatically deduplicated to name (1).extSize limitThe combined size of all files must be ≤ 100MB; exceeding this fails the entire exportArray of length 1Downloads the file directly, without wrapping it in a zipOpen in StudioSupports 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.",{"id":368,"title":369,"titles":370,"content":371,"level":132},"\u002Fen\u002Fdocs\u002Fexport#getting-a-blob","Getting a Blob",[50],"Almost any export source can be converted to a Blob in a single line: What you haveConvert to BlobCanvas (2D drawing)canvas.toBlob(cb, 'image\u002Fpng')SVG string \u002F text \u002F JSONnew Blob([str], { type: 'image\u002Fsvg+xml' })ArrayBuffer \u002F 3D mesh (glb\u002Fstl, etc.)new Blob([buffer], { type: 'model\u002Fgltf-binary' })Existing data URL \u002F remote URLawait (await fetch(x)).blob()File from \u003Cinput 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. html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sPWt5, html code.shiki .sPWt5{--shiki-default:#7EE787}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}",{"id":53,"title":54,"titles":373,"content":374,"level":122},[],"The platform reads processing intent from colour: stroke cuts in #FE0002, stroke engravings in #2366FF, fill engravings in #2366FF. Every other colour still exports — the user just assigns its processing type by hand.",{"id":376,"title":54,"titles":377,"content":378,"level":122},"\u002Fen\u002Fdocs\u002Fexport\u002Fsvg-color-spec#svg-export-color-spec",[],"The platform reads processing intent from colour: stroke cuts in #FE0002, stroke engravings in #2366FF, fill engravings in #2366FF. Every other colour still exports — the user just assigns its processing type by hand. Your colours are read, never rewritten: colour is only used to identify, and the only thing written into the file is the processing type, data-processing-type.",{"id":380,"title":381,"titles":382,"content":383,"level":132},"\u002Fen\u002Fdocs\u002Fexport\u002Fsvg-color-spec#two-colours-five-groups","Two colours, five groups",[54],"GroupHow to draw itRed linestroke= Blue linestroke= Fill vectorfill= Bitmap\u003Cimage> elements, colour-independentOther vectorevery other visible vector, split into one row per colour However many elements a group holds, it takes one row and the user configures it once. This is the dialog they get after picking Open in Studio from the dropdown (Download saves straight to disk with no dialog). Machine, processing mode and material sit at the top; together they decide which processing types this machine supports. The dropdown on the right of each row is that group's processing type.",{"id":385,"title":386,"titles":387,"content":388,"level":132},"\u002Fen\u002Fdocs\u002Fexport\u002Fsvg-color-spec#a-complete-example","A complete example",[54],"Your export hook returns an SVG like this: \u003Csvg xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\" width=\"200\" height=\"100\">\n  \u003Crect x=\"4\" y=\"4\" width=\"192\" height=\"92\" fill=\"none\" stroke=\"#FE0002\"\u002F>\n  \u003Cpath d=\"M20 30h160\" fill=\"none\" stroke=\"#2366FF\"\u002F>\n  \u003Ccircle cx=\"100\" cy=\"65\" r=\"20\" fill=\"#2366FF\"\u002F>\n\u003C\u002Fsvg> It only uses the first three groups, so the dialog shows three rows — Red line, Blue line and Fill vector, one element each. The user picks a processing type for each, and on confirm your file becomes: \u003Crect   … stroke=\"#FE0002\" data-processing-type=\"VECTOR_CUTTING\"\u002F>\n\u003Cpath   … stroke=\"#2366FF\" data-processing-type=\"VECTOR_ENGRAVING\"\u002F>\n\u003Ccircle … fill=\"#2366FF\"   data-processing-type=\"FILL_VECTOR_ENGRAVING\"\u002F> The attribute goes on each element that is actually processed, never on a \u003Cg> to be inherited. The file carries the processing type and nothing else — no power, speed or similar parameters. Studio applies the recommended parameters for whichever material the user picked in the dialog. That is everything you need to make export work. The three sections below are detail — reach for them when an export does not come out the way you expected.",{"id":390,"title":391,"titles":392,"content":393,"level":132},"\u002Fen\u002Fdocs\u002Fexport\u002Fsvg-color-spec#the-colour-was-not-recognised","The colour was not recognised",[54],"First check the notation is one the platform reads. Each element resolves in the order inline style → \u003Cstyle> rule → element attribute, and only inherits from its parent when all three are absent. That order follows SVG 2: a presentation attribute has specificity 0, so any CSS rule beats it. Every one of these is recognised as a cut line: \u003Cpath stroke=\"#FE0002\" fill=\"none\"\u002F>\n\u003Cpath stroke=\"#fe0002\" fill=\"none\"\u002F>\n\u003Cpath style=\"stroke:#FE0002\" fill=\"none\"\u002F>\n\u003Cg stroke=\"rgb(254, 0, 2)\" fill=\"none\">\u003Cpath\u002F>\u003C\u002Fg>\n\n\u003Cstyle>.st0{fill:none;stroke:#FE0002}\u003C\u002Fstyle>\n\u003Cpath class=\"st0\"\u002F>\n\n\u003Cdefs>\u003Cstyle>\u003C![CDATA[.str0{stroke:#FE0002;fill:none}]]>\u003C\u002Fstyle>\u003C\u002Fdefs>\n\u003Cpath class=\"str0\"\u002F> \u003Cstyle> works anywhere in the document — inside \u003Cdefs>, or even after the elements it styles. CDATA wrapping is supported, as are class \u002F id \u002F type \u002F attribute \u002F descendant selectors and comma-grouped lists, ordered by specificity then document order. The one exception is !important, which does not participate: an inline value always wins. Only these colour notations are parsed: #RGB, #RGBA, #RRGGBB, #RRGGBBAA, and numeric rgb() \u002F rgba(). Alpha is ignored when matching, so #FE0002FF, #FE000280 and rgba(254,0,2,.5) all group as #FE0002 — a translucent red is still a cut line. Colour cannot be read in four cases: external stylesheets (\u003Clink> \u002F @import)declarations inside conditional rules such as @mediaCSS variables and currentColorunrecognised notations: named colours, hsl(), percentage rgb() Those elements are not lost. They land in \"Other vector\", and the user assigns the processing type themselves.",{"id":395,"title":396,"titles":397,"content":398,"level":132},"\u002Fen\u002Fdocs\u002Fexport\u002Fsvg-color-spec#the-groups-do-not-match-what-you-drew","The groups do not match what you drew",[54],"\"Other vector\" splits by colour. Off-spec colours are not lumped together — each distinct colour gets its own row, which is where the green and purple rows at the bottom of the dialog above come from: the grouping colour takes stroke over fillthe same colour written differently (#22C55E \u002F rgb(34,197,94)) stays one rowthe same colour across several SVG files merges into one rowevery row's processing type is empty by default — the platform never guessesorder: the four spec groups first, then other-vector rows by element count, most first A shape that is both blue-filled and red-stroked is split in two. fill=\"#2366FF\" plus stroke=\"#FE0002\" expresses two operations — fill-engrave the interior, cut the outline — but the protocol allows only one processing type per element: \u003C!-- what you exported -->\n\u003Crect fill=\"#2366FF\" stroke=\"#FE0002\"\u002F>\n\n\u003C!-- after the platform processes it -->\n\u003Crect fill=\"#2366FF\" stroke=\"none\" data-processing-type=\"FILL_VECTOR_ENGRAVING\"\u002F>\n\u003Crect fill=\"none\"    stroke=\"#FE0002\" data-processing-type=\"VECTOR_CUTTING\"\u002F> The fill copy is inserted before the stroke copy, preserving the original fill-then-stroke paint order, so nothing looks different. The copy carries no id, and marker-* stays on the stroke copy only. The shape appears in both groups, so the element counts add up to more than you drew. The split only happens when both sides are spec colours. A red stroke on a green fill is not split — green expresses no processing intent, so the whole shape groups as a cut line by its stroke.",{"id":400,"title":401,"titles":402,"content":403,"level":132},"\u002Fen\u002Fdocs\u002Fexport\u002Fsvg-color-spec#the-shape-did-not-appear-at-all","The shape did not appear at all",[54],"Elements that participate: path rect circle ellipse line polyline polygon text, plus image for bitmaps. Elements that do not: shapes inside \u003Cdefs> \u003CclipPath> \u003Cmask> \u003Cmarker> \u003Cpattern> \u003Csymbol> — they are definitions, nothing is paintedelements with display: none or visibility: hidden, whether written as an attribute, an inline style, or a \u003Cstyle> ruleelements with both fill=\"none\" and stroke=\"none\" \u003Cuse> is the easiest trap to fall into. The shape it draws never appears in any group, so it never receives a processing type: \u003Cuse> itself is not a vector shape, and a referenced original sitting inside \u003Cdefs> \u002F \u003Csymbol> does not count either. If the whole drawing is assembled from \u003Cuse>, the dialog shows no rows at all. The sneakier case is a referenced original that is itself painted. With \u003Cpath id=\"p\" …\u002F> plus \u003Cuse href=\"#p\" x=\"20\"\u002F>, the original groups normally and gets a processing type while the copy drawn by \u003Cuse> gets none — one shape is cut, its duplicate is not. Emit the real shapes instead. Multi-file exports: identical groups merge across files and an unparsable SVG is skipped on its own. With Open in Studio, standalone bitmap files (PNG \u002F JPEG \u002F WebP \u002F GIF \u002F BMP) are wrapped into an SVG \u003Cimage>, so they show up under the \"Bitmap\" group too; anything else passes through untouched. Download always delivers your bytes as-is — no wrapping, no attributes. When no group can be produced at all — no SVG, or SVGs made entirely of the elements above — the dialog hides the \"Processing type\" section. html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sPWt5, html code.shiki .sPWt5{--shiki-default:#7EE787}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}",{"id":57,"title":58,"titles":405,"content":406,"level":122},[],"Once you've integrated the platform capabilities and verified everything in local preview, you're ready to publish from the Developer Console. There's no local packaging tool needed anymore — just produce static files using your own build process.",{"id":408,"title":58,"titles":409,"content":406,"level":122},"\u002Fen\u002Fdocs\u002Fpublish#submit-for-review-publish",[],{"id":411,"title":412,"titles":413,"content":414,"level":132},"\u002Fen\u002Fdocs\u002Fpublish#_1-prepare-the-artifact","1. Prepare the Artifact",[58],"Build a static Web bundle that's ready to deploy (with the platform SDK already included in the page), then package it into a single .zip file (50 MB max). Your app depends on the platform-sdk.js script tag in the page to connect to platform capabilities in production. Some build configurations can cause the production entry page to drop this script, which results in \"works fine in local preview, but export stops working after publishing.\" Before uploading, make sure the entry page in your production artifact is the same one you verified locally — and that it still includes the SDK.",{"id":416,"title":417,"titles":418,"content":419,"level":132},"\u002Fen\u002Fdocs\u002Fpublish#_2-create-the-application","2. Create the Application",[58],"Click \"Create Application\" in the Developer Console, and give your generator a name to serve as its runtime identifier (must start with a lowercase letter, and contain only lowercase letters, digits, and hyphens). This name is tied to the production subdomain and access path, and cannot be changed after creation — choose carefully. The card title shown to users is configured separately in the next step.",{"id":421,"title":422,"titles":423,"content":424,"level":132},"\u002Fen\u002Fdocs\u002Fpublish#_3-configure-listing-details","3. Configure Listing Details",[58],"Go to \"Application List Details\" on the app detail page, and fill in the card title, short description, and cover image. A live preview of the app card is shown on the right. This information determines how users see your app on the Creative Tools page, and any changes here also require review. The preview has two channels: Atomm shows a 4:3 card, Studio shows a 1:1 square slot. You only upload the 4:3 cover — the square slot is centre-cropped from it automatically. Switch to the Studio tab to see the crop, and keep your subject away from the edges so nothing important gets cut.",{"id":426,"title":427,"titles":428,"content":429,"level":132},"\u002Fen\u002Fdocs\u002Fpublish#_4-upload-the-code-artifact","4. Upload the Code Artifact",[58],"Switch to \"Code Artifact\" and upload your packaged .zip (containing index.html and other static files). For live local debugging during development, use the online DevTool — see Local Debugging for details.",{"id":431,"title":432,"titles":433,"content":434,"level":132},"\u002Fen\u002Fdocs\u002Fpublish#_5-submit-for-review","5. Submit for Review",[58],"In \"Submit for Review,\" the platform checks that both the listing details and the code artifact are complete. Once everything is ready, click \"Submit for Review\" — the listing changes and the new artifact are bundled into a single \"pending change\" and submitted together. The review result (approved \u002F rejected, with reasons) is sent back to your console via a message. Once submitted, the application enters \"Under Review.\" You'll need to wait for the result before submitting new changes. Once approved, the application is published automatically, and its runtime URL becomes  — users can open it directly from the generator gallery.",{"id":61,"title":62,"titles":436,"content":199,"level":122},[],{"id":438,"title":62,"titles":439,"content":199,"level":122},"\u002Fen\u002Fdocs\u002Ffaq#faq",[],{"id":441,"title":442,"titles":443,"content":444,"level":132},"\u002Fen\u002Fdocs\u002Ffaq#do-i-need-to-install-a-command-line-tool","Do I need to install a command-line tool?",[62],"No. Integration only requires adding a single line to import platform-sdk.js into your page. For debugging, just open the online DevTool in your browser and load your local service.",{"id":446,"title":447,"titles":448,"content":449,"level":132},"\u002Fen\u002Fdocs\u002Ffaq#which-tech-stacks-are-supported","Which tech stacks are supported?",[62],"Any of them. Plain HTML, Vite, Vue, React — anything that ultimately produces deployable static web files will work.",{"id":451,"title":452,"titles":453,"content":454,"level":132},"\u002Fen\u002Fdocs\u002Ffaq#what-address-should-my-local-service-use-why-wont-my-address-load","What address should my local service use? Why won't my address load?",[62],"Preview loads your local service via ?local= (only loopback addresses such as localhost \u002F 127.0.0.1 are supported). www.atomm.com runs over https, and browsers only allow http for localhost; http addresses other than localhost are blocked by the mixed-content policy. Use localhost, or enable https for your local service.",{"id":456,"title":457,"titles":458,"content":459,"level":132},"\u002Fen\u002Fdocs\u002Ffaq#platform-capabilities-arent-available-or-the-panel-shows-integration-incomplete-what-should-i-do","Platform capabilities aren't available, or the panel shows \"Integration incomplete\" — what should I do?",[62],"This means the page failed to load platform-sdk.js. Check that the script is included correctly and that your local service is reachable, then reload the page. Once the SDK handshake succeeds, the message will clear automatically.",{"id":461,"title":462,"titles":463,"content":464,"level":132},"\u002Fen\u002Fdocs\u002Ffaq#can-the-generator-name-runtime-identifier-be-changed","Can the generator name (runtime identifier) be changed?",[62],"No. The name becomes part of the runtime subdomain and access path and is tied to your live URL. It cannot be changed after creation, so choose it carefully.",{"id":466,"title":467,"titles":468,"content":469,"level":132},"\u002Fen\u002Fdocs\u002Ffaq#what-happens-if-i-dont-integrate-export-functionality","What happens if I don't integrate export functionality?",[62],"The export button is available to every generator (it appears wherever you place data-atomm-export-button in your app). If you haven't registered the export hook, users who click export will get \"The app does not provide a download method,\" and this will get your submission rejected during review. To support export, you must register the export hook — you can test it in local preview by clicking \"Export\" to confirm it produces a file correctly.",{"id":65,"title":66,"titles":471,"content":472,"level":122},[],"Let Claude Code, Cursor, Copilot and the like read the atomm platform's APIs and requirements themselves, instead of you relaying them a paragraph at a time.",{"id":474,"title":66,"titles":475,"content":476,"level":122},"\u002Fen\u002Fdocs\u002Fai\u002Fllms-txt#llmstxt",[],"Let Claude Code, Cursor, Copilot and the like read the atomm platform's APIs and requirements themselves, instead of you relaying them a paragraph at a time. llms.txt is a convention for documentation written to be read by large language models. This site serves two routes: RouteWhat it holdsSize\u002Fllms.txtAn index of every doc, with links~750 tokens\u002Fllms-full.txtEvery English doc concatenated into one file~30k tokens Append .md to any docs URL to get that page as plain markdown —  or .",{"id":478,"title":479,"titles":480,"content":481,"level":132},"\u002Fen\u002Fdocs\u002Fai\u002Fllms-txt#which-one-to-hand-over","Which one to hand over",[66],"Your agent can fetch URLs: give it \u002Fllms.txt and let it pull only the pages it needs. Cheapest on context.You want everything at once: give it \u002Fllms-full.txt. Saves twenty-odd round trips, costs ~30k tokens of context.You only care about one chapter: give it that page's .md URL.",{"id":483,"title":484,"titles":485,"content":486,"level":132},"\u002Fen\u002Fdocs\u002Fai\u002Fllms-txt#usage","Usage",[66],"Paste the URL into the conversation: For example: read  first, then wire up the export flow in this generator the way atomm requires. The design system and the 3D preview requirements also ship as single files you can hand straight to an agent — see Skills.",{"id":69,"title":70,"titles":488,"content":489,"level":122},[],"A skill is a single-file spec written for a coding agent: no background, just what to build and what gets rejected at review. Hand the file over and the agent works from it — you don't have to restate the requirements in chat.",{"id":491,"title":70,"titles":492,"content":493,"level":122},"\u002Fen\u002Fdocs\u002Fai\u002Fskills#skills",[],"A skill is a single-file spec written for a coding agent: no background, just what to build and what gets rejected at review. Hand the file over and the agent works from it — you don't have to restate the requirements in chat. Two are published here, both downloadable: FileWhat it coversWhen to hand it overdesign.mdThe design system: semantic tokens, type, radii and spacing, component baselines, plus a contract and a self-check for agents. DESIGN.md formatWhenever an agent writes generator UI3d-preview-skill.mdthree.js 3D preview: architecture, interaction, render quality, acceptance checklistWhen the generator ships a 3D preview Both are also one click away from the top right of their own sections (Design, 3D Preview). design.md has to reach the agent together with the code it describes. It carries the templates' full addresses and one hard rule: fetch the matching template before writing any markup. How you hand it over depends on whether your agent can fetch a URL. It can fetch. Give it design.md alone — . It reads the decision table on Layout, fetches the skeleton it needs (swapping in the layout it picked — for example ) and builds on that file.It cannot fetch. Paste in design.md and the one skeleton that matches your layout. Without the template the agent is required to stop and ask you for it rather than invent one — the difference between a slow answer and a wrong one. Either way the project ends up with two files: your-project\u002F\n├── design.md                      # the spec — the agent reads this first\n└── index.html                     # a copy of the skeleton for your layout\n                                   #   \u003Cstyle>  tokens + component classes — don't rewrite them\n                                   #   match the structure and the class names; the logic is yours Fetch one template, not all five. They share the same stylesheet, and it is most of every skeleton by weight — so fetching them all costs context and adds nothing. Pick your base with the decision table on Layout; if you later need a block only another layout has, fetch that one then. Don't hand over a design-file link or screenshots instead. A screenshot carries the look but none of the numbers, so the agent guesses every colour, size and spacing — differently each time. The templates have the numbers.",{"id":495,"title":496,"titles":497,"content":498,"level":132},"\u002Fen\u002Fdocs\u002Fai\u002Fskills#handing-it-to-claude-code","Handing it to Claude Code",[70],"Save it as SKILL.md under your project's skills directory and the agent loads it on its own when the task calls for it: .claude\u002Fskills\u002Fatomm-3d-preview\u002FSKILL.md The 3D preview file already downloads as SKILL.md — create the directory and drop it in.",{"id":500,"title":501,"titles":502,"content":503,"level":132},"\u002Fen\u002Fdocs\u002Fai\u002Fskills#handing-it-to-other-tools","Handing it to other tools",[70],"Cursor, Copilot and friends: keep the file in the repo (say docs\u002Fatomm-3d-preview.md) and @-reference it in chat. For a one-off, pasting the whole file into the conversation works too.",{"id":505,"title":506,"titles":507,"content":508,"level":132},"\u002Fen\u002Fdocs\u002Fai\u002Fskills#how-this-differs-from-llmstxt","How this differs from LLMs.txt",[70],"LLMs.txt indexes the whole documentation set. It fixes \"the agent doesn't know what atomm can do.\"A skill is the spec for one job. It fixes \"the agent knows, but what it built won't pass review.\" Building a 3D preview? Hand over both: llms.txt to learn the platform, the skill to get the details right.",{"id":73,"title":18,"titles":510,"content":511,"level":122},[],"The design system is two things, handed to a coding agent together:",{"id":513,"title":18,"titles":514,"content":515,"level":122},"\u002Fen\u002Fdocs\u002Fdesign\u002Foverview#overview",[],"The design system is two things, handed to a coding agent together: FileWhat it isHow to use itdesign.mdThe spec as a single DESIGN.md-format file: tokens, component specs, design intent, a contract for agents at the top and a self-check at the endPut it in the project and have the agent read it firstFive layout templatesOne per generator layout, each in two HTML files: a skeleton (structure + stylesheet) and a runnable one (behaviour inlined as well)Read the skeleton and copy it into your project; take the runnable one to see the example work The pages below are written for people — the same tokens and components, with demos you can touch. Changes happen in one place: design.md is the single source of truth, and the docs pages and templates follow it.",{"id":517,"title":518,"titles":519,"content":520,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Foverview#why-templates-not-descriptions","Why templates, not descriptions",[18],"There used to be a written spec only. The same file handed to different agents produced good UI one day and poor UI the next, because every agent was rebuilding the 28px input, the property-card header row and the select's keyboard contract from prose, and rebuilding always varies. The agent's job is now reuse: the template already has the right skeleton, components and behaviour, so the work left is content and whatever your generator adds on top. That is what makes the output steady. A template is deliberately one file: no second stylesheet or script to copy alongside it, no relative path to get right, and double-clicking it runs it in a browser.",{"id":522,"title":523,"titles":524,"content":525,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Foverview#skeleton-skin-and-behaviour","Skeleton, skin and behaviour",[18],"The spec has three layers, and the contract section of design.md keeps them apart: Skeleton — must match: the five layouts and their region widths, where the tabs \u002F Fabrication Tips \u002F zoom \u002F export sit, control heights (24 \u002F 28 \u002F 32 \u002F 40), radii (4 \u002F 6 \u002F 8 \u002F 12), the closed state set, the keyboard and screen-reader contract each control owes, and every platform constraint. All of it is judged on what renders — the framework, the tags and the class names are yours.Skin — default, replaceable: every colour value and the type sizes. The values shipped here come from the platform's own token export, so they match its surfaces; you may bring your own neutrals and accent as long as the result stays light, text that carries meaning clears 4.5:1, every control state stays distinct, and you change token values, never token names.Behaviour — yours: what the generator actually makes, which parameters it exposes, what Generate produces, how far zoom goes, what the history holds — none of it is specified. The runnable templates answer these one way so the example runs; treat those answers as a demo, not a requirement. Review checks two things: the skeleton — the layouts, widths, control sizes and behaviour listed above — and the platform constraints: export goes through the platform button, the generator uses only the region it is given, the exported file is 1:1 actual size. The skin is advice.",{"id":527,"title":528,"titles":529,"content":530,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Foverview#a-workbench-not-a-storefront","A workbench, not a storefront",[18],"The reference for a generator is the properties panel of a professional creative tool: quiet chrome, dense enough to work in, never decorated. Whoever is using it will run the same generator twenty times in an afternoon, changing one value each time. At that repetition rate the value of the interface is its predictability: controls stay in the same place, values stay legible, and the twentieth transition looks exactly like the first. SizeWidthmmColumnsRandomness35%StyleShapeCircleCircleSquareHexagonStarShow ticksA real slice of the parameters rail: white property cards standing on a bg-subtlest panel, header rows at 12 \u002F 500, field rows at 12 \u002F 400, grey-filled borderless 28px inputs aligned to the end edge. Click a header to collapse the card, drag the slider and the value follows — the spec describes controls in every state, and you only really understand a state you can touch. What it is not: not a marketing page, not a dashboard, not a consumer app. No hero moment, no gradients, no glass, no glow, no illustrated empty state, no entrance animation. Those belong to products that still have to convince you to stay. This one already has your attention.",{"id":75,"title":76,"titles":532,"content":533,"level":122},[],"This chapter is not a matter of style — these are the conditions of running inside atomm. Review checks the skeleton and this chapter: ignore the skin and you can still publish; ignore this chapter and the submission fails review.",{"id":535,"title":76,"titles":536,"content":537,"level":122},"\u002Fen\u002Fdocs\u002Fdesign\u002Fplatform#platform-constraints",[],"This chapter is not a matter of style — these are the conditions of running inside atomm. Review checks the skeleton and this chapter: ignore the skin and you can still publish; ignore this chapter and the submission fails review. Export must go through the platform button. Place \u003Cdiv data-atomm-export-button>\u003C\u002Fdiv> in the export slot and register an atomm.lifecycle.on('export', …) hook. A submission with no export hook is rejected automatically. The slot is where the templates put it: the footer of the parameters rail, or the canvas's bottom-end corner when there is no rail. The SDK renders the button, and you theme it only through the documented --atomm-export-* variables — the templates set width 100%, height 32px, radius 6px and the brand colours, which is exactly the button-primary shape. During development you may render a fallback button inside the slot; the SDK takes precedence when present. See Export. …property cards…ExportA full-width 32px button inside the rail footer's 16px padding. It does not move while content scrolls, so nobody ever has to go looking for it. Treat the exported file as machine-facing. 1:1 real dimensions, mm units, a viewBox in mm coordinates. Cut-line colours are mapped to machine processes by the device and do not follow the interface palette; see the SVG export colour spec. Reference images used for preview never enter the drawing. The top bar belongs to the platform — brand, account, credits. Do not rebuild it, do not imitate it, do not add a second one. Your own controls (project name, save, history) go in the header of your lead rail. You render inside the region the platform allocates. You cannot cover the platform chrome, you do not know the viewport, and the stacking context is entirely yours: one overlay layer plus one for toasts is enough, and z-index caps at 3.",{"id":79,"title":80,"titles":539,"content":540,"level":122},[],"Generators combine two questions — what goes on the canvas (left) and what does it look like (right) — into one of five layouts. The platform's own generators cover all five; yours starts from the template closest to it.",{"id":542,"title":80,"titles":543,"content":540,"level":122},"\u002Fen\u002Fdocs\u002Fdesign\u002Flayout#layout",[],{"id":545,"title":546,"titles":547,"content":548,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Flayout#the-five-templates","The five templates",[80],"#TemplateLeftRightUse it when1layout-1-params.htmlnone320 paramsNothing to pick or generate; the user only adjusts values2layout-2-templates.html320 templates320 paramsOne left-side job: browse and pick a template3layout-3-generate.html320 generate, docked320 paramsStart from an input (prompt, style, image), then tune4layout-4-floating.html360 generate, floatingnonePick a style, set the run up in a dialog, nothing to tune afterwards5layout-5-multi.html64 tools + 320 panel320 paramsSeveral left-side jobs: generate, templates, import If you cannot decide between 3 and 4, there is one question: is anything adjusted after generating? If yes, 3. If the result is final, 4. 1 No left rail + params2 Templates 320 + params3 Generate 320 docked + params4 Generate 360 floating, no params5 Tools 64 + panel 320 + paramsFive combinations, one skeleton: something to pick or generate on the left, the result in the middle, what to adjust on the right, and export (the dark block) always bottom-right. Each template comes in two forms. The skeleton (\u003Clayout>.skeleton.html) is the structure plus the full stylesheet with the script block removed — read this one when you write markup, because nothing in it competes with the layout and the tokens for attention. The runnable form (the same URL without .skeleton) has the behaviour inlined as well, for seeing the example work; what its controls do is a demo, not the spec. Both are a single HTML file with no second file to copy alongside. Every control in it works — reset, the unit toggle, drag-to-change on numeric fields, zoom, the select, collapse, the Fabrication Tips walkthrough, the second-level views. The export button is the one stand-in: it says what the platform would do. Try them below, then download one and drop it into your project.",{"id":550,"title":551,"titles":552,"content":553,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Flayout#regions","Regions",[80],"RegionWidthWhat is in itPlatform top bar64The platform draws it — never add a second oneTool rail64Layout 5 only: one icon per left-side panelLeft rail320, or 360 when floatingWhat there is to add or to generate: templates, styles, a prompt, an importCanvaseverything the rails leaveThe preview, plus four overlays: view tabs, Fabrication Tips, zoom, and export when there is no parameters railParameters rail320The stack of property cards, with export pinned at the bottom Rails are fixed width. The canvas absorbs every resize: when the window gets wider, the parameters rail does not. Docked regions have no gap and no radius between them — a docked rail meets the canvas at a single 1px stroke-default line. The floating rail is the one exception: a white card, 12px radius, shadow-100, inset 16px from the canvas's top, start and bottom edges. Left rails collapse to a 40px pill in the canvas's top-start corner (rail title plus the panel icon), and the canvas grows. In layout 5 the panel collapses by clicking the already-selected tool. The parameters rail is always 320px and always the same three bands: Header row — unit toggle at the start, Reset at the endMiddle — a scrolling stack of property cardsFooter — the export button, pinned so nobody has to scroll to find it Derived results (usage, part count, duration) are annotations on the canvas, or one card at the bottom of the stack. They never get a column of their own.",{"id":555,"title":556,"titles":557,"content":558,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Flayout#the-four-corners-of-the-canvas","The four corners of the canvas",[80],"The canvas carries four overlays and nothing else: view tabs at the top centre, the Fabrication Tips capsule at the top end, the zoom cluster at the bottom start. The fourth is export, and it appears at the bottom end only when there is no parameters rail. Overlays do not move on hover. Zoom is a free value, not a set of steps: the wheel and a two-finger pinch scale the canvas continuously and anchor on the pointer, dragging the canvas pans it, the readout is a menu button whose menu lists a few levels as shortcuts, and fit measures the canvas and re-centres. Which levels the menu lists, and how far the range goes, is yours to decide. A lead rail can hold a second level: \"View all\" on a section, or the history row at the rail's foot, replaces the rail's content with a back row and a three-column grid of 90px tiles. The canvas does not change and no dialog opens; Back is the only way out. 2D design3D previewExportTips100%Fit to canvas50%75%100%200%400%ExportThe view switch is a segmented control, not a tab strip: two or three mutually exclusive views (2D design \u002F 3D preview \u002F Export). More than three means the canvas is carrying too much. The capsule at the top end opens the Fabrication Tips walkthrough and nothing else.",{"id":560,"title":561,"titles":562,"content":563,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Flayout#property-cards","Property cards",[80],"Parameters are grouped by object or by function into property cards: white, stroke-divider border, an 8px radius on all four corners and shadow-100, stacked 12px apart on the bg-subtlest panel. A card is a header row plus the field rows under it. The header row is one \u003Cbutton aria-expanded> across the full width, title at the start and disclosure chevron at the end. The card itself is a \u003Csection> pointed at that header with aria-labelledby. Cards with few parameters stay open; cards with many may start collapsed. Collapsing changes two things and nothing else: hidden on the body, and the chevron's rotation. No height animation. A field row is label ⋯ control, 8px vertical and 16px horizontal padding, label in text-primary, control aligned to the end. The kind of value decides the control: a number takes a 92px input, a choice a 110px select, a range a slider with the number beside it, on\u002Foff a switch, a colour a 24px swatch. StyleShapeCircleCircleSquareHexagonStarStroke widthmmColorFill modeOption AOption BDecorationShow ticksThe second card starts collapsed: the header row stays, the chevron flips, the card is one row tall. Click the header to open it.",{"id":565,"title":566,"titles":567,"content":568,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Flayout#flow-and-states","Flow and states",[80],"The layout has to make the flow obvious: pick or input → configure → generate → review → export. Live-preview generators redraw as a slider moves, so they have no generate step; adding a Generate button to one is a defect. The preview has three states and all three need designing: Empty — examples or a hint, never a blank rectangleComputing — a stage name, a number, and a way to cancel; never an indefinite spinnerDone — zoom, pan, compare, regenerate Errors belong beside the control that caused them, together with the next action. A toast at the top of the screen is where errors go to be missed.",{"id":570,"title":571,"titles":572,"content":573,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Flayout#sizing-and-scrolling","Sizing and scrolling",[80],"Your width is the region the platform allocates, not the viewport, so adapt with container queries. Below 960px the three regions stack, canvas first and rails after, and the export button stays pinned to the bottom. Rails scroll inside themselves with thin scrollbars. Never introduce horizontal scrolling, and never nest one scroll container inside another. A panel that fills its space and scrolls needs both flex: 1 and min-height: 0. flex: 1 makes it grow; min-height: 0 lets it shrink below its content. Without the second, a flex child keeps its default min-height: auto, refuses to shrink, and overflows instead of scrolling. Every scroll container in the templates carries the pair.",{"id":83,"title":84,"titles":575,"content":576,"level":122},[],"Every interactive element supports the same closed state set: default, hover, active, focus-visible, disabled, loading, selected, error. There is no ninth state; a control that seems to need one needs rethinking instead.",{"id":578,"title":84,"titles":579,"content":580,"level":122},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#components",[],"Every interactive element supports the same closed state set: default, hover, active, focus-visible, disabled, loading, selected, error. There is no ninth state; a control that seems to need one needs rethinking instead. Heights are skeleton: 28 for the input family, 32 for buttons, segmented controls and icon buttons, 40 for the one large call to action, 24 for icon plates and swatches. Every component below has a class of the same name in the \u003Cstyle> block of every template — using those classes is the shortest path, and building the same sizes and states in your own framework is just as valid.",{"id":582,"title":583,"titles":584,"content":585,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#buttons","Buttons",[84],"ExportResetCancelDeleteExportExportGenerateTipsThe top row is six 32px shells, disabled and loading included; the bottom row is the 40px call to action, an icon-only button and the Tips capsule. button-primary — near-black, 32px, answers most questions. The platform's own Export button has exactly this shapebutton-primary-large — 40px and full width, for the one call to action in a lead railbutton-secondary — white, with a border that darkens on hoverbutton-ghost — transparent until hovered Destructive actions use the secondary shell with red-default text and a confirmation, never a solid red fill. Loading keeps the button's width, swaps in a spinner and sets aria-busy. Labels are verbs (Generate, Export, Reset), never OK or Submit. Icon-only buttons are 32px squares on an 8px radius, white on the canvas. The Tips button in the canvas's top-end corner is the system's second and last capsule: 32px, white, stroke-default border, a 20px lightbulb and the word — it has to read as help, not as one more action.",{"id":587,"title":588,"titles":589,"content":590,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#the-input-family-grey-fill-no-border","The input family: grey fill, no border",[84],"DefaultmmHover · deeper fillmmFocus · white + 1px stroke-activemmPlaceholdermmDisabledmmErrormmWidth cannot exceed 300 mm.input-field: 28px tall, 92px wide, bg-control fill, no border, 6px radius, text in text-secondary with tabular figures. The error message sits beside the field, not only in a toast.The unit is pinned to the field's end, not carried inside the value: 10px in text-tertiary, a separate element. Numbers then line up at the start edge and units at the end, and switching mm ↔ inch rewrites the two independently. A unit inside the value moves as the number grows, and reorders under RTL bidi. A numeric field carries its starting value in value and inputmode=\"decimal\". Numeric fields support horizontal drag: press and drag to change the value, Shift for ten-times steps, Alt for fine steps, and a movement under 3px counts as a click. Use setPointerCapture so dragging outside the panel keeps the gesture, invert the direction in RTL, and keep Shift + ↑\u002F↓ as the keyboard equivalent. Never show native spinners: the targets are tiny and the register is wrong. The prompt box in a generate rail is the same family grown up: 8px radius, 10\u002F12 padding, a character counter in text-disabled at the bottom-end corner.",{"id":592,"title":593,"titles":594,"content":595,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#select","Select",[84],"The trigger is the input box at 110px with a text-tertiary chevron that flips when the list opens. The list is a floating surface, so unlike the docked panels it takes a border, an 8px radius and shadow-200. CircleCircleSquareHexagonStarClick the trigger to close and reopen it, and click an option to select it. Open, the trigger goes white with a 1px `stroke-active` border — the same state as focus — and the chevron flips.Two signals that never collide, and each is legible alone. The fill says where you are: pointer and keyboard share it, so exactly one row is ever filled. The brand-default checkmark plus 500 weight says what is chosen, and it still reads with no fill under it. Above, Circle is selected and Square is where the pointer sits.The row highlight gets a 12% step of its own because the 6% and 8% panel fills both vanish on a popup's white ground. Rows are 32px on a 4px radius, 10px inline padding, 12px text in text-primary. The list is 6px-padded, at least as wide as the trigger, and caps at seven rows before it scrolls with a thin scrollbar. Above three options a select beats a segmented control; below three, the segmented control beats a select.",{"id":597,"title":598,"titles":599,"content":600,"level":346},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#the-contract-this-owes","The contract this owes",[84,593],"A native \u003Cselect> gives the keyboard, the screen reader and the platform picker away for free. Styling the popup means writing all of that yourself. Every template already ships it as upgradeSelects in its script block: port that, do not re-derive it. It is written as progressive enhancement, so the control works before the script runs: the markup ships a real \u003Cselect> inside a \u003Cspan class=\"select-field\" data-select>. The script hides that element, builds the trigger and the list, and keeps the native one as the value's source of truth — form submission and your own change listeners keep working untouched. 1Focus never leaves the trigger. aria-activedescendant points at the active option — moving focus into the list is how you end up stranding it on body when the list closes.2role=\"combobox\" and aria-expanded on the trigger, role=\"listbox\" on the list, role=\"option\" and aria-selected on each row.3Closed: ↓ ↑ Enter Space Home End or any character opens it.4Open: ↓↑ move, Home\u002FEnd jump to the ends, Enter or Space commits, Escape closes without changing the value, Tab closes, an outside click closes, type-ahead jumps.5Focus returns to the trigger on close, and the active row scrolls inside the list's own box rather than the page's.6The list mounts on body with position: fixed — left in place, a scrolling parameters rail clips it — and repositions on scroll and resize.7It flips above the trigger when the space below runs out, and aligns to the trigger's end edge in RTL.8On narrow touch screens fall back to the native \u003Cselect>: you give up the system picker otherwise, and 32px rows are small under a thumb.Everything above is implemented in upgradeSelects, in every template's script block. It is the one piece of a template you keep: the rest is demo wiring you replace with your own state.",{"id":602,"title":603,"titles":604,"content":605,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#switch","Switch",[84],"36 × 20 track · 14px knob · travels 16pxstroke-default off, brand-default on. The track is below the 24px target minimum, so the field row's \u003Clabel for> is the larger target that makes it reachable — every template wires the switch that way.The track and the knob are decoration painted over a real \u003Cinput type=\"checkbox\">; every layer needs pointer-events: none and aria-hidden.",{"id":607,"title":608,"titles":609,"content":610,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#slider","Slider",[84],"Corner radius6 mmDensity40%Disabled40%Never alone: the value always appears as a number at the end of the row, and when precision matters pair it with an input. A 3px track in track-off, the filled part in brand-default, a 12px thumb with a white centre and a 2px brand ring. Step and range come from the model's real constraints; a silently clamped value is a bug the user cannot see.",{"id":612,"title":613,"titles":614,"content":615,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#segmented-control","Segmented control",[84],"2D design3D previewmminchOption AOption BOption CA 32px bg-hover track, 2px padding, 6px radius; items are 12-medium in text-secondary, and the active item is white on a 4px radius with shadow-100; a 1px stroke-divider line separates adjacent inactive items. The container takes role=\"group\" with a name, each option a \u003Cbutton aria-pressed>. Use it for two or three mutually exclusive views; above three, use a select. It is not a tab strip and does not carry page navigation.",{"id":617,"title":618,"titles":619,"content":620,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#thumbnails-and-the-tool-rail","Thumbnails and the tool rail",[84],"Classic circleSquare frameMinimalAntique goldphoto.pngGenerateTemplatesA template thumbnail is a 140px white card: 8px radius, stroke-divider border, shadow-100. An inline SVG sits inset 14px; a photo fills the card edge to edge. Style tiles are the same card at 90px with no shadow (SVG inset 10px), and a round subject keeps the square card with a circular image inside it. Selection is a 2px brand-default ring drawn over the card, so the content never resizes; hover is stroke-default.The tool rail is 64px wide with 40px plates. The selected tool is brand-default with a white glyph.",{"id":622,"title":623,"titles":624,"content":625,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#platform-fed-blocks","Platform-fed blocks",[84],"Two pieces of UI draw their data from the platform rather than from your generator: the material the preview is rendered on, and the user's history. The templates ship them working with placeholders so they sit in the right place from day one; wire the data when the capability is open to you. SurfaceExportMy historyLoad moreSurface is a property card at the bottom of the stack: 57px tiles four to a row, one selected with a 2px text-primary ring 2px outside the tile. It changes how the preview renders, never what exports. History is a second level inside the lead rail: a back row, 90px tiles three to a row, Load more beneath. Tiles follow the thumbnail rule: an inline SVG sits inset 10px, a photo fills the tile.",{"id":627,"title":628,"titles":629,"content":630,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#dialogs","Dialogs",[84],"Fabrication Tips1 \u002F 3IllustrationReal-world unitsAll dimensions are physical (mm \u002F inch) and match the exported file 1:1 — what you set is what gets cut.BackNextNative \u003Cdialog> with showModal(): 480px wide, 12px radius, shadow-300, scrim bg-overlay (70% black). On a short viewport the body scrolls; header and footer stay put. Header 16\u002F24: title in 16-semibold, a 20px info glyph in text-tertiary beside it, the 24px close button at the end, hairline below. Then an optional media band in bg-control, and a footer with pagination dots at the start and buttons at the end. Most things put in dialogs do not belong there: errors go beside their control, progress goes in the preview, settings go in the parameters rail. Fabrication Tips is the canonical dialog. Antique bronze1:1Transparent backgroundReference imageClick or drag an image hereYour photo, logo or artwork becomes the coin face · max 10MBDescribe your design (optional)CancelGenerateThe generate dialog (layout 4). The dark bg-inverse header repeats the pick: a 66px thumbnail, the name in 16-semibold, one tag per fact the choice already fixed. The body stacks form fields — the one place where a form-label sits above its control rather than beside it, because a 480px dialog has room for a full-width drop target and a 320px rail does not. Picking a style tile opens this, so the tile itself never starts the call.",{"id":632,"title":633,"titles":634,"content":635,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#alerts-links-helper-text","Alerts, links, helper text",[84],"Alerts put the tint behind the message and the solid on the icon, with text-primary words, next to whatever they describe (see Colour). Links take blue-default and keep their underline — colour alone is not a link affordance. Helper text under a field is 12-regular in text-tertiary and explains a constraint rather than restating the label. Be stingy. Anything that fits in a unit suffix should not become a sentence — 20 % beats \"percentage of a single tile's edge length\". Anything readable from the results should not be repeated in the parameters rail. The ideal end state is a parameters rail with two or three genuinely constraining notes left in it.",{"id":87,"title":88,"titles":637,"content":638,"level":122},[],"The palette is written in two layers. Primitives hold the raw values and are the only place a hex appears; semantic tokens reference them and say what a colour is for, and components reference the semantic layer only. That is what makes a reskin a change to a dozen lines rather than to every component.",{"id":640,"title":88,"titles":641,"content":642,"level":122},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#colour",[],"The palette is written in two layers. Primitives hold the raw values and are the only place a hex appears; semantic tokens reference them and say what a colour is for, and components reference the semantic layer only. That is what makes a reskin a change to a dozen lines rather than to every component. The whole colour layer is skin: the values here are the platform default and the five templates ship with them; a generator may replace the set. What does not change is the structure — the role each token plays, and the contrast each role has to clear.",{"id":644,"title":645,"titles":646,"content":647,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#the-neutral-ramp","The neutral ramp",[88],"neutral-0#ffffffcards, white controlsneutral-25#fcfcfddocked panelsneutral-50#f6f6f7the plate under a swatchneutral-100#eeeff1control fillneutral-200#e7e8eathe ground, disabled fillneutral-300#d6d8dbstrokes, off trackneutral-400#b9bbc0hover stroke, disabled textneutral-500#85878btertiary text 3.60:1neutral-800#3d3e42secondary text 10.68:1neutral-900#292a2dbrandneutral-950#171719primary text 17.90:1Eleven near-neutral greys; click to copy a hex. The token export's ramp has fourteen steps — 600, 700 and 1000 belong to the platform's own editor chrome (grid lines, the shadow base), so a generator region never reaches for them. The three lightest (0 \u002F 25 \u002F 100) carry card, panel and control fill; the three darkest (800 \u002F 900 \u002F 950) carry secondary text, the brand colour and primary text.",{"id":649,"title":650,"titles":651,"content":652,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#a-neutral-workbench-with-one-dark-accent","A neutral workbench with one dark accent",[88],"Panels are bg-subtlest, cards on the panels are pure white, and the ground the preview stands on is bg-editor — three steps darker than the panels. The ground is the floor of the room and the panels are furniture standing on it; a preview reads as an object placed on that floor. Controls take a third grey, bg-control, one step lighter than the ground — a recess in a white surface rather than the floor itself. bg-editorbg-defaultbg-subtlestThree surfaces a step or two apart are enough to separate floor, panel and card. Docked regions carry no shadow — only tone and a 1px stroke-default line. brand-default is a near-black (neutral-900). It carries the primary button, the filled part of a slider, a switch in its on state, the selected tool in the rail and the selection ring on thumbnails. Hover lightens it one step to neutral-800 and press darkens it to neutral-950 — a press always lands darker than rest. emphasize-default (atomm-600) is the platform's red and the one exception to a neutral chrome: at most one emphasized action per screen, on the thing that carries a cost or a consequence. The platform's own generators leave Generate on brand-default, so no template ships an emphasized button.",{"id":654,"title":655,"titles":656,"content":657,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#four-text-steps","Four text steps",[88],"text-primaryField labels, card titles, values, headings17.90:1text-secondaryText inside inputs and selects, button labels on white, thumbnail labels10.68:1text-tertiaryHelper text, placeholders, \"View all\", inactive rail items3.60:1text-disabledDisabled labels, character counters1.92:1The first two clear 4.5:1 on every surface in the system. text-tertiary does not — 3.60:1 on white, 3.13:1 on the control fill. It follows that tertiary carries only text a reader can afford to miss: a helper line, a placeholder, an inactive label. Never a value, never a message, never a label a decision rests on. Disabled is not meant to be read at all.",{"id":659,"title":660,"titles":661,"content":662,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#three-strokes","Three strokes",[88],"stroke-divider · 8% inkThe border of a property card, the line under a card header, the edge of a floating panel. Hairline: it separates, it does not outline.stroke-default · neutral-300The edge of a docked rail, a secondary button, a thumbnail on hover, the pill-shaped Fabrication Tips button.stroke-strong · neutral-400The hover step for anything that has a stroke-default border.Inputs have no border at rest: the grey fill is the affordance. A border appears only on focus, and then it is stroke-active.",{"id":664,"title":665,"titles":666,"content":667,"level":346},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#three-neutral-fills","Three neutral fills",[88,660],"They look close and they are not interchangeable: bg-hover · 6%bg-active · 8%bg-highlight · 12%6% is the hover step on a panel, 8% is a pressed or selected state there, and 12% is the row highlight inside a floating list. The first two land at 1.10–1.14:1 on the white ground of a popup and simply disappear, which is why a floating row has a step of its own — a menu row you cannot see is a menu row you cannot use.",{"id":669,"title":670,"titles":671,"content":672,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#status","Status",[88],"The chrome is neutral: brand-default carries everything that needs weight, and emphasize-default is rationed to one action per screen. Parameters were recomputed against the latest model constraints.File exported, 200 × 120 mm.A 0.1 mm line really is a hair on screen. That is correct.Width cannot exceed 300 mm.Status colours work in pairs: the tint is the fill, the solid is the icon, and the words stay text-primary (15–17:1 on every tint). Coloured text on a coloured tint is the most common accessibility mistake in status messaging — and it would fail here, since green-default and warning-default measure 2.6–2.7:1 on their own tints. stroke-surround is a solid blue at 4.76:1 — often the only sign of where the keyboard is, so it clears 3:1 on its own with room to spare.",{"id":674,"title":675,"titles":676,"content":677,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#reskinning","Reskinning",[88],"If you replace the palette: Stay light — the ground darker than the panels, never dark mode.Recompute every text step against every surface it sits on, and keep the two that carry meaning (text-primary, text-secondary) at 4.5:1 or better.Keep stroke-divider \u002F stroke-default \u002F stroke-strong distinguishable from each other and from the fills.Change values, not names. Variables like --color-brand-default are published to every generator; renaming one is a breaking change.",{"id":91,"title":92,"titles":679,"content":680,"level":122},[],"Inter throughout, three weights, line heights in even pixels. The token export names a style by its metrics and ships no role aliases, so the table below is the mapping: which kind of thing takes which style. Seven of the export's twenty styles appear inside a generator region — the rest belong to the platform's own marketing and landing pages.",{"id":682,"title":92,"titles":683,"content":684,"level":122},"\u002Fen\u002Fdocs\u002Fdesign\u002Ftypography#typography",[],"Inter throughout, three weights, line heights in even pixels. The token export names a style by its metrics and ships no role aliases, so the table below is the mapping: which kind of thing takes which style. Seven of the export's twenty styles appear inside a generator region — the rest belong to the platform's own marketing and landing pages. StyleMetricsCarries16-semibold16 \u002F 600 \u002F 22Dialog titles only14-medium14 \u002F 500 \u002F 20Buttons, rail headings (\"Templates\", \"AI Generate\")12-medium12 \u002F 500 \u002F 16Property-card titles, segmented items12-regular12 \u002F 400 \u002F 16Field labels, values inside inputs, select text, helper text, alert and dialog body11-regular11 \u002F 400 \u002F 16Thumbnail labels, \"View all\"10-medium10 \u002F 500 \u002F 14The selected tool-rail label10-regular10 \u002F 400 \u002F 14The unit suffix, and a tool-rail label at rest 16-semiboldFabrication Tips16 \u002F 600 \u002F 2214-mediumGenerate14 \u002F 500 \u002F 2012-mediumSize12 \u002F 500 \u002F 1612-regularCorner radius12 \u002F 400 \u002F 1611-regularClassic circle11 \u002F 400 \u002F 1610-mediumTemplates10 \u002F 500 \u002F 1410-regularmm10 \u002F 400 \u002F 14Each style at its real size, carrying the kind of thing it is for. Only two of the seven are above 12px, and the four 10–12px steps differ by weight rather than by size — that is what keeps a 320px rail readable without making it tall.",{"id":686,"title":687,"titles":688,"content":689,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Ftypography#the-panel-is-built-on-12px","The panel is built on 12px",[92],"Labels, values and card titles all share 12px. The hierarchy inside a card comes from weight (card title 500, field label 400) and from position (header row above, field rows below), not from size. 14px is reserved for the two things that have to read from across the room: a button and a rail heading. Templates label · 14 \u002F 500Size field-strong · 12 \u002F 500Width field · 12 \u002F 400mmnote · 12 \u002F 400 · text-tertiaryGenerate label · 14 \u002F 500Three layers inside one card: card title 12 \u002F 500 \u002F text-primary, field label 12 \u002F 400 \u002F text-primary, value inside the input 12 \u002F 400 \u002F text-secondary. What separates them is weight and colour, not size — so the panel never grows taller as it grows richer. Raising the field register to 14 makes every row 4px taller and a 320px rail lose a card of content. Field labels, values and helper text all share 12-regular, so a generator that needs bigger text moves the whole register up one step — to 14-regular — and accepts the taller rows. There is no way to raise one of them and not the others, which is the point.",{"id":691,"title":692,"titles":693,"content":694,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Ftypography#numbers","Numbers",[92],"Values always use tabular-nums: equal-width digits keep 200, 3 and 12 aligned in a column of 92px inputs, and a value does not jitter sideways as it changes. The unit sits at the end of the same field as its own element, in 10-regular; it never becomes a column of its own.",{"id":95,"title":96,"titles":696,"content":697,"level":122},[],"The token export defines nine radii; a generator region uses six of them, each tied to a kind of thing. The pairing is skeleton: a reskin does not change it. (The other three — tiny 2, xxlarge 16, xxxlarge 24 — belong to the platform's own surfaces.)",{"id":699,"title":96,"titles":700,"content":701,"level":122},"\u002Fen\u002Fdocs\u002Fdesign\u002Fshapes#shapes",[],"The token export defines nine radii; a generator region uses six of them, each tied to a kind of thing. The pairing is skeleton: a reskin does not change it. (The other three — tiny 2, xxlarge 16, xxxlarge 24 — belong to the platform's own surfaces.) rounded.none (0) — the bottom two corners of an expanded property card's header row (collapsed, the header is the whole card and takes all four)rounded.small (4px) — tags, the 24px icon plates in section headers, the active item of a segmented controlrounded.medium (6px) — every control: input, select, button, segmented track, swatchrounded.large (8px) — thumbnail cards, the zoom cluster, icon buttons, the collapsed pill, the 40px tool plate, and property cardsrounded.xlarge (12px) — dialogs and the floating rail 24 · sm 428 · md 632 · md 640 · lg 8card · lg 8dialog · xl 12Radius follows height: 28 and 32px controls take 6px, the 40px call to action takes 8px, floating containers take 8 and 12, and a property card takes 8 on all four corners. The pairing matters more than the numbers — a 40px button at 6px looks unfinished, and at 20px it looks like a different product. rounded.circle is the export's 100px, not 9999px: it only reaches a pill on something narrower than 200px, which every fully-round thing here is — a 36px switch track, a 12px thumb, the 68px Tips capsule. It is for true circles: the switch knob, the slider thumb, status dots. Two capsules exist in the system and no more: the switch track, because a rounded-rectangle switch reads as a checkbox, and the Tips button, which has to read as help rather than as one more action. Every other button is a rounded rectangle. Corners are the only shape device. No notches, no angled cuts, no decorative borders, no gradient strokes.",{"id":98,"title":99,"titles":703,"content":704,"level":122},[],"Depth comes from surface tone and hairlines first, shadow second. Docked regions carry no shadow at all: three tones — panel (bg-subtlest), white card, grey canvas — plus 1px lines already keep them apart. Shadows go only to things that genuinely leave the plane.",{"id":706,"title":99,"titles":707,"content":708,"level":122},"\u002Fen\u002Fdocs\u002Fdesign\u002Felevation#elevation-depth",[],"Depth comes from surface tone and hairlines first, shadow second. Docked regions carry no shadow at all: three tones — panel (bg-subtlest), white card, grey canvas — plus 1px lines already keep them apart. Shadows go only to things that genuinely leave the plane. TokenValueUsed forshadow-1000 4px 12px rgba(11,11,13,.06), 0 0 4px rgba(11,11,13,.06)Floating rail, property cards, collapsed pill, thumbnail cards, the active segment, switch knob, slider thumbshadow-2000 16px 32px rgba(11,11,13,.06), 0 2px 8px rgba(11,11,13,.06)Dropdown listsshadow-3000 40px 80px rgba(11,11,13,.06), 0 2px 8px rgba(11,11,13,.06)Dialogs shadow-100shadow-200shadow-300Three steps and no fourth: needing one usually means something that belongs flat on the panel has been lifted off it. Every step is two layers — an offset shadow gives direction, a zero-offset one gives a white card an edge even on a white panel — and all three share a single shadow colour at 6%. shadow-100 is also the smallest step there is: knobs and thumbs take it, and nothing in the system takes anything smaller. No shadow is ever animated, and nothing lifts on hover.",{"id":710,"title":711,"titles":712,"content":713,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Felevation#focus-is-its-own-layer","Focus is its own layer",[99],"Keyboard focus is a 2px stroke-surround outline with a 2px offset. The offset is load-bearing: it separates the ring from the control's own fill, so the ring is as visible on a near-black button as on a white one. Setting it to 0 is exactly why focus rings disappear on primary buttons. ExportResetoffset 2pxExportoffset 0 — the ring hugs the dark fill and nearly vanishesThe input family is the exception: inputs and selects show focus by turning their fill white and drawing a 1px stroke-active border. They are already a filled box; a ring around one reads as two boundaries.",{"id":101,"title":102,"titles":715,"content":199,"level":122},[],{"id":717,"title":102,"titles":718,"content":719,"level":122},"\u002Fen\u002Fdocs\u002Fdesign\u002Fmotion#motion",[],"TokenValuemotion.fast150msmotion.base200msmotion.easingcubic-bezier(0.2, 0, 0.38, 1) Motion confirms that something changed. It never carries meaning on its own and it never makes the user wait. 1Interactive feedback — hover, press, toggle, a border changing colour: fast.2Content transitions — rail collapse, dialog enter and exit: base.3Animate opacity, transform and colour — never a layout property. The GPU is busy rendering your preview; animating height or width drops frames.4Nothing exceeds base. Nothing bounces, overshoots or lingers.5Generation progress is not an animation. It is a number and a stage name.6No entrance animation on load.7prefers-reduced-motion: reduce collapses every duration to zero. Nothing is lost, because no state is conveyed by motion alone.A property card collapses by changing hidden and the chevron's transform, with no height animation. A segmented control switches with a 150ms change of fill and shadow. Every state change in the demos on these pages uses the three values above.",{"id":104,"title":105,"titles":721,"content":722,"level":122},[],"Icons are inlined as stroked SVG, never an icon font.",{"id":724,"title":105,"titles":725,"content":726,"level":122},"\u002Fen\u002Fdocs\u002Fdesign\u002Ficon#iconography",[],"Icons are inlined as stroked SVG, never an icon font. The probability of an icon font failing to load from a CDN is not small (network policy, sandboxes, offline), and the failure mode is bad: icon-only buttons become empty boxes, or degrade into a substitute glyph that is stylistically wrong. Given how often icons appear, betting on an external dependency is not worth it. ExpandView allZoom inZoom outFitCollapseResetGenerateTemplatesImportCloseSelectedSpecification: viewBox=\"0 0 16 16\" (20 for the tips lightbulb); fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round. Colour always comes from the parent's text token — an icon with its own colour is an icon that is wrong in the next state.",{"id":728,"title":729,"titles":730,"content":731,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Ficon#three-hit-area-steps","Three hit-area steps",[105],"24 · minimum32 · default40 · tool plateIcon-only controls use 24 \u002F 32 \u002F 40, with 32 the default. There is no step below 24. The glyph inside is free to be smaller: a 24px plate and a 32px button both carry a 16px glyph. When a control has to look smaller than its target — a switch track, a slider, an 11px text link — keep the visual size and pad the hit area to 24, with an equal negative margin so the layout does not move.",{"id":733,"title":734,"titles":735,"content":736,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Ficon#three-kinds-of-icon-three-obligations","Three kinds of icon, three obligations",[105],"A decorative icon beside its own text label takes aria-hidden so it is not announced twice.An interactive icon-only button takes an aria-label describing the action, not the picture (\"Zoom in\", not \"plus\").An indicative icon carrying state needs screen-reader text, because colour and shape alone convey nothing. Directional icons — arrows, undo, indent — need a class that RTL can flip. Clocks, checkmarks, play buttons, logos and charts with a time axis must never be flipped. Prefer up\u002Fdown chevrons where there is a choice: they need no flipping, which is why the property-card disclosure points up and down rather than left and right.",{"id":108,"title":109,"titles":738,"content":739,"level":122},[],"The platform renders your generator in a document whose dir follows the user's locale, and several supported locales read right to left. Mirroring is not optional, and there is no second stylesheet: the layout has to flip itself.",{"id":741,"title":109,"titles":742,"content":743,"level":122},"\u002Fen\u002Fdocs\u002Fdesign\u002Fdirection#direction-rtl",[],"The platform renders your generator in a document whose dir follows the user's locale, and several supported locales read right to left. Mirroring is not optional, and there is no second stylesheet: the layout has to flip itself. Written the modern way this is nearly free. Every one-sided value uses a logical property — margin-inline-start, padding-inline-end, inset-inline-start, text-align: start; symmetric shorthands need no attention; flex and grid mirror themselves, and adding flex-row-reverse for RTL flips a layout that had already flipped. WidthmmDensity40%Show ticks100%TipsSwitch to RTL: the parameters rail moves to the right, labels and controls swap sides in every row, the slider fills the other way, the switch knob travels -16px, and the zoom cluster and the Tips capsule swap corners. All of that is interface, and it mirrors. The artboard stays put. Canvas content is never mirrored. Lock the layer that holds the artboard to direction: ltr; the logical properties inside always resolve to the left, so the artboard's orientation and the exported file's are independent of the interface around them. The lock stops at the artboard: the overlays on the canvas are interface, not artifact, so they mirror with everything else — in RTL the zoom cluster and the Tips capsule belong on the mirrored side. Locking the whole canvas region would strand a right-to-left reader's controls on the wrong edge.",{"id":745,"title":746,"titles":747,"content":748,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Fdirection#what-changes-item-by-item","What changes, item by item",[109],"The slider's webkit fill uses linear-gradient(to right …) and needs to left in RTL.The drag-to-change direction inverts: dragging toward the start edge increases the value.A dropdown positioned by JS aligns to the trigger's end edge rather than copying left.Do not centre a floating hint with inset-inline-start: 50% plus translateX(-50%); use inset-inline: 0; margin-inline: auto.The progress bar's transform-origin switches sides.The switch knob starts from inset-inline-start but travels with translateX(16px); transform is physical, so RTL needs -16px.A number and its unit read as one run. In an RTL paragraph direction \"6 mm\" reorders to \"mm 6\". A numeric field is safe — its unit is a separate element pinned to the end — but a readout that concatenates them, like the value beside a slider, takes unicode-bidi: plaintext. Any transform that means \"move forward\" has to decide its own direction. Set dir=\"rtl\" and walk the whole flow before submitting.",{"id":112,"title":113,"titles":750,"content":751,"level":122},[],"The floor, not the ceiling:",{"id":753,"title":113,"titles":754,"content":755,"level":122},"\u002Fen\u002Fdocs\u002Fdesign\u002Faccessibility#accessibility",[],"The floor, not the ceiling: Every interactive target is at least 24×24px (WCAG 2.2 SC 2.5.8), and the templates hold to it with none left over. Check the width too — icon-only buttons are square. Where the visual is deliberately smaller — the 20px switch track, the 12px slider thumb, an 11px text link — the templates keep the visual size and grow the hit area with padding plus an equal negative margin, so the target reaches 24 without the row getting taller. --min-target is the value they all reference; change it in one place and every one of them follows.Verify contrast per pairing, never assume it. Recompute the key pairings after a reskin, including the worst case of small text on the control fill. text-tertiary sits below AA by design, so it never carries a value, a message, or a label a decision rests on.Keyboard reaches everything a mouse reaches, in visible order. Dialogs trap focus while open and return it to the trigger on close. A hand-built listbox owes the contract a native \u003Cselect> gives away; see Components.[hidden] needs restoring. Any component that sets display: flex or grid overrides the browser default [hidden] { display: none }. Ship [hidden] { display: none !important } in the base stylesheet — a collapsed panel leaking into view is usually this.Meaning never rides on colour, shape or motion alone. A red border needs a message; a spinner needs text; a selected thumbnail needs its ring, not just a tint.",{"id":116,"title":117,"titles":757,"content":758,"level":122},[],"The conclusions of the previous chapters, compressed into something you can check line by line. The first part is the contract at the top of design.md; the second is the self-check at the end of it, which an agent should run against its own output and fix before submitting.",{"id":760,"title":117,"titles":761,"content":758,"level":122},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules#rules",[],{"id":763,"title":764,"titles":765,"content":766,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules#ten-rules","Ten rules",[117],"1The layout starts from one of the five templates. Refine it and combine blocks across layouts freely — but keep the skeleton: rails are fixed-width, the canvas takes everything else.2The parameters rail is 320px; parameters live in property cards; export is pinned to the rail's bottom.3View tabs sit top-centre of the canvas, the tips button top-end, the zoom cluster bottom-start.4Controls are 28 (inputs, selects), 32 (buttons, segmented, icon buttons), 40 (the one big call to action).5Radii are 6 on controls, 8 on cards and panels, 12 on dialogs. No pills except the switch track and the Tips button.6Inputs are grey-filled with no border; focus draws a 1px stroke-active border. Selection is a 2px brand ring, never colour alone.7Panel text is 12px; only buttons and rail headings are 14px. No raw hex — every colour is a --color-* variable.8Export goes through data-atomm-export-button and the export hook. No top bar of your own.9Icons are inline stroked SVG. Every interactive target is at least 24×24 and shows a pointer cursor. Nothing conveys meaning by colour or motion alone.10Nothing exceeds 200ms, nothing bounces, nothing animates on load.These ten cover most rejections. The skeleton (layout and sizes, as they render) must match; the skin (colour, type sizes) is a default you may replace as a set — changing values, never names; what the generator does is not the spec's business.",{"id":768,"title":769,"titles":770,"content":771,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules#self-check","Self-check",[117],"The page is one of the five layouts; every rail width is 64 \u002F 320 \u002F 360 and the parameters rail is 320.View tabs are top-centre of the canvas, the Tips capsule top-end, the zoom cluster bottom-start; export is in the rail footer or the canvas's bottom-end corner and stays put while content scrolls.There is no top bar of your own and no second overlay layer; z-index never exceeds 3.Every control height is 24, 28, 32 or 40; inputs and selects are 28, buttons 32, the one large call to action 40.Every radius is 4, 6, 8, 12 or a true circle, except the bottom two corners of an expanded property card's header row, which are square; the switch track and the Tips button are the only capsules.Inputs are grey-filled with no border at rest, white with a 1px stroke-active border on focus, 92px wide; selects 110px.Parameters are inside property cards with a disclosure header; rows are label ⋯ control with 8\u002F16 padding.No raw colour appears in component CSS; every colour is a --color-* variable and every hex in :root is in the palette (or your declared reskin).Panel text is 12px, buttons and rail headings 14px, thumbnail labels 11px, rail labels 10px (or your declared scale applied consistently).Selected states have a 2px brand ring or a checkmark, not colour alone; every error has a message beside its control.Every interactive target is at least 24×24; every icon-only button has an aria-label; every decorative icon is aria-hidden.Every control that reacts to a click shows cursor: pointer, every text field a caret, everything disabled not-allowed; a value readout keeps the default arrow.Icons are inline SVG with stroke: currentColor; no icon font is loaded.Sliders show their value as a number; switches are real checkboxes with pointer-events: none on their decoration; selects follow the combobox contract or are native.Nothing animates longer than 200ms, nothing animates on load, and prefers-reduced-motion zeroes every duration.data-atomm-export-button exists and the export lifecycle hook is registered; the exported file is 1:1 in mm.The flow works with dir=\"rtl\": rails, overlays, slider fill and switch knob all mirror; the canvas content does not.Below 960px the regions stack, the canvas comes first, export stays pinned, and there is no horizontal scrolling.",{"id":773,"title":774,"titles":775,"content":199,"level":132},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules#dos-and-donts","Do's and don'ts",[117],{"id":777,"title":80,"titles":778,"content":779,"level":346},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules#layout",[117,774],"Do pick one of the five templates and keep its regions and widths.Do put export where the template puts it — rail footer, or bottom-end of the canvas.Don't add a top bar, a second overlay layer, or a column for derived results.Don't put a Generate button on a live-preview generator.",{"id":781,"title":782,"titles":783,"content":784,"level":346},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules#controls","Controls",[117,774],"Do group parameters into property cards with a disclosure header and label ⋯ control rows.Do keep the state set closed; a control that seems to need a ninth state needs rethinking.Do write out the keyboard and screen-reader contract before replacing a native control, then repay it item by item.Don't give inputs a border at rest, and don't use native spinners.Don't fill a destructive button with red; use the secondary shell with red-default text and a confirmation.Don't let a decorative layer take pointer events away from the control underneath it.",{"id":786,"title":787,"titles":788,"content":789,"level":346},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules#colour-and-type","Colour and type",[117,774],"Do reference --color-* variables everywhere; a raw hex in a component is a bug.Do keep the ground (bg-editor) darker than the panels, and white cards on the panels.Do recompute the text steps against every surface if you reskin.Don't invent a size between the export's steps; move the whole field register from 12-regular to 14-regular, or leave it.Don't convey state by colour alone — a selected thumbnail has a ring, an error has a message.",{"id":791,"title":792,"titles":793,"content":794,"level":346},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules#engineering","Engineering",[117,774],"Do give a panel that fills its space and scrolls both flex: 1 and min-height: 0 — one without the other is the overflow bug.Do mount popovers on body with position: fixed.Do inline icons as stroked SVG and inherit their colour.Don't rely on a classic scrollbar taking no space; use a thin one.Don't animate a layout property (width, height, inset); opacity, transform and colour are the safe set.",{"id":796,"title":42,"body":797,"description":301,"extension":875,"icon":43,"meta":876,"navigation":877,"path":41,"seo":878,"stem":879,"__hash__":880},"docs_en\u002Fen\u002Fdocs\u002F3d-preview\u002F06.recipes.md",{"type":798,"value":799,"toc":873},"minimark",[800,804,807],[801,802,42],"h1",{"id":803},"common-parts",[805,806,301],"p",{},[808,809,810,823],"table",{},[811,812,813],"thead",{},[814,815,816,820],"tr",{},[817,818,819],"th",{},"Part",[817,821,822],{},"How to build it",[824,825,826,853,865],"tbody",{},[814,827,828,832],{},[829,830,831],"td",{},"Raised marks \u002F lettering",[829,833,834,835,839,840,843,844,847,848,852],{},"Reuse the cut part's outline path → ",[836,837,838],"code",{},"SVGLoader"," → ",[836,841,842],{},"ExtrudeGeometry",", and place it at ",[836,845,846],{},"z = panel thickness",". Add it to the content group like everything else — it inherits the group's Y flip, so do ",[849,850,851],"strong",{},"not"," flip it again",[814,854,855,858],{},[829,856,857],{},"Thin rings \u002F outlined parts",[829,859,860,861,864],{},"Generate inner and outer outlines at ",[836,862,863],{},"radius ± stroke\u002F2"," and extrude the thin ring; sample circles smoothly, flatten polygons",[814,866,867,870],{},[829,868,869],{},"Fold-up flaps",[829,871,872],{},"Treat every open cut arc as one flap, with the hinge axis running between the arc's two endpoints (its chord). Extrude the flap and rotate it about that axis, in whichever direction lifts the flap's centroid toward +z. The base panel gets the matching hole punched out, while the uncut core stays solid. Cycle the fold angle through a small set of values so the result looks natural",{"title":199,"searchDepth":132,"depth":132,"links":874},[],"md",{},true,{"title":42,"description":301},"en\u002Fdocs\u002F3d-preview\u002F06.recipes","-CIgl2WlpTjf3ITZrmYq2zRax7a51rOmA9MEmwVHX3g",[882,884],{"title":38,"path":37,"stem":883,"children":-1},"en\u002Fdocs\u002F3d-preview\u002F05.dependencies",{"title":46,"path":45,"stem":885,"children":-1},"en\u002Fdocs\u002F3d-preview\u002F07.checklist",1789616867645]