@bycrux/editor 0.11.2 → 1.0.1
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/package.json +3 -1
- package/src/ControlsInfoModal.tsx +251 -61
- package/src/__tests__/ControlsInfoModal.test.tsx +43 -0
- package/src/__tests__/adapter.test.ts +59 -1
- package/src/__tests__/schema-assignability.test.ts +93 -0
- package/src/__tests__/schema.test.ts +15 -0
- package/src/__tests__/video-adapter-contract.test.ts +128 -5
- package/src/carousel/AddElementMenu.tsx +10 -4
- package/src/carousel/CarouselEditor.tsx +46 -8
- package/src/carousel/CarouselRenderModal.tsx +15 -10
- package/src/carousel/OverlayPicker.tsx +10 -3
- package/src/carousel/SlidePropertyPanel.tsx +43 -21
- package/src/components/FilmstripScrubber.tsx +277 -0
- package/src/components/__tests__/FilmstripScrubber.test.tsx +216 -0
- package/src/engine/__tests__/audio-clock.test.ts +655 -0
- package/src/engine/__tests__/audio-worklet-source.test.ts +316 -0
- package/src/engine/__tests__/batch-planner.test.ts +298 -0
- package/src/engine/__tests__/decode-worker-source.test.ts +249 -0
- package/src/engine/__tests__/demux-ranged.test.ts +495 -0
- package/src/engine/__tests__/demux-truncated.test.ts +136 -0
- package/src/engine/__tests__/demux.test.ts +305 -0
- package/src/engine/__tests__/eligibility.test.ts +146 -0
- package/src/engine/__tests__/engine-recovery.test.ts +263 -0
- package/src/engine/__tests__/engine.test.ts +326 -0
- package/src/engine/__tests__/frame-server-ranged.test.ts +261 -0
- package/src/engine/__tests__/frame-server.test.ts +326 -0
- package/src/engine/__tests__/media-loader-ranged.test.ts +289 -0
- package/src/engine/__tests__/media-loader.test.ts +100 -0
- package/src/engine/__tests__/scheduler-crop.test.ts +353 -0
- package/src/engine/__tests__/scheduler.test.ts +1310 -0
- package/src/engine/__tests__/scrub-resolve.test.ts +96 -0
- package/src/engine/__tests__/scrub-source.test.ts +195 -0
- package/src/engine/__tests__/source-host.test.ts +455 -0
- package/src/engine/__tests__/time-stretch.test.ts +182 -0
- package/src/engine/audio-clock.ts +1179 -0
- package/src/engine/audio-worklet-source.ts +160 -0
- package/src/engine/batch-planner.ts +308 -0
- package/src/engine/debug-hud.tsx +54 -0
- package/src/engine/decode-worker-source.ts +142 -0
- package/src/engine/demux.ts +900 -0
- package/src/engine/eligibility.ts +140 -0
- package/src/engine/frame-server.ts +644 -0
- package/src/engine/index.ts +950 -0
- package/src/engine/media-loader.ts +380 -0
- package/src/engine/mp4box.d.ts +214 -0
- package/src/engine/scheduler.ts +1324 -0
- package/src/engine/scrub-resolve.ts +66 -0
- package/src/engine/scrub-source.ts +496 -0
- package/src/engine/time-stretch.ts +224 -0
- package/src/index.ts +83 -1
- package/src/preview/OverlayPreview.tsx +2 -24
- package/src/schema.ts +146 -3
- package/src/state/__tests__/use-project-sync.test.tsx +56 -0
- package/src/state/use-project-sync.ts +17 -0
- package/src/test-setup.ts +32 -0
- package/src/text/FontPicker.tsx +85 -51
- package/src/text/TextFormattingToolbar.tsx +6 -1
- package/src/text/__tests__/FontPicker.options.test.ts +43 -0
- package/src/text/__tests__/TextFormattingToolbar.test.tsx +4 -4
- package/src/theme.ts +108 -3
- package/src/types.ts +601 -22
- package/src/ui/Loader.tsx +64 -0
- package/src/ui/NumberField.tsx +254 -0
- package/src/ui/Slider.tsx +128 -0
- package/src/ui/Tooltip.tsx +118 -0
- package/src/ui/__tests__/NumberField.test.tsx +298 -0
- package/src/ui/__tests__/Slider.test.tsx +150 -0
- package/src/ui/__tests__/Tooltip.test.tsx +105 -0
- package/src/ui/__tests__/usePersistentState.test.tsx +112 -0
- package/src/ui/badge.tsx +12 -4
- package/src/ui/index.ts +6 -0
- package/src/ui/input.tsx +1 -1
- package/src/ui/select.tsx +1 -1
- package/src/ui/switch.tsx +12 -2
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/usePersistentState.ts +63 -0
- package/src/video/AudioPolishModal.tsx +983 -0
- package/src/video/CaptionListPanel.test.tsx +944 -0
- package/src/video/CaptionListPanel.tsx +1106 -0
- package/src/video/CaptionRegenModal.tsx +37 -9
- package/src/video/CaptionSpecimen.tsx +160 -0
- package/src/video/CaptionStyleGallery.tsx +466 -0
- package/src/video/CommandPalette.tsx +165 -0
- package/src/video/ImageToneMenu.tsx +137 -0
- package/src/video/OverlayInspector.tsx +977 -0
- package/src/video/RenderModal.tsx +1035 -62
- package/src/video/VersionCompare.tsx +258 -0
- package/src/video/VersionPanel.tsx +117 -42
- package/src/video/VideoEditor.tsx +2229 -243
- package/src/video/__tests__/AudioPolishModal.test.tsx +825 -0
- package/src/video/__tests__/CaptionListPanel.font.test.tsx +397 -0
- package/src/video/__tests__/CaptionListPanel.generate.test.tsx +153 -0
- package/src/video/__tests__/CaptionRegenModal.test.tsx +29 -0
- package/src/video/__tests__/CaptionSpecimen.test.tsx +213 -0
- package/src/video/__tests__/CaptionStyleGallery.test.tsx +362 -0
- package/src/video/__tests__/CommandPalette.test.tsx +119 -0
- package/src/video/__tests__/OverlayInspector.test.tsx +1367 -0
- package/src/video/__tests__/RenderModal.exportControls.test.tsx +319 -0
- package/src/video/__tests__/RenderModal.options.test.tsx +562 -0
- package/src/video/__tests__/RenderModal.progress.test.ts +65 -0
- package/src/video/__tests__/VersionCompare.test.tsx +182 -0
- package/src/video/__tests__/VersionPanel.test.tsx +279 -0
- package/src/video/__tests__/VideoEditor.audioPolish.test.tsx +241 -0
- package/src/video/__tests__/VideoEditor.captionDelete.test.tsx +147 -0
- package/src/video/__tests__/VideoEditor.captionGesture.test.tsx +170 -0
- package/src/video/__tests__/VideoEditor.captionSeam.test.tsx +134 -0
- package/src/video/__tests__/VideoEditor.clipKeyframes.test.tsx +59 -0
- package/src/video/__tests__/VideoEditor.context.test.tsx +44 -0
- package/src/video/__tests__/VideoEditor.editFocus.test.tsx +55 -0
- package/src/video/__tests__/VideoEditor.keymap.test.tsx +725 -0
- package/src/video/__tests__/VideoEditor.layout.test.tsx +338 -0
- package/src/video/__tests__/VideoEditor.propertiesPanel.test.tsx +765 -0
- package/src/video/__tests__/VideoEditor.rippleDeleteCaptions.test.tsx +159 -0
- package/src/video/__tests__/VideoEditor.sourcePreview.test.tsx +122 -0
- package/src/video/__tests__/VideoEditor.test.tsx +403 -50
- package/src/video/__tests__/audioMagnet.test.ts +158 -0
- package/src/video/__tests__/audioPolish.test.ts +1202 -0
- package/src/video/__tests__/captionActiveWord.test.ts +107 -0
- package/src/video/__tests__/captionLanes.test.ts +262 -0
- package/src/video/__tests__/captionPositioning.test.tsx +8 -3
- package/src/video/__tests__/captionWordFloor.test.ts +228 -0
- package/src/video/__tests__/clipboard-ops.test.ts +430 -0
- package/src/video/__tests__/cuts.insert.test.ts +244 -0
- package/src/video/__tests__/cuts.test.ts +1213 -34
- package/src/video/__tests__/export-limits.test.ts +195 -0
- package/src/video/__tests__/hover-scrub.test.ts +163 -0
- package/src/video/__tests__/keyframeOps.canKeyframeProp.test.ts +61 -0
- package/src/video/__tests__/keyframeOps.test.ts +643 -0
- package/src/video/__tests__/keymap.test.tsx +171 -0
- package/src/video/__tests__/render-progress.test.tsx +20 -4
- package/src/video/__tests__/shuttle.test.ts +210 -0
- package/src/video/__tests__/source-preview.test.ts +60 -0
- package/src/video/__tests__/timecode.test.ts +77 -0
- package/src/video/__tests__/use-report-context.test.tsx +101 -0
- package/src/video/audioMagnet.ts +72 -0
- package/src/video/audioPolish.ts +774 -0
- package/src/video/captionActiveWord.ts +74 -0
- package/src/video/captionLanes.ts +202 -0
- package/src/video/captionRepair.ts +9 -5
- package/src/video/captionStyleDefaults.ts +100 -0
- package/src/video/captionWordFloor.ts +83 -0
- package/src/video/clipboard-ops.ts +377 -0
- package/src/video/cuts.ts +713 -37
- package/src/video/export-limits.ts +102 -0
- package/src/video/hover-scrub.ts +102 -0
- package/src/video/imageTone.ts +58 -0
- package/src/video/imageToneExamples.ts +10 -0
- package/src/video/keyframeOps.ts +384 -0
- package/src/video/keymap.ts +146 -0
- package/src/video/panels/ClipPropertiesPanel.tsx +706 -0
- package/src/video/panels/LeftPanelTabs.tsx +187 -0
- package/src/video/panels/OverlayContentPanel.tsx +351 -0
- package/src/video/panels/TabNav.tsx +60 -0
- package/src/video/panels/__tests__/ClipPropertiesPanel.test.tsx +645 -0
- package/src/video/panels/__tests__/LeftPanelTabs.test.tsx +189 -0
- package/src/video/panels/__tests__/OverlayContentPanel.test.tsx +222 -0
- package/src/video/panels/__tests__/TabNav.test.tsx +71 -0
- package/src/video/preview/CaptionPreview.tsx +57 -69
- package/src/video/preview/EngineSurface.tsx +103 -0
- package/src/video/preview/OverlayItemsLayer.tsx +315 -103
- package/src/video/preview/PreviewPlayer.tsx +337 -56
- package/src/video/preview/SocialPreviewMenu.tsx +214 -0
- package/src/video/preview/SocialSafeZoneOverlay.tsx +478 -0
- package/src/video/preview/__tests__/CaptionPreview.fonts.test.tsx +97 -0
- package/src/video/preview/__tests__/EngineSurface.test.tsx +114 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.edit.test.tsx +4 -2
- package/src/video/preview/__tests__/OverlayItemsLayer.keyframes.test.tsx +363 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.selection.test.tsx +329 -0
- package/src/video/preview/__tests__/PreviewPlayer.engine.test.tsx +139 -0
- package/src/video/preview/__tests__/SocialPreviewMenu.test.tsx +121 -0
- package/src/video/preview/__tests__/SocialSafeZoneOverlay.test.tsx +165 -0
- package/src/video/preview/__tests__/captionDragState.test.ts +120 -1
- package/src/video/preview/__tests__/latencyCompensation.test.tsx +451 -0
- package/src/video/preview/__tests__/proxySupport.test.ts +75 -0
- package/src/video/preview/__tests__/transformStyle.test.ts +29 -1
- package/src/video/preview/__tests__/useDragOverlay.perAxis.test.ts +197 -0
- package/src/video/preview/__tests__/useEnginePlayback.test.tsx +530 -0
- package/src/video/preview/__tests__/useVideoPlayback.corpus.test.ts +313 -0
- package/src/video/preview/__tests__/useVideoPlayback.test.ts +38 -5
- package/src/video/preview/__tests__/useVideoPlayback.trackAudio.test.ts +278 -0
- package/src/video/preview/audio-context.ts +111 -0
- package/src/video/preview/captionDragState.ts +91 -1
- package/src/video/preview/proxySupport.ts +86 -0
- package/src/video/preview/transformStyle.ts +22 -12
- package/src/video/preview/useDragOverlay.ts +92 -18
- package/src/video/preview/useEnginePlayback.ts +625 -0
- package/src/video/preview/useVideoPlayback.ts +211 -167
- package/src/video/sdrCurves.ts +56 -0
- package/src/video/shuttle.ts +159 -0
- package/src/video/source-preview.ts +66 -0
- package/src/video/timecode.ts +59 -0
- package/src/video/timeline/EditableSegment.tsx +1 -1
- package/src/video/timeline/Scrubber.tsx +46 -174
- package/src/video/timeline/SpeedControl.tsx +95 -0
- package/src/video/timeline/Timeline.tsx +1061 -328
- package/src/video/timeline/TimelineContext.ts +17 -10
- package/src/video/timeline/TrackGutter.tsx +560 -0
- package/src/video/timeline/TrackSettingsPopover.tsx +228 -0
- package/src/video/timeline/VolumeControl.tsx +113 -0
- package/src/video/timeline/__tests__/Timeline.backgroundClick.test.tsx +110 -0
- package/src/video/timeline/__tests__/Timeline.crossfade.test.tsx +104 -0
- package/src/video/timeline/__tests__/Timeline.fadeCurveMenu.test.tsx +174 -0
- package/src/video/timeline/__tests__/Timeline.keyframeDelete.test.tsx +273 -0
- package/src/video/timeline/__tests__/Timeline.keyframeFollow.test.tsx +215 -0
- package/src/video/timeline/__tests__/Timeline.keyframeMenu.test.tsx +253 -0
- package/src/video/timeline/__tests__/Timeline.keymap.test.tsx +372 -0
- package/src/video/timeline/__tests__/Timeline.subcutRegen.test.tsx +351 -0
- package/src/video/timeline/__tests__/TrackGutter.test.tsx +372 -0
- package/src/video/timeline/__tests__/_canvasSelect.test.tsx +273 -0
- package/src/video/timeline/__tests__/_canvasSelect.ts +414 -0
- package/src/video/timeline/__tests__/dragdrop-math.test.ts +135 -0
- package/src/video/timeline/__tests__/effectiveItemAudio.test.ts +50 -0
- package/src/video/timeline/__tests__/enabledTrackItems.test.ts +164 -0
- package/src/video/timeline/__tests__/moveItemAcrossTracks.test.ts +363 -0
- package/src/video/timeline/__tests__/multiSelectOps.test.ts +447 -0
- package/src/video/timeline/__tests__/placement.test.ts +278 -0
- package/src/video/timeline/__tests__/resizeWindowedItem.test.ts +140 -0
- package/src/video/timeline/__tests__/timeline-model.test.ts +576 -0
- package/src/video/timeline/__tests__/visualItemLabel.test.ts +69 -0
- package/src/video/timeline/canvas/TimelineCanvas.tsx +1447 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.drop.test.tsx +439 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.edgeScroll.test.tsx +346 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.panefill.test.tsx +122 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pendingDrops.test.tsx +316 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pointer.test.tsx +606 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.test.tsx +407 -0
- package/src/video/timeline/canvas/__tests__/clip-bands.test.ts +77 -0
- package/src/video/timeline/canvas/__tests__/draw.test.ts +2198 -0
- package/src/video/timeline/canvas/__tests__/fade-curve.test.ts +187 -0
- package/src/video/timeline/canvas/__tests__/filmstrips.test.ts +561 -0
- package/src/video/timeline/canvas/__tests__/hit-test.test.ts +818 -0
- package/src/video/timeline/canvas/__tests__/pending-drop.test.ts +210 -0
- package/src/video/timeline/canvas/__tests__/pointer-machine.test.ts +3358 -0
- package/src/video/timeline/canvas/__tests__/snap.test.ts +257 -0
- package/src/video/timeline/canvas/__tests__/viewport.test.ts +399 -0
- package/src/video/timeline/canvas/__tests__/waveforms.test.ts +946 -0
- package/src/video/timeline/canvas/clip-bands.ts +56 -0
- package/src/video/timeline/canvas/draw.ts +2187 -0
- package/src/video/timeline/canvas/fade-curve.ts +111 -0
- package/src/video/timeline/canvas/filmstrips.ts +418 -0
- package/src/video/timeline/canvas/hit-test.ts +501 -0
- package/src/video/timeline/canvas/keyframe-strip.ts +73 -0
- package/src/video/timeline/canvas/pointer-machine.ts +1828 -0
- package/src/video/timeline/canvas/snap.ts +232 -0
- package/src/video/timeline/canvas/viewport.ts +457 -0
- package/src/video/timeline/canvas/waveforms.ts +664 -0
- package/src/video/timeline/makeCaptionEdit.ts +5 -1
- package/src/video/timeline/multiSelectOps.ts +214 -55
- package/src/video/timeline/placement.ts +282 -0
- package/src/video/timeline/timeline-model.ts +919 -0
- package/src/video/timeline/useItemDragDrop.ts +151 -178
- package/src/video/timeline/useTimelineZoom.ts +25 -60
- package/src/video/timeline/utils.ts +0 -12
- package/src/video/use-report-context.ts +75 -0
- package/src/video/preview/OverlayPropsModal.tsx +0 -292
- package/src/video/preview/__tests__/OverlayPropsModal.test.tsx +0 -32
- package/src/video/timeline/AudioTrackRow.tsx +0 -404
- package/src/video/timeline/AudioWaveformLayer.tsx +0 -117
- package/src/video/timeline/CaptionTrackRow.tsx +0 -235
- package/src/video/timeline/PlayheadLine.tsx +0 -18
- package/src/video/timeline/TranscriptModal.tsx +0 -70
- package/src/video/timeline/TranscriptPanel.tsx +0 -273
- package/src/video/timeline/VisualTrackRow.tsx +0 -300
- package/src/video/timeline/__tests__/CaptionTrackRow.test.tsx +0 -241
- package/src/video/timeline/__tests__/PlayheadLine.test.tsx +0 -60
- package/src/video/timeline/__tests__/TranscriptModal.test.tsx +0 -41
- package/src/video/timeline/__tests__/TranscriptPanel.test.tsx +0 -184
- package/src/video/timeline/__tests__/useItemDragDrop.test.ts +0 -72
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SP4 T7 — the Preparing placeholder's show-delay and the debug HUD's gate.
|
|
3
|
+
*
|
|
4
|
+
* `EngineSurface`'s canvas plumbing (attach identity, z-index, transform
|
|
5
|
+
* container placement) is T6's concern and already covered by
|
|
6
|
+
* `PreviewPlayer.engine.test.tsx`. What's new here: the placeholder must
|
|
7
|
+
* survive a sustained `preparing` but never flash for a one-tick transitional
|
|
8
|
+
* one, and the HUD must render only when `debugHud` is truthy.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
11
|
+
import { render, screen, act, cleanup } from '@testing-library/react'
|
|
12
|
+
import EngineSurface from '../EngineSurface'
|
|
13
|
+
import type { EngineStats } from '../../../engine'
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
cleanup()
|
|
17
|
+
vi.useRealTimers()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const noStats = () => null as EngineStats | null
|
|
21
|
+
|
|
22
|
+
describe('EngineSurface Preparing placeholder', () => {
|
|
23
|
+
it('does not show for `video`, even well past the show-delay', () => {
|
|
24
|
+
vi.useFakeTimers()
|
|
25
|
+
render(<EngineSurface attach={() => {}} picture="video" getStats={noStats} />)
|
|
26
|
+
act(() => { vi.advanceTimersByTime(5_000) })
|
|
27
|
+
expect(screen.queryByText('Preparing preview…')).toBeNull()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('does not show for `black` or `opaque`', () => {
|
|
31
|
+
vi.useFakeTimers()
|
|
32
|
+
const { rerender } = render(<EngineSurface attach={() => {}} picture="black" getStats={noStats} />)
|
|
33
|
+
act(() => { vi.advanceTimersByTime(5_000) })
|
|
34
|
+
expect(screen.queryByText('Preparing preview…')).toBeNull()
|
|
35
|
+
|
|
36
|
+
rerender(<EngineSurface attach={() => {}} picture="opaque" getStats={noStats} />)
|
|
37
|
+
act(() => { vi.advanceTimersByTime(5_000) })
|
|
38
|
+
expect(screen.queryByText('Preparing preview…')).toBeNull()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('shows once `preparing` has been sustained past the show-delay', () => {
|
|
42
|
+
vi.useFakeTimers()
|
|
43
|
+
render(<EngineSurface attach={() => {}} picture="preparing" reason="no editing proxy yet" getStats={noStats} />)
|
|
44
|
+
|
|
45
|
+
// Not yet — still inside the delay window.
|
|
46
|
+
act(() => { vi.advanceTimersByTime(100) })
|
|
47
|
+
expect(screen.queryByText('Preparing preview…')).toBeNull()
|
|
48
|
+
|
|
49
|
+
act(() => { vi.advanceTimersByTime(200) })
|
|
50
|
+
expect(screen.getByText('Preparing preview…')).toBeTruthy()
|
|
51
|
+
expect(screen.getByText('Preparing preview…').closest('.montaj-engine-preparing')?.getAttribute('title')).toBe(
|
|
52
|
+
'no editing proxy yet',
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('never shows for a one-tick transitional `preparing` inside a boundary swap', () => {
|
|
57
|
+
vi.useFakeTimers()
|
|
58
|
+
const { rerender } = render(<EngineSurface attach={() => {}} picture="video" getStats={noStats} />)
|
|
59
|
+
|
|
60
|
+
rerender(<EngineSurface attach={() => {}} picture="preparing" getStats={noStats} />)
|
|
61
|
+
act(() => { vi.advanceTimersByTime(50) })
|
|
62
|
+
// Resolves before the delay elapses — the very next clip's session landed.
|
|
63
|
+
rerender(<EngineSurface attach={() => {}} picture="video" getStats={noStats} />)
|
|
64
|
+
act(() => { vi.advanceTimersByTime(5_000) })
|
|
65
|
+
|
|
66
|
+
expect(screen.queryByText('Preparing preview…')).toBeNull()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('hides immediately when `preparing` resolves after already being shown', () => {
|
|
70
|
+
vi.useFakeTimers()
|
|
71
|
+
const { rerender } = render(<EngineSurface attach={() => {}} picture="preparing" getStats={noStats} />)
|
|
72
|
+
act(() => { vi.advanceTimersByTime(300) })
|
|
73
|
+
expect(screen.getByText('Preparing preview…')).toBeTruthy()
|
|
74
|
+
|
|
75
|
+
rerender(<EngineSurface attach={() => {}} picture="video" getStats={noStats} />)
|
|
76
|
+
expect(screen.queryByText('Preparing preview…')).toBeNull()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('EngineSurface debug HUD gate', () => {
|
|
81
|
+
it('renders nothing when `debugHud` is absent or false', () => {
|
|
82
|
+
const { container, rerender } = render(
|
|
83
|
+
<EngineSurface attach={() => {}} picture="video" getStats={noStats} />,
|
|
84
|
+
)
|
|
85
|
+
expect(container.querySelector('.montaj-engine-debug-hud')).toBeNull()
|
|
86
|
+
|
|
87
|
+
rerender(<EngineSurface attach={() => {}} picture="video" debugHud={false} getStats={noStats} />)
|
|
88
|
+
expect(container.querySelector('.montaj-engine-debug-hud')).toBeNull()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('renders fps/dropped/buffered/clock when `debugHud` is true', () => {
|
|
92
|
+
const stats: EngineStats = { fps: 29.97, dropped: 3, buffered: 5, clock: 'audio' }
|
|
93
|
+
const { container } = render(
|
|
94
|
+
<EngineSurface attach={() => {}} picture="video" debugHud getStats={() => stats} />,
|
|
95
|
+
)
|
|
96
|
+
const hud = container.querySelector('.montaj-engine-debug-hud')
|
|
97
|
+
expect(hud).not.toBeNull()
|
|
98
|
+
expect(hud!.textContent).toContain('fps 30.0')
|
|
99
|
+
expect(hud!.textContent).toContain('dropped 3')
|
|
100
|
+
expect(hud!.textContent).toContain('buffered 5')
|
|
101
|
+
expect(hud!.textContent).toContain('clock audio')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('renders regardless of the Preparing placeholder', () => {
|
|
105
|
+
vi.useFakeTimers()
|
|
106
|
+
const stats: EngineStats = { fps: 0, dropped: 0, buffered: 0, clock: 'fallback' }
|
|
107
|
+
const { container } = render(
|
|
108
|
+
<EngineSurface attach={() => {}} picture="preparing" debugHud getStats={() => stats} />,
|
|
109
|
+
)
|
|
110
|
+
act(() => { vi.advanceTimersByTime(300) })
|
|
111
|
+
expect(container.querySelector('.montaj-engine-debug-hud')).not.toBeNull()
|
|
112
|
+
expect(container.querySelector('.montaj-engine-preparing')).not.toBeNull()
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -97,8 +97,10 @@ describe('OverlayItemsLayer — double-click to edit overlay', () => {
|
|
|
97
97
|
const wrapper = container.querySelector('div') as HTMLDivElement
|
|
98
98
|
fireEvent.dblClick(wrapper)
|
|
99
99
|
|
|
100
|
-
//
|
|
101
|
-
//
|
|
100
|
+
// Prop editing lives in the right panel's Content tab now — there is no
|
|
101
|
+
// dialog anywhere, so the layer has nothing to render for it. The textbox
|
|
102
|
+
// assertion is the load-bearing one (no INLINE editor in the layer); the
|
|
103
|
+
// two below are belt-and-braces against a modal being reintroduced here.
|
|
102
104
|
expect(screen.queryByRole('textbox')).toBeNull()
|
|
103
105
|
expect(screen.queryByRole('heading', { name: 'Edit overlay' })).toBeNull()
|
|
104
106
|
expect(screen.queryByRole('button', { name: 'Edit overlay props' })).toBeNull()
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/// <reference types="vitest/globals" />
|
|
2
|
+
import { render } from '@testing-library/react'
|
|
3
|
+
import { geometryAt, geometryFor } from '@bycrux/timeline-core'
|
|
4
|
+
import type { EditorProject, VisualItem } from '../../../schema'
|
|
5
|
+
import type { OverlayFactory } from '../../../types'
|
|
6
|
+
import type { OverlayChanges } from '../useDragOverlay'
|
|
7
|
+
import OverlayItemsLayer from '../OverlayItemsLayer'
|
|
8
|
+
|
|
9
|
+
// T1.2 (SP9b Phase 1) — the preview must sample curve-aware geometry through
|
|
10
|
+
// @bycrux/timeline-core's `geometryAt` for keyframed OVERLAYS, and must NOT
|
|
11
|
+
// animate image/video CLIPS even when they carry a `keyframes` field: the
|
|
12
|
+
// renderer composites clips through `geometryFor` directly (encode-segment.js
|
|
13
|
+
// :305 and :375) with no per-frame browser step to bake a moving transform
|
|
14
|
+
// into, so animating a clip here would be a preview/render divergence — see
|
|
15
|
+
// the comment at OverlayItemsLayer.tsx's interactive-tracks geometry call
|
|
16
|
+
// site. No easing/interpolation math is duplicated in this file — every
|
|
17
|
+
// expected value below is produced by calling geometryAt/geometryFor
|
|
18
|
+
// directly, the same functions the component itself calls.
|
|
19
|
+
|
|
20
|
+
const emptySnap = { x: false, y: false, left: false, right: false, top: false, bottom: false }
|
|
21
|
+
|
|
22
|
+
function makeProject(): EditorProject {
|
|
23
|
+
return {
|
|
24
|
+
id: 'p',
|
|
25
|
+
status: 'draft',
|
|
26
|
+
settings: { resolution: [1080, 1920], fps: 30 },
|
|
27
|
+
tracks: [[]],
|
|
28
|
+
} as unknown as EditorProject
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderLayer(item: VisualItem, opts: {
|
|
32
|
+
currentTime: number
|
|
33
|
+
selected?: boolean
|
|
34
|
+
liveOffset?: { id: string; x: number; y: number } | null
|
|
35
|
+
liveScale?: { id: string; scale: number; scaleX: number; scaleY: number } | null
|
|
36
|
+
liveRotation?: { id: string; rotation: number } | null
|
|
37
|
+
onOverlayChange?: (id: string, changes: OverlayChanges) => void
|
|
38
|
+
}) {
|
|
39
|
+
const utils = render(
|
|
40
|
+
<OverlayItemsLayer
|
|
41
|
+
project={makeProject()}
|
|
42
|
+
currentTime={opts.currentTime}
|
|
43
|
+
isPlaying={false}
|
|
44
|
+
isCanvasProject={false}
|
|
45
|
+
overlayTracks={[[item]]}
|
|
46
|
+
tracks0NonVideo={[]}
|
|
47
|
+
renderScale={0.2}
|
|
48
|
+
selectedOverlayId={opts.selected ? item.id : undefined}
|
|
49
|
+
containerRef={{ current: document.createElement('div') }}
|
|
50
|
+
dragState={null}
|
|
51
|
+
setDragState={vi.fn()}
|
|
52
|
+
liveOffset={opts.liveOffset ?? null}
|
|
53
|
+
liveScale={opts.liveScale ?? null}
|
|
54
|
+
liveRotation={opts.liveRotation ?? null}
|
|
55
|
+
snapGuides={emptySnap}
|
|
56
|
+
snapRotation={null}
|
|
57
|
+
onOverlayChange={opts.onOverlayChange}
|
|
58
|
+
compileOverlay={vi.fn(async (): Promise<OverlayFactory> => () => null)}
|
|
59
|
+
fileUrl={(pth: string) => pth}
|
|
60
|
+
/>,
|
|
61
|
+
)
|
|
62
|
+
// The item's wrapper is the outermost div — see OverlayItemsLayer.edit.test.tsx.
|
|
63
|
+
const wrapper = utils.container.querySelector('div') as HTMLDivElement
|
|
64
|
+
return { ...utils, wrapper }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
69
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
70
|
+
})
|
|
71
|
+
afterEach(() => vi.restoreAllMocks())
|
|
72
|
+
|
|
73
|
+
// start=5; offsetX animates 0 -> 100 and opacity animates 1 -> 0 across
|
|
74
|
+
// item-relative t=[0,10]. Chosen so that treating absolute `currentTime` as
|
|
75
|
+
// localT (the bug this file guards against) produces a clearly different,
|
|
76
|
+
// wrong number at currentTime=5 than the correct item-relative sample.
|
|
77
|
+
function keyframedOverlay(over: Partial<VisualItem> = {}): VisualItem {
|
|
78
|
+
return {
|
|
79
|
+
id: 'kf-overlay',
|
|
80
|
+
type: 'overlay',
|
|
81
|
+
src: 'o.jsx',
|
|
82
|
+
start: 5,
|
|
83
|
+
end: 15,
|
|
84
|
+
props: {},
|
|
85
|
+
keyframes: [
|
|
86
|
+
{ prop: 'offsetX', points: [{ t: 0, value: 0 }, { t: 10, value: 100 }] },
|
|
87
|
+
{ prop: 'opacity', points: [{ t: 0, value: 1 }, { t: 10, value: 0 }] },
|
|
88
|
+
],
|
|
89
|
+
...over,
|
|
90
|
+
} as VisualItem
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// PARITY-CRITICAL, KEEP IN SYNC WITH render/test/overlay-transform-parity
|
|
94
|
+
// .test.mjs's `previewStyle` (verbatim copy of OverlayItemsLayer.tsx's own
|
|
95
|
+
// `wrapperStyle` template — that file carries a matching comment pointing
|
|
96
|
+
// back here). That suite proves the RENDER bake matches this exact literal
|
|
97
|
+
// template, but it cannot import this .tsx component into its plain
|
|
98
|
+
// node:test runner, so it hand-transcribes the template instead of reading
|
|
99
|
+
// it from the real component. The test below is what closes that gap: it
|
|
100
|
+
// pins the SAME template against what OverlayItemsLayer ACTUALLY renders, so
|
|
101
|
+
// a drift in the component's real template fails THIS test even though the
|
|
102
|
+
// render-side suite has no way to see it.
|
|
103
|
+
//
|
|
104
|
+
// `scale(sx, sy)` is a strict superset of the old `scale(s)`: on an item with
|
|
105
|
+
// no scaleX/scaleY the resolver hands back both axes equal to `scale`, so the
|
|
106
|
+
// two arguments are the same number and the box is unchanged. The render side
|
|
107
|
+
// is migrating onto this two-argument form under its own slice and still
|
|
108
|
+
// transcribes the one-argument form for now — a known, tracked gap.
|
|
109
|
+
function previewStyle(g: ReturnType<typeof geometryAt>) {
|
|
110
|
+
return {
|
|
111
|
+
transform: `translate(${g.offsetX}%, ${g.offsetY}%) rotate(${g.rotation}deg) scale(${g.scaleX}, ${g.scaleY})`,
|
|
112
|
+
transformOrigin: 'center center',
|
|
113
|
+
opacity: g.opacity,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describe('OverlayItemsLayer — keyframed geometry (T1.2)', () => {
|
|
118
|
+
it('pins the REAL rendered wrapper style against the same literal template the render-side parity test uses', () => {
|
|
119
|
+
const item = keyframedOverlay()
|
|
120
|
+
const g = geometryAt(item, 'overlay', 10 - item.start) // currentTime=10 -> localT=5, mid-curve
|
|
121
|
+
const want = previewStyle(g)
|
|
122
|
+
|
|
123
|
+
const { wrapper } = renderLayer(item, { currentTime: 10 })
|
|
124
|
+
|
|
125
|
+
expect(wrapper.style.transform).toBe(want.transform)
|
|
126
|
+
expect(wrapper.style.transformOrigin).toBe(want.transformOrigin)
|
|
127
|
+
expect(wrapper.style.opacity).toBe(String(want.opacity))
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('samples different transform/opacity at two currentTime values, matching geometryAt', () => {
|
|
131
|
+
const item = keyframedOverlay()
|
|
132
|
+
|
|
133
|
+
const at5 = renderLayer(item, { currentTime: 5 })
|
|
134
|
+
const g5 = geometryAt(item, 'overlay', 5 - item.start)
|
|
135
|
+
expect(at5.wrapper.style.transform).toContain(`translate(${g5.offsetX}%,`)
|
|
136
|
+
expect(at5.wrapper.style.opacity).toBe(String(g5.opacity))
|
|
137
|
+
at5.unmount()
|
|
138
|
+
|
|
139
|
+
const at10 = renderLayer(item, { currentTime: 10 })
|
|
140
|
+
const g10 = geometryAt(item, 'overlay', 10 - item.start)
|
|
141
|
+
expect(at10.wrapper.style.transform).toContain(`translate(${g10.offsetX}%,`)
|
|
142
|
+
expect(at10.wrapper.style.opacity).toBe(String(g10.opacity))
|
|
143
|
+
at10.unmount()
|
|
144
|
+
|
|
145
|
+
// Sanity: the two samples actually differ, so this test cannot pass by
|
|
146
|
+
// accident even if geometryAt were never wired up at all.
|
|
147
|
+
expect(g5.offsetX).not.toBe(g10.offsetX)
|
|
148
|
+
expect(g5.opacity).not.toBe(g10.opacity)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('uses ITEM-RELATIVE time, not absolute project time', () => {
|
|
152
|
+
// item.start = 5, keyframe at localT=0 has offsetX=0. At currentTime=5
|
|
153
|
+
// the correct localT is 0, so offsetX must be exactly 0. A wiring bug
|
|
154
|
+
// that passed absolute `currentTime` as localT would land at localT=5 —
|
|
155
|
+
// halfway between the t=0 (value 0) and t=10 (value 100) keyframes —
|
|
156
|
+
// producing offsetX=50 instead. The two outcomes are far enough apart
|
|
157
|
+
// that this cannot pass by luck.
|
|
158
|
+
const item = keyframedOverlay()
|
|
159
|
+
const { wrapper } = renderLayer(item, { currentTime: 5 })
|
|
160
|
+
|
|
161
|
+
expect(wrapper.style.transform).toContain('translate(0%,')
|
|
162
|
+
expect(wrapper.style.transform).not.toContain('translate(50%,')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('a keyframe-less overlay renders identically at every currentTime (no regression)', () => {
|
|
166
|
+
const item = {
|
|
167
|
+
id: 'static-overlay', type: 'overlay', src: 'o.jsx', start: 0, end: 10,
|
|
168
|
+
offsetX: 12, offsetY: -8, scale: 1.3, rotation: 15, opacity: 0.6, props: {},
|
|
169
|
+
} as VisualItem
|
|
170
|
+
|
|
171
|
+
const at2 = renderLayer(item, { currentTime: 2 })
|
|
172
|
+
const transformAt2 = at2.wrapper.style.transform
|
|
173
|
+
const opacityAt2 = at2.wrapper.style.opacity
|
|
174
|
+
at2.unmount()
|
|
175
|
+
|
|
176
|
+
const at8 = renderLayer(item, { currentTime: 8 })
|
|
177
|
+
expect(at8.wrapper.style.transform).toBe(transformAt2)
|
|
178
|
+
expect(at8.wrapper.style.opacity).toBe(opacityAt2)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('ANIMATES an image clip carrying keyframes (SP9d)', () => {
|
|
182
|
+
// The clip/overlay asymmetry is gone for geometry: encode-segment.js now
|
|
183
|
+
// compiles a clip's curves into ffmpeg expressions, so motion shown here is
|
|
184
|
+
// motion the export reproduces.
|
|
185
|
+
const item = {
|
|
186
|
+
id: 'kf-image', type: 'image', src: 'img.png', start: 5, end: 15,
|
|
187
|
+
offsetX: 42, opacity: 0.9,
|
|
188
|
+
keyframes: [{ prop: 'offsetX', points: [{ t: 0, value: 0 }, { t: 10, value: 100 }] }],
|
|
189
|
+
} as VisualItem
|
|
190
|
+
|
|
191
|
+
const at5 = renderLayer(item, { currentTime: 5 })
|
|
192
|
+
const t5 = at5.wrapper.style.transform
|
|
193
|
+
at5.unmount()
|
|
194
|
+
const at10 = renderLayer(item, { currentTime: 10 })
|
|
195
|
+
const t10 = at10.wrapper.style.transform
|
|
196
|
+
|
|
197
|
+
// localT 0 → 0%, localT 5 → 50%. The static 42 is NOT what either shows.
|
|
198
|
+
expect(t5).toContain('translate(0%,')
|
|
199
|
+
expect(t10).toContain('translate(50%,')
|
|
200
|
+
expect(t10).not.toBe(t5)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('ANIMATES a video clip carrying keyframes (SP9d)', () => {
|
|
204
|
+
const item = {
|
|
205
|
+
id: 'kf-video', type: 'video', src: 'vid.mp4', start: 5, end: 15,
|
|
206
|
+
offsetX: 7, inPoint: 0,
|
|
207
|
+
keyframes: [{ prop: 'offsetX', points: [{ t: 0, value: 0 }, { t: 10, value: 100 }] }],
|
|
208
|
+
} as VisualItem
|
|
209
|
+
|
|
210
|
+
const at5 = renderLayer(item, { currentTime: 5 })
|
|
211
|
+
const t5 = at5.wrapper.style.transform
|
|
212
|
+
at5.unmount()
|
|
213
|
+
const at10 = renderLayer(item, { currentTime: 10 })
|
|
214
|
+
expect(at10.wrapper.style.transform).not.toBe(t5)
|
|
215
|
+
expect(at10.wrapper.style.transform).toContain('translate(50%,')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('does NOT animate a CLIP\'s opacity, even with a curve on it', () => {
|
|
219
|
+
// The one property that stays static on a clip: ffmpeg's colorchannelmixer
|
|
220
|
+
// takes its alpha gain as a <double> and accepts no expression, so a fade
|
|
221
|
+
// shown here could never be exported. Overlays are exempt — they bake in a
|
|
222
|
+
// browser. See keyframeOps.canKeyframeProp.
|
|
223
|
+
const item = {
|
|
224
|
+
id: 'kf-op', type: 'video', src: 'vid.mp4', start: 5, end: 15,
|
|
225
|
+
offsetX: 0, opacity: 0.6, inPoint: 0,
|
|
226
|
+
keyframes: [{ prop: 'opacity', points: [{ t: 0, value: 0 }, { t: 10, value: 1 }] }],
|
|
227
|
+
} as VisualItem
|
|
228
|
+
|
|
229
|
+
const at5 = renderLayer(item, { currentTime: 5 })
|
|
230
|
+
const o5 = at5.wrapper.style.opacity
|
|
231
|
+
at5.unmount()
|
|
232
|
+
const at10 = renderLayer(item, { currentTime: 10 })
|
|
233
|
+
expect(at10.wrapper.style.opacity).toBe(o5)
|
|
234
|
+
expect(Number(o5)).toBeCloseTo(0.6)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('a live drag overrides the sampled curve value', () => {
|
|
238
|
+
const item = keyframedOverlay()
|
|
239
|
+
const { wrapper } = renderLayer(item, {
|
|
240
|
+
currentTime: 10,
|
|
241
|
+
liveOffset: { id: item.id, x: -33, y: 4 },
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
// Without the drag, offsetX at currentTime=10 (localT=5) samples to 50 —
|
|
245
|
+
// the drag value must win instead.
|
|
246
|
+
expect(wrapper.style.transform).toContain('translate(-33%, 4%)')
|
|
247
|
+
expect(wrapper.style.transform).not.toContain('translate(50%,')
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Per-axis scale. The preview's wrapper emits the two-argument
|
|
253
|
+
// `scale(scaleX, scaleY)`; every chrome element inside it counter-scales by the
|
|
254
|
+
// inverse of BOTH axes so handles stay square rather than stretching with the
|
|
255
|
+
// item. A legacy item that carries only the uniform `scale` must resolve both
|
|
256
|
+
// axes to that same number and render exactly the box it always did.
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
describe('OverlayItemsLayer — per-axis scale', () => {
|
|
260
|
+
const baseOverlay = { id: 'ax', type: 'overlay', src: 'o.jsx', start: 0, end: 10, props: {} }
|
|
261
|
+
|
|
262
|
+
it('renders a non-uniform item as scale(scaleX, scaleY)', () => {
|
|
263
|
+
const item = { ...baseOverlay, scaleX: 1.5, scaleY: 0.5 } as VisualItem
|
|
264
|
+
const { wrapper } = renderLayer(item, { currentTime: 1 })
|
|
265
|
+
expect(wrapper.style.transform).toContain('scale(1.5, 0.5)')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('a legacy scale-only item renders both arguments equal (back-compat)', () => {
|
|
269
|
+
const item = { ...baseOverlay, scale: 1.3 } as VisualItem
|
|
270
|
+
const { wrapper } = renderLayer(item, { currentTime: 1 })
|
|
271
|
+
expect(wrapper.style.transform).toContain('scale(1.3, 1.3)')
|
|
272
|
+
// …and that is the resolver's doing, not this file's: the same fallback the
|
|
273
|
+
// renderer reads. Guards against the preview inventing its own default.
|
|
274
|
+
const g = geometryFor(item, 'overlay')
|
|
275
|
+
expect(g.scaleX).toBe(1.3)
|
|
276
|
+
expect(g.scaleY).toBe(1.3)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('an item with NO scale at all renders scale(1, 1)', () => {
|
|
280
|
+
const item = { ...baseOverlay } as VisualItem
|
|
281
|
+
const { wrapper } = renderLayer(item, { currentTime: 1 })
|
|
282
|
+
expect(wrapper.style.transform).toContain('scale(1, 1)')
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('falls back to the uniform `scale` one axis at a time', () => {
|
|
286
|
+
// Only scaleY is per-axis; scaleX must inherit `scale`.
|
|
287
|
+
const item = { ...baseOverlay, scale: 2, scaleY: 0.25 } as VisualItem
|
|
288
|
+
const { wrapper } = renderLayer(item, { currentTime: 1 })
|
|
289
|
+
expect(wrapper.style.transform).toContain('scale(2, 0.25)')
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('a live resize drag overrides each axis independently', () => {
|
|
293
|
+
const item = { ...baseOverlay, scale: 1 } as VisualItem
|
|
294
|
+
const { wrapper } = renderLayer(item, {
|
|
295
|
+
currentTime: 1,
|
|
296
|
+
liveScale: { id: item.id, scale: 1, scaleX: 3, scaleY: 0.75 },
|
|
297
|
+
})
|
|
298
|
+
expect(wrapper.style.transform).toContain('scale(3, 0.75)')
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('samples keyframed scaleX/scaleY through geometryAt', () => {
|
|
302
|
+
const item = {
|
|
303
|
+
...baseOverlay,
|
|
304
|
+
keyframes: [
|
|
305
|
+
{ prop: 'scaleX', points: [{ t: 0, value: 1 }, { t: 10, value: 3 }] },
|
|
306
|
+
{ prop: 'scaleY', points: [{ t: 0, value: 1 }, { t: 10, value: 2 }] },
|
|
307
|
+
],
|
|
308
|
+
} as VisualItem
|
|
309
|
+
|
|
310
|
+
const at0 = renderLayer(item, { currentTime: 0 })
|
|
311
|
+
const g0 = geometryAt(item, 'overlay', 0)
|
|
312
|
+
expect(at0.wrapper.style.transform).toContain(`scale(${g0.scaleX}, ${g0.scaleY})`)
|
|
313
|
+
at0.unmount()
|
|
314
|
+
|
|
315
|
+
const at5 = renderLayer(item, { currentTime: 5 })
|
|
316
|
+
const g5 = geometryAt(item, 'overlay', 5)
|
|
317
|
+
expect(at5.wrapper.style.transform).toContain(`scale(${g5.scaleX}, ${g5.scaleY})`)
|
|
318
|
+
|
|
319
|
+
// Sanity: the axes actually diverge, so this cannot pass by rendering 1,1.
|
|
320
|
+
expect(g5.scaleX).not.toBe(g5.scaleY)
|
|
321
|
+
expect(g5.scaleX).not.toBe(g0.scaleX)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('counter-scales the corner handles per axis so they stay square', () => {
|
|
325
|
+
const item = { ...baseOverlay, scaleX: 2, scaleY: 0.5 } as VisualItem
|
|
326
|
+
const { container } = renderLayer(item, { currentTime: 1, selected: true })
|
|
327
|
+
const nw = container.querySelector('.cursor-nw-resize') as HTMLElement
|
|
328
|
+
expect(nw).toBeTruthy()
|
|
329
|
+
// scale(1/2, 1/0.5), then the -50%/-50% that centres the square on the
|
|
330
|
+
// corner. A single scale(1/s) would leave the handle stretched.
|
|
331
|
+
expect(nw.style.transform).toBe('scale(0.5, 2) translate(-50%, -50%)')
|
|
332
|
+
const se = container.querySelector('.cursor-se-resize') as HTMLElement
|
|
333
|
+
expect(se.style.transform).toBe('scale(0.5, 2) translate(-50%, -50%)')
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('counter-scales the rotate handle per axis', () => {
|
|
337
|
+
const item = { ...baseOverlay, scaleX: 2, scaleY: 0.5 } as VisualItem
|
|
338
|
+
const { container } = renderLayer(item, { currentTime: 1, selected: true })
|
|
339
|
+
const rot = container.querySelector('.cursor-grab.flex-col') as HTMLElement
|
|
340
|
+
expect(rot).toBeTruthy()
|
|
341
|
+
expect(rot.style.transform).toBe('translateX(-50%) translateY(-100%) scale(0.5, 2)')
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('counter-scales the fit control per axis', () => {
|
|
345
|
+
const item = {
|
|
346
|
+
id: 'ax-img', type: 'image', src: 'i.png', start: 0, end: 10, scaleX: 4, scaleY: 0.25,
|
|
347
|
+
} as VisualItem
|
|
348
|
+
const { container, getByRole } = renderLayer(item, { currentTime: 1, selected: true, onOverlayChange: vi.fn() })
|
|
349
|
+
// Located through its own buttons rather than a colour class — the control
|
|
350
|
+
// is token-driven now, so there is no colour left to key off.
|
|
351
|
+
const fit = getByRole('button', { name: 'cover' }).parentElement as HTMLElement
|
|
352
|
+
expect(fit).toBeTruthy()
|
|
353
|
+
expect(container.contains(fit)).toBe(true)
|
|
354
|
+
expect(fit.style.transform).toBe('translateX(-50%) translateY(140%) scale(0.25, 4)')
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('a legacy uniform item still counter-scales to a square handle', () => {
|
|
358
|
+
const item = { ...baseOverlay, scale: 2 } as VisualItem
|
|
359
|
+
const { container } = renderLayer(item, { currentTime: 1, selected: true })
|
|
360
|
+
const nw = container.querySelector('.cursor-nw-resize') as HTMLElement
|
|
361
|
+
expect(nw.style.transform).toBe('scale(0.5, 0.5) translate(-50%, -50%)')
|
|
362
|
+
})
|
|
363
|
+
})
|