diff --git a/CutNode.py b/CutNode.py index 210575c..ef9fbd9 100644 --- a/CutNode.py +++ b/CutNode.py @@ -7,6 +7,7 @@ from shapely.geometry.polygon import orient from shapely import affinity from PIL import Image, ImageDraw, ImageOps import matplotlib.pyplot as plt +import random import math import copy @@ -358,6 +359,17 @@ class CutNode: return out + @staticmethod + def gen_rand_node(rng: random.Random, max_cuts: int = 2, min_angle: int = -15, max_angle: int = 15): + vertical = rng.choice([True, False]) + angle = rng.randint(min_angle, max_angle) + split_mode = rng.choice(list(SPLIT_MODES.keys())) + cuts = rng.randint(1, max_cuts) if split_mode == 0 else 1 + node = CutNode(vertical, angle, split_mode) + for _ in range(cuts + 1): + node.add_child(None) + return node + def layout_to_image( cut_tree: CutNode, @@ -489,4 +501,3 @@ def draw_polygon_contours( img = ImageOps.flip(img) return img.resize((width, height), Image.Resampling.LANCZOS) - diff --git a/nodes.py b/nodes.py index b2a8984..df7746a 100644 --- a/nodes.py +++ b/nodes.py @@ -393,8 +393,9 @@ class RandomPanelLayoutGenerator: return { "required": { "num_panels": ("INT", {"default": 5, "min": 2, "max": 32}), - "min_angle": ("INT", {"default": -30, "min": -45, "max": 45}), - "max_angle": ("INT", {"default": 30, "min": -45, "max": 45}), + "max_cuts": ("INT", {"default": 2, "min": 1, "max": 9}), + "min_angle": ("INT", {"default": -25, "min": -45, "max": 45}), + "max_angle": ("INT", {"default": 25, "min": -45, "max": 45}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True}) } } @@ -406,9 +407,10 @@ class RandomPanelLayoutGenerator: @staticmethod def random_cut_tree( - num_panels: int = 5, - angle_min: int = -45, - angle_max: int = 45, + num_panels: int, + max_cuts: int = 2, + min_angle: int = -15, + max_angle: int = 15, seed: Optional[int] = None ) -> Optional[CutNode]: """ @@ -421,43 +423,41 @@ class RandomPanelLayoutGenerator: rng = random.Random(seed) - # recursive builder - def build(panels_left: int) -> CutNode: - # choose random orientation - vertical = rng.choice([True, False]) - # random angle - angle = rng.randint(angle_min, angle_max) - # random split mode - split_mode = rng.choice(list(SPLIT_MODES.keys())) + # 1st Generate nodes until we reach desired panel count + panels = 1 + unused_nodes: list[CutNode] = [] + while panels < num_panels: + remaining = num_panels - panels + max_possible_cuts = min(max_cuts, remaining) # not -1, cause the cuts are nested within a prior panel + node = CutNode.gen_rand_node(rng, max_possible_cuts, min_angle, max_angle) + panels += node.cuts + unused_nodes.append(node) - # enforce cut count rules - cuts = rng.randint(1, 3) if split_mode == 0 else 1 + if not unused_nodes: + return None - # create node - node = CutNode(vertical, angle, split_mode) + # Build hierarchy after all nodes have been generated + root = unused_nodes.pop(rng.randrange(len(unused_nodes))) + used_nodes_available = [root] + used_nodes_unavailable: list[CutNode] = [] # without empty children slots - # always creates 2 children - # decide how many panels to allocate to each branch - if panels_left <= 2: - # exactly 2 panels → both leaves - node.add_child(None) - node.add_child(None) - return node + while unused_nodes: + parent = rng.choice(used_nodes_available) + child_idx = rng.choice([i for i, c in enumerate(parent.children) if c is None]) + node = unused_nodes.pop(rng.randrange(len(unused_nodes))) + parent.children[child_idx] = node - # randomly split the remaining panels - left_panels = rng.randint(1, panels_left - 1) - right_panels = panels_left - left_panels + # update availability + if all(c is not None for c in parent.children): + used_nodes_available.remove(parent) + used_nodes_unavailable.append(parent) - # recursively build children - node.add_child(build(left_panels) if left_panels > 1 else None) - node.add_child(build(right_panels) if right_panels > 1 else None) + used_nodes_available.append(node) - return node + return root - return build(num_panels) - - def func(self, num_panels, min_angle, max_angle, seed): - cut_tree = self.random_cut_tree(num_panels, min_angle, max_angle, seed) + def func(self, num_panels, max_cuts, min_angle, max_angle, seed): + cut_tree = self.random_cut_tree(num_panels, max_cuts, min_angle, max_angle, seed) return (cut_tree,) @@ -505,17 +505,23 @@ class MutatePanelLayout: rng = random.Random(seed) def _mutate_node(n: CutNode): - # 1) Add a child - if n.split_mode == 0 and rng.random() < prob_add: - insert_idx = rng.randint(0, len(n.children)) - n.children.insert(insert_idx, None) + # 0) Recurse ( run from leafs to top to prevent endless operations ) + for child in n.children: + if child is not None: + _mutate_node(child) - # 2) Remove a child - if n.split_mode == 0 and n.cuts > 0 and rng.random() < prob_remove: - removable_indices = [i for i, c in enumerate(n.children) if c is not None or len(n.children) > 1] - if removable_indices: - idx = rng.choice(removable_indices) - n.children.pop(idx) + removable_indices = [i for i, c in enumerate(n.children) if c is not None + and rng.random() < prob_remove] # roll for each individually + addable_indices = [i for i, c in enumerate(n.children) if c is None + and rng.random() < prob_add] # roll for each individually + + # 1) Add a cut + for idx in addable_indices: + n.children[idx] = CutNode.gen_rand_node(rng, 2, -max_angle_delta, max_angle_delta) + + # 2) Remove a cut (non None child) + for idx in removable_indices: + n.children[idx] = None # 3) Change number of cuts if n.split_mode == 0 and rng.random() < prob_change_cuts: @@ -545,11 +551,6 @@ class MutatePanelLayout: if n.split_mode != 0 and len(n.children) > 2: n.children = n.children[:2] - # 6) Recurse - for child in n.children: - if child is not None: - _mutate_node(child) - _mutate_node(node) return node diff --git a/workflows/Grid Layout Generation and Layout Mutation.png b/workflows/Grid Layout Generation and Layout Mutation.png index cd49483..1fd8f00 100644 Binary files a/workflows/Grid Layout Generation and Layout Mutation.png and b/workflows/Grid Layout Generation and Layout Mutation.png differ diff --git a/workflows/Random Layout Generation.png b/workflows/Random Layout Generation.png index 0a2d8e2..73d940b 100644 Binary files a/workflows/Random Layout Generation.png and b/workflows/Random Layout Generation.png differ