""" H/S/L Fractal Pattern Node - Generates a generative fractal structure based on H (Hub), S (State), and L (Loop) inputs. Ported from hslcity.html """ import numpy as np from PyQt6 import QtGui import cv2 import sys import os import __main__ BaseNode = __main__.BaseNode PA_INSTANCE = getattr(__main__, "PA_INSTANCE", None) QtGui = __main__.QtGui # --- Core Simulation Classes (from hslcity.html) --- class HSLPattern: def __init__(self, x, y, angle, scale, depth, patternType='h'): self.x = x self.y = y self.angle = angle self.scale = scale self.depth = depth self.patternType = patternType self.phase = np.random.rand() * np.pi * 2 self.children = [] self.age = 0 self.time = 0.0 if depth > 0: self.generateChildren() def generateChildren(self): branchAngle = 45.0 * np.pi / 180 childScale = self.scale * 0.6 childDepth = self.depth - 1 if self.patternType == 'h': # Hubs branch into states for i in range(3): childAngle = self.angle + (i - 1) * branchAngle childType = ['s', 'l', 's'][i] self.children.append(HSLPattern( self.x + np.cos(childAngle) * self.scale * 40, self.y + np.sin(childAngle) * self.scale * 40, childAngle, childScale, childDepth, childType )) elif self.patternType == 'l': # Loops create circular patterns for i in range(4): childAngle = self.angle + i * np.pi / 2 childType = 'l' self.children.append(HSLPattern( self.x + np.cos(childAngle) * self.scale * 30, self.y + np.sin(childAngle) * self.scale * 30, childAngle, childScale, childDepth, childType )) else: # 's' states transition childType = 'l' if np.random.rand() > 0.5 else 'h' self.children.append(HSLPattern( self.x + np.cos(self.angle) * self.scale * 50, self.y + np.sin(self.angle) * self.scale * 50, self.angle + (np.random.rand() - 0.5) * branchAngle, childScale, childDepth, childType )) def update(self, dt, global_time): self.age += dt self.time = global_time for child in self.children: child.update(dt, global_time) def draw(self, ctx_img, pulse_intensity): # Calculate pulsation pulse = 1.0 if self.patternType == 'h': pulse = 1 + np.sin(self.time * 3 + self.phase) * pulse_intensity * 0.5 elif self.patternType == 'l': pulse = 1 + np.sin(self.time + self.phase) * pulse_intensity * 0.2 else: pulse = 1 + np.sin(self.time * 2 + self.phase) * pulse_intensity * 0.3 # Set color (BGR) color = (0,0,0) if self.patternType == 'h': color = (100, 100, 255) # Red elif self.patternType == 'l': color = (100, 255, 100) # Green else: color = (255, 100, 100) # Blue radius = int(self.scale * 15 * pulse) if radius < 1: radius = 1 # Draw the node pt = (int(self.x), int(self.y)) cv2.circle(ctx_img, pt, radius, color, -1, cv2.LINE_AA) # Draw connections for child in self.children: child_pt = (int(child.x), int(child.y)) cv2.line(ctx_img, pt, child_pt, (100, 100, 100), 1, cv2.LINE_AA) child.draw(ctx_img, pulse_intensity) # --- The Main Node Class --- class HSLPatternNode(BaseNode): NODE_CATEGORY = "Source" NODE_COLOR = QtGui.QColor(100, 200, 250) # Crystalline blue def __init__(self, size=128, speed=1.0, pulse=0.8, depth=4): super().__init__() self.node_title = "HSL Pattern (MTX)" self.inputs = { 'H_in': 'signal', # Hub trigger 'S_in': 'signal', # State trigger 'L_in': 'signal' # Loop trigger } self.outputs = {'image': 'image'} self.size = int(size) self.speed = float(speed) self.pulse = float(pulse) self.depth = int(depth) self.time = 0.0 self.root_patterns = [] self.output_image = np.zeros((self.size, self.size, 3), dtype=np.uint8) # Last trigger values self.last_h = 0.0 self.last_s = 0.0 self.last_l = 0.0 # Initialize self._add_seed(self.size // 2, self.size // 2, 'h') def _add_seed(self, x, y, pattern_type): """Adds a new root pattern to the simulation.""" new_pattern = HSLPattern( x, y, np.random.rand() * 2 * np.pi, scale=1.0, depth=self.depth, patternType=pattern_type ) self.root_patterns.append(new_pattern) # Limit total patterns if len(self.root_patterns) > 20: self.root_patterns.pop(0) def step(self): # 1. Handle Inputs (check for rising edge) h_in = self.get_blended_input('H_in', 'sum') or 0.0 s_in = self.get_blended_input('S_in', 'sum') or 0.0 l_in = self.get_blended_input('L_in', 'sum') or 0.0 rand_x = np.random.randint(self.size * 0.2, self.size * 0.8) rand_y = np.random.randint(self.size * 0.2, self.size * 0.8) if h_in > 0.5 and self.last_h <= 0.5: self._add_seed(rand_x, rand_y, 'h') if s_in > 0.5 and self.last_s <= 0.5: self._add_seed(rand_x, rand_y, 's') if l_in > 0.5 and self.last_l <= 0.5: self._add_seed(rand_x, rand_y, 'l') self.last_h, self.last_s, self.last_l = h_in, s_in, l_in # 2. Update time and simulation self.time += self.speed * 0.02 # 3. Draw # Fade the background self.output_image = (self.output_image * 0.9).astype(np.uint8) for pattern in self.root_patterns: pattern.update(0.016, self.time) pattern.draw(self.output_image, self.pulse) def get_output(self, port_name): if port_name == 'image': return self.output_image.astype(np.float32) / 255.0 return None def get_display_image(self): img_rgb = np.ascontiguousarray(self.output_image) h, w = img_rgb.shape[:2] return QtGui.QImage(img_rgb.data, w, h, 3*w, QtGui.QImage.Format.Format_BGR888) def get_config_options(self): return [ ("Resolution", "size", self.size, None), ("Speed", "speed", self.speed, None), ("Pulsation", "pulse", self.pulse, None), ("Recursion Depth", "depth", self.depth, None), ]