API reference

Build plugins to extend Acme Design Studio.

Getting Started

Acme plugins are written in JavaScript and have access to the Acme API.

Creating a Plugin

Create a folder with this structure:

my-plugin/
├── manifest.json
├── code.js
└── ui.html (optional)

manifest.json

{
  "name": "My Plugin",
  "id": "com.example.myplugin",
  "version": "1.0.0",
  "description": "A description of what your plugin does",
  "author": "Your Name",
  "main": "code.js",
  "ui": "ui.html",
  "permissions": ["read", "write"],
  "menu": [
    {
      "name": "My Plugin",
      "command": "show-ui"
    }
  ]
}

code.js

// Show plugin UI
acme.ui.onMessage(msg => {
  if (msg.type === 'create-rectangles') {
    const count = msg.count

    for (let i = 0; i < count; i++) {
      const rect = acme.createRectangle()
      rect.x = i * 150
      rect.y = 100
      rect.width = 100
      rect.height = 100
      rect.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]
    }

    acme.ui.postMessage({ type: 'complete' })
  }
})

acme.showUI(__html__, { width: 300, height: 200 })

ui.html

<!DOCTYPE html>
<html>
<body>
  <h2>Rectangle Generator</h2>
  <label>Count: <input id="count" type="number" value="5"></label>
  <button id="create">Create</button>

  <script>
    document.getElementById('create').onclick = () => {
      const count = document.getElementById('count').value
      parent.postMessage({
        pluginMessage: { type: 'create-rectangles', count: parseInt(count) }
      }, '*')
    }

    window.onmessage = (event) => {
      const msg = event.data.pluginMessage
      if (msg.type === 'complete') {
        console.log('Rectangles created!')
      }
    }
  </script>
</body>
</html>

Core API

Selection

Get and manipulate selected layers:

// Get current selection
const selection = acme.currentPage.selection

// Select specific nodes
acme.currentPage.selection = [node1, node2]

// Clear selection
acme.currentPage.selection = []

// Listen for selection changes
acme.on('selectionchange', () => {
  console.log('Selection changed:', acme.currentPage.selection)
})

Creating Nodes

Create design elements programmatically:

// Rectangle
const rect = acme.createRectangle()
rect.x = 100
rect.y = 100
rect.width = 200
rect.height = 150
rect.fills = [{ type: 'SOLID', color: { r: 0.2, g: 0.5, b: 1 } }]

// Circle
const circle = acme.createEllipse()
circle.x = 50
circle.y = 50
circle.width = 100
circle.height = 100

// Text
const text = acme.createText()
text.characters = "Hello World"
text.fontSize = 24
text.fontName = { family: "Inter", style: "Bold" }

// Frame
const frame = acme.createFrame()
frame.name = "Container"
frame.resize(400, 300)

// Add children to frame
frame.appendChild(rect)
frame.appendChild(text)

Modifying Properties

Change properties of existing nodes:

const node = acme.currentPage.selection[0]

// Position and size
node.x = 100
node.y = 200
node.resize(300, 200)

// Fills
node.fills = [
  { type: 'SOLID', color: { r: 1, g: 0, b: 0 } },
  { type: 'GRADIENT_LINEAR', gradientStops: [...] }
]

// Strokes
node.strokes = [
  { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }
]
node.strokeWeight = 2

// Effects
node.effects = [
  {
    type: 'DROP_SHADOW',
    color: { r: 0, g: 0, b: 0, a: 0.25 },
    offset: { x: 0, y: 4 },
    radius: 8
  }
]

// Opacity
node.opacity = 0.5

// Corner radius (for rectangles)
if (node.type === 'RECTANGLE') {
  node.cornerRadius = 12
}

Traversing the Tree

Navigate the layer hierarchy:

// Get current page
const page = acme.currentPage

// Iterate through all children
for (const node of page.children) {
  console.log(node.name, node.type)
}

// Recursive traversal
function traverse(node) {
  console.log(node.name)

  if ('children' in node) {
    for (const child of node.children) {
      traverse(child)
    }
  }
}

traverse(page)

// Find nodes by name
function findByName(name) {
  return page.findOne(node => node.name === name)
}

// Find all text nodes
function findAllText() {
  return page.findAll(node => node.type === 'TEXT')
}

Text API

Work with text layers:

const text = acme.createText()

// Set content
text.characters = "Hello World"

// Load font before setting
await acme.loadFontAsync({ family: "Inter", style: "Bold" })
text.fontName = { family: "Inter", style: "Bold" }

// Text properties
text.fontSize = 24
text.letterSpacing = { value: 0, unit: 'PIXELS' }
text.lineHeight = { value: 32, unit: 'PIXELS' }
text.textAlignHorizontal = 'LEFT' // 'LEFT', 'CENTER', 'RIGHT', 'JUSTIFIED'
text.textAlignVertical = 'TOP'     // 'TOP', 'CENTER', 'BOTTOM'

// Text case
text.textCase = 'UPPER' // 'UPPER', 'LOWER', 'TITLE', 'ORIGINAL'

// Get text length
console.log(text.characters.length)

// Styled text (mixed formatting)
text.setRangeFontSize(0, 5, 32) // Make first 5 chars size 32
text.setRangeFillStyleId(0, 5, styleId) // Apply color style

Component API

Create and manage components:

// Create component from selection
const selection = acme.currentPage.selection[0]
const component = acme.createComponent()
component.appendChild(selection.clone())

// Create instance
const instance = component.createInstance()
instance.x = 200
instance.y = 200

// Check if node is component or instance
if (node.type === 'COMPONENT') {
  console.log('This is a main component')
}

if (node.type === 'INSTANCE') {
  console.log('This is an instance')
  console.log('Main component:', node.mainComponent)
}

// Detach instance (convert to regular frame)
const instance = acme.currentPage.selection[0]
if (instance.type === 'INSTANCE') {
  instance.detachInstance()
}

Styles API

Work with color, text, and effect styles:

// Get all styles
const colorStyles = acme.getLocalPaintStyles()
const textStyles = acme.getLocalTextStyles()

// Create color style
const style = acme.createPaintStyle()
style.name = "Primary Blue"
style.paints = [{ type: 'SOLID', color: { r: 0, g: 0.5, b: 1 } }]

// Apply style to node
node.fillStyleId = style.id

// Create text style
const textStyle = acme.createTextStyle()
textStyle.name = "Heading 1"
textStyle.fontSize = 32
textStyle.fontName = { family: "Inter", style: "Bold" }

// Apply to text node
await acme.loadFontAsync({ family: "Inter", style: "Bold" })
textNode.textStyleId = textStyle.id

Export API

Export nodes as images:

// Export as PNG
const node = acme.currentPage.selection[0]
const bytes = await node.exportAsync({
  format: 'PNG',
  constraint: { type: 'SCALE', value: 2 } // 2x scale
})

// Export as SVG
const svgBytes = await node.exportAsync({
  format: 'SVG'
})

// Export as JPEG
const jpegBytes = await node.exportAsync({
  format: 'JPG',
  quality: 0.8 // 0-1
})

// Save to file system (with user permission)
acme.ui.postMessage({
  type: 'export',
  bytes: bytes,
  filename: 'export.png'
})

Plugin UI API

Communication between plugin code and UI:

// code.js - Show UI
acme.showUI(__html__, {
  width: 400,
  height: 300,
  title: "My Plugin"
})

// code.js - Receive messages from UI
acme.ui.onMessage(msg => {
  if (msg.type === 'create-shape') {
    const rect = acme.createRectangle()
    // ... configure rect

    acme.ui.postMessage({ type: 'done' })
  }

  if (msg.type === 'close') {
    acme.closePlugin()
  }
})

// ui.html - Send message to plugin
parent.postMessage({
  pluginMessage: { type: 'create-shape', color: 'red' }
}, '*')

// ui.html - Receive messages from plugin
window.onmessage = (event) => {
  const msg = event.data.pluginMessage
  if (msg.type === 'done') {
    console.log('Shape created!')
  }
}

Network API

Make HTTP requests from plugins:

// GET request
const response = await acme.fetch('https://api.example.com/data')
const data = await response.json()

// POST request
const response = await acme.fetch('https://api.example.com/create', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'New Item' })
})

Storage API

Persist plugin data:

// Save data
await acme.clientStorage.setAsync('myKey', { foo: 'bar', count: 42 })

// Load data
const data = await acme.clientStorage.getAsync('myKey')
console.log(data) // { foo: 'bar', count: 42 }

// Delete data
await acme.clientStorage.deleteAsync('myKey')

// Get all keys
const keys = await acme.clientStorage.keysAsync()

Notifications

Show messages to the user:

// Success notification
acme.notify('Operation completed!', { timeout: 3000 })

// Error notification
acme.notify('Something went wrong', { error: true })

// Persistent notification (must be dismissed manually)
acme.notify('Processing...', { timeout: Infinity })

Best Practices

  1. Always check types before modifying nodes
  2. Load fonts before setting fontName on text
  3. Use async/await for asynchronous operations
  4. Handle errors gracefully
  5. Clean up when plugin closes
  6. Minimize expensive operations in loops
  7. Cache frequently accessed nodes and styles

Example Plugins

Batch Rename

const selection = acme.currentPage.selection

acme.ui.onMessage(msg => {
  if (msg.type === 'rename') {
    const { find, replace } = msg

    let count = 0
    for (const node of selection) {
      if (node.name.includes(find)) {
        node.name = node.name.replace(find, replace)
        count++
      }
    }

    acme.notify(`Renamed ${count} layers`)
    acme.ui.postMessage({ type: 'complete', count })
  }
})

Color Palette Generator

// Generate color palette from selected image
const node = acme.currentPage.selection[0]

if (node.type === 'RECTANGLE' && node.fills[0].type === 'IMAGE') {
  const bytes = await node.exportAsync({ format: 'PNG' })

  // Extract colors (simplified)
  const colors = extractColors(bytes) // Your color extraction logic

  // Create color styles
  for (const color of colors) {
    const style = acme.createPaintStyle()
    style.name = `Palette ${color.name}`
    style.paints = [{ type: 'SOLID', color }]
  }

  acme.notify(`Created ${colors.length} color styles`)
}