Skip to content

Commit f97d08c

Browse files
committed
Release 1.6.6
1 parent 922a330 commit f97d08c

34 files changed

Lines changed: 1538 additions & 191 deletions

docs/Features/3D-Layers.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Prepared splat runtime metadata, native splat rasterization, preview, nested com
5555
- glTF / GLB base color textures are loaded from data URIs, external image URIs, or embedded bufferViews when `baseColorTexture` and `TEXCOORD_0` are present.
5656
- Textured glTF / GLB materials render unlit in the native pass, matching scan/photogrammetry assets better than the simple fallback directional light.
5757
- GLB sequences normalize every frame against the first renderable frame's bounds and preload nearby playback frames, avoiding per-frame center/scale jumps and visible current-frame loading flicker.
58+
- GLB sequences imported into an open project are copied to `Raw/<sequence-name>/` using the original frame names; existing same-size frame files are reused instead of written again.
5859
- Untextured models use ambient plus directional fallback lighting.
5960
- The Transform panel exposes a wireframe debug toggle for model clips.
6061
- Imported models use a native runtime/cache path.
@@ -117,6 +118,7 @@ Gaussian splat clips are imported through the SuperSplat-compatible `@playcanvas
117118
- Gaussian splats participate in scene cameras, object transforms, object-level effectors, preview, nested compositions, export, preload, and readiness checks through the same native shared-scene path.
118119
- Realtime splat rendering uses a worker-backed back-to-front order buffer based on the SuperSplat/PlayCanvas sorter approach. Precise export can still fall back to the existing GPU sort path.
119120
- Sequence splats follow the same shared runtime contract and preload nearby frames without replacing foreground playback with repeated loading overlays.
121+
- PLY/splat sequences imported into an open project are copied to `Raw/<sequence-name>/` using the original frame names; existing same-size frame files are reused instead of written again.
120122
- Imported splats and numbered splat sequences store media-panel stats: container label, file size, per-frame splat count, and total sequence splat count.
121123
- The Transform tab now exposes normal object transforms for gaussian splats. Scene navigation lives on camera clips.
122124
- Large gaussian splats show viewport loading progress during project restore, URL fetch, parser work, normalization, and GPU upload.

docs/Features/Project-Persistence.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ MyProject/
9898
+-- Raw/ # Auto-copied media files (portable)
9999
| +-- Interview_01.mp4
100100
| +-- Music.wav
101+
| +-- hero/ # Imported GLB sequence frames
102+
| | +-- hero000000.glb
103+
| | +-- hero000001.glb
104+
| +-- scan/ # Imported PLY/splat sequence frames
105+
| | +-- scan000000.ply
106+
| | +-- scan000001.ply
101107
+-- Downloads/ # Downloaded videos (platform subfolders)
102108
| +-- YT/
103109
| | +-- video_title.mp4
@@ -141,6 +147,8 @@ const PROJECT_FOLDERS = {
141147
### Auto-Copy to Raw Folder
142148
When importing media files (controlled by `copyMediaToProject` setting):
143149
- Files are **copied** to the project's `Raw/` folder
150+
- Numbered `.glb`, `.ply`, and `.splat` sequences are copied whenever a project is open, even when global auto-copy is off, so sequence frames survive reloads and project moves
151+
- Sequence frames are stored under `Raw/<sequence-name>/` with their original frame filenames
144152
- Original files remain untouched at their source location
145153
- If a file with the same name and size already exists, reuses the existing copy
146154
- If a file with the same name but different size exists, adds a numeric suffix
@@ -327,7 +335,7 @@ interface ProjectFile {
327335
- Composition view state per composition (playhead, zoom, scroll, in/out points)
328336
- Media panel column order and name width
329337
- Transcript language preference
330-
- Global MIDI state: enabled flag, transport bindings (`Play / Pause`, `Stop`), parameter mappings, mapping ranges, and invert flags
338+
- Global MIDI state: enabled flag, transport bindings (`Play / Pause`, `Stop`), parameter mappings, mapping ranges, invert flags, and damping flags
331339
- View toggles: thumbnails, waveforms, proxy, transcript markers
332340
- Changelog preferences (`showChangelogOnStartup`, `lastSeenChangelogVersion`)
333341
- Export panel state: live export settings, named export presets, and the selected preset

docs/Features/UI-Panels.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,9 @@ MasterSelects currently exposes 17 dockable panel types, plus the Slot Grid over
183183
- Includes global transport bindings, per-marker bindings, Slot Grid trigger bindings, and property parameter bindings
184184
- Each row shows the note, target, and resulting command behavior
185185
- Click any transport, marker, or slot mapping card to trigger the assigned action and preview what the MIDI note does
186+
- Mappings flash live while matching MIDI input is currently driving them
186187
- `Edit` opens inline controls for manual channel/note changes and marker reassignment
187-
- Parameter mappings expose inline `Min` / `Max` range inputs plus an `Invert` toggle
188+
- Parameter mappings expose inline `Min` / `Max` range inputs, an `Invert` toggle, and a `Damp` checkbox for smoothed value changes
188189
- `Learn` and `Clear` remain available directly from the panel
189190
- Marker bindings support `Jump To Marker`, `Play From Marker`, and `Jump To Marker And Stop`
190191
- Slot bindings can be created from the Slot Grid filled-slot context menu, which opens this panel with a pending `Listening...` mapping
@@ -426,7 +427,8 @@ MIDI Control (N devices)
426427
- `View -> Panels -> MIDI Mapping` opens the dedicated mapping panel
427428
- The panel lists all assigned transport, marker, slot, and parameter bindings
428429
- Clicking a binding previews the exact trigger path without needing a physical MIDI note
429-
- Parameter bindings can be range-adjusted and inverted directly in the mapping panel
430+
- Matching mappings flash in the panel when the hardware input is used
431+
- Parameter bindings can be range-adjusted, inverted, and damped directly in the mapping panel
430432
- Empty-state guidance points back to Settings and the marker right-click menu
431433
- Right-click a numeric parameter name in the Properties panel to open its MIDI menu and learn or clear a MIDI note/CC binding. Right-clicking the numeric value itself still resets the value to its default. Learned CC values map across the parameter's configured range and then use the same edit path as manual changes.
432434

src/changelog-data.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,25 @@
11
[
2+
{
3+
"date": "2026-04-30",
4+
"type": "fix",
5+
"title": "3D Sequence Playback and Scrubbing Stay Responsive",
6+
"description": "GLB frame sequences now throttle realtime model loading and hold the last renderable frame while the next asset prepares. PLY splat scrubbing keeps requesting the dragged target frame in the background instead of freezing on held frames.",
7+
"section": "3D / Native Scene"
8+
},
9+
{
10+
"date": "2026-04-30",
11+
"type": "improve",
12+
"title": "Sequence Imports Keep Project Raw Folders Tidy",
13+
"description": "PLY and GLB frame sequences copied into project storage now land in stable Raw subfolders and skip duplicate copies when matching files are already present.",
14+
"section": "Project Persistence"
15+
},
16+
{
17+
"date": "2026-04-30",
18+
"type": "improve",
19+
"title": "MIDI Mapping Controls Gain Broader Command Coverage",
20+
"description": "MIDI mappings now carry richer command metadata, range handling, and panel summaries so live-control setups are easier to inspect and restore.",
21+
"section": "MIDI"
22+
},
223
{
324
"date": "2026-04-27",
425
"type": "new",

src/components/panels/MIDIMappingPanel.css

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@
106106
0 0 20px color-mix(in srgb, var(--accent) 12%, transparent);
107107
}
108108

109+
.midi-mapping-card.is-active {
110+
border-color: color-mix(in srgb, var(--success-light) 70%, var(--border-color));
111+
background: color-mix(in srgb, var(--success-light) 10%, var(--bg-tertiary));
112+
box-shadow:
113+
0 0 0 1px color-mix(in srgb, var(--success-light) 38%, transparent),
114+
0 0 18px color-mix(in srgb, var(--success-light) 16%, transparent);
115+
}
116+
109117
.midi-mapping-card.is-learning {
110118
border-color: color-mix(in srgb, var(--accent) 60%, var(--border-color));
111119
background: color-mix(in srgb, var(--accent-subtle) 45%, var(--bg-tertiary));
@@ -190,7 +198,7 @@
190198
padding-top: var(--sp-2);
191199
border-top: 1px solid var(--border-color);
192200
display: grid;
193-
grid-template-columns: minmax(80px, 1fr) minmax(80px, 1fr) auto;
201+
grid-template-columns: minmax(80px, 1fr) minmax(80px, 1fr) auto auto;
194202
align-items: end;
195203
gap: var(--sp-2);
196204
}
@@ -212,6 +220,29 @@
212220
color: var(--accent);
213221
}
214222

223+
.midi-mapping-checkbox {
224+
min-height: 30px;
225+
display: inline-flex;
226+
align-items: center;
227+
gap: var(--sp-1);
228+
color: var(--text-secondary);
229+
font-size: var(--font-xs);
230+
white-space: nowrap;
231+
}
232+
233+
.midi-mapping-checkbox input {
234+
width: 14px;
235+
height: 14px;
236+
accent-color: var(--accent);
237+
}
238+
239+
.midi-mapping-checkbox span {
240+
color: inherit;
241+
font-size: inherit;
242+
text-transform: none;
243+
letter-spacing: 0;
244+
}
245+
215246
.midi-mapping-editor {
216247
margin-top: var(--sp-2);
217248
padding-top: var(--sp-2);

src/components/panels/MIDIMappingPanel.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export function MIDIMappingPanel() {
122122
cancelLearning,
123123
slotBindings,
124124
parameterBindings,
125+
activeMappingIds,
125126
} = useMIDI();
126127
const markers = useTimelineStore((state) => state.markers);
127128
const compositions = useMediaStore((state) => state.compositions);
@@ -334,6 +335,10 @@ export function MIDIMappingPanel() {
334335
updateParameterMIDIBinding(binding.id, { invert: !binding.invert });
335336
};
336337

338+
const toggleParameterDamping = (binding: MIDIParameterBinding) => {
339+
updateParameterMIDIBinding(binding.id, { damping: !binding.damping });
340+
};
341+
337342
const clearMapping = (mapping: MIDIMappingSummaryEntry) => {
338343
if (mapping.scope === 'transport') {
339344
setTransportMIDIBinding(mapping.action as MIDITransportAction, null);
@@ -528,6 +533,7 @@ export function MIDIMappingPanel() {
528533
{mappings.map((mapping) => {
529534
const isEditing = draft?.mappingId === mapping.id;
530535
const isParameterMapping = mapping.scope === 'parameter';
536+
const isActiveMapping = activeMappingIds[mapping.id] !== undefined;
531537
const channelValue = isEditing ? draft.channel : String(mapping.binding.channel);
532538
const noteValue = isEditing && !isParameterMapping
533539
? draft.note
@@ -572,7 +578,7 @@ export function MIDIMappingPanel() {
572578
return (
573579
<div
574580
key={mapping.id}
575-
className={`midi-mapping-card${isPreviewable && !isEditing ? ' is-clickable' : ''}${previewingMappingId === mapping.id ? ' is-previewing' : ''}`}
581+
className={`midi-mapping-card${isPreviewable && !isEditing ? ' is-clickable' : ''}${previewingMappingId === mapping.id ? ' is-previewing' : ''}${isActiveMapping ? ' is-active' : ''}`}
576582
role={isPreviewable && !isEditing ? 'button' : undefined}
577583
tabIndex={isPreviewable && !isEditing ? 0 : undefined}
578584
onClick={isPreviewable && !isEditing ? () => previewMapping(mapping) : undefined}
@@ -652,6 +658,17 @@ export function MIDIMappingPanel() {
652658
>
653659
Invert
654660
</button>
661+
<label
662+
className="midi-mapping-checkbox"
663+
title="Smooth MIDI parameter changes"
664+
>
665+
<input
666+
type="checkbox"
667+
checked={!!mapping.parameterBinding.damping}
668+
onChange={() => toggleParameterDamping(mapping.parameterBinding!)}
669+
/>
670+
<span>Damp</span>
671+
</label>
655672
</div>
656673
)}
657674

src/engine/WebGPUEngine.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Orchestrates: PerformanceStats, RenderTargetManager, OutputWindowManager,
33
// RenderLoop, LayerCollector, Compositor, NestedCompRenderer
44

5+
import type { ModelSequenceData } from '../types';
56
import type { Layer, EngineStats } from './core/types';
67
// OutputWindow type no longer needed — state lives in renderTargetStore
78
import { WebGPUContext, type GPUPowerPreference } from './core/WebGPUContext';
@@ -661,6 +662,8 @@ export class WebGPUEngine {
661662
url?: string;
662663
fileName: string;
663664
file?: File;
665+
showProgress?: boolean;
666+
maxSplats?: number;
664667
}): Promise<boolean> {
665668
return this.renderDispatcher?.ensureGaussianSplatSceneLoaded(options) ?? false;
666669
}
@@ -669,8 +672,8 @@ export class WebGPUEngine {
669672
return this.renderDispatcher?.ensureSceneRendererInitialized(width, height) ?? false;
670673
}
671674

672-
async preloadSceneModelAsset(url: string, fileName: string): Promise<boolean> {
673-
return this.renderDispatcher?.preloadSceneModelAsset(url, fileName) ?? false;
675+
async preloadSceneModelAsset(url: string, fileName: string, modelSequence?: ModelSequenceData): Promise<boolean> {
676+
return this.renderDispatcher?.preloadSceneModelAsset(url, fileName, modelSequence) ?? false;
674677
}
675678

676679
async ensureExportLayersReady(layers: Layer[]): Promise<void> {

src/engine/export/preloadGaussianSplats.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ export async function preloadGaussianSplatsForExport(options: PreloadOptions): P
148148
url,
149149
fileName,
150150
file,
151+
showProgress: false,
151152
}),
152153
),
153154
);
@@ -194,24 +195,37 @@ export async function preload3DAssetsForExport(options: Preload3DOptions): Promi
194195
const modelPreloads = [...new Map(
195196
clips
196197
.filter((clip) => clip.source?.type === 'model' && !!clip.source.modelUrl)
197-
.map((clip) => [
198-
clip.source!.modelUrl!,
199-
{
200-
clipId: clip.id,
201-
modelUrl: clip.source!.modelUrl!,
202-
fileName: clip.file?.name ?? clip.name,
203-
},
204-
]),
198+
.map((clip) => {
199+
const mediaFileId = clip.mediaFileId ?? clip.source?.mediaFileId;
200+
const mediaFile = mediaFileId
201+
? useMediaStore.getState().files.find((file) => file.id === mediaFileId) ?? null
202+
: null;
203+
const modelSequence = clip.source?.modelSequence ?? mediaFile?.modelSequence;
204+
const preloadKey = modelSequence
205+
? `${clip.source!.modelUrl!}|sequence|${modelSequence.sequenceName ?? ''}|${modelSequence.frameCount}|${modelSequence.fps}`
206+
: clip.source!.modelUrl!;
207+
return [
208+
preloadKey,
209+
{
210+
clipId: clip.id,
211+
modelUrl: clip.source!.modelUrl!,
212+
fileName: clip.file?.name ?? clip.name,
213+
...(modelSequence ? { modelSequence } : {}),
214+
},
215+
] as const;
216+
}),
205217
).values()];
206218

207219
if (modelPreloads.length === 0) {
208220
return;
209221
}
210222

211223
const results = await Promise.allSettled(
212-
modelPreloads.map((preload) =>
213-
engine.preloadSceneModelAsset(preload.modelUrl, preload.fileName),
214-
),
224+
modelPreloads.map((preload) => (
225+
preload.modelSequence
226+
? engine.preloadSceneModelAsset(preload.modelUrl, preload.fileName, preload.modelSequence)
227+
: engine.preloadSceneModelAsset(preload.modelUrl, preload.fileName)
228+
)),
215229
);
216230
results.forEach((result, index) => {
217231
const clip = modelPreloads[index];

0 commit comments

Comments
 (0)