Add path point manipulation commands
This commit is contained in:
108
src/commands/add-path-point-command.ts
Normal file
108
src/commands/add-path-point-command.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import { cloneEditorSelection, type EditorSelection } from "../core/selection";
|
||||
import type { ToolMode } from "../core/tool-mode";
|
||||
import {
|
||||
cloneScenePath,
|
||||
cloneScenePathPoint,
|
||||
createAppendedScenePathPoint,
|
||||
type ScenePathPoint
|
||||
} from "../document/paths";
|
||||
|
||||
import type { EditorCommand } from "./command";
|
||||
|
||||
interface AddPathPointCommandOptions {
|
||||
pathId: string;
|
||||
point?: ScenePathPoint;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
function setSelectedPathPointSelection(
|
||||
pathId: string,
|
||||
pointId: string
|
||||
): EditorSelection {
|
||||
return {
|
||||
kind: "pathPoint",
|
||||
pathId,
|
||||
pointId
|
||||
};
|
||||
}
|
||||
|
||||
export function createAddPathPointCommand(
|
||||
options: AddPathPointCommandOptions
|
||||
): EditorCommand {
|
||||
let appendedPoint: ScenePathPoint | null =
|
||||
options.point === undefined ? null : cloneScenePathPoint(options.point);
|
||||
let previousSelection: EditorSelection | null = null;
|
||||
let previousToolMode: ToolMode | null = null;
|
||||
|
||||
return {
|
||||
id: createOpaqueId("command"),
|
||||
label: options.label ?? "Add path point",
|
||||
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 (appendedPoint === null) {
|
||||
appendedPoint = createAppendedScenePathPoint(path);
|
||||
}
|
||||
|
||||
if (previousSelection === null) {
|
||||
previousSelection = cloneEditorSelection(context.getSelection());
|
||||
}
|
||||
|
||||
if (previousToolMode === null) {
|
||||
previousToolMode = context.getToolMode();
|
||||
}
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
paths: {
|
||||
...currentDocument.paths,
|
||||
[path.id]: cloneScenePath({
|
||||
...path,
|
||||
points: [...path.points, cloneScenePathPoint(appendedPoint)]
|
||||
})
|
||||
}
|
||||
});
|
||||
context.setSelection(
|
||||
setSelectedPathPointSelection(options.pathId, appendedPoint.id)
|
||||
);
|
||||
context.setToolMode("select");
|
||||
},
|
||||
undo(context) {
|
||||
if (appendedPoint === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
points: path.points.filter((point) => point.id !== appendedPoint?.id)
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
if (previousSelection !== null) {
|
||||
context.setSelection(previousSelection);
|
||||
}
|
||||
|
||||
if (previousToolMode !== null) {
|
||||
context.setToolMode(previousToolMode);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
141
src/commands/delete-path-point-command.ts
Normal file
141
src/commands/delete-path-point-command.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import { cloneEditorSelection, type EditorSelection } from "../core/selection";
|
||||
import type { ToolMode } from "../core/tool-mode";
|
||||
import {
|
||||
cloneScenePath,
|
||||
cloneScenePathPoint,
|
||||
getScenePathPointIndex,
|
||||
MIN_SCENE_PATH_POINT_COUNT,
|
||||
type ScenePathPoint
|
||||
} from "../document/paths";
|
||||
|
||||
import type { EditorCommand } from "./command";
|
||||
|
||||
interface DeletePathPointCommandOptions {
|
||||
pathId: string;
|
||||
pointId: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
function setSinglePathSelection(pathId: string): EditorSelection {
|
||||
return {
|
||||
kind: "paths",
|
||||
ids: [pathId]
|
||||
};
|
||||
}
|
||||
|
||||
function setSelectedPathPointSelection(
|
||||
pathId: string,
|
||||
pointId: string
|
||||
): EditorSelection {
|
||||
return {
|
||||
kind: "pathPoint",
|
||||
pathId,
|
||||
pointId
|
||||
};
|
||||
}
|
||||
|
||||
export function createDeletePathPointCommand(
|
||||
options: DeletePathPointCommandOptions
|
||||
): EditorCommand {
|
||||
let deletedPoint: ScenePathPoint | null = null;
|
||||
let deletedPointIndex: number | null = null;
|
||||
let previousSelection: EditorSelection | null = null;
|
||||
let previousToolMode: ToolMode | null = null;
|
||||
|
||||
return {
|
||||
id: createOpaqueId("command"),
|
||||
label: options.label ?? "Delete path point",
|
||||
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 (path.points.length <= MIN_SCENE_PATH_POINT_COUNT) {
|
||||
throw new Error(
|
||||
`Paths must keep at least ${MIN_SCENE_PATH_POINT_COUNT} points.`
|
||||
);
|
||||
}
|
||||
|
||||
const pointIndex = getScenePathPointIndex(path, options.pointId);
|
||||
|
||||
if (pointIndex === -1) {
|
||||
throw new Error(`Path point ${options.pointId} does not exist on path ${options.pathId}.`);
|
||||
}
|
||||
|
||||
if (deletedPoint === null) {
|
||||
deletedPoint = cloneScenePathPoint(path.points[pointIndex]);
|
||||
}
|
||||
|
||||
if (deletedPointIndex === null) {
|
||||
deletedPointIndex = pointIndex;
|
||||
}
|
||||
|
||||
if (previousSelection === null) {
|
||||
previousSelection = cloneEditorSelection(context.getSelection());
|
||||
}
|
||||
|
||||
if (previousToolMode === null) {
|
||||
previousToolMode = context.getToolMode();
|
||||
}
|
||||
|
||||
const nextPoints = path.points.filter((point) => point.id !== options.pointId);
|
||||
const fallbackPoint =
|
||||
nextPoints[Math.min(pointIndex, nextPoints.length - 1)] ?? null;
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
paths: {
|
||||
...currentDocument.paths,
|
||||
[path.id]: cloneScenePath({
|
||||
...path,
|
||||
points: nextPoints
|
||||
})
|
||||
}
|
||||
});
|
||||
context.setSelection(
|
||||
fallbackPoint === null
|
||||
? setSinglePathSelection(options.pathId)
|
||||
: setSelectedPathPointSelection(options.pathId, fallbackPoint.id)
|
||||
);
|
||||
context.setToolMode("select");
|
||||
},
|
||||
undo(context) {
|
||||
if (deletedPoint === null || deletedPointIndex === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDocument = context.getDocument();
|
||||
const path = currentDocument.paths[options.pathId];
|
||||
|
||||
if (path === undefined) {
|
||||
throw new Error(`Path ${options.pathId} does not exist.`);
|
||||
}
|
||||
|
||||
const nextPoints = [...path.points];
|
||||
nextPoints.splice(deletedPointIndex, 0, cloneScenePathPoint(deletedPoint));
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
paths: {
|
||||
...currentDocument.paths,
|
||||
[path.id]: cloneScenePath({
|
||||
...path,
|
||||
points: nextPoints
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
if (previousSelection !== null) {
|
||||
context.setSelection(previousSelection);
|
||||
}
|
||||
|
||||
if (previousToolMode !== null) {
|
||||
context.setToolMode(previousToolMode);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
142
src/commands/set-path-point-position-command.ts
Normal file
142
src/commands/set-path-point-position-command.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import { cloneEditorSelection, type EditorSelection } from "../core/selection";
|
||||
import type { ToolMode } from "../core/tool-mode";
|
||||
import {
|
||||
cloneScenePath,
|
||||
getScenePathPointIndex
|
||||
} from "../document/paths";
|
||||
import type { Vec3 } from "../core/vector";
|
||||
|
||||
import type { EditorCommand } from "./command";
|
||||
|
||||
interface SetPathPointPositionCommandOptions {
|
||||
pathId: string;
|
||||
pointId: string;
|
||||
position: Vec3;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
function cloneVec3(vector: Vec3): Vec3 {
|
||||
return {
|
||||
x: vector.x,
|
||||
y: vector.y,
|
||||
z: vector.z
|
||||
};
|
||||
}
|
||||
|
||||
function setSelectedPathPointSelection(
|
||||
pathId: string,
|
||||
pointId: string
|
||||
): EditorSelection {
|
||||
return {
|
||||
kind: "pathPoint",
|
||||
pathId,
|
||||
pointId
|
||||
};
|
||||
}
|
||||
|
||||
export function createSetPathPointPositionCommand(
|
||||
options: SetPathPointPositionCommandOptions
|
||||
): EditorCommand {
|
||||
const nextPosition = cloneVec3(options.position);
|
||||
let previousPosition: Vec3 | null = null;
|
||||
let previousSelection: EditorSelection | null = null;
|
||||
let previousToolMode: ToolMode | null = null;
|
||||
|
||||
return {
|
||||
id: createOpaqueId("command"),
|
||||
label: options.label ?? "Move path point",
|
||||
execute(context) {
|
||||
const currentDocument = context.getDocument();
|
||||
const path = currentDocument.paths[options.pathId];
|
||||
|
||||
if (path === undefined) {
|
||||
throw new Error(`Path ${options.pathId} does not exist.`);
|
||||
}
|
||||
|
||||
const pointIndex = getScenePathPointIndex(path, options.pointId);
|
||||
|
||||
if (pointIndex === -1) {
|
||||
throw new Error(`Path point ${options.pointId} does not exist on path ${options.pathId}.`);
|
||||
}
|
||||
|
||||
if (previousPosition === null) {
|
||||
previousPosition = cloneVec3(path.points[pointIndex].position);
|
||||
}
|
||||
|
||||
if (previousSelection === null) {
|
||||
previousSelection = cloneEditorSelection(context.getSelection());
|
||||
}
|
||||
|
||||
if (previousToolMode === null) {
|
||||
previousToolMode = context.getToolMode();
|
||||
}
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
paths: {
|
||||
...currentDocument.paths,
|
||||
[path.id]: cloneScenePath({
|
||||
...path,
|
||||
points: path.points.map((point, index) =>
|
||||
index === pointIndex
|
||||
? {
|
||||
...point,
|
||||
position: cloneVec3(nextPosition)
|
||||
}
|
||||
: point
|
||||
)
|
||||
})
|
||||
}
|
||||
});
|
||||
context.setSelection(
|
||||
setSelectedPathPointSelection(options.pathId, options.pointId)
|
||||
);
|
||||
context.setToolMode("select");
|
||||
},
|
||||
undo(context) {
|
||||
if (previousPosition === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDocument = context.getDocument();
|
||||
const path = currentDocument.paths[options.pathId];
|
||||
|
||||
if (path === undefined) {
|
||||
throw new Error(`Path ${options.pathId} does not exist.`);
|
||||
}
|
||||
|
||||
const pointIndex = getScenePathPointIndex(path, options.pointId);
|
||||
|
||||
if (pointIndex === -1) {
|
||||
throw new Error(`Path point ${options.pointId} does not exist on path ${options.pathId}.`);
|
||||
}
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
paths: {
|
||||
...currentDocument.paths,
|
||||
[path.id]: cloneScenePath({
|
||||
...path,
|
||||
points: path.points.map((point, index) =>
|
||||
index === pointIndex
|
||||
? {
|
||||
...point,
|
||||
position: cloneVec3(previousPosition)
|
||||
}
|
||||
: point
|
||||
)
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
if (previousSelection !== null) {
|
||||
context.setSelection(previousSelection);
|
||||
}
|
||||
|
||||
if (previousToolMode !== null) {
|
||||
context.setToolMode(previousToolMode);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user