Add path-related commands and update selection handling
This commit is contained in:
82
src/commands/delete-path-command.ts
Normal file
82
src/commands/delete-path-command.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import { cloneEditorSelection, type EditorSelection } from "../core/selection";
|
||||
import type { ToolMode } from "../core/tool-mode";
|
||||
import { cloneScenePath, type ScenePath } from "../document/paths";
|
||||
|
||||
import type { EditorCommand } from "./command";
|
||||
|
||||
function selectionIncludesPath(selection: EditorSelection, pathId: string): boolean {
|
||||
return selection.kind === "paths" && selection.ids.includes(pathId);
|
||||
}
|
||||
|
||||
export function createDeletePathCommand(pathId: string): EditorCommand {
|
||||
let previousPath: ScenePath | null = null;
|
||||
let previousSelection: EditorSelection | null = null;
|
||||
let previousToolMode: ToolMode | null = null;
|
||||
|
||||
return {
|
||||
id: createOpaqueId("command"),
|
||||
label: "Delete path",
|
||||
execute(context) {
|
||||
const currentDocument = context.getDocument();
|
||||
const currentPath = currentDocument.paths[pathId];
|
||||
|
||||
if (currentPath === undefined) {
|
||||
throw new Error(`Path ${pathId} does not exist.`);
|
||||
}
|
||||
|
||||
if (previousPath === null) {
|
||||
previousPath = cloneScenePath(currentPath);
|
||||
}
|
||||
|
||||
if (previousSelection === null) {
|
||||
previousSelection = cloneEditorSelection(context.getSelection());
|
||||
}
|
||||
|
||||
if (previousToolMode === null) {
|
||||
previousToolMode = context.getToolMode();
|
||||
}
|
||||
|
||||
const nextPaths = {
|
||||
...currentDocument.paths
|
||||
};
|
||||
delete nextPaths[pathId];
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
paths: nextPaths
|
||||
});
|
||||
|
||||
if (selectionIncludesPath(context.getSelection(), pathId)) {
|
||||
context.setSelection({
|
||||
kind: "none"
|
||||
});
|
||||
}
|
||||
|
||||
context.setToolMode("select");
|
||||
},
|
||||
undo(context) {
|
||||
if (previousPath === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDocument = context.getDocument();
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
paths: {
|
||||
...currentDocument.paths,
|
||||
[previousPath.id]: cloneScenePath(previousPath)
|
||||
}
|
||||
});
|
||||
|
||||
if (previousSelection !== null) {
|
||||
context.setSelection(previousSelection);
|
||||
}
|
||||
|
||||
if (previousToolMode !== null) {
|
||||
context.setToolMode(previousToolMode);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { createOpaqueId } from "../core/ids";
|
||||
import { cloneEditorSelection, type EditorSelection } from "../core/selection";
|
||||
import type { ToolMode } from "../core/tool-mode";
|
||||
import { cloneBoxBrush, type BoxBrush } from "../document/brushes";
|
||||
import { cloneScenePath, type ScenePath } from "../document/paths";
|
||||
import type { SceneDocument } from "../document/scene-document";
|
||||
import {
|
||||
cloneEntityInstance,
|
||||
@@ -15,6 +16,7 @@ import type { EditorCommand } from "./command";
|
||||
interface DuplicateSelectionResult {
|
||||
selection: EditorSelection;
|
||||
brushes: BoxBrush[] | null;
|
||||
paths: ScenePath[] | null;
|
||||
entities: EntityInstance[] | null;
|
||||
modelInstances: ModelInstance[] | null;
|
||||
}
|
||||
@@ -25,6 +27,16 @@ function duplicateBrush(brush: BoxBrush): BoxBrush {
|
||||
return duplicatedBrush;
|
||||
}
|
||||
|
||||
function duplicatePath(path: ScenePath): ScenePath {
|
||||
const duplicatedPath = cloneScenePath(path);
|
||||
duplicatedPath.id = createOpaqueId("path");
|
||||
duplicatedPath.points = duplicatedPath.points.map((point) => ({
|
||||
...point,
|
||||
id: createOpaqueId("path-point")
|
||||
}));
|
||||
return duplicatedPath;
|
||||
}
|
||||
|
||||
function duplicateEntity(entity: EntityInstance): EntityInstance {
|
||||
const duplicatedEntity = cloneEntityInstance(entity);
|
||||
duplicatedEntity.id = createOpaqueId(`entity-${duplicatedEntity.kind}`);
|
||||
@@ -83,6 +95,34 @@ function createDuplicateSelectionResult(currentDocument: SceneDocument, selectio
|
||||
ids: duplicatedBrushes.map((brush) => brush.id)
|
||||
},
|
||||
brushes: duplicatedBrushes,
|
||||
paths: null,
|
||||
entities: null,
|
||||
modelInstances: null
|
||||
};
|
||||
}
|
||||
|
||||
if (selection.kind === "paths") {
|
||||
if (selection.ids.length === 0) {
|
||||
throw new Error("Select at least one path to duplicate.");
|
||||
}
|
||||
|
||||
const duplicatedPaths = selection.ids.map((pathId) => {
|
||||
const sourcePath = currentDocument.paths[pathId];
|
||||
|
||||
if (sourcePath === undefined) {
|
||||
throw new Error(`Path ${pathId} does not exist.`);
|
||||
}
|
||||
|
||||
return duplicatePath(sourcePath);
|
||||
});
|
||||
|
||||
return {
|
||||
selection: {
|
||||
kind: "paths",
|
||||
ids: duplicatedPaths.map((path) => path.id)
|
||||
},
|
||||
brushes: null,
|
||||
paths: duplicatedPaths,
|
||||
entities: null,
|
||||
modelInstances: null
|
||||
};
|
||||
@@ -109,6 +149,7 @@ function createDuplicateSelectionResult(currentDocument: SceneDocument, selectio
|
||||
ids: duplicatedEntities.map((entity) => entity.id)
|
||||
},
|
||||
brushes: null,
|
||||
paths: null,
|
||||
entities: duplicatedEntities,
|
||||
modelInstances: null
|
||||
};
|
||||
@@ -135,12 +176,13 @@ function createDuplicateSelectionResult(currentDocument: SceneDocument, selectio
|
||||
ids: duplicatedModelInstances.map((modelInstance) => modelInstance.id)
|
||||
},
|
||||
brushes: null,
|
||||
paths: null,
|
||||
entities: null,
|
||||
modelInstances: duplicatedModelInstances
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error("Selection must contain whitebox solids, entities, or model instances to duplicate.");
|
||||
throw new Error("Selection must contain whitebox solids, paths, entities, or model instances to duplicate.");
|
||||
}
|
||||
|
||||
export function createDuplicateSelectionCommand(): EditorCommand {
|
||||
@@ -174,6 +216,16 @@ export function createDuplicateSelectionCommand(): EditorCommand {
|
||||
...Object.fromEntries(duplicateSelectionResult.brushes.map((brush) => [brush.id, cloneBoxBrush(brush)]))
|
||||
}
|
||||
});
|
||||
} else if (duplicateSelectionResult.paths !== null) {
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
paths: {
|
||||
...currentDocument.paths,
|
||||
...Object.fromEntries(
|
||||
duplicateSelectionResult.paths.map((path) => [path.id, cloneScenePath(path)])
|
||||
)
|
||||
}
|
||||
});
|
||||
} else if (duplicateSelectionResult.entities !== null) {
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
@@ -217,6 +269,19 @@ export function createDuplicateSelectionCommand(): EditorCommand {
|
||||
...currentDocument,
|
||||
brushes: nextBrushes
|
||||
});
|
||||
} else if (duplicateSelectionResult.paths !== null) {
|
||||
const nextPaths = {
|
||||
...currentDocument.paths
|
||||
};
|
||||
|
||||
for (const duplicatedPath of duplicateSelectionResult.paths) {
|
||||
delete nextPaths[duplicatedPath.id];
|
||||
}
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
paths: nextPaths
|
||||
});
|
||||
} else if (duplicateSelectionResult.entities !== null) {
|
||||
const nextEntities = {
|
||||
...currentDocument.entities
|
||||
@@ -254,4 +319,4 @@ export function createDuplicateSelectionCommand(): EditorCommand {
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
90
src/commands/set-path-authored-state-command.ts
Normal file
90
src/commands/set-path-authored-state-command.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import { cloneScenePath } from "../document/paths";
|
||||
|
||||
import type { EditorCommand } from "./command";
|
||||
|
||||
interface SetPathAuthoredStateCommandOptions {
|
||||
pathId: string;
|
||||
visible?: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
function createCommandLabel(options: SetPathAuthoredStateCommandOptions): string {
|
||||
if (options.enabled !== undefined && options.visible === undefined) {
|
||||
return options.enabled ? "Enable path" : "Disable path";
|
||||
}
|
||||
|
||||
if (options.visible !== undefined && options.enabled === undefined) {
|
||||
return options.visible ? "Show path" : "Hide path";
|
||||
}
|
||||
|
||||
return "Update path state";
|
||||
}
|
||||
|
||||
export function createSetPathAuthoredStateCommand(
|
||||
options: SetPathAuthoredStateCommandOptions
|
||||
): EditorCommand {
|
||||
if (options.visible === undefined && options.enabled === undefined) {
|
||||
throw new Error("Path authored state command requires at least one change.");
|
||||
}
|
||||
|
||||
let previousVisible: boolean | null = null;
|
||||
let previousEnabled: boolean | null = null;
|
||||
|
||||
return {
|
||||
id: createOpaqueId("command"),
|
||||
label: createCommandLabel(options),
|
||||
execute(context) {
|
||||
const currentDocument = context.getDocument();
|
||||
const path = currentDocument.paths[options.pathId];
|
||||
|
||||
if (path === undefined) {
|
||||
throw new Error(`Path ${options.pathId} does not exist.`);
|
||||
}
|
||||
|
||||
if (previousVisible === null) {
|
||||
previousVisible = path.visible;
|
||||
}
|
||||
|
||||
if (previousEnabled === null) {
|
||||
previousEnabled = path.enabled;
|
||||
}
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
paths: {
|
||||
...currentDocument.paths,
|
||||
[path.id]: cloneScenePath({
|
||||
...path,
|
||||
visible: options.visible ?? path.visible,
|
||||
enabled: options.enabled ?? path.enabled
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
undo(context) {
|
||||
if (previousVisible === null || previousEnabled === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDocument = context.getDocument();
|
||||
const path = currentDocument.paths[options.pathId];
|
||||
|
||||
if (path === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
paths: {
|
||||
...currentDocument.paths,
|
||||
[path.id]: cloneScenePath({
|
||||
...path,
|
||||
visible: previousVisible,
|
||||
enabled: previousEnabled
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
69
src/commands/set-path-name-command.ts
Normal file
69
src/commands/set-path-name-command.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import {
|
||||
cloneScenePath,
|
||||
normalizeScenePathName
|
||||
} from "../document/paths";
|
||||
|
||||
import type { EditorCommand } from "./command";
|
||||
|
||||
interface SetPathNameCommandOptions {
|
||||
pathId: string;
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
export function createSetPathNameCommand(
|
||||
options: SetPathNameCommandOptions
|
||||
): EditorCommand {
|
||||
const normalizedName = normalizeScenePathName(options.name);
|
||||
let previousName: string | undefined;
|
||||
|
||||
return {
|
||||
id: createOpaqueId("command"),
|
||||
label:
|
||||
normalizedName === undefined
|
||||
? "Clear path name"
|
||||
: `Rename path to ${normalizedName}`,
|
||||
execute(context) {
|
||||
const currentDocument = context.getDocument();
|
||||
const path = currentDocument.paths[options.pathId];
|
||||
|
||||
if (path === undefined) {
|
||||
throw new Error(`Path ${options.pathId} does not exist.`);
|
||||
}
|
||||
|
||||
if (previousName === undefined) {
|
||||
previousName = path.name;
|
||||
}
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
paths: {
|
||||
...currentDocument.paths,
|
||||
[path.id]: cloneScenePath({
|
||||
...path,
|
||||
name: normalizedName
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
undo(context) {
|
||||
const currentDocument = context.getDocument();
|
||||
const path = currentDocument.paths[options.pathId];
|
||||
|
||||
if (path === undefined) {
|
||||
throw new Error(`Path ${options.pathId} does not exist.`);
|
||||
}
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
paths: {
|
||||
...currentDocument.paths,
|
||||
[path.id]: cloneScenePath({
|
||||
...path,
|
||||
name: previousName
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
90
src/commands/upsert-path-command.ts
Normal file
90
src/commands/upsert-path-command.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import { cloneEditorSelection, type EditorSelection } from "../core/selection";
|
||||
import type { ToolMode } from "../core/tool-mode";
|
||||
import {
|
||||
cloneScenePath,
|
||||
type ScenePath
|
||||
} from "../document/paths";
|
||||
|
||||
import type { EditorCommand } from "./command";
|
||||
|
||||
interface UpsertPathCommandOptions {
|
||||
path: ScenePath;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
function setSinglePathSelection(pathId: string): EditorSelection {
|
||||
return {
|
||||
kind: "paths",
|
||||
ids: [pathId]
|
||||
};
|
||||
}
|
||||
|
||||
function createDefaultPathCommandLabel(isNewPath: boolean): string {
|
||||
return isNewPath ? "Create path" : "Update path";
|
||||
}
|
||||
|
||||
export function createUpsertPathCommand(
|
||||
options: UpsertPathCommandOptions
|
||||
): EditorCommand {
|
||||
const nextPath = cloneScenePath(options.path);
|
||||
let previousPath: ScenePath | null = null;
|
||||
let previousSelection: EditorSelection | null = null;
|
||||
let previousToolMode: ToolMode | null = null;
|
||||
|
||||
return {
|
||||
id: createOpaqueId("command"),
|
||||
label: options.label ?? createDefaultPathCommandLabel(true),
|
||||
execute(context) {
|
||||
const currentDocument = context.getDocument();
|
||||
const currentPath = currentDocument.paths[nextPath.id];
|
||||
|
||||
if (previousSelection === null) {
|
||||
previousSelection = cloneEditorSelection(context.getSelection());
|
||||
}
|
||||
|
||||
if (previousToolMode === null) {
|
||||
previousToolMode = context.getToolMode();
|
||||
}
|
||||
|
||||
if (previousPath === null && currentPath !== undefined) {
|
||||
previousPath = cloneScenePath(currentPath);
|
||||
}
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
paths: {
|
||||
...currentDocument.paths,
|
||||
[nextPath.id]: cloneScenePath(nextPath)
|
||||
}
|
||||
});
|
||||
context.setSelection(setSinglePathSelection(nextPath.id));
|
||||
context.setToolMode("select");
|
||||
},
|
||||
undo(context) {
|
||||
const currentDocument = context.getDocument();
|
||||
const nextPaths = {
|
||||
...currentDocument.paths
|
||||
};
|
||||
|
||||
if (previousPath === null) {
|
||||
delete nextPaths[nextPath.id];
|
||||
} else {
|
||||
nextPaths[nextPath.id] = cloneScenePath(previousPath);
|
||||
}
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
paths: nextPaths
|
||||
});
|
||||
|
||||
if (previousSelection !== null) {
|
||||
context.setSelection(previousSelection);
|
||||
}
|
||||
|
||||
if (previousToolMode !== null) {
|
||||
context.setToolMode(previousToolMode);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user