From 9cbb54cc395c61db5c8d0fe585dac9c276fb16cd Mon Sep 17 00:00:00 2001 From: Eric Smith <5d@fifthdread.com> Date: Wed, 20 May 2026 16:18:33 -0400 Subject: [PATCH] add cell-span grid with resize handles, 3d water surface effect, and shader preset system with double-click popup menu --- scripts/dashboard_grid.gd | 819 ++++++++++++++++++++++++++----------- scripts/shader_presets.gd | 128 ++++++ shaders/vial_fill.gdshader | 26 +- 3 files changed, 721 insertions(+), 252 deletions(-) create mode 100644 scripts/shader_presets.gd diff --git a/scripts/dashboard_grid.gd b/scripts/dashboard_grid.gd index d15667f..0f3173b 100644 --- a/scripts/dashboard_grid.gd +++ b/scripts/dashboard_grid.gd @@ -13,8 +13,17 @@ signal module_removed(module: Control) var columns: int = 0 var rows: int = 0 -var _cells: Array = [] # 2D: _cells[row][col] -> PanelContainer -var _module_positions: Dictionary = {} # "col,row" -> Control +# Visual cells — shown as drag guides +var _cells: Array = [] # _cells[row][col] -> PanelContainer + +# Occupancy: _grid[row][col] -> Control or null +var _grid: Array = [] + +# Ordered module list (row-major order for layout) +var _module_list: Array[Control] = [] + +# Cell styles cached for show/hide +var _cell_styles: Dictionary = {} # Drag state var _is_dragging: bool = false @@ -25,43 +34,131 @@ var _drag_mouse_offset: Vector2 = Vector2.ZERO var _hover_col: int = -1 var _hover_row: int = -1 -# Visible cell styles cached per-cell so we can modify them during drag -var _cell_styles: Dictionary = {} # "col,row" -> StyleBoxFlat +# Resize state +var _is_resizing: bool = false +var _resize_module: Control = null +var _resize_edge: int = 0 # bitfield: 1=left, 2=right, 4=top, 8=bottom +var _resize_origin_col: int = -1 +var _resize_origin_row: int = -1 +var _resize_origin_w: int = 1 +var _resize_origin_h: int = 1 +var _resize_preview: ColorRect = null # ghost showing new size + +# Size of a single cell in pixels (computed during _layout_cells) +var _cell_w: float = 1.0 +var _cell_h: float = 1.0 + +# Resize edge detection threshold (pixels) +const RESIZE_THRESHOLD: float = 10.0 +const EDGE_LEFT: int = 1 +const EDGE_RIGHT: int = 2 +const EDGE_TOP: int = 4 +const EDGE_BOTTOM: int = 8 func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_STOP resized.connect(_rebuild_grid) gui_input.connect(_on_gui_input) + mouse_exited.connect(_on_mouse_exited) _rebuild_grid() +func _on_mouse_exited() -> void: + if _is_dragging: + if _drag_module != null: + _drag_module.modulate = Color(1, 1, 1, 1) + _drop_to_source() + _layout_cells() + _finish_drag() + _show_cells(false) + elif _is_resizing: + _end_resize(Vector2.ZERO) + + func _on_gui_input(event: InputEvent) -> void: if event is InputEventMouseButton: var mb: InputEventMouseButton = event - if mb.button_index == MOUSE_BUTTON_LEFT and not mb.double_click: + if mb.button_index == MOUSE_BUTTON_LEFT and mb.double_click and mb.pressed: + _show_preset_popup(mb.position) + elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.double_click: if mb.pressed: - _begin_drag(mb.position) + if not _try_begin_resize(mb.position): + _begin_drag(mb.position) + elif _is_resizing: + _end_resize(mb.position) elif _is_dragging: _end_drag(mb.position) - elif event is InputEventMouseMotion and _is_dragging: - _update_drag(event.position) + + elif event is InputEventMouseMotion: + if _is_resizing: + _update_resize(event.position) + elif _is_dragging: + _update_drag(event.position) + else: + _update_hover_cursor(event.position) -func place_module(module: Control, col: int, row: int) -> void: - if not _is_valid_cell(col, row): +func _show_preset_popup(mouse_pos: Vector2) -> void: + var cell_pos := _cell_at_position(mouse_pos) + if cell_pos.x < 0 or cell_pos.y < 0: return - _place_module_in_cell(module, col, row) + var module := _get_module_at(cell_pos.x, cell_pos.y) + if module == null: + return + # Don't show popup if already dragging or resizing + if _is_dragging or _is_resizing: + return + + var popup := PopupMenu.new() + popup.name = "PresetPopup" + var names := ShaderPresets.get_preset_names() + for i in names.size(): + popup.add_item(names[i], i) + popup.position = Vector2i(get_screen_position()) + Vector2i(mouse_pos) + add_child(popup) + popup.popup() + + var on_select := func(id: int): + if id >= 0 and id < names.size(): + ShaderPresets.apply_preset(module, names[id]) + if is_instance_valid(popup): + popup.queue_free() + + popup.id_pressed.connect(on_select) + + +# -------------------------------------------------------------------------- +# Public API +# -------------------------------------------------------------------------- + +func place_module(module: Control, col: int, row: int, span_w: int = 1, span_h: int = 1) -> void: + _ensure_occupancy_for_span(col, row, span_w, span_h) + _set_module_grid_data(module, col, row, span_w, span_h) + _occupy_cells(module, col, row, span_w, span_h) + + # Add as direct child of the grid + add_child(module) + if module not in _module_list: + _module_list.append(module) + module_placed.emit(module, col, row) + _layout_cells() -func remove_module(col: int, row: int) -> void: - if not _is_valid_cell(col, row): +func remove_module(module: Control) -> void: + if not is_instance_valid(module): return - var module := _take_module_from_cell(col, row) - if module != null: - module_removed.emit(module) - module.queue_free() + var col: int = module.get_meta("grid_col", -1) + var row: int = module.get_meta("grid_row", -1) + var w: int = module.get_meta("grid_w", 1) + var h: int = module.get_meta("grid_h", 1) + _clear_occupancy(col, row, w, h) + _module_list.erase(module) + if module.get_parent() == self: + remove_child(module) + module_removed.emit(module) + _layout_cells() func get_grid_size() -> Vector2i: @@ -75,38 +172,259 @@ func get_cell_rect(col: int, row: int) -> Rect2: return Rect2(cell.position, cell.size) -func _is_valid_cell(col: int, row: int) -> bool: - return row >= 0 and row < _cells.size() and col >= 0 and col < _cells[row].size() +# -------------------------------------------------------------------------- +# Module grid-data helpers +# -------------------------------------------------------------------------- +func _set_module_grid_data(module: Control, col: int, row: int, w: int, h: int) -> void: + module.set_meta("grid_col", col) + module.set_meta("grid_row", row) + module.set_meta("grid_w", w) + module.set_meta("grid_h", h) + + +func _get_module_grid_data(module: Control) -> Dictionary: + return { + col = module.get_meta("grid_col", 0), + row = module.get_meta("grid_row", 0), + w = module.get_meta("grid_w", 1), + h = module.get_meta("grid_h", 1), + } + + +func _occupy_cells(module: Control, col: int, row: int, w: int, h: int) -> void: + for r in range(row, row + h): + for c in range(col, col + w): + if r < _grid.size() and c < _grid[r].size(): + _grid[r][c] = module + + +func _clear_occupancy(col: int, row: int, w: int, h: int) -> void: + for r in range(row, row + h): + for c in range(col, col + w): + if r < _grid.size() and c < _grid[r].size(): + _grid[r][c] = null + + +func _ensure_occupancy_for_span(col: int, row: int, w: int, h: int) -> void: + var needed_rows := row + h + var needed_cols := col + w + while _grid.size() < needed_rows: + _grid.append([]) + for r in range(_grid.size()): + while _grid[r].size() < needed_cols: + _grid[r].append(null) + if columns < needed_cols: + columns = needed_cols + if rows < needed_rows: + rows = needed_rows + + +func _is_valid_cell(col: int, row: int) -> bool: + return row >= 0 and row < _grid.size() and col >= 0 and col < _grid[row].size() + + +# -------------------------------------------------------------------------- +# Occupancy checks +# -------------------------------------------------------------------------- + +func _span_fits(col: int, row: int, w: int, h: int, exclude: Control = null) -> bool: + for r in range(row, row + h): + for c in range(col, col + w): + if r < 0 or c < 0: + return false + if r >= _grid.size() or c >= _grid[r].size(): + continue # beyond grid is OK — will expand + var occupant: Control = _grid[r][c] + if occupant != null and occupant != exclude: + return false + return true + + +# -------------------------------------------------------------------------- +# Cell / position helpers +# -------------------------------------------------------------------------- func _cell_at_position(pos: Vector2) -> Vector2i: - if columns == 0 or rows == 0: + if columns == 0 or rows == 0 or _cell_w <= 0 or _cell_h <= 0: return Vector2i(-1, -1) var inner_x := margin + cell_spacing var inner_y := margin + cell_spacing - var inner_w := size.x - margin * 2.0 - cell_spacing - var inner_h := size.y - margin * 2.0 - cell_spacing - var gap_x := cell_spacing * (columns - 1) - var gap_y := cell_spacing * (rows - 1) - var cell_w := (inner_w - gap_x) / columns - var cell_h := (inner_h - gap_y) / rows - - var col := int((pos.x - inner_x) / (cell_w + cell_spacing)) - var row := int((pos.y - inner_y) / (cell_h + cell_spacing)) + var col := int((pos.x - inner_x) / (_cell_w + cell_spacing)) + var row := int((pos.y - inner_y) / (_cell_h + cell_spacing)) col = clampi(col, 0, columns - 1) row = clampi(row, 0, rows - 1) - return Vector2i(col, row) -func _get_cell_module(col: int, row: int) -> Control: +func _get_module_at(col: int, row: int) -> Control: if not _is_valid_cell(col, row): return null - var cell: PanelContainer = _cells[row][col] - if cell.get_child_count() > 0: - return cell.get_child(0) - return null + return _grid[row][col] as Control + + +func _get_module_in_grid() -> Array[Control]: + var result: Array[Control] = [] + for r in range(rows): + for c in range(columns): + var mod := _get_module_at(c, r) as Control + if mod != null and mod not in result: + result.append(mod) + return result + + +# -------------------------------------------------------------------------- +# Hover cursor +# -------------------------------------------------------------------------- + +func _update_hover_cursor(mouse_pos: Vector2) -> void: + var edge := _detect_resize_edge(mouse_pos) + mouse_default_cursor_shape = Control.CURSOR_ARROW + if edge != 0: + if edge & (EDGE_TOP | EDGE_BOTTOM): + mouse_default_cursor_shape = Control.CURSOR_VSIZE + elif edge & (EDGE_LEFT | EDGE_RIGHT): + mouse_default_cursor_shape = Control.CURSOR_HSIZE + + +func _detect_resize_edge(mouse_pos: Vector2) -> int: + for module in _module_list: + if not is_instance_valid(module): + continue + if not module.visible: + continue + var d := _get_module_grid_data(module) + var rect := _get_module_rect(d.col, d.row, d.w, d.h) + if not rect.has_point(mouse_pos): + continue + + var edge := 0 + var dist_left: float = abs(mouse_pos.x - rect.position.x) + var dist_right: float = abs(mouse_pos.x - (rect.position.x + rect.size.x)) + var dist_top: float = abs(mouse_pos.y - rect.position.y) + var dist_bottom: float = abs(mouse_pos.y - (rect.position.y + rect.size.y)) + + if dist_left < RESIZE_THRESHOLD: + edge |= EDGE_LEFT + if dist_right < RESIZE_THRESHOLD: + edge |= EDGE_RIGHT + if dist_top < RESIZE_THRESHOLD: + edge |= EDGE_TOP + if dist_bottom < RESIZE_THRESHOLD: + edge |= EDGE_BOTTOM + + return edge + return 0 + + +# -------------------------------------------------------------------------- +# Resize +# -------------------------------------------------------------------------- + +func _try_begin_resize(mouse_pos: Vector2) -> bool: + var edge := _detect_resize_edge(mouse_pos) + if edge == 0: + return false + + # Find which module + for module in _module_list: + if not is_instance_valid(module): + continue + var d := _get_module_grid_data(module) + var rect := _get_module_rect(d.col, d.row, d.w, d.h) + if rect.has_point(mouse_pos): + _is_resizing = true + _resize_module = module + _resize_edge = edge + _resize_origin_col = d.col + _resize_origin_row = d.row + _resize_origin_w = d.w + _resize_origin_h = d.h + + # Create preview ghost + _resize_preview = ColorRect.new() + _resize_preview.color = Color(0.3, 0.5, 1.0, 0.2) + _resize_preview.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(_resize_preview) + _update_resize_preview(mouse_pos) + + # Show cells + _show_cells(true) + return true + + return false + + +func _update_resize(mouse_pos: Vector2) -> void: + if _resize_preview == null: + return + _update_resize_preview(mouse_pos) + + +func _update_resize_preview(mouse_pos: Vector2) -> void: + var d := _get_module_grid_data(_resize_module) + var cell_pos := _cell_at_position(mouse_pos) + if cell_pos.x < 0 or cell_pos.y < 0: + _resize_preview.visible = false + return + + var new_col: int = d.col + var new_row: int = d.row + var new_w: int = d.w + var new_h: int = d.h + + if _resize_edge & EDGE_LEFT: + new_col = min(cell_pos.x, d.col + d.w - 1) + new_w = d.col + d.w - new_col + if _resize_edge & EDGE_RIGHT: + new_w = max(1, cell_pos.x - d.col + 1) + if _resize_edge & EDGE_TOP: + new_row = min(cell_pos.y, d.row + d.h - 1) + new_h = d.row + d.h - new_row + if _resize_edge & EDGE_BOTTOM: + new_h = max(1, cell_pos.y - d.row + 1) + + # Clamp to grid + new_w = clampi(new_w, 1, columns - new_col) + new_h = clampi(new_h, 1, rows - new_row) + + # Check for overlap (excluding ourselves) + var fits := _span_fits(new_col, new_row, new_w, new_h, _resize_module) + + _resize_preview.visible = fits + if fits: + var prect := _get_module_rect(new_col, new_row, new_w, new_h) + _resize_preview.set_position(prect.position) + _resize_preview.set_size(prect.size) + + # Store proposed span for end_resize + _resize_origin_col = new_col + _resize_origin_row = new_row + _resize_origin_w = new_w + _resize_origin_h = new_h + + +func _end_resize(_mouse_pos: Vector2) -> void: + if _resize_preview != null: + _resize_preview.queue_free() + _resize_preview = null + + _show_cells(false) + + if _resize_module != null and is_instance_valid(_resize_module): + # Clear old occupancy + var d := _get_module_grid_data(_resize_module) + _clear_occupancy(d.col, d.row, d.w, d.h) + + # Apply new span + _set_module_grid_data(_resize_module, _resize_origin_col, _resize_origin_row, _resize_origin_w, _resize_origin_h) + _occupy_cells(_resize_module, _resize_origin_col, _resize_origin_row, _resize_origin_w, _resize_origin_h) + _layout_cells() + + _is_resizing = false + _resize_module = null + _resize_edge = 0 # -------------------------------------------------------------------------- @@ -120,43 +438,39 @@ func _begin_drag(mouse_pos: Vector2) -> void: var col := cell_pos.x var row := cell_pos.y - var module := _get_cell_module(col, row) + var module := _get_module_at(col, row) if module == null: return - _drag_source_col = col - _drag_source_row = row + var d := _get_module_grid_data(module) + + _drag_source_col = d.col + _drag_source_row = d.row - # Show grid cells so the user can see drop zones _show_cells(true) - # Calculate where the module sits in grid coordinates before reparenting. - # module.position is relative to the cell; cell.position is relative to us (the grid). - var cell_ref: PanelContainer = _cells[row][col] - var module_origin_in_grid := cell_ref.position + module.position - _drag_mouse_offset = mouse_pos - module_origin_in_grid + # Module position in grid space + var module_rect := _get_module_rect(d.col, d.row, d.w, d.h) + _drag_mouse_offset = mouse_pos - module_rect.position - _drag_module = _take_module_from_cell(col, row) + # Reparent module to top of tree (already a child if direct child of grid) + _drag_module = module + move_child(_drag_module, get_child_count()) _is_dragging = true - # Reparent to grid so it renders above all cells - add_child(_drag_module) _drag_module.position = mouse_pos - _drag_mouse_offset - - # Make the dragged module semi-transparent _drag_module.modulate = Color(1, 1, 1, 0.7) - - # Clear hover state _clear_hover() + # Remove from occupancy temporarily + _clear_occupancy(d.col, d.row, d.w, d.h) + func _update_drag(mouse_pos: Vector2) -> void: if _drag_module == null: return - _drag_module.position = mouse_pos - _drag_mouse_offset - # Update hover highlight var cell_pos := _cell_at_position(mouse_pos) if cell_pos.x != _hover_col or cell_pos.y != _hover_row: _clear_hover() @@ -174,11 +488,12 @@ func _end_drag(mouse_pos: Vector2) -> void: _show_cells(false) _drag_module.modulate = Color(1, 1, 1, 1) + var d := _get_module_grid_data(_drag_module) var cell_pos := _cell_at_position(mouse_pos) var target_col := cell_pos.x var target_row := cell_pos.y - # Check whether the mouse is actually inside the cell bounds (not in the gap) + # Check if mouse is actually inside the cell bounds var on_cell := false if _is_valid_cell(target_col, target_row): var cell_ref: PanelContainer = _cells[target_row][target_col] @@ -186,42 +501,50 @@ func _end_drag(mouse_pos: Vector2) -> void: if not on_cell or not _is_valid_cell(target_col, target_row): _drop_to_source() + _layout_cells() _finish_drag() return - # Remove from its temporary parent (the grid) - _drag_module.get_parent().remove_child(_drag_module) - - var existing := _get_cell_module(target_col, target_row) + # Check if target is occupied + var existing := _get_module_at(target_col, target_row) if existing != null: - # SWAP: put existing in source, dragged module in target - existing.get_parent().remove_child(existing) - var was_source_valid := _is_valid_cell(_drag_source_col, _drag_source_row) - if was_source_valid: - _place_module_in_cell(existing, _drag_source_col, _drag_source_row) - module_placed.emit(existing, _drag_source_col, _drag_source_row) - else: - existing.queue_free() - - _place_module_in_cell(_drag_module, target_col, target_row) - module_placed.emit(_drag_module, target_col, target_row) + var ed := _get_module_grid_data(existing) + _swap_modules(_drag_module, d.col, d.row, d.w, d.h, existing, ed) + var ad := _get_module_grid_data(_drag_module) + module_placed.emit(_drag_module, ad.col, ad.row) + module_placed.emit(existing, d.col, d.row) else: - # Place in the empty cell - _place_module_in_cell(_drag_module, target_col, target_row) + # Place in new position + _clear_occupancy(d.col, d.row, d.w, d.h) + _set_module_grid_data(_drag_module, target_col, target_row, d.w, d.h) + if _span_fits(target_col, target_row, d.w, d.h, _drag_module): + _occupy_cells(_drag_module, target_col, target_row, d.w, d.h) + else: + _occupy_cells(_drag_module, d.col, d.row, d.w, d.h) + _set_module_grid_data(_drag_module, d.col, d.row, d.w, d.h) module_placed.emit(_drag_module, target_col, target_row) + _layout_cells() _finish_drag() +func _swap_modules(a: Control, a_col: int, a_row: int, a_w: int, a_h: int, + b: Control, bd: Dictionary) -> void: + _clear_occupancy(a_col, a_row, a_w, a_h) + _clear_occupancy(bd.col, bd.row, bd.w, bd.h) + + _set_module_grid_data(a, bd.col, bd.row, bd.w, bd.h) + _set_module_grid_data(b, a_col, a_row, a_w, a_h) + + _occupy_cells(a, bd.col, bd.row, bd.w, bd.h) + _occupy_cells(b, a_col, a_row, a_w, a_h) + + func _drop_to_source() -> void: if _drag_module == null: return - if _is_valid_cell(_drag_source_col, _drag_source_row): - _drag_module.get_parent().remove_child(_drag_module) - _place_module_in_cell(_drag_module, _drag_source_col, _drag_source_row) - module_placed.emit(_drag_module, _drag_source_col, _drag_source_row) - else: - _drag_module.queue_free() + var d := _get_module_grid_data(_drag_module) + _occupy_cells(_drag_module, d.col, d.row, d.w, d.h) func _finish_drag() -> void: @@ -231,6 +554,18 @@ func _finish_drag() -> void: _drag_source_row = -1 +# -------------------------------------------------------------------------- +# Module rect in grid coordinates +# -------------------------------------------------------------------------- + +func _get_module_rect(col: int, row: int, w: int, h: int) -> Rect2: + var x := margin + cell_spacing + col * (_cell_w + cell_spacing) + var y := margin + cell_spacing + row * (_cell_h + cell_spacing) + var mw := w * _cell_w + (w - 1) * cell_spacing + var mh := h * _cell_h + (h - 1) * cell_spacing + return Rect2(x, y, mw, mh) + + # -------------------------------------------------------------------------- # Cell highlights # -------------------------------------------------------------------------- @@ -255,118 +590,22 @@ func _highlight_cell(col: int, row: int, highlighted: bool) -> void: style.border_color = Color(0.25, 0.25, 0.35, 1.0) -# -------------------------------------------------------------------------- -# Module placement helpers -# -------------------------------------------------------------------------- - -func _take_module_from_cell(col: int, row: int) -> Control: - if not _is_valid_cell(col, row): - return null - var cell: PanelContainer = _cells[row][col] - for child in cell.get_children(): - cell.remove_child(child) - _module_positions.erase("%d,%d" % [col, row]) - return child - return null - - -func _place_module_in_cell(module: Control, col: int, row: int) -> void: - if not _is_valid_cell(col, row): - return - var cell: PanelContainer = _cells[row][col] - # Clear existing children - for child in cell.get_children(): - cell.remove_child(child) - if is_instance_valid(child): - child.queue_free() - cell.add_child(module) - _module_positions["%d,%d" % [col, row]] = module - - -func _set_cell_child(cell: PanelContainer, child: Control) -> void: - for existing in cell.get_children(): - cell.remove_child(existing) - if is_instance_valid(existing): - existing.queue_free() - cell.add_child(child) - - -# -------------------------------------------------------------------------- -# List-reflow helpers -# -------------------------------------------------------------------------- - -# Returns all modules in the grid, in row-major order (left-to-right, top-to-bottom). -func _collect_all_modules() -> Array[Control]: - var result: Array[Control] = [] - for r in range(rows): - for c in range(columns): - var mod := _get_cell_module(c, r) - if mod != null: - result.append(mod) - return result - - -# Clears every cell, then places the given modules in row-major order, -# adding extra rows as needed to fit them all. -func _reflow_modules(modules: Array[Control]) -> void: - # Remove all modules from cells - for r in range(rows): - for c in range(columns): - _take_module_from_cell(c, r) - - if columns == 0: - return - - # Ensure enough rows exist - var needed := maxi(rows, ceili(float(modules.size()) / float(columns))) - while _cells.size() < needed: - _add_row() - rows = _cells.size() - - # Place modules sequentially - for i in range(modules.size()): - var c := i % columns - var r := floori(i / columns) - _place_module_in_cell(modules[i], c, r) - module_placed.emit(modules[i], c, r) - - -func _add_row() -> void: - var new_row := _cells.size() - _cells.append([]) - for col in range(columns): - var cell := PanelContainer.new() - cell.name = "Cell_%d_%d" % [col, new_row] - cell.mouse_filter = Control.MOUSE_FILTER_PASS - _style_cell(cell, col, new_row) - add_child(cell) - _cells[new_row].append(cell) - rows = _cells.size() - _layout_cells() - - -func _count_modules_in_grid() -> int: - var count := 0 - for r in range(rows): - for c in range(columns): - if _get_cell_module(c, r) != null: - count += 1 - return count - - # -------------------------------------------------------------------------- # Grid rebuild # -------------------------------------------------------------------------- func _rebuild_grid() -> void: - # Cancel any active drag if _is_dragging: _cancel_drag() + if _is_resizing: + _end_resize(Vector2.ZERO) + # Clean up orphaned popups from preset selection + for child in get_children(): + if child is PopupMenu: + child.queue_free() var avail := size - Vector2(margin * 2.0, margin * 2.0) - # If layout hasn't resolved yet (size too small), build a minimum 1x1 grid - # so modules can be placed. The resized signal will recalculate later. if avail.x < cell_min_size.x or avail.y < cell_min_size.y: if _cells.size() > 0: _layout_cells() @@ -387,8 +626,7 @@ func _rebuild_grid() -> void: var new_cols := maxi(1, int(avail.x / (cell_min_size.x + cell_spacing))) - # Ensure enough rows for all current modules after column change - var module_count := _count_modules_in_grid() + var module_count := _module_list.size() var min_rows := maxi(1, ceili(float(module_count) / float(new_cols))) var avail_rows := maxi(1, int(avail.y / (cell_min_size.y + cell_spacing))) var new_rows := maxi(avail_rows, min_rows) @@ -410,13 +648,8 @@ func _rebuild_grid() -> void: func _cancel_drag() -> void: if _drag_module != null: _drag_module.modulate = Color(1, 1, 1, 1) - if _is_valid_cell(_drag_source_col, _drag_source_row): - var parent := _drag_module.get_parent() - if parent != null: - parent.remove_child(_drag_module) - _place_module_in_cell(_drag_module, _drag_source_col, _drag_source_row) - else: - _drag_module.queue_free() + var d := _get_module_grid_data(_drag_module) + _occupy_cells(_drag_module, d.col, d.row, d.w, d.h) _show_cells(false) _clear_hover() _drag_module = null @@ -425,6 +658,10 @@ func _cancel_drag() -> void: _drag_source_row = -1 +# -------------------------------------------------------------------------- +# Cell building / teardown / styling +# -------------------------------------------------------------------------- + func _build_cells() -> void: _cells.clear() for row in range(rows): @@ -439,7 +676,6 @@ func _build_cells() -> void: func _style_cell(cell: PanelContainer, col: int, row: int) -> void: - # Hidden style — used when not dragging var hidden := StyleBoxFlat.new() hidden.bg_color = Color.TRANSPARENT hidden.border_width_left = 1 @@ -452,7 +688,6 @@ func _style_cell(cell: PanelContainer, col: int, row: int) -> void: hidden.corner_radius_bottom_right = 12 hidden.corner_radius_bottom_left = 12 - # Visible style — used during drag, may be modified by highlights var visible := StyleBoxFlat.new() visible.bg_color = Color(0.14, 0.14, 0.18, 1.0) visible.border_width_left = 1 @@ -486,62 +721,22 @@ func _show_cells(show: bool) -> void: func _teardown_cells() -> void: _cell_styles.clear() for child in get_children(): - remove_child(child) - if is_instance_valid(child): - child.queue_free() + # Keep modules — only remove cells + if child is PanelContainer and child.name.begins_with("Cell_"): + remove_child(child) + if is_instance_valid(child): + child.queue_free() _cells.clear() -func _layout_cells() -> void: - if rows == 0 or columns == 0: - return +# -------------------------------------------------------------------------- +# Layout +# -------------------------------------------------------------------------- - var inner_w := maxf(0.0, size.x - margin * 2.0 - cell_spacing) - var inner_h := maxf(0.0, size.y - margin * 2.0 - cell_spacing) - var gap_x := cell_spacing * (columns - 1) - var gap_y := cell_spacing * (rows - 1) - var cell_w := maxf(1.0, (inner_w - gap_x) / columns) - var cell_h := maxf(1.0, (inner_h - gap_y) / rows) - - for row in range(rows): - for col in range(columns): - var cell: PanelContainer = _cells[row][col] - if not is_instance_valid(cell): - continue - var x := margin + cell_spacing + col * (cell_w + cell_spacing) - var y := margin + cell_spacing + row * (cell_h + cell_spacing) - cell.set_position(Vector2(x, y)) - cell.set_size(Vector2(cell_w, cell_h)) - - -func _save_modules() -> void: - _module_positions.clear() - for row in range(_cells.size()): - for col in range(_cells[row].size()): - var cell: PanelContainer = _cells[row][col] - if cell.get_child_count() > 0: - var mod := cell.get_child(0) - cell.remove_child(mod) - _module_positions["%d,%d" % [col, row]] = mod - - -func _restore_modules() -> void: - # Collect modules in insertion order (row-major from _save_modules) - var module_list: Array[Control] = [] - for key in _module_positions: - module_list.append(_module_positions[key]) - - _module_positions.clear() - - if module_list.is_empty(): - return - - # Reflow onto the new grid dimensions - var module_count := module_list.size() - var needed := maxi(rows, ceili(float(module_count) / float(columns))) - while _cells.size() < needed: - var new_row := _cells.size() +func _ensure_cells_match_grid() -> void: + while _cells.size() < rows: _cells.append([]) + var new_row := _cells.size() - 1 for col in range(columns): var cell := PanelContainer.new() cell.name = "Cell_%d_%d" % [col, new_row] @@ -549,10 +744,140 @@ func _restore_modules() -> void: _style_cell(cell, col, new_row) add_child(cell) _cells[new_row].append(cell) - rows = _cells.size() - for i in range(module_list.size()): - var c := i % columns - var r := floori(i / columns) - if _is_valid_cell(c, r): - _place_module_in_cell(module_list[i], c, r) + +func _layout_cells() -> void: + if rows == 0 or columns == 0: + return + _ensure_cells_match_grid() + + var inner_w := maxf(0.0, size.x - margin * 2.0 - cell_spacing) + var inner_h := maxf(0.0, size.y - margin * 2.0 - cell_spacing) + var gap_x := cell_spacing * (columns - 1) + var gap_y := cell_spacing * (rows - 1) + _cell_w = maxf(1.0, (inner_w - gap_x) / columns) + _cell_h = maxf(1.0, (inner_h - gap_y) / rows) + + # Position cells + for row in range(rows): + for col in range(columns): + if row >= _cells.size() or col >= _cells[row].size(): + continue + var cell: PanelContainer = _cells[row][col] + if not is_instance_valid(cell): + continue + var x := margin + cell_spacing + col * (_cell_w + cell_spacing) + var y := margin + cell_spacing + row * (_cell_h + cell_spacing) + cell.set_position(Vector2(x, y)) + cell.set_size(Vector2(_cell_w, _cell_h)) + + # Position modules + for module in _module_list: + if not is_instance_valid(module): + continue + var d := _get_module_grid_data(module) + var rect := _get_module_rect(d.col, d.row, d.w, d.h) + module.set_position(rect.position) + module.set_size(rect.size) + + +# -------------------------------------------------------------------------- +# Save / restore (used during grid rebuild) +# -------------------------------------------------------------------------- + +func _save_modules() -> void: + # Modules already have their grid data in metadata + pass + + +func _restore_modules() -> void: + # Rebuild grid + _grid.clear() + for r in range(rows): + _grid.append([]) + for c in range(columns): + _grid[r].append(null) + + if _module_list.is_empty(): + return + + # Ensure enough rows + while _grid.size() < rows: + _grid.append([]) + for c in range(columns): + _grid[_grid.size() - 1].append(null) + + # Re-place modules row-major + var reordered: Array[Control] = [] + for r in range(rows): + for c in range(columns): + for module in _module_list: + if module in reordered: + continue + var d := _get_module_grid_data(module) + if d.col == c and d.row == r: + if _span_fits(c, r, d.w, d.h, null): + # Ensure grid is big enough + _ensure_occupancy_for_span(c, r, d.w, d.h) + _occupy_cells(module, c, r, d.w, d.h) + reordered.append(module) + break + + # Add any modules that didn't find their spot + for module in _module_list: + if module not in reordered: + reordered.append(module) + + _module_list = reordered + + # Place orphaned modules + for module in _module_list: + if module.get_parent() != self: + add_child(module) + # Check if it's in the grid + var d := _get_module_grid_data(module) + if _get_module_at(d.col, d.row) != module: + # Find a spot + _find_and_place(module) + + # Rebuild columns/rows from grid occupancy + _ensure_occupancy_from_grid() + _layout_cells() + + +func _ensure_occupancy_from_grid() -> void: + var max_c := 0 + var max_r := 0 + for r in range(_grid.size()): + for c in range(_grid[r].size()): + if _grid[r][c] != null: + max_c = maxi(max_c, c + 1) + max_r = maxi(max_r, r + 1) + columns = maxi(columns, max_c) + rows = maxi(rows, max_r) + + +func _find_and_place(module: Control) -> void: + var d := _get_module_grid_data(module) + # Try the desired position first + if _span_fits(d.col, d.row, d.w, d.h, null): + _ensure_occupancy_for_span(d.col, d.row, d.w, d.h) + _occupy_cells(module, d.col, d.row, d.w, d.h) + return + + # Find first available position + for r in range(rows): + for c in range(columns): + if _span_fits(c, r, d.w, d.h, null): + _set_module_grid_data(module, c, r, d.w, d.h) + _ensure_occupancy_for_span(c, r, d.w, d.h) + _occupy_cells(module, c, r, d.w, d.h) + return + + # No room — expand grid + var new_col := columns + var new_row := 0 + _ensure_occupancy_for_span(new_col, new_row, d.w, d.h) + columns = new_col + d.w + _set_module_grid_data(module, new_col, new_row, d.w, d.h) + _occupy_cells(module, new_col, new_row, d.w, d.h) diff --git a/scripts/shader_presets.gd b/scripts/shader_presets.gd new file mode 100644 index 0000000..7d62f8f --- /dev/null +++ b/scripts/shader_presets.gd @@ -0,0 +1,128 @@ +class_name ShaderPresets +# Collection of vial_fill shader presets. +# Each preset is a Dictionary with a "name" key and shader-parameter key/value pairs. +# Use apply_preset(module, preset_name) to set parameters on a module's ShaderMaterial. + +static var presets: Array[Dictionary] = [ + { + "name": "Vivid Vial", + "liquid_color": Color(0.2, 0.5, 0.8, 1.0), + "wave_amp": 0.012, + "wave_freq": 4.0, + "wave_strength": 0.05, + "ripple_speed": 1.0, + "edge_color": Color(0.3, 0.6, 1.0, 1.0), + "glow_intensity": 1.5, + "edge_pulse": 1.0, + "noise_scale": 1.0, + "swirl_strength": 0.5, + "hue_shift_speed": 0.0, + }, + { + "name": "Emerald Deep", + "liquid_color": Color(0.1, 0.6, 0.3, 1.0), + "wave_amp": 0.015, + "wave_freq": 5.0, + "wave_strength": 0.08, + "ripple_speed": 0.8, + "edge_color": Color(0.2, 0.8, 0.5, 1.0), + "glow_intensity": 1.2, + "edge_pulse": 0.8, + "noise_scale": 1.2, + "swirl_strength": 0.3, + "hue_shift_speed": 0.0, + }, + { + "name": "Lava Flow", + "liquid_color": Color(0.8, 0.2, 0.05, 1.0), + "wave_amp": 0.025, + "wave_freq": 3.0, + "wave_strength": 0.15, + "ripple_speed": 0.5, + "edge_color": Color(1.0, 0.5, 0.1, 1.0), + "glow_intensity": 2.0, + "edge_pulse": 1.5, + "noise_scale": 0.8, + "swirl_strength": 1.0, + "hue_shift_speed": 0.0, + }, + { + "name": "Neon Dream", + "liquid_color": Color(0.6, 0.2, 0.9, 1.0), + "wave_amp": 0.01, + "wave_freq": 6.0, + "wave_strength": 0.06, + "ripple_speed": 2.0, + "edge_color": Color(0.7, 0.3, 1.0, 1.0), + "glow_intensity": 2.5, + "edge_pulse": 1.2, + "noise_scale": 1.5, + "swirl_strength": 0.8, + "hue_shift_speed": 0.0, + }, + { + "name": "Deep Purple", + "liquid_color": Color(0.3, 0.1, 0.5, 1.0), + "wave_amp": 0.008, + "wave_freq": 3.5, + "wave_strength": 0.04, + "ripple_speed": 0.6, + "edge_color": Color(0.5, 0.2, 0.8, 1.0), + "glow_intensity": 1.0, + "edge_pulse": 0.7, + "noise_scale": 1.0, + "swirl_strength": 0.4, + "hue_shift_speed": 0.0, + }, + { + "name": "Rainbow Swirl", + "liquid_color": Color(0.3, 0.5, 0.8, 1.0), + "wave_amp": 0.012, + "wave_freq": 4.0, + "wave_strength": 0.1, + "ripple_speed": 1.5, + "edge_color": Color(0.5, 0.7, 1.0, 1.0), + "glow_intensity": 1.5, + "edge_pulse": 1.0, + "noise_scale": 1.5, + "swirl_strength": 1.2, + "hue_shift_speed": 1.5, + }, + { + "name": "Frostbite", + "liquid_color": Color(0.5, 0.8, 1.0, 1.0), + "wave_amp": 0.006, + "wave_freq": 7.0, + "wave_strength": 0.03, + "ripple_speed": 2.5, + "edge_color": Color(0.7, 0.9, 1.0, 1.0), + "glow_intensity": 0.8, + "edge_pulse": 0.5, + "noise_scale": 2.0, + "swirl_strength": 0.2, + "hue_shift_speed": 0.0, + }, +] + + +static func get_preset_names() -> PackedStringArray: + var names: PackedStringArray = [] + for p in presets: + names.append(p["name"]) + return names + + +static func apply_preset(module: Control, preset_name: String) -> void: + var vial_fill: ColorRect = module.find_child("VialFill", true, false) + if vial_fill == null: + return + var mat := vial_fill.material as ShaderMaterial + if mat == null: + return + for p in presets: + if p["name"] == preset_name: + for key in p: + if key == "name": + continue + mat.set_shader_parameter(key, p[key]) + return diff --git a/shaders/vial_fill.gdshader b/shaders/vial_fill.gdshader index 2d34a89..4c04289 100644 --- a/shaders/vial_fill.gdshader +++ b/shaders/vial_fill.gdshader @@ -128,15 +128,31 @@ void fragment() { // tiny sparkle effect_rgb += 0.03 * vec3(sin(t * 3.0), cos(t * 2.0), noise_val * 0.1); + // --- 3D water surface --- + float d_surf = UV.y - fill_line; // positive = below surface + float d_abs = abs(d_surf); + + // subsurface light scatter — brightest just below surface, fades with depth + float subsurface = exp(-d_surf * 25.0) * step(0.0, d_surf); + effect_rgb *= 1.0 + subsurface * 0.25; + // --- composite --- vec3 col = bg_color.rgb; col = mix(col, effect_rgb, liquid); - // surface foam line (wider glow + bright core) - float foam_glow = smoothstep(fill_line - 0.02, fill_line, UV.y) - smoothstep(fill_line, fill_line + 0.004, UV.y); - col += foam_glow * vec3(0.5, 0.65, 0.9) * 0.6 * liquid; - float foam_core = smoothstep(fill_line - 0.006, fill_line, UV.y) - smoothstep(fill_line, fill_line + 0.001, UV.y); - col += foam_core * vec3(0.7, 0.85, 1.0) * liquid; + // foam glow — smooth double-sided falloff (gaussian-like) + float foam_glow = exp(-d_abs * 70.0); + float foam_core = exp(-d_abs * 350.0); + vec3 foam_tint = vec3(0.6, 0.75, 0.95); + col += foam_glow * foam_tint * 0.5; + col += foam_core * vec3(0.75, 0.9, 1.0) * 0.6; + + // wave-slope highlight — brightens at wave crests facing the viewer + float wave_slope = (cos(UV.x * wf + TIME * 5.0) * wf + + cos(UV.x * wf * 1.7 + TIME * 7.0 + 1.3) * wf * 1.7 * 0.5 + + cos(UV.x * wf * 0.6 + TIME * 2.5 + 2.9) * wf * 0.6 * 0.3) * edge_damp / 1.8 * wave_amp; + float slope_highlight = max(0.0, wave_slope) * exp(-d_abs * 200.0); + col += slope_highlight * vec3(0.6, 0.8, 1.0) * 0.4; // border col = mix(col, border_color.rgb, outer - inner);