diff --git a/src/commands/set-box-brush-name-command.ts b/src/commands/set-box-brush-name-command.ts new file mode 100644 index 00000000..11dad58e --- /dev/null +++ b/src/commands/set-box-brush-name-command.ts @@ -0,0 +1,46 @@ +import { createOpaqueId } from "../core/ids"; +import { normalizeBrushName } from "../document/brushes"; + +import { getBoxBrushOrThrow, replaceBrush } from "./brush-command-helpers"; +import type { EditorCommand } from "./command"; + +interface SetBoxBrushNameCommandOptions { + brushId: string; + name: string | null; +} + +export function createSetBoxBrushNameCommand(options: SetBoxBrushNameCommandOptions): EditorCommand { + const normalizedName = normalizeBrushName(options.name); + let previousName: string | undefined; + + return { + id: createOpaqueId("command"), + label: normalizedName === undefined ? "Clear box brush name" : `Rename box brush to ${normalizedName}`, + execute(context) { + const currentDocument = context.getDocument(); + const brush = getBoxBrushOrThrow(currentDocument, options.brushId); + + if (previousName === undefined) { + previousName = brush.name; + } + + context.setDocument( + replaceBrush(currentDocument, { + ...brush, + name: normalizedName + }) + ); + }, + undo(context) { + const currentDocument = context.getDocument(); + const brush = getBoxBrushOrThrow(currentDocument, options.brushId); + + context.setDocument( + replaceBrush(currentDocument, { + ...brush, + name: previousName + }) + ); + } + }; +} diff --git a/src/document/migrate-scene-document.ts b/src/document/migrate-scene-document.ts index 6821299c..3f2d4110 100644 --- a/src/document/migrate-scene-document.ts +++ b/src/document/migrate-scene-document.ts @@ -5,6 +5,7 @@ import { createDefaultFaceUvState, isBoxFaceId, isFaceUvRotationQuarterTurns, + normalizeBrushName, type BoxBrushFaces, type BrushFace, type FaceUvState @@ -13,6 +14,7 @@ import { BOX_BRUSH_SCENE_DOCUMENT_VERSION, FACE_MATERIALS_SCENE_DOCUMENT_VERSION, FOUNDATION_SCENE_DOCUMENT_VERSION, + RUNNER_V1_SCENE_DOCUMENT_VERSION, SCENE_DOCUMENT_VERSION, type SceneDocument, type WorldSettings @@ -80,6 +82,10 @@ function expectOptionalString(value: unknown, label: string): string | undefined return expectString(value, label); } +function readOptionalBrushName(value: unknown, label: string): string | undefined { + return normalizeBrushName(expectOptionalString(value, label)); +} + function expectEmptyCollection(value: unknown, label: string): Record { if (!isRecord(value)) { throw new Error(`${label} must be a record.`); @@ -264,6 +270,7 @@ function readBrushes( brushes[brushId] = createBoxBrush({ id: expectString(brushValue.id, `brushes.${brushId}.id`), + name: readOptionalBrushName(brushValue.name, `brushes.${brushId}.name`), center, size, faces: readBoxBrushFaces(brushValue.faces, `brushes.${brushId}.faces`, materials, allowMissingUvState), @@ -428,6 +435,23 @@ export function migrateSceneDocument(source: unknown): SceneDocument { }; } + if (source.version === RUNNER_V1_SCENE_DOCUMENT_VERSION) { + const materials = readMaterialRegistry(source.materials, "materials"); + + 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, false), + modelInstances: expectEmptyCollection(source.modelInstances, "modelInstances"), + entities: readEntities(source.entities), + interactionLinks: expectEmptyCollection(source.interactionLinks, "interactionLinks") + }; + } + if (source.version !== SCENE_DOCUMENT_VERSION) { throw new Error(`Unsupported scene document version: ${String(source.version)}.`); } diff --git a/src/document/scene-document-validation.ts b/src/document/scene-document-validation.ts index 6f024293..51f6f719 100644 --- a/src/document/scene-document-validation.ts +++ b/src/document/scene-document-validation.ts @@ -109,6 +109,10 @@ export function validateSceneDocument(document: SceneDocument): SceneDocumentVal registerAuthoredId(brush.id, path, seenIds, diagnostics); + if (brush.name !== undefined && brush.name.trim().length === 0) { + diagnostics.push(createDiagnostic("error", "invalid-box-name", "Box brush names must be non-empty when authored.", `${path}.name`)); + } + if (!isFiniteVec3(brush.size) || !hasPositiveBoxSize(brush.size)) { diagnostics.push( createDiagnostic("error", "invalid-box-size", "Box brush sizes must remain finite and positive on every axis.", `${path}.size`) diff --git a/src/document/scene-document.ts b/src/document/scene-document.ts index 4d0d90b8..bcaf4405 100644 --- a/src/document/scene-document.ts +++ b/src/document/scene-document.ts @@ -3,10 +3,11 @@ import type { Brush } from "./brushes"; import type { EntityInstance } from "../entities/entity-instances"; import { cloneMaterialRegistry, createStarterMaterialRegistry, type MaterialDef } from "../materials/starter-material-library"; -export const SCENE_DOCUMENT_VERSION = 4 as const; +export const SCENE_DOCUMENT_VERSION = 5 as const; export const FOUNDATION_SCENE_DOCUMENT_VERSION = 1 as const; export const BOX_BRUSH_SCENE_DOCUMENT_VERSION = 2 as const; export const FACE_MATERIALS_SCENE_DOCUMENT_VERSION = 3 as const; +export const RUNNER_V1_SCENE_DOCUMENT_VERSION = 4 as const; export interface WorldBackgroundSettings { mode: "solid";