Refactor and enhance scene document migration logic
This commit is contained in:
@@ -1,5 +1,15 @@
|
|||||||
import { createBoxBrush, isBoxFaceId, type BoxBrushFaces, type BrushFace } from "./brushes";
|
import { createStarterMaterialRegistry, type MaterialDef, type MaterialPattern } from "../materials/starter-material-library";
|
||||||
import {
|
import {
|
||||||
|
createBoxBrush,
|
||||||
|
createDefaultFaceUvState,
|
||||||
|
isBoxFaceId,
|
||||||
|
isFaceUvRotationQuarterTurns,
|
||||||
|
type BoxBrushFaces,
|
||||||
|
type BrushFace,
|
||||||
|
type FaceUvState
|
||||||
|
} from "./brushes";
|
||||||
|
import {
|
||||||
|
BOX_BRUSH_SCENE_DOCUMENT_VERSION,
|
||||||
FOUNDATION_SCENE_DOCUMENT_VERSION,
|
FOUNDATION_SCENE_DOCUMENT_VERSION,
|
||||||
SCENE_DOCUMENT_VERSION,
|
SCENE_DOCUMENT_VERSION,
|
||||||
type SceneDocument,
|
type SceneDocument,
|
||||||
@@ -26,6 +36,22 @@ function expectString(value: unknown, label: string): string {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expectBoolean(value: unknown, label: string): boolean {
|
||||||
|
if (typeof value !== "boolean") {
|
||||||
|
throw new Error(`${label} must be a boolean.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectStringArray(value: unknown, label: string): string[] {
|
||||||
|
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
||||||
|
throw new Error(`${label} must be a string array.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...value];
|
||||||
|
}
|
||||||
|
|
||||||
function expectHexColor(value: unknown, label: string): string {
|
function expectHexColor(value: unknown, label: string): string {
|
||||||
const normalizedValue = expectString(value, label);
|
const normalizedValue = expectString(value, label);
|
||||||
|
|
||||||
@@ -44,18 +70,6 @@ function expectLiteralString<T extends string>(value: unknown, expectedValue: T,
|
|||||||
return expectedValue;
|
return expectedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectEmptyCollection(value: unknown, label: string): Record<string, never> {
|
|
||||||
if (!isRecord(value)) {
|
|
||||||
throw new Error(`${label} must be a record.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(value).length > 0) {
|
|
||||||
throw new Error(`${label} must be empty in the foundation schema.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectOptionalString(value: unknown, label: string): string | undefined {
|
function expectOptionalString(value: unknown, label: string): string | undefined {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -64,19 +78,26 @@ function expectOptionalString(value: unknown, label: string): string | undefined
|
|||||||
return expectString(value, label);
|
return expectString(value, label);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectBrushFace(value: unknown, label: string): BrushFace {
|
function expectEmptyCollection(value: unknown, label: string): Record<string, never> {
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
throw new Error(`${label} must be a record.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(value).length > 0) {
|
||||||
|
throw new Error(`${label} must be empty in the current schema.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readVec2(value: unknown, label: string) {
|
||||||
if (!isRecord(value)) {
|
if (!isRecord(value)) {
|
||||||
throw new Error(`${label} must be an object.`);
|
throw new Error(`${label} must be an object.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const materialId = value.materialId;
|
|
||||||
|
|
||||||
if (materialId !== null && materialId !== undefined && typeof materialId !== "string") {
|
|
||||||
throw new Error(`${label}.materialId must be a string or null.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
materialId: materialId ?? null
|
x: expectFiniteNumber(value.x, `${label}.x`),
|
||||||
|
y: expectFiniteNumber(value.y, `${label}.y`)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +113,106 @@ function readVec3(value: unknown, label: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function readBoxBrushFaces(value: unknown, label: string): BoxBrushFaces {
|
function expectMaterialPattern(value: unknown, label: string): MaterialPattern {
|
||||||
|
if (value !== "grid" && value !== "checker" && value !== "stripes" && value !== "diamond") {
|
||||||
|
throw new Error(`${label} must be a supported starter material pattern.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readMaterialRegistry(value: unknown, label: string): SceneDocument["materials"] {
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
throw new Error(`${label} must be a record.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const materials: SceneDocument["materials"] = {};
|
||||||
|
|
||||||
|
for (const [materialId, materialValue] of Object.entries(value)) {
|
||||||
|
if (!isRecord(materialValue)) {
|
||||||
|
throw new Error(`${label}.${materialId} must be an object.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const material: MaterialDef = {
|
||||||
|
id: expectString(materialValue.id, `${label}.${materialId}.id`),
|
||||||
|
name: expectString(materialValue.name, `${label}.${materialId}.name`),
|
||||||
|
baseColorHex: expectHexColor(materialValue.baseColorHex, `${label}.${materialId}.baseColorHex`),
|
||||||
|
accentColorHex: expectHexColor(materialValue.accentColorHex, `${label}.${materialId}.accentColorHex`),
|
||||||
|
pattern: expectMaterialPattern(materialValue.pattern, `${label}.${materialId}.pattern`),
|
||||||
|
tags: expectStringArray(materialValue.tags, `${label}.${materialId}.tags`)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (material.id !== materialId) {
|
||||||
|
throw new Error(`${label}.${materialId}.id must match the registry key.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
materials[materialId] = material;
|
||||||
|
}
|
||||||
|
|
||||||
|
return materials;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFaceUvState(value: unknown, label: string): FaceUvState {
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
throw new Error(`${label} must be an object.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rotationQuarterTurns = expectFiniteNumber(value.rotationQuarterTurns, `${label}.rotationQuarterTurns`);
|
||||||
|
|
||||||
|
if (!isFaceUvRotationQuarterTurns(rotationQuarterTurns)) {
|
||||||
|
throw new Error(`${label}.rotationQuarterTurns must be 0, 1, 2, or 3.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const scale = readVec2(value.scale, `${label}.scale`);
|
||||||
|
|
||||||
|
if (scale.x <= 0 || scale.y <= 0) {
|
||||||
|
throw new Error(`${label}.scale values must remain positive.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
offset: readVec2(value.offset, `${label}.offset`),
|
||||||
|
scale,
|
||||||
|
rotationQuarterTurns,
|
||||||
|
flipU: expectBoolean(value.flipU, `${label}.flipU`),
|
||||||
|
flipV: expectBoolean(value.flipV, `${label}.flipV`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readBrushFace(
|
||||||
|
value: unknown,
|
||||||
|
label: string,
|
||||||
|
materials: SceneDocument["materials"],
|
||||||
|
allowMissingUvState: boolean
|
||||||
|
): BrushFace {
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
throw new Error(`${label} must be an object.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const materialId = value.materialId;
|
||||||
|
|
||||||
|
if (materialId !== null && materialId !== undefined && typeof materialId !== "string") {
|
||||||
|
throw new Error(`${label}.materialId must be a string or null.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (materialId !== null && materialId !== undefined && materials[materialId] === undefined) {
|
||||||
|
throw new Error(`${label}.materialId references missing material ${materialId}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
materialId: materialId ?? null,
|
||||||
|
uv:
|
||||||
|
value.uv === undefined && allowMissingUvState
|
||||||
|
? createDefaultFaceUvState()
|
||||||
|
: readFaceUvState(value.uv, `${label}.uv`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readBoxBrushFaces(
|
||||||
|
value: unknown,
|
||||||
|
label: string,
|
||||||
|
materials: SceneDocument["materials"],
|
||||||
|
allowMissingUvState: boolean
|
||||||
|
): BoxBrushFaces {
|
||||||
if (!isRecord(value)) {
|
if (!isRecord(value)) {
|
||||||
throw new Error(`${label} must be an object.`);
|
throw new Error(`${label} must be an object.`);
|
||||||
}
|
}
|
||||||
@@ -104,16 +224,20 @@ function readBoxBrushFaces(value: unknown, label: string): BoxBrushFaces {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
posX: expectBrushFace(value.posX, `${label}.posX`),
|
posX: readBrushFace(value.posX, `${label}.posX`, materials, allowMissingUvState),
|
||||||
negX: expectBrushFace(value.negX, `${label}.negX`),
|
negX: readBrushFace(value.negX, `${label}.negX`, materials, allowMissingUvState),
|
||||||
posY: expectBrushFace(value.posY, `${label}.posY`),
|
posY: readBrushFace(value.posY, `${label}.posY`, materials, allowMissingUvState),
|
||||||
negY: expectBrushFace(value.negY, `${label}.negY`),
|
negY: readBrushFace(value.negY, `${label}.negY`, materials, allowMissingUvState),
|
||||||
posZ: expectBrushFace(value.posZ, `${label}.posZ`),
|
posZ: readBrushFace(value.posZ, `${label}.posZ`, materials, allowMissingUvState),
|
||||||
negZ: expectBrushFace(value.negZ, `${label}.negZ`)
|
negZ: readBrushFace(value.negZ, `${label}.negZ`, materials, allowMissingUvState)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function readBrushes(value: unknown): SceneDocument["brushes"] {
|
function readBrushes(
|
||||||
|
value: unknown,
|
||||||
|
materials: SceneDocument["materials"],
|
||||||
|
allowMissingUvState: boolean
|
||||||
|
): SceneDocument["brushes"] {
|
||||||
if (!isRecord(value)) {
|
if (!isRecord(value)) {
|
||||||
throw new Error("brushes must be a record.");
|
throw new Error("brushes must be a record.");
|
||||||
}
|
}
|
||||||
@@ -140,7 +264,7 @@ function readBrushes(value: unknown): SceneDocument["brushes"] {
|
|||||||
id: expectString(brushValue.id, `brushes.${brushId}.id`),
|
id: expectString(brushValue.id, `brushes.${brushId}.id`),
|
||||||
center,
|
center,
|
||||||
size,
|
size,
|
||||||
faces: readBoxBrushFaces(brushValue.faces, `brushes.${brushId}.faces`),
|
faces: readBoxBrushFaces(brushValue.faces, `brushes.${brushId}.faces`, materials, allowMissingUvState),
|
||||||
layerId: expectOptionalString(brushValue.layerId, `brushes.${brushId}.layerId`),
|
layerId: expectOptionalString(brushValue.layerId, `brushes.${brushId}.layerId`),
|
||||||
groupId: expectOptionalString(brushValue.groupId, `brushes.${brushId}.groupId`)
|
groupId: expectOptionalString(brushValue.groupId, `brushes.${brushId}.groupId`)
|
||||||
});
|
});
|
||||||
@@ -207,7 +331,7 @@ export function migrateSceneDocument(source: unknown): SceneDocument {
|
|||||||
version: SCENE_DOCUMENT_VERSION,
|
version: SCENE_DOCUMENT_VERSION,
|
||||||
name: expectString(source.name, "name"),
|
name: expectString(source.name, "name"),
|
||||||
world: readWorldSettings(source.world),
|
world: readWorldSettings(source.world),
|
||||||
materials: expectEmptyCollection(source.materials, "materials"),
|
materials: createStarterMaterialRegistry(),
|
||||||
textures: expectEmptyCollection(source.textures, "textures"),
|
textures: expectEmptyCollection(source.textures, "textures"),
|
||||||
assets: expectEmptyCollection(source.assets, "assets"),
|
assets: expectEmptyCollection(source.assets, "assets"),
|
||||||
brushes: {},
|
brushes: {},
|
||||||
@@ -217,18 +341,37 @@ export function migrateSceneDocument(source: unknown): SceneDocument {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (source.version === BOX_BRUSH_SCENE_DOCUMENT_VERSION) {
|
||||||
|
const materials = createStarterMaterialRegistry();
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: SCENE_DOCUMENT_VERSION,
|
||||||
|
name: expectString(source.name, "name"),
|
||||||
|
world: readWorldSettings(source.world),
|
||||||
|
materials,
|
||||||
|
textures: expectEmptyCollection(source.textures, "textures"),
|
||||||
|
assets: expectEmptyCollection(source.assets, "assets"),
|
||||||
|
brushes: readBrushes(source.brushes, materials, true),
|
||||||
|
modelInstances: expectEmptyCollection(source.modelInstances, "modelInstances"),
|
||||||
|
entities: expectEmptyCollection(source.entities, "entities"),
|
||||||
|
interactionLinks: expectEmptyCollection(source.interactionLinks, "interactionLinks")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (source.version !== SCENE_DOCUMENT_VERSION) {
|
if (source.version !== SCENE_DOCUMENT_VERSION) {
|
||||||
throw new Error(`Unsupported scene document version: ${String(source.version)}.`);
|
throw new Error(`Unsupported scene document version: ${String(source.version)}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const materials = readMaterialRegistry(source.materials, "materials");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: SCENE_DOCUMENT_VERSION,
|
version: SCENE_DOCUMENT_VERSION,
|
||||||
name: expectString(source.name, "name"),
|
name: expectString(source.name, "name"),
|
||||||
world: readWorldSettings(source.world),
|
world: readWorldSettings(source.world),
|
||||||
materials: expectEmptyCollection(source.materials, "materials"),
|
materials,
|
||||||
textures: expectEmptyCollection(source.textures, "textures"),
|
textures: expectEmptyCollection(source.textures, "textures"),
|
||||||
assets: expectEmptyCollection(source.assets, "assets"),
|
assets: expectEmptyCollection(source.assets, "assets"),
|
||||||
brushes: readBrushes(source.brushes),
|
brushes: readBrushes(source.brushes, materials, false),
|
||||||
modelInstances: expectEmptyCollection(source.modelInstances, "modelInstances"),
|
modelInstances: expectEmptyCollection(source.modelInstances, "modelInstances"),
|
||||||
entities: expectEmptyCollection(source.entities, "entities"),
|
entities: expectEmptyCollection(source.entities, "entities"),
|
||||||
interactionLinks: expectEmptyCollection(source.interactionLinks, "interactionLinks")
|
interactionLinks: expectEmptyCollection(source.interactionLinks, "interactionLinks")
|
||||||
|
|||||||
Reference in New Issue
Block a user