@kokoa/clotho-editor 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -56
- package/dist/{chunk-Z2K5HFHI.js → chunk-FAI2G3XK.js} +284 -21
- package/dist/chunk-FAI2G3XK.js.map +1 -0
- package/dist/clotho-editor.css +134 -0
- package/dist/index.d.ts +151 -18
- package/dist/index.js +515 -9
- package/dist/index.js.map +1 -1
- package/dist/{main-5YXLTYPQ.js → main-SKM3P4X7.js} +427 -22
- package/dist/main-SKM3P4X7.js.map +1 -0
- package/package.json +7 -3
- package/dist/chunk-Z2K5HFHI.js.map +0 -1
- package/dist/main-5YXLTYPQ.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,113 +1,158 @@
|
|
|
1
1
|
# @kokoa/clotho-editor
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+
`@kokoa/clotho-editor`는 [Clotho](https://github.com/shinkeonkim/clotho) 애니메이션 문서를 만드는 React 기반 시각 편집기입니다. 저장소, 이미지 업로드, 예제 목록과 저장 동작을 host application에서 주입할 수 있으므로 독립형 편집기와 기존 관리 화면에서 같은 package를 사용할 수 있습니다.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
실제 화면은 [Clotho Editor](https://clotho-editor.shinkeonkim.com/)에서 확인할 수 있습니다.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
가장 큰 변화는 **미리보기에도 clotho를 사용한다**는 점이다. 이전 Studio는 자체 canvas renderer를 사용했기 때문에 편집 화면과 실제 사이트의 화면이 다를 수 있었다. 이제 두 화면 모두 `buildScene`과 `patchScene`을 거치므로 같은 문서는 같은 결과로 표시된다.
|
|
10
|
-
|
|
11
|
-
자세한 내용과 남은 정리 항목은 [`docs/PORTING.md`](./docs/PORTING.md).
|
|
12
|
-
|
|
13
|
-
## 사용
|
|
7
|
+
## 설치
|
|
14
8
|
|
|
15
9
|
```bash
|
|
16
10
|
npm install @kokoa/clotho @kokoa/clotho-editor react react-dom
|
|
17
|
-
# yarn add / pnpm add / bun add 도 같은 패키지 목록을 사용한다.
|
|
18
11
|
```
|
|
19
12
|
|
|
20
13
|
```tsx
|
|
21
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
StudioMount,
|
|
16
|
+
createLocalStorageRepository,
|
|
17
|
+
} from "@kokoa/clotho-editor";
|
|
18
|
+
import "@kokoa/clotho/styles.css";
|
|
22
19
|
import "@kokoa/clotho-editor/styles.css";
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
const repository = createLocalStorageRepository({
|
|
22
|
+
storageKey: "my-app.animations",
|
|
23
|
+
examples,
|
|
24
|
+
});
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
export function AnimationEditor() {
|
|
27
|
+
return <StudioMount editorTitle="Clotho Editor" repository={repository} />;
|
|
28
|
+
}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
`
|
|
31
|
+
`StudioMount`는 목록 조회, 새 문서 생성, 저장, 삭제와 JSON 내보내기를 포함한 편집기 화면입니다. 편집기 shell을 application이 직접 구성해야 한다면 `Studio`를 사용할 수 있습니다.
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
## 저장소 연결
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
const repository = createLocalStorageRepository({
|
|
37
|
-
storageKey: "my-editor.animations",
|
|
38
|
-
examples,
|
|
39
|
-
});
|
|
35
|
+
편집기는 특정 backend에 의존하지 않습니다. `AnimationRepository`를 구현하면 REST API, browser storage, Git 기반 저장소 등 원하는 데이터 계층을 연결할 수 있습니다.
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
```ts
|
|
38
|
+
import type { AnimationRepository } from "@kokoa/clotho-editor";
|
|
39
|
+
|
|
40
|
+
const repository: AnimationRepository = {
|
|
41
|
+
list: async () => api.list(),
|
|
42
|
+
load: async (id) => api.load(id),
|
|
43
|
+
create: async (id, title) => api.create(id, title),
|
|
44
|
+
save: async (document) => api.save(document),
|
|
45
|
+
delete: async (id) => api.delete(id),
|
|
46
|
+
};
|
|
42
47
|
```
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
간단한 demo나 offline 편집기는 `createLocalStorageRepository`를 사용하면 됩니다. 기존 HTTP API를 사용하는 application은 `configureApi({ baseUrl })`로 기본 adapter를 설정할 수도 있습니다.
|
|
50
|
+
|
|
51
|
+
## Host hooks
|
|
52
|
+
|
|
53
|
+
상단 제목과 이미지 처리 방식은 실행 환경에서 정할 수 있습니다. `resolveImage`를 지정하지 않으면 이미지를 data URL로 변환해 문서의 `assets`에 저장합니다. 같은 이미지는 하나의 asset을 공유하며, 사용되지 않는 asset은 JSON을 내보낼 때 제거됩니다.
|
|
45
54
|
|
|
46
55
|
```tsx
|
|
47
56
|
<StudioMount
|
|
48
57
|
editorTitle="블로그 애니메이션 편집기"
|
|
58
|
+
repository={repository}
|
|
49
59
|
resolveImage={async (file) => uploadToMediaServer(file)}
|
|
50
60
|
/>
|
|
51
61
|
```
|
|
52
62
|
|
|
53
|
-
|
|
63
|
+
저장 버튼의 동작 자체를 바꾸려면 repository의 `create`와 `save`를 구현합니다. 예제 목록은 repository의 `list`와 `load`에서 제공하므로 editor package 안에 application별 API 경로를 넣을 필요가 없습니다.
|
|
54
64
|
|
|
55
|
-
|
|
65
|
+
## Plugin host
|
|
56
66
|
|
|
57
|
-
|
|
58
|
-
import "@kokoa/clotho-editor/styles.css";
|
|
59
|
-
import "@kokoa/clotho/styles.css";
|
|
60
|
-
```
|
|
67
|
+
application 전용 도구는 `plugins`로 추가할 수 있습니다. plugin은 toolbar, 왼쪽 panel, inspector와 command palette에 기능을 붙일 수 있지만, 문서를 읽거나 바꾸려면 host가 권한을 명시적으로 허용해야 합니다. 저장소, selection, undo/redo와 같은 편집기의 기본 기능은 plugin으로 분리하지 않습니다.
|
|
61
68
|
|
|
62
|
-
|
|
69
|
+
```tsx
|
|
70
|
+
import { StudioMount, type EditorPluginDefinition } from "@kokoa/clotho-editor";
|
|
71
|
+
|
|
72
|
+
const reviewPlugin: EditorPluginDefinition = {
|
|
73
|
+
manifest: {
|
|
74
|
+
id: "com.example.review",
|
|
75
|
+
version: "1.0.0",
|
|
76
|
+
capabilities: ["editor"],
|
|
77
|
+
editor: { toolbarItems: ["review"] },
|
|
78
|
+
},
|
|
79
|
+
toolbarItems: {
|
|
80
|
+
review: ({ container, document }) => {
|
|
81
|
+
const button = document.createElement("button");
|
|
82
|
+
button.textContent = "검토 요청";
|
|
83
|
+
container.append(button);
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
63
87
|
|
|
64
|
-
|
|
88
|
+
<StudioMount
|
|
89
|
+
plugins={[reviewPlugin]}
|
|
90
|
+
resolvePluginPermissions={(manifest) =>
|
|
91
|
+
manifest.id === "com.example.review" ? { ui: true, documentRead: true } : {}
|
|
92
|
+
}
|
|
93
|
+
/>;
|
|
94
|
+
```
|
|
65
95
|
|
|
66
|
-
|
|
96
|
+
Clotho compiler plugin이 새로운 JSON 입력 형식을 처리해야 한다면 `importDocument`에서 compiler pipeline을 연결합니다. 이 경계 덕분에 Editor는 application의 plugin registry나 backend에 의존하지 않습니다.
|
|
67
97
|
|
|
68
|
-
|
|
98
|
+
```tsx
|
|
99
|
+
import { createPluginRegistry, runPluginPipeline } from "@kokoa/clotho/plugins";
|
|
69
100
|
|
|
70
|
-
|
|
101
|
+
const registry = createPluginRegistry(compilerPlugins);
|
|
71
102
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
103
|
+
<StudioMount
|
|
104
|
+
importDocument={(input) => {
|
|
105
|
+
const result = runPluginPipeline(input, { registry });
|
|
106
|
+
if (!result.ok) throw result.error;
|
|
107
|
+
return result.document;
|
|
108
|
+
}}
|
|
109
|
+
/>;
|
|
75
110
|
```
|
|
76
111
|
|
|
77
|
-
|
|
112
|
+
현재 plugin API는 신뢰할 수 있는 application code를 위한 실험적 API입니다. 외부에서 받은 plugin은 별도 Worker나 격리 환경에서 실행한 뒤 JSON 결과만 Editor로 전달해야 합니다.
|
|
113
|
+
|
|
114
|
+
## 주요 기능
|
|
115
|
+
|
|
116
|
+
- Clotho v1 JSON 문서 작성과 검증
|
|
117
|
+
- 사각형, 원, 선, 화살표, 텍스트, 이미지, Path, 다각형과 그룹 편집
|
|
118
|
+
- anchor를 유지하는 선 연결과 다중 선택
|
|
119
|
+
- keyframe, effect, chapter와 재생 설정 편집
|
|
120
|
+
- light/dark theme와 단계 목록 위치 설정
|
|
121
|
+
- 별도 브라우저 창으로 분리할 수 있는 timeline
|
|
122
|
+
- 실제 `@kokoa/clotho/dom` player를 사용하는 독립 미리보기
|
|
123
|
+
- JSON 가져오기와 내보내기, 참조되지 않는 image asset 정리
|
|
78
124
|
|
|
79
|
-
|
|
125
|
+
도구 단축키는 편집기 안에서 확인할 수 있으며 input, textarea와 contenteditable에 입력하는 동안에는 동작하지 않습니다.
|
|
126
|
+
|
|
127
|
+
## 독립 실행형 앱
|
|
128
|
+
|
|
129
|
+
저장소의 `app`은 package 통합 예제이자 Cloudflare Workers Static Assets용 demo입니다.
|
|
80
130
|
|
|
81
131
|
```bash
|
|
82
|
-
bun
|
|
132
|
+
bun install
|
|
133
|
+
bun run app:dev
|
|
134
|
+
bun run build:app
|
|
135
|
+
bun run dev:cloudflare
|
|
83
136
|
```
|
|
84
137
|
|
|
85
|
-
|
|
138
|
+
Cloudflare Dashboard에서는 Build command를 `bun run build:app`, Deploy command를 `npx wrangler deploy`로 지정합니다. custom domain은 Dashboard에서 별도로 연결합니다.
|
|
86
139
|
|
|
87
140
|
## 개발
|
|
88
141
|
|
|
89
142
|
```bash
|
|
90
|
-
bun install
|
|
143
|
+
bun install --frozen-lockfile
|
|
91
144
|
bun run typecheck
|
|
92
145
|
bun test
|
|
93
146
|
bun run build
|
|
94
147
|
```
|
|
95
148
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
## Cloudflare Workers 배포
|
|
99
|
-
|
|
100
|
-
독립 에디터는 `app`에서 시작하며 Cloudflare Workers Static Assets용 설정은 [`wrangler.jsonc`](./wrangler.jsonc)에 들어 있다. 로컬에서 실제 화면을 확인하거나 배포 파일을 만들려면 다음 명령을 사용한다.
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
bun run app:dev
|
|
104
|
-
bun run build:app
|
|
105
|
-
bun run dev:cloudflare
|
|
106
|
-
```
|
|
149
|
+
npm release는 GitHub Release와 [publish workflow](./.github/workflows/publish.yml)를 통해 진행합니다. `@kokoa/clotho`의 호환 버전이 먼저 공개되어 있어야 합니다.
|
|
107
150
|
|
|
108
|
-
|
|
151
|
+
## 문서
|
|
109
152
|
|
|
110
|
-
|
|
153
|
+
- [Clotho 저장소](https://github.com/shinkeonkim/clotho)
|
|
154
|
+
- [Clotho Editor demo](https://clotho-editor.shinkeonkim.com/)
|
|
155
|
+
- [Porting 기록](./docs/PORTING.md)
|
|
111
156
|
|
|
112
157
|
## 라이선스
|
|
113
158
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { animationDocumentSchema, computeSnapshot, encodeImageAsset, inlineAssetFromDataUri } from '@kokoa/clotho';
|
|
1
|
+
import { animationDocumentSchema, computeSnapshot, compileDataBindings, encodeImageAsset, inlineAssetFromDataUri, compileLayouts } from '@kokoa/clotho';
|
|
2
2
|
|
|
3
3
|
// src/export-json.ts
|
|
4
4
|
function animationDocumentToJson(def) {
|
|
@@ -201,7 +201,19 @@ function mutateDef(fn, label = "edit", kind = "other") {
|
|
|
201
201
|
console.warn("[studio.state] invalid mutation", parsed.error.issues);
|
|
202
202
|
return;
|
|
203
203
|
}
|
|
204
|
-
|
|
204
|
+
const localized = cloned;
|
|
205
|
+
const next = parsed.data;
|
|
206
|
+
if (localized.locales) next.locales = localized.locales;
|
|
207
|
+
next.elements = next.elements.map((element, index) => {
|
|
208
|
+
const source = localized.elements[index];
|
|
209
|
+
if (source?.id !== element.id || element.type !== "text") return element;
|
|
210
|
+
return {
|
|
211
|
+
...element,
|
|
212
|
+
...source.locales ? { locales: source.locales } : {},
|
|
213
|
+
...source.translations ? { translations: source.translations } : {}
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
state.def = next;
|
|
205
217
|
state.dirty = true;
|
|
206
218
|
emit();
|
|
207
219
|
}
|
|
@@ -280,7 +292,10 @@ function toggleSelectionFor(sel, id) {
|
|
|
280
292
|
}
|
|
281
293
|
function getCurrentSnapshot() {
|
|
282
294
|
if (!state.def) return /* @__PURE__ */ new Map();
|
|
283
|
-
return computeSnapshot(
|
|
295
|
+
return computeSnapshot(
|
|
296
|
+
compileDataBindings(state.def).document,
|
|
297
|
+
state.currentTime
|
|
298
|
+
);
|
|
284
299
|
}
|
|
285
300
|
|
|
286
301
|
// src/legacy/state/history.ts
|
|
@@ -705,6 +720,33 @@ function updateMeta(patch) {
|
|
|
705
720
|
"meta"
|
|
706
721
|
);
|
|
707
722
|
}
|
|
723
|
+
function updateLocales(locales) {
|
|
724
|
+
mutateDef(
|
|
725
|
+
(def) => {
|
|
726
|
+
def.locales = locales;
|
|
727
|
+
},
|
|
728
|
+
"\uBB38\uC11C \uC5B8\uC5B4 \uBCC0\uACBD",
|
|
729
|
+
"meta"
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
function updateData(data) {
|
|
733
|
+
mutateDef(
|
|
734
|
+
(def) => {
|
|
735
|
+
def.data = data;
|
|
736
|
+
},
|
|
737
|
+
"\uC0D8\uD50C \uB370\uC774\uD130 \uBCC0\uACBD",
|
|
738
|
+
"meta"
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
function updateResponsive(responsive) {
|
|
742
|
+
mutateDef(
|
|
743
|
+
(def) => {
|
|
744
|
+
def.responsive = responsive;
|
|
745
|
+
},
|
|
746
|
+
"Responsive Stage \uBCC0\uACBD",
|
|
747
|
+
"canvas"
|
|
748
|
+
);
|
|
749
|
+
}
|
|
708
750
|
function updateCanvas(patch) {
|
|
709
751
|
const keys = Object.keys(patch).join(", ");
|
|
710
752
|
mutateDef(
|
|
@@ -809,6 +851,125 @@ function registerDataUriAsset(dataUri) {
|
|
|
809
851
|
);
|
|
810
852
|
return id;
|
|
811
853
|
}
|
|
854
|
+
function uniqueLayoutId(def) {
|
|
855
|
+
const used = new Set(def.layouts.map((layout) => layout.id));
|
|
856
|
+
let index = 1;
|
|
857
|
+
while (used.has(`layout-${index}`)) index += 1;
|
|
858
|
+
return `layout-${index}`;
|
|
859
|
+
}
|
|
860
|
+
function createLayout(elementIds, mode) {
|
|
861
|
+
if (elementIds.length === 0) return;
|
|
862
|
+
mutateDef(
|
|
863
|
+
(def) => {
|
|
864
|
+
const selected = def.elements.filter(
|
|
865
|
+
(element) => elementIds.includes(element.id)
|
|
866
|
+
);
|
|
867
|
+
if (selected.length === 0) return;
|
|
868
|
+
const measured = compileLayouts({ ...def, layouts: [] }).boxes;
|
|
869
|
+
const boxes = selected.flatMap((element) => {
|
|
870
|
+
const box = measured[element.id];
|
|
871
|
+
return box ? [box] : [];
|
|
872
|
+
});
|
|
873
|
+
const x = boxes.length > 0 ? Math.min(...boxes.map((box) => box.x)) : 0;
|
|
874
|
+
const y = boxes.length > 0 ? Math.min(...boxes.map((box) => box.y)) : 0;
|
|
875
|
+
def.layouts = def.layouts.filter(
|
|
876
|
+
(layout) => !layout.elementIds.some((id) => elementIds.includes(id))
|
|
877
|
+
);
|
|
878
|
+
def.layouts.push({
|
|
879
|
+
id: uniqueLayoutId(def),
|
|
880
|
+
mode,
|
|
881
|
+
elementIds: selected.map((element) => element.id),
|
|
882
|
+
x,
|
|
883
|
+
y,
|
|
884
|
+
gap: 16,
|
|
885
|
+
align: "start",
|
|
886
|
+
constraints: []
|
|
887
|
+
});
|
|
888
|
+
def.elements = compileLayouts(def).document.elements;
|
|
889
|
+
},
|
|
890
|
+
`${mode} layout \uC0DD\uC131`,
|
|
891
|
+
"layout"
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
function detachFromLayout(elementIds) {
|
|
895
|
+
const ids = new Set(elementIds);
|
|
896
|
+
mutateDef(
|
|
897
|
+
(def) => {
|
|
898
|
+
def.layouts = def.layouts.flatMap((layout) => {
|
|
899
|
+
const remaining = layout.elementIds.filter((id) => !ids.has(id));
|
|
900
|
+
return remaining.length === 0 ? [] : [{ ...layout, elementIds: remaining }];
|
|
901
|
+
});
|
|
902
|
+
},
|
|
903
|
+
"layout\uC5D0\uC11C \uBD84\uB9AC",
|
|
904
|
+
"layout"
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
function layoutIdsFor(elementIds) {
|
|
908
|
+
if (!state.def) return [];
|
|
909
|
+
const ids = new Set(elementIds);
|
|
910
|
+
return state.def.layouts.filter((layout) => layout.elementIds.some((id) => ids.has(id))).map((layout) => layout.id);
|
|
911
|
+
}
|
|
912
|
+
function findLayoutCollisions(def) {
|
|
913
|
+
const { boxes } = compileLayouts(def);
|
|
914
|
+
const collisions = [];
|
|
915
|
+
for (const layout of def.layouts) {
|
|
916
|
+
for (let firstIndex = 0; firstIndex < layout.elementIds.length; firstIndex += 1) {
|
|
917
|
+
for (let secondIndex = firstIndex + 1; secondIndex < layout.elementIds.length; secondIndex += 1) {
|
|
918
|
+
const firstId = layout.elementIds[firstIndex];
|
|
919
|
+
const secondId = layout.elementIds[secondIndex];
|
|
920
|
+
const first = boxes[firstId];
|
|
921
|
+
const second = boxes[secondId];
|
|
922
|
+
if (!first || !second) continue;
|
|
923
|
+
const overlaps = first.x < second.x + second.width && first.x + first.width > second.x && first.y < second.y + second.height && first.y + first.height > second.y;
|
|
924
|
+
if (overlaps) collisions.push({ firstId, secondId });
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return collisions;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/legacy/state/checkpoints.ts
|
|
932
|
+
function addCheckpoint(checkpoint) {
|
|
933
|
+
mutateDef(
|
|
934
|
+
(def) => {
|
|
935
|
+
def.checkpoints.push(checkpoint);
|
|
936
|
+
def.checkpoints.sort((a, b) => a.time - b.time);
|
|
937
|
+
},
|
|
938
|
+
`Checkpoint \uCD94\uAC00: ${checkpoint.id}`,
|
|
939
|
+
"checkpoint"
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
function updateCheckpoint(id, patch) {
|
|
943
|
+
mutateDef(
|
|
944
|
+
(def) => {
|
|
945
|
+
const index = def.checkpoints.findIndex(
|
|
946
|
+
(checkpoint) => checkpoint.id === id
|
|
947
|
+
);
|
|
948
|
+
if (index < 0) return;
|
|
949
|
+
def.checkpoints[index] = {
|
|
950
|
+
...def.checkpoints[index],
|
|
951
|
+
...patch
|
|
952
|
+
};
|
|
953
|
+
def.checkpoints.sort((a, b) => a.time - b.time);
|
|
954
|
+
},
|
|
955
|
+
`Checkpoint \uC218\uC815: ${id}`,
|
|
956
|
+
"checkpoint"
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
function deleteCheckpoint(id) {
|
|
960
|
+
mutateDef(
|
|
961
|
+
(def) => {
|
|
962
|
+
def.checkpoints = def.checkpoints.filter(
|
|
963
|
+
(checkpoint) => checkpoint.id !== id
|
|
964
|
+
);
|
|
965
|
+
},
|
|
966
|
+
`Checkpoint \uC0AD\uC81C: ${id}`,
|
|
967
|
+
"checkpoint"
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
function uniqueCheckpointId() {
|
|
971
|
+
return `checkpoint-${Date.now().toString(36)}`;
|
|
972
|
+
}
|
|
812
973
|
|
|
813
974
|
// src/legacy/studio-groups.ts
|
|
814
975
|
function isGroup(el) {
|
|
@@ -953,21 +1114,6 @@ function groupElements(ids) {
|
|
|
953
1114
|
const valid = ids.filter((id) => def.elements.some((e) => e.id === id));
|
|
954
1115
|
if (valid.length < 2) return null;
|
|
955
1116
|
const newId = uniqueElementId("group");
|
|
956
|
-
(() => {
|
|
957
|
-
let minX = Infinity, minY = Infinity;
|
|
958
|
-
for (const id of valid) {
|
|
959
|
-
const el = def.elements.find((e) => e.id === id);
|
|
960
|
-
if (!el) continue;
|
|
961
|
-
const b = elementBbox(el);
|
|
962
|
-
if (!b) continue;
|
|
963
|
-
if (b.x < minX) minX = b.x;
|
|
964
|
-
if (b.y < minY) minY = b.y;
|
|
965
|
-
}
|
|
966
|
-
return {
|
|
967
|
-
x: Number.isFinite(minX) ? minX : 0,
|
|
968
|
-
y: Number.isFinite(minY) ? minY : 0
|
|
969
|
-
};
|
|
970
|
-
})();
|
|
971
1117
|
const group = {
|
|
972
1118
|
type: "group",
|
|
973
1119
|
id: newId,
|
|
@@ -975,6 +1121,7 @@ function groupElements(ids) {
|
|
|
975
1121
|
rotation: 0,
|
|
976
1122
|
appearances: [],
|
|
977
1123
|
tracks: [],
|
|
1124
|
+
bindings: [],
|
|
978
1125
|
// Children keep their absolute coordinates, so the group's own transform starts at
|
|
979
1126
|
// the identity. Setting x/y here would shift every member on the next render.
|
|
980
1127
|
x: 0,
|
|
@@ -1012,6 +1159,122 @@ function placeholderImageUrl() {
|
|
|
1012
1159
|
return placeholderUrl;
|
|
1013
1160
|
}
|
|
1014
1161
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1162
|
+
// src/plugin-host.ts
|
|
1163
|
+
var SLOT_KEYS = {
|
|
1164
|
+
toolbar: "toolbarItems",
|
|
1165
|
+
panel: "panels",
|
|
1166
|
+
inspector: "inspectors"
|
|
1167
|
+
};
|
|
1168
|
+
function viewsFor(plugin, slot) {
|
|
1169
|
+
if (slot === "toolbar") return plugin.toolbarItems ?? {};
|
|
1170
|
+
if (slot === "panel") return plugin.panels ?? {};
|
|
1171
|
+
return plugin.inspectors ?? {};
|
|
1172
|
+
}
|
|
1173
|
+
function validateEditorPlugin(plugin) {
|
|
1174
|
+
const issues = [];
|
|
1175
|
+
const { id, capabilities, editor } = plugin.manifest;
|
|
1176
|
+
if (!capabilities.includes("editor")) {
|
|
1177
|
+
issues.push({ pluginId: id, message: "editor capability\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4." });
|
|
1178
|
+
}
|
|
1179
|
+
for (const slot of ["toolbar", "panel", "inspector"]) {
|
|
1180
|
+
const declared = new Set(editor?.[SLOT_KEYS[slot]] ?? []);
|
|
1181
|
+
const implemented = new Set(Object.keys(viewsFor(plugin, slot)));
|
|
1182
|
+
for (const viewId of declared) {
|
|
1183
|
+
if (!implemented.has(viewId)) {
|
|
1184
|
+
issues.push({
|
|
1185
|
+
pluginId: id,
|
|
1186
|
+
message: `${slot} ${viewId} \uAD6C\uD604\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
for (const viewId of implemented) {
|
|
1191
|
+
if (!declared.has(viewId)) {
|
|
1192
|
+
issues.push({
|
|
1193
|
+
pluginId: id,
|
|
1194
|
+
message: `${slot} ${viewId}\uAC00 manifest\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4.`
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return issues;
|
|
1200
|
+
}
|
|
1201
|
+
function permissionError(pluginId, permission) {
|
|
1202
|
+
return new Error(
|
|
1203
|
+
`Editor plugin ${pluginId}\uC5D0\uB294 ${permission} \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
function createEditorPluginContext(pluginId, permissions, state2) {
|
|
1207
|
+
return Object.freeze({
|
|
1208
|
+
pluginId,
|
|
1209
|
+
getDocument() {
|
|
1210
|
+
if (!permissions.documentRead)
|
|
1211
|
+
throw permissionError(pluginId, "documentRead");
|
|
1212
|
+
const document2 = state2.getDocument();
|
|
1213
|
+
if (!document2) throw new Error("\uC5F4\uB9B0 \uC560\uB2C8\uBA54\uC774\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
1214
|
+
return structuredClone(document2);
|
|
1215
|
+
},
|
|
1216
|
+
replaceDocument(document2) {
|
|
1217
|
+
if (!permissions.documentWrite)
|
|
1218
|
+
throw permissionError(pluginId, "documentWrite");
|
|
1219
|
+
state2.replaceDocument(structuredClone(document2));
|
|
1220
|
+
},
|
|
1221
|
+
getSelection() {
|
|
1222
|
+
if (!permissions.documentRead)
|
|
1223
|
+
throw permissionError(pluginId, "documentRead");
|
|
1224
|
+
return structuredClone(state2.getSelection());
|
|
1225
|
+
},
|
|
1226
|
+
setSelection(selection) {
|
|
1227
|
+
if (!permissions.documentWrite)
|
|
1228
|
+
throw permissionError(pluginId, "documentWrite");
|
|
1229
|
+
state2.setSelection(structuredClone(selection));
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
function mountEditorPlugins(root, plugins, resolvePermissions, state2) {
|
|
1234
|
+
const cleanups = [];
|
|
1235
|
+
const commands = [];
|
|
1236
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1237
|
+
for (const plugin of plugins) {
|
|
1238
|
+
const { id } = plugin.manifest;
|
|
1239
|
+
if (seen.has(id))
|
|
1240
|
+
throw new Error(`Editor plugin ${id}\uAC00 \uC911\uBCF5 \uB4F1\uB85D\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`);
|
|
1241
|
+
seen.add(id);
|
|
1242
|
+
const issues = validateEditorPlugin(plugin);
|
|
1243
|
+
if (issues.length > 0)
|
|
1244
|
+
throw new Error(issues.map((issue) => issue.message).join(" "));
|
|
1245
|
+
const permissions = resolvePermissions(plugin.manifest);
|
|
1246
|
+
if (!permissions.ui) continue;
|
|
1247
|
+
const context = createEditorPluginContext(id, permissions, state2);
|
|
1248
|
+
commands.push(...plugin.commands ?? []);
|
|
1249
|
+
for (const slot of ["toolbar", "panel", "inspector"]) {
|
|
1250
|
+
const target = root.querySelector(
|
|
1251
|
+
`[data-editor-plugin-slot="${slot}"]`
|
|
1252
|
+
);
|
|
1253
|
+
if (!target) continue;
|
|
1254
|
+
for (const viewId of plugin.manifest.editor?.[SLOT_KEYS[slot]] ?? []) {
|
|
1255
|
+
const view = viewsFor(plugin, slot)[viewId];
|
|
1256
|
+
if (!view) continue;
|
|
1257
|
+
const container = document.createElement(
|
|
1258
|
+
slot === "toolbar" ? "span" : "section"
|
|
1259
|
+
);
|
|
1260
|
+
container.dataset.editorPlugin = id;
|
|
1261
|
+
container.dataset.editorPluginView = viewId;
|
|
1262
|
+
container.setAttribute("aria-label", view.label);
|
|
1263
|
+
target.append(container);
|
|
1264
|
+
const cleanup = view.mount(container, context);
|
|
1265
|
+
if (cleanup) cleanups.push(cleanup);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return {
|
|
1270
|
+
commands,
|
|
1271
|
+
dispose() {
|
|
1272
|
+
cleanups.reverse().forEach((cleanup) => cleanup());
|
|
1273
|
+
root.querySelectorAll("[data-editor-plugin]").forEach((element) => element.remove());
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
export { addAppearance, addChapter, addCheckpoint, addEffect, addElement, animationDocumentFileName, animationDocumentToJson, apiBaseUrl, beginTransient, canRedo, canUndo, childIdsOf, configureAnimationRepository, configureApi, configureHost, createAnimation, createEditorPluginContext, createLayout, deleteAnimation, deleteChapter, deleteCheckpoint, deleteEffect, deleteElement, detachFromLayout, downloadAnimationJson, duplicateAnimation, endTransient, findContainingGroup, findLayoutCollisions, garbageCollectAnimationAssets, getCurrentSnapshot, getCurrentTime, getDef, getHistory, getSelectedElementIds, getSelection, groupBbox, groupElements, importAnimation, isDirty, isDraft, isElementSelected, isGroup, jumpBack, jumpForward, layoutIdsFor, listAnimations, loadAnimation, markClean, mountEditorPlugins, moveElementToEnd, moveElementToFront, moveGroupBy, placeholderImageUrl, promoteDraftToSaved, redo, registerDataUriAsset, registerExternalAsset, registerInlineAsset, removeAppearance, removeTrack, removeTrackKeyframe, reorderElement, resetHistory, saveAnimation, setCurrentTime, setDef, setDraft, setElementValueAtTime, setSelection, setTrackKeyframe, subscribe, toggleSelectionFor, undo, ungroupElement, uniqueChapterId, uniqueCheckpointId, uniqueEffectId, uniqueElementId, updateAppearance, updateCanvas, updateChapter, updateCheckpoint, updateData, updateDuration, updateEffect, updateElementBase, updateLocales, updateMeta, updateResponsive, updateSettings, validateEditorPlugin };
|
|
1279
|
+
//# sourceMappingURL=chunk-FAI2G3XK.js.map
|
|
1280
|
+
//# sourceMappingURL=chunk-FAI2G3XK.js.map
|