From b4b857d9d2197f083e7e0acae0588be1216da5de Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Sun, 27 Apr 2025 23:53:44 -0700 Subject: [PATCH 01/22] basic combining shit all working yippie --- package.json | 1 + scripts/classes/editor/rendering.lua | 85 ++++++++--- scripts/classes/editor/state.lua | 97 ++---------- scripts/classes/editor/state_operations.lua | 155 ++++++++++++++++++++ scripts/constants.lua | 2 + 5 files changed, 231 insertions(+), 109 deletions(-) create mode 100644 scripts/classes/editor/state_operations.lua diff --git a/package.json b/package.json index a680086..2b8aa47 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ { "path": "./scripts/classes/preferences.lua" }, { "path": "./scripts/classes/editor/_editor.lua" }, { "path": "./scripts/classes/editor/rendering.lua" }, + { "path": "./scripts/classes/editor/state_operations.lua" }, { "path": "./scripts/classes/editor/state.lua" }, { "path": "./scripts/classes/statesprite.lua" }, { "path": "./scripts/classes/widget.lua" }, diff --git a/scripts/classes/editor/rendering.lua b/scripts/classes/editor/rendering.lua index 17676fc..dc16ec4 100644 --- a/scripts/classes/editor/rendering.lua +++ b/scripts/classes/editor/rendering.lua @@ -44,31 +44,32 @@ function Editor:onpaint(ctx) local hovers = {} --[[ @as (string)[] ]] for _, widget in ipairs(self.widgets) do - local state = COMMON_STATE.normal + -- Styling + local stateStyle = COMMON_STATE.normal if widget == self.focused_widget then - state = COMMON_STATE.focused or state + stateStyle = COMMON_STATE.focused or stateStyle end - local is_mouse_over = not self.context_widget and widget.bounds:contains(self.mouse.position) - if is_mouse_over then - state = COMMON_STATE.hot or state - + stateStyle = COMMON_STATE.hot or stateStyle if self.mouse.leftClick then - state = COMMON_STATE.selected or state + stateStyle = COMMON_STATE.selected or stateStyle end end + -- Override style if widget's state is selected by Control-click + if widget.type == "IconWidget" and widget.state and self.selected_states and table.index_of(self.selected_states, widget.state) > 0 then + stateStyle = COMMON_STATE.selected or stateStyle + end if widget.type == "IconWidget" then - local widget = widget --[[ @as IconWidget ]] - - ctx:drawThemeRect(state.part, widget.bounds) + ctx:drawThemeRect(stateStyle.part, widget.bounds) ctx:drawImage( widget.icon, widget.icon.bounds, Rectangle(widget.bounds.x + (widget.bounds.width - self.dmi.width) / 2, - widget.bounds.y + (widget.bounds.height - self.dmi.height) / 2, widget.icon.bounds.width, + widget.bounds.y + (widget.bounds.height - self.dmi.height) / 2, + widget.icon.bounds.width, widget.icon.bounds.height) ) elseif widget.type == "TextWidget" then @@ -77,7 +78,8 @@ function Editor:onpaint(ctx) local text = self.fit_text(widget.text, ctx, widget.bounds.width) local size = ctx:measureText(text) - ctx.color = widget.text_color or app.theme.color[state.color] + -- Fix: Use fallback text color instead of undefined 'state' + ctx.color = widget.text_color or app.theme.color.text ctx:fillText( text, widget.bounds.x + (widget.bounds.width - size.width) / 2, @@ -90,7 +92,7 @@ function Editor:onpaint(ctx) elseif widget.type == "ThemeWidget" then local widget = widget --[[ @as ThemeWidget ]] - ctx:drawThemeRect(state.part, widget.bounds) + ctx:drawThemeRect(stateStyle.part, widget.bounds) if widget.partId then ctx:drawThemeImage(widget.partId, @@ -233,13 +235,16 @@ function Editor:repaint_states() local icon = Image(icon.width, icon.height) icon.bytes = bytes - table.insert(self.widgets, IconWidget.new( + -- Create IconWidget and store its state for selection logic + local iconWidget = IconWidget.new( self, bounds, icon, function() self:open_state(state) end, function(ev) self:state_context(state, ev) end - )) + ) + iconWidget.state = state + table.insert(self.widgets, iconWidget) table.insert(self.widgets, TextWidget.new( self, @@ -249,9 +254,9 @@ function Editor:repaint_states() bounds.width, TEXT_HEIGHT ), - name, + #state.name > 0 and state.name or "no name", text_color, - name, + state.name, function() self:state_properties(state) end, function(ev) self:state_context(state, ev) end )) @@ -299,6 +304,17 @@ end --- Handles the mouse down event in the editor and triggers a repaint. --- @param ev MouseEvent The mouse event object. function Editor:onmousedown(ev) + -- Add Control-click selection + if ev.ctrlKey then + for _, widget in ipairs(self.widgets) do + if widget.type == "IconWidget" and widget.bounds:contains(Point(ev.x, ev.y)) then + self:toggle_state_selection(widget) + self:repaint() + return + end + end + end + if ev.button == MouseButton.LEFT then self.mouse.leftClick = true self.focused_widget = nil @@ -358,15 +374,26 @@ function Editor:onmouseup(ev) end if not triggered then if ev.button == MouseButton.RIGHT then - self.context_widget = ContextWidget.new( - Rectangle(ev.x, ev.y, 0, 0), - { - { text = "Paste", onclick = function() self:clipboard_paste_state() end }, - } - ) + -- If multiple states are selected, show combine option. + if self.selected_states and #self.selected_states > 1 then + self.context_widget = ContextWidget.new( + Rectangle(ev.x, ev.y, 0, 0), + { + { text = "Combine", onclick = function() self:combine_selected_states() end } + } + ) + else + self.context_widget = ContextWidget.new( + Rectangle(ev.x, ev.y, 0, 0), + { + { text = "Paste", onclick = function() self:clipboard_paste_state() end }, + } + ) + end end end end + -- Add logic to finalize drag-and-drop or click actions if ev.button == MouseButton.LEFT then if self.dragging and self.drag_widget and self.drop_index then -- Find source state index using widget index and scroll offset @@ -561,3 +588,15 @@ function Editor.fit_text(text, ctx, maxWidth) end return text end + +-- Helper method to toggle state selection +function Editor:toggle_state_selection(widget) + self.selected_states = self.selected_states or {} + local state = widget.state + local idx = table.index_of(self.selected_states, state) + if idx and idx > 0 then + table.remove(self.selected_states, idx) + else + table.insert(self.selected_states, state) + end +end diff --git a/scripts/classes/editor/state.lua b/scripts/classes/editor/state.lua index b6636a8..3746b1c 100644 --- a/scripts/classes/editor/state.lua +++ b/scripts/classes/editor/state.lua @@ -127,15 +127,19 @@ end --- @param state State The state to be opened. --- @param ev MouseEvent The mouse event object. function Editor:state_context(state, ev) + local buttons = { + { text = "Properties", onclick = function() self:state_properties(state) end }, + { text = "Open", onclick = function() self:open_state(state) end }, + { text = "Copy", onclick = function() self:clipboard_copy_state(state) end }, + { text = "Remove", onclick = function() self:remove_state(state) end }, + { text = "Split", onclick = function() self:split_state(state) end }, + } + if self.selected_states and #self.selected_states > 1 then + table.insert(buttons, { text = "Combine", onclick = function() self:combine_selected_states() end }) + end self.context_widget = ContextWidget.new( Rectangle(ev.x, ev.y, 0, 0), - { - { text = "Properties", onclick = function() self:state_properties(state) end }, - { text = "Open", onclick = function() self:open_state(state) end }, - { text = "Copy", onclick = function() self:clipboard_copy_state(state) end }, - { text = "Remove", onclick = function() self:remove_state(state) end }, - { text = "Split", onclick = function() self:split_state(state) end }, - } + buttons ) self:repaint() end @@ -1019,82 +1023,3 @@ function Editor:reload_open_states() self:open_state(state) end end - ---- Splits a multi-directional state into individual states, one for each direction. ---- @param state State The state to be split. -function Editor:split_state(state) - if not self.dmi then return end - if state.dirs == 1 then - app.alert { title = "Warning", text = "Cannot split a state with only one direction" } - return - end - - -- Check if state is open and modified - for _, state_sprite in ipairs(self.open_sprites) do - if state_sprite.state == state then - if state_sprite.sprite.isModified then - app.alert { title = self.title, text = "Save the open sprite first" } - return - end - break - end - end - - local original_name = state.name - local direction_names = { - [4] = { "S", "N", "E", "W" }, - [8] = { "S", "N", "E", "W", "SE", "SW", "NE", "NW" } - } - - -- Create a new state for each direction - for i = 1, state.dirs do - local new_state, error = libdmi.new_state(self.dmi.width, self.dmi.height, self.dmi.temp) - if error then - app.alert { title = "Error", text = { "Failed to create new state", error } } - return - end - - if not new_state then - app.alert { title = "Error", text = "Failed to create new state" } - return - end - - -- Set the new state properties - new_state.name = original_name .. " - " .. direction_names[state.dirs][i] - new_state.dirs = 1 - new_state.loop = state.loop - new_state.rewind = state.rewind - new_state.movement = state.movement - new_state.delays = table.clone(state.delays) - - -- Copy the image data for this direction - local frames_per_dir = state.frame_count - local start_frame = (i - 1) * frames_per_dir - - for frame = 1, frames_per_dir do - local src_path = app.fs.joinPath(self.dmi.temp, state.frame_key .. "." .. tostring(start_frame + frame - 1) .. ".bytes") - local dst_path = app.fs.joinPath(self.dmi.temp, new_state.frame_key .. "." .. tostring(frame - 1) .. ".bytes") - - -- Copy the image file - local src_file = io.open(src_path, "rb") - if src_file then - local content = src_file:read("*all") - src_file:close() - - local dst_file = io.open(dst_path, "wb") - if dst_file then - dst_file:write(content) - dst_file:close() - end - end - end - - table.insert(self.dmi.states, new_state) - self.image_cache:load_state(self.dmi, new_state) - end - - -- Mark as modified and remove the original state - self.modified = true - self:remove_state(state) - self:repaint_states() -end diff --git a/scripts/classes/editor/state_operations.lua b/scripts/classes/editor/state_operations.lua new file mode 100644 index 0000000..79b23cf --- /dev/null +++ b/scripts/classes/editor/state_operations.lua @@ -0,0 +1,155 @@ +-- This file defines split_state and combine_selected_states for the Editor. +-- Assumes global Editor is already defined. + +--- Splits a multi-directional state into individual states, one for each direction. +--- @param state State The state to be split. +function Editor:split_state(state) + if not self.dmi then return end + if state.dirs == 1 then + app.alert { title = "Warning", text = "Cannot split a state with only one direction" } + return + end + + -- Check if state is open and modified + for _, state_sprite in ipairs(self.open_sprites) do + if state_sprite.state == state then + if state_sprite.sprite.isModified then + app.alert { title = self.title, text = "Save the open sprite first" } + return + end + break + end + end + + local original_name = state.name + local direction_names = { + [4] = { "S", "N", "E", "W" }, + [8] = { "S", "N", "E", "W", "SE", "SW", "NE", "NW" } + } + + -- Create a new state for each direction + for i = 1, state.dirs do + local new_state, error = libdmi.new_state(self.dmi.width, self.dmi.height, self.dmi.temp) + if error then + app.alert { title = "Error", text = { "Failed to create new state", error } } + return + end + + if not new_state then + app.alert { title = "Error", text = "Failed to create new state" } + return + end + + -- Set the new state properties + new_state.name = original_name .. " - " .. direction_names[state.dirs][i] + new_state.dirs = 1 + new_state.loop = state.loop + new_state.rewind = state.rewind + new_state.movement = state.movement + new_state.delays = table.clone(state.delays) + + -- Copy the image data for this direction + local frames_per_dir = state.frame_count + local start_frame = (i - 1) * frames_per_dir + + for frame = 1, frames_per_dir do + local src_path = app.fs.joinPath(self.dmi.temp, state.frame_key .. "." .. tostring(start_frame + frame - 1) .. ".bytes") + local dst_path = app.fs.joinPath(self.dmi.temp, new_state.frame_key .. "." .. tostring(frame - 1) .. ".bytes") + + -- Copy the image file + local src_file = io.open(src_path, "rb") + if src_file then + local content = src_file:read("*all") + src_file:close() + + local dst_file = io.open(dst_path, "wb") + if dst_file then + dst_file:write(content) + dst_file:close() + end + end + end + + table.insert(self.dmi.states, new_state) + self.image_cache:load_state(self.dmi, new_state) + end + + -- Mark as modified and remove the original state + self.modified = true + self:remove_state(state) + self:repaint_states() +end + +--- Combines multiple selected states into one state. +function Editor:combine_selected_states() + if not self.dmi or not self.selected_states or #self.selected_states < 2 then + app.alert { title = self.title, text = "Select at least two states to combine." } + return + end + local combined_state, error = libdmi.new_state(self.dmi.width, self.dmi.height, self.dmi.temp) + if error or (not combined_state) then + app.alert { title = "Error", text = { "Failed to create combined state", error } } + return + end + + -- Normalize a string by removing dashes, underscores, and numbers. + local function normalize(s) + return s:gsub("[-_%d]", "") + end + + -- Compute longest common prefix between two strings. + local function commonPrefix(a, b) + local len = math.min(#a, #b) + local prefix = "" + for i = 1, len do + if a:sub(i, i) == b:sub(i, i) then + prefix = prefix .. a:sub(i, i) + else + break + end + end + return prefix + end + + local commonBase = normalize(self.selected_states[1].name) + for i = 2, #self.selected_states do + commonBase = commonPrefix(commonBase, normalize(self.selected_states[i].name)) + if commonBase == "" then break end + end + if commonBase and #commonBase > 0 then + combined_state.name = commonBase + else + combined_state.name = "Combined" + end + + -- Reorder selected states in the order they appear in the dmi. + local sortedStates = {} + for _, state in ipairs(self.dmi.states) do + for _, sel in ipairs(self.selected_states) do + if state == sel then + table.insert(sortedStates, state) + break + end + end + end + + combined_state.frame_count = #sortedStates + combined_state.delays = {} + -- For each state in sorted order, copy its preview image as a new frame. + for i, state in ipairs(sortedStates) do + local preview = self.image_cache:get(state.frame_key) + if not preview then + app.alert { title = "Error", text = "Preview image missing for state: " .. (state.name or "unknown") } + return + end + save_image_bytes(preview, app.fs.joinPath(self.dmi.temp, combined_state.frame_key .. "." .. (i - 1) .. ".bytes")) + combined_state.delays[i] = 100 -- set default delay + end + table.insert(self.dmi.states, combined_state) + self.image_cache:load_state(self.dmi, combined_state) + self.modified = true + self:repaint_states() + -- Clear selection after combining. + self.selected_states = {} + app.alert { title = self.title, text = "States have been combined." } +end diff --git a/scripts/constants.lua b/scripts/constants.lua index e28bead..c7c5fd3 100644 --- a/scripts/constants.lua +++ b/scripts/constants.lua @@ -10,4 +10,6 @@ TEMP_DIR = app.fs.joinPath(app.fs.tempPath, TEMP_NAME) COMMON_STATE = { normal = { part = "sunken_normal", color = "button_normal_text" }, hot = { part = "sunken_focused", color = "button_hot_text" }, + focused = { part = "sunken_mini_focused", color = "button_normal_text" }, + selected = { part = "sunken_mini_focused", color = "button_normal_text" }, } --[[@as WidgetState]] From 6cf1edc9f19d89929d0c0b21fd27556979ec4d5d Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Mon, 28 Apr 2025 00:17:38 -0700 Subject: [PATCH 02/22] modalize --- scripts/classes/editor/state_operations.lua | 112 ++++++++++++++------ scripts/constants.lua | 6 +- 2 files changed, 84 insertions(+), 34 deletions(-) diff --git a/scripts/classes/editor/state_operations.lua b/scripts/classes/editor/state_operations.lua index 79b23cf..7307cee 100644 --- a/scripts/classes/editor/state_operations.lua +++ b/scripts/classes/editor/state_operations.lua @@ -86,43 +86,70 @@ function Editor:combine_selected_states() app.alert { title = self.title, text = "Select at least two states to combine." } return end - local combined_state, error = libdmi.new_state(self.dmi.width, self.dmi.height, self.dmi.temp) - if error or (not combined_state) then - app.alert { title = "Error", text = { "Failed to create combined state", error } } - return - end + local dialog = Dialog { title = "Combine States" } + dialog:entry { + id = "combined_name", + label = "Combined Name:", + text = self.getCombinedDefaultName(self), + } + dialog:combobox { + id = "combine_type", + label = "Combine Method:", + option = COMBINE_TYPES.onedir, + options = { COMBINE_TYPES.onedir }, + } + dialog:button { + text = "&OK", + focus = true, + onclick = function() + local combinedName = dialog.data.combined_name or "Combined" + local combineType = dialog.data.combine_type or COMBINE_TYPES.onedir + dialog:close() + self:performCombineStates(combinedName, combineType) + end + } + dialog:button { + text = "&Cancel", + onclick = function() + dialog:close() + end + } + dialog:show() +end - -- Normalize a string by removing dashes, underscores, and numbers. - local function normalize(s) +-- Add a new function to compute the default combined name +function Editor:getCombinedDefaultName() + local function normalize(s) -- Remove _ and - and numbers return s:gsub("[-_%d]", "") end - - -- Compute longest common prefix between two strings. local function commonPrefix(a, b) - local len = math.min(#a, #b) - local prefix = "" - for i = 1, len do - if a:sub(i, i) == b:sub(i, i) then - prefix = prefix .. a:sub(i, i) - else - break - end + local i = 1 + while i <= #a and i <= #b and a:sub(i, i) == b:sub(i, i) do + i = i + 1 end - return prefix + return a:sub(1, i - 1) end - local commonBase = normalize(self.selected_states[1].name) for i = 2, #self.selected_states do commonBase = commonPrefix(commonBase, normalize(self.selected_states[i].name)) if commonBase == "" then break end end - if commonBase and #commonBase > 0 then - combined_state.name = commonBase + if commonBase and #commonBase > 2 then -- <3 char names are not useful + return commonBase else - combined_state.name = "Combined" + return "Combined" + end +end + +-- Modified performCombineStates function to use combine1direction +function Editor:performCombineStates(combinedName, combineType) + local combined_state, error = libdmi.new_state(self.dmi.width, self.dmi.height, self.dmi.temp) + if error or not combined_state then + app.alert { title = "Error", text = { "Failed to create combined state", error } } + return end - -- Reorder selected states in the order they appear in the dmi. + -- Reorder selected states based on DMI order local sortedStates = {} for _, state in ipairs(self.dmi.states) do for _, sel in ipairs(self.selected_states) do @@ -133,23 +160,42 @@ function Editor:combine_selected_states() end end - combined_state.frame_count = #sortedStates - combined_state.delays = {} - -- For each state in sorted order, copy its preview image as a new frame. - for i, state in ipairs(sortedStates) do - local preview = self.image_cache:get(state.frame_key) - if not preview then - app.alert { title = "Error", text = "Preview image missing for state: " .. (state.name or "unknown") } + combined_state.name = combinedName + if combineType == COMBINE_TYPES.onedir then + if not self:combine1direction(combined_state, sortedStates) then return end - save_image_bytes(preview, app.fs.joinPath(self.dmi.temp, combined_state.frame_key .. "." .. (i - 1) .. ".bytes")) - combined_state.delays[i] = 100 -- set default delay + else + -- Future expansion placeholder for other combine types. + combined_state.frame_count = #sortedStates + combined_state.delays = {} + for i, state in ipairs(sortedStates) do + local preview = self.image_cache:get(state.frame_key) + if not preview then + app.alert { title = "Error", text = "Preview image missing for state: " .. (state.name or "unknown") } + return + end + save_image_bytes(preview, app.fs.joinPath(self.dmi.temp, combined_state.frame_key .. "." .. (i - 1) .. ".bytes")) + combined_state.delays[i] = 100 -- set default delay + end end table.insert(self.dmi.states, combined_state) self.image_cache:load_state(self.dmi, combined_state) self.modified = true self:repaint_states() - -- Clear selection after combining. self.selected_states = {} app.alert { title = self.title, text = "States have been combined." } end + +function Editor:combine1direction(combined_state, sortedStates) + combined_state.frame_count = #sortedStates + for i, state in ipairs(sortedStates) do + local preview = self.image_cache:get(state.frame_key) + if not preview then + app.alert { title = "Error", text = "Preview image missing for state: " .. (state.name or "unknown") } + return false + end + save_image_bytes(preview, app.fs.joinPath(self.dmi.temp, combined_state.frame_key .. "." .. (i - 1) .. ".bytes")) + end + return true +end diff --git a/scripts/constants.lua b/scripts/constants.lua index c7c5fd3..b78276c 100644 --- a/scripts/constants.lua +++ b/scripts/constants.lua @@ -10,6 +10,10 @@ TEMP_DIR = app.fs.joinPath(app.fs.tempPath, TEMP_NAME) COMMON_STATE = { normal = { part = "sunken_normal", color = "button_normal_text" }, hot = { part = "sunken_focused", color = "button_hot_text" }, - focused = { part = "sunken_mini_focused", color = "button_normal_text" }, + focused = { part = "sunken_focused", color = "button_normal_text" }, selected = { part = "sunken_mini_focused", color = "button_normal_text" }, } --[[@as WidgetState]] + +COMBINE_TYPES = { + onedir = "1 direction", +} From 113731be0220ff780d86ace6c35a3c7831997133 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Mon, 28 Apr 2025 00:30:48 -0700 Subject: [PATCH 03/22] better autoflatten --- scripts/classes/statesprite.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/classes/statesprite.lua b/scripts/classes/statesprite.lua index f6c0c22..b57e9ec 100644 --- a/scripts/classes/statesprite.lua +++ b/scripts/classes/statesprite.lua @@ -94,9 +94,9 @@ function StateSprite:save() return false end - -- Store original layers if we need to auto-flatten + -- Store original layers if we need to auto-flatten (only if multiple layers exist) local original_layers = nil - if Preferences.getAutoFlatten() then + if Preferences.getAutoFlatten() and #self.sprite.layers > 1 then original_layers = {} for _, layer in ipairs(self.sprite.layers) do table.insert(original_layers, layer) @@ -169,7 +169,7 @@ function StateSprite:save() self.editor.modified = true -- Restore original layers if we flattened by undoing the transaction from above - if Preferences.getAutoFlatten() then + if original_layers then app.command.Undo() end From 086f5fe550939d5021283c2cbaae9119e13c69c8 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Mon, 28 Apr 2025 00:30:57 -0700 Subject: [PATCH 04/22] always allow pasting --- scripts/classes/editor/rendering.lua | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/scripts/classes/editor/rendering.lua b/scripts/classes/editor/rendering.lua index dc16ec4..cfa916d 100644 --- a/scripts/classes/editor/rendering.lua +++ b/scripts/classes/editor/rendering.lua @@ -375,21 +375,16 @@ function Editor:onmouseup(ev) if not triggered then if ev.button == MouseButton.RIGHT then -- If multiple states are selected, show combine option. + local buttonsToAdd = { + { text = "Paste", onclick = function() self:clipboard_paste_state() end }, + } if self.selected_states and #self.selected_states > 1 then - self.context_widget = ContextWidget.new( - Rectangle(ev.x, ev.y, 0, 0), - { - { text = "Combine", onclick = function() self:combine_selected_states() end } - } - ) - else - self.context_widget = ContextWidget.new( - Rectangle(ev.x, ev.y, 0, 0), - { - { text = "Paste", onclick = function() self:clipboard_paste_state() end }, - } - ) + table.insert(buttonsToAdd, { text = "Combine", onclick = function() self:combine_selected_states() end }) end + self.context_widget = ContextWidget.new( + Rectangle(ev.x, ev.y, 0, 0), + buttonsToAdd + ) end end end From f50f6f9aa8fa5ee30dc3e7274060e5164f1c81bc Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Mon, 28 Apr 2025 00:33:26 -0700 Subject: [PATCH 05/22] deselection --- scripts/classes/editor/rendering.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/classes/editor/rendering.lua b/scripts/classes/editor/rendering.lua index cfa916d..62f8d55 100644 --- a/scripts/classes/editor/rendering.lua +++ b/scripts/classes/editor/rendering.lua @@ -321,14 +321,19 @@ function Editor:onmousedown(ev) -- Only start drag if we're not clicking on a context menu if not self.context_widget then - -- Start potential drag + local clickedState = false for _, widget in ipairs(self.widgets) do if widget.type == "IconWidget" and widget.bounds:contains(Point(ev.x, ev.y)) then self.drag_widget = widget self.drag_start_time = os.clock() + clickedState = true break end end + -- If click did not hit any state widget, clear selected states. + if not clickedState then + self.selected_states = {} + end end elseif ev.button == MouseButton.RIGHT then self.mouse.rightClick = true From 8bcaaba7bb9f74c7849274690ed2537a30794a11 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Mon, 28 Apr 2025 00:36:12 -0700 Subject: [PATCH 06/22] lol --- scripts/classes/editor/rendering.lua | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/scripts/classes/editor/rendering.lua b/scripts/classes/editor/rendering.lua index 62f8d55..c96e528 100644 --- a/scripts/classes/editor/rendering.lua +++ b/scripts/classes/editor/rendering.lua @@ -304,21 +304,21 @@ end --- Handles the mouse down event in the editor and triggers a repaint. --- @param ev MouseEvent The mouse event object. function Editor:onmousedown(ev) - -- Add Control-click selection - if ev.ctrlKey then - for _, widget in ipairs(self.widgets) do - if widget.type == "IconWidget" and widget.bounds:contains(Point(ev.x, ev.y)) then - self:toggle_state_selection(widget) - self:repaint() - return - end - end - end - if ev.button == MouseButton.LEFT then self.mouse.leftClick = true self.focused_widget = nil + -- Selection mode + if ev.ctrlKey then + for _, widget in ipairs(self.widgets) do + if widget.type == "IconWidget" and widget.bounds:contains(Point(ev.x, ev.y)) then + self:toggle_state_selection(widget) + self:repaint() + return + end + end + end + -- Only start drag if we're not clicking on a context menu if not self.context_widget then local clickedState = false From 12465e0781909810b4a40ff7370518ae3ae14838 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Mon, 28 Apr 2025 02:10:57 -0700 Subject: [PATCH 07/22] m(ass) selection --- scripts/classes/editor/rendering.lua | 135 ++++++++++++++++---- scripts/classes/editor/state.lua | 2 +- scripts/classes/editor/state_operations.lua | 23 ++-- 3 files changed, 124 insertions(+), 36 deletions(-) diff --git a/scripts/classes/editor/rendering.lua b/scripts/classes/editor/rendering.lua index c96e528..2f06679 100644 --- a/scripts/classes/editor/rendering.lua +++ b/scripts/classes/editor/rendering.lua @@ -57,8 +57,7 @@ function Editor:onpaint(ctx) stateStyle = COMMON_STATE.selected or stateStyle end end - -- Override style if widget's state is selected by Control-click - if widget.type == "IconWidget" and widget.state and self.selected_states and table.index_of(self.selected_states, widget.state) > 0 then + if widget.type == "IconWidget"and self.selected_widgets and table.index_of(self.selected_widgets, widget) > 0 then stateStyle = COMMON_STATE.selected or stateStyle end @@ -308,31 +307,47 @@ function Editor:onmousedown(ev) self.mouse.leftClick = true self.focused_widget = nil - -- Selection mode - if ev.ctrlKey then + -- Don't do anything fancy if we're clicking on a context menu + if not self.context_widget then + + local clickedWidget = nil for _, widget in ipairs(self.widgets) do if widget.type == "IconWidget" and widget.bounds:contains(Point(ev.x, ev.y)) then - self:toggle_state_selection(widget) + clickedWidget = widget + break + end + end + if clickedWidget then + -- Handle range selection with Shift + Ctrl + if ev.shiftKey and ev.ctrlKey then + local iconWidgets = {} + for _, widget in ipairs(self.widgets) do + if widget.type == "IconWidget" then + table.insert(iconWidgets, widget) + end + end + local clickedWidgetIdx = table.index_of(iconWidgets, clickedWidget) + local closestSelWidgetIdx = self:find_closest_selected_widget_index(clickedWidgetIdx) + if not closestSelWidgetIdx then return end + local low = math.min(clickedWidgetIdx, closestSelWidgetIdx) + local high = math.max(clickedWidgetIdx, closestSelWidgetIdx) + self:select_iconwidgets_range(low, high) self:repaint() return end - end - end - -- Only start drag if we're not clicking on a context menu - if not self.context_widget then - local clickedState = false - for _, widget in ipairs(self.widgets) do - if widget.type == "IconWidget" and widget.bounds:contains(Point(ev.x, ev.y)) then - self.drag_widget = widget - self.drag_start_time = os.clock() - clickedState = true - break + -- Selection mode with just Ctrl + if ev.ctrlKey then + self:toggle_state_selection(clickedWidget) + self:repaint() + return end - end - -- If click did not hit any state widget, clear selected states. - if not clickedState then - self.selected_states = {} + + -- Setup Dragging + self.drag_widget = clickedWidget + self.drag_start_time = os.clock() + else -- No clicked widget, handle deselection + self.selected_widgets = {} end end elseif ev.button == MouseButton.RIGHT then @@ -383,7 +398,7 @@ function Editor:onmouseup(ev) local buttonsToAdd = { { text = "Paste", onclick = function() self:clipboard_paste_state() end }, } - if self.selected_states and #self.selected_states > 1 then + if self.selected_widgets and #self.selected_widgets > 1 then table.insert(buttonsToAdd, { text = "Combine", onclick = function() self:combine_selected_states() end }) end self.context_widget = ContextWidget.new( @@ -589,14 +604,80 @@ function Editor.fit_text(text, ctx, maxWidth) return text end --- Helper method to toggle state selection +--- Helper method to toggle state selection +--- @param widget IconWidget The widget to toggle selection for. function Editor:toggle_state_selection(widget) - self.selected_states = self.selected_states or {} - local state = widget.state - local idx = table.index_of(self.selected_states, state) + self.selected_widgets = self.selected_widgets or {} + local idx = table.index_of(self.selected_widgets, widget) if idx and idx > 0 then - table.remove(self.selected_states, idx) + table.remove(self.selected_widgets, idx) else - table.insert(self.selected_states, state) + table.insert(self.selected_widgets, widget) + end +end + +--- Helper method to select a range of widgets +--- @param lowIndex number The lower index of the range. +--- @param highIndex number The higher index of the range. +function Editor:select_iconwidgets_range(lowIndex, highIndex) + local iconWidgets = {} + for _, widget in ipairs(self.widgets) do + if widget.type == "IconWidget" then + table.insert(iconWidgets, widget) + end + end + + -- Determine if all widgets in the range are already selected. + local allSelected = true + for i = lowIndex, highIndex do + local widget = iconWidgets[i] + if table.index_of(self.selected_widgets, widget) == 0 then + allSelected = false + break + end + end + + -- (De)select each widget in the range. + for i = lowIndex, highIndex do + local widget = iconWidgets[i] + if allSelected then + -- Could be better but I'm not sure what the logic would be for + -- determining which neighboring states to deselect. + local idx = table.index_of(self.selected_widgets, widget) + if idx and idx > 0 then + table.remove(self.selected_widgets, idx) + end + else + if table.index_of(self.selected_widgets, widget) == 0 then + table.insert(self.selected_widgets, widget) + end + end + end +end + +--- Finds the closest selected widget index to the clicked widget index. +--- @param clickedWidgetIndex number The index of the clicked widget. +--- @return number|nil closestIndex The index of the closest selected widget. +function Editor:find_closest_selected_widget_index(clickedWidgetIndex) + local closestIndex = nil + local closestDistance = math.huge + + local iconWidgets = {} + for _, widget in ipairs(self.widgets) do + if widget.type == "IconWidget" then + table.insert(iconWidgets, widget) + end end + + for i, widget in ipairs(iconWidgets) do + if widget.type == "IconWidget" and table.index_of(self.selected_widgets, widget) > 0 then + local distance = math.abs(i - clickedWidgetIndex) + if distance < closestDistance then + closestDistance = distance + closestIndex = i + end + end + end + + return closestIndex or nil end diff --git a/scripts/classes/editor/state.lua b/scripts/classes/editor/state.lua index 3746b1c..20c5a96 100644 --- a/scripts/classes/editor/state.lua +++ b/scripts/classes/editor/state.lua @@ -134,7 +134,7 @@ function Editor:state_context(state, ev) { text = "Remove", onclick = function() self:remove_state(state) end }, { text = "Split", onclick = function() self:split_state(state) end }, } - if self.selected_states and #self.selected_states > 1 then + if self.selected_widgets and #self.selected_widgets > 1 then table.insert(buttons, { text = "Combine", onclick = function() self:combine_selected_states() end }) end self.context_widget = ContextWidget.new( diff --git a/scripts/classes/editor/state_operations.lua b/scripts/classes/editor/state_operations.lua index 7307cee..c963fc8 100644 --- a/scripts/classes/editor/state_operations.lua +++ b/scripts/classes/editor/state_operations.lua @@ -82,7 +82,7 @@ end --- Combines multiple selected states into one state. function Editor:combine_selected_states() - if not self.dmi or not self.selected_states or #self.selected_states < 2 then + if not self.dmi or not self.selected_widgets or #self.selected_widgets < 2 then app.alert { title = self.title, text = "Select at least two states to combine." } return end @@ -129,9 +129,10 @@ function Editor:getCombinedDefaultName() end return a:sub(1, i - 1) end - local commonBase = normalize(self.selected_states[1].name) - for i = 2, #self.selected_states do - commonBase = commonPrefix(commonBase, normalize(self.selected_states[i].name)) + + local commonBase = normalize(self.selected_widgets[1].state.name) + for i = 2, #self.selected_widgets do + commonBase = commonPrefix(commonBase, normalize(self.selected_widgets[i].state.name)) if commonBase == "" then break end end if commonBase and #commonBase > 2 then -- <3 char names are not useful @@ -152,8 +153,8 @@ function Editor:performCombineStates(combinedName, combineType) -- Reorder selected states based on DMI order local sortedStates = {} for _, state in ipairs(self.dmi.states) do - for _, sel in ipairs(self.selected_states) do - if state == sel then + for _, selWidget in ipairs(self.selected_widgets) do + if state == selWidget.state then table.insert(sortedStates, state) break end @@ -182,9 +183,8 @@ function Editor:performCombineStates(combinedName, combineType) table.insert(self.dmi.states, combined_state) self.image_cache:load_state(self.dmi, combined_state) self.modified = true + self.selected_widgets = {} self:repaint_states() - self.selected_states = {} - app.alert { title = self.title, text = "States have been combined." } end function Editor:combine1direction(combined_state, sortedStates) @@ -199,3 +199,10 @@ function Editor:combine1direction(combined_state, sortedStates) end return true end + +function printtable(table, indent) + print(tostring(table) .. '\n') + for index, value in pairs(table) do + print(' ' .. tostring(index) .. ' : ' .. tostring(value) .. '\n') + end + end From deb81f35d26b4913295c2aa4fe9d7528ca310c02 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Mon, 28 Apr 2025 02:12:07 -0700 Subject: [PATCH 08/22] rm dbg --- scripts/classes/editor/state_operations.lua | 7 ------- 1 file changed, 7 deletions(-) diff --git a/scripts/classes/editor/state_operations.lua b/scripts/classes/editor/state_operations.lua index c963fc8..5ceaefc 100644 --- a/scripts/classes/editor/state_operations.lua +++ b/scripts/classes/editor/state_operations.lua @@ -199,10 +199,3 @@ function Editor:combine1direction(combined_state, sortedStates) end return true end - -function printtable(table, indent) - print(tostring(table) .. '\n') - for index, value in pairs(table) do - print(' ' .. tostring(index) .. ' : ' .. tostring(value) .. '\n') - end - end From ca96ba1a7ad6022aa3da7a1c226b2821c9cd6384 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Mon, 28 Apr 2025 02:14:38 -0700 Subject: [PATCH 09/22] rm old unused --- scripts/classes/editor/state_operations.lua | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/scripts/classes/editor/state_operations.lua b/scripts/classes/editor/state_operations.lua index 5ceaefc..99d74ae 100644 --- a/scripts/classes/editor/state_operations.lua +++ b/scripts/classes/editor/state_operations.lua @@ -168,17 +168,6 @@ function Editor:performCombineStates(combinedName, combineType) end else -- Future expansion placeholder for other combine types. - combined_state.frame_count = #sortedStates - combined_state.delays = {} - for i, state in ipairs(sortedStates) do - local preview = self.image_cache:get(state.frame_key) - if not preview then - app.alert { title = "Error", text = "Preview image missing for state: " .. (state.name or "unknown") } - return - end - save_image_bytes(preview, app.fs.joinPath(self.dmi.temp, combined_state.frame_key .. "." .. (i - 1) .. ".bytes")) - combined_state.delays[i] = 100 -- set default delay - end end table.insert(self.dmi.states, combined_state) self.image_cache:load_state(self.dmi, combined_state) From b85c7b54c67c33fcfd3928bc7f1898458ceff873 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Mon, 28 Apr 2025 02:16:45 -0700 Subject: [PATCH 10/22] clean --- scripts/classes/editor/state_operations.lua | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/classes/editor/state_operations.lua b/scripts/classes/editor/state_operations.lua index 99d74ae..2a7dc65 100644 --- a/scripts/classes/editor/state_operations.lua +++ b/scripts/classes/editor/state_operations.lua @@ -1,6 +1,3 @@ --- This file defines split_state and combine_selected_states for the Editor. --- Assumes global Editor is already defined. - --- Splits a multi-directional state into individual states, one for each direction. --- @param state State The state to be split. function Editor:split_state(state) @@ -117,7 +114,7 @@ function Editor:combine_selected_states() dialog:show() end --- Add a new function to compute the default combined name +--- Gets the default name for the combined state based on the selected states. function Editor:getCombinedDefaultName() local function normalize(s) -- Remove _ and - and numbers return s:gsub("[-_%d]", "") @@ -135,14 +132,15 @@ function Editor:getCombinedDefaultName() commonBase = commonPrefix(commonBase, normalize(self.selected_widgets[i].state.name)) if commonBase == "" then break end end - if commonBase and #commonBase > 2 then -- <3 char names are not useful + if commonBase and #commonBase > 2 then -- < 3 char names are not useful return commonBase else return "Combined" end end --- Modified performCombineStates function to use combine1direction +--- Combines the selected states into one state, based off of the selected combination type. +--- @param combinedName string The name for the combined state. function Editor:performCombineStates(combinedName, combineType) local combined_state, error = libdmi.new_state(self.dmi.width, self.dmi.height, self.dmi.temp) if error or not combined_state then From cba1a423be4a254d9f1fcf6c157113d9dae8f699 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Tue, 29 Apr 2025 00:54:49 -0700 Subject: [PATCH 11/22] final cleanup --- scripts/classes/editor/_editor.lua | 15 +++++-- scripts/classes/editor/rendering.lua | 28 +++++------- scripts/classes/editor/state.lua | 2 +- scripts/classes/editor/state_operations.lua | 2 +- scripts/types.lua | 49 +++------------------ 5 files changed, 30 insertions(+), 66 deletions(-) diff --git a/scripts/classes/editor/_editor.lua b/scripts/classes/editor/_editor.lua index 3c8237b..9870728 100644 --- a/scripts/classes/editor/_editor.lua +++ b/scripts/classes/editor/_editor.lua @@ -28,8 +28,6 @@ Editor.__index = Editor --- @class Editor.Mouse --- @field position Point The current mouse position. ---- @field leftClick boolean Whether the left mouse button is pressed. ---- @field rightClick boolean Whether the right mouse button is pressed. --- Creates a new instance of the Editor class. --- @param title string The title of the editor. @@ -44,14 +42,19 @@ function Editor.new(title, dmi) self.focused_widget = nil self.hovering_widgets = {} self.scroll = 0 - self.mouse = { position = Point(0, 0), leftClick = false, rightClick = false } + self.mouse = { position = Point(0, 0) } self.dmi = nil self.open_sprites = {} self.widgets = {} + self.selected_widgets = {} self.context_widget = nil self.save_path = nil self.open_path = is_filename and dmi --[[@as string]] or nil + self.dragging = false + self.drag_start_time = math.huge + self.drop_index = nil + self.canvas_width = 185 self.canvas_height = 215 self.max_in_a_row = 1 @@ -220,7 +223,10 @@ end --- Shows the editor dialog. function Editor:show() - self.dialog:show { wait = false } + self.dialog:show { + wait = false, + autoscrollbars=true, + } end --- Opens a DMI file and displays it in the editor. @@ -239,6 +245,7 @@ function Editor:open_file(dmi) self.scroll = 0 self.dmi = nil self.widgets = {} + self.selected_widgets = {} self.open_sprites = {} self.save_path = nil diff --git a/scripts/classes/editor/rendering.lua b/scripts/classes/editor/rendering.lua index 2f06679..554d464 100644 --- a/scripts/classes/editor/rendering.lua +++ b/scripts/classes/editor/rendering.lua @@ -53,9 +53,6 @@ function Editor:onpaint(ctx) local is_mouse_over = not self.context_widget and widget.bounds:contains(self.mouse.position) if is_mouse_over then stateStyle = COMMON_STATE.hot or stateStyle - if self.mouse.leftClick then - stateStyle = COMMON_STATE.selected or stateStyle - end end if widget.type == "IconWidget"and self.selected_widgets and table.index_of(self.selected_widgets, widget) > 0 then stateStyle = COMMON_STATE.selected or stateStyle @@ -77,7 +74,7 @@ function Editor:onpaint(ctx) local text = self.fit_text(widget.text, ctx, widget.bounds.width) local size = ctx:measureText(text) - -- Fix: Use fallback text color instead of undefined 'state' + -- fallback text color ctx.color = widget.text_color or app.theme.color.text ctx:fillText( text, @@ -206,6 +203,10 @@ function Editor:repaint_states() local min_index = (self.max_in_a_row * self.scroll) local max_index = min_index + self.max_in_a_row * (self.max_in_a_column + 1) for index, state in ipairs(self.dmi.states) do +-- TODO: fix bug here and in box_bounds below where you can have +-- index = 7, min_index = 9, max_index = 10, max_in_a_row = 9 +-- and we don't render anything despite there being no more +-- states past 7, need to render at least max_in_a_row if index > min_index and index <= max_index then local bounds = self:box_bounds(index) local text_color = nil @@ -225,8 +226,6 @@ function Editor:repaint_states() end end - local name = #state.name > 0 and state.name or "no name" - local icon = self.image_cache:get(state.frame_key) local bytes = string.char(libdmi.overlay_color(app.theme.color.face.red, app.theme.color.face.green, app.theme.color.face.blue, icon.width, icon.height, string.byte(icon.bytes, 1, #icon.bytes)) --[[@as number]]) @@ -290,11 +289,10 @@ end function Editor:box_bounds(index) local row_index = index - self.max_in_a_row * self.scroll - return Rectangle( (self.dmi.width + BOX_PADDING) * ((row_index - 1) % self.max_in_a_row), - (self.dmi.height + BOX_BORDER + BOX_PADDING * 2 + TEXT_HEIGHT) * math.floor((row_index - 1) / self.max_in_a_row) + - BOX_PADDING, + (self.dmi.height + BOX_BORDER + BOX_PADDING * 2 + TEXT_HEIGHT) * + math.floor((row_index - 1) / self.max_in_a_row) + BOX_PADDING, self.dmi.width + BOX_BORDER, self.dmi.height + BOX_BORDER ) @@ -304,7 +302,7 @@ end --- @param ev MouseEvent The mouse event object. function Editor:onmousedown(ev) if ev.button == MouseButton.LEFT then - self.mouse.leftClick = true + -- Don't set this until after selection behaivor is handled self.focused_widget = nil -- Don't do anything fancy if we're clicking on a context menu @@ -351,7 +349,6 @@ function Editor:onmousedown(ev) end end elseif ev.button == MouseButton.RIGHT then - self.mouse.rightClick = true self.focused_widget = nil self.context_widget = nil end @@ -444,6 +441,8 @@ function Editor:onmouseup(ev) self.scroll = target_row - visible_rows + 1 end + -- Reset selected widgets since we've moved around states (and thus idx) + self.selected_widgets = {} self:repaint_states() end end @@ -459,9 +458,6 @@ function Editor:onmouseup(ev) self.drag_widget = nil self.drag_start_time = nil self.drop_index = nil - self.mouse.leftClick = false - elseif ev.button == MouseButton.RIGHT then - self.mouse.rightClick = false end end if repaint then @@ -470,7 +466,7 @@ function Editor:onmouseup(ev) end --- Updates the mouse position and triggers a repaint. ---- @param ev table The mouse event containing the x and y coordinates. +--- @param ev MouseEvent The mouse event containing the x and y coordinates. function Editor:onmousemove(ev) local mouse_position = Point(ev.x, ev.y) local should_repaint = false @@ -488,7 +484,7 @@ function Editor:onmousemove(ev) end -- Handle dragging - if self.mouse.leftClick and self.drag_widget and not self.dragging then + if (ev.button == MouseButton.LEFT) and self.drag_widget and not self.dragging then -- Start drag after small delay/movement if os.clock() - self.drag_start_time > 0.1 then self.dragging = true diff --git a/scripts/classes/editor/state.lua b/scripts/classes/editor/state.lua index 20c5a96..769a63d 100644 --- a/scripts/classes/editor/state.lua +++ b/scripts/classes/editor/state.lua @@ -134,7 +134,7 @@ function Editor:state_context(state, ev) { text = "Remove", onclick = function() self:remove_state(state) end }, { text = "Split", onclick = function() self:split_state(state) end }, } - if self.selected_widgets and #self.selected_widgets > 1 then + if #self.selected_widgets > 1 then table.insert(buttons, { text = "Combine", onclick = function() self:combine_selected_states() end }) end self.context_widget = ContextWidget.new( diff --git a/scripts/classes/editor/state_operations.lua b/scripts/classes/editor/state_operations.lua index 2a7dc65..9263270 100644 --- a/scripts/classes/editor/state_operations.lua +++ b/scripts/classes/editor/state_operations.lua @@ -79,7 +79,7 @@ end --- Combines multiple selected states into one state. function Editor:combine_selected_states() - if not self.dmi or not self.selected_widgets or #self.selected_widgets < 2 then + if not self.dmi or #self.selected_widgets < 2 then app.alert { title = self.title, text = "Select at least two states to combine." } return end diff --git a/scripts/types.lua b/scripts/types.lua index c637141..cea439a 100644 --- a/scripts/types.lua +++ b/scripts/types.lua @@ -198,6 +198,7 @@ end ------------------- ENUMS ------------------- +--- @enum ColorMode ColorMode = { RGB = 1, GRAY = 2, @@ -205,7 +206,9 @@ ColorMode = { TILEMAP = 4, } +--- @enum MouseButton MouseButton = { + NONE = 0, LEFT = 1, MIDDLE = 2, RIGHT = 3, @@ -213,6 +216,7 @@ MouseButton = { X2 = 5, } +--- @enum BlendMode BlendMode = { NORMAL = 0, SRC = 1, @@ -236,6 +240,7 @@ BlendMode = { DIVIDE = 19, } +--- @enum WebSocketMessageType WebSocketMessageType = { TEXT = "TEXT", BINARY = "BINARY", @@ -746,56 +751,12 @@ WebSocketMessageType = { --- @alias PixelColor number ---- @class MouseButton ---- @field LEFT number ---- @field MIDDLE number ---- @field RIGHT number ---- @field X1 number ---- @field X2 number - ---- @alias ColorMode ----| 0 ----| 1 ----| 2 ----| 3 - ---- @alias BlendMode ----| 0 ----| 1 ----| 2 ----| 3 ----| 4 ----| 5 ----| 6 ----| 7 ----| 8 ----| 9 ----| 10 ----| 11 ----| 12 ----| 13 ----| 14 ----| 15 ----| 16 ----| 17 ----| 18 ----| 19 - --- @alias AniDir ---| 0 ---| 1 ---| 2 ---| 3 ---- @alias WebSocketMessageType ----| 'TEXT' ----| 'BINARY' ----| 'OPEN' ----| 'CLOSE' ----| 'PING' ----| 'PONG' ----| 'FRAGMENT' - --- @class LibDmi: table --- @field new_file fun(name: string, width: number, height: number, temp: string): Dmi?, string? Creates a new DMI file. If fails, returns nil and an error message. --- @field open_file fun(path: string, temp: string): Dmi?, string? Opens a DMI file. If fails, returns nil and an error message. From 0ee6579b4373d02b175329ca8b2be39d3684f203 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Tue, 29 Apr 2025 01:27:40 -0700 Subject: [PATCH 12/22] fix state var overload and combine ase naming --- lib/src/lua.rs | 5 +++-- scripts/classes/editor/_editor.lua | 1 + scripts/classes/editor/rendering.lua | 4 ++-- scripts/classes/editor/state_operations.lua | 21 ++++++++++++--------- scripts/classes/widget.lua | 5 ++++- scripts/types.lua | 2 +- 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/lib/src/lua.rs b/lib/src/lua.rs index 7c7b51e..a848a82 100644 --- a/lib/src/lua.rs +++ b/lib/src/lua.rs @@ -62,12 +62,13 @@ fn save_file(_: &Lua, (dmi, filename): (LuaTable, String)) -> LuaResult LuaResult { +fn new_state(lua: &Lua, (width, height, temp, name): (u32, u32, String, Option)) -> LuaResult { if !Path::new(&temp).exists() { Err("Temp directory does not exist".to_string()).into_lua_err()? } - let state = State::new_blank(String::new(), width, height).to_serialized(temp)?; + let state_name = name.unwrap_or(String::new()); + let state = State::new_blank(state_name, width, height).to_serialized(temp)?; let table = state.into_lua_table(lua)?; Ok(table) diff --git a/scripts/classes/editor/_editor.lua b/scripts/classes/editor/_editor.lua index 9870728..a193dd5 100644 --- a/scripts/classes/editor/_editor.lua +++ b/scripts/classes/editor/_editor.lua @@ -13,6 +13,7 @@ --- @field dmi Dmi The currently opened DMI file. --- @field open_sprites (StateSprite)[] A table containing all open sprites. --- @field widgets (AnyWidget)[] A table containing all state widgets. +--- @field selected_widgets (IconWidget)[] Selected icon widgets to possibly combine. --- @field context_widget ContextWidget|nil The state that is currently being right clicked --- @field beforecommand number The event object for the "beforecommand" event. --- @field aftercommand number The event object for the "aftercommand" event. diff --git a/scripts/classes/editor/rendering.lua b/scripts/classes/editor/rendering.lua index 554d464..9d805e6 100644 --- a/scripts/classes/editor/rendering.lua +++ b/scripts/classes/editor/rendering.lua @@ -239,9 +239,9 @@ function Editor:repaint_states() bounds, icon, function() self:open_state(state) end, - function(ev) self:state_context(state, ev) end + function(ev) self:state_context(state, ev) end, + state ) - iconWidget.state = state table.insert(self.widgets, iconWidget) table.insert(self.widgets, TextWidget.new( diff --git a/scripts/classes/editor/state_operations.lua b/scripts/classes/editor/state_operations.lua index 9263270..541266e 100644 --- a/scripts/classes/editor/state_operations.lua +++ b/scripts/classes/editor/state_operations.lua @@ -127,9 +127,9 @@ function Editor:getCombinedDefaultName() return a:sub(1, i - 1) end - local commonBase = normalize(self.selected_widgets[1].state.name) + local commonBase = normalize(self.selected_widgets[1].iconstate.name) for i = 2, #self.selected_widgets do - commonBase = commonPrefix(commonBase, normalize(self.selected_widgets[i].state.name)) + commonBase = commonPrefix(commonBase, normalize(self.selected_widgets[i].iconstate.name)) if commonBase == "" then break end end if commonBase and #commonBase > 2 then -- < 3 char names are not useful @@ -142,17 +142,17 @@ end --- Combines the selected states into one state, based off of the selected combination type. --- @param combinedName string The name for the combined state. function Editor:performCombineStates(combinedName, combineType) - local combined_state, error = libdmi.new_state(self.dmi.width, self.dmi.height, self.dmi.temp) + local combined_state, error = libdmi.new_state(self.dmi.width, self.dmi.height, self.dmi.temp, combinedName) if error or not combined_state then app.alert { title = "Error", text = { "Failed to create combined state", error } } return end - -- Reorder selected states based on DMI order + -- Selected iconstates based on DMI order local sortedStates = {} for _, state in ipairs(self.dmi.states) do for _, selWidget in ipairs(self.selected_widgets) do - if state == selWidget.state then + if state == selWidget.iconstate then table.insert(sortedStates, state) break end @@ -174,15 +174,18 @@ function Editor:performCombineStates(combinedName, combineType) self:repaint_states() end +--- Combines the selected states into one new 1-dir iconstate, so each frame is a different state. +--- @param combined_state State The combined state inject all the parts into. +--- @param sortedStates State[] The iconstates to combine. function Editor:combine1direction(combined_state, sortedStates) combined_state.frame_count = #sortedStates for i, state in ipairs(sortedStates) do - local preview = self.image_cache:get(state.frame_key) - if not preview then - app.alert { title = "Error", text = "Preview image missing for state: " .. (state.name or "unknown") } + local img = self.image_cache:get(state.frame_key) + if not img then + app.alert { title = "Error", text = "Image data missing for state: " .. (state.name or "unknown") } return false end - save_image_bytes(preview, app.fs.joinPath(self.dmi.temp, combined_state.frame_key .. "." .. (i - 1) .. ".bytes")) + save_image_bytes(img, app.fs.joinPath(self.dmi.temp, combined_state.frame_key .. "." .. (i - 1) .. ".bytes")) end return true end diff --git a/scripts/classes/widget.lua b/scripts/classes/widget.lua index 37b6bad..3e629b8 100644 --- a/scripts/classes/widget.lua +++ b/scripts/classes/widget.lua @@ -19,6 +19,7 @@ --- @field icon Image The icon of the widget. --- @field onleftclick MouseFunction|nil The onleftclick function of the widget. --- @field onrightclick MouseFunction|nil The onrightclick function of the widget. +--- @field iconstate State|nil The iconstate of the widget (optional). IconWidget = { type = "IconWidget" } IconWidget.__index = IconWidget @@ -28,8 +29,9 @@ IconWidget.__index = IconWidget --- @param icon Image The icon of the widget. --- @param onleftclick MouseFunction|nil The function to be called when the widget is clicked (optional). --- @param onrightclick MouseFunction|nil The function to be called when the widget is right clicked (optional). +--- @param iconstate State an associated iconstate (optional) --- @return IconWidget widget The newly created widget. -function IconWidget.new(editor, bounds, icon, onleftclick, onrightclick) +function IconWidget.new(editor, bounds, icon, onleftclick, onrightclick, iconstate) local self = setmetatable({}, IconWidget) self.editor = editor @@ -37,6 +39,7 @@ function IconWidget.new(editor, bounds, icon, onleftclick, onrightclick) self.icon = icon self.onleftclick = onleftclick or nil self.onrightclick = onrightclick or nil + self.iconstate = iconstate or nil return self end diff --git a/scripts/types.lua b/scripts/types.lua index cea439a..32c656f 100644 --- a/scripts/types.lua +++ b/scripts/types.lua @@ -761,7 +761,7 @@ WebSocketMessageType = { --- @field new_file fun(name: string, width: number, height: number, temp: string): Dmi?, string? Creates a new DMI file. If fails, returns nil and an error message. --- @field open_file fun(path: string, temp: string): Dmi?, string? Opens a DMI file. If fails, returns nil and an error message. --- @field save_file fun(dmi: Dmi, filename: string): nil, string? Saves the DMI file. If fails, returns an error message. ---- @field new_state fun(width: number, height: number, temp: string): State?, string? Creates a new state. If fails, returns nil and an error message. +--- @field new_state fun(width: number, height: number, temp: string, name?: string): State?, string? Creates a new state. If fails, returns nil and an error message. --- @field copy_state fun(state: State, temp: string): nil, string? Copies the state to the clipboard. If fails, returns an error message. --- @field paste_state fun(width: number, height: number, temp: string): State?, string? Pastes the state from the clipboard. If fails, returns nil and an error message. --- @field resize fun(dmi: Dmi, width: number, height: number, medhod: string): nil, string? Resizes the DMI file. If fails, returns an error message. From 58d79208ad6fb3dca5cd9c388b30d7ec9b3e0a08 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Tue, 29 Apr 2025 01:33:16 -0700 Subject: [PATCH 13/22] even better flatten logic --- scripts/classes/statesprite.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/scripts/classes/statesprite.lua b/scripts/classes/statesprite.lua index b57e9ec..5ea3c1e 100644 --- a/scripts/classes/statesprite.lua +++ b/scripts/classes/statesprite.lua @@ -94,9 +94,17 @@ function StateSprite:save() return false end - -- Store original layers if we need to auto-flatten (only if multiple layers exist) + -- Store original layers if we need to auto-flatten + -- And only if there's layers that aren't the magic directional names local original_layers = nil - if Preferences.getAutoFlatten() and #self.sprite.layers > 1 then + local hasNonDirectional = false + for _, layer in ipairs(self.sprite.layers) do + if table.index_of(DIRECTION_NAMES, layer.name) == 0 then + hasNonDirectional = true + break + end + end + if Preferences.getAutoFlatten() and hasNonDirectional then original_layers = {} for _, layer in ipairs(self.sprite.layers) do table.insert(original_layers, layer) From 3d0d2134ed405aee034c631ef5ed049e22420cea Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Tue, 29 Apr 2025 01:35:55 -0700 Subject: [PATCH 14/22] fmt --- lib/src/lua.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/lua.rs b/lib/src/lua.rs index a848a82..e54ef24 100644 --- a/lib/src/lua.rs +++ b/lib/src/lua.rs @@ -62,7 +62,10 @@ fn save_file(_: &Lua, (dmi, filename): (LuaTable, String)) -> LuaResult)) -> LuaResult { +fn new_state( + lua: &Lua, + (width, height, temp, name): (u32, u32, String, Option), +) -> LuaResult { if !Path::new(&temp).exists() { Err("Temp directory does not exist".to_string()).into_lua_err()? } From ae06883a9488ac7858c707fc6ee2d5e9c8ef4aaf Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Tue, 29 Apr 2025 20:36:10 -0700 Subject: [PATCH 15/22] selection fixes n cleanup n shit --- scripts/classes/editor/_editor.lua | 6 +-- scripts/classes/editor/rendering.lua | 59 ++++++++++----------- scripts/classes/editor/state.lua | 2 +- scripts/classes/editor/state_operations.lua | 17 +++--- 4 files changed, 43 insertions(+), 41 deletions(-) diff --git a/scripts/classes/editor/_editor.lua b/scripts/classes/editor/_editor.lua index a193dd5..3c0025b 100644 --- a/scripts/classes/editor/_editor.lua +++ b/scripts/classes/editor/_editor.lua @@ -13,7 +13,7 @@ --- @field dmi Dmi The currently opened DMI file. --- @field open_sprites (StateSprite)[] A table containing all open sprites. --- @field widgets (AnyWidget)[] A table containing all state widgets. ---- @field selected_widgets (IconWidget)[] Selected icon widgets to possibly combine. +--- @field selected_states State[] Selected icon widgets to possibly combine. --- @field context_widget ContextWidget|nil The state that is currently being right clicked --- @field beforecommand number The event object for the "beforecommand" event. --- @field aftercommand number The event object for the "aftercommand" event. @@ -47,7 +47,7 @@ function Editor.new(title, dmi) self.dmi = nil self.open_sprites = {} self.widgets = {} - self.selected_widgets = {} + self.selected_states = {} self.context_widget = nil self.save_path = nil self.open_path = is_filename and dmi --[[@as string]] or nil @@ -246,7 +246,7 @@ function Editor:open_file(dmi) self.scroll = 0 self.dmi = nil self.widgets = {} - self.selected_widgets = {} + self.selected_states = {} self.open_sprites = {} self.save_path = nil diff --git a/scripts/classes/editor/rendering.lua b/scripts/classes/editor/rendering.lua index 9d805e6..538499c 100644 --- a/scripts/classes/editor/rendering.lua +++ b/scripts/classes/editor/rendering.lua @@ -54,7 +54,7 @@ function Editor:onpaint(ctx) if is_mouse_over then stateStyle = COMMON_STATE.hot or stateStyle end - if widget.type == "IconWidget"and self.selected_widgets and table.index_of(self.selected_widgets, widget) > 0 then + if widget.type == "IconWidget" and table.index_of(self.selected_states, widget.iconstate) > 0 then stateStyle = COMMON_STATE.selected or stateStyle end @@ -308,7 +308,7 @@ function Editor:onmousedown(ev) -- Don't do anything fancy if we're clicking on a context menu if not self.context_widget then - local clickedWidget = nil + local clickedWidget = nil --[[ @type AnyWidget ]] for _, widget in ipairs(self.widgets) do if widget.type == "IconWidget" and widget.bounds:contains(Point(ev.x, ev.y)) then clickedWidget = widget @@ -325,18 +325,18 @@ function Editor:onmousedown(ev) end end local clickedWidgetIdx = table.index_of(iconWidgets, clickedWidget) - local closestSelWidgetIdx = self:find_closest_selected_widget_index(clickedWidgetIdx) + local closestSelWidgetIdx = self:find_closest_selected_state_index(clickedWidgetIdx) if not closestSelWidgetIdx then return end local low = math.min(clickedWidgetIdx, closestSelWidgetIdx) local high = math.max(clickedWidgetIdx, closestSelWidgetIdx) - self:select_iconwidgets_range(low, high) + self:select_states_range(low, high) self:repaint() return end -- Selection mode with just Ctrl if ev.ctrlKey then - self:toggle_state_selection(clickedWidget) + self:toggle_state_selection(clickedWidget.iconstate) self:repaint() return end @@ -345,7 +345,7 @@ function Editor:onmousedown(ev) self.drag_widget = clickedWidget self.drag_start_time = os.clock() else -- No clicked widget, handle deselection - self.selected_widgets = {} + self.selected_states = {} end end elseif ev.button == MouseButton.RIGHT then @@ -395,7 +395,7 @@ function Editor:onmouseup(ev) local buttonsToAdd = { { text = "Paste", onclick = function() self:clipboard_paste_state() end }, } - if self.selected_widgets and #self.selected_widgets > 1 then + if #self.selected_states > 1 then table.insert(buttonsToAdd, { text = "Combine", onclick = function() self:combine_selected_states() end }) end self.context_widget = ContextWidget.new( @@ -441,8 +441,8 @@ function Editor:onmouseup(ev) self.scroll = target_row - visible_rows + 1 end - -- Reset selected widgets since we've moved around states (and thus idx) - self.selected_widgets = {} + -- Reset selected states since we've moved around states (and thus idx) + self.selected_states = {} self:repaint_states() end end @@ -601,21 +601,20 @@ function Editor.fit_text(text, ctx, maxWidth) end --- Helper method to toggle state selection ---- @param widget IconWidget The widget to toggle selection for. -function Editor:toggle_state_selection(widget) - self.selected_widgets = self.selected_widgets or {} - local idx = table.index_of(self.selected_widgets, widget) - if idx and idx > 0 then - table.remove(self.selected_widgets, idx) +--- @param state State The state to toggle selection for. +function Editor:toggle_state_selection(state) + local idx = table.index_of(self.selected_states, state) + if idx ~= 0 then + table.remove(self.selected_states, idx) else - table.insert(self.selected_widgets, widget) + table.insert(self.selected_states, state) end end ---- Helper method to select a range of widgets +--- Helper method to select a range of states --- @param lowIndex number The lower index of the range. --- @param highIndex number The higher index of the range. -function Editor:select_iconwidgets_range(lowIndex, highIndex) +function Editor:select_states_range(lowIndex, highIndex) local iconWidgets = {} for _, widget in ipairs(self.widgets) do if widget.type == "IconWidget" then @@ -623,42 +622,42 @@ function Editor:select_iconwidgets_range(lowIndex, highIndex) end end - -- Determine if all widgets in the range are already selected. + -- Determine if all states in the range are already selected. local allSelected = true for i = lowIndex, highIndex do local widget = iconWidgets[i] - if table.index_of(self.selected_widgets, widget) == 0 then + if table.index_of(self.selected_states, widget.iconstate) == 0 then allSelected = false break end end - -- (De)select each widget in the range. + -- (De)select each state in the range. for i = lowIndex, highIndex do local widget = iconWidgets[i] + local idx = table.index_of(self.selected_states, widget.iconstate) if allSelected then -- Could be better but I'm not sure what the logic would be for -- determining which neighboring states to deselect. - local idx = table.index_of(self.selected_widgets, widget) if idx and idx > 0 then - table.remove(self.selected_widgets, idx) + table.remove(self.selected_states, idx) end else - if table.index_of(self.selected_widgets, widget) == 0 then - table.insert(self.selected_widgets, widget) + if idx == 0 then + table.insert(self.selected_states, widget.iconstate) end end end end ---- Finds the closest selected widget index to the clicked widget index. +--- Finds the closest selected state index to the clicked widget index. --- @param clickedWidgetIndex number The index of the clicked widget. ---- @return number|nil closestIndex The index of the closest selected widget. -function Editor:find_closest_selected_widget_index(clickedWidgetIndex) +--- @return number|nil closestIndex The index of the closest selected state. +function Editor:find_closest_selected_state_index(clickedWidgetIndex) local closestIndex = nil local closestDistance = math.huge - local iconWidgets = {} + local iconWidgets = {} --[[ @type IconWidget[] ]] for _, widget in ipairs(self.widgets) do if widget.type == "IconWidget" then table.insert(iconWidgets, widget) @@ -666,7 +665,7 @@ function Editor:find_closest_selected_widget_index(clickedWidgetIndex) end for i, widget in ipairs(iconWidgets) do - if widget.type == "IconWidget" and table.index_of(self.selected_widgets, widget) > 0 then + if widget.type == "IconWidget" and table.index_of(self.selected_states, widget.iconstate) > 0 then local distance = math.abs(i - clickedWidgetIndex) if distance < closestDistance then closestDistance = distance diff --git a/scripts/classes/editor/state.lua b/scripts/classes/editor/state.lua index 769a63d..254c4b8 100644 --- a/scripts/classes/editor/state.lua +++ b/scripts/classes/editor/state.lua @@ -134,7 +134,7 @@ function Editor:state_context(state, ev) { text = "Remove", onclick = function() self:remove_state(state) end }, { text = "Split", onclick = function() self:split_state(state) end }, } - if #self.selected_widgets > 1 then + if #self.selected_states > 1 then table.insert(buttons, { text = "Combine", onclick = function() self:combine_selected_states() end }) end self.context_widget = ContextWidget.new( diff --git a/scripts/classes/editor/state_operations.lua b/scripts/classes/editor/state_operations.lua index 541266e..2d5ae1f 100644 --- a/scripts/classes/editor/state_operations.lua +++ b/scripts/classes/editor/state_operations.lua @@ -79,7 +79,7 @@ end --- Combines multiple selected states into one state. function Editor:combine_selected_states() - if not self.dmi or #self.selected_widgets < 2 then + if not self.dmi or #self.selected_states < 2 then app.alert { title = self.title, text = "Select at least two states to combine." } return end @@ -95,6 +95,9 @@ function Editor:combine_selected_states() option = COMBINE_TYPES.onedir, options = { COMBINE_TYPES.onedir }, } + dialog:label { + text = "Note: This does not work for combining animated states currently.", + } dialog:button { text = "&OK", focus = true, @@ -127,9 +130,9 @@ function Editor:getCombinedDefaultName() return a:sub(1, i - 1) end - local commonBase = normalize(self.selected_widgets[1].iconstate.name) - for i = 2, #self.selected_widgets do - commonBase = commonPrefix(commonBase, normalize(self.selected_widgets[i].iconstate.name)) + local commonBase = normalize(self.selected_states[1].name) + for i = 2, #self.selected_states do + commonBase = commonPrefix(commonBase, normalize(self.selected_states[i].name)) if commonBase == "" then break end end if commonBase and #commonBase > 2 then -- < 3 char names are not useful @@ -151,8 +154,8 @@ function Editor:performCombineStates(combinedName, combineType) -- Selected iconstates based on DMI order local sortedStates = {} for _, state in ipairs(self.dmi.states) do - for _, selWidget in ipairs(self.selected_widgets) do - if state == selWidget.iconstate then + for _, selState in ipairs(self.selected_states) do + if state == selState then table.insert(sortedStates, state) break end @@ -170,7 +173,7 @@ function Editor:performCombineStates(combinedName, combineType) table.insert(self.dmi.states, combined_state) self.image_cache:load_state(self.dmi, combined_state) self.modified = true - self.selected_widgets = {} + self.selected_states = {} self:repaint_states() end From 4cadcdfa7736be1312ed1bf99e9f600d99349971 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Tue, 29 Apr 2025 21:59:55 -0700 Subject: [PATCH 16/22] first --- scripts/classes/editor/state_operations.lua | 15 ++++++++++----- scripts/constants.lua | 5 +++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/scripts/classes/editor/state_operations.lua b/scripts/classes/editor/state_operations.lua index 2d5ae1f..bb85582 100644 --- a/scripts/classes/editor/state_operations.lua +++ b/scripts/classes/editor/state_operations.lua @@ -95,6 +95,12 @@ function Editor:combine_selected_states() option = COMBINE_TYPES.onedir, options = { COMBINE_TYPES.onedir }, } + dialog:combobox { + id = "frame_sel_type", + label = "Frame Selection:", + option = FRAME_SEL_TYPES.all_seq, + options = { FRAME_SEL_TYPES.all_seq, FRAME_SEL_TYPES.first_only, }, + } dialog:label { text = "Note: This does not work for combining animated states currently.", } @@ -104,8 +110,9 @@ function Editor:combine_selected_states() onclick = function() local combinedName = dialog.data.combined_name or "Combined" local combineType = dialog.data.combine_type or COMBINE_TYPES.onedir + local frameSelType = dialog.data.frame_sel_type or FRAME_SEL_TYPES.all_seq dialog:close() - self:performCombineStates(combinedName, combineType) + self:performCombineStates(combinedName, combineType, frameSelType) end } dialog:button { @@ -144,7 +151,7 @@ end --- Combines the selected states into one state, based off of the selected combination type. --- @param combinedName string The name for the combined state. -function Editor:performCombineStates(combinedName, combineType) +function Editor:performCombineStates(combinedName, combineType, frameSelType) local combined_state, error = libdmi.new_state(self.dmi.width, self.dmi.height, self.dmi.temp, combinedName) if error or not combined_state then app.alert { title = "Error", text = { "Failed to create combined state", error } } @@ -164,11 +171,9 @@ function Editor:performCombineStates(combinedName, combineType) combined_state.name = combinedName if combineType == COMBINE_TYPES.onedir then - if not self:combine1direction(combined_state, sortedStates) then + if not self:combine1direction(combined_state, sortedStates, frameSelType) then return end - else - -- Future expansion placeholder for other combine types. end table.insert(self.dmi.states, combined_state) self.image_cache:load_state(self.dmi, combined_state) diff --git a/scripts/constants.lua b/scripts/constants.lua index b78276c..b7c1bb8 100644 --- a/scripts/constants.lua +++ b/scripts/constants.lua @@ -17,3 +17,8 @@ COMMON_STATE = { COMBINE_TYPES = { onedir = "1 direction", } + +FRAME_SEL_TYPES = { + first_only = "First frame only", + all_seq = "All frames", +} From 4e9accc249c18c20991a06c217f6c9fd8ee9fc09 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Tue, 29 Apr 2025 22:00:05 -0700 Subject: [PATCH 17/22] working multiframe type --- scripts/classes/editor/state_operations.lua | 42 +++++++++++++++------ 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/scripts/classes/editor/state_operations.lua b/scripts/classes/editor/state_operations.lua index bb85582..dacacec 100644 --- a/scripts/classes/editor/state_operations.lua +++ b/scripts/classes/editor/state_operations.lua @@ -101,9 +101,6 @@ function Editor:combine_selected_states() option = FRAME_SEL_TYPES.all_seq, options = { FRAME_SEL_TYPES.all_seq, FRAME_SEL_TYPES.first_only, }, } - dialog:label { - text = "Note: This does not work for combining animated states currently.", - } dialog:button { text = "&OK", focus = true, @@ -185,15 +182,38 @@ end --- Combines the selected states into one new 1-dir iconstate, so each frame is a different state. --- @param combined_state State The combined state inject all the parts into. --- @param sortedStates State[] The iconstates to combine. -function Editor:combine1direction(combined_state, sortedStates) - combined_state.frame_count = #sortedStates - for i, state in ipairs(sortedStates) do - local img = self.image_cache:get(state.frame_key) - if not img then - app.alert { title = "Error", text = "Image data missing for state: " .. (state.name or "unknown") } - return false +function Editor:combine1direction(combined_state, sortedStates, frameSelType) + combined_state.dirs = 1 + local total_frames = 0 + if frameSelType == FRAME_SEL_TYPES.first_only then + total_frames = #sortedStates + else + for _, st in ipairs(sortedStates) do + total_frames = total_frames + st.frame_count + end + end + combined_state.frame_count = total_frames + + local frameIndex = 0 + for _, st in ipairs(sortedStates) do + local framesToUse = (frameSelType == FRAME_SEL_TYPES.first_only) and 1 or st.frame_count + for i = 0, framesToUse - 1 do + local srcPath = app.fs.joinPath(self.dmi.temp, st.frame_key .. "." .. i .. ".bytes") + local dstPath = app.fs.joinPath(self.dmi.temp, combined_state.frame_key .. "." .. frameIndex .. ".bytes") + -- Copy the bytes from srcPath to dstPath + local src_file = io.open(srcPath, "rb") + if src_file then + local content = src_file:read("*all") + src_file:close() + + local dst_file = io.open(dstPath, "wb") + if dst_file then + dst_file:write(content) + dst_file:close() + end + end + frameIndex = frameIndex + 1 end - save_image_bytes(img, app.fs.joinPath(self.dmi.temp, combined_state.frame_key .. "." .. (i - 1) .. ".bytes")) end return true end From 4795833921dd9621a7bf94442b77b0cf2f1263f6 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Tue, 29 Apr 2025 22:27:19 -0700 Subject: [PATCH 18/22] working combine all dirs --- scripts/classes/editor/state_operations.lua | 54 ++++++++++++++++++++- scripts/constants.lua | 1 + 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/scripts/classes/editor/state_operations.lua b/scripts/classes/editor/state_operations.lua index dacacec..576ff6b 100644 --- a/scripts/classes/editor/state_operations.lua +++ b/scripts/classes/editor/state_operations.lua @@ -93,7 +93,7 @@ function Editor:combine_selected_states() id = "combine_type", label = "Combine Method:", option = COMBINE_TYPES.onedir, - options = { COMBINE_TYPES.onedir }, + options = { COMBINE_TYPES.onedir, COMBINE_TYPES.alldirs }, } dialog:combobox { id = "frame_sel_type", @@ -171,6 +171,10 @@ function Editor:performCombineStates(combinedName, combineType, frameSelType) if not self:combine1direction(combined_state, sortedStates, frameSelType) then return end + elseif combineType == COMBINE_TYPES.alldirs then + if not self:combineAllDirections(combined_state, sortedStates, frameSelType) then + return + end end table.insert(self.dmi.states, combined_state) self.image_cache:load_state(self.dmi, combined_state) @@ -217,3 +221,51 @@ function Editor:combine1direction(combined_state, sortedStates, frameSelType) end return true end + +function Editor:combineAllDirections(combined_state, sortedStates, frameSelType) + local dirs = sortedStates[1].dirs + for _, st in ipairs(sortedStates) do + if st.dirs ~= dirs then + app.alert { title = "Error", text = "All selected states must have the same number of directions." } + return false + end + end + combined_state.dirs = dirs + + local totalFrames = 0 + if frameSelType == FRAME_SEL_TYPES.first_only then + totalFrames = #sortedStates + else + for _, st in ipairs(sortedStates) do + totalFrames = totalFrames + st.frame_count + end + end + combined_state.frame_count = totalFrames + + local frameOffset = 0 + for _, st in ipairs(sortedStates) do + local framesToUse = (frameSelType == FRAME_SEL_TYPES.first_only) and 1 or st.frame_count + for frame = 0, framesToUse - 1 do + for d = 0, dirs - 1 do + local srcIndex = (frame * dirs) + d + local dstIndex = ((frameOffset + frame) * dirs) + d + local srcPath = app.fs.joinPath(self.dmi.temp, st.frame_key .. "." .. srcIndex .. ".bytes") + local dstPath = app.fs.joinPath(self.dmi.temp, combined_state.frame_key .. "." .. dstIndex .. ".bytes") + -- Copy the bytes from srcPath to dstPath + local src_file = io.open(srcPath, "rb") + if src_file then + local content = src_file:read("*all") + src_file:close() + + local dst_file = io.open(dstPath, "wb") + if dst_file then + dst_file:write(content) + dst_file:close() + end + end + end + end + frameOffset = frameOffset + framesToUse + end + return true +end diff --git a/scripts/constants.lua b/scripts/constants.lua index b7c1bb8..019f1e1 100644 --- a/scripts/constants.lua +++ b/scripts/constants.lua @@ -16,6 +16,7 @@ COMMON_STATE = { COMBINE_TYPES = { onedir = "1 direction", + alldirs = "All directions", } FRAME_SEL_TYPES = { From a1356cb3909f63ae2e50c50fdb1f632c19a54e38 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Tue, 29 Apr 2025 22:38:34 -0700 Subject: [PATCH 19/22] Refactor cleanliness --- scripts/classes/editor/state_operations.lua | 112 +++++++++----------- scripts/constants.lua | 2 + 2 files changed, 53 insertions(+), 61 deletions(-) diff --git a/scripts/classes/editor/state_operations.lua b/scripts/classes/editor/state_operations.lua index 576ff6b..5cd2cb5 100644 --- a/scripts/classes/editor/state_operations.lua +++ b/scripts/classes/editor/state_operations.lua @@ -53,18 +53,7 @@ function Editor:split_state(state) local src_path = app.fs.joinPath(self.dmi.temp, state.frame_key .. "." .. tostring(start_frame + frame - 1) .. ".bytes") local dst_path = app.fs.joinPath(self.dmi.temp, new_state.frame_key .. "." .. tostring(frame - 1) .. ".bytes") - -- Copy the image file - local src_file = io.open(src_path, "rb") - if src_file then - local content = src_file:read("*all") - src_file:close() - - local dst_file = io.open(dst_path, "wb") - if dst_file then - dst_file:write(content) - dst_file:close() - end - end + self:copyImageBytes(src_path, dst_path) end table.insert(self.dmi.states, new_state) @@ -91,9 +80,9 @@ function Editor:combine_selected_states() } dialog:combobox { id = "combine_type", - label = "Combine Method:", - option = COMBINE_TYPES.onedir, - options = { COMBINE_TYPES.onedir, COMBINE_TYPES.alldirs }, + label = "Used Directions:", + option = COMBINE_TYPES.alldirs, + options = { COMBINE_TYPES.alldirs, COMBINE_TYPES.onedir }, } dialog:combobox { id = "frame_sel_type", @@ -106,8 +95,8 @@ function Editor:combine_selected_states() focus = true, onclick = function() local combinedName = dialog.data.combined_name or "Combined" - local combineType = dialog.data.combine_type or COMBINE_TYPES.onedir - local frameSelType = dialog.data.frame_sel_type or FRAME_SEL_TYPES.all_seq + local combineType = dialog.data.combine_type or COMBINE_TYPES.onedir --[[@as CombineType]] + local frameSelType = dialog.data.frame_sel_type or FRAME_SEL_TYPES.all_seq --[[@as FrameSelType]] dialog:close() self:performCombineStates(combinedName, combineType, frameSelType) end @@ -148,6 +137,8 @@ end --- Combines the selected states into one state, based off of the selected combination type. --- @param combinedName string The name for the combined state. +--- @param combineType CombineType The type of combination to perform. +--- @param frameSelType FrameSelType The frame selection type. function Editor:performCombineStates(combinedName, combineType, frameSelType) local combined_state, error = libdmi.new_state(self.dmi.width, self.dmi.height, self.dmi.temp, combinedName) if error or not combined_state then @@ -188,40 +179,27 @@ end --- @param sortedStates State[] The iconstates to combine. function Editor:combine1direction(combined_state, sortedStates, frameSelType) combined_state.dirs = 1 - local total_frames = 0 - if frameSelType == FRAME_SEL_TYPES.first_only then - total_frames = #sortedStates - else - for _, st in ipairs(sortedStates) do - total_frames = total_frames + st.frame_count - end - end + local framesToUseList, total_frames = self:getFrameUsage(sortedStates, frameSelType) combined_state.frame_count = total_frames local frameIndex = 0 - for _, st in ipairs(sortedStates) do - local framesToUse = (frameSelType == FRAME_SEL_TYPES.first_only) and 1 or st.frame_count + for idx, st in ipairs(sortedStates) do + local framesToUse = framesToUseList[idx] for i = 0, framesToUse - 1 do local srcPath = app.fs.joinPath(self.dmi.temp, st.frame_key .. "." .. i .. ".bytes") local dstPath = app.fs.joinPath(self.dmi.temp, combined_state.frame_key .. "." .. frameIndex .. ".bytes") - -- Copy the bytes from srcPath to dstPath - local src_file = io.open(srcPath, "rb") - if src_file then - local content = src_file:read("*all") - src_file:close() - - local dst_file = io.open(dstPath, "wb") - if dst_file then - dst_file:write(content) - dst_file:close() - end - end + self:copyImageBytes(srcPath, dstPath) frameIndex = frameIndex + 1 end end return true end +--- Combines the selected states into one new multi-dir iconstate, so each frame is a different state. +--- For example, if 2 selected states are 4-dir with 2 frames, the combined state will be 4 dir with 4 frames. +--- @param combined_state State The combined state inject all the parts into. +--- @param sortedStates State[] The iconstates to combine. +--- @param frameSelType FrameSelType The frame selection type. function Editor:combineAllDirections(combined_state, sortedStates, frameSelType) local dirs = sortedStates[1].dirs for _, st in ipairs(sortedStates) do @@ -232,40 +210,52 @@ function Editor:combineAllDirections(combined_state, sortedStates, frameSelType) end combined_state.dirs = dirs - local totalFrames = 0 - if frameSelType == FRAME_SEL_TYPES.first_only then - totalFrames = #sortedStates - else - for _, st in ipairs(sortedStates) do - totalFrames = totalFrames + st.frame_count - end - end + local framesToUseList, totalFrames = self:getFrameUsage(sortedStates, frameSelType) combined_state.frame_count = totalFrames local frameOffset = 0 - for _, st in ipairs(sortedStates) do - local framesToUse = (frameSelType == FRAME_SEL_TYPES.first_only) and 1 or st.frame_count + for idx, st in ipairs(sortedStates) do + local framesToUse = framesToUseList[idx] for frame = 0, framesToUse - 1 do for d = 0, dirs - 1 do local srcIndex = (frame * dirs) + d local dstIndex = ((frameOffset + frame) * dirs) + d local srcPath = app.fs.joinPath(self.dmi.temp, st.frame_key .. "." .. srcIndex .. ".bytes") local dstPath = app.fs.joinPath(self.dmi.temp, combined_state.frame_key .. "." .. dstIndex .. ".bytes") - -- Copy the bytes from srcPath to dstPath - local src_file = io.open(srcPath, "rb") - if src_file then - local content = src_file:read("*all") - src_file:close() - - local dst_file = io.open(dstPath, "wb") - if dst_file then - dst_file:write(content) - dst_file:close() - end - end + self:copyImageBytes(srcPath, dstPath) end end frameOffset = frameOffset + framesToUse end return true end + +--- Gets the number of frames to use for each state based on the selected frame selection type. +--- @param sortedStates State[] The states to use. +--- @param frameSelType FrameSelType The frame selection type. +function Editor:getFrameUsage(sortedStates, frameSelType) + local framesToUseList = {} + local totalFrames = 0 + for _, st in ipairs(sortedStates) do + local useCount = ((frameSelType == FRAME_SEL_TYPES.first_only) and 1) or st.frame_count + table.insert(framesToUseList, useCount) + totalFrames = totalFrames + useCount + end + return framesToUseList, totalFrames +end + +--- Copies the image bytes from the source path to the destination path. +--- @param srcPath string The source path +--- @param dstPath string The destination path +function Editor:copyImageBytes(srcPath, dstPath) + local src_file = io.open(srcPath, "rb") + if src_file then + local content = src_file:read("*all") + src_file:close() + local dst_file = io.open(dstPath, "wb") + if dst_file then + dst_file:write(content) + dst_file:close() + end + end +end diff --git a/scripts/constants.lua b/scripts/constants.lua index 019f1e1..5132135 100644 --- a/scripts/constants.lua +++ b/scripts/constants.lua @@ -14,11 +14,13 @@ COMMON_STATE = { selected = { part = "sunken_mini_focused", color = "button_normal_text" }, } --[[@as WidgetState]] +--- @alias CombineType "1 direction"|"All directions" COMBINE_TYPES = { onedir = "1 direction", alldirs = "All directions", } +--- @alias FrameSelType "First frame only"|"All frames" FRAME_SEL_TYPES = { first_only = "First frame only", all_seq = "All frames", From aca9f2c27ab871e8f36163c72e61160964f90366 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Tue, 29 Apr 2025 22:53:53 -0700 Subject: [PATCH 20/22] add deselect --- scripts/classes/editor/state.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/classes/editor/state.lua b/scripts/classes/editor/state.lua index 254c4b8..b5832b4 100644 --- a/scripts/classes/editor/state.lua +++ b/scripts/classes/editor/state.lua @@ -136,6 +136,7 @@ function Editor:state_context(state, ev) } if #self.selected_states > 1 then table.insert(buttons, { text = "Combine", onclick = function() self:combine_selected_states() end }) + table.insert(buttons, { text = "Deselect", onclick = function() self.selected_states = {}; self:repaint() end }) end self.context_widget = ContextWidget.new( Rectangle(ev.x, ev.y, 0, 0), From bd94756fdad38aa72646a55198f6dd1d835b96e0 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Tue, 29 Apr 2025 23:00:02 -0700 Subject: [PATCH 21/22] deselection menus --- scripts/classes/editor/rendering.lua | 1 + scripts/classes/editor/state.lua | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/classes/editor/rendering.lua b/scripts/classes/editor/rendering.lua index 538499c..a926b64 100644 --- a/scripts/classes/editor/rendering.lua +++ b/scripts/classes/editor/rendering.lua @@ -397,6 +397,7 @@ function Editor:onmouseup(ev) } if #self.selected_states > 1 then table.insert(buttonsToAdd, { text = "Combine", onclick = function() self:combine_selected_states() end }) + table.insert(buttonsToAdd, { text = "Deselect", onclick = function() self.selected_states = {}; self:repaint() end }) end self.context_widget = ContextWidget.new( Rectangle(ev.x, ev.y, 0, 0), diff --git a/scripts/classes/editor/state.lua b/scripts/classes/editor/state.lua index b5832b4..31f50d6 100644 --- a/scripts/classes/editor/state.lua +++ b/scripts/classes/editor/state.lua @@ -136,7 +136,16 @@ function Editor:state_context(state, ev) } if #self.selected_states > 1 then table.insert(buttons, { text = "Combine", onclick = function() self:combine_selected_states() end }) - table.insert(buttons, { text = "Deselect", onclick = function() self.selected_states = {}; self:repaint() end }) + table.insert(buttons, { + text = "Deselect", + onclick = function() + local i = table.index_of(self.selected_states, state) + if i ~= 0 then + table.remove(self.selected_states, i) + end + self:repaint() + end + }) end self.context_widget = ContextWidget.new( Rectangle(ev.x, ev.y, 0, 0), From e29c9af4f13e56c31164378bbaa59f598b549e75 Mon Sep 17 00:00:00 2001 From: ZeWaka Date: Tue, 29 Apr 2025 23:03:31 -0700 Subject: [PATCH 22/22] select menu option too why not --- scripts/classes/editor/state.lua | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/classes/editor/state.lua b/scripts/classes/editor/state.lua index 31f50d6..36be9cc 100644 --- a/scripts/classes/editor/state.lua +++ b/scripts/classes/editor/state.lua @@ -133,6 +133,15 @@ function Editor:state_context(state, ev) { text = "Copy", onclick = function() self:clipboard_copy_state(state) end }, { text = "Remove", onclick = function() self:remove_state(state) end }, { text = "Split", onclick = function() self:split_state(state) end }, + { text = "Select", + onclick = function() + local i = table.index_of(self.selected_states, state) + if i == 0 then + table.insert(self.selected_states, state) + end + self:repaint() + end + }, } if #self.selected_states > 1 then table.insert(buttons, { text = "Combine", onclick = function() self:combine_selected_states() end })