Files
statusbartweak/python/positions.py
T

874 lines
30 KiB
Python

from dataclasses import dataclass
from layout_item import Box, LayoutItem
SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 160
@dataclass
class Scenario:
cutout: bool = True
camera_x: int = 770
camera_width: int = 100
clock_pos: str = "left"
chip_pos: tuple[str, str] = ("middle", "right")
clock_vertical: int = 20
chip_vertical: int = 90
notifs_count: int = 2
status_count: int = 2
dummy_count: int = 3
notifs_pos: tuple[str | tuple[str, str], ...] = ("left",)
status_pos: tuple[str | tuple[str, str], ...] = ("right",)
dummy_pos: tuple[str | tuple[str, str], ...] = (("middle", "left"),)
notifs_vertical: tuple[int, ...] = (0, 0, 0)
status_vertical: tuple[int, ...] = (0, 100)
dummy_vertical: tuple[int, ...] = (50, 0, 20)
notifs_icons: int = 20
status_icons: int = 20
notifs_icon_width: int = 50
status_icon_widths: tuple[int, ...] = (30, 30, 30, 30)
status_icon_height: int = 30
container_spacing: int = 10
order: tuple[str, ...] = ("Clock", "Chip", "Status", "Notifs", "Dummy")
shrink_order: tuple[str, ...] = ("Notifs", "Status", "Chip")
@dataclass
class PathState:
boxes: list[Box]
gaps: float = 0
def added(self, box):
gap = self.gaps
previous = self.boxes[-1]
if previous.item is not box.item:
actual_gap = box.get_left() - previous.get_right()
required_gap = previous.item.spacing_to(box.item)
gap += max(0, actual_gap - required_gap)
return PathState(self.boxes + [box], gap)
def get_right(self):
return self.boxes[-1].get_right()
def position_containers(scenario=None):
scenario = scenario or Scenario()
# 1. Build the test items from explicit scenario settings.
items = make_test_items(scenario)
regions = make_regions(scenario)
# 2. Assign each item to a cutout-separated region and build collision edges.
place_into_regions(regions, items)
for region in regions:
collision_tree(region["items"])
print_regions(regions)
shrink_priority = [
item
for kind in scenario.shrink_order
for item in items
if item.kind == kind
]
# 3. Build constraints for both regions first. Shared icon pools can span
# regions, so truncation must make decisions with all paths visible at once.
constraints = []
for region in regions:
region_shrink_priority = [item for item in shrink_priority if item in region["items"]]
constraints.extend(collect_constraints(region, region_shrink_priority))
# 4. Run one shared truncation/allocation pass over both regions.
if constraints:
truncate(constraints, shrink_priority, items)
# 5. Restack the final truncated items into their selected positions.
for region in regions:
final_stack(region)
if scenario.cutout:
return items + [make_camera_item(scenario)]
return items
def make_test_items(scenario):
items = []
clock = LayoutItem.multi_box(
"Clock",
"Clock",
(120, 180, 255),
scenario.clock_pos,
[
Box(0, 0, 240, 40),
Box(80, -20, 30, 30),
],
xy=(0, scenario.clock_vertical),
)
clock.spacing = scenario.container_spacing
items.append(clock)
chip = LayoutItem.fixed(
"Chip",
"Chip",
(220, 140, 255),
scenario.chip_pos,
400,
40,
xy=(0, scenario.chip_vertical),
)
chip.spacing = scenario.container_spacing
items.append(chip)
for i in range(scenario.notifs_count):
items.append(LayoutItem.icon_strip(
f"Notifs{i}",
"Notifs",
(120, 255, 160),
repeated(scenario.notifs_pos, i),
"notifications",
scenario.notifs_icons,
scenario.notifs_icon_width,
xy=(0, repeated(scenario.notifs_vertical, i)),
icon_width=scenario.notifs_icon_width,
spacing=scenario.container_spacing,
))
status_widths = repeated_widths(scenario.status_icon_widths, scenario.status_icons)
for i in range(scenario.status_count):
items.append(LayoutItem.icon_strip(
f"Status{i}",
"Status",
(120, 255, 160),
repeated(scenario.status_pos, i),
"status",
scenario.status_icons,
scenario.status_icon_height,
xy=(0, repeated(scenario.status_vertical, i)),
icon_width=scenario.status_icon_height,
icon_widths=status_widths,
spacing=scenario.container_spacing,
))
for i in range(scenario.dummy_count):
dummy = LayoutItem.fixed(
f"Dummy{i}",
"Dummy",
(120, 255, 160),
repeated(scenario.dummy_pos, i),
100,
30,
xy=(0, repeated(scenario.dummy_vertical, i)),
)
dummy.spacing = scenario.container_spacing
items.append(dummy)
items = sort_items_by_kind_order(items, scenario.order)
configure_icon_groups(items)
return items
def repeated(values, index):
if not isinstance(values, tuple) or is_single_position(values):
return values
if len(values) == 0:
return None
return values[min(index, len(values) - 1)]
def is_single_position(value):
return (
isinstance(value, tuple)
and len(value) == 2
and value[0] == "middle"
and value[1] in ("left", "right")
)
def repeated_widths(values, count):
if count <= 0:
return []
widths = list(values)
if not widths:
widths = [40]
if len(widths) < count:
widths.extend([widths[-1]] * (count - len(widths)))
return widths[:count]
def configure_icon_groups(items):
groups = {}
for item in items:
if item.icon_group is not None:
groups.setdefault(item.icon_group, []).append(item)
for group_items in groups.values():
if not group_items:
continue
group_items[0].allow_zero = False
group_items[0].allow_dot = True
for item in group_items[1:]:
item.allow_zero = True
item.allow_dot = False
def make_regions(scenario):
if not scenario.cutout:
return [{"leftwall": 0, "rightwall": SCREEN_WIDTH, "items": []}]
camera_left = scenario.camera_x
camera_right = scenario.camera_x + scenario.camera_width
return [
{"leftwall": 0, "rightwall": camera_left, "items": []},
{"leftwall": camera_right, "rightwall": SCREEN_WIDTH, "items": []},
]
def make_camera_item(scenario):
return LayoutItem.fixed(
"Camera",
"Camera",
(0, 0, 0),
"camera",
scenario.camera_width,
SCREEN_HEIGHT,
xy=(scenario.camera_x, 0),
)
def collect_constraints(region, shrink_priority):
stackleft(region["items"], region["leftwall"])
paths = get_paths(region["items"], region["rightwall"])
if not paths:
return []
wall = region["rightwall"]
print("Does not fit!")
print(f" Wall is at {wall}")
path_constraints = []
best_path_by_constraint_key = {}
for path in paths:
shrinkables = unique_items(
box.item
for box in path.boxes
if box.item in shrink_priority
)
items_on_path = unique_items(box.item for box in path.boxes)
effective_right = path.get_right() - path.gaps
key = (
path.boxes[-1],
tuple(shrinkables),
tuple(items_on_path),
)
if key not in best_path_by_constraint_key:
best_path_by_constraint_key[key] = len(path_constraints)
path_constraints.append({
"path": path,
"end_box": path.boxes[-1],
"shrinkables": shrinkables,
"effective_right": effective_right,
"items_on_path": items_on_path,
})
continue
previous = path_constraints[best_path_by_constraint_key[key]]
if effective_right > previous["effective_right"]:
previous.update({
"path": path,
"effective_right": effective_right,
})
print(" Paths:")
for constraint in path_constraints:
print(f" {describe_path(constraint['path'])}")
# Convert unique overflowing paths into live constraints immediately. Most
# scenarios do not need relaxed walls, so avoid a second pass unless one is
# discovered.
final_constraints = []
constraints_by_end_box = {}
new_wall_by_end_box = {}
for path_constraint in path_constraints:
shrinkables = path_constraint["shrinkables"]
effective_right = path_constraint["effective_right"]
total_capacity = sum(truncation_phase_shrink_capacity(item) for item in shrinkables)
minimum_possible_right = effective_right - total_capacity
if minimum_possible_right > wall:
current = new_wall_by_end_box.get(path_constraint["end_box"], wall)
if minimum_possible_right > current:
new_wall_by_end_box[path_constraint["end_box"]] = minimum_possible_right
constraint = {
"end_box": path_constraint["end_box"],
"shrinkables": shrinkables,
"effective_right": effective_right,
"required": effective_right - wall,
"items_on_path": path_constraint["items_on_path"],
}
if constraint["required"] > 0:
final_constraints.append(constraint)
constraints_by_end_box.setdefault(path_constraint["end_box"], []).append(constraint)
print(" Constraint:")
print(f" end: {path_constraint['end_box']}")
print(f" shrinkables: {shrinkables}")
print(f" effective right: {effective_right}")
print(f" capacity: {total_capacity}")
# Relax only impossible paths. If this map is empty, final_constraints is
# already complete from the single pass above.
for end_box, relaxed_wall in new_wall_by_end_box.items():
for constraint in constraints_by_end_box.get(end_box, []):
constraint["required"] = constraint["effective_right"] - relaxed_wall
final_constraints = [
constraint
for constraint in final_constraints
if constraint["required"] > 0
]
for constraint in final_constraints:
print(" Final constraint:")
print(f" required: {constraint['required']}")
print(f" shrinkables: {constraint['shrinkables']}")
return final_constraints
def truncate(final_constraints, shrink_priority, all_items):
# The scenario order is an absorption order: earlier kinds are more willing
# to absorb shrinkage. Within a kind, lower-numbered copies are more
# important and are therefore processed before later copies.
#
# For each item, ask how much it must shrink if every still-unprocessed,
# more-willing item on the same path shrinks as far as it can. Already
# processed shrink is fixed and counted exactly.
# Icon groups are special: when one item in a shared pool is processed, immediately
# reallocate that pool so sibling containers stop reserving space for icons that
# have already been claimed by earlier containers.
processed = set()
execution_order = truncation_execution_order(shrink_priority)
absorption_order = truncation_absorption_order(shrink_priority)
absorption_index = {item: index for index, item in enumerate(absorption_order)}
kind_absorption_index = kind_order_index(shrink_priority)
icon_groups = grouped_icon_items(all_items)
remaining_icon_widths = {
group: list(group_items[0].icon_widths)
for group, group_items in icon_groups.items()
}
print(" Truncation order:")
print(f" shrink absorption order: {absorption_order}")
print(f" execution least-willing kind first, preserving copy order: {execution_order}")
for index, item in enumerate(execution_order):
print(f" Considering {item}:")
needed_for_item = 0
for constraint in final_constraints:
if constraint["required"] <= 0:
continue
shrinkables = constraint["shrinkables"]
if item not in shrinkables:
continue
assumed_absorbers = [
other
for other in shrinkables
if other not in processed
and other is not item
and can_absorb_for(item, other, kind_absorption_index, absorption_index)
]
assumed_absorber_capacity = sum(
contextual_shrink_capacity(item, other)
for other in assumed_absorbers
)
needed = (
constraint["required"]
- assumed_absorber_capacity
)
needed_for_item = max(needed_for_item, needed)
print(" Live constraint:")
print(f" required: {constraint['required']}")
print(f" shrinkables: {shrinkables}")
print(f" assumed absorbers: {assumed_absorbers}")
print(f" assumed absorber capacity: {assumed_absorber_capacity}")
print(f" needed from this item: {needed}")
amount = min(max(0, needed_for_item), contextual_shrink_capacity(item, item))
old_width = item.get_width()
if item.is_icon_item:
direct_actual = truncate_icon_item_without_dot(item, amount)
else:
direct_actual = item.truncate_at_least(amount)
actual = direct_actual
if direct_actual != 0:
adjust_constraints_for_item_width_delta(final_constraints, item, direct_actual)
if item.is_icon_item:
remaining_icon_widths[item.icon_group] = allocate_current_icon_item(
item,
icon_groups[item.icon_group],
remaining_icon_widths[item.icon_group],
constraints=final_constraints,
)
actual = old_width - item.get_width()
print(" Truncation:")
print(f" item: {item}")
print(f" requested: {amount}")
print(f" actual: {actual}")
print(f" final width: {item.get_width()}")
processed.add(item)
next_item = execution_order[index + 1] if index + 1 < len(execution_order) else None
if item.is_icon_item and (next_item is None or next_item.kind != item.kind):
finish_icon_group(
item.icon_group,
icon_groups[item.icon_group],
remaining_icon_widths[item.icon_group],
constraints=final_constraints,
)
def can_absorb_for(current, other, kind_absorption_index, absorption_index):
current_kind = kind_absorption_index.get(current.kind)
other_kind = kind_absorption_index.get(other.kind)
if current_kind is None or other_kind is None:
return False
if other_kind < current_kind:
return True
if other_kind > current_kind:
return False
return absorption_index[other] < absorption_index[current]
def contextual_shrink_capacity(current, other):
if current.icon_group is not None and current.icon_group == other.icon_group:
return other.get_width()
return other.can_shrink_by()
def truncation_phase_shrink_capacity(item):
if item.icon_group is not None:
return item.get_width()
return item.can_shrink_by()
def truncate_icon_item_without_dot(item, amount):
old_allow_zero = item.allow_zero
old_allow_dot = item.allow_dot
item.allow_zero = True
item.allow_dot = False
try:
return item.truncate_at_least(amount)
finally:
item.allow_zero = old_allow_zero
item.allow_dot = old_allow_dot
def truncation_execution_order(shrink_priority):
by_kind = {}
for item in shrink_priority:
by_kind.setdefault(item.kind, []).append(item)
result = []
for kind in reversed(list(by_kind.keys())):
result.extend(by_kind[kind])
return result
def truncation_absorption_order(shrink_priority):
by_kind = {}
for item in shrink_priority:
by_kind.setdefault(item.kind, []).append(item)
result = []
for items in by_kind.values():
result.extend(reversed(items))
return result
def grouped_icon_items(items):
grouped_items = {}
for item in items:
if item.icon_group is not None:
grouped_items.setdefault(item.icon_group, []).append(item)
for group_items in grouped_items.values():
group_items.sort(key=item_copy_index)
return grouped_items
def allocate_current_icon_item(item, group_items, remaining_widths, constraints):
print(f"Icon distribution while truncating {item}:")
print(f" Group {item.icon_group}:")
print(f" remaining before: {remaining_widths}")
old_widths = {group_item: group_item.get_width() for group_item in group_items}
set_icon_item_pool(item, remaining_widths)
remaining_after_item = remaining_widths[item.visible_icons:]
item_index = group_items.index(item)
for later_item in group_items[item_index + 1:]:
set_icon_item_pool(later_item, remaining_after_item)
for group_item in group_items:
delta = old_widths[group_item] - group_item.get_width()
print(f" Item {group_item}:")
print(f" available before: {group_item.pool_icon_widths}")
print(f" visible widths: {group_item.visible_icon_widths}")
print(f" dot: {group_item.dot}")
print(f" width: {old_widths[group_item]} -> {group_item.get_width()}")
print(f" remaining after item: {remaining_after_item}")
if delta != 0:
adjust_constraints_for_item_width_delta(constraints, group_item, delta)
return remaining_after_item
def finish_icon_group(group, group_items, remaining_widths, constraints):
print(f"Final icon distribution while finishing {group_items[0].kind}:")
print(f" Group {group}:")
print(f" remaining before dot: {remaining_widths}")
old_widths = {item: item.get_width() for item in group_items}
if remaining_widths:
growth_rooms = {
item: expansion_room_for_constraints(item, constraints)
for item in group_items
}
place_final_dot(group_items, growth_rooms)
for item in group_items:
delta = old_widths[item] - item.get_width()
print(f" Item {item}:")
print(f" available before: {item.pool_icon_widths}")
print(f" visible widths: {item.visible_icon_widths}")
print(f" dot: {item.dot}")
print(f" width: {old_widths[item]} -> {item.get_width()}")
print(f" remaining after group: {remaining_widths}")
if delta != 0:
adjust_constraints_for_item_width_delta(constraints, item, delta)
def set_icon_item_pool(item, widths):
old_allow_zero = item.allow_zero
old_allow_dot = item.allow_dot
item.allow_zero = True
item.allow_dot = False
try:
item.width_limit = item.get_width()
item.set_pool_icons(widths)
finally:
item.allow_zero = old_allow_zero
item.allow_dot = old_allow_dot
def place_final_dot(group_items, growth_rooms):
print(" Final dot placement:")
capacities = {
item: item.get_width() + growth_rooms.get(item, 0)
for item in group_items
}
while True:
last_icons = last_item_with_icons(group_items)
if last_icons is None:
last_icons = 0
for item in group_items[last_icons:]:
dot_width = dot_width_for_final_placement(group_items, item)
capacity = capacities[item]
visible_widths = list(item.visible_icon_widths)
print(f" trying {item}: capacity={capacity}, growth={growth_rooms.get(item, 0)}, own_limit={item.width_limit}, icons={visible_widths}, dot={dot_width}")
if sum(visible_widths) + dot_width <= capacity:
item.visible_icon_widths = visible_widths
item.visible_icons = len(visible_widths)
item.dot = True
item._apply_icon_width(sum(visible_widths) + dot_width)
item.width_limit = item.get_width()
print(f" placed dot in {item}")
return True
item = group_items[last_icons]
if not item.visible_icon_widths:
break
removed = item.visible_icon_widths.pop()
item.visible_icons = len(item.visible_icon_widths)
item._apply_icon_width(sum(item.visible_icon_widths))
item.width_limit = item.get_width()
print(f" sacrificed icon width {removed} from {item}")
print(" no item can fit the dot")
return False
def dot_width_for_final_placement(group_items, item):
dot_width = item._dot_width()
dot_side = item._dot_side()
index = group_items.index(item)
if dot_side == "right" and all(other.get_width() <= 0 for other in group_items[index + 1:]):
return min(dot_width, item.dot_width - item.dot_width / 3)
if dot_side == "left" and all(other.get_width() <= 0 for other in group_items[:index]):
return min(dot_width, item.dot_width - item.dot_width / 3)
return dot_width
def last_item_with_icons(group_items):
for index in range(len(group_items) - 1, -1, -1):
if group_items[index].visible_icons > 0:
return index
return None
def expansion_room_for_constraints(item, constraints):
relevant = [
constraint
for constraint in constraints
if item in constraint["items_on_path"]
]
if not relevant:
return float("inf")
return max(0, min(-constraint["required"] for constraint in relevant))
def adjust_constraints_for_item_width_delta(constraints, item, delta):
for constraint in constraints:
if item in constraint["items_on_path"]:
constraint["required"] -= delta
def live_constraints(constraints):
return [constraint for constraint in constraints if constraint["required"] > 0]
def item_copy_index(item):
suffix = ""
for char in reversed(item.name):
if not char.isdigit():
break
suffix = char + suffix
return int(suffix) if suffix else 0
def get_paths(items, wall):
boxes = [box for item in items for box in item.boxes]
paths = []
def can_start(box):
if box.collision_left:
return False
if len(box.item.boxes) > 1 and box.get_left() != box.item.get_left():
return False
return True
def can_end(box):
if box.collision_right:
return False
if box.get_right() <= wall:
return False
if len(box.item.boxes) > 1 and box.get_right() != box.item.get_right():
return False
return True
def can_item_hop(path, box):
return len(box.item.boxes) > 1 and sum(1 for b in path.boxes if b.item is box.item) < 2
def iter_next_boxes(path):
box = path.boxes[-1]
yield from box.collision_right
if can_item_hop(path, box):
for other in box.item.boxes:
if other is not box:
yield other
def dfs(path):
if can_end(path.boxes[-1]):
paths.append(path)
seen = set(path.boxes)
for next_box in iter_next_boxes(path):
if next_box in seen:
continue
dfs(path.added(next_box))
for box in boxes:
if can_start(box):
dfs(PathState([box]))
return paths
def describe_path(path):
return (
" -> ".join(str(box) for box in path.boxes)
+ f" | right={path.get_right()}"
+ f" | collapsible_gap={path.gaps}"
)
def final_stack(region):
stackleft([item for item in region["items"] if item.position == "left"], region["leftwall"])
stackright([item for item in region["items"] if item.position == "right"], region["rightwall"])
items = [item for item in region["items"] if item.position == "middle"]
middle = SCREEN_WIDTH / 2
for item in items:
print(f"Positioning {item} in the middle.")
item.set_left(middle - item.get_width() / 2)
shift_amount = item.collision_amount_left(items)
if shift_amount != 0:
print(f" It collides. Push it to the right by {shift_amount}")
item.shift_x(shift_amount)
collision_tree_items = [collision for collision in item.get_collisionleft_items() if collision in items]
left = min(collision.get_left() for collision in collision_tree_items)
right = item.get_right()
current_mid = (left + right) / 2
print(f" Then, push the whole left collision tree left by {current_mid - middle}")
for collision in collision_tree_items:
collision.shift_x(middle - current_mid)
for item in items:
print(f"Checking {item} for collisions left.")
amount_left = max(item.collision_amount_left(), region["leftwall"] - item.get_left())
if amount_left > 0:
print(f" It collides on the left. Push its right collision tree to the right by {amount_left}")
collision_tree_items = [collision for collision in item.get_collisionright_items() if collision in items]
for collision in collision_tree_items:
print(f" shifting {collision} by {amount_left}")
collision.shift_x(amount_left)
for item in reversed(items):
print(f"Checking {item} for collisions right.")
amount_right = max(item.collision_amount_right(), item.get_right() - region["rightwall"])
if amount_right > 0:
print(f" It collides on the right. Push its left collision tree to the left by {amount_right}")
collision_tree_items = [collision for collision in item.get_collisionleft_items() if collision in items]
for collision in collision_tree_items:
collision.shift_x(-amount_right)
def stackleft(items, against_wall):
for item in items:
item.set_left(against_wall)
item.shift_x(item.collision_amount_left())
def stackright(items, against_wall):
for item in reversed(items):
item.set_right(against_wall)
item.shift_x(-item.collision_amount_right())
def place_into_regions(regions, items):
left_region = 0
right_region = len(regions) - 1
forceleft = False
forceright = False
middle = SCREEN_WIDTH / 2
if middle < regions[left_region]["rightwall"]:
midregion = left_region
elif regions[right_region]["leftwall"] < middle:
midregion = right_region
else:
midregion = None
forceleft = True
forceright = True
for item in items:
if item.position == "middle" and item.fallback == "left":
print(f"Placing {item} in middle/left")
if forceleft:
print(" Force is in effect. Position it in the left region.")
regions[left_region]["items"].insert(0, item)
elif overlaps_cutout(item, middle, regions):
print(" Overlap with cutout. Force.")
regions[left_region]["items"].insert(0, item)
forceleft = True
else:
print(f" Middle in region {midregion}")
regions[midregion]["items"].insert(0, item)
elif item.position == "middle" and item.fallback == "right":
print(f"Placing {item} in middle/right")
if forceright:
print(" Force is in effect. Position it in the right region.")
regions[right_region]["items"].append(item)
elif overlaps_cutout(item, middle, regions):
print(" Overlap with cutout. Force.")
regions[right_region]["items"].append(item)
forceright = True
else:
print(f" Middle in region {midregion}")
regions[midregion]["items"].append(item)
for item in reversed(items):
if item.position == "left":
regions[left_region]["items"].insert(0, item)
elif item.position == "right":
regions[right_region]["items"].append(item)
def overlaps_cutout(item, middle, regions):
if len(regions) < 2:
return False
item_left = middle - item.get_width() / 2
item_right = middle + item.get_width() / 2
return max(item_left, regions[0]["rightwall"]) < min(item_right, regions[1]["leftwall"])
def collision_tree(items):
for item in items:
for box in item.boxes:
box.collision_left.clear()
box.collision_right.clear()
left = []
for item in items:
for candidate in reversed(left):
for box in item.boxes:
for candidate_box in candidate.boxes:
if v_overlap(box, candidate_box):
if box.is_in_collision_left_tree(candidate_box):
continue
box.collision_left.append(candidate_box)
candidate_box.collision_right.append(box)
left.append(item)
def v_overlap(a, b):
return max(a.get_top(), b.get_top()) < min(a.get_bottom(), b.get_bottom())
def sort_items_by_kind_order(items, order):
order_index = {group: i for i, group in enumerate(order)}
return sorted(
sorted(items, key=lambda item: item.name),
key=lambda item: order_index[item.kind],
)
def unique_items(items):
result = []
for item in items:
if item not in result:
result.append(item)
return result
def kind_order_index(items):
result = {}
for item in items:
if item.kind not in result:
result[item.kind] = len(result)
return result
def print_regions(regions):
print("We have regions:")
for region in regions:
print(f"Region: {region['leftwall']} - {region['rightwall']}")
for item in region["items"]:
print(f" Item: {item}")
print(f" Width: {item.get_width()}")
print(f" Height: {item.get_bottom() - item.get_top()}")
print(f" x-pos: {item.get_left()}")
print(f" y-pos: {item.get_top()}")
for box in item.boxes:
print(f" Box: {box}")
print(f" left-collision: {[c.item for c in box.collision_left]}")
print(f" right-collision: {[c.item for c in box.collision_right]}")