create-aura3d 1.3.1 → 1.3.3
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/dist/cli.js +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/package.json +11 -3
- package/templates/animation-channel/package.json +1 -1
- package/templates/animation-channel/tests/screenshot.spec.ts +16 -0
- package/templates/animation-studio/dist/episodes/prompt-to-scene-proof/two-robots-fixing-a-car.document.json +680 -0
- package/templates/animation-studio/dist/episodes/scene/episode-3d.webm +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/action.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/dialogue.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/final.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/first.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/mouth-closed.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/mouth-open.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/render-live-summary.json +2021 -2542
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/broken.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/chefs.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/customer.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/friend-1.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/friend-2.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-1.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-2.png +0 -0
- package/templates/animation-studio/dist/scene/working.document.json +219 -269
- package/templates/animation-studio/index.html +12 -0
- package/templates/animation-studio/node_modules.publish-aside/.vite/deps/_metadata.json +8 -0
- package/templates/animation-studio/node_modules.publish-aside/.vite/deps/package.json +3 -0
- package/templates/animation-studio/package-lock.json +1307 -55
- package/templates/animation-studio/package.json +11 -5
- package/templates/animation-studio/scripts/asset-motion-probe.ts +7 -1
- package/templates/animation-studio/scripts/render-live.ts +24 -0
- package/templates/animation-studio/src/director/prompt-to-scene.ts +73 -5
- package/templates/animation-studio/studio/dist/assets/index-C7f8tHfi.css +1 -0
- package/templates/animation-studio/studio/dist/assets/index-CLo3GXyN.js +43 -0
- package/templates/animation-studio/studio/dist/index.html +19 -0
- package/templates/animation-studio/studio/index.html +18 -0
- package/templates/animation-studio/studio/src/App.tsx +498 -0
- package/templates/animation-studio/studio/src/components/Console.tsx +372 -0
- package/templates/animation-studio/studio/src/components/Icon.tsx +90 -0
- package/templates/animation-studio/studio/src/components/Inspector.tsx +265 -0
- package/templates/animation-studio/studio/src/components/Outliner.tsx +210 -0
- package/templates/animation-studio/studio/src/components/Palette.tsx +204 -0
- package/templates/animation-studio/studio/src/components/Stage.tsx +344 -0
- package/templates/animation-studio/studio/src/components/Timeline.tsx +230 -0
- package/templates/animation-studio/studio/src/components/Topbar.tsx +66 -0
- package/templates/animation-studio/studio/src/main.tsx +10 -0
- package/templates/animation-studio/studio/src/state/backend.ts +108 -0
- package/templates/animation-studio/studio/src/state/fidelity.ts +110 -0
- package/templates/animation-studio/studio/src/state/mapDocument.ts +430 -0
- package/templates/animation-studio/studio/src/state/sceneTool.ts +66 -0
- package/templates/animation-studio/studio/src/state/types.ts +173 -0
- package/templates/animation-studio/studio/src/state/util.ts +46 -0
- package/templates/animation-studio/studio/src/styles.css +528 -0
- package/templates/animation-studio/studio/vite.config.ts +175 -0
- package/templates/animation-studio/tests/route-health.spec.ts +33 -0
- package/templates/animation-studio/tests/screenshot.spec.ts +30 -0
- package/templates/character-controller/package.json +3 -3
- package/templates/character-controller/src/main.ts +18 -1
- package/templates/cinematic-scene/package.json +1 -1
- package/templates/cinematic-scene/tests/screenshot.spec.ts +3 -1
- package/templates/episode-builder/package.json +1 -1
- package/templates/episode-builder/tests/screenshot.spec.ts +15 -0
- package/templates/fighting-game/package.json +1 -1
- package/templates/fighting-game/tests/screenshot.spec.ts +15 -0
- package/templates/mini-game/package.json +1 -1
- package/templates/product-viewer/package.json +1 -1
- package/templates/product-viewer/tests/screenshot.spec.ts +3 -1
- package/templates/prompt-animation-channel/package.json +1 -1
- package/templates/prompt-animation-channel/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-architecture-interior/index.html +16 -0
- package/templates/three-compat-architecture-interior/package.json +21 -0
- package/templates/three-compat-architecture-interior/playwright.config.ts +14 -0
- package/templates/three-compat-architecture-interior/src/main.ts +27 -0
- package/templates/three-compat-architecture-interior/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-architecture-interior/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-architecture-interior/tsconfig.json +19 -0
- package/templates/three-compat-asset-inspector/index.html +16 -0
- package/templates/three-compat-asset-inspector/package.json +21 -0
- package/templates/three-compat-asset-inspector/playwright.config.ts +14 -0
- package/templates/three-compat-asset-inspector/src/main.ts +17 -0
- package/templates/three-compat-asset-inspector/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-asset-inspector/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-asset-inspector/tsconfig.json +19 -0
- package/templates/three-compat-character-viewer/index.html +16 -0
- package/templates/three-compat-character-viewer/package.json +21 -0
- package/templates/three-compat-character-viewer/playwright.config.ts +14 -0
- package/templates/three-compat-character-viewer/src/main.ts +17 -0
- package/templates/three-compat-character-viewer/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-character-viewer/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-character-viewer/tsconfig.json +19 -0
- package/templates/three-compat-custom-threejs-migration/index.html +16 -0
- package/templates/three-compat-custom-threejs-migration/package.json +21 -0
- package/templates/three-compat-custom-threejs-migration/playwright.config.ts +14 -0
- package/templates/three-compat-custom-threejs-migration/src/main.ts +16 -0
- package/templates/three-compat-custom-threejs-migration/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-custom-threejs-migration/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-custom-threejs-migration/tsconfig.json +19 -0
- package/templates/three-compat-large-scene/index.html +16 -0
- package/templates/three-compat-large-scene/package.json +21 -0
- package/templates/three-compat-large-scene/playwright.config.ts +14 -0
- package/templates/three-compat-large-scene/src/main.ts +31 -0
- package/templates/three-compat-large-scene/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-large-scene/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-large-scene/tsconfig.json +19 -0
- package/templates/three-compat-material-authoring/index.html +16 -0
- package/templates/three-compat-material-authoring/package.json +21 -0
- package/templates/three-compat-material-authoring/playwright.config.ts +14 -0
- package/templates/three-compat-material-authoring/src/main.ts +33 -0
- package/templates/three-compat-material-authoring/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-material-authoring/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-material-authoring/tsconfig.json +19 -0
- package/templates/three-compat-postprocess-scene/index.html +16 -0
- package/templates/three-compat-postprocess-scene/package.json +21 -0
- package/templates/three-compat-postprocess-scene/playwright.config.ts +14 -0
- package/templates/three-compat-postprocess-scene/src/main.ts +17 -0
- package/templates/three-compat-postprocess-scene/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-postprocess-scene/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-postprocess-scene/tsconfig.json +19 -0
- package/templates/three-compat-premium-product-viewer/index.html +16 -0
- package/templates/three-compat-premium-product-viewer/package.json +21 -0
- package/templates/three-compat-premium-product-viewer/playwright.config.ts +14 -0
- package/templates/three-compat-premium-product-viewer/src/main.ts +18 -0
- package/templates/three-compat-premium-product-viewer/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-premium-product-viewer/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-premium-product-viewer/tsconfig.json +19 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/* Inspector / Properties — context panel for the current selection. */
|
|
2
|
+
import { Fragment, type ReactNode } from "react";
|
|
3
|
+
import { Icon } from "./Icon";
|
|
4
|
+
import { cap, fmt, shade } from "../state/util";
|
|
5
|
+
import type { EpisodeDocument, Selection } from "../state/types";
|
|
6
|
+
|
|
7
|
+
const STANDARD_CLIP_IDS = [
|
|
8
|
+
"idle", "talk", "gesture", "point", "nod", "wave",
|
|
9
|
+
"walk", "run", "react",
|
|
10
|
+
"sit", "shrug", "cross_arms", "salute", "shake_head"
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function Field({
|
|
14
|
+
label,
|
|
15
|
+
icon,
|
|
16
|
+
value,
|
|
17
|
+
mono,
|
|
18
|
+
act,
|
|
19
|
+
chev
|
|
20
|
+
}: {
|
|
21
|
+
label: string;
|
|
22
|
+
icon?: string;
|
|
23
|
+
value: ReactNode;
|
|
24
|
+
mono?: boolean;
|
|
25
|
+
act?: boolean;
|
|
26
|
+
chev?: boolean;
|
|
27
|
+
}) {
|
|
28
|
+
return (
|
|
29
|
+
<div className={"field" + (act ? " act" : "")}>
|
|
30
|
+
<div className="fl">
|
|
31
|
+
{icon && <Icon name={icon} size={11} />}
|
|
32
|
+
{label}
|
|
33
|
+
</div>
|
|
34
|
+
<div className={"fv" + (mono ? " mono" : "")}>
|
|
35
|
+
{value}
|
|
36
|
+
{chev && (
|
|
37
|
+
<span className="chev">
|
|
38
|
+
<Icon name="chevD" size={13} />
|
|
39
|
+
</span>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function Sec({ children }: { children: ReactNode }) {
|
|
47
|
+
return (
|
|
48
|
+
<div className="insp-sec">
|
|
49
|
+
{children}
|
|
50
|
+
<span className="ln" />
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface InspectorProps {
|
|
56
|
+
data: EpisodeDocument;
|
|
57
|
+
sel: Selection | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function Inspector({ data, sel }: InspectorProps) {
|
|
61
|
+
if (!sel) {
|
|
62
|
+
return (
|
|
63
|
+
<section className="panel insp">
|
|
64
|
+
<div className="panel-h">
|
|
65
|
+
<Icon name="sliders" size={15} style={{ color: "var(--tx-dim)" }} />
|
|
66
|
+
<span className="ttl">Inspector</span>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="insp-empty">
|
|
69
|
+
<Icon name="sliders" size={26} style={{ color: "var(--tx-faint)", opacity: 0.6 }} />
|
|
70
|
+
<div>Nothing selected.</div>
|
|
71
|
+
<div>Select a shot, character, set, or prop — or describe a scene in the Director Console.</div>
|
|
72
|
+
</div>
|
|
73
|
+
</section>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const typeLabel = ({ shot: "Shot", cast: "Character", set: "Set", prop: "Prop" } as Record<string, string>)[sel.type] || "—";
|
|
77
|
+
let body: ReactNode = null;
|
|
78
|
+
|
|
79
|
+
if (sel.type === "shot") {
|
|
80
|
+
const s = data.shots.find((x) => x.id === sel.id) || data.shots[0];
|
|
81
|
+
const idx = data.shots.indexOf(s) + 1;
|
|
82
|
+
const beats = data.beats.filter((b) => b.shot === s.id);
|
|
83
|
+
body = (
|
|
84
|
+
<Fragment>
|
|
85
|
+
<div className="insp-hero" style={{ backgroundImage: "url(" + s.frame + ")" }}>
|
|
86
|
+
<div className="ov" />
|
|
87
|
+
<div className="tag">{"SHOT " + String(idx).padStart(2, "0")}</div>
|
|
88
|
+
<div className="cap">
|
|
89
|
+
<div className="nm">{s.name}</div>
|
|
90
|
+
<div className="sub">{s.cam}</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="prop-grid">
|
|
94
|
+
<Field label="Duration" icon="history" value={s.dur + "s"} mono />
|
|
95
|
+
<Field label="Lens" icon="camera" value={s.cam.split("·")[1] || s.cam} mono />
|
|
96
|
+
<Field label="In" value={fmt(s.start)} mono />
|
|
97
|
+
<Field label="Out" value={fmt(s.start + s.dur)} mono />
|
|
98
|
+
</div>
|
|
99
|
+
<div className="prop-grid one">
|
|
100
|
+
<Field label="Framing" icon="frame" value={s.cam.split("·")[0]} />
|
|
101
|
+
</div>
|
|
102
|
+
<Sec>Cast in shot</Sec>
|
|
103
|
+
<div className="castrow">
|
|
104
|
+
{s.who.map((id) => {
|
|
105
|
+
const c = data.cast.find((x) => x.id === id);
|
|
106
|
+
if (!c) return null;
|
|
107
|
+
return (
|
|
108
|
+
<div key={id} className="castchip">
|
|
109
|
+
<span className="d" style={{ background: c.color }}>
|
|
110
|
+
{c.glyph}
|
|
111
|
+
</span>
|
|
112
|
+
{c.name}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
})}
|
|
116
|
+
</div>
|
|
117
|
+
<Sec>{beats.length + " beat" + (beats.length === 1 ? "" : "s") + " · director plan"}</Sec>
|
|
118
|
+
{beats.length ? (
|
|
119
|
+
<div className="beat-plan">
|
|
120
|
+
{beats.map((b) => {
|
|
121
|
+
const speaker = data.cast.find((x) => x.id === b.who);
|
|
122
|
+
const listener = data.cast.find((x) => x.id === b.listener);
|
|
123
|
+
return (
|
|
124
|
+
<div key={b.id} className="beat-row" data-testid="director-beat">
|
|
125
|
+
<div className="beat-line">“{cap(b.text)}”</div>
|
|
126
|
+
<div className="beat-intents">
|
|
127
|
+
<span className="beat-chip cam" title="camera framing">{b.camera}</span>
|
|
128
|
+
<span className="beat-chip spk" title="speaker action">
|
|
129
|
+
{(speaker?.name ?? b.who)} · {b.speakingIntent}
|
|
130
|
+
</span>
|
|
131
|
+
{b.listener && (
|
|
132
|
+
<span className="beat-chip lst" title="listener reaction">
|
|
133
|
+
{(listener?.name ?? b.listener)} · {b.listenerIntent}
|
|
134
|
+
</span>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
})}
|
|
140
|
+
</div>
|
|
141
|
+
) : (
|
|
142
|
+
<div className="insp-note">No dialogue beats yet — direct one from the console.</div>
|
|
143
|
+
)}
|
|
144
|
+
</Fragment>
|
|
145
|
+
);
|
|
146
|
+
} else if (sel.type === "cast") {
|
|
147
|
+
const c = data.cast.find((x) => x.id === sel.id) || data.cast[0];
|
|
148
|
+
const appears = data.shots.filter((s) => s.who.includes(c.id)).length;
|
|
149
|
+
body = (
|
|
150
|
+
<Fragment>
|
|
151
|
+
<div className="insp-avatar" style={{ background: "linear-gradient(150deg," + c.color + "," + shade(c.color) + ")" }}>
|
|
152
|
+
{c.glyph}
|
|
153
|
+
</div>
|
|
154
|
+
<div className="insp-name">{c.name}</div>
|
|
155
|
+
<div className="insp-sub">{c.kind}</div>
|
|
156
|
+
<div className="prop-grid">
|
|
157
|
+
<Field label="Lines" icon="mic" value={c.lines || 0} mono />
|
|
158
|
+
<Field label="In shots" icon="film" value={appears} mono />
|
|
159
|
+
<Field
|
|
160
|
+
label="Accent"
|
|
161
|
+
value={
|
|
162
|
+
<span style={{ display: "flex", alignItems: "center", gap: 7 }}>
|
|
163
|
+
<span style={{ width: 13, height: 13, borderRadius: 4, background: c.color }} />
|
|
164
|
+
{c.color}
|
|
165
|
+
</span>
|
|
166
|
+
}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
<div className="prop-grid one">
|
|
170
|
+
<Field
|
|
171
|
+
label="Default clip"
|
|
172
|
+
icon="film"
|
|
173
|
+
value={
|
|
174
|
+
<select
|
|
175
|
+
disabled
|
|
176
|
+
value={c.defaultClip ?? "idle"}
|
|
177
|
+
style={{
|
|
178
|
+
width: "100%",
|
|
179
|
+
background: "transparent",
|
|
180
|
+
color: "inherit",
|
|
181
|
+
border: "none",
|
|
182
|
+
fontFamily: "var(--mono)",
|
|
183
|
+
fontSize: 12,
|
|
184
|
+
cursor: "not-allowed"
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
{STANDARD_CLIP_IDS.map((id) => (
|
|
188
|
+
<option key={id} value={id}>{id}</option>
|
|
189
|
+
))}
|
|
190
|
+
</select>
|
|
191
|
+
}
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
<div className="prop-grid">
|
|
195
|
+
<Field label="Mouth morph" icon="mic" value={c.mouthMorphIndex ?? -1} mono />
|
|
196
|
+
<Field
|
|
197
|
+
label="Foot IK"
|
|
198
|
+
icon="zap"
|
|
199
|
+
value={
|
|
200
|
+
<input
|
|
201
|
+
type="checkbox"
|
|
202
|
+
disabled
|
|
203
|
+
checked={!!c.footIk}
|
|
204
|
+
style={{ cursor: "not-allowed" }}
|
|
205
|
+
/>
|
|
206
|
+
}
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
<div className="prop-grid one">
|
|
210
|
+
<Field
|
|
211
|
+
label="Grade intent"
|
|
212
|
+
value={
|
|
213
|
+
<span className="beat-chip cam" style={{ fontSize: 10 }}>
|
|
214
|
+
{c.gradeAwareIntent ?? "—"}
|
|
215
|
+
</span>
|
|
216
|
+
}
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
<div className="insp-note">
|
|
220
|
+
{c.name + " is rigged and assignable to any beat. Type \"cast\" commands in the console to re-pose or re-cast."}
|
|
221
|
+
</div>
|
|
222
|
+
</Fragment>
|
|
223
|
+
);
|
|
224
|
+
} else {
|
|
225
|
+
const list = sel.type === "set" ? data.sets : data.props;
|
|
226
|
+
const o = list.find((x) => x.id === sel.id) || list[0];
|
|
227
|
+
body = (
|
|
228
|
+
<Fragment>
|
|
229
|
+
<div className="insp-avatar" style={{ background: "linear-gradient(150deg,#2b3350,#1a2032)", fontSize: 22 }}>
|
|
230
|
+
<Icon name={o.icon} size={26} style={{ color: "var(--acc)" }} />
|
|
231
|
+
</div>
|
|
232
|
+
<div className="insp-name">{o.name}</div>
|
|
233
|
+
<div className="insp-sub">{typeLabel + " · " + o.meta}</div>
|
|
234
|
+
<div className="prop-grid">
|
|
235
|
+
<Field label="Type" value={o.meta} mono />
|
|
236
|
+
</div>
|
|
237
|
+
<div className="insp-note">Linked into the working document. Re-light or swap from the console with “set …”.</div>
|
|
238
|
+
</Fragment>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<section className="panel insp">
|
|
244
|
+
<div className="panel-h">
|
|
245
|
+
<Icon name="sliders" size={15} style={{ color: "var(--tx-dim)" }} />
|
|
246
|
+
<span className="ttl">Inspector</span>
|
|
247
|
+
<span className="sp" />
|
|
248
|
+
<span
|
|
249
|
+
style={{
|
|
250
|
+
fontFamily: "var(--mono)",
|
|
251
|
+
fontSize: 10,
|
|
252
|
+
color: "var(--acc)",
|
|
253
|
+
background: "rgba(123,123,255,.12)",
|
|
254
|
+
border: "1px solid rgba(123,123,255,.25)",
|
|
255
|
+
padding: "2px 7px",
|
|
256
|
+
borderRadius: 6
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
{typeLabel}
|
|
260
|
+
</span>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="insp-scroll">{body}</div>
|
|
263
|
+
</section>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/* Scene Outliner — collapsible Shots / Cast / Sets / Props with counts + visibility. */
|
|
2
|
+
import { useState, type ReactNode } from "react";
|
|
3
|
+
import { Icon } from "./Icon";
|
|
4
|
+
import type { CastMember, CastSource, EpisodeDocument, PropEntity, Selection, SelectionType, SetEntity } from "../state/types";
|
|
5
|
+
import { fidelityLabel, type CharacterFidelity } from "../state/fidelity";
|
|
6
|
+
|
|
7
|
+
interface RowProps {
|
|
8
|
+
name: string;
|
|
9
|
+
sel: boolean;
|
|
10
|
+
onSelect: () => void;
|
|
11
|
+
hidden: boolean;
|
|
12
|
+
onToggle: () => void;
|
|
13
|
+
meta?: string;
|
|
14
|
+
accent?: string;
|
|
15
|
+
glyph?: string;
|
|
16
|
+
icon?: string;
|
|
17
|
+
/** Provenance badge (Phase E3): authored-fallback vs catalog-resolved vs user-uploaded. */
|
|
18
|
+
source?: CastSource;
|
|
19
|
+
sourceLabel?: string;
|
|
20
|
+
/** M7 — fidelity tier badge (A/B/C); C renders as "Previz". */
|
|
21
|
+
fidelity?: CharacterFidelity;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Short, honest badge text + class per cast provenance class (E3). */
|
|
25
|
+
const SOURCE_BADGE: Record<CastSource, { text: string; cls: string }> = {
|
|
26
|
+
"authored-fallback": { text: "fallback", cls: "src-fallback" },
|
|
27
|
+
"catalog-resolved": { text: "catalog", cls: "src-catalog" },
|
|
28
|
+
"user-uploaded": { text: "uploaded", cls: "src-uploaded" }
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function Row({ name, sel, onSelect, hidden, onToggle, meta, accent, glyph, icon, source, sourceLabel, fidelity }: RowProps) {
|
|
32
|
+
const badge = source ? SOURCE_BADGE[source] : undefined;
|
|
33
|
+
return (
|
|
34
|
+
<div className={"ol-item" + (sel ? " sel" : "")} onClick={onSelect}>
|
|
35
|
+
{glyph ? (
|
|
36
|
+
<div className="av" style={{ background: accent }}>
|
|
37
|
+
{glyph}
|
|
38
|
+
</div>
|
|
39
|
+
) : (
|
|
40
|
+
<div className="ic">
|
|
41
|
+
<Icon name={icon || "cube"} size={13} />
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
<span className="nm">{name}</span>
|
|
45
|
+
{fidelity && (
|
|
46
|
+
<span
|
|
47
|
+
className={"fid-badge fid-" + fidelity.grade.toLowerCase() + (fidelity.previz ? " fid-previz" : "")}
|
|
48
|
+
title={fidelity.reason}
|
|
49
|
+
data-grade={fidelity.grade}
|
|
50
|
+
data-previz={fidelity.previz ? "true" : "false"}
|
|
51
|
+
>
|
|
52
|
+
{fidelity.previz ? "Previz" : fidelityLabel(fidelity.grade)}
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
55
|
+
{badge && (
|
|
56
|
+
<span className={"src-badge " + badge.cls} title={sourceLabel ?? badge.text} data-source={source}>
|
|
57
|
+
{badge.text}
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
{meta && <span className="meta">{meta}</span>}
|
|
61
|
+
<button
|
|
62
|
+
className={"vis" + (hidden ? " off" : "")}
|
|
63
|
+
title={hidden ? "Show" : "Hide"}
|
|
64
|
+
onClick={(e) => {
|
|
65
|
+
e.stopPropagation();
|
|
66
|
+
onToggle();
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
<Icon name={hidden ? "eyeOff" : "eye"} size={14} />
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function Group({
|
|
76
|
+
title,
|
|
77
|
+
count,
|
|
78
|
+
children,
|
|
79
|
+
onAdd
|
|
80
|
+
}: {
|
|
81
|
+
title: string;
|
|
82
|
+
count: number;
|
|
83
|
+
children: ReactNode;
|
|
84
|
+
onAdd?: () => void;
|
|
85
|
+
}) {
|
|
86
|
+
const [open, setOpen] = useState(true);
|
|
87
|
+
return (
|
|
88
|
+
<div className="ol-group">
|
|
89
|
+
<div className={"ol-gh" + (open ? "" : " closed")} onClick={() => setOpen(!open)}>
|
|
90
|
+
<span className="tw">
|
|
91
|
+
<Icon name="chevD" size={13} />
|
|
92
|
+
</span>
|
|
93
|
+
{title}
|
|
94
|
+
<span className="cnt">{count}</span>
|
|
95
|
+
{onAdd && (
|
|
96
|
+
<button
|
|
97
|
+
className="add"
|
|
98
|
+
onClick={(e) => {
|
|
99
|
+
e.stopPropagation();
|
|
100
|
+
onAdd();
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<Icon name="plus" size={13} />
|
|
104
|
+
</button>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
{open && <div>{children}</div>}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface OutlinerProps {
|
|
113
|
+
data: EpisodeDocument;
|
|
114
|
+
sel: Selection | null;
|
|
115
|
+
onSelect: (type: SelectionType, id: string) => void;
|
|
116
|
+
hidden: Set<string>;
|
|
117
|
+
onToggle: (id: string) => void;
|
|
118
|
+
onAddCast: () => void;
|
|
119
|
+
onOpenPalette: () => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function Outliner({ data, sel, onSelect, hidden, onToggle, onAddCast, onOpenPalette }: OutlinerProps) {
|
|
123
|
+
return (
|
|
124
|
+
<section className="panel col">
|
|
125
|
+
<div className="ol-search" onClick={onOpenPalette} style={{ cursor: "text" }}>
|
|
126
|
+
<Icon name="search" size={14} />
|
|
127
|
+
<input placeholder="Search scene…" readOnly style={{ cursor: "text" }} />
|
|
128
|
+
<kbd>⌘K</kbd>
|
|
129
|
+
</div>
|
|
130
|
+
<div className="ol-scroll">
|
|
131
|
+
{/* M7 — scene fidelity tier. A grade-C scene is labeled "previz" and never sold as finished. */}
|
|
132
|
+
<div
|
|
133
|
+
className={"scene-fidelity fid-" + data.fidelity.grade.toLowerCase() + (data.fidelity.previz ? " fid-previz" : "")}
|
|
134
|
+
title={data.fidelity.reason}
|
|
135
|
+
data-grade={data.fidelity.grade}
|
|
136
|
+
data-previz={data.fidelity.previz ? "true" : "false"}
|
|
137
|
+
>
|
|
138
|
+
<span className="fid-tier">{data.fidelity.previz ? "Previz" : fidelityLabel(data.fidelity.grade)}</span>
|
|
139
|
+
<span className="fid-note">
|
|
140
|
+
{data.fidelity.previz ? "preview-quality — not a finished render" : "scene fidelity"}
|
|
141
|
+
</span>
|
|
142
|
+
</div>
|
|
143
|
+
<Group title="Shots" count={data.shots.length}>
|
|
144
|
+
{data.shots.map((s) => (
|
|
145
|
+
<div
|
|
146
|
+
key={s.id}
|
|
147
|
+
className={"ol-item" + (sel?.type === "shot" && sel.id === s.id ? " sel" : "")}
|
|
148
|
+
onClick={() => onSelect("shot", s.id)}
|
|
149
|
+
>
|
|
150
|
+
<div className="ic" style={{ background: s.color + "33", color: s.color }}>
|
|
151
|
+
<Icon name="film" size={13} />
|
|
152
|
+
</div>
|
|
153
|
+
<span className="nm">{s.name}</span>
|
|
154
|
+
<span className="meta">{s.dur + "s"}</span>
|
|
155
|
+
</div>
|
|
156
|
+
))}
|
|
157
|
+
</Group>
|
|
158
|
+
|
|
159
|
+
<Group title="Cast" count={data.cast.length} onAdd={onAddCast}>
|
|
160
|
+
{data.cast.map((c: CastMember) => (
|
|
161
|
+
<Row
|
|
162
|
+
key={c.id}
|
|
163
|
+
name={c.name}
|
|
164
|
+
glyph={c.glyph}
|
|
165
|
+
accent={c.color}
|
|
166
|
+
meta={c.lines ? c.lines + " lines" : "—"}
|
|
167
|
+
source={c.source}
|
|
168
|
+
sourceLabel={c.sourceLabel}
|
|
169
|
+
fidelity={c.fidelity}
|
|
170
|
+
sel={sel?.type === "cast" && sel.id === c.id}
|
|
171
|
+
onSelect={() => onSelect("cast", c.id)}
|
|
172
|
+
hidden={hidden.has(c.id)}
|
|
173
|
+
onToggle={() => onToggle(c.id)}
|
|
174
|
+
/>
|
|
175
|
+
))}
|
|
176
|
+
</Group>
|
|
177
|
+
|
|
178
|
+
<Group title="Sets" count={data.sets.length}>
|
|
179
|
+
{data.sets.map((s: SetEntity) => (
|
|
180
|
+
<Row
|
|
181
|
+
key={s.id}
|
|
182
|
+
name={s.name}
|
|
183
|
+
icon={s.icon}
|
|
184
|
+
meta={s.meta}
|
|
185
|
+
sel={sel?.type === "set" && sel.id === s.id}
|
|
186
|
+
onSelect={() => onSelect("set", s.id)}
|
|
187
|
+
hidden={hidden.has(s.id)}
|
|
188
|
+
onToggle={() => onToggle(s.id)}
|
|
189
|
+
/>
|
|
190
|
+
))}
|
|
191
|
+
</Group>
|
|
192
|
+
|
|
193
|
+
<Group title="Props" count={data.props.length}>
|
|
194
|
+
{data.props.map((p: PropEntity) => (
|
|
195
|
+
<Row
|
|
196
|
+
key={p.id}
|
|
197
|
+
name={p.name}
|
|
198
|
+
icon={p.icon}
|
|
199
|
+
meta={p.meta}
|
|
200
|
+
sel={sel?.type === "prop" && sel.id === p.id}
|
|
201
|
+
onSelect={() => onSelect("prop", p.id)}
|
|
202
|
+
hidden={hidden.has(p.id)}
|
|
203
|
+
onToggle={() => onToggle(p.id)}
|
|
204
|
+
/>
|
|
205
|
+
))}
|
|
206
|
+
</Group>
|
|
207
|
+
</div>
|
|
208
|
+
</section>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/* Command palette (⌘K) — jump to shots/cast, run Scene-Tool commands, switch views. */
|
|
2
|
+
import { useEffect, useMemo, useRef, useState, type MutableRefObject } from "react";
|
|
3
|
+
import { Icon } from "./Icon";
|
|
4
|
+
import { fmt } from "../state/util";
|
|
5
|
+
import type { ConsoleApi } from "./Console";
|
|
6
|
+
import type { EpisodeDocument, SelectionType, ViewMode } from "../state/types";
|
|
7
|
+
|
|
8
|
+
interface PaletteItem {
|
|
9
|
+
g: string;
|
|
10
|
+
nm: string;
|
|
11
|
+
ds?: string;
|
|
12
|
+
ic?: string;
|
|
13
|
+
av?: string;
|
|
14
|
+
glyph?: string;
|
|
15
|
+
tag?: string;
|
|
16
|
+
mono?: boolean;
|
|
17
|
+
kw?: string;
|
|
18
|
+
run: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PaletteProps {
|
|
22
|
+
open: boolean;
|
|
23
|
+
setOpen: (o: boolean) => void;
|
|
24
|
+
data: EpisodeDocument;
|
|
25
|
+
api: MutableRefObject<ConsoleApi>;
|
|
26
|
+
onJumpShot: (id: string) => void;
|
|
27
|
+
onSelectEntity: (type: SelectionType, id: string) => void;
|
|
28
|
+
onRender: (scope: "shot" | "sequence") => void;
|
|
29
|
+
onView: (m: ViewMode) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function Palette({ open, setOpen, data, api, onJumpShot, onSelectEntity, onRender, onView }: PaletteProps) {
|
|
33
|
+
const [q, setQ] = useState("");
|
|
34
|
+
const [idx, setIdx] = useState(0);
|
|
35
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
36
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (open) {
|
|
40
|
+
setQ("");
|
|
41
|
+
setIdx(0);
|
|
42
|
+
window.setTimeout(() => inputRef.current?.focus(), 30);
|
|
43
|
+
}
|
|
44
|
+
}, [open]);
|
|
45
|
+
|
|
46
|
+
const items = useMemo<PaletteItem[]>(() => {
|
|
47
|
+
const out: PaletteItem[] = [];
|
|
48
|
+
out.push({ g: "Actions", nm: "Render full sequence", ds: "Low-fi preview of all shots", ic: "play2", kw: "render export preview", run: () => onRender("sequence") });
|
|
49
|
+
out.push({ g: "Actions", nm: "Render current shot", ds: "Preview just the active shot", ic: "film", kw: "render shot preview", run: () => onRender("shot") });
|
|
50
|
+
(["Render", "Wireframe", "Storyboard"] as ViewMode[]).forEach((m) =>
|
|
51
|
+
out.push({
|
|
52
|
+
g: "Actions",
|
|
53
|
+
nm: "View: " + m,
|
|
54
|
+
ds: "Switch viewport mode",
|
|
55
|
+
ic: m === "Render" ? "film" : m === "Wireframe" ? "cube" : "grid",
|
|
56
|
+
kw: "view mode " + m,
|
|
57
|
+
run: () => onView(m)
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
data.shots.forEach((s) =>
|
|
61
|
+
out.push({ g: "Go to shot", nm: s.name, ds: s.cam, ic: "film", tag: fmt(s.start), kw: "shot " + s.name + " " + s.cam, run: () => onJumpShot(s.id) })
|
|
62
|
+
);
|
|
63
|
+
data.cast.forEach((c) =>
|
|
64
|
+
out.push({ g: "Cast", av: c.color, glyph: c.glyph, nm: c.name, ds: c.kind, kw: "cast character " + c.name, run: () => onSelectEntity("cast", c.id) })
|
|
65
|
+
);
|
|
66
|
+
(
|
|
67
|
+
[
|
|
68
|
+
["cast add Pip", "Cast a new character"],
|
|
69
|
+
["shot retime --id shot-2 --duration 30", "Retime a shot + ripple"],
|
|
70
|
+
["cam orbit", "Set active shot camera"],
|
|
71
|
+
["fx add rim light", "Add an effect to the timeline"],
|
|
72
|
+
["set station --hdr dawn", "Re-light the active set"],
|
|
73
|
+
["shot add --after shot-2", "Block a new shot"]
|
|
74
|
+
] as Array<[string, string]>
|
|
75
|
+
).forEach(([cmd, ds]) =>
|
|
76
|
+
out.push({ g: "Run command", nm: cmd, ds, mono: true, ic: "bolt", kw: "command " + cmd, run: () => api.current.run?.(cmd, true) })
|
|
77
|
+
);
|
|
78
|
+
return out;
|
|
79
|
+
}, [data, api, onJumpShot, onRender, onSelectEntity, onView]);
|
|
80
|
+
|
|
81
|
+
const filtered = useMemo(() => {
|
|
82
|
+
const s = q.trim().toLowerCase();
|
|
83
|
+
if (!s) return items;
|
|
84
|
+
return items.filter((it) => (it.nm + " " + (it.ds || "") + " " + (it.kw || "")).toLowerCase().includes(s));
|
|
85
|
+
}, [q, items]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
setIdx(0);
|
|
89
|
+
}, [q]);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const el = listRef.current;
|
|
92
|
+
if (!el) return;
|
|
93
|
+
const on = el.querySelector(".cmdk-item.on");
|
|
94
|
+
if (on) on.scrollIntoView({ block: "nearest" });
|
|
95
|
+
}, [idx]);
|
|
96
|
+
|
|
97
|
+
if (!open) return null;
|
|
98
|
+
|
|
99
|
+
const exec = (it?: PaletteItem) => {
|
|
100
|
+
if (!it) return;
|
|
101
|
+
setOpen(false);
|
|
102
|
+
window.setTimeout(() => it.run(), 10);
|
|
103
|
+
};
|
|
104
|
+
const onKey = (e: React.KeyboardEvent) => {
|
|
105
|
+
if (e.key === "Escape") {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
setOpen(false);
|
|
108
|
+
} else if (e.key === "ArrowDown") {
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
setIdx((i) => Math.min(filtered.length - 1, i + 1));
|
|
111
|
+
} else if (e.key === "ArrowUp") {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
setIdx((i) => Math.max(0, i - 1));
|
|
114
|
+
} else if (e.key === "Enter") {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
exec(filtered[idx]);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// group while keeping a flat index for keyboard navigation
|
|
121
|
+
let flat = -1;
|
|
122
|
+
let lastG: string | null = null;
|
|
123
|
+
const rows: Array<{ grp?: string; key: string; it?: PaletteItem; myIdx?: number }> = [];
|
|
124
|
+
filtered.forEach((it) => {
|
|
125
|
+
if (it.g !== lastG) {
|
|
126
|
+
rows.push({ grp: it.g, key: "g" + it.g });
|
|
127
|
+
lastG = it.g;
|
|
128
|
+
}
|
|
129
|
+
flat++;
|
|
130
|
+
rows.push({ it, myIdx: flat, key: it.g + it.nm });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div className="cmdk-ov" onMouseDown={() => setOpen(false)}>
|
|
135
|
+
<div className="cmdk" onMouseDown={(e) => e.stopPropagation()}>
|
|
136
|
+
<div className="cmdk-in">
|
|
137
|
+
<span className="si">
|
|
138
|
+
<Icon name="search" size={18} />
|
|
139
|
+
</span>
|
|
140
|
+
<input
|
|
141
|
+
ref={inputRef}
|
|
142
|
+
value={q}
|
|
143
|
+
placeholder="Search shots, cast, commands…"
|
|
144
|
+
onChange={(e) => setQ(e.target.value)}
|
|
145
|
+
onKeyDown={onKey}
|
|
146
|
+
/>
|
|
147
|
+
<span className="esc">ESC</span>
|
|
148
|
+
</div>
|
|
149
|
+
<div className="cmdk-list" ref={listRef}>
|
|
150
|
+
{filtered.length === 0 ? (
|
|
151
|
+
<div className="cmdk-empty">{"No matches for “" + q + "”"}</div>
|
|
152
|
+
) : (
|
|
153
|
+
rows.map((r) =>
|
|
154
|
+
r.grp ? (
|
|
155
|
+
<div className="cmdk-grp" key={r.key}>
|
|
156
|
+
{r.grp}
|
|
157
|
+
</div>
|
|
158
|
+
) : (
|
|
159
|
+
<div
|
|
160
|
+
key={r.key}
|
|
161
|
+
className={"cmdk-item" + (r.myIdx === idx ? " on" : "")}
|
|
162
|
+
onMouseEnter={() => setIdx(r.myIdx!)}
|
|
163
|
+
onClick={() => exec(r.it)}
|
|
164
|
+
>
|
|
165
|
+
{r.it!.av ? (
|
|
166
|
+
<div className="av" style={{ background: r.it!.av }}>
|
|
167
|
+
{r.it!.glyph}
|
|
168
|
+
</div>
|
|
169
|
+
) : (
|
|
170
|
+
<div className="ic">
|
|
171
|
+
<Icon name={r.it!.ic || "bolt"} size={15} />
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
<div className="tx">
|
|
175
|
+
<div className="nm">{r.it!.nm}</div>
|
|
176
|
+
{r.it!.ds && <div className={"ds" + (r.it!.mono ? " mono" : "")}>{r.it!.ds}</div>}
|
|
177
|
+
</div>
|
|
178
|
+
{r.it!.tag ? (
|
|
179
|
+
<span className="tag">{r.it!.tag}</span>
|
|
180
|
+
) : (
|
|
181
|
+
<span className="ent">
|
|
182
|
+
<Icon name="send" size={14} />
|
|
183
|
+
</span>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
<div className="cmdk-foot">
|
|
191
|
+
<span>
|
|
192
|
+
<span className="kk">↑↓</span>navigate
|
|
193
|
+
</span>
|
|
194
|
+
<span>
|
|
195
|
+
<span className="kk">↵</span>run
|
|
196
|
+
</span>
|
|
197
|
+
<span>
|
|
198
|
+
<span className="kk">⌘K</span>toggle
|
|
199
|
+
</span>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|