@kokoa/clotho-editor 0.1.4 → 0.3.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 +53 -1
- package/dist/{chunk-GMGB7NIL.js → chunk-N47HF4VB.js} +436 -20
- package/dist/chunk-N47HF4VB.js.map +1 -0
- package/dist/clotho-editor.css +425 -0
- package/dist/index.d.ts +199 -18
- package/dist/index.js +515 -9
- package/dist/index.js.map +1 -1
- package/dist/{main-NCPWDB2U.js → main-DC6D6RNQ.js} +937 -22
- package/dist/main-DC6D6RNQ.js.map +1 -0
- package/package.json +7 -3
- package/dist/chunk-GMGB7NIL.js.map +0 -1
- package/dist/main-NCPWDB2U.js.map +0 -1
package/README.md
CHANGED
|
@@ -11,7 +11,10 @@ npm install @kokoa/clotho @kokoa/clotho-editor react react-dom
|
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
```tsx
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
StudioMount,
|
|
16
|
+
createLocalStorageRepository,
|
|
17
|
+
} from "@kokoa/clotho-editor";
|
|
15
18
|
import "@kokoa/clotho/styles.css";
|
|
16
19
|
import "@kokoa/clotho-editor/styles.css";
|
|
17
20
|
|
|
@@ -59,6 +62,55 @@ const repository: AnimationRepository = {
|
|
|
59
62
|
|
|
60
63
|
저장 버튼의 동작 자체를 바꾸려면 repository의 `create`와 `save`를 구현합니다. 예제 목록은 repository의 `list`와 `load`에서 제공하므로 editor package 안에 application별 API 경로를 넣을 필요가 없습니다.
|
|
61
64
|
|
|
65
|
+
## Plugin host
|
|
66
|
+
|
|
67
|
+
application 전용 도구는 `plugins`로 추가할 수 있습니다. plugin은 toolbar, 왼쪽 panel, inspector와 command palette에 기능을 붙일 수 있지만, 문서를 읽거나 바꾸려면 host가 권한을 명시적으로 허용해야 합니다. 저장소, selection, undo/redo와 같은 편집기의 기본 기능은 plugin으로 분리하지 않습니다.
|
|
68
|
+
|
|
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
|
+
};
|
|
87
|
+
|
|
88
|
+
<StudioMount
|
|
89
|
+
plugins={[reviewPlugin]}
|
|
90
|
+
resolvePluginPermissions={(manifest) =>
|
|
91
|
+
manifest.id === "com.example.review" ? { ui: true, documentRead: true } : {}
|
|
92
|
+
}
|
|
93
|
+
/>;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Clotho compiler plugin이 새로운 JSON 입력 형식을 처리해야 한다면 `importDocument`에서 compiler pipeline을 연결합니다. 이 경계 덕분에 Editor는 application의 plugin registry나 backend에 의존하지 않습니다.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { createPluginRegistry, runPluginPipeline } from "@kokoa/clotho/plugins";
|
|
100
|
+
|
|
101
|
+
const registry = createPluginRegistry(compilerPlugins);
|
|
102
|
+
|
|
103
|
+
<StudioMount
|
|
104
|
+
importDocument={(input) => {
|
|
105
|
+
const result = runPluginPipeline(input, { registry });
|
|
106
|
+
if (!result.ok) throw result.error;
|
|
107
|
+
return result.document;
|
|
108
|
+
}}
|
|
109
|
+
/>;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
현재 plugin API는 신뢰할 수 있는 application code를 위한 실험적 API입니다. 외부에서 받은 plugin은 별도 Worker나 격리 환경에서 실행한 뒤 JSON 결과만 Editor로 전달해야 합니다.
|
|
113
|
+
|
|
62
114
|
## 주요 기능
|
|
63
115
|
|
|
64
116
|
- Clotho v1 JSON 문서 작성과 검증
|
|
@@ -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) {
|
|
@@ -292,7 +292,10 @@ function toggleSelectionFor(sel, id) {
|
|
|
292
292
|
}
|
|
293
293
|
function getCurrentSnapshot() {
|
|
294
294
|
if (!state.def) return /* @__PURE__ */ new Map();
|
|
295
|
-
return computeSnapshot(
|
|
295
|
+
return computeSnapshot(
|
|
296
|
+
compileDataBindings(state.def).document,
|
|
297
|
+
state.currentTime
|
|
298
|
+
);
|
|
296
299
|
}
|
|
297
300
|
|
|
298
301
|
// src/legacy/state/history.ts
|
|
@@ -707,6 +710,180 @@ function updateDuration(ms) {
|
|
|
707
710
|
);
|
|
708
711
|
if (state.currentTime > ms) state.currentTime = ms;
|
|
709
712
|
}
|
|
713
|
+
|
|
714
|
+
// src/legacy/state/camera.ts
|
|
715
|
+
var EMPTY = { tracks: [], focus: [], strokeScaling: "scale" };
|
|
716
|
+
function getCamera() {
|
|
717
|
+
return state.def?.camera ?? EMPTY;
|
|
718
|
+
}
|
|
719
|
+
function hasCamera() {
|
|
720
|
+
const camera = state.def?.camera;
|
|
721
|
+
return camera !== void 0 && (camera.tracks.length > 0 || camera.focus.length > 0 || camera.strokeScaling !== "scale");
|
|
722
|
+
}
|
|
723
|
+
function cameraControlAt(time) {
|
|
724
|
+
const camera = state.def?.camera;
|
|
725
|
+
if (!camera || camera.tracks.length === 0 && camera.focus.length === 0) {
|
|
726
|
+
return { kind: "none" };
|
|
727
|
+
}
|
|
728
|
+
let index = -1;
|
|
729
|
+
camera.focus.forEach((entry, i) => {
|
|
730
|
+
if (entry.time <= time) index = i;
|
|
731
|
+
});
|
|
732
|
+
return index >= 0 ? { kind: "focus", index } : { kind: "tracks" };
|
|
733
|
+
}
|
|
734
|
+
function ensure(def) {
|
|
735
|
+
def.camera ??= { tracks: [], focus: [], strokeScaling: "scale" };
|
|
736
|
+
return def.camera;
|
|
737
|
+
}
|
|
738
|
+
function setCameraStrokeScaling(value) {
|
|
739
|
+
mutateDef(
|
|
740
|
+
(def) => {
|
|
741
|
+
ensure(def).strokeScaling = value;
|
|
742
|
+
},
|
|
743
|
+
`\uCE74\uBA54\uB77C \uC120 \uB450\uAED8: ${value}`,
|
|
744
|
+
"camera"
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
function setCameraKeyframe(property, time, value, ease) {
|
|
748
|
+
mutateDef(
|
|
749
|
+
(def) => {
|
|
750
|
+
const camera = ensure(def);
|
|
751
|
+
let track = camera.tracks.find((t) => t.property === property);
|
|
752
|
+
if (!track) {
|
|
753
|
+
track = { property, keyframes: [] };
|
|
754
|
+
camera.tracks.push(track);
|
|
755
|
+
camera.tracks.sort(
|
|
756
|
+
(a, b) => ORDER.indexOf(a.property) - ORDER.indexOf(b.property)
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
const existing = track.keyframes.findIndex((kf) => kf.time === time);
|
|
760
|
+
const next = { time, value, ...ease ? { ease } : {} };
|
|
761
|
+
if (existing >= 0) track.keyframes[existing] = next;
|
|
762
|
+
else track.keyframes.push(next);
|
|
763
|
+
track.keyframes.sort((a, b) => a.time - b.time);
|
|
764
|
+
},
|
|
765
|
+
`\uCE74\uBA54\uB77C keyframe: ${property} @ ${time}ms`,
|
|
766
|
+
"camera"
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
var ORDER = ["zoom", "x", "y"];
|
|
770
|
+
function moveCameraKeyframe(property, from, to) {
|
|
771
|
+
if (from === to) return;
|
|
772
|
+
mutateDef(
|
|
773
|
+
(def) => {
|
|
774
|
+
const track = def.camera?.tracks.find((t) => t.property === property);
|
|
775
|
+
const kf = track?.keyframes.find((k) => k.time === from);
|
|
776
|
+
if (!track || !kf) return;
|
|
777
|
+
track.keyframes = track.keyframes.filter(
|
|
778
|
+
(k) => k.time !== from && k.time !== to
|
|
779
|
+
);
|
|
780
|
+
track.keyframes.push({ ...kf, time: to });
|
|
781
|
+
track.keyframes.sort((a, b) => a.time - b.time);
|
|
782
|
+
},
|
|
783
|
+
`\uCE74\uBA54\uB77C keyframe \uC774\uB3D9: ${property} ${from}\u2192${to}ms`,
|
|
784
|
+
"camera"
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
function removeCameraKeyframe(property, time) {
|
|
788
|
+
mutateDef(
|
|
789
|
+
(def) => {
|
|
790
|
+
const camera = def.camera;
|
|
791
|
+
const track = camera?.tracks.find((t) => t.property === property);
|
|
792
|
+
if (!camera || !track) return;
|
|
793
|
+
track.keyframes = track.keyframes.filter((kf) => kf.time !== time);
|
|
794
|
+
if (track.keyframes.length === 0) {
|
|
795
|
+
camera.tracks = camera.tracks.filter((t) => t.property !== property);
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
`\uCE74\uBA54\uB77C keyframe \uC0AD\uC81C: ${property} @ ${time}ms`,
|
|
799
|
+
"camera"
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
function removeCameraTrack(property) {
|
|
803
|
+
mutateDef(
|
|
804
|
+
(def) => {
|
|
805
|
+
if (def.camera) {
|
|
806
|
+
def.camera.tracks = def.camera.tracks.filter(
|
|
807
|
+
(t) => t.property !== property
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
`\uCE74\uBA54\uB77C track \uC0AD\uC81C: ${property}`,
|
|
812
|
+
"camera"
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
function addCameraFocus(focus) {
|
|
816
|
+
mutateDef(
|
|
817
|
+
(def) => {
|
|
818
|
+
const camera = ensure(def);
|
|
819
|
+
camera.focus.push(focus);
|
|
820
|
+
camera.focus.sort((a, b) => a.time - b.time);
|
|
821
|
+
},
|
|
822
|
+
`\uCE74\uBA54\uB77C focus \uCD94\uAC00 @ ${focus.time}ms`,
|
|
823
|
+
"camera"
|
|
824
|
+
);
|
|
825
|
+
const index = getCamera().focus.findIndex(
|
|
826
|
+
(f) => f.time === focus.time && f.elementIds.join() === focus.elementIds.join()
|
|
827
|
+
);
|
|
828
|
+
state.selection = {
|
|
829
|
+
kind: "camera",
|
|
830
|
+
focusIndex: index < 0 ? void 0 : index
|
|
831
|
+
};
|
|
832
|
+
emit();
|
|
833
|
+
}
|
|
834
|
+
function updateCameraFocus(index, patch) {
|
|
835
|
+
const before = getCamera().focus[index];
|
|
836
|
+
if (!before) return;
|
|
837
|
+
const after = { ...before, ...patch };
|
|
838
|
+
mutateDef(
|
|
839
|
+
(def) => {
|
|
840
|
+
const camera = def.camera;
|
|
841
|
+
if (!camera?.focus[index]) return;
|
|
842
|
+
camera.focus[index] = after;
|
|
843
|
+
camera.focus.sort((a, b) => a.time - b.time);
|
|
844
|
+
},
|
|
845
|
+
`\uCE74\uBA54\uB77C focus \uC218\uC815: ${Object.keys(patch).join(", ")}`,
|
|
846
|
+
"camera"
|
|
847
|
+
);
|
|
848
|
+
if (state.selection.kind === "camera") {
|
|
849
|
+
const moved = getCamera().focus.findIndex(
|
|
850
|
+
(f) => f.time === after.time && f.elementIds.join() === after.elementIds.join() && f.duration === after.duration
|
|
851
|
+
);
|
|
852
|
+
state.selection = {
|
|
853
|
+
kind: "camera",
|
|
854
|
+
focusIndex: moved < 0 ? void 0 : moved
|
|
855
|
+
};
|
|
856
|
+
emit();
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function deleteCameraFocus(index) {
|
|
860
|
+
mutateDef(
|
|
861
|
+
(def) => {
|
|
862
|
+
if (def.camera) {
|
|
863
|
+
def.camera.focus = def.camera.focus.filter((_, i) => i !== index);
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
`\uCE74\uBA54\uB77C focus \uC0AD\uC81C #${index + 1}`,
|
|
867
|
+
"camera"
|
|
868
|
+
);
|
|
869
|
+
if (state.selection.kind === "camera") {
|
|
870
|
+
state.selection = { kind: "camera" };
|
|
871
|
+
emit();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function clearCamera() {
|
|
875
|
+
mutateDef(
|
|
876
|
+
(def) => {
|
|
877
|
+
delete def.camera;
|
|
878
|
+
},
|
|
879
|
+
"\uCE74\uBA54\uB77C \uC81C\uAC70",
|
|
880
|
+
"camera"
|
|
881
|
+
);
|
|
882
|
+
if (state.selection.kind === "camera") {
|
|
883
|
+
state.selection = { kind: "none" };
|
|
884
|
+
emit();
|
|
885
|
+
}
|
|
886
|
+
}
|
|
710
887
|
function updateMeta(patch) {
|
|
711
888
|
const keys = Object.keys(patch).join(", ");
|
|
712
889
|
mutateDef(
|
|
@@ -726,6 +903,24 @@ function updateLocales(locales) {
|
|
|
726
903
|
"meta"
|
|
727
904
|
);
|
|
728
905
|
}
|
|
906
|
+
function updateData(data) {
|
|
907
|
+
mutateDef(
|
|
908
|
+
(def) => {
|
|
909
|
+
def.data = data;
|
|
910
|
+
},
|
|
911
|
+
"\uC0D8\uD50C \uB370\uC774\uD130 \uBCC0\uACBD",
|
|
912
|
+
"meta"
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
function updateResponsive(responsive) {
|
|
916
|
+
mutateDef(
|
|
917
|
+
(def) => {
|
|
918
|
+
def.responsive = responsive;
|
|
919
|
+
},
|
|
920
|
+
"Responsive Stage \uBCC0\uACBD",
|
|
921
|
+
"canvas"
|
|
922
|
+
);
|
|
923
|
+
}
|
|
729
924
|
function updateCanvas(patch) {
|
|
730
925
|
const keys = Object.keys(patch).join(", ");
|
|
731
926
|
mutateDef(
|
|
@@ -830,6 +1025,125 @@ function registerDataUriAsset(dataUri) {
|
|
|
830
1025
|
);
|
|
831
1026
|
return id;
|
|
832
1027
|
}
|
|
1028
|
+
function uniqueLayoutId(def) {
|
|
1029
|
+
const used = new Set(def.layouts.map((layout) => layout.id));
|
|
1030
|
+
let index = 1;
|
|
1031
|
+
while (used.has(`layout-${index}`)) index += 1;
|
|
1032
|
+
return `layout-${index}`;
|
|
1033
|
+
}
|
|
1034
|
+
function createLayout(elementIds, mode) {
|
|
1035
|
+
if (elementIds.length === 0) return;
|
|
1036
|
+
mutateDef(
|
|
1037
|
+
(def) => {
|
|
1038
|
+
const selected = def.elements.filter(
|
|
1039
|
+
(element) => elementIds.includes(element.id)
|
|
1040
|
+
);
|
|
1041
|
+
if (selected.length === 0) return;
|
|
1042
|
+
const measured = compileLayouts({ ...def, layouts: [] }).boxes;
|
|
1043
|
+
const boxes = selected.flatMap((element) => {
|
|
1044
|
+
const box = measured[element.id];
|
|
1045
|
+
return box ? [box] : [];
|
|
1046
|
+
});
|
|
1047
|
+
const x = boxes.length > 0 ? Math.min(...boxes.map((box) => box.x)) : 0;
|
|
1048
|
+
const y = boxes.length > 0 ? Math.min(...boxes.map((box) => box.y)) : 0;
|
|
1049
|
+
def.layouts = def.layouts.filter(
|
|
1050
|
+
(layout) => !layout.elementIds.some((id) => elementIds.includes(id))
|
|
1051
|
+
);
|
|
1052
|
+
def.layouts.push({
|
|
1053
|
+
id: uniqueLayoutId(def),
|
|
1054
|
+
mode,
|
|
1055
|
+
elementIds: selected.map((element) => element.id),
|
|
1056
|
+
x,
|
|
1057
|
+
y,
|
|
1058
|
+
gap: 16,
|
|
1059
|
+
align: "start",
|
|
1060
|
+
constraints: []
|
|
1061
|
+
});
|
|
1062
|
+
def.elements = compileLayouts(def).document.elements;
|
|
1063
|
+
},
|
|
1064
|
+
`${mode} layout \uC0DD\uC131`,
|
|
1065
|
+
"layout"
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
function detachFromLayout(elementIds) {
|
|
1069
|
+
const ids = new Set(elementIds);
|
|
1070
|
+
mutateDef(
|
|
1071
|
+
(def) => {
|
|
1072
|
+
def.layouts = def.layouts.flatMap((layout) => {
|
|
1073
|
+
const remaining = layout.elementIds.filter((id) => !ids.has(id));
|
|
1074
|
+
return remaining.length === 0 ? [] : [{ ...layout, elementIds: remaining }];
|
|
1075
|
+
});
|
|
1076
|
+
},
|
|
1077
|
+
"layout\uC5D0\uC11C \uBD84\uB9AC",
|
|
1078
|
+
"layout"
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
function layoutIdsFor(elementIds) {
|
|
1082
|
+
if (!state.def) return [];
|
|
1083
|
+
const ids = new Set(elementIds);
|
|
1084
|
+
return state.def.layouts.filter((layout) => layout.elementIds.some((id) => ids.has(id))).map((layout) => layout.id);
|
|
1085
|
+
}
|
|
1086
|
+
function findLayoutCollisions(def) {
|
|
1087
|
+
const { boxes } = compileLayouts(def);
|
|
1088
|
+
const collisions = [];
|
|
1089
|
+
for (const layout of def.layouts) {
|
|
1090
|
+
for (let firstIndex = 0; firstIndex < layout.elementIds.length; firstIndex += 1) {
|
|
1091
|
+
for (let secondIndex = firstIndex + 1; secondIndex < layout.elementIds.length; secondIndex += 1) {
|
|
1092
|
+
const firstId = layout.elementIds[firstIndex];
|
|
1093
|
+
const secondId = layout.elementIds[secondIndex];
|
|
1094
|
+
const first = boxes[firstId];
|
|
1095
|
+
const second = boxes[secondId];
|
|
1096
|
+
if (!first || !second) continue;
|
|
1097
|
+
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;
|
|
1098
|
+
if (overlaps) collisions.push({ firstId, secondId });
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return collisions;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// src/legacy/state/checkpoints.ts
|
|
1106
|
+
function addCheckpoint(checkpoint) {
|
|
1107
|
+
mutateDef(
|
|
1108
|
+
(def) => {
|
|
1109
|
+
def.checkpoints.push(checkpoint);
|
|
1110
|
+
def.checkpoints.sort((a, b) => a.time - b.time);
|
|
1111
|
+
},
|
|
1112
|
+
`Checkpoint \uCD94\uAC00: ${checkpoint.id}`,
|
|
1113
|
+
"checkpoint"
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
function updateCheckpoint(id, patch) {
|
|
1117
|
+
mutateDef(
|
|
1118
|
+
(def) => {
|
|
1119
|
+
const index = def.checkpoints.findIndex(
|
|
1120
|
+
(checkpoint) => checkpoint.id === id
|
|
1121
|
+
);
|
|
1122
|
+
if (index < 0) return;
|
|
1123
|
+
def.checkpoints[index] = {
|
|
1124
|
+
...def.checkpoints[index],
|
|
1125
|
+
...patch
|
|
1126
|
+
};
|
|
1127
|
+
def.checkpoints.sort((a, b) => a.time - b.time);
|
|
1128
|
+
},
|
|
1129
|
+
`Checkpoint \uC218\uC815: ${id}`,
|
|
1130
|
+
"checkpoint"
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
function deleteCheckpoint(id) {
|
|
1134
|
+
mutateDef(
|
|
1135
|
+
(def) => {
|
|
1136
|
+
def.checkpoints = def.checkpoints.filter(
|
|
1137
|
+
(checkpoint) => checkpoint.id !== id
|
|
1138
|
+
);
|
|
1139
|
+
},
|
|
1140
|
+
`Checkpoint \uC0AD\uC81C: ${id}`,
|
|
1141
|
+
"checkpoint"
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
function uniqueCheckpointId() {
|
|
1145
|
+
return `checkpoint-${Date.now().toString(36)}`;
|
|
1146
|
+
}
|
|
833
1147
|
|
|
834
1148
|
// src/legacy/studio-groups.ts
|
|
835
1149
|
function isGroup(el) {
|
|
@@ -974,21 +1288,6 @@ function groupElements(ids) {
|
|
|
974
1288
|
const valid = ids.filter((id) => def.elements.some((e) => e.id === id));
|
|
975
1289
|
if (valid.length < 2) return null;
|
|
976
1290
|
const newId = uniqueElementId("group");
|
|
977
|
-
(() => {
|
|
978
|
-
let minX = Infinity, minY = Infinity;
|
|
979
|
-
for (const id of valid) {
|
|
980
|
-
const el = def.elements.find((e) => e.id === id);
|
|
981
|
-
if (!el) continue;
|
|
982
|
-
const b = elementBbox(el);
|
|
983
|
-
if (!b) continue;
|
|
984
|
-
if (b.x < minX) minX = b.x;
|
|
985
|
-
if (b.y < minY) minY = b.y;
|
|
986
|
-
}
|
|
987
|
-
return {
|
|
988
|
-
x: Number.isFinite(minX) ? minX : 0,
|
|
989
|
-
y: Number.isFinite(minY) ? minY : 0
|
|
990
|
-
};
|
|
991
|
-
})();
|
|
992
1291
|
const group = {
|
|
993
1292
|
type: "group",
|
|
994
1293
|
id: newId,
|
|
@@ -996,6 +1295,7 @@ function groupElements(ids) {
|
|
|
996
1295
|
rotation: 0,
|
|
997
1296
|
appearances: [],
|
|
998
1297
|
tracks: [],
|
|
1298
|
+
bindings: [],
|
|
999
1299
|
// Children keep their absolute coordinates, so the group's own transform starts at
|
|
1000
1300
|
// the identity. Setting x/y here would shift every member on the next render.
|
|
1001
1301
|
x: 0,
|
|
@@ -1033,6 +1333,122 @@ function placeholderImageUrl() {
|
|
|
1033
1333
|
return placeholderUrl;
|
|
1034
1334
|
}
|
|
1035
1335
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1336
|
+
// src/plugin-host.ts
|
|
1337
|
+
var SLOT_KEYS = {
|
|
1338
|
+
toolbar: "toolbarItems",
|
|
1339
|
+
panel: "panels",
|
|
1340
|
+
inspector: "inspectors"
|
|
1341
|
+
};
|
|
1342
|
+
function viewsFor(plugin, slot) {
|
|
1343
|
+
if (slot === "toolbar") return plugin.toolbarItems ?? {};
|
|
1344
|
+
if (slot === "panel") return plugin.panels ?? {};
|
|
1345
|
+
return plugin.inspectors ?? {};
|
|
1346
|
+
}
|
|
1347
|
+
function validateEditorPlugin(plugin) {
|
|
1348
|
+
const issues = [];
|
|
1349
|
+
const { id, capabilities, editor } = plugin.manifest;
|
|
1350
|
+
if (!capabilities.includes("editor")) {
|
|
1351
|
+
issues.push({ pluginId: id, message: "editor capability\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4." });
|
|
1352
|
+
}
|
|
1353
|
+
for (const slot of ["toolbar", "panel", "inspector"]) {
|
|
1354
|
+
const declared = new Set(editor?.[SLOT_KEYS[slot]] ?? []);
|
|
1355
|
+
const implemented = new Set(Object.keys(viewsFor(plugin, slot)));
|
|
1356
|
+
for (const viewId of declared) {
|
|
1357
|
+
if (!implemented.has(viewId)) {
|
|
1358
|
+
issues.push({
|
|
1359
|
+
pluginId: id,
|
|
1360
|
+
message: `${slot} ${viewId} \uAD6C\uD604\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
for (const viewId of implemented) {
|
|
1365
|
+
if (!declared.has(viewId)) {
|
|
1366
|
+
issues.push({
|
|
1367
|
+
pluginId: id,
|
|
1368
|
+
message: `${slot} ${viewId}\uAC00 manifest\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4.`
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return issues;
|
|
1374
|
+
}
|
|
1375
|
+
function permissionError(pluginId, permission) {
|
|
1376
|
+
return new Error(
|
|
1377
|
+
`Editor plugin ${pluginId}\uC5D0\uB294 ${permission} \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
function createEditorPluginContext(pluginId, permissions, state2) {
|
|
1381
|
+
return Object.freeze({
|
|
1382
|
+
pluginId,
|
|
1383
|
+
getDocument() {
|
|
1384
|
+
if (!permissions.documentRead)
|
|
1385
|
+
throw permissionError(pluginId, "documentRead");
|
|
1386
|
+
const document2 = state2.getDocument();
|
|
1387
|
+
if (!document2) throw new Error("\uC5F4\uB9B0 \uC560\uB2C8\uBA54\uC774\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
1388
|
+
return structuredClone(document2);
|
|
1389
|
+
},
|
|
1390
|
+
replaceDocument(document2) {
|
|
1391
|
+
if (!permissions.documentWrite)
|
|
1392
|
+
throw permissionError(pluginId, "documentWrite");
|
|
1393
|
+
state2.replaceDocument(structuredClone(document2));
|
|
1394
|
+
},
|
|
1395
|
+
getSelection() {
|
|
1396
|
+
if (!permissions.documentRead)
|
|
1397
|
+
throw permissionError(pluginId, "documentRead");
|
|
1398
|
+
return structuredClone(state2.getSelection());
|
|
1399
|
+
},
|
|
1400
|
+
setSelection(selection) {
|
|
1401
|
+
if (!permissions.documentWrite)
|
|
1402
|
+
throw permissionError(pluginId, "documentWrite");
|
|
1403
|
+
state2.setSelection(structuredClone(selection));
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
function mountEditorPlugins(root, plugins, resolvePermissions, state2) {
|
|
1408
|
+
const cleanups = [];
|
|
1409
|
+
const commands = [];
|
|
1410
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1411
|
+
for (const plugin of plugins) {
|
|
1412
|
+
const { id } = plugin.manifest;
|
|
1413
|
+
if (seen.has(id))
|
|
1414
|
+
throw new Error(`Editor plugin ${id}\uAC00 \uC911\uBCF5 \uB4F1\uB85D\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`);
|
|
1415
|
+
seen.add(id);
|
|
1416
|
+
const issues = validateEditorPlugin(plugin);
|
|
1417
|
+
if (issues.length > 0)
|
|
1418
|
+
throw new Error(issues.map((issue) => issue.message).join(" "));
|
|
1419
|
+
const permissions = resolvePermissions(plugin.manifest);
|
|
1420
|
+
if (!permissions.ui) continue;
|
|
1421
|
+
const context = createEditorPluginContext(id, permissions, state2);
|
|
1422
|
+
commands.push(...plugin.commands ?? []);
|
|
1423
|
+
for (const slot of ["toolbar", "panel", "inspector"]) {
|
|
1424
|
+
const target = root.querySelector(
|
|
1425
|
+
`[data-editor-plugin-slot="${slot}"]`
|
|
1426
|
+
);
|
|
1427
|
+
if (!target) continue;
|
|
1428
|
+
for (const viewId of plugin.manifest.editor?.[SLOT_KEYS[slot]] ?? []) {
|
|
1429
|
+
const view = viewsFor(plugin, slot)[viewId];
|
|
1430
|
+
if (!view) continue;
|
|
1431
|
+
const container = document.createElement(
|
|
1432
|
+
slot === "toolbar" ? "span" : "section"
|
|
1433
|
+
);
|
|
1434
|
+
container.dataset.editorPlugin = id;
|
|
1435
|
+
container.dataset.editorPluginView = viewId;
|
|
1436
|
+
container.setAttribute("aria-label", view.label);
|
|
1437
|
+
target.append(container);
|
|
1438
|
+
const cleanup = view.mount(container, context);
|
|
1439
|
+
if (cleanup) cleanups.push(cleanup);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return {
|
|
1444
|
+
commands,
|
|
1445
|
+
dispose() {
|
|
1446
|
+
cleanups.reverse().forEach((cleanup) => cleanup());
|
|
1447
|
+
root.querySelectorAll("[data-editor-plugin]").forEach((element) => element.remove());
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
export { addAppearance, addCameraFocus, addChapter, addCheckpoint, addEffect, addElement, animationDocumentFileName, animationDocumentToJson, apiBaseUrl, beginTransient, cameraControlAt, canRedo, canUndo, childIdsOf, clearCamera, configureAnimationRepository, configureApi, configureHost, createAnimation, createEditorPluginContext, createLayout, deleteAnimation, deleteCameraFocus, deleteChapter, deleteCheckpoint, deleteEffect, deleteElement, detachFromLayout, downloadAnimationJson, duplicateAnimation, endTransient, findContainingGroup, findLayoutCollisions, garbageCollectAnimationAssets, getCamera, getCurrentSnapshot, getCurrentTime, getDef, getHistory, getSelectedElementIds, getSelection, groupBbox, groupElements, hasCamera, importAnimation, isDirty, isDraft, isElementSelected, isGroup, jumpBack, jumpForward, layoutIdsFor, listAnimations, loadAnimation, markClean, mountEditorPlugins, moveCameraKeyframe, moveElementToEnd, moveElementToFront, moveGroupBy, placeholderImageUrl, promoteDraftToSaved, redo, registerDataUriAsset, registerExternalAsset, registerInlineAsset, removeAppearance, removeCameraKeyframe, removeCameraTrack, removeTrack, removeTrackKeyframe, reorderElement, resetHistory, saveAnimation, setCameraKeyframe, setCameraStrokeScaling, setCurrentTime, setDef, setDraft, setElementValueAtTime, setSelection, setTrackKeyframe, subscribe, toggleSelectionFor, undo, ungroupElement, uniqueChapterId, uniqueCheckpointId, uniqueEffectId, uniqueElementId, updateAppearance, updateCameraFocus, updateCanvas, updateChapter, updateCheckpoint, updateData, updateDuration, updateEffect, updateElementBase, updateLocales, updateMeta, updateResponsive, updateSettings, validateEditorPlugin };
|
|
1453
|
+
//# sourceMappingURL=chunk-N47HF4VB.js.map
|
|
1454
|
+
//# sourceMappingURL=chunk-N47HF4VB.js.map
|