Skip to content

Vue 3 Integration: Pendant Generator Example

Goals:

  1. Create a Vue 3 project.
  2. Integrate the Login SDK.
  3. 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.

View Full Example

Step 1: Create a Vue 3 Project

According to the Vue official documentation, it is recommended to use create-vue:

bash
npm create vue@latest

If you use pnpm:

bash
pnpm create vue@latest

For 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:

bash
cd your-project-name
npm install
npm run dev

Step 2: Install generator-sdk

bash
npm install @atomm-developer/generator-sdk

Step 3: Create SDK Singleton

Create src/sdk.js:

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:

bash
VITE_ATOMM_APP_KEY=your_app_key

Step 4: Implement Login

The login logic is similar to the CDN version, focusing on these three actions:

  1. Call sdk.auth.login() when the button is clicked.
  2. Listen to sdk.auth.onChange().
  3. Use status.userInfo.headpic directly.
js
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:

vue
<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.

js
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:

vue
<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.

vue
<canvas ref="exportCanvasRef" style="display: none;" />
js
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:

js
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

  1. Package resources into a zip file. The zip file must contain an html file. For Vue/React projects, run the build command (e.g., npm run build) and compress the dist directory into a zip file.
  2. 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

  1. Create a minimal Vue 3 project using create-vue.
  2. Initialize the SDK in a separate src/sdk.js file; do not put the SDK instance in reactive objects.
  3. Integrate login via sdk.auth.login() + sdk.auth.onChange() and display userInfo.headpic.
  4. Register the hidden canvas via sdk.export.register() and launch xTool Studio via sdk.export.openInStudio().

For more details on creating Vue 3 projects, refer to the Vue 3 Quick Start.

MIT Licensed