Vue 3 Integration: Pendant Generator Example
Goals:
- Create a Vue 3 project.
- Integrate the Login SDK.
- Integrate "Open in Studio."
For new generators, prefer generator-workbench as the official shell. This guide remains useful when you need to understand or customize the raw SDK integration flow inside a Vue project.
Step 1: Create a Vue 3 Project
According to the Vue official documentation, it is recommended to use create-vue:
npm create vue@latestIf you use pnpm:
pnpm create vue@latestFor a quick setup, choose the simplest configuration:
- TypeScript:
No - JSX:
No - Vue Router:
No - Pinia:
No - Vitest:
No - E2E:
No - ESLint:
No - Prettier:
No
After creation, enter the directory and start the project:
cd your-project-name
npm install
npm run devStep 2: Install generator-sdk
npm install @atomm-developer/generator-sdkStep 3: Create SDK Singleton
Create src/sdk.js:
import { GeneratorSDK } from '@atomm-developer/generator-sdk'
export const sdk = GeneratorSDK.init({
appKey: import.meta.env.VITE_ATOMM_APP_KEY,
env: import.meta.env.MODE === 'production' ? 'prod' : 'dev',
})Add .env.local in the project root:
VITE_ATOMM_APP_KEY=your_app_keyStep 4: Implement Login
The login logic is similar to the CDN version, focusing on these three actions:
- Call
sdk.auth.login()when the button is clicked. - Listen to
sdk.auth.onChange(). - Use
status.userInfo.headpicdirectly.
import { onMounted, onUnmounted, ref } from 'vue'
import { sdk } from './sdk'
const authStatus = ref({ isLogin: false, userInfo: null })
let unsubscribeAuth = null
onMounted(() => {
authStatus.value = sdk.auth.getStatus()
unsubscribeAuth = sdk.auth.onChange((status) => {
authStatus.value = status
})
})
onUnmounted(() => {
unsubscribeAuth?.()
})
async function handleLogin() {
try {
await sdk.auth.login()
} catch (error) {}
}
async function handleLogout() {
try {
await sdk.auth.logout()
} catch (error) {}
}In the template:
<button v-if="!authStatus.isLogin" @click="handleLogin">Log in</button>
<div v-else class="user-menu">
<img :src="authStatus.userInfo?.headpic" alt="User avatar" />
<button @click="handleLogout">Log out</button>
</div>Step 5: Implement Pendant Preview
The pendant in this demo is a minimalist rectangular tag with a hole inside, containing the word "generator" and a 2px stroke.
import { computed, reactive } from 'vue'
const state = reactive({
width: 96,
height: 128,
})
function getPendantGeometry(width, height) {
const viewBoxWidth = 360
const viewBoxHeight = 420
const bodyWidth = Math.max(168, width * 2.08)
const bodyHeight = Math.max(210, height * 2.08)
const bodyX = (viewBoxWidth - bodyWidth) / 2
const bodyY = 96
const cornerRadius = Math.max(14, Math.min(bodyWidth, bodyHeight) * 0.06)
const holeRadius = Math.max(10, Math.min(width, height) * 0.1)
const holeCenterX = viewBoxWidth / 2
const holeCenterY = bodyY + Math.max(26, bodyHeight * 0.14)
const textY = bodyY + bodyHeight * 0.58
const textSize = Math.max(20, Math.min(28, bodyWidth * 0.11))
return {
viewBoxWidth, viewBoxHeight, bodyWidth, bodyHeight, bodyX, bodyY,
cornerRadius, holeRadius, holeCenterX, holeCenterY, textY, textSize,
}
}
// For on-screen preview — white fill + drop-shadow
const previewSvg = computed(() => {
const g = getPendantGeometry(state.width, state.height)
return `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${g.viewBoxWidth} ${g.viewBoxHeight}" style="width:min(100%,520px);height:auto;">
<defs>
<filter id="softShadow" x="-20%" y="-20%" width="140%" height="160%">
<feDropShadow dx="0" dy="14" stdDeviation="18" flood-color="#000000" flood-opacity="0.16" />
</filter>
</defs>
<g filter="url(#softShadow)">
<path d="M ${g.bodyX + g.cornerRadius} ${g.bodyY}
L ${g.bodyX + g.bodyWidth - g.cornerRadius} ${g.bodyY}
Q ${g.bodyX + g.bodyWidth} ${g.bodyY} ${g.bodyX + g.bodyWidth} ${g.bodyY + g.cornerRadius}
L ${g.bodyX + g.bodyWidth} ${g.bodyY + g.bodyHeight - g.cornerRadius}
Q ${g.bodyX + g.bodyWidth} ${g.bodyY + g.bodyHeight} ${g.bodyX + g.bodyWidth - g.cornerRadius} ${g.bodyY + g.bodyHeight}
L ${g.bodyX + g.cornerRadius} ${g.bodyY + g.bodyHeight}
Q ${g.bodyX} ${g.bodyY + g.bodyHeight} ${g.bodyX} ${g.bodyY + g.bodyHeight - g.cornerRadius}
L ${g.bodyX} ${g.bodyY + g.cornerRadius}
Q ${g.bodyX} ${g.bodyY} ${g.bodyX + g.cornerRadius} ${g.bodyY}
Z"
fill="#ffffff" stroke="#111111" stroke-width="2"
/>
<circle cx="${g.holeCenterX}" cy="${g.holeCenterY}" r="${g.holeRadius}" fill="#ffffff" stroke="#111111" stroke-width="2" />
<text x="${g.holeCenterX}" y="${g.textY}" text-anchor="middle" dominant-baseline="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="${g.textSize}" font-weight="600" letter-spacing="0.06em" fill="#111111">generator</text>
</g>
</svg>
`
})
// For export — root sets fill="none"; <path> and <circle> have NO fill attribute.
// Do NOT add fill="#ffffff" to child elements — it overrides the root and
// causes Studio to import shapes as solid black.
function buildExportSvg(width, height) {
const g = getPendantGeometry(width, height)
return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="${width}mm" height="${height}mm" viewBox="0 0 ${g.viewBoxWidth} ${g.viewBoxHeight}" fill="none">
<path d="M ${g.bodyX + g.cornerRadius} ${g.bodyY} L ${g.bodyX + g.bodyWidth - g.cornerRadius} ${g.bodyY} Q ${g.bodyX + g.bodyWidth} ${g.bodyY} ${g.bodyX + g.bodyWidth} ${g.bodyY + g.cornerRadius} L ${g.bodyX + g.bodyWidth} ${g.bodyY + g.bodyHeight - g.cornerRadius} Q ${g.bodyX + g.bodyWidth} ${g.bodyY + g.bodyHeight} ${g.bodyX + g.bodyWidth - g.cornerRadius} ${g.bodyY + g.bodyHeight} L ${g.bodyX + g.cornerRadius} ${g.bodyY + g.bodyHeight} Q ${g.bodyX} ${g.bodyY + g.bodyHeight} ${g.bodyX} ${g.bodyY + g.bodyHeight - g.cornerRadius} L ${g.bodyX} ${g.bodyY + g.cornerRadius} Q ${g.bodyX} ${g.bodyY} ${g.bodyX + g.cornerRadius} ${g.bodyY} Z"
stroke="#111111" stroke-width="2"/>
<circle cx="${g.holeCenterX}" cy="${g.holeCenterY}" r="${g.holeRadius}" stroke="#111111" stroke-width="2"/>
<text x="${g.holeCenterX}" y="${g.textY}" text-anchor="middle" dominant-baseline="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="${g.textSize}" font-weight="600" letter-spacing="0.06em" fill="#111111">generator</text>
</svg>`
}Render in the template:
<div v-html="previewSvg"></div>Step 6: Register Export Provider
Register an export provider that returns SVG directly for Studio, and falls back to canvas for other formats. Call sdk.export.openInStudio({ format: 'svg' }) to send an SVG to Studio.
<canvas ref="exportCanvasRef" style="display: none;" />import { onMounted, ref } from 'vue'
const exportCanvasRef = ref(null)
onMounted(() => {
sdk.export.register({
getExportData: (purpose, format) => {
if (format === 'svg') {
return { type: 'svg', svgString: buildExportSvg(state.width, state.height) }
}
return { type: 'canvas', canvas: exportCanvasRef.value }
},
getFileName: () => `pendant-${state.width}x${state.height}.svg`,
})
})
async function syncExportCanvas() {
const svgText = buildExportSvg(state.width, state.height)
const blob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' })
const url = URL.createObjectURL(blob)
const image = new Image()
const canvas = exportCanvasRef.value
const ctx = canvas.getContext('2d')
try {
await new Promise((resolve, reject) => {
image.onload = resolve
image.onerror = reject
image.src = url
})
canvas.width = 1080
canvas.height = 1260
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = '#ffffff'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(image, 0, 0, canvas.width, canvas.height)
} finally {
URL.revokeObjectURL(url)
}
}Add loading state to the button:
const isStudioLoading = ref(false)
async function handleOpenInStudio() {
try {
isStudioLoading.value = true
await sdk.export.openInStudio({ format: 'svg' })
} finally {
isStudioLoading.value = false
}
}Step 7: Upload Resources
- Package resources into a zip file. The zip file must contain an
htmlfile. For Vue/React projects, run the build command (e.g.,npm run build) and compress thedistdirectory into a zip file. - Upload the zip file to https://www.atomm.com/generator-upload. Note: If you don't have an xTool account, you need to register one first.
Key Steps Recap
- Create a minimal Vue 3 project using
create-vue. - Initialize the SDK in a separate
src/sdk.jsfile; do not put the SDK instance in reactive objects. - Integrate login via
sdk.auth.login()+sdk.auth.onChange()and displayuserInfo.headpic. - Register the hidden canvas via
sdk.export.register()and launch xTool Studio viasdk.export.openInStudio().
For more details on creating Vue 3 projects, refer to the Vue 3 Quick Start.