@scenar/core 0.8.0 → 0.9.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/index.d.ts CHANGED
@@ -10,6 +10,8 @@ export type { StepTimeline } from "./timeline/compute-step-timeline.js";
10
10
  export { computeStepTimeline } from "./timeline/compute-step-timeline.js";
11
11
  export { deriveStepFromTime } from "./timeline/derive-step.js";
12
12
  export { getStepDurationMs } from "./timeline/step-duration.js";
13
+ export type { ScenarioShot } from "./timeline/collect-shots.js";
14
+ export { SHOT_NAME_PATTERN, collectScenarioShots } from "./timeline/collect-shots.js";
13
15
  export { CLICK_DELAY_MS, TYPE_CHAR_DELAY_MS, HOVER_HOLD_MS, DRAG_SETTLE_MS, CAMERA_TRANSITION_MS, VIEWPORT_SETTLE_MS, } from "./timing/constants.js";
14
16
  export { findScrollParent, scrollTargetIntoView, scrollTargetIntoViewInstant, } from "./dom/scroll.js";
15
17
  export { CURSOR_TARGET_ATTRIBUTE, SCROLL_TARGET_ATTRIBUTE, HOVER_STATE_ATTRIBUTE, DRAG_STATE_ATTRIBUTE, cursorTargetSelector, scrollTargetSelector, } from "./targeting/data-attributes.js";
package/index.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,YAAY,EACV,UAAU,EACV,UAAU,EACV,0BAA0B,GAC3B,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAG9E,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,EACvB,UAAU,EACV,4BAA4B,GAC7B,MAAM,yBAAyB,CAAC;AAGjC,YAAY,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,YAAY,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAGhE,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EACL,mBAAmB,EACnB,6BAA6B,EAC7B,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,YAAY,EACV,yBAAyB,EACzB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,YAAY,EACV,UAAU,EACV,UAAU,EACV,0BAA0B,GAC3B,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAG9E,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,EACvB,UAAU,EACV,4BAA4B,GAC7B,MAAM,yBAAyB,CAAC;AAGjC,YAAY,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,YAAY,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,YAAY,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGtF,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EACL,mBAAmB,EACnB,6BAA6B,EAC7B,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,YAAY,EACV,yBAAyB,EACzB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC"}
package/index.js CHANGED
@@ -5,6 +5,7 @@ export { computeCursorPosition } from "./cursor/compute-position.js";
5
5
  export { computeStepTimeline } from "./timeline/compute-step-timeline.js";
6
6
  export { deriveStepFromTime } from "./timeline/derive-step.js";
7
7
  export { getStepDurationMs } from "./timeline/step-duration.js";
8
+ export { SHOT_NAME_PATTERN, collectScenarioShots } from "./timeline/collect-shots.js";
8
9
  // Timing constants
9
10
  export { CLICK_DELAY_MS, TYPE_CHAR_DELAY_MS, HOVER_HOLD_MS, DRAG_SETTLE_MS, CAMERA_TRANSITION_MS, VIEWPORT_SETTLE_MS, } from "./timing/constants.js";
10
11
  // DOM scroll utilities
package/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAC5B,sEAAsE;AAgBtE,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,EACvB,UAAU,EACV,4BAA4B,GAC7B,MAAM,yBAAyB,CAAC;AAIjC,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAIrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAEhE,mBAAmB;AACnB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAE/B,uBAAuB;AACvB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AAEzB,oCAAoC;AACpC,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,gCAAgC,CAAC;AAExC,0EAA0E;AAC1E,OAAO,EACL,mBAAmB,EACnB,6BAA6B,EAC7B,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAQ7B,uEAAuE;AACvE,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAC5B,sEAAsE;AAgBtE,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,EACvB,UAAU,EACV,4BAA4B,GAC7B,MAAM,yBAAyB,CAAC;AAIjC,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAIrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAEhE,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAEtF,mBAAmB;AACnB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAE/B,uBAAuB;AACvB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AAEzB,oCAAoC;AACpC,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,gCAAgC,CAAC;AAExC,0EAA0E;AAC1E,OAAO,EACL,mBAAmB,EACnB,6BAA6B,EAC7B,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAQ7B,uEAAuE;AACvE,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scenar/core",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Pure TypeScript types, timing, and utilities for the Scenar scenario engine.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,5 +29,21 @@ export interface ScenarioStep<T> {
29
29
  * share the same atPercent, they fire in array order.
30
30
  */
31
31
  readonly interactions?: readonly StepAction[];
32
+ /**
33
+ * Names this step as a still-capture point for `scenar shoot`.
34
+ *
35
+ * The capture happens at the step's settled *end* — after every
36
+ * interaction (and its tail: click dispatch, per-char typing, camera
37
+ * tween) has fired — so a shot-bearing step's duration must fit its
38
+ * interaction tails.
39
+ *
40
+ * Shots are addressed by this name, never by step index, so inserting a
41
+ * beat into a scenario never renumbers existing docs references. Names
42
+ * must be unique within a scenario and kebab-case
43
+ * (`^[a-z0-9]+(-[a-z0-9]+)*$`) — each becomes a filename
44
+ * (`stills/<shot>.<theme>.png`) and a URL segment in the deployed
45
+ * bundle. Steps without a `shot` are simply walked through.
46
+ */
47
+ readonly shot?: string;
32
48
  }
33
49
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/scenario/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD;;;;GAIG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;OAOG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;CAC/C"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/scenario/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD;;;;GAIG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;OAOG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAC9C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB"}
package/src/index.ts CHANGED
@@ -31,6 +31,8 @@ export type { StepTimeline } from "./timeline/compute-step-timeline.js";
31
31
  export { computeStepTimeline } from "./timeline/compute-step-timeline.js";
32
32
  export { deriveStepFromTime } from "./timeline/derive-step.js";
33
33
  export { getStepDurationMs } from "./timeline/step-duration.js";
34
+ export type { ScenarioShot } from "./timeline/collect-shots.js";
35
+ export { SHOT_NAME_PATTERN, collectScenarioShots } from "./timeline/collect-shots.js";
34
36
 
35
37
  // Timing constants
36
38
  export {
@@ -31,4 +31,20 @@ export interface ScenarioStep<T> {
31
31
  * share the same atPercent, they fire in array order.
32
32
  */
33
33
  readonly interactions?: readonly StepAction[];
34
+ /**
35
+ * Names this step as a still-capture point for `scenar shoot`.
36
+ *
37
+ * The capture happens at the step's settled *end* — after every
38
+ * interaction (and its tail: click dispatch, per-char typing, camera
39
+ * tween) has fired — so a shot-bearing step's duration must fit its
40
+ * interaction tails.
41
+ *
42
+ * Shots are addressed by this name, never by step index, so inserting a
43
+ * beat into a scenario never renumbers existing docs references. Names
44
+ * must be unique within a scenario and kebab-case
45
+ * (`^[a-z0-9]+(-[a-z0-9]+)*$`) — each becomes a filename
46
+ * (`stills/<shot>.<theme>.png`) and a URL segment in the deployed
47
+ * bundle. Steps without a `shot` are simply walked through.
48
+ */
49
+ readonly shot?: string;
34
50
  }
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { computeStepTimeline } from "./compute-step-timeline.js";
3
+ import { SHOT_NAME_PATTERN, collectScenarioShots } from "./collect-shots.js";
4
+
5
+ describe("collectScenarioShots", () => {
6
+ it("resolves each shot to the settled end of its step", () => {
7
+ const steps = [
8
+ { delayMs: 0, shot: "opening" },
9
+ { delayMs: 1000 },
10
+ { delayMs: 2000, shot: "detail-open" },
11
+ ];
12
+ const timeline = computeStepTimeline(steps, null);
13
+ // Starts: [0, 1000, 3000]; total: 3000 + 3000 (final dwell).
14
+ expect(collectScenarioShots(steps, timeline)).toEqual([
15
+ { name: "opening", timeMs: 999, stepIndex: 0 },
16
+ { name: "detail-open", timeMs: 5999, stepIndex: 2 },
17
+ ]);
18
+ });
19
+
20
+ it("places a final-step shot inside the final dwell", () => {
21
+ const steps = [{ delayMs: 0 }, { delayMs: 500, shot: "finale" }];
22
+ const timeline = computeStepTimeline(steps, null);
23
+ expect(collectScenarioShots(steps, timeline)).toEqual([
24
+ { name: "finale", timeMs: timeline.totalDurationMs - 1, stepIndex: 1 },
25
+ ]);
26
+ });
27
+
28
+ it("resolves shot times against narration-driven step durations", () => {
29
+ const steps = [{ delayMs: 0, shot: "narrated" }, { delayMs: 500 }];
30
+ const timeline = computeStepTimeline(steps, {
31
+ steps: [{ src: "a.mp3", durationMs: 4000 }, null],
32
+ });
33
+ // Step 1 starts at max(500, 4000) = 4000; the shot lands just before it.
34
+ expect(collectScenarioShots(steps, timeline)).toEqual([
35
+ { name: "narrated", timeMs: 3999, stepIndex: 0 },
36
+ ]);
37
+ });
38
+
39
+ it("returns an empty list when no step declares a shot", () => {
40
+ const steps = [{ delayMs: 0 }, { delayMs: 1000 }];
41
+ expect(collectScenarioShots(steps, computeStepTimeline(steps, null))).toEqual([]);
42
+ });
43
+
44
+ it("rejects a non-kebab-case name, naming the step", () => {
45
+ const steps = [{ delayMs: 0 }, { delayMs: 100, shot: "Detail_Open" }];
46
+ expect(() =>
47
+ collectScenarioShots(steps, computeStepTimeline(steps, null)),
48
+ ).toThrowError(/step 1 declares shot "Detail_Open"/);
49
+ });
50
+
51
+ it.each(["-leading", "trailing-", "double--dash", "UPPER", "with.dot", "with space", ""])(
52
+ 'rejects invalid shot name "%s"',
53
+ (name) => {
54
+ const steps = [{ delayMs: 0, shot: name }];
55
+ expect(() =>
56
+ collectScenarioShots(steps, computeStepTimeline(steps, null)),
57
+ ).toThrowError(/not kebab-case/);
58
+ },
59
+ );
60
+
61
+ it.each(["a", "shot-1", "agent-detail", "step2-open"])(
62
+ 'accepts valid shot name "%s"',
63
+ (name) => {
64
+ expect(SHOT_NAME_PATTERN.test(name)).toBe(true);
65
+ },
66
+ );
67
+
68
+ it("rejects duplicate names, naming both steps", () => {
69
+ const steps = [
70
+ { delayMs: 0, shot: "opening" },
71
+ { delayMs: 100 },
72
+ { delayMs: 100, shot: "opening" },
73
+ ];
74
+ expect(() =>
75
+ collectScenarioShots(steps, computeStepTimeline(steps, null)),
76
+ ).toThrowError(/steps 0 and 2 both declare shot "opening"/);
77
+ });
78
+ });
@@ -0,0 +1,84 @@
1
+ import type { StepTimeline } from "./compute-step-timeline.js";
2
+
3
+ /**
4
+ * Minimal step shape for shot collection. Accepts any ScenarioStep<T>
5
+ * without caring about the data payload.
6
+ */
7
+ interface StepWithShot {
8
+ delayMs: number;
9
+ shot?: string;
10
+ }
11
+
12
+ /** A named still-capture point resolved to its position on the timeline. */
13
+ export interface ScenarioShot {
14
+ /** The step's declared `shot` name (validated kebab-case, unique). */
15
+ readonly name: string;
16
+ /**
17
+ * The capture time: the step's settled end. For step i this is
18
+ * `stepStartTimesMs[i + 1] - 1`; for the final step it is
19
+ * `totalDurationMs - 1`, which already includes the final dwell (or the
20
+ * final narration clip, whichever is longer).
21
+ */
22
+ readonly timeMs: number;
23
+ /** Index of the step that declared the shot. */
24
+ readonly stepIndex: number;
25
+ }
26
+
27
+ /**
28
+ * The shape a `shot` name must have. Each name becomes a filename
29
+ * (`stills/<shot>.<theme>.png`) and a URL segment in the deployed bundle,
30
+ * so the charset is deliberately the strict kebab-case subset of what the
31
+ * deploy path contract admits.
32
+ */
33
+ export const SHOT_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
34
+
35
+ /**
36
+ * Resolve every `shot`-bearing step to its named capture point, in
37
+ * timeline order.
38
+ *
39
+ * Throws on the two authoring mistakes that must never reach a deployed
40
+ * bundle: a name that cannot be a clean filename/URL segment, and a
41
+ * duplicate name (shots are addressed by name, never by index — a
42
+ * collision would silently overwrite a still). Error messages name the
43
+ * offending step index so the fix is local.
44
+ *
45
+ * The `timeline` must be computed from the same steps (and the same
46
+ * narration manifest) the capture will walk — shot times are meaningless
47
+ * against any other timeline.
48
+ */
49
+ export function collectScenarioShots(
50
+ steps: readonly StepWithShot[],
51
+ timeline: StepTimeline,
52
+ ): ScenarioShot[] {
53
+ const shots: ScenarioShot[] = [];
54
+ const seen = new Map<string, number>();
55
+
56
+ for (let i = 0; i < steps.length; i++) {
57
+ const name = steps[i]?.shot;
58
+ if (name === undefined) continue;
59
+
60
+ if (!SHOT_NAME_PATTERN.test(name)) {
61
+ throw new Error(
62
+ `step ${i} declares shot "${name}", which is not kebab-case ` +
63
+ `(expected ${SHOT_NAME_PATTERN}); it becomes a filename and a URL segment`,
64
+ );
65
+ }
66
+ const firstIndex = seen.get(name);
67
+ if (firstIndex !== undefined) {
68
+ throw new Error(
69
+ `steps ${firstIndex} and ${i} both declare shot "${name}"; ` +
70
+ `shot names must be unique within a scenario`,
71
+ );
72
+ }
73
+ seen.set(name, i);
74
+
75
+ const nextStartMs = timeline.stepStartTimesMs[i + 1];
76
+ shots.push({
77
+ name,
78
+ timeMs: (nextStartMs ?? timeline.totalDurationMs) - 1,
79
+ stepIndex: i,
80
+ });
81
+ }
82
+
83
+ return shots;
84
+ }
@@ -0,0 +1,47 @@
1
+ import type { StepTimeline } from "./compute-step-timeline.js";
2
+ /**
3
+ * Minimal step shape for shot collection. Accepts any ScenarioStep<T>
4
+ * without caring about the data payload.
5
+ */
6
+ interface StepWithShot {
7
+ delayMs: number;
8
+ shot?: string;
9
+ }
10
+ /** A named still-capture point resolved to its position on the timeline. */
11
+ export interface ScenarioShot {
12
+ /** The step's declared `shot` name (validated kebab-case, unique). */
13
+ readonly name: string;
14
+ /**
15
+ * The capture time: the step's settled end. For step i this is
16
+ * `stepStartTimesMs[i + 1] - 1`; for the final step it is
17
+ * `totalDurationMs - 1`, which already includes the final dwell (or the
18
+ * final narration clip, whichever is longer).
19
+ */
20
+ readonly timeMs: number;
21
+ /** Index of the step that declared the shot. */
22
+ readonly stepIndex: number;
23
+ }
24
+ /**
25
+ * The shape a `shot` name must have. Each name becomes a filename
26
+ * (`stills/<shot>.<theme>.png`) and a URL segment in the deployed bundle,
27
+ * so the charset is deliberately the strict kebab-case subset of what the
28
+ * deploy path contract admits.
29
+ */
30
+ export declare const SHOT_NAME_PATTERN: RegExp;
31
+ /**
32
+ * Resolve every `shot`-bearing step to its named capture point, in
33
+ * timeline order.
34
+ *
35
+ * Throws on the two authoring mistakes that must never reach a deployed
36
+ * bundle: a name that cannot be a clean filename/URL segment, and a
37
+ * duplicate name (shots are addressed by name, never by index — a
38
+ * collision would silently overwrite a still). Error messages name the
39
+ * offending step index so the fix is local.
40
+ *
41
+ * The `timeline` must be computed from the same steps (and the same
42
+ * narration manifest) the capture will walk — shot times are meaningless
43
+ * against any other timeline.
44
+ */
45
+ export declare function collectScenarioShots(steps: readonly StepWithShot[], timeline: StepTimeline): ScenarioShot[];
46
+ export {};
47
+ //# sourceMappingURL=collect-shots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect-shots.d.ts","sourceRoot":"","sources":["../../src/timeline/collect-shots.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D;;;GAGG;AACH,UAAU,YAAY;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,QAA6B,CAAC;AAE5D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,YAAY,EAAE,EAC9B,QAAQ,EAAE,YAAY,GACrB,YAAY,EAAE,CAgChB"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The shape a `shot` name must have. Each name becomes a filename
3
+ * (`stills/<shot>.<theme>.png`) and a URL segment in the deployed bundle,
4
+ * so the charset is deliberately the strict kebab-case subset of what the
5
+ * deploy path contract admits.
6
+ */
7
+ export const SHOT_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
8
+ /**
9
+ * Resolve every `shot`-bearing step to its named capture point, in
10
+ * timeline order.
11
+ *
12
+ * Throws on the two authoring mistakes that must never reach a deployed
13
+ * bundle: a name that cannot be a clean filename/URL segment, and a
14
+ * duplicate name (shots are addressed by name, never by index — a
15
+ * collision would silently overwrite a still). Error messages name the
16
+ * offending step index so the fix is local.
17
+ *
18
+ * The `timeline` must be computed from the same steps (and the same
19
+ * narration manifest) the capture will walk — shot times are meaningless
20
+ * against any other timeline.
21
+ */
22
+ export function collectScenarioShots(steps, timeline) {
23
+ const shots = [];
24
+ const seen = new Map();
25
+ for (let i = 0; i < steps.length; i++) {
26
+ const name = steps[i]?.shot;
27
+ if (name === undefined)
28
+ continue;
29
+ if (!SHOT_NAME_PATTERN.test(name)) {
30
+ throw new Error(`step ${i} declares shot "${name}", which is not kebab-case ` +
31
+ `(expected ${SHOT_NAME_PATTERN}); it becomes a filename and a URL segment`);
32
+ }
33
+ const firstIndex = seen.get(name);
34
+ if (firstIndex !== undefined) {
35
+ throw new Error(`steps ${firstIndex} and ${i} both declare shot "${name}"; ` +
36
+ `shot names must be unique within a scenario`);
37
+ }
38
+ seen.set(name, i);
39
+ const nextStartMs = timeline.stepStartTimesMs[i + 1];
40
+ shots.push({
41
+ name,
42
+ timeMs: (nextStartMs ?? timeline.totalDurationMs) - 1,
43
+ stepIndex: i,
44
+ });
45
+ }
46
+ return shots;
47
+ }
48
+ //# sourceMappingURL=collect-shots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect-shots.js","sourceRoot":"","sources":["../../src/timeline/collect-shots.ts"],"names":[],"mappings":"AA0BA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,0BAA0B,CAAC;AAE5D;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAA8B,EAC9B,QAAsB;IAEtB,MAAM,KAAK,GAAmB,EAAE,CAAC;IACjC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC;QAC5B,IAAI,IAAI,KAAK,SAAS;YAAE,SAAS;QAEjC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CACb,QAAQ,CAAC,mBAAmB,IAAI,6BAA6B;gBAC3D,aAAa,iBAAiB,4CAA4C,CAC7E,CAAC;QACJ,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CACb,SAAS,UAAU,QAAQ,CAAC,uBAAuB,IAAI,KAAK;gBAC1D,6CAA6C,CAChD,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAElB,MAAM,WAAW,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACrD,KAAK,CAAC,IAAI,CAAC;YACT,IAAI;YACJ,MAAM,EAAE,CAAC,WAAW,IAAI,QAAQ,CAAC,eAAe,CAAC,GAAG,CAAC;YACrD,SAAS,EAAE,CAAC;SACb,CAAC,CAAC;IACL,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}