본문 바로가기

개발자

플러그인 API 베타

플러그인이 에디터와 대화하는 전체 표면의 명세입니다. 모든 호출은 비동기이며, 문서는 액션으로만 바뀝니다.

빠른 시작

플러그인은 파일 세 개를 담은 zip입니다. 화면은 평범한 HTML 문서 하나 — 에디터가 스크립트보다 먼저 SSHOWPlugin SDK를 주입하고 샌드박스 패널에 띄우며, 아래가 그 화면이 닿을 수 있는 표면 전부입니다.

my-plugin.sshowplugin  (zip)
├─ plugin.json         — manifest
├─ ui.html             — the plugin screen (manifest.main)
└─ icon.svg            — listing icon (optional)

plugin.json은 신원과 진입 문서를 선언합니다:

{
    "id": "com.example.hello",
    "name": "Hello",
    "version": "1.0.0",
    "api": 1,
    "main": "ui.html",
    "description": "Inserts a greeting card.",
    "author": "SSHOW",
    "icon": "icon.svg"
}

ui.html이 곧 플러그인입니다 — 연결한 뒤 핸들로 읽고 씁니다:

<!doctype html>
<button id="insert">Insert</button>
<script>
    (async () => {
        const api = await SSHOWPlugin.connect();
        api.ui.resize(160);

        document.querySelector('#insert').addEventListener('click', async () => {
            await api.document.applyActions([{
                op: 'create_object', type: 'text', config: {
                    name: 'greeting',
                    data: { text: 'Hello, SSHOW!', fontSize: 48, autoSize: true },
                    transform: { x: 200, y: 200 }
                }
            }], 'Hello plugin');
        });
    })();
</script>

바로 실행해 보기: 세 파일을 my-plugin.sshowplugin 으로 zip해 에디터 플러그인 패널의 + 버튼으로 임포트하세요. 수정할 때마다 다시 임포트하거나, 아래 데스크톱 개발 루프를 쓰면 됩니다.

에디터 안 호스트 패널에서 실행 중인 플러그인
임포트한 플러그인은 전용 플로팅 패널에서 실행됩니다 — 제목은 매니페스트에서, 화면은 ui.html에서 옵니다.

매니페스트 레퍼런스

필수 5개, 선택 3개. 모르는 필드는 무시되므로 최신 매니페스트도 구버전 에디터에서 로드됩니다.

필드 설명
id 고유 id — 소문자 역도메인 형식 (필수)
name 목록과 패널 제목에 보이는 이름 (필수)
version x.y.z 버전 (필수)
api 플러그인 API 버전 — 현재 1 (필수)
main 화면 문서 파일명, 예: ui.html (필수)
description 목록에 보이는 한 줄 설명 (선택)
author 목록에 보이는 제작자 이름 (선택)
icon 패키지 안의 아이콘 파일명 — png/svg/jpg/webp (선택)

제약 — id: 소문자 역도메인([a-z0-9.-], 최대 100자, 최초 제출자가 영구 소유) · version: 정확한 x.y.z · api: 현재 1 · name ≤ 100자 · description ≤ 2000자 · author ≤ 100자 · icon: 패키지 안의 png / svg / jpg / webp. 카탈로그의 제작자 표시는 항상 검증된 제출 계정을 따릅니다.

연결

화면 문서 안에서 SSHOWPlugin.connect()를 호출하면 API 핸들이 반환됩니다. 핸들에는 apiVersion(현재 1), engineVersion, 그리고 아래의 document·events·ui 네임스페이스가 들어 있습니다.

const api = await SSHOWPlugin.connect();

api.apiVersion;      // 1 — the bridge contract this editor speaks
api.engineVersion;   // engine build, for display only

api.document;        // getState() · getObject(id) · getSelection() · setSelection(ids)
                     // setActiveScene(sceneId) · getTimelineTime() · applyActions(actions, label)
api.assets;          // get(uri) · register(bytes, { mimeType, originalName })
api.events;          // on(type, callback) · off(type, callback)
api.ui;              // resize(size) · getTheme()

읽기

document.getState()
문서 전체의 스냅샷 — 캔버스 크기, 활성 씬 id, 씬 목록(활성 씬은 오브젝트 전체, 나머지는 요약), 폰트 목록.
document.getObject(id)
오브젝트 하나의 스냅샷, 없으면 null.
document.getSelection()
현재 선택된 오브젝트들의 스냅샷 배열.

전형적인 읽기 흐름:

const { canvas, activeSceneId, scenes } = await api.document.getState();
const selection = await api.document.getSelection();
// selection[0] → { id, type, name, transform, size, style, data, … }

반환값은 모두 스냅샷(사본)입니다. 값을 고쳐도 문서는 바뀌지 않습니다 — 변경은 반드시 액션으로 보냅니다.

아주 큰 필드는 스냅샷에서 생략됩니다 — 긴 data.src는 실제 값 대신 <src len=…> 마커로 옵니다. 마커를 set에 다시 넣지 마세요. 실제 바이트는 assets.get으로 읽습니다.

에디터 상태

읽기를 보완하는 UI 상태 세터 두 개와 에디터 상태 읽기 하나입니다. 모두 문서나 undo 히스토리를 건드리지 않습니다.

document.setSelection(ids)
활성 씬의 오브젝트 id들을 에디터에서 선택합니다 — 방금 만든 오브젝트를 선택된 상태로 사용자에게 넘기세요. 없는 id는 조용히 제외됩니다.
document.setActiveScene(sceneId)
활성 씬을 전환합니다. 모르는 id는 거부되므로 잘못된 씬에 계속 쓰는 일이 없습니다.
document.getTimelineTime()
에디터의 애니메이션 시계(ms) — Animation 모드에서는 플레이헤드, Design 모드에서는 0(문서 포즈)입니다. 타임라인 작업(베이크·프리셋)의 시작점으로 쓰고, 쓰기 직전에 다시 읽으세요.

쓰기 — applyActions

document.applyActions(actions, label)이 문서를 편집하는 유일한 방법입니다. 액션 배열이 하나의 편집으로 묶여 실행취소 한 번에 전부 되돌아가고, 결과로 { applied, skipped }가 반환됩니다. 깨진 액션은 전체를 막지 않고 skipped로 보고됩니다.

각 액션은 op와 대상, 바꿀 내용으로 구성됩니다. 오브젝트는 id, 씬은 sceneId로 지정하고(생략하면 활성 씬), update_* 액션은 set 객체에 바꿀 필드만 담습니다.

19개 op 전부 같은 actions 배열에 담겨 순서대로 커밋됩니다. 괄호 안 필드는 선택이며, sceneId를 생략하면 어디서든 활성 씬을 대상으로 합니다.

오브젝트

Op 필드 (+ 선택) 비고
create_object type, config (+ sceneId, options) type은 rect · circle · path · text · image · video · audio · group · frame 중 하나입니다. config.id에 직접 id를 주면 같은 배치의 뒤 액션에서 그 오브젝트를 겨냥할 수 있고, options.parentObjectId는 그룹/프레임 안에 생성, options.index는 목록 위치를 지정합니다.
update_object id, set (+ sceneId) set 안에 담은 키만 바뀝니다 — 아래 유효한 set 키를 참조하세요.
delete_object id (+ sceneId) 유효하지 않은 id는 배치를 실패시키지 않고 skipped로 빠집니다.
duplicate_object id (+ sceneId, options)
move_object id (+ sceneId, options) options.parentObjectId는 컨테이너로 재부모화, options.index는 그 안에서 순서를 바꿉니다.
group_objects ids (+ config, sceneId) id 2개 이상이 필요하며, 같은 배치에서 앞서 만든 config.id 값도 포함할 수 있습니다.
ungroup id (+ sceneId)
convert_to_path id (+ sceneId)

Op 필드 (+ 선택) 비고
create_scene config (+ options) 여기서도 config.id 직접 지정이 동작합니다.
update_scene set (+ sceneId)
delete_scene (+ sceneId)
duplicate_scene (+ sceneId)
move_scene sceneId, newIndex
set_scene_size size: { width, height } 캔버스 크기는 문서 전역입니다 — 모든 씬에 적용됩니다.

변수

Op 필드 (+ 선택) 비고
create_variable config (+ options)
update_variable variableId, set
delete_variable variableId
move_variable variableId, newIndex

문서

Op 필드 (+ 선택) 비고
set_document set 문서 메타데이터 — 아래 유효한 set 키를 참조하세요.

유효한 set 키

set 안에 모르는 키가 있으면 그 액션 전체가 건너뛰어집니다(skipped에 이유와 함께 담김):

update_object.set — name · description · size · transform · distort · layout · style ·
                    opacity · blendMode · locked · visible · motion · interaction · data
update_scene.set  — name · description · notes · style · visible · motion · interaction · data · clip
set_document.set  — name · description · notes

호출 하나가 undo 한 스텝 — 타깃은 스냅샷에서 얻은 오브젝트 id입니다:

const { applied, skipped } = await api.document.applyActions([
    {
        op: 'create_object', type: 'rect', config: {
            name: 'bar',
            size: { width: 120, height: 240 },
            transform: { x: 400, y: 300, anchorX: 0.5, anchorY: 0 },
            style: { fills: [{ type: 'solid', color: '#8A8A8E' }], strokes: [], effects: [] }
        }
    },
    { op: 'update_object', id: selection[0].id, set: { transform: { rotateZ: 15 } } },
    { op: 'delete_object', id: obsoleteId }
], 'My plugin edit');
// applied — actions committed as ONE undo step · skipped — malformed entries with reasons

set 병합 규칙

  • transform · size · data · layout — 보낸 키만 바뀌고 나머지는 유지됩니다.
  • style · distort — 통째로 교체됩니다. style은 항상 fills·strokes·effects 전체를 보내세요.
  • motion — 서브컨테이너 단위로 병합됩니다. animations만 보내면 transitions는 유지됩니다(반대도 동일). 키프레임 하나를 고칠 때는 스냅샷에서 서브컨테이너 전체를 읽어 수정한 뒤 통째로 되돌려 보내세요.

정규화 브리지

  • transform.rotateX / rotateY / rotateZ(도 단위, 레거시 transform.rotate는 rotateZ)는 라디안으로 자동 변환됩니다. 모션 트랙의 transform.rotate* 값은 라디안(엔진 단위)입니다.
  • color만 있고 type이 없는 style 페인트는 'solid'로 기본 처리되고, 잘못된 effects 항목은 버려집니다.
  • data.text 안의 리터럴 \n 과 \t 는 실제 줄바꿈과 탭이 됩니다.

에셋

assets.get(uri)
asset:// 주소의 원본 바이트와 MIME 타입·파일명을 반환합니다. 없으면 null. 선택한 이미지의 data.src로 원본을 읽어 캔버스에서 가공할 수 있습니다.
assets.register(bytes, { mimeType, originalName })
바이트를 프로젝트 에셋으로 등록하고 asset:// 주소를 반환합니다. 같은 내용은 같은 주소로 중복 제거되며, 개당 10MB까지 허용됩니다(기본 요금제의 파일 업로드 한도와 동일).

선택한 이미지의 픽셀 편집 풀사이클:

const [image] = await api.document.getSelection();          // an image object
const { bytes, mimeType } = await api.assets.get(image.data.src);

const edited = await process(bytes);                        // your pixel work
const uri = await api.assets.register(edited, { mimeType, originalName: 'edited.png' });

await api.document.applyActions([
    { op: 'update_object', id: image.id, set: { data: { src: uri } } }
], 'Edit image');

등록한 주소는 곧바로 액션(예: 이미지의 data.src)으로 참조하세요. 아무 곳에서도 참조되지 않는 에셋은 정리 대상이 됩니다.

이벤트

events.on(type, callback) / events.off(type, callback)로 구독합니다. 콜백에는 인자가 없습니다 — 다시 조회하라는 신호이므로 읽기 API로 재조회합니다. 이벤트 타입은 아래 세 가지가 전부이며, 그 밖의 구독은 거부됩니다. 플러그인이 닫히면 구독은 자동으로 해제됩니다.

  • history:update — 문서가 바뀔 때(편집·실행취소·다시실행).
  • ui:modes:edit:changeSelectedObjects — 선택이 바뀔 때.
  • motion:animation:timeUpdate — 애니메이션 시계가 움직일 때(시크, 재생 중엔 매 프레임 — 디바운스 권장).
await api.events.on('ui:modes:edit:changeSelectedObjects', async () => {
    const selection = await api.document.getSelection();    // re-query — events carry no payload
    render(selection);
});

테마

모든 플러그인 문서에 에디터 디자인 토큰이 CSS 커스텀 프로퍼티로 주입되며, 에디터와 같은 라이트/다크 미디어쿼리에 연결됩니다 — var(--sshow-…)로 스타일하면 테마 전환 시 패널이 코드 없이 자동으로 따라갑니다.

--sshow-primary               /* accent (#2196F3) */
--sshow-primary-strong        /* filled active/selected surface */
--sshow-primary-soft          /* its hover */
--sshow-primary-foreground    /* text over the accent */
--sshow-secondary
--sshow-foreground            /* body text — follows light/dark */
--sshow-background            /* panel surface (translucent) */
--sshow-background-solid
--sshow-border-color
--sshow-radius                /* 13px */
--sshow-font-size             /* 12px — matches native panel text */
--sshow-scrollbar-size        /* 6px */
--sshow-scrollbar-radius      /* 3px */

문서의 스크롤바도 에디터와 같은 것을 씁니다 — 운영체제 기본 스크롤바(윈도우는 15px) 대신 패널이 쓰는 6px 슬림 바입니다. ::-webkit-scrollbar 규칙을 직접 선언하면 덮어쓸 수 있습니다.

스크립트 로직(예: 캔버스 드로잉)에서는 활성 모드와 해석된 색을 읽으세요:

const { mode, colors } = await api.ui.getTheme();
// mode   → 'light' | 'dark'
// colors → { primary, primaryForeground, secondary,
//            foreground, background, backgroundSolid, borderColor }

// React to a theme flip in JS (styles via var() follow automatically):
matchMedia('(prefers-color-scheme: dark)')
    .addEventListener('change', () => render());

패널 UI

ui.resize(size) — 화면 크기를 요청합니다. 숫자는 높이 요청이고, { width, height }로 두 축을 지정할 수 있습니다. 패널이 요청한 화면 크기에 맞춰 리사이즈되며(패널 자체 한계로 제한), 요청하지 않으면 기본 패널 크기를 가득 채웁니다.

키보드 — 화면이 포커스를 쥔 동안에도 에디터 단축키는 그대로 동작합니다. 플러그인이 처리하지 않은 키는 에디터로 전달됩니다(Tab은 디자인/애니메이션 전환, Ctrl+Z는 실행 취소, Space는 팬). 키를 직접 쓰려면 window에 닿기 전에 preventDefault() 또는 stopPropagation()을 호출하세요. 텍스트 입력란에 포커스가 있는 동안에는 아무것도 전달되지 않습니다.

제약과 버전

  • 화면 문서는 자기완결이어야 합니다 — 네트워크가 차단되므로 스크립트·스타일은 인라인으로, 이미지는 data:/blob:만 동작합니다. WebAssembly는 컴파일되지만 eval·new Function은 막힙니다.
  • 패키지는 zip 엔트리 64개, 파일당 압축 해제 5MB, 패키지당 10MB까지 허용됩니다. 등록하는 에셋도 개당 10MB가 상한입니다.
  • plugin.json의 api가 에디터의 API 버전(1)과 다르면 로드가 거부됩니다. API 확장은 기존 플러그인이 깨지지 않는 방향으로만 이뤄집니다.
  • 실패한 호출은 이유 문자열과 함께 reject됩니다.

개발 루프

게시 전 반복 작업 두 가지 방법:

재임포트 (웹 + 데스크톱)
에디터 플러그인 패널의 + 버튼이 .sshowplugin 을 임포트합니다. 같은 id는 중복으로 거부되므로, 행의 − 버튼으로 제거한 뒤 다시 임포트하세요.
devPath 핫 리로드 (데스크톱)
plugins.devPath 설정을 플러그인 폴더(plugin.json + main + 아이콘)로 지정하세요. 저장할 때마다 열린 에디터에 재등록되고 실행 중이던 플러그인은 다시 열립니다 — 반복 중엔 zip이 필요 없습니다.

카탈로그에 게시하기

누구나 /developers 개발자 콘솔에서 플러그인을 제출할 수 있습니다. 모든 버전은 게시 전 사람이 검수하며, 결과는 인앱 알림과 이메일로 전달됩니다.

  1. 1 plugin.json, 메인 문서, 아이콘을 .sshowplugin 패키지(zip)로 묶으세요.
  2. 2 /developers 에서 업로드하세요 — 서버가 패키지를 검증하고 manifest 를 추출합니다. 다시 입력할 것은 없습니다.
  3. 3 콘솔에서 검수 상태를 확인하세요. 승인된 버전은 즉시 카탈로그에 게시됩니다.
  • 버전은 불변이며 항상 증가해야 합니다(x.y.z) — 거절을 고치려면 더 높은 버전을 제출하세요.
  • manifest id 는 최초 제출자가 영구 소유합니다. 직접 관리하는 역도메인 id 를 사용하세요.
  • 검수된 zip 이 사용자가 받는 파일 그대로입니다 — 서버는 패키지를 절대 재작성하지 않습니다.