Add packed layout Python prototype
This commit is contained in:
@@ -0,0 +1,873 @@
|
||||
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]}")
|
||||
Reference in New Issue
Block a user