@clypra/ui 1.0.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/.releaserc.json +16 -0
- package/package.json +47 -0
- package/src/components/GraphInspector/GraphInspector.tsx +91 -0
- package/src/components/GraphInspector/index.ts +2 -0
- package/src/components/PassInspector/PassInspector.tsx +128 -0
- package/src/components/PassInspector/index.ts +2 -0
- package/src/components/PerformanceMonitor/PerformanceMonitor.tsx +129 -0
- package/src/components/PerformanceMonitor/index.ts +2 -0
- package/src/components/PresetManager/PresetManager.tsx +420 -0
- package/src/components/PresetManager/index.ts +2 -0
- package/src/components/PreviewCanvas/PreviewCanvas.tsx +168 -0
- package/src/components/PreviewCanvas/index.ts +2 -0
- package/src/components/ResourceInspector/ResourceInspector.tsx +126 -0
- package/src/components/ResourceInspector/index.ts +2 -0
- package/src/components/Timeline/Timeline.tsx +291 -0
- package/src/components/Timeline/index.ts +2 -0
- package/src/components/ValidationPanel/ValidationPanel.tsx +428 -0
- package/src/components/ValidationPanel/index.ts +2 -0
- package/src/index.ts +35 -0
- package/tsconfig.json +22 -0
- package/tsup.config.ts +14 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResourceInspector Component
|
|
3
|
+
*
|
|
4
|
+
* Shows GPU resources: textures, uniforms, buffers, and memory usage.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import type { FrameGraph } from "@clypra/runtime/planner";
|
|
9
|
+
|
|
10
|
+
export interface ResourceInspectorProps {
|
|
11
|
+
frameGraph: FrameGraph;
|
|
12
|
+
memoryUsage?: number; // bytes
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ResourceInspector: React.FC<ResourceInspectorProps> = ({ frameGraph, memoryUsage = 0 }) => {
|
|
16
|
+
const textures = frameGraph.resourceRequests.filter((r) => r.type === "texture");
|
|
17
|
+
const buffers = frameGraph.resourceRequests.filter((r) => r.type === "buffer");
|
|
18
|
+
const transient = frameGraph.resourceRequests.filter((r) => r.transient);
|
|
19
|
+
const permanent = frameGraph.resourceRequests.filter((r) => !r.transient);
|
|
20
|
+
|
|
21
|
+
const totalMemory = frameGraph.resourceRequests.reduce((sum, resource) => {
|
|
22
|
+
const bytesPerPixel = getBytesPerPixel(resource.format);
|
|
23
|
+
return sum + resource.width * resource.height * bytesPerPixel;
|
|
24
|
+
}, 0);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="resource-inspector" style={{ padding: "16px", fontFamily: "monospace" }}>
|
|
28
|
+
<div style={{ marginBottom: "16px", fontWeight: "bold", fontSize: "14px" }}>Resource Inspector</div>
|
|
29
|
+
|
|
30
|
+
{/* Summary */}
|
|
31
|
+
<div style={{ marginBottom: "16px", padding: "12px", backgroundColor: "#f5f5f5", borderRadius: "6px" }}>
|
|
32
|
+
<div style={{ fontSize: "12px", marginBottom: "4px" }}>
|
|
33
|
+
<span style={{ fontWeight: "600" }}>Total Resources:</span> {frameGraph.resourceRequests.length}
|
|
34
|
+
</div>
|
|
35
|
+
<div style={{ fontSize: "12px", marginBottom: "4px" }}>
|
|
36
|
+
<span style={{ fontWeight: "600" }}>Textures:</span> {textures.length} | <span style={{ fontWeight: "600" }}>Buffers:</span> {buffers.length}
|
|
37
|
+
</div>
|
|
38
|
+
<div style={{ fontSize: "12px", marginBottom: "4px" }}>
|
|
39
|
+
<span style={{ fontWeight: "600" }}>Transient:</span> {transient.length} | <span style={{ fontWeight: "600" }}>Permanent:</span> {permanent.length}
|
|
40
|
+
</div>
|
|
41
|
+
<div style={{ fontSize: "12px", fontWeight: "600", color: "#1976d2" }}>Memory: {formatBytes(totalMemory)}</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{/* Texture List */}
|
|
45
|
+
<div style={{ marginBottom: "16px" }}>
|
|
46
|
+
<div style={{ fontSize: "13px", fontWeight: "600", marginBottom: "8px" }}>Textures ({textures.length})</div>
|
|
47
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
|
48
|
+
{textures.map((resource) => (
|
|
49
|
+
<ResourceItem key={resource.id} resource={resource} />
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{/* Buffer List */}
|
|
55
|
+
{buffers.length > 0 && (
|
|
56
|
+
<div>
|
|
57
|
+
<div style={{ fontSize: "13px", fontWeight: "600", marginBottom: "8px" }}>Buffers ({buffers.length})</div>
|
|
58
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
|
59
|
+
{buffers.map((resource) => (
|
|
60
|
+
<ResourceItem key={resource.id} resource={resource} />
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
interface ResourceItemProps {
|
|
70
|
+
resource: {
|
|
71
|
+
id: string;
|
|
72
|
+
type: "texture" | "buffer";
|
|
73
|
+
width: number;
|
|
74
|
+
height: number;
|
|
75
|
+
format: string;
|
|
76
|
+
transient: boolean;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const ResourceItem: React.FC<ResourceItemProps> = ({ resource }) => {
|
|
81
|
+
const bytesPerPixel = getBytesPerPixel(resource.format);
|
|
82
|
+
const size = resource.width * resource.height * bytesPerPixel;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
style={{
|
|
87
|
+
padding: "8px",
|
|
88
|
+
backgroundColor: resource.transient ? "#fff9c4" : "#e3f2fd",
|
|
89
|
+
border: "1px solid #e0e0e0",
|
|
90
|
+
borderRadius: "4px",
|
|
91
|
+
fontSize: "11px",
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<div style={{ fontWeight: "600", marginBottom: "4px" }}>
|
|
95
|
+
{resource.id}
|
|
96
|
+
{resource.transient && <span style={{ marginLeft: "8px", fontSize: "10px", color: "#f57c00" }}>⏱ Transient</span>}
|
|
97
|
+
</div>
|
|
98
|
+
<div style={{ color: "#666" }}>
|
|
99
|
+
{resource.width}×{resource.height} | {resource.format} | {formatBytes(size)}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function getBytesPerPixel(format: string): number {
|
|
106
|
+
switch (format) {
|
|
107
|
+
case "rgba8":
|
|
108
|
+
return 4;
|
|
109
|
+
case "rgba16f":
|
|
110
|
+
return 8;
|
|
111
|
+
case "rgba32f":
|
|
112
|
+
return 16;
|
|
113
|
+
case "r8":
|
|
114
|
+
return 1;
|
|
115
|
+
case "depth24":
|
|
116
|
+
return 3;
|
|
117
|
+
default:
|
|
118
|
+
return 4;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatBytes(bytes: number): string {
|
|
123
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
124
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
125
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
126
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline Component
|
|
3
|
+
*
|
|
4
|
+
* Draggable playhead with frame-accurate scrubbing.
|
|
5
|
+
* Supports keyboard shortcuts for navigation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useRef, useState, useEffect } from "react";
|
|
9
|
+
|
|
10
|
+
export interface TimelineProps {
|
|
11
|
+
/** Total duration in seconds */
|
|
12
|
+
duration: number;
|
|
13
|
+
/** Current playback time in seconds */
|
|
14
|
+
currentTime: number;
|
|
15
|
+
/** Callback when user seeks to a new time */
|
|
16
|
+
onSeek: (time: number) => void;
|
|
17
|
+
/** Frame rate for frame-accurate navigation */
|
|
18
|
+
frameRate?: number;
|
|
19
|
+
/** Show frame numbers instead of timecodes */
|
|
20
|
+
showFrames?: boolean;
|
|
21
|
+
/** Height of the timeline in pixels */
|
|
22
|
+
height?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Timeline({ duration, currentTime, onSeek, frameRate = 60, showFrames = false, height = 80 }: TimelineProps) {
|
|
26
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
27
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
28
|
+
const [hoverTime, setHoverTime] = useState<number | null>(null);
|
|
29
|
+
|
|
30
|
+
const currentFrame = Math.round(currentTime * frameRate);
|
|
31
|
+
const totalFrames = Math.round(duration * frameRate);
|
|
32
|
+
|
|
33
|
+
// Format time as MM:SS:FF or frame number
|
|
34
|
+
const formatTime = (time: number): string => {
|
|
35
|
+
if (showFrames) {
|
|
36
|
+
return `Frame ${Math.round(time * frameRate)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const totalSeconds = Math.floor(time);
|
|
40
|
+
const frames = Math.round((time - totalSeconds) * frameRate);
|
|
41
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
42
|
+
const seconds = totalSeconds % 60;
|
|
43
|
+
|
|
44
|
+
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${frames.toString().padStart(2, "0")}`;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Convert mouse position to time
|
|
48
|
+
const getTimeFromMouseEvent = (e: React.MouseEvent | MouseEvent): number => {
|
|
49
|
+
if (!containerRef.current) return 0;
|
|
50
|
+
|
|
51
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
52
|
+
const x = e.clientX - rect.left;
|
|
53
|
+
const progress = Math.max(0, Math.min(1, x / rect.width));
|
|
54
|
+
return progress * duration;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Handle mouse down to start dragging
|
|
58
|
+
const handleMouseDown = (e: React.MouseEvent) => {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
setIsDragging(true);
|
|
61
|
+
const time = getTimeFromMouseEvent(e);
|
|
62
|
+
onSeek(time);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Handle mouse move for dragging and hover
|
|
66
|
+
const handleMouseMove = (e: React.MouseEvent) => {
|
|
67
|
+
const time = getTimeFromMouseEvent(e);
|
|
68
|
+
setHoverTime(time);
|
|
69
|
+
|
|
70
|
+
if (isDragging) {
|
|
71
|
+
onSeek(time);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleMouseLeave = () => {
|
|
76
|
+
setHoverTime(null);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Handle global mouse events while dragging
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!isDragging) return;
|
|
82
|
+
|
|
83
|
+
const handleGlobalMouseMove = (e: MouseEvent) => {
|
|
84
|
+
if (!containerRef.current) return;
|
|
85
|
+
const time = getTimeFromMouseEvent(e);
|
|
86
|
+
onSeek(time);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const handleGlobalMouseUp = () => {
|
|
90
|
+
setIsDragging(false);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
document.addEventListener("mousemove", handleGlobalMouseMove);
|
|
94
|
+
document.addEventListener("mouseup", handleGlobalMouseUp);
|
|
95
|
+
|
|
96
|
+
return () => {
|
|
97
|
+
document.removeEventListener("mousemove", handleGlobalMouseMove);
|
|
98
|
+
document.removeEventListener("mouseup", handleGlobalMouseUp);
|
|
99
|
+
};
|
|
100
|
+
}, [isDragging, onSeek, duration]);
|
|
101
|
+
|
|
102
|
+
// Keyboard shortcuts for frame navigation
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
105
|
+
const frameDuration = 1 / frameRate;
|
|
106
|
+
|
|
107
|
+
switch (e.key) {
|
|
108
|
+
case "ArrowLeft":
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
onSeek(Math.max(0, currentTime - frameDuration));
|
|
111
|
+
break;
|
|
112
|
+
case "ArrowRight":
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
onSeek(Math.min(duration, currentTime + frameDuration));
|
|
115
|
+
break;
|
|
116
|
+
case "Home":
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
onSeek(0);
|
|
119
|
+
break;
|
|
120
|
+
case "End":
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
onSeek(duration);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
128
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
129
|
+
}, [currentTime, duration, frameRate, onSeek]);
|
|
130
|
+
|
|
131
|
+
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
|
|
132
|
+
const hoverProgress = hoverTime !== null && duration > 0 ? (hoverTime / duration) * 100 : null;
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
style={{
|
|
137
|
+
width: "100%",
|
|
138
|
+
padding: "16px",
|
|
139
|
+
background: "#0f172a",
|
|
140
|
+
borderRadius: "8px",
|
|
141
|
+
border: "1px solid #334155",
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
{/* Timeline header */}
|
|
145
|
+
<div
|
|
146
|
+
style={{
|
|
147
|
+
display: "flex",
|
|
148
|
+
justifyContent: "space-between",
|
|
149
|
+
marginBottom: "12px",
|
|
150
|
+
fontSize: "14px",
|
|
151
|
+
color: "#94a3b8",
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<span>{formatTime(currentTime)}</span>
|
|
155
|
+
<span>{formatTime(duration)}</span>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* Timeline track */}
|
|
159
|
+
<div
|
|
160
|
+
ref={containerRef}
|
|
161
|
+
onMouseDown={handleMouseDown}
|
|
162
|
+
onMouseMove={handleMouseMove}
|
|
163
|
+
onMouseLeave={handleMouseLeave}
|
|
164
|
+
style={{
|
|
165
|
+
position: "relative",
|
|
166
|
+
height: `${height}px`,
|
|
167
|
+
background: "#1e293b",
|
|
168
|
+
borderRadius: "6px",
|
|
169
|
+
cursor: isDragging ? "grabbing" : "grab",
|
|
170
|
+
overflow: "hidden",
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
{/* Progress bar */}
|
|
174
|
+
<div
|
|
175
|
+
style={{
|
|
176
|
+
position: "absolute",
|
|
177
|
+
left: 0,
|
|
178
|
+
top: 0,
|
|
179
|
+
bottom: 0,
|
|
180
|
+
width: `${progress}%`,
|
|
181
|
+
background: "linear-gradient(90deg, #3b82f6 0%, #2563eb 100%)",
|
|
182
|
+
transition: isDragging ? "none" : "width 0.1s ease-out",
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
|
|
186
|
+
{/* Hover indicator */}
|
|
187
|
+
{hoverProgress !== null && !isDragging && (
|
|
188
|
+
<div
|
|
189
|
+
style={{
|
|
190
|
+
position: "absolute",
|
|
191
|
+
left: `${hoverProgress}%`,
|
|
192
|
+
top: 0,
|
|
193
|
+
bottom: 0,
|
|
194
|
+
width: "2px",
|
|
195
|
+
background: "#f59e0b",
|
|
196
|
+
pointerEvents: "none",
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
<div
|
|
200
|
+
style={{
|
|
201
|
+
position: "absolute",
|
|
202
|
+
bottom: "100%",
|
|
203
|
+
left: "50%",
|
|
204
|
+
transform: "translateX(-50%)",
|
|
205
|
+
padding: "4px 8px",
|
|
206
|
+
background: "#f59e0b",
|
|
207
|
+
color: "#000",
|
|
208
|
+
fontSize: "12px",
|
|
209
|
+
fontWeight: 600,
|
|
210
|
+
borderRadius: "4px",
|
|
211
|
+
marginBottom: "4px",
|
|
212
|
+
whiteSpace: "nowrap",
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
{hoverTime !== null && formatTime(hoverTime)}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Playhead */}
|
|
221
|
+
<div
|
|
222
|
+
style={{
|
|
223
|
+
position: "absolute",
|
|
224
|
+
left: `${progress}%`,
|
|
225
|
+
top: 0,
|
|
226
|
+
bottom: 0,
|
|
227
|
+
width: "3px",
|
|
228
|
+
background: "#ef4444",
|
|
229
|
+
transform: "translateX(-50%)",
|
|
230
|
+
pointerEvents: "none",
|
|
231
|
+
}}
|
|
232
|
+
>
|
|
233
|
+
{/* Playhead handle */}
|
|
234
|
+
<div
|
|
235
|
+
style={{
|
|
236
|
+
position: "absolute",
|
|
237
|
+
top: "50%",
|
|
238
|
+
left: "50%",
|
|
239
|
+
transform: "translate(-50%, -50%)",
|
|
240
|
+
width: "16px",
|
|
241
|
+
height: "16px",
|
|
242
|
+
background: "#ef4444",
|
|
243
|
+
border: "2px solid #fff",
|
|
244
|
+
borderRadius: "50%",
|
|
245
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
|
|
246
|
+
}}
|
|
247
|
+
/>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{/* Frame markers */}
|
|
251
|
+
<div
|
|
252
|
+
style={{
|
|
253
|
+
position: "absolute",
|
|
254
|
+
left: 0,
|
|
255
|
+
right: 0,
|
|
256
|
+
bottom: 0,
|
|
257
|
+
height: "20px",
|
|
258
|
+
display: "flex",
|
|
259
|
+
alignItems: "flex-end",
|
|
260
|
+
pointerEvents: "none",
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
{Array.from({ length: 11 }, (_, i) => i * 10).map((percent) => (
|
|
264
|
+
<div
|
|
265
|
+
key={percent}
|
|
266
|
+
style={{
|
|
267
|
+
position: "absolute",
|
|
268
|
+
left: `${percent}%`,
|
|
269
|
+
height: percent % 20 === 0 ? "12px" : "6px",
|
|
270
|
+
width: "1px",
|
|
271
|
+
background: "#475569",
|
|
272
|
+
}}
|
|
273
|
+
/>
|
|
274
|
+
))}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
{/* Timeline footer */}
|
|
279
|
+
<div
|
|
280
|
+
style={{
|
|
281
|
+
marginTop: "8px",
|
|
282
|
+
fontSize: "12px",
|
|
283
|
+
color: "#64748b",
|
|
284
|
+
textAlign: "center",
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
{showFrames ? `Frame ${currentFrame} / ${totalFrames}` : `${frameRate} FPS • Use ← → arrows for frame navigation`}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|