Compare commits

...
Author SHA1 Message Date
Tanish Manku ddedaeb47e also bind Cmd+Y to redo on macOS 2026-04-22 08:23:48 -07:00
Tanish Manku 525fdb7f80 fix 3D editor undo/redo stack indexing
- check_scene_for_updates was resetting undoIndex to 0 on every mouseUp,
  even when the scene was unchanged; redo became a silent no-op after
  any innocuous click. Only reset when a command is actually pushed.
- undo past the bottom of the stack re-undid the oldest command forever
  due to the clamp-then-index pattern; early-return instead.
- Pushing a new command after undo left stale redo-future entries on
  the stack; added dropRedoHistory() called before each push so new
  work replaces the branch the user had undone past.
2026-04-22 08:13:45 -07:00
Tanish Manku a71b0a8e7a fix 3D editor undo/redo keybinds and hotkey signal bugs
- Ctrl+Z/Cmd+Z was calling redo() instead of undo(); fixed and added
  missing Ctrl+Shift+Z/Cmd+Shift+Z and Ctrl+Y redo bindings
- Wrap all async keybind handlers in try/finally so isProcessing is
  always cleared even if undo/redo/copy/paste throws
- Fix === typo in disableHotkeyInput (comparison instead of assignment),
  and use full value replacement so the Preact signal update is reactive
2026-03-30 08:11:25 -07:00
3 changed files with 56 additions and 15 deletions
@@ -375,25 +375,53 @@ export class MouseControls {
if ((event.ctrlKey || event.metaKey) && !this.isProcessing) {
const keyLower = event.key.toLowerCase();
if (keyLower === "z") {
// redo
if (keyLower === "z" && !event.shiftKey) {
// undo
event.preventDefault();
this.isProcessing = true;
await this.sceneManager?.redo();
this.isProcessing = false;
try {
await this.sceneManager?.undo();
} finally {
this.isProcessing = false;
}
} else if (keyLower === "z" && event.shiftKey) {
// redo (Ctrl+Shift+Z / Cmd+Shift+Z)
event.preventDefault();
this.isProcessing = true;
try {
await this.sceneManager?.redo();
} finally {
this.isProcessing = false;
}
} else if (keyLower === "y") {
// redo (Ctrl+Y on Windows/Linux, Cmd+Y on macOS)
event.preventDefault();
this.isProcessing = true;
try {
await this.sceneManager?.redo();
} finally {
this.isProcessing = false;
}
} else if (keyLower === "c") {
// Copy
event.preventDefault();
event.stopPropagation();
this.isProcessing = true;
await this.sceneManager?.copy();
this.isProcessing = false;
try {
await this.sceneManager?.copy();
} finally {
this.isProcessing = false;
}
} else if (keyLower === "v") {
// Paste
event.preventDefault();
event.stopPropagation();
this.isProcessing = true;
await this.sceneManager?.paste();
this.isProcessing = false;
try {
await this.sceneManager?.paste();
} finally {
this.isProcessing = false;
}
} else if (event.key === "0") {
// Stats Menu
this.enable_stats();
@@ -118,10 +118,10 @@ export class SceneManager implements SceneManagerAPI {
}
public async undo() {
this.undoIndex += 1;
if (this.undoIndex >= this.undoStack.length) {
this.undoIndex = this.undoStack.length;
return;
}
this.undoIndex += 1;
const undoCommand = this.undoStack.at(
this.undoStack.length - this.undoIndex,
);
@@ -131,18 +131,30 @@ export class SceneManager implements SceneManagerAPI {
}
public async redo() {
if (this.undoIndex <= 0) {
return;
}
const undoCommand = this.undoStack.at(
this.undoStack.length - this.undoIndex,
);
await undoCommand?.redo();
this.undoIndex -= 1;
if (this.undoIndex <= -1) {
this.undoIndex = 0;
}
this.updateOutliner(); // In the future we will address this because of its relational issues with editor and the current class.
this.lastSceneState = this.getSceneState();
}
// Drop any redo-future before pushing a fresh command so the new action
// replaces the branch of history the user had undone past.
private dropRedoHistory() {
if (this.undoIndex > 0) {
this.undoStack = this.undoStack.slice(
0,
this.undoStack.length - this.undoIndex,
);
this.undoIndex = 0;
}
}
public async create(
media_token: string,
name: string,
@@ -467,6 +479,7 @@ export class SceneManager implements SceneManagerAPI {
}
public async add_creation_undostack(object: THREE.Object3D) {
this.dropRedoHistory();
this.undoStack.push(
new CreationSceneItem(this, {
object_uuid: object.uuid,
@@ -488,6 +501,7 @@ export class SceneManager implements SceneManagerAPI {
const is_transform = this.is_transformation(sceneState);
const is_userdata = this.is_userdata(sceneState);
this.dropRedoHistory();
this.undoStack = this.undoStack.slice(-16);
if (is_deleted != "") {
@@ -558,7 +572,6 @@ export class SceneManager implements SceneManagerAPI {
}
}
}
this.undoIndex = 0;
this.lastSceneState = sceneState;
}
}
@@ -23,7 +23,7 @@ export const isHotkeyDisabled = ()=>{
export const disableHotkeyInput = (level: number)=>{
if(hotkeysStatus.value.disabled === true){
if (level > hotkeysStatus.value.disabledBy){
hotkeysStatus.value.disabledBy === level;
hotkeysStatus.value = { ...hotkeysStatus.value, disabledBy: level };
}
}else{
hotkeysStatus.value = {