Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
6d9ac4c
feat(scene): add LightFX baking services
xubing0906 Aug 29, 2026
4678705
fix(scene): harden lightmap asset binding
xubing0906 Aug 29, 2026
e0cc2ff
update doc
xubing0906 Aug 31, 2026
7bfed94
Merge branch 'main' into main-light-probe-bake
xubing0906 Sep 4, 2026
3373ada
fix(scene): support LightFX baking in active renderer
xubing0906 Sep 4, 2026
d0a56fc
feat(scene): query lightmap bake information
xubing0906 Sep 8, 2026
4aef5f3
Merge branch 'main' into main-light-probe-bake
xubing0906 Sep 8, 2026
0abbf64
feat(scene): persist light probe bake settings
xubing0906 Sep 8, 2026
ef1b2d4
fix(scene): align lightmap options with Creator
xubing0906 Sep 8, 2026
3017aad
Merge branch 'main' into main-light-probe-bake
xubing0906 Sep 8, 2026
eb3ee08
Merge branch 'main' into main-light-probe-bake
xubing0906 Sep 9, 2026
3287450
fix(scene): 修复光照探针烘焙与清理的撤销快照
zenoskongfu Sep 10, 2026
dd589f5
fix(scene): 保留多探针组场景加载过程中的烘焙数据
zenoskongfu Sep 10, 2026
8f5e814
feat(scene): 补齐光照探针凸包与边界法线绘制
zenoskongfu Sep 10, 2026
7b41997
fix(scene): 串行化光照烘焙与清理的场景事务
zenoskongfu Sep 10, 2026
d2861b0
fix(scene): 补齐跨渲染实例的光照事务互斥
zenoskongfu Sep 10, 2026
6a1f79a
feat(scene): 提供光照探针烘焙能力与宿主忙状态查询
zenoskongfu Sep 10, 2026
fd27afa
fix(scene): 同步光照探针组位移并保留撤销结果
zenoskongfu Sep 10, 2026
2c05f87
fix(scene): 修正探针烘焙与清理的自动保存基线
zenoskongfu Sep 10, 2026
e05d4bb
fix(scene): 补齐光照贴图结果的撤销目标与空引用
zenoskongfu Sep 10, 2026
1fac1df
fix(scene): 按烘焙版本保留光照贴图资产
zenoskongfu Sep 10, 2026
60cb8c8
fix(scene): 刷新地形光照贴图撤销后的渲染块绑定
zenoskongfu Sep 10, 2026
831ac21
feat(scene): 提供光照贴图结果与资产版本能力查询
zenoskongfu Sep 10, 2026
157006c
fix(scene): 按场景与任务归属取消光照烘焙
zenoskongfu Sep 10, 2026
54664c8
feat(scene): 提供光照探针取消能力与就绪查询
zenoskongfu Sep 10, 2026
c7dc8bc
feat(scene): 提供光照贴图取消能力与就绪查询
zenoskongfu Sep 10, 2026
b2ea9b2
feat(scene): 整合探针编辑并修复跨组选择与坐标显示
zenoskongfu Sep 10, 2026
b690ed4
fix(scene): 保留探针改父节点的全局结果撤销快照
zenoskongfu Sep 10, 2026
1bbadaf
fix(scene): 撤销探针编辑时同步引擎注册缓存
zenoskongfu Sep 10, 2026
ec824ae
feat(lighting): 提供烘焙对象诊断与任务归属日志
zenoskongfu Sep 10, 2026
6781dcb
docs(lighting): 同步探针编辑与任务诊断接口说明
zenoskongfu Sep 10, 2026
42b2880
fix(scene): 修复光照探针编辑前后的工具状态恢复
zenoskongfu Sep 10, 2026
c98d5d7
fix(lighting): 避免保存与提交失败导致烘焙结果错位
zenoskongfu Sep 10, 2026
db2f6aa
fix(scene): 保留地形保存异常与批量失败状态
zenoskongfu Sep 10, 2026
3a85226
Merge pull request #3 from zenoskongfu/feat/fix-light-probe-bake
xubing0906 Sep 10, 2026
0d091e3
test(types): update DTS snapshot for lighting APIs
xubing0906 Sep 10, 2026
567f6f0
Merge branch 'main' into main-light-probe-bake
xubing0906 Sep 10, 2026
2d3c49a
fix(scene): 对齐探针组平移后保留烘焙系数的行为
zenoskongfu Sep 10, 2026
531413b
refactor(lightmap): 移除额外对象扫描并保留导出校验
zenoskongfu Sep 10, 2026
14e94bc
fix(light-probe): 清空结果不再随撤销恢复并保留未保存状态
zenoskongfu Sep 10, 2026
4153fd2
fix(lightmap): 重烘焙后撤销普通编辑时保留最新贴图
zenoskongfu Sep 10, 2026
11d5d5d
feat: 支持选择光照贴图烘焙输出目录
zenoskongfu Sep 10, 2026
afabcdb
fix: 恢复探针清空与贴图重烘焙的完整撤销
zenoskongfu Sep 10, 2026
eabafbe
feat(scene): 提供光照烘焙数值进度
zenoskongfu Sep 11, 2026
c97fc8c
fix(scene): 重算探针法线前清除旧值
zenoskongfu Sep 11, 2026
4e5a79d
fix(lightmap): 清空后精确删除烘焙资源
zenoskongfu Sep 11, 2026
ea25864
Merge branch 'main-light-probe-bake' of https://github.com/xubing0906…
zenoskongfu Sep 11, 2026
6dd8072
test(types): update DTS snapshot for LightFX APIs
zenoskongfu Sep 11, 2026
0b3fd8e
fix(lightmap): harden destructive clear
zenoskongfu Sep 11, 2026
36c02f3
fix/ 修复光照贴图清空历史边界与旧绑定残留
zenoskongfu Sep 11, 2026
4aad117
feat/ 补齐光照贴图原生进度历史与烘焙日志
zenoskongfu Sep 11, 2026
481a600
docs/ 同步光照贴图Clear历史边界与原生日志契约
zenoskongfu Sep 11, 2026
f58a3e2
fix/ 修复未启用地形的烘焙绑定清理与失败恢复
zenoskongfu Sep 11, 2026
f1bef49
fix/ 补齐场景历史烘焙产物的精确清理与落盘核验
zenoskongfu Sep 11, 2026
ad22316
fix/ 成功重烘焙后清理旧贴图并收敛结果撤销
zenoskongfu Sep 11, 2026
f58779a
docs/ 同步重烘焙旧产物清理与结果历史契约
zenoskongfu Sep 11, 2026
ca2ecb8
fix/ 保护非覆盖资产移动失败时的原始元数据
zenoskongfu Sep 11, 2026
8183dd2
feat/ 将已保存光照贴图发布到固定输出目录
zenoskongfu Sep 11, 2026
fe01d76
docs/ 记录固定贴图发布与面板清理实机验收
zenoskongfu Sep 11, 2026
bbb5140
feat/ 发布并精确清理光照烘焙原生配套文件
zenoskongfu Sep 11, 2026
16f4d4f
docs/ 记录原生烘焙配套产物契约与实机验收
zenoskongfu Sep 11, 2026
a3a4a1d
fix/ 修复光照贴图 GI 采样溢出导致原生崩溃
zenoskongfu Sep 12, 2026
299def7
docs/ 记录 GI 采样溢出边界及隔离实机验证
zenoskongfu Sep 12, 2026
b21aa0f
fix/ 清理光照贴图前检查子资源的外部引用
zenoskongfu Sep 12, 2026
dba06b7
fix/ 对齐光照贴图阶段统计日志并标识原生任务
zenoskongfu Sep 12, 2026
01f3870
docs/ 记录共享贴图保护与烘焙日志实机复验
zenoskongfu Sep 12, 2026
9af761b
Merge pull request #4 from zenoskongfu/feat/fix-light-probe-bake
xubing0906 Sep 12, 2026
26ffae4
Merge branch 'main' into main-light-probe-bake
xubing0906 Sep 12, 2026
2dbe96f
fix(lightfx): guard scene sessions and isolate bake assets
xubing0906 Sep 12, 2026
561d15f
test(lightfx): consolidate related bake test suites
xubing0906 Sep 12, 2026
0d2373a
fix(lightfx): pin scene before reservation and verify asset moves
xubing0906 Sep 12, 2026
6f5c515
fix(scene): reset probe gestures and stabilize renderer timeout tests
xubing0906 Sep 12, 2026
b847915
refactor(lightfx): declare bake timeout timer as const
xubing0906 Sep 12, 2026
a6763dc
Merge branch 'main' into main-light-probe-bake
xubing0906 Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions docs/dev/scene/history/lightfx-bake-validation.md

Large diffs are not rendered by default.

382 changes: 382 additions & 0 deletions docs/dev/scene/lightfx-bake.md

Large diffs are not rendered by default.

425 changes: 425 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@
"sharp": "0.32.6",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"socket.io-v2": "npm:socket.io@2.3.0",
"strip-ansi": "^6.0.1",
"tga-js": "^1.1.1",
"tmp": "^0.0.33",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6704,8 +6704,8 @@ export declare interface IGizmoService {
selectAllLightProbes(): void;
unselectAllLightProbes(): void;
queryLightProbeSelectedCount(): number;
deleteSelectedLightProbes(): number;
duplicateSelectedLightProbes(): number;
deleteSelectedLightProbes(): Promise<number>;
duplicateSelectedLightProbes(): Promise<number>;
generateLightProbes(): number;
regionSelectLightProbes(left: number, right: number, top: number, bottom: number, additive: boolean): number;
}
Expand All @@ -6718,6 +6718,132 @@ export declare interface IInsertLODOptions {
export declare interface IIsPrefabInstanceParams {
nodePath: string;
}
export declare interface ILightFXCancelResult {
cancelled: boolean;
target: 'light-probe' | 'lightmap' | null;
}
export declare interface ILightFXDiagnostics {
operationId?: string;
version: 1;
stage: string;
logs: string[];
progress?: string;
rate?: number;
}
export declare interface ILightmapBakeCapabilities {
outputDirectory?: true;
diagnostics?: ILightFXDiagnostics;
version: 1;
resultLifecycleVersion: 1;
sceneTransactionVersion: 1;
assetVersion: 1;
assetCleanupVersion?: 1;
cancelVersion?: 1;
cancellable?: boolean;
busy: boolean;
}
export declare interface ILightmapBakeInfo {
sceneUrl: string;
baked: boolean;
meshCount: number;
terrainCount: number;
highp: boolean;
stationaryMainLight: boolean;
textures: ILightmapTextureInfo[];
missingTextureUuids: string[];
}
export declare interface ILightmapBakeOptions {
outputUrl?: string;
msaa?: 1 | 2 | 4 | 8;
resolution?: 128 | 256 | 512 | 1024 | 2048;
filter?: boolean;
highp?: boolean;
giScale?: number;
giSamples?: number;
giPathLength?: 1 | 2 | 3 | 4;
aoLevel?: 0 | 1 | 2;
aoStrength?: number;
aoRadius?: number;
aoColor?: [number, number, number, number?];
threads?: number;
saveScene?: boolean;
timeoutMs?: number;
}
export declare interface ILightmapBakeResult {
diagnostics?: ILightFXDiagnostics;
sceneUrl: string;
textureUrls: string[];
meshCount: number;
terrainCount: number;
durationMs: number;
}
export declare interface ILightmapBakeService extends IServiceEvents {
queryCapabilities(): Promise<ILightmapBakeCapabilities>;
bake(options: ILightmapBakeOptions): Promise<ILightmapBakeResult>;
queryBakeInfo(): Promise<ILightmapBakeInfo>;
clearBake(options?: {
saveScene?: boolean;
deleteAssets?: boolean;
}): Promise<ILightmapClearResult>;
cancel(): Promise<ILightFXCancelResult>;
}
export declare interface ILightmapClearResult {
clearedCount: number;
deletedAssetCount: number;
retainedAssetCount: number;
failedAssetCount: number;
}
export declare interface ILightmapTextureInfo {
uuid: string;
url: string;
filename: string;
size: number;
createdAt: number;
modifiedAt: number;
}
export declare interface ILightProbeBakeCapabilities {
diagnostics?: ILightFXDiagnostics;
version: 1;
cancelVersion?: 1;
cancellable?: boolean;
resultLifecycleVersion: 1;
sceneTransactionVersion: 1;
busy: boolean;
}
export declare interface ILightProbeBakeOptions {
giScale?: number;
giSamples?: number;
bounces?: number;
reduceRinging?: number;
showWireframe?: boolean;
showConvex?: boolean;
lightProbeSphereVolume?: number;
saveScene?: boolean;
timeoutMs?: number;
}
export declare interface ILightProbeBakeResult {
diagnostics?: ILightFXDiagnostics;
sceneUrl: string;
probeCount: number;
giScale: number;
giSamples: number;
bounces: number;
reduceRinging: number;
showWireframe: boolean;
showConvex: boolean;
lightProbeSphereVolume: number;
durationMs: number;
}
export declare interface ILightProbeBakeService extends IServiceEvents {
queryCapabilities(): Promise<ILightProbeBakeCapabilities>;
bake(options: ILightProbeBakeOptions): Promise<ILightProbeBakeResult>;
clearBake(options?: {
saveScene?: boolean;
}): Promise<{
probeCount: number;
}>;
cancel(): Promise<ILightFXCancelResult>;
}
export declare interface ILODGroupBoundsResult {
localBoundaryCenter: IVec3;
objectSize: number;
Expand Down Expand Up @@ -7332,6 +7458,8 @@ export declare interface IServiceManager {
UI: IUIService;
ReflectionProbe: IReflectionProbeService;
ReferenceImage: IReferenceImageService;
LightProbeBake: ILightProbeBakeService;
LightmapBake: ILightmapBakeService;
Particle: IParticleService;
Terrain: ITerrainService;
}
Expand Down
87 changes: 87 additions & 0 deletions src/api/scene/lightfx-bake-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { z } from 'zod';
import { MAX_LIGHTMAP_GI_SAMPLES } from '../../core/scene/common/lightfx-limits';

const SaveAndTimeout = {
saveScene: z.boolean().optional().describe('Save the current scene after applying the bake result; defaults to true'),
timeoutMs: z.number().int().min(1_000).max(3_600_000).optional().describe('Whole-operation timeout in milliseconds'),
};

export const SchemaLightProbeBakeOptions = z.object({
giScale: z.number().finite().min(0).max(100).optional().describe('GI multiplier; defaults to the current scene value'),
giSamples: z.number().int().min(64).max(65535).optional().describe('GI probe sample count; defaults to the current scene value'),
bounces: z.number().int().min(1).max(4).optional().describe('Probe ray bounce count; defaults to the current scene value'),
reduceRinging: z.number().finite().min(0).max(0.05).optional().describe('Spherical-harmonic ringing reduction; defaults to the current scene value'),
showWireframe: z.boolean().optional().describe('Show light-probe connections in the scene view; defaults to the current scene value'),
showConvex: z.boolean().optional().describe('Show the light-probe convex hull in the scene view; defaults to the current scene value'),
lightProbeSphereVolume: z.number().finite().min(0).max(100).optional().describe('Light-probe sphere display size; defaults to the current scene value'),
...SaveAndTimeout,
}).describe('Light probe bake options');

export const SchemaLightProbeBakeResult = z.object({
sceneUrl: z.string(), probeCount: z.number().int().nonnegative(),
giScale: z.number(), giSamples: z.number().int(), bounces: z.number().int(),
reduceRinging: z.number(), showWireframe: z.boolean(), showConvex: z.boolean(), lightProbeSphereVolume: z.number(),
durationMs: z.number().nonnegative(),
});

export const SchemaLightmapBakeOptions = z.object({
outputUrl: z.string().optional().describe('Existing parent directory under db://assets. Saved results publish into scene-<scene UUID>/output; omitted or db://assets uses db://assets/LightFX as parent. Use returned texture URLs.'),
msaa: z.union([z.literal(1), z.literal(2), z.literal(4), z.literal(8)]).optional(),
resolution: z.union([z.literal(128), z.literal(256), z.literal(512), z.literal(1024), z.literal(2048)]).optional(),
filter: z.boolean().optional(), highp: z.boolean().optional(),
giScale: z.number().finite().min(0).max(100).optional(),
giSamples: z.number().int().min(1).max(MAX_LIGHTMAP_GI_SAMPLES).optional()
.describe('Lightmap GI sampling factor; 1–2590. Large values have quadratic memory and time costs.'),
giPathLength: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]).optional(),
aoLevel: z.union([z.literal(0), z.literal(1), z.literal(2)]).optional(),
aoStrength: z.number().finite().min(0).optional(),
aoRadius: z.number().finite().min(0).optional(),
aoColor: z.tuple([z.number().min(0).max(255), z.number().min(0).max(255), z.number().min(0).max(255), z.number().min(0).max(255).optional()]).optional(),
threads: z.number().int().min(1).max(256).optional(),
...SaveAndTimeout,
}).describe('Lightmap bake options');

export const SchemaLightmapBakeResult = z.object({
sceneUrl: z.string(), textureUrls: z.array(z.string()), meshCount: z.number().int().nonnegative(),
terrainCount: z.number().int().nonnegative(), durationMs: z.number().nonnegative(),
});

export const SchemaLightmapTextureInfo = z.object({
uuid: z.string(),
url: z.string(),
filename: z.string(),
size: z.number().int().nonnegative(),
createdAt: z.number().finite().nonnegative(),
modifiedAt: z.number().finite().nonnegative(),
});

export const SchemaLightmapBakeInfo = z.object({
sceneUrl: z.string(),
baked: z.boolean(),
meshCount: z.number().int().nonnegative(),
terrainCount: z.number().int().nonnegative(),
highp: z.boolean(),
stationaryMainLight: z.boolean(),
textures: z.array(SchemaLightmapTextureInfo),
missingTextureUuids: z.array(z.string()),
});

export const SchemaLightFXCancelResult = z.object({
cancelled: z.boolean(), target: z.enum(['light-probe', 'lightmap']).nullable(),
});

export const SchemaLightProbeClearOptions = z.object({ saveScene: z.boolean().optional() });
export const SchemaLightmapClearOptions = z.object({ saveScene: z.boolean().optional(), deleteAssets: z.boolean().optional() });
export const SchemaClearCountResult = z.object({
probeCount: z.number().int().nonnegative().optional(),
clearedCount: z.number().int().nonnegative().optional(),
deletedAssetCount: z.number().int().nonnegative().optional(),
retainedAssetCount: z.number().int().nonnegative().optional(),
failedAssetCount: z.number().int().nonnegative().optional(),
});

export type TLightProbeBakeOptions = z.infer<typeof SchemaLightProbeBakeOptions>;
export type TLightProbeBakeResult = z.infer<typeof SchemaLightProbeBakeResult>;
export type TLightmapBakeOptions = z.infer<typeof SchemaLightmapBakeOptions>;
export type TLightmapBakeResult = z.infer<typeof SchemaLightmapBakeResult>;
export type TLightmapBakeInfo = z.infer<typeof SchemaLightmapBakeInfo>;
72 changes: 72 additions & 0 deletions src/api/scene/lightfx-bake.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { COMMON_STATUS, CommonResultType } from '../base/schema-base';
import { description, param, result, title, tool } from '../decorator/decorator';
import { Scene } from '../../core/scene';
import {
SchemaClearCountResult, SchemaLightFXCancelResult, SchemaLightmapBakeInfo,
SchemaLightmapBakeOptions, SchemaLightmapBakeResult,
SchemaLightmapClearOptions, SchemaLightProbeBakeOptions, SchemaLightProbeBakeResult, SchemaLightProbeClearOptions,
TLightmapBakeInfo, TLightmapBakeOptions, TLightmapBakeResult, TLightProbeBakeOptions, TLightProbeBakeResult,
} from './lightfx-bake-schema';

async function execute<T>(operation: () => Promise<T>): Promise<CommonResultType<T>> {
try { return { code: COMMON_STATUS.SUCCESS, data: await operation() }; }
catch (error) { return { code: COMMON_STATUS.FAIL, reason: error instanceof Error ? error.message : String(error) }; }
}

export class LightFXBakeApi {
@tool('scene-bake-light-probes')
@title('Bake light probes')
@description('Bake all light probes with the effective panel settings and write both settings and spherical-harmonic coefficients back to the current scene.')
@result(SchemaLightProbeBakeResult)
bakeLightProbes(@param(SchemaLightProbeBakeOptions) options: TLightProbeBakeOptions): Promise<CommonResultType<TLightProbeBakeResult>> {
return execute(() => Scene.LightProbeBake.bake(options));
}

@tool('scene-clear-light-probes')
@title('Clear baked light probes')
@description('Clear spherical-harmonic bake results from all light probes in the current scene.')
@result(SchemaClearCountResult)
clearLightProbes(@param(SchemaLightProbeClearOptions) options: { saveScene?: boolean }): Promise<CommonResultType<{ probeCount: number }>> {
return execute(() => Scene.LightProbeBake.clearBake(options));
}

@tool('scene-bake-lightmap')
@title('Bake lightmap')
@description('Bake the current scene lightmap with LightFX, import generated textures, bind them to renderers and save the scene.')
@result(SchemaLightmapBakeResult)
bakeLightmap(@param(SchemaLightmapBakeOptions) options: TLightmapBakeOptions): Promise<CommonResultType<TLightmapBakeResult>> {
return execute(() => Scene.LightmapBake.bake(options));
}

@tool('scene-query-lightmap-bake-info')
@title('Query lightmap bake information')
@description('Query lightmap textures and bake flags currently bound to meshes and terrains in the active scene.')
@result(SchemaLightmapBakeInfo)
queryLightmapBakeInfo(): Promise<CommonResultType<TLightmapBakeInfo>> {
return execute(() => Scene.LightmapBake.queryBakeInfo());
}

@tool('scene-clear-lightmap')
@title('Clear baked lightmap')
@description('Unbind baked lightmaps and optionally delete unreferenced generated assets. Asset deletion saves the scene and prevents Undo/Redo from restoring pre-Clear baked results; unrelated edit history is preserved.')
@result(SchemaClearCountResult)
clearLightmap(@param(SchemaLightmapClearOptions) options: { saveScene?: boolean; deleteAssets?: boolean }): Promise<CommonResultType<{
clearedCount: number;
deletedAssetCount: number;
retainedAssetCount: number;
failedAssetCount: number;
}>> {
return execute(() => Scene.LightmapBake.clearBake(options));
}

@tool('scene-cancel-lightfx-bake')
@title('Cancel LightFX bake')
@description('Cancel the currently running light-probe or lightmap bake.')
@result(SchemaLightFXCancelResult)
cancel(): Promise<CommonResultType<{ cancelled: boolean; target: 'light-probe' | 'lightmap' | null }>> {
return execute(async () => {
const probe = await Scene.LightProbeBake.cancel();
return probe.cancelled ? probe : Scene.LightmapBake.cancel();
});
}
}
3 changes: 3 additions & 0 deletions src/api/scene/scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { NodeApi } from './node';
import { PrefabApi } from './prefab';
import { ReflectionProbeApi } from './reflection-probe';
import { ReferenceImageApi } from './reference-image';
import { LightFXBakeApi } from './lightfx-bake';
import { ParticleApi } from './particle';
import { options } from '../../core/builder/platforms/android/i18n/en';

Expand All @@ -34,6 +35,7 @@ export class SceneApi {
public prefab: PrefabApi;
public reflectionProbe: ReflectionProbeApi;
public referenceImage: ReferenceImageApi;
public lightFXBake: LightFXBakeApi;
public particle: ParticleApi;

constructor() {
Expand All @@ -42,6 +44,7 @@ export class SceneApi {
this.prefab = new PrefabApi();
this.reflectionProbe = new ReflectionProbeApi();
this.referenceImage = new ReferenceImageApi();
this.lightFXBake = new LightFXBakeApi();
this.particle = new ParticleApi();
}

Expand Down
24 changes: 22 additions & 2 deletions src/core/assets/manager/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,27 @@ export async function moveAssetSource(source: string, target: string, options?:

try {
if (!Utils.Path.contains(source, target)) {
await renamePath(source + '.meta', target + '.meta', { overwrite: true });
await renamePath(source, target, renameOptions);
const originalMeta = !renameOptions.overwrite ? Buffer.from(await readPath(source + '.meta')) : undefined;
await renamePath(source + '.meta', target + '.meta', renameOptions);
try {
await renamePath(source, target, renameOptions);
} catch (error) {
// Keep the original UUID when a non-overwriting source move fails.
// Propagate failure before Asset DB refresh can generate a replacement meta.
if (originalMeta && existsSync(source)) {
try {
// A competing destination PNG does not own our metadata. Restore only
// the exact bytes moved by this operation, never a replacement meta.
if (existsSync(source + '.meta') || !Buffer.from(await readPath(target + '.meta')).equals(originalMeta)) {
throw new Error('Asset metadata changed during the failed move; manual recovery is required.');
}
await renamePath(target + '.meta', source + '.meta', { overwrite: false });
} catch (recoveryError) {
throw new AggregateError([error, recoveryError], 'Asset move failed and original metadata could not be restored safely.');
}
}
throw error;
}
return;
}

Expand All @@ -168,5 +187,6 @@ export async function moveAssetSource(source: string, target: string, options?:
} catch (error) {
console.error(`asset db moveFile from ${source} -> ${target} fail!`);
console.error(error);
if (!renameOptions.overwrite) throw error;
}
}
Loading
Loading