auto-git:

[unlink] playwright.config.js
 [unlink] src/app/App.js
 [unlink] src/app/editor-store.js
 [unlink] src/app/use-editor-store.js
 [unlink] src/assets/audio-assets.js
 [unlink] src/assets/gltf-model-import.js
 [unlink] src/assets/image-assets.js
 [unlink] src/assets/model-instance-labels.js
 [unlink] src/assets/model-instance-rendering.js
 [unlink] src/assets/model-instances.js
 [unlink] src/assets/project-asset-storage.js
 [unlink] src/assets/project-assets.js
 [unlink] src/commands/brush-command-helpers.js
 [unlink] src/commands/command-history.js
 [unlink] src/commands/command.js
 [unlink] src/commands/commit-transform-session-command.js
 [unlink] src/commands/create-box-brush-command.js
 [unlink] src/commands/delete-box-brush-command.js
 [unlink] src/commands/delete-entity-command.js
 [unlink] src/commands/delete-interaction-link-command.js
 [unlink] src/commands/delete-model-instance-command.js
 [unlink] src/commands/duplicate-selection-command.js
 [unlink] src/commands/import-audio-asset-command.js
 [unlink] src/commands/import-background-image-asset-command.js
 [unlink] src/commands/import-model-asset-command.js
 [unlink] src/commands/move-box-brush-command.js
 [unlink] src/commands/resize-box-brush-command.js
 [unlink] src/commands/rotate-box-brush-command.js
 [unlink] src/commands/set-box-brush-face-material-command.js
 [unlink] src/commands/set-box-brush-face-uv-state-command.js
 [unlink] src/commands/set-box-brush-name-command.js
 [unlink] src/commands/set-box-brush-transform-command.js
 [unlink] src/commands/set-box-brush-volume-settings-command.js
 [unlink] src/commands/set-entity-name-command.js
 [unlink] src/commands/set-model-instance-name-command.js
 [unlink] src/commands/set-player-start-command.js
 [unlink] src/commands/set-scene-name-command.js
 [unlink] src/commands/set-world-settings-command.js
 [unlink] src/commands/upsert-entity-command.js
 [unlink] src/commands/upsert-interaction-link-command.js
 [unlink] src/commands/upsert-model-instance-command.js
 [unlink] src/core/ids.js
 [unlink] src/core/selection.js
 [unlink] src/core/tool-mode.js
 [unlink] src/core/transform-session.js
 [unlink] src/core/vector.js
 [unlink] src/core/whitebox-selection-feedback.js
 [unlink] src/core/whitebox-selection-mode.js
 [unlink] src/document/brushes.js
 [unlink] src/document/migrate-scene-document.js
 [unlink] src/document/scene-document-validation.js
 [unlink] src/document/scene-document.js
 [unlink] src/document/world-settings.js
 [unlink] src/entities/entity-instances.js
 [unlink] src/entities/entity-labels.js
 [unlink] src/geometry/box-brush-components.js
 [unlink] src/geometry/box-brush-mesh.js
 [unlink] src/geometry/box-brush.js
 [unlink] src/geometry/box-face-uvs.js
 [unlink] src/geometry/grid-snapping.js
 [unlink] src/geometry/model-instance-collider-debug-mesh.js
 [unlink] src/geometry/model-instance-collider-generation.js
 [unlink] src/interactions/interaction-links.js
 [unlink] src/main.js
 [unlink] src/materials/starter-material-library.js
 [unlink] src/materials/starter-material-textures.js
 [unlink] src/rendering/advanced-rendering.js
 [unlink] src/rendering/fog-material.js
 [unlink] src/rendering/planar-reflection.js
 [unlink] src/rendering/water-material.js
 [unlink] src/runner-web/RunnerCanvas.js
 [unlink] src/runtime-three/first-person-navigation-controller.js
 [unlink] src/runtime-three/navigation-controller.js
 [unlink] src/runtime-three/orbit-visitor-navigation-controller.js
 [unlink] src/runtime-three/player-collision.js
 [unlink] src/runtime-three/rapier-collision-world.js
 [unlink] src/runtime-three/runtime-audio-system.js
 [unlink] src/runtime-three/runtime-host.js
 [unlink] src/runtime-three/runtime-interaction-system.js
 [unlink] src/runtime-three/runtime-scene-build.js
 [unlink] src/runtime-three/runtime-scene-validation.js
 [unlink] src/runtime-three/underwater-fog.js
 [unlink] src/serialization/local-draft-storage.js
 [unlink] src/serialization/scene-document-json.js
 [unlink] src/shared-ui/HierarchicalMenu.js
 [unlink] src/shared-ui/Panel.js
 [unlink] src/shared-ui/world-background-style.js
 [unlink] src/viewport-three/ViewportCanvas.js
 [unlink] src/viewport-three/ViewportPanel.js
 [unlink] src/viewport-three/viewport-entity-markers.js
 [unlink] src/viewport-three/viewport-focus.js
 [unlink] src/viewport-three/viewport-host.js
 [unlink] src/viewport-three/viewport-layout.js
 [unlink] src/viewport-three/viewport-transient-state.js
 [unlink] src/viewport-three/viewport-view-modes.js
 [unlink] vite.config.js
 [unlink] vitest.config.js
This commit is contained in:
2026-04-11 15:48:39 +02:00
parent 277706e950
commit 9b7706bf5b
97 changed files with 0 additions and 21624 deletions

View File

@@ -1,82 +0,0 @@
import { cloneEditorSelection } from "../core/selection";
import { cloneFaceUvState } from "../document/brushes";
export function getBoxBrushOrThrow(document, brushId) {
const brush = document.brushes[brushId];
if (brush === undefined) {
throw new Error(`Box brush ${brushId} does not exist.`);
}
if (brush.kind !== "box") {
throw new Error(`Brush ${brushId} is not a supported box brush.`);
}
return brush;
}
export function setSingleBrushSelection(brushId) {
return {
kind: "brushes",
ids: [brushId]
};
}
export function setSingleBrushFaceSelection(brushId, faceId) {
return {
kind: "brushFace",
brushId,
faceId
};
}
export function setSingleBrushEdgeSelection(brushId, edgeId) {
return {
kind: "brushEdge",
brushId,
edgeId
};
}
export function setSingleBrushVertexSelection(brushId, vertexId) {
return {
kind: "brushVertex",
brushId,
vertexId
};
}
export function cloneSelectionForCommand(selection) {
return cloneEditorSelection(selection);
}
export function replaceBrush(document, brush) {
return {
...document,
brushes: {
...document.brushes,
[brush.id]: brush
}
};
}
export function removeBrush(document, brushId) {
const remainingBrushes = {
...document.brushes
};
delete remainingBrushes[brushId];
return {
...document,
brushes: remainingBrushes
};
}
export function getBoxBrushFaceOrThrow(document, brushId, faceId) {
const brush = getBoxBrushOrThrow(document, brushId);
const face = brush.faces[faceId];
if (face === undefined) {
throw new Error(`Box brush ${brushId} does not contain face ${faceId}.`);
}
return face;
}
export function replaceBoxBrushFace(document, brushId, faceId, face) {
const brush = getBoxBrushOrThrow(document, brushId);
return replaceBrush(document, {
...brush,
faces: {
...brush.faces,
[faceId]: {
materialId: face.materialId,
uv: cloneFaceUvState(face.uv)
}
}
});
}

View File

@@ -1,37 +0,0 @@
export class CommandHistory {
undoStack = [];
redoStack = [];
execute(command, context) {
command.execute(context);
this.undoStack.push(command);
this.redoStack.length = 0;
}
undo(context) {
const command = this.undoStack.pop();
if (command === undefined) {
return null;
}
command.undo(context);
this.redoStack.push(command);
return command;
}
redo(context) {
const command = this.redoStack.pop();
if (command === undefined) {
return null;
}
command.execute(context);
this.undoStack.push(command);
return command;
}
clear() {
this.undoStack.length = 0;
this.redoStack.length = 0;
}
canUndo() {
return this.undoStack.length > 0;
}
canRedo() {
return this.redoStack.length > 0;
}
}

View File

@@ -1 +0,0 @@
export {};

View File

@@ -1,200 +0,0 @@
import { createMoveBoxBrushCommand } from "./move-box-brush-command";
import { createResizeBoxBrushCommand } from "./resize-box-brush-command";
import { createRotateBoxBrushCommand } from "./rotate-box-brush-command";
import { createSetBoxBrushTransformCommand } from "./set-box-brush-transform-command";
import { createUpsertEntityCommand } from "./upsert-entity-command";
import { createUpsertModelInstanceCommand } from "./upsert-model-instance-command";
import { createModelInstance } from "../assets/model-instances";
import { createInteractableEntity, createPlayerStartEntity, createPointLightEntity, createSoundEmitterEntity, createSpotLightEntity, createTeleportTargetEntity, createTriggerVolumeEntity } from "../entities/entity-instances";
import { getTransformOperationLabel } from "../core/transform-session";
function createTransformCommandLabel(session) {
return `${getTransformOperationLabel(session.operation)} ${session.target.kind === "brush"
? "whitebox box"
: session.target.kind === "brushFace"
? "whitebox face"
: session.target.kind === "brushEdge"
? "whitebox edge"
: session.target.kind === "brushVertex"
? "whitebox vertex"
: session.target.kind === "entity"
? session.target.entityKind === "playerStart"
? "player start"
: session.target.entityKind === "pointLight"
? "point light"
: session.target.entityKind === "spotLight"
? "spot light"
: session.target.entityKind === "soundEmitter"
? "sound emitter"
: session.target.entityKind === "triggerVolume"
? "trigger volume"
: session.target.entityKind === "teleportTarget"
? "teleport target"
: "interactable"
: "model instance"}`;
}
export function createCommitTransformSessionCommand(document, session) {
switch (session.target.kind) {
case "brush":
if (session.preview.kind !== "brush") {
throw new Error("Brush transform preview is invalid.");
}
switch (session.operation) {
case "translate":
return createMoveBoxBrushCommand({
brushId: session.target.brushId,
center: session.preview.center,
snapToGrid: false,
label: createTransformCommandLabel(session)
});
case "rotate":
return createRotateBoxBrushCommand({
brushId: session.target.brushId,
rotationDegrees: session.preview.rotationDegrees,
label: createTransformCommandLabel(session)
});
case "scale":
return createResizeBoxBrushCommand({
brushId: session.target.brushId,
size: session.preview.size,
snapToGrid: false,
label: createTransformCommandLabel(session)
});
}
case "brushFace":
if (session.preview.kind !== "brush") {
throw new Error("Whitebox face transform preview is invalid.");
}
return createSetBoxBrushTransformCommand({
selection: {
kind: "brushFace",
brushId: session.target.brushId,
faceId: session.target.faceId
},
center: session.preview.center,
rotationDegrees: session.preview.rotationDegrees,
size: session.preview.size,
geometry: session.preview.geometry,
label: createTransformCommandLabel(session)
});
case "brushEdge":
if (session.preview.kind !== "brush") {
throw new Error("Whitebox edge transform preview is invalid.");
}
return createSetBoxBrushTransformCommand({
selection: {
kind: "brushEdge",
brushId: session.target.brushId,
edgeId: session.target.edgeId
},
center: session.preview.center,
rotationDegrees: session.preview.rotationDegrees,
size: session.preview.size,
geometry: session.preview.geometry,
label: createTransformCommandLabel(session)
});
case "brushVertex":
if (session.preview.kind !== "brush") {
throw new Error("Whitebox vertex transform preview is invalid.");
}
return createSetBoxBrushTransformCommand({
selection: {
kind: "brushVertex",
brushId: session.target.brushId,
vertexId: session.target.vertexId
},
center: session.preview.center,
rotationDegrees: session.preview.rotationDegrees,
size: session.preview.size,
geometry: session.preview.geometry,
label: createTransformCommandLabel(session)
});
case "modelInstance": {
if (session.preview.kind !== "modelInstance") {
throw new Error("Model instance transform preview is invalid.");
}
const modelInstance = document.modelInstances[session.target.modelInstanceId];
if (modelInstance === undefined) {
throw new Error(`Model instance ${session.target.modelInstanceId} does not exist.`);
}
return createUpsertModelInstanceCommand({
modelInstance: createModelInstance({
...modelInstance,
position: session.preview.position,
rotationDegrees: session.preview.rotationDegrees,
scale: session.preview.scale
}),
label: createTransformCommandLabel(session)
});
}
case "entity": {
if (session.preview.kind !== "entity") {
throw new Error("Entity transform preview is invalid.");
}
const entity = document.entities[session.target.entityId];
if (entity === undefined) {
throw new Error(`Entity ${session.target.entityId} does not exist.`);
}
switch (entity.kind) {
case "pointLight":
return createUpsertEntityCommand({
entity: createPointLightEntity({
...entity,
position: session.preview.position
}),
label: createTransformCommandLabel(session)
});
case "spotLight":
return createUpsertEntityCommand({
entity: createSpotLightEntity({
...entity,
position: session.preview.position,
direction: session.preview.rotation.kind === "direction" ? session.preview.rotation.direction : entity.direction
}),
label: createTransformCommandLabel(session)
});
case "playerStart":
return createUpsertEntityCommand({
entity: createPlayerStartEntity({
...entity,
position: session.preview.position,
yawDegrees: session.preview.rotation.kind === "yaw" ? session.preview.rotation.yawDegrees : entity.yawDegrees
}),
label: createTransformCommandLabel(session)
});
case "soundEmitter":
return createUpsertEntityCommand({
entity: createSoundEmitterEntity({
...entity,
position: session.preview.position
}),
label: createTransformCommandLabel(session)
});
case "triggerVolume":
return createUpsertEntityCommand({
entity: createTriggerVolumeEntity({
...entity,
position: session.preview.position
}),
label: createTransformCommandLabel(session)
});
case "teleportTarget":
return createUpsertEntityCommand({
entity: createTeleportTargetEntity({
...entity,
position: session.preview.position,
yawDegrees: session.preview.rotation.kind === "yaw" ? session.preview.rotation.yawDegrees : entity.yawDegrees
}),
label: createTransformCommandLabel(session)
});
case "interactable":
return createUpsertEntityCommand({
entity: createInteractableEntity({
...entity,
position: session.preview.position
}),
label: createTransformCommandLabel(session)
});
}
}
}
}

View File

@@ -1,51 +0,0 @@
import { createBoxBrush, DEFAULT_BOX_BRUSH_CENTER, DEFAULT_BOX_BRUSH_SIZE } from "../document/brushes";
import { DEFAULT_GRID_SIZE, snapPositiveSizeToGrid, snapVec3ToGrid } from "../geometry/grid-snapping";
import { createOpaqueId } from "../core/ids";
import { cloneSelectionForCommand, removeBrush, setSingleBrushSelection } from "./brush-command-helpers";
export function createCreateBoxBrushCommand(options = {}) {
const snapToGrid = options.snapToGrid ?? true;
const brush = createBoxBrush({
center: snapToGrid === false
? options.center ?? DEFAULT_BOX_BRUSH_CENTER
: snapVec3ToGrid(options.center ?? DEFAULT_BOX_BRUSH_CENTER, options.gridSize ?? DEFAULT_GRID_SIZE),
size: snapToGrid === false
? options.size ?? DEFAULT_BOX_BRUSH_SIZE
: snapPositiveSizeToGrid(options.size ?? DEFAULT_BOX_BRUSH_SIZE, options.gridSize ?? DEFAULT_GRID_SIZE)
});
let previousSelection = null;
let previousToolMode = null;
return {
id: createOpaqueId("command"),
label: "Create box brush",
execute(context) {
const currentDocument = context.getDocument();
if (currentDocument.brushes[brush.id] !== undefined) {
throw new Error(`Box brush ${brush.id} already exists.`);
}
if (previousSelection === null) {
previousSelection = cloneSelectionForCommand(context.getSelection());
}
if (previousToolMode === null) {
previousToolMode = context.getToolMode();
}
context.setDocument({
...currentDocument,
brushes: {
...currentDocument.brushes,
[brush.id]: brush
}
});
context.setSelection(setSingleBrushSelection(brush.id));
context.setToolMode("select");
},
undo(context) {
context.setDocument(removeBrush(context.getDocument(), brush.id));
if (previousSelection !== null) {
context.setSelection(previousSelection);
}
if (previousToolMode !== null) {
context.setToolMode(previousToolMode);
}
}
};
}

View File

@@ -1,59 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneBoxBrush } from "../document/brushes";
import { cloneSelectionForCommand, removeBrush } from "./brush-command-helpers";
function selectionIncludesBrush(selection, brushId) {
return ((selection.kind === "brushes" && selection.ids.includes(brushId)) ||
((selection.kind === "brushFace" || selection.kind === "brushEdge" || selection.kind === "brushVertex") &&
selection.brushId === brushId));
}
export function createDeleteBoxBrushCommand(brushId) {
let previousBrush = null;
let previousSelection = null;
let previousToolMode = null;
return {
id: createOpaqueId("command"),
label: "Delete box brush",
execute(context) {
const currentDocument = context.getDocument();
const currentBrush = currentDocument.brushes[brushId];
if (currentBrush === undefined) {
throw new Error(`Box brush ${brushId} does not exist.`);
}
if (previousBrush === null) {
previousBrush = cloneBoxBrush(currentBrush);
}
if (previousSelection === null) {
previousSelection = cloneSelectionForCommand(context.getSelection());
}
if (previousToolMode === null) {
previousToolMode = context.getToolMode();
}
context.setDocument(removeBrush(currentDocument, brushId));
if (selectionIncludesBrush(context.getSelection(), brushId)) {
context.setSelection({
kind: "none"
});
}
context.setToolMode("select");
},
undo(context) {
if (previousBrush === null) {
return;
}
const currentDocument = context.getDocument();
context.setDocument({
...currentDocument,
brushes: {
...currentDocument.brushes,
[previousBrush.id]: cloneBoxBrush(previousBrush)
}
});
if (previousSelection !== null) {
context.setSelection(previousSelection);
}
if (previousToolMode !== null) {
context.setToolMode(previousToolMode);
}
}
};
}

View File

@@ -1,64 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneEditorSelection } from "../core/selection";
import { cloneEntityInstance } from "../entities/entity-instances";
function selectionIncludesEntity(selection, entityId) {
return selection.kind === "entities" && selection.ids.includes(entityId);
}
export function createDeleteEntityCommand(entityId) {
let previousEntity = null;
let previousSelection = null;
let previousToolMode = null;
return {
id: createOpaqueId("command"),
label: "Delete entity",
execute(context) {
const currentDocument = context.getDocument();
const currentEntity = currentDocument.entities[entityId];
if (currentEntity === undefined) {
throw new Error(`Entity ${entityId} does not exist.`);
}
if (previousEntity === null) {
previousEntity = cloneEntityInstance(currentEntity);
}
if (previousSelection === null) {
previousSelection = cloneEditorSelection(context.getSelection());
}
if (previousToolMode === null) {
previousToolMode = context.getToolMode();
}
const nextEntities = {
...currentDocument.entities
};
delete nextEntities[entityId];
context.setDocument({
...currentDocument,
entities: nextEntities
});
if (selectionIncludesEntity(context.getSelection(), entityId)) {
context.setSelection({
kind: "none"
});
}
context.setToolMode("select");
},
undo(context) {
if (previousEntity === null) {
return;
}
const currentDocument = context.getDocument();
context.setDocument({
...currentDocument,
entities: {
...currentDocument.entities,
[previousEntity.id]: cloneEntityInstance(previousEntity)
}
});
if (previousSelection !== null) {
context.setSelection(previousSelection);
}
if (previousToolMode !== null) {
context.setToolMode(previousToolMode);
}
}
};
}

View File

@@ -1,40 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneInteractionLink } from "../interactions/interaction-links";
export function createDeleteInteractionLinkCommand(linkId) {
let previousLink = null;
return {
id: createOpaqueId("command"),
label: "Delete interaction link",
execute(context) {
const currentDocument = context.getDocument();
const currentLink = currentDocument.interactionLinks[linkId];
if (currentLink === undefined) {
throw new Error(`Interaction link ${linkId} does not exist.`);
}
if (previousLink === null) {
previousLink = cloneInteractionLink(currentLink);
}
const nextInteractionLinks = {
...currentDocument.interactionLinks
};
delete nextInteractionLinks[linkId];
context.setDocument({
...currentDocument,
interactionLinks: nextInteractionLinks
});
},
undo(context) {
if (previousLink === null) {
return;
}
const currentDocument = context.getDocument();
context.setDocument({
...currentDocument,
interactionLinks: {
...currentDocument.interactionLinks,
[previousLink.id]: cloneInteractionLink(previousLink)
}
});
}
};
}

View File

@@ -1,64 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneEditorSelection } from "../core/selection";
import { cloneModelInstance } from "../assets/model-instances";
function selectionIncludesModelInstance(selection, modelInstanceId) {
return selection.kind === "modelInstances" && selection.ids.includes(modelInstanceId);
}
export function createDeleteModelInstanceCommand(modelInstanceId) {
let previousModelInstance = null;
let previousSelection = null;
let previousToolMode = null;
return {
id: createOpaqueId("command"),
label: "Delete model instance",
execute(context) {
const currentDocument = context.getDocument();
const currentModelInstance = currentDocument.modelInstances[modelInstanceId];
if (currentModelInstance === undefined) {
throw new Error(`Model instance ${modelInstanceId} does not exist.`);
}
if (previousModelInstance === null) {
previousModelInstance = cloneModelInstance(currentModelInstance);
}
if (previousSelection === null) {
previousSelection = cloneEditorSelection(context.getSelection());
}
if (previousToolMode === null) {
previousToolMode = context.getToolMode();
}
const nextModelInstances = {
...currentDocument.modelInstances
};
delete nextModelInstances[modelInstanceId];
context.setDocument({
...currentDocument,
modelInstances: nextModelInstances
});
if (selectionIncludesModelInstance(context.getSelection(), modelInstanceId)) {
context.setSelection({
kind: "none"
});
}
context.setToolMode("select");
},
undo(context) {
if (previousModelInstance === null) {
return;
}
const currentDocument = context.getDocument();
context.setDocument({
...currentDocument,
modelInstances: {
...currentDocument.modelInstances,
[previousModelInstance.id]: cloneModelInstance(previousModelInstance)
}
});
if (previousSelection !== null) {
context.setSelection(previousSelection);
}
if (previousToolMode !== null) {
context.setToolMode(previousToolMode);
}
}
};
}

View File

@@ -1,237 +0,0 @@
import { cloneModelInstance } from "../assets/model-instances";
import { createOpaqueId } from "../core/ids";
import { cloneEditorSelection } from "../core/selection";
import { cloneBoxBrush } from "../document/brushes";
import { cloneEntityInstance } from "../entities/entity-instances";
function duplicateBrush(brush) {
const duplicatedBrush = cloneBoxBrush(brush);
duplicatedBrush.id = createOpaqueId("brush");
return duplicatedBrush;
}
function duplicateEntity(entity) {
const duplicatedEntity = cloneEntityInstance(entity);
duplicatedEntity.id = createOpaqueId(`entity-${duplicatedEntity.kind}`);
return duplicatedEntity;
}
function duplicateModelInstance(modelInstance) {
const duplicatedModelInstance = cloneModelInstance(modelInstance);
duplicatedModelInstance.id = createOpaqueId("model-instance");
return duplicatedModelInstance;
}
function resolveDuplicatableBrushIds(selection) {
switch (selection.kind) {
case "brushes":
return selection.ids;
case "brushFace":
case "brushEdge":
case "brushVertex":
return [selection.brushId];
default:
return null;
}
}
function createDuplicateSelectionResult(currentDocument, selection) {
const duplicatableBrushIds = resolveDuplicatableBrushIds(selection);
if (duplicatableBrushIds !== null) {
if (duplicatableBrushIds.length === 0) {
throw new Error("Select at least one whitebox solid to duplicate.");
}
const duplicatedBrushes = duplicatableBrushIds.map((brushId) => {
const sourceBrush = currentDocument.brushes[brushId];
if (sourceBrush === undefined) {
throw new Error(`Box brush ${brushId} does not exist.`);
}
if (sourceBrush.kind !== "box") {
throw new Error(`Brush ${brushId} is not a supported box brush.`);
}
return duplicateBrush(sourceBrush);
});
return {
selection: {
kind: "brushes",
ids: duplicatedBrushes.map((brush) => brush.id)
},
brushes: duplicatedBrushes,
entities: null,
modelInstances: null
};
}
if (selection.kind === "entities") {
if (selection.ids.length === 0) {
throw new Error("Select at least one entity to duplicate.");
}
const duplicatedEntities = selection.ids.map((entityId) => {
const sourceEntity = currentDocument.entities[entityId];
if (sourceEntity === undefined) {
throw new Error(`Entity ${entityId} does not exist.`);
}
return duplicateEntity(sourceEntity);
});
return {
selection: {
kind: "entities",
ids: duplicatedEntities.map((entity) => entity.id)
},
brushes: null,
entities: duplicatedEntities,
modelInstances: null
};
}
if (selection.kind === "modelInstances") {
if (selection.ids.length === 0) {
throw new Error("Select at least one model instance to duplicate.");
}
const duplicatedModelInstances = selection.ids.map((modelInstanceId) => {
const sourceModelInstance = currentDocument.modelInstances[modelInstanceId];
if (sourceModelInstance === undefined) {
throw new Error(`Model instance ${modelInstanceId} does not exist.`);
}
return duplicateModelInstance(sourceModelInstance);
});
return {
selection: {
kind: "modelInstances",
ids: duplicatedModelInstances.map((modelInstance) => modelInstance.id)
},
brushes: null,
entities: null,
modelInstances: duplicatedModelInstances
};
}
throw new Error("Selection must contain whitebox solids, entities, or model instances to duplicate.");
}
export function createDuplicateSelectionCommand() {
let previousSelection = null;
let previousToolMode = null;
let duplicateSelectionResult = null;
return {
id: createOpaqueId("command"),
label: "Duplicate selection",
execute(context) {
const currentDocument = context.getDocument();
if (previousSelection === null) {
previousSelection = cloneEditorSelection(context.getSelection());
}
if (previousToolMode === null) {
previousToolMode = context.getToolMode();
}
if (duplicateSelectionResult === null) {
duplicateSelectionResult = createDuplicateSelectionResult(currentDocument, context.getSelection());
}
if (duplicateSelectionResult.brushes !== null) {
context.setDocument({
...currentDocument,
brushes: {
...currentDocument.brushes,
...Object.fromEntries(duplicateSelectionResult.brushes.map((brush) => [brush.id, cloneBoxBrush(brush)]))
}
});
} else if (duplicateSelectionResult.entities !== null) {
context.setDocument({
...currentDocument,
entities: {
...currentDocument.entities,
...Object.fromEntries(duplicateSelectionResult.entities.map((entity) => [entity.id, cloneEntityInstance(entity)]))
}
});
} else if (duplicateSelectionResult.modelInstances !== null) {
context.setDocument({
...currentDocument,
modelInstances: {
...currentDocument.modelInstances,
...Object.fromEntries(
duplicateSelectionResult.modelInstances.map((modelInstance) => [modelInstance.id, cloneModelInstance(modelInstance)])
)
}
});
}
context.setSelection(cloneEditorSelection(duplicateSelectionResult.selection));
context.setToolMode("select");
},
undo(context) {
if (duplicateSelectionResult === null) {
return;
}
const currentDocument = context.getDocument();
if (duplicateSelectionResult.brushes !== null) {
const nextBrushes = {
...currentDocument.brushes
};
for (const duplicatedBrush of duplicateSelectionResult.brushes) {
delete nextBrushes[duplicatedBrush.id];
}
context.setDocument({
...currentDocument,
brushes: nextBrushes
});
} else if (duplicateSelectionResult.entities !== null) {
const nextEntities = {
...currentDocument.entities
};
for (const duplicatedEntity of duplicateSelectionResult.entities) {
delete nextEntities[duplicatedEntity.id];
}
context.setDocument({
...currentDocument,
entities: nextEntities
});
} else if (duplicateSelectionResult.modelInstances !== null) {
const nextModelInstances = {
...currentDocument.modelInstances
};
for (const duplicatedModelInstance of duplicateSelectionResult.modelInstances) {
delete nextModelInstances[duplicatedModelInstance.id];
}
context.setDocument({
...currentDocument,
modelInstances: nextModelInstances
});
}
if (previousSelection !== null) {
context.setSelection(previousSelection);
}
if (previousToolMode !== null) {
context.setToolMode(previousToolMode);
}
}
};
}

View File

@@ -1,33 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneProjectAssetRecord } from "../assets/project-assets";
export function createImportAudioAssetCommand(options) {
const nextAsset = cloneProjectAssetRecord(options.asset);
return {
id: createOpaqueId("command"),
label: options.label ?? `Import ${nextAsset.sourceName}`,
execute(context) {
const currentDocument = context.getDocument();
if (currentDocument.assets[nextAsset.id] !== undefined) {
throw new Error(`Asset ${nextAsset.id} already exists.`);
}
context.setDocument({
...currentDocument,
assets: {
...currentDocument.assets,
[nextAsset.id]: cloneProjectAssetRecord(nextAsset)
}
});
},
undo(context) {
const currentDocument = context.getDocument();
const nextAssets = {
...currentDocument.assets
};
delete nextAssets[nextAsset.id];
context.setDocument({
...currentDocument,
assets: nextAssets
});
}
};
}

View File

@@ -1,41 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneWorldSettings } from "../document/world-settings";
import { cloneProjectAssetRecord } from "../assets/project-assets";
export function createImportBackgroundImageAssetCommand(options) {
const nextAsset = cloneProjectAssetRecord(options.asset);
const nextWorld = cloneWorldSettings(options.world);
let previousWorld = null;
return {
id: createOpaqueId("command"),
label: options.label ?? `Import ${nextAsset.sourceName} as background`,
execute(context) {
const currentDocument = context.getDocument();
if (currentDocument.assets[nextAsset.id] !== undefined) {
throw new Error(`Asset ${nextAsset.id} already exists.`);
}
if (previousWorld === null) {
previousWorld = cloneWorldSettings(currentDocument.world);
}
context.setDocument({
...currentDocument,
assets: {
...currentDocument.assets,
[nextAsset.id]: cloneProjectAssetRecord(nextAsset)
},
world: cloneWorldSettings(nextWorld)
});
},
undo(context) {
const currentDocument = context.getDocument();
const nextAssets = {
...currentDocument.assets
};
delete nextAssets[nextAsset.id];
context.setDocument({
...currentDocument,
assets: nextAssets,
world: previousWorld === null ? cloneWorldSettings(currentDocument.world) : cloneWorldSettings(previousWorld)
});
}
};
}

View File

@@ -1,70 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneEditorSelection } from "../core/selection";
import { cloneModelInstance } from "../assets/model-instances";
import { cloneProjectAssetRecord } from "../assets/project-assets";
function setSingleModelInstanceSelection(modelInstanceId) {
return {
kind: "modelInstances",
ids: [modelInstanceId]
};
}
export function createImportModelAssetCommand(options) {
const nextAsset = cloneProjectAssetRecord(options.asset);
const nextModelInstance = cloneModelInstance(options.modelInstance);
let previousSelection = null;
let previousToolMode = null;
return {
id: createOpaqueId("command"),
label: options.label ?? `Import ${nextAsset.sourceName}`,
execute(context) {
const currentDocument = context.getDocument();
if (currentDocument.assets[nextAsset.id] !== undefined) {
throw new Error(`Asset ${nextAsset.id} already exists.`);
}
if (currentDocument.modelInstances[nextModelInstance.id] !== undefined) {
throw new Error(`Model instance ${nextModelInstance.id} already exists.`);
}
if (previousSelection === null) {
previousSelection = cloneEditorSelection(context.getSelection());
}
if (previousToolMode === null) {
previousToolMode = context.getToolMode();
}
context.setDocument({
...currentDocument,
assets: {
...currentDocument.assets,
[nextAsset.id]: cloneProjectAssetRecord(nextAsset)
},
modelInstances: {
...currentDocument.modelInstances,
[nextModelInstance.id]: cloneModelInstance(nextModelInstance)
}
});
context.setSelection(setSingleModelInstanceSelection(nextModelInstance.id));
context.setToolMode("select");
},
undo(context) {
const currentDocument = context.getDocument();
const nextAssets = {
...currentDocument.assets
};
const nextModelInstances = {
...currentDocument.modelInstances
};
delete nextAssets[nextAsset.id];
delete nextModelInstances[nextModelInstance.id];
context.setDocument({
...currentDocument,
assets: nextAssets,
modelInstances: nextModelInstances
});
if (previousSelection !== null) {
context.setSelection(previousSelection);
}
if (previousToolMode !== null) {
context.setToolMode(previousToolMode);
}
}
};
}

View File

@@ -1,55 +0,0 @@
import { DEFAULT_GRID_SIZE, snapVec3ToGrid } from "../geometry/grid-snapping";
import { createOpaqueId } from "../core/ids";
import { cloneSelectionForCommand, getBoxBrushOrThrow, replaceBrush, setSingleBrushSelection } from "./brush-command-helpers";
export function createMoveBoxBrushCommand(options) {
const resolvedCenter = options.snapToGrid === false ? options.center : snapVec3ToGrid(options.center, options.gridSize ?? DEFAULT_GRID_SIZE);
let previousCenter = null;
let previousSelection = null;
let previousToolMode = null;
return {
id: createOpaqueId("command"),
label: options.label ?? "Move box brush",
execute(context) {
const currentDocument = context.getDocument();
const brush = getBoxBrushOrThrow(currentDocument, options.brushId);
if (previousCenter === null) {
previousCenter = {
...brush.center
};
}
if (previousSelection === null) {
previousSelection = cloneSelectionForCommand(context.getSelection());
}
if (previousToolMode === null) {
previousToolMode = context.getToolMode();
}
context.setDocument(replaceBrush(currentDocument, {
...brush,
center: {
...resolvedCenter
}
}));
context.setSelection(setSingleBrushSelection(options.brushId));
context.setToolMode("select");
},
undo(context) {
if (previousCenter === null) {
return;
}
const currentDocument = context.getDocument();
const brush = getBoxBrushOrThrow(currentDocument, options.brushId);
context.setDocument(replaceBrush(currentDocument, {
...brush,
center: {
...previousCenter
}
}));
if (previousSelection !== null) {
context.setSelection(previousSelection);
}
if (previousToolMode !== null) {
context.setToolMode(previousToolMode);
}
}
};
}

View File

@@ -1,61 +0,0 @@
import { DEFAULT_GRID_SIZE, snapPositiveSizeToGrid } from "../geometry/grid-snapping";
import { createOpaqueId } from "../core/ids";
import { cloneBoxBrushGeometry, scaleBoxBrushGeometryToSize } from "../document/brushes";
import { cloneSelectionForCommand, getBoxBrushOrThrow, replaceBrush, setSingleBrushSelection } from "./brush-command-helpers";
export function createResizeBoxBrushCommand(options) {
const resolvedSize = options.snapToGrid === false ? options.size : snapPositiveSizeToGrid(options.size, options.gridSize ?? DEFAULT_GRID_SIZE);
let previousSize = null;
let previousGeometry = null;
let previousSelection = null;
let previousToolMode = null;
return {
id: createOpaqueId("command"),
label: options.label ?? "Resize box brush",
execute(context) {
const currentDocument = context.getDocument();
const brush = getBoxBrushOrThrow(currentDocument, options.brushId);
if (previousSize === null) {
previousSize = {
...brush.size
};
previousGeometry = cloneBoxBrushGeometry(brush.geometry);
}
if (previousSelection === null) {
previousSelection = cloneSelectionForCommand(context.getSelection());
}
if (previousToolMode === null) {
previousToolMode = context.getToolMode();
}
const nextGeometry = scaleBoxBrushGeometryToSize(brush.geometry, resolvedSize);
context.setDocument(replaceBrush(currentDocument, {
...brush,
size: {
...resolvedSize
},
geometry: nextGeometry
}));
context.setSelection(setSingleBrushSelection(options.brushId));
context.setToolMode("select");
},
undo(context) {
if (previousSize === null || previousGeometry === null) {
return;
}
const currentDocument = context.getDocument();
const brush = getBoxBrushOrThrow(currentDocument, options.brushId);
context.setDocument(replaceBrush(currentDocument, {
...brush,
size: {
...previousSize
},
geometry: cloneBoxBrushGeometry(previousGeometry)
}));
if (previousSelection !== null) {
context.setSelection(previousSelection);
}
if (previousToolMode !== null) {
context.setToolMode(previousToolMode);
}
}
};
}

View File

@@ -1,53 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneSelectionForCommand, getBoxBrushOrThrow, replaceBrush, setSingleBrushSelection } from "./brush-command-helpers";
export function createRotateBoxBrushCommand(options) {
let previousRotationDegrees = null;
let previousSelection = null;
let previousToolMode = null;
return {
id: createOpaqueId("command"),
label: options.label ?? "Rotate box brush",
execute(context) {
const currentDocument = context.getDocument();
const brush = getBoxBrushOrThrow(currentDocument, options.brushId);
if (previousRotationDegrees === null) {
previousRotationDegrees = {
...brush.rotationDegrees
};
}
if (previousSelection === null) {
previousSelection = cloneSelectionForCommand(context.getSelection());
}
if (previousToolMode === null) {
previousToolMode = context.getToolMode();
}
context.setDocument(replaceBrush(currentDocument, {
...brush,
rotationDegrees: {
...options.rotationDegrees
}
}));
context.setSelection(setSingleBrushSelection(options.brushId));
context.setToolMode("select");
},
undo(context) {
if (previousRotationDegrees === null) {
return;
}
const currentDocument = context.getDocument();
const brush = getBoxBrushOrThrow(currentDocument, options.brushId);
context.setDocument(replaceBrush(currentDocument, {
...brush,
rotationDegrees: {
...previousRotationDegrees
}
}));
if (previousSelection !== null) {
context.setSelection(previousSelection);
}
if (previousToolMode !== null) {
context.setToolMode(previousToolMode);
}
}
};
}

View File

@@ -1,50 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneSelectionForCommand, getBoxBrushFaceOrThrow, replaceBoxBrushFace, setSingleBrushFaceSelection } from "./brush-command-helpers";
export function createSetBoxBrushFaceMaterialCommand(options) {
let previousMaterialId;
let previousSelection = null;
let previousToolMode = null;
return {
id: createOpaqueId("command"),
label: options.materialId === null ? `Clear ${options.faceId} face material` : `Apply material to ${options.faceId} face`,
execute(context) {
const currentDocument = context.getDocument();
const currentFace = getBoxBrushFaceOrThrow(currentDocument, options.brushId, options.faceId);
if (options.materialId !== null && currentDocument.materials[options.materialId] === undefined) {
throw new Error(`Material ${options.materialId} does not exist in the document registry.`);
}
if (previousMaterialId === undefined) {
previousMaterialId = currentFace.materialId;
}
if (previousSelection === null) {
previousSelection = cloneSelectionForCommand(context.getSelection());
}
if (previousToolMode === null) {
previousToolMode = context.getToolMode();
}
context.setDocument(replaceBoxBrushFace(currentDocument, options.brushId, options.faceId, {
...currentFace,
materialId: options.materialId
}));
context.setSelection(setSingleBrushFaceSelection(options.brushId, options.faceId));
context.setToolMode("select");
},
undo(context) {
if (previousMaterialId === undefined) {
return;
}
const currentDocument = context.getDocument();
const currentFace = getBoxBrushFaceOrThrow(currentDocument, options.brushId, options.faceId);
context.setDocument(replaceBoxBrushFace(currentDocument, options.brushId, options.faceId, {
...currentFace,
materialId: previousMaterialId
}));
if (previousSelection !== null) {
context.setSelection(previousSelection);
}
if (previousToolMode !== null) {
context.setToolMode(previousToolMode);
}
}
};
}

View File

@@ -1,48 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneFaceUvState } from "../document/brushes";
import { cloneSelectionForCommand, getBoxBrushFaceOrThrow, replaceBoxBrushFace, setSingleBrushFaceSelection } from "./brush-command-helpers";
export function createSetBoxBrushFaceUvStateCommand(options) {
let previousUvState = null;
let previousSelection = null;
let previousToolMode = null;
return {
id: createOpaqueId("command"),
label: options.label ?? `Update ${options.faceId} face UVs`,
execute(context) {
const currentDocument = context.getDocument();
const currentFace = getBoxBrushFaceOrThrow(currentDocument, options.brushId, options.faceId);
if (previousUvState === null) {
previousUvState = cloneFaceUvState(currentFace.uv);
}
if (previousSelection === null) {
previousSelection = cloneSelectionForCommand(context.getSelection());
}
if (previousToolMode === null) {
previousToolMode = context.getToolMode();
}
context.setDocument(replaceBoxBrushFace(currentDocument, options.brushId, options.faceId, {
...currentFace,
uv: cloneFaceUvState(options.uvState)
}));
context.setSelection(setSingleBrushFaceSelection(options.brushId, options.faceId));
context.setToolMode("select");
},
undo(context) {
if (previousUvState === null) {
return;
}
const currentDocument = context.getDocument();
const currentFace = getBoxBrushFaceOrThrow(currentDocument, options.brushId, options.faceId);
context.setDocument(replaceBoxBrushFace(currentDocument, options.brushId, options.faceId, {
...currentFace,
uv: cloneFaceUvState(previousUvState)
}));
if (previousSelection !== null) {
context.setSelection(previousSelection);
}
if (previousToolMode !== null) {
context.setToolMode(previousToolMode);
}
}
};
}

View File

@@ -1,30 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { normalizeBrushName } from "../document/brushes";
import { getBoxBrushOrThrow, replaceBrush } from "./brush-command-helpers";
export function createSetBoxBrushNameCommand(options) {
const normalizedName = normalizeBrushName(options.name);
let previousName;
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
}));
}
};
}

View File

@@ -1,95 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneBoxBrushGeometry, deriveBoxBrushSizeFromGeometry, scaleBoxBrushGeometryToSize } from "../document/brushes";
import { cloneSelectionForCommand, getBoxBrushOrThrow, replaceBrush, setSingleBrushEdgeSelection, setSingleBrushFaceSelection, setSingleBrushSelection, setSingleBrushVertexSelection } from "./brush-command-helpers";
function cloneVec3(vector) {
return {
x: vector.x,
y: vector.y,
z: vector.z
};
}
function selectionToEditorSelection(selection) {
switch (selection.kind) {
case "brush":
return setSingleBrushSelection(selection.brushId);
case "brushFace":
return setSingleBrushFaceSelection(selection.brushId, selection.faceId);
case "brushEdge":
return setSingleBrushEdgeSelection(selection.brushId, selection.edgeId);
case "brushVertex":
return setSingleBrushVertexSelection(selection.brushId, selection.vertexId);
}
}
function getBrushId(selection) {
return selection.brushId;
}
function assertPositiveSize(size) {
if (!(size.x > 0 && size.y > 0 && size.z > 0)) {
throw new Error("Whitebox box size must remain positive on every axis.");
}
if (!Number.isFinite(size.x) || !Number.isFinite(size.y) || !Number.isFinite(size.z)) {
throw new Error("Whitebox box size values must be finite numbers.");
}
}
export function createSetBoxBrushTransformCommand(options) {
assertPositiveSize(options.size);
let previousSnapshot = null;
let previousSelection = null;
let previousToolMode = null;
return {
id: createOpaqueId("command"),
label: options.label ?? "Set box brush transform",
execute(context) {
const currentDocument = context.getDocument();
const brushId = getBrushId(options.selection);
const brush = getBoxBrushOrThrow(currentDocument, brushId);
if (previousSnapshot === null) {
previousSnapshot = {
center: cloneVec3(brush.center),
rotationDegrees: cloneVec3(brush.rotationDegrees),
size: cloneVec3(brush.size),
geometry: cloneBoxBrushGeometry(brush.geometry)
};
}
if (previousSelection === null) {
previousSelection = cloneSelectionForCommand(context.getSelection());
}
if (previousToolMode === null) {
previousToolMode = context.getToolMode();
}
const nextGeometry = options.geometry === undefined ? scaleBoxBrushGeometryToSize(brush.geometry, options.size) : cloneBoxBrushGeometry(options.geometry);
const nextSize = deriveBoxBrushSizeFromGeometry(nextGeometry);
assertPositiveSize(nextSize);
context.setDocument(replaceBrush(currentDocument, {
...brush,
center: cloneVec3(options.center),
rotationDegrees: cloneVec3(options.rotationDegrees),
size: nextSize,
geometry: nextGeometry
}));
context.setSelection(selectionToEditorSelection(options.selection));
context.setToolMode("select");
},
undo(context) {
if (previousSnapshot === null) {
return;
}
const currentDocument = context.getDocument();
const brushId = getBrushId(options.selection);
const brush = getBoxBrushOrThrow(currentDocument, brushId);
context.setDocument(replaceBrush(currentDocument, {
...brush,
center: cloneVec3(previousSnapshot.center),
rotationDegrees: cloneVec3(previousSnapshot.rotationDegrees),
size: cloneVec3(previousSnapshot.size),
geometry: cloneBoxBrushGeometry(previousSnapshot.geometry)
}));
if (previousSelection !== null) {
context.setSelection(previousSelection);
}
if (previousToolMode !== null) {
context.setToolMode(previousToolMode);
}
}
};
}

View File

@@ -1,33 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneBoxBrushVolumeSettings } from "../document/brushes";
import { getBoxBrushOrThrow, replaceBrush } from "./brush-command-helpers";
export function createSetBoxBrushVolumeSettingsCommand(options) {
const nextVolume = cloneBoxBrushVolumeSettings(options.volume);
let previousVolume = null;
return {
id: createOpaqueId("command"),
label: options.label ?? "Set box volume settings",
execute(context) {
const currentDocument = context.getDocument();
const brush = getBoxBrushOrThrow(currentDocument, options.brushId);
if (previousVolume === null) {
previousVolume = cloneBoxBrushVolumeSettings(brush.volume);
}
context.setDocument(replaceBrush(currentDocument, {
...brush,
volume: cloneBoxBrushVolumeSettings(nextVolume)
}));
},
undo(context) {
if (previousVolume === null) {
return;
}
const currentDocument = context.getDocument();
const brush = getBoxBrushOrThrow(currentDocument, options.brushId);
context.setDocument(replaceBrush(currentDocument, {
...brush,
volume: cloneBoxBrushVolumeSettings(previousVolume)
}));
}
};
}

View File

@@ -1,47 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneEntityInstance, normalizeEntityName } from "../entities/entity-instances";
export function createSetEntityNameCommand(options) {
const normalizedName = normalizeEntityName(options.name);
let previousName;
return {
id: createOpaqueId("command"),
label: normalizedName === undefined ? "Clear entity name" : `Rename entity to ${normalizedName}`,
execute(context) {
const currentDocument = context.getDocument();
const entity = currentDocument.entities[options.entityId];
if (entity === undefined) {
throw new Error(`Entity ${options.entityId} does not exist.`);
}
if (previousName === undefined) {
previousName = entity.name;
}
context.setDocument({
...currentDocument,
entities: {
...currentDocument.entities,
[entity.id]: cloneEntityInstance({
...entity,
name: normalizedName
})
}
});
},
undo(context) {
const currentDocument = context.getDocument();
const entity = currentDocument.entities[options.entityId];
if (entity === undefined) {
throw new Error(`Entity ${options.entityId} does not exist.`);
}
context.setDocument({
...currentDocument,
entities: {
...currentDocument.entities,
[entity.id]: cloneEntityInstance({
...entity,
name: previousName
})
}
});
}
};
}

View File

@@ -1,47 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneModelInstance, normalizeModelInstanceName } from "../assets/model-instances";
export function createSetModelInstanceNameCommand(options) {
const normalizedName = normalizeModelInstanceName(options.name);
let previousName;
return {
id: createOpaqueId("command"),
label: normalizedName === undefined ? "Clear model instance name" : `Rename model instance to ${normalizedName}`,
execute(context) {
const currentDocument = context.getDocument();
const modelInstance = currentDocument.modelInstances[options.modelInstanceId];
if (modelInstance === undefined) {
throw new Error(`Model instance ${options.modelInstanceId} does not exist.`);
}
if (previousName === undefined) {
previousName = modelInstance.name;
}
context.setDocument({
...currentDocument,
modelInstances: {
...currentDocument.modelInstances,
[modelInstance.id]: cloneModelInstance({
...modelInstance,
name: normalizedName
})
}
});
},
undo(context) {
const currentDocument = context.getDocument();
const modelInstance = currentDocument.modelInstances[options.modelInstanceId];
if (modelInstance === undefined) {
throw new Error(`Model instance ${options.modelInstanceId} does not exist.`);
}
context.setDocument({
...currentDocument,
modelInstances: {
...currentDocument.modelInstances,
[modelInstance.id]: cloneModelInstance({
...modelInstance,
name: previousName
})
}
});
}
};
}

View File

@@ -1,12 +0,0 @@
import { createPlayerStartEntity } from "../entities/entity-instances";
import { createUpsertEntityCommand } from "./upsert-entity-command";
export function createSetPlayerStartCommand(options) {
return createUpsertEntityCommand({
entity: createPlayerStartEntity({
id: options.entityId,
position: options.position,
yawDegrees: options.yawDegrees
}),
label: options.entityId === undefined ? "Place player start" : "Move player start"
});
}

View File

@@ -1,29 +0,0 @@
import { createOpaqueId } from "../core/ids";
export function createSetSceneNameCommand(nextName) {
const normalizedName = nextName.trim() || "Untitled Scene";
let previousName = null;
return {
id: createOpaqueId("command"),
label: `Rename scene to ${normalizedName}`,
execute(context) {
const currentDocument = context.getDocument();
if (previousName === null) {
previousName = currentDocument.name;
}
context.setDocument({
...currentDocument,
name: normalizedName
});
},
undo(context) {
if (previousName === null) {
return;
}
const currentDocument = context.getDocument();
context.setDocument({
...currentDocument,
name: previousName
});
}
};
}

View File

@@ -1,30 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneWorldSettings } from "../document/world-settings";
export function createSetWorldSettingsCommand(options) {
const nextWorld = cloneWorldSettings(options.world);
let previousWorld = null;
return {
id: createOpaqueId("command"),
label: options.label,
execute(context) {
const currentDocument = context.getDocument();
if (previousWorld === null) {
previousWorld = cloneWorldSettings(currentDocument.world);
}
context.setDocument({
...currentDocument,
world: cloneWorldSettings(nextWorld)
});
},
undo(context) {
if (previousWorld === null) {
return;
}
const currentDocument = context.getDocument();
context.setDocument({
...currentDocument,
world: cloneWorldSettings(previousWorld)
});
}
};
}

View File

@@ -1,70 +0,0 @@
import { cloneEditorSelection } from "../core/selection";
import { createOpaqueId } from "../core/ids";
import { cloneEntityInstance, getEntityKindLabel } from "../entities/entity-instances";
function setSingleEntitySelection(entityId) {
return {
kind: "entities",
ids: [entityId]
};
}
function createDefaultEntityCommandLabel(entity, isNewEntity) {
const action = isNewEntity ? "Place" : "Update";
return `${action} ${getEntityKindLabel(entity.kind).toLowerCase()}`;
}
export function createUpsertEntityCommand(options) {
const nextEntity = cloneEntityInstance(options.entity);
let previousEntity = null;
let previousSelection = null;
let previousToolMode = null;
return {
id: createOpaqueId("command"),
label: options.label ?? createDefaultEntityCommandLabel(nextEntity, true),
execute(context) {
const currentDocument = context.getDocument();
const currentEntity = currentDocument.entities[nextEntity.id];
if (currentEntity !== undefined && currentEntity.kind !== nextEntity.kind) {
throw new Error(`Entity ${nextEntity.id} is a ${currentEntity.kind}, not a ${nextEntity.kind}.`);
}
if (previousSelection === null) {
previousSelection = cloneEditorSelection(context.getSelection());
}
if (previousToolMode === null) {
previousToolMode = context.getToolMode();
}
if (previousEntity === null && currentEntity !== undefined) {
previousEntity = cloneEntityInstance(currentEntity);
}
context.setDocument({
...currentDocument,
entities: {
...currentDocument.entities,
[nextEntity.id]: cloneEntityInstance(nextEntity)
}
});
context.setSelection(setSingleEntitySelection(nextEntity.id));
context.setToolMode("select");
},
undo(context) {
const currentDocument = context.getDocument();
const nextEntities = {
...currentDocument.entities
};
if (previousEntity === null) {
delete nextEntities[nextEntity.id];
}
else {
nextEntities[nextEntity.id] = cloneEntityInstance(previousEntity);
}
context.setDocument({
...currentDocument,
entities: nextEntities
});
if (previousSelection !== null) {
context.setSelection(previousSelection);
}
if (previousToolMode !== null) {
context.setToolMode(previousToolMode);
}
}
};
}

View File

@@ -1,40 +0,0 @@
import { cloneInteractionLink } from "../interactions/interaction-links";
import { createOpaqueId } from "../core/ids";
export function createUpsertInteractionLinkCommand(options) {
const nextLink = cloneInteractionLink(options.link);
let previousLink = null;
return {
id: createOpaqueId("command"),
label: options.label ?? "Update interaction link",
execute(context) {
const currentDocument = context.getDocument();
const currentLink = currentDocument.interactionLinks[nextLink.id];
if (previousLink === null && currentLink !== undefined) {
previousLink = cloneInteractionLink(currentLink);
}
context.setDocument({
...currentDocument,
interactionLinks: {
...currentDocument.interactionLinks,
[nextLink.id]: cloneInteractionLink(nextLink)
}
});
},
undo(context) {
const currentDocument = context.getDocument();
const nextInteractionLinks = {
...currentDocument.interactionLinks
};
if (previousLink === null) {
delete nextInteractionLinks[nextLink.id];
}
else {
nextInteractionLinks[nextLink.id] = cloneInteractionLink(previousLink);
}
context.setDocument({
...currentDocument,
interactionLinks: nextInteractionLinks
});
}
};
}

View File

@@ -1,75 +0,0 @@
import { createOpaqueId } from "../core/ids";
import { cloneEditorSelection } from "../core/selection";
import { cloneModelInstance, getModelInstanceKindLabel } from "../assets/model-instances";
import { getProjectAssetKindLabel } from "../assets/project-assets";
function setSingleModelInstanceSelection(modelInstanceId) {
return {
kind: "modelInstances",
ids: [modelInstanceId]
};
}
function createDefaultModelInstanceCommandLabel(isNewModelInstance) {
const action = isNewModelInstance ? "Place" : "Update";
return `${action} ${getModelInstanceKindLabel().toLowerCase()}`;
}
export function createUpsertModelInstanceCommand(options) {
const nextModelInstance = cloneModelInstance(options.modelInstance);
let previousModelInstance = null;
let previousSelection = null;
let previousToolMode = null;
return {
id: createOpaqueId("command"),
label: options.label ?? createDefaultModelInstanceCommandLabel(true),
execute(context) {
const currentDocument = context.getDocument();
const currentAsset = currentDocument.assets[nextModelInstance.assetId];
if (currentAsset === undefined) {
throw new Error(`Model instance ${nextModelInstance.id} cannot reference missing asset ${nextModelInstance.assetId}.`);
}
if (currentAsset.kind !== "model") {
throw new Error(`Model instance ${nextModelInstance.id} must reference a model asset, not ${getProjectAssetKindLabel(currentAsset.kind).toLowerCase()}.`);
}
const currentModelInstance = currentDocument.modelInstances[nextModelInstance.id];
if (previousSelection === null) {
previousSelection = cloneEditorSelection(context.getSelection());
}
if (previousToolMode === null) {
previousToolMode = context.getToolMode();
}
if (previousModelInstance === null && currentModelInstance !== undefined) {
previousModelInstance = cloneModelInstance(currentModelInstance);
}
context.setDocument({
...currentDocument,
modelInstances: {
...currentDocument.modelInstances,
[nextModelInstance.id]: cloneModelInstance(nextModelInstance)
}
});
context.setSelection(setSingleModelInstanceSelection(nextModelInstance.id));
context.setToolMode("select");
},
undo(context) {
const currentDocument = context.getDocument();
const nextModelInstances = {
...currentDocument.modelInstances
};
if (previousModelInstance === null) {
delete nextModelInstances[nextModelInstance.id];
}
else {
nextModelInstances[nextModelInstance.id] = cloneModelInstance(previousModelInstance);
}
context.setDocument({
...currentDocument,
modelInstances: nextModelInstances
});
if (previousSelection !== null) {
context.setSelection(previousSelection);
}
if (previousToolMode !== null) {
context.setToolMode(previousToolMode);
}
}
};
}