Files
- Create
Sources/FCPKitDSL/PresentationSlide.swift
- Create
Sources/FCPKitDSL/PresentationDocument.swift
- Delete
Sources/FCPKitDSL/RGBDocument.swift
- Modify
Tests/FCPKitDSLTests/FCPTimeIntervalTests.swift — see below
Placement: library type, thin CLI command
PresentationDocument and PresentationSlide are public and shipped in the
library, so users browsing FCPKitDSL can read the showcase and tests can import
it. The executable holds only a thin command that invokes the library type — not
a duplicated copy (decision 2026-08-02).
RGBDocument is the counter-example to clean up in the same pass. It is currently
duplicated across Sources/FCPKitDSL/RGBDocument.swift and
Sources/fcpxml-dsl/RGBDocument.swift. Demo scaffolding belongs only in the CLI,
so delete the library copy and keep the executable's.
That deletion breaks Tests/FCPKitDSLTests/FCPTimeIntervalTests.swift, which imports
the library copy. That test is really about FCPTimeInterval, not about
RGBDocument — give it a small local fixture document rather than moving it to a
CLI test target.
API
/// A slide in a ``PresentationDocument``: a heading over a solid color background.
public struct PresentationSlide: Sendable {
public var heading: String
public var subheading: String?
public var background: Color
public var duration: FCPTime
public init(
heading: String,
subheading: String? = nil,
background: Color,
duration: FCPTime = .seconds(6)
)
}
/// A media-free slide deck showcasing FCPKitDSL, built from color generators
/// and anchored titles.
public struct PresentationDocument: Document {
public let projectName: String
public let slides: [PresentationSlide]
public let transitionDuration: FCPTime
/// The default FCPKit feature deck.
public static let featureShowcase: [PresentationSlide]
public init(
projectName: String = "FCPKit Presentation",
slides: [PresentationSlide] = PresentationDocument.featureShowcase,
transitionDuration: FCPTime = FCPTime(numerator: 1)
)
public var body: some DocumentContent { ... }
}
The body loops slides, interleaving Transition(.crossDissolve) between them and
anchoring a styled Title on lane 1 of each Color background. This exercises
Issue 2's Color → Generator promotion, Issue 3's title modifiers, and the result
builder's buildArray / buildOptional — a genuine showcase rather than a contrived
one. If if index > 0 inside the for proves awkward through the builder, fall back
to a helper returning DocumentGroup.
Dissolve-safe title timing
Anchored items are not automatically included in a primary-storyline transition,
so a title spanning a dissolve would hard-cut while its background dissolves. We
design around this rather than discovering it at import.
placeOverlapping (Layout+Packing.swift:130-142) sets a clip's
start = overlap.previous (= T/2) and shrinks its duration by both overlaps.
Anchored offset is relative to the parent's trimmed start, so a title at
offset: .zero already begins exactly where the incoming dissolve ends. Only the
tail needs trimming:
titleDuration = slideDuration - (incomingT / 2) - (outgoingT / 2)
with incomingT / outgoingT zero for the first and last slide. The title then sits
entirely inside the non-overlapping region and hard-cuts nowhere visible. Assert this
formula in tests.
Deck content
Seven slides at 6s with 1s dissolves lands ~36s, inside the 30-45s target. Assert on
the packer's computed sequence duration rather than hand arithmetic.
| # |
Heading |
Background |
Showcases |
| 1 |
FCPKit |
near-black Color(white: 0.08) |
title positioning |
| 2 |
Typed FCPXML model |
deep blue |
— |
| 3 |
Ordered spine, preserved |
teal |
— |
| 4 |
SwiftUI-shaped DSL |
purple |
alignment-positioned subheading |
| 5 |
Resource interning |
orange |
— |
| 6 |
DTD-validated output |
green |
— |
| 7 |
brightdigit/FCPKit |
near-black |
— |
Use Helvetica so the video reproduces on any machine.
Show more than cross dissolves. A deck with seven identical dissolves undersells
the library. Once Issues 7 and 8 land, vary the transitions and apply at least a few
clip modifiers (opacity, blend mode, scale) across the deck so the video demonstrates
range rather than repetition.
Two constraints on that variety:
- The 30-45s target is a soft guide, not a hard gate. If showing more capability
costs a few seconds, take the seconds — but assert on the packer's computed
duration so the number is never guessed.
- Every effect used must have a UID that is either derivable from the Motion template
catalog or captured from a real Final Cut export (see Issue 8). Do not invent UIDs.
Until Issues 7 and 8 land, the deck ships with cross dissolves and is revisited
afterward; this issue is not blocked on them.
Tests
New Tests/FCPKitDSLTests/PresentationDocumentTests.swift:
- Spine children alternate
["video", "transition", "video", "transition", ...]
(copy the assertSpineChildNames helper pattern from FeaturePairAcceptanceTests).
- Each
video has exactly one anchored title at lane == "1".
- Every
text-style-def id in the document is unique — ties Issue 1 to the showcase.
- Each title's duration matches the dissolve-safe formula above.
- Sequence duration falls in the 30-45s range.
- Effect interning: exactly one
Custom generator effect and one Basic Title
effect despite seven slides, proving ResourceStore dedup.
assertDTDValidates(generatedData) — the end-to-end gate.
Acceptance criteria
FCPKIT_REQUIRE_DTD=1 swift test passes.
- Both new files stay under the 225-line warning.
Conventions
- MIT header block on every new file (see
Scripts/header.sh).
- Swift Testing (
import Testing, @Test) for new tests, per .claude/agent-notes.md. Include "Tests" in either the parent enum or the child struct, never both.
- Doc comments on every public declaration.
- Keep files under 225 lines (SwiftLint
file_length); prefer Type+Modifiers.swift splits.
- Run
swift test and swift run fcpxml-diff schema-completeness Tests/FCPKitTests/TestData before each PR. Prefer opening a PR over merging.
Full spec with context and rationale: docs/planning/demo-presentation-video.md
Files
Sources/FCPKitDSL/PresentationSlide.swiftSources/FCPKitDSL/PresentationDocument.swiftSources/FCPKitDSL/RGBDocument.swiftTests/FCPKitDSLTests/FCPTimeIntervalTests.swift— see belowPlacement: library type, thin CLI command
PresentationDocumentandPresentationSlideare public and shipped in thelibrary, so users browsing
FCPKitDSLcan read the showcase and tests can importit. The executable holds only a thin command that invokes the library type — not
a duplicated copy (decision 2026-08-02).
RGBDocumentis the counter-example to clean up in the same pass. It is currentlyduplicated across
Sources/FCPKitDSL/RGBDocument.swiftandSources/fcpxml-dsl/RGBDocument.swift. Demo scaffolding belongs only in the CLI,so delete the library copy and keep the executable's.
That deletion breaks
Tests/FCPKitDSLTests/FCPTimeIntervalTests.swift, which importsthe library copy. That test is really about
FCPTimeInterval, not aboutRGBDocument— give it a small local fixture document rather than moving it to aCLI test target.
API
The body loops
slides, interleavingTransition(.crossDissolve)between them andanchoring a styled
Titleon lane 1 of eachColorbackground. This exercisesIssue 2's
Color→Generatorpromotion, Issue 3's title modifiers, and the resultbuilder's
buildArray/buildOptional— a genuine showcase rather than a contrivedone. If
if index > 0inside theforproves awkward through the builder, fall backto a helper returning
DocumentGroup.Dissolve-safe title timing
Anchored items are not automatically included in a primary-storyline transition,
so a title spanning a dissolve would hard-cut while its background dissolves. We
design around this rather than discovering it at import.
placeOverlapping(Layout+Packing.swift:130-142) sets a clip'sstart = overlap.previous(= T/2) and shrinks itsdurationby both overlaps.Anchored
offsetis relative to the parent's trimmedstart, so a title atoffset: .zeroalready begins exactly where the incoming dissolve ends. Only thetail needs trimming:
with
incomingT/outgoingTzero for the first and last slide. The title then sitsentirely inside the non-overlapping region and hard-cuts nowhere visible. Assert this
formula in tests.
Deck content
Seven slides at 6s with 1s dissolves lands ~36s, inside the 30-45s target. Assert on
the packer's computed sequence duration rather than hand arithmetic.
Color(white: 0.08)Use Helvetica so the video reproduces on any machine.
Show more than cross dissolves. A deck with seven identical dissolves undersells
the library. Once Issues 7 and 8 land, vary the transitions and apply at least a few
clip modifiers (opacity, blend mode, scale) across the deck so the video demonstrates
range rather than repetition.
Two constraints on that variety:
costs a few seconds, take the seconds — but assert on the packer's computed
duration so the number is never guessed.
catalog or captured from a real Final Cut export (see Issue 8). Do not invent UIDs.
Until Issues 7 and 8 land, the deck ships with cross dissolves and is revisited
afterward; this issue is not blocked on them.
Tests
New
Tests/FCPKitDSLTests/PresentationDocumentTests.swift:["video", "transition", "video", "transition", ...](copy the
assertSpineChildNameshelper pattern fromFeaturePairAcceptanceTests).videohas exactly one anchoredtitleatlane == "1".text-style-defid in the document is unique — ties Issue 1 to the showcase.Customgenerator effect and oneBasic Titleeffect despite seven slides, proving
ResourceStorededup.assertDTDValidates(generatedData)— the end-to-end gate.Acceptance criteria
FCPKIT_REQUIRE_DTD=1 swift testpasses.Conventions
Scripts/header.sh).import Testing,@Test) for new tests, per.claude/agent-notes.md. Include "Tests" in either the parent enum or the child struct, never both.file_length); preferType+Modifiers.swiftsplits.swift testandswift run fcpxml-diff schema-completeness Tests/FCPKitTests/TestDatabefore each PR. Prefer opening a PR over merging.Full spec with context and rationale: docs/planning/demo-presentation-video.md