Skip to content

Commit ecd1920

Browse files
committed
feat(web): add animation editor and refine inspector workflow
1 parent 5fe690e commit ecd1920

File tree

62 files changed

+8972
-663
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+8972
-663
lines changed

apps/web/index.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
<script id="mdr-esm-importmap" type="importmap">
88
{
99
"imports": {
10-
"react": "/src/esm-bridge/react-interop.mjs",
11-
"react-dom": "/src/esm-bridge/react-dom-interop.mjs",
12-
"react/jsx-runtime": "/src/esm-bridge/react-jsx-runtime-interop.mjs",
13-
"react/jsx-dev-runtime": "/src/esm-bridge/react-jsx-dev-runtime-interop.mjs"
10+
"react": "/esm-bridge/react-interop.mjs",
11+
"react-dom": "/esm-bridge/react-dom-interop.mjs",
12+
"react/jsx-runtime": "/esm-bridge/react-jsx-runtime-interop.mjs",
13+
"react/jsx-dev-runtime": "/esm-bridge/react-jsx-dev-runtime-interop.mjs"
1414
}
1515
}
1616
</script>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const bridge = globalThis.__MDR_HOST_REACT_BRIDGE__;
2+
3+
if (!bridge?.reactDom) {
4+
throw new Error(
5+
'[ELIB-1011] Host React DOM bridge is not initialized before loading external ESM modules.'
6+
);
7+
}
8+
9+
const ReactDOM = bridge.reactDom;
10+
11+
export default ReactDOM;
12+
13+
export const createPortal = ReactDOM.createPortal;
14+
export const flushSync = ReactDOM.flushSync;
15+
export const preconnect = ReactDOM.preconnect;
16+
export const prefetchDNS = ReactDOM.prefetchDNS;
17+
export const preinit = ReactDOM.preinit;
18+
export const preinitModule = ReactDOM.preinitModule;
19+
export const preload = ReactDOM.preload;
20+
export const preloadModule = ReactDOM.preloadModule;
21+
export const requestFormReset = ReactDOM.requestFormReset;
22+
export const unstable_batchedUpdates = ReactDOM.unstable_batchedUpdates;
23+
export const useFormState = ReactDOM.useFormState;
24+
export const useFormStatus = ReactDOM.useFormStatus;
25+
export const version = ReactDOM.version;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const bridge = globalThis.__MDR_HOST_REACT_BRIDGE__;
2+
3+
if (!bridge?.react) {
4+
throw new Error(
5+
'[ELIB-1011] Host React bridge is not initialized before loading external ESM modules.'
6+
);
7+
}
8+
9+
const React = bridge.react;
10+
11+
export default React;
12+
13+
export const Children = React.Children;
14+
export const Component = React.Component;
15+
export const Fragment = React.Fragment;
16+
export const Profiler = React.Profiler;
17+
export const PureComponent = React.PureComponent;
18+
export const StrictMode = React.StrictMode;
19+
export const Suspense = React.Suspense;
20+
export const act = React.act;
21+
export const cache = React.cache;
22+
export const cacheSignal = React.cacheSignal;
23+
export const captureOwnerStack = React.captureOwnerStack;
24+
export const cloneElement = React.cloneElement;
25+
export const createContext = React.createContext;
26+
export const createElement = React.createElement;
27+
export const createRef = React.createRef;
28+
export const forwardRef = React.forwardRef;
29+
export const isValidElement = React.isValidElement;
30+
export const lazy = React.lazy;
31+
export const memo = React.memo;
32+
export const startTransition = React.startTransition;
33+
export const unstable_useCacheRefresh = React.unstable_useCacheRefresh;
34+
export const use = React.use;
35+
export const useActionState = React.useActionState;
36+
export const useCallback = React.useCallback;
37+
export const useContext = React.useContext;
38+
export const useDebugValue = React.useDebugValue;
39+
export const useDeferredValue = React.useDeferredValue;
40+
export const useEffect = React.useEffect;
41+
export const useEffectEvent = React.useEffectEvent;
42+
export const useId = React.useId;
43+
export const useImperativeHandle = React.useImperativeHandle;
44+
export const useInsertionEffect = React.useInsertionEffect;
45+
export const useLayoutEffect = React.useLayoutEffect;
46+
export const useMemo = React.useMemo;
47+
export const useOptimistic = React.useOptimistic;
48+
export const useReducer = React.useReducer;
49+
export const useRef = React.useRef;
50+
export const useState = React.useState;
51+
export const useSyncExternalStore = React.useSyncExternalStore;
52+
export const useTransition = React.useTransition;
53+
export const version = React.version;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const bridge = globalThis.__MDR_HOST_REACT_BRIDGE__;
2+
3+
if (!bridge?.jsxDevRuntime) {
4+
throw new Error(
5+
'[ELIB-1011] Host React JSX dev runtime bridge is not initialized before loading external ESM modules.'
6+
);
7+
}
8+
9+
const Runtime = bridge.jsxDevRuntime;
10+
11+
export default Runtime;
12+
export const Fragment = Runtime.Fragment;
13+
export const jsxDEV = Runtime.jsxDEV;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const bridge = globalThis.__MDR_HOST_REACT_BRIDGE__;
2+
3+
if (!bridge?.jsxRuntime) {
4+
throw new Error(
5+
'[ELIB-1011] Host React JSX runtime bridge is not initialized before loading external ESM modules.'
6+
);
7+
}
8+
9+
const Runtime = bridge.jsxRuntime;
10+
11+
export default Runtime;
12+
export const Fragment = Runtime.Fragment;
13+
export const jsx = Runtime.jsx;
14+
export const jsxs = Runtime.jsxs;

apps/web/src/App.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ const BlueprintEditor = lazy(
1616
const NodeGraphEditor = lazy(
1717
() => import('./editor/features/development/NodeGraphEditor')
1818
);
19+
const AnimationEditor = lazy(
20+
() => import('./editor/features/animation/AnimationEditor')
21+
);
1922
const ProjectResources = lazy(() =>
2023
import('./editor/features/resources/ProjectResources').then((module) => ({
2124
default: module.ProjectResources,
@@ -85,11 +88,7 @@ export const createRoutes = (t: TFunction) => [
8588
},
8689
{
8790
path: 'animation',
88-
element: (
89-
<div>
90-
{t('animationEditor', 'animationEditor', { ns: 'routes' })}
91-
</div>
92-
),
91+
element: withEditorSuspense(<AnimationEditor />),
9392
},
9493
{
9594
path: 'resources',

apps/web/src/core/types/engine.types.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,103 @@ export type NodeListRender = {
3838
emptyNodeId?: string;
3939
};
4040

41+
export type AnimationIterations = number | 'infinite';
42+
43+
export type AnimationKeyframe = {
44+
atMs: number;
45+
value: number | string;
46+
easing?: string;
47+
hold?: boolean;
48+
};
49+
50+
export type AnimationTrack =
51+
| {
52+
id: string;
53+
kind: 'style';
54+
property:
55+
| 'opacity'
56+
| 'transform.translateX'
57+
| 'transform.translateY'
58+
| 'transform.scale'
59+
| 'color';
60+
keyframes: AnimationKeyframe[];
61+
}
62+
| {
63+
id: string;
64+
kind: 'css-filter';
65+
fn:
66+
| 'blur'
67+
| 'brightness'
68+
| 'contrast'
69+
| 'grayscale'
70+
| 'hue-rotate'
71+
| 'invert'
72+
| 'saturate'
73+
| 'sepia';
74+
unit?: 'px' | '%' | 'deg';
75+
keyframes: AnimationKeyframe[];
76+
}
77+
| {
78+
id: string;
79+
kind: 'svg-filter-attr';
80+
filterId: string;
81+
primitiveId: string;
82+
attr: string;
83+
keyframes: AnimationKeyframe[];
84+
};
85+
86+
export type AnimationBinding = {
87+
id: string;
88+
targetNodeId: string;
89+
tracks: AnimationTrack[];
90+
};
91+
92+
export type SvgFilterDefinition = {
93+
id: string;
94+
units?: 'objectBoundingBox' | 'userSpaceOnUse';
95+
primitives: Array<{
96+
id: string;
97+
type:
98+
| 'feGaussianBlur'
99+
| 'feColorMatrix'
100+
| 'feComponentTransfer'
101+
| 'feOffset'
102+
| 'feBlend'
103+
| 'feMerge';
104+
in?: string;
105+
in2?: string;
106+
result?: string;
107+
attrs?: Record<string, number | string>;
108+
}>;
109+
};
110+
111+
export type AnimationTimeline = {
112+
id: string;
113+
name: string;
114+
durationMs: number;
115+
delayMs?: number;
116+
iterations?: AnimationIterations;
117+
direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
118+
fillMode?: 'none' | 'forwards' | 'backwards' | 'both';
119+
easing?: string;
120+
bindings: AnimationBinding[];
121+
};
122+
123+
export type AnimationEditorState = {
124+
version: 1;
125+
activeTimelineId?: string;
126+
cursorMs?: number;
127+
zoom?: number;
128+
expandedTrackIds?: string[];
129+
};
130+
131+
export interface AnimationDefinition {
132+
version: 1;
133+
timelines: AnimationTimeline[];
134+
svgFilters?: SvgFilterDefinition[];
135+
'x-animationEditor'?: AnimationEditorState;
136+
}
137+
41138
export interface ComponentNode {
42139
id: string;
43140
type: string;
@@ -94,4 +191,5 @@ export interface MIRDocument {
94191
root: ComponentNode;
95192
};
96193
logic?: LogicDefinition; // 👈 挂载逻辑定义
194+
animation?: AnimationDefinition;
97195
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { AnimationEditorContent } from './AnimationEditorContent';
2+
3+
function AnimationEditor() {
4+
return <AnimationEditorContent />;
5+
}
6+
7+
export default AnimationEditor;

0 commit comments

Comments
 (0)