Skip to content

Commit 5100267

Browse files
authored
Merge pull request #22 from bhowiebkr/features/node_refactor
Features/node refactor
2 parents cc7e41e + 1a83f7b commit 5100267

17 files changed

Lines changed: 718 additions & 335 deletions

.github/workflows/windows-build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ jobs:
7070
--remove-output `
7171
--lto=yes `
7272
--include-data-dir=examples=examples `
73+
--include-data-dir=resources=resources `
7374
--include-data-file=dark_theme.qss=dark_theme.qss `
7475
--assume-yes-for-downloads `
7576
main.py
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
{
2+
"nodes": [
3+
{
4+
"uuid": "noise-params",
5+
"title": "1. Noise Parameters",
6+
"pos": [
7+
92.58449999999999,
8+
34.22462500000006
9+
],
10+
"size": [
11+
250,
12+
416
13+
],
14+
"code": "from typing import Tuple\n\n@node_entry\ndef get_noise_parameters(seed: int, scale: float, octaves: int, persistence: float, lacunarity: float) -> Tuple[int, float, int, float, float]:\n return seed, scale, octaves, persistence, lacunarity",
15+
"gui_code": "from PySide6.QtWidgets import QLabel, QSpinBox, QDoubleSpinBox\n\nlayout.addWidget(QLabel('Seed:', parent))\nwidgets['seed'] = QSpinBox(parent)\nwidgets['seed'].setRange(0, 9999)\nwidgets['seed'].setValue(42)\nlayout.addWidget(widgets['seed'])\n\nlayout.addWidget(QLabel('Scale:', parent))\nwidgets['scale'] = QDoubleSpinBox(parent)\nwidgets['scale'].setRange(10.0, 200.0)\nwidgets['scale'].setValue(50.0)\nlayout.addWidget(widgets['scale'])\n\nlayout.addWidget(QLabel('Octaves:', parent))\nwidgets['octaves'] = QSpinBox(parent)\nwidgets['octaves'].setRange(1, 8)\nwidgets['octaves'].setValue(4)\nlayout.addWidget(widgets['octaves'])\n\nlayout.addWidget(QLabel('Persistence:', parent))\nwidgets['persistence'] = QDoubleSpinBox(parent)\nwidgets['persistence'].setRange(0.1, 1.0)\nwidgets['persistence'].setValue(0.5)\nwidgets['persistence'].setSingleStep(0.1)\nlayout.addWidget(widgets['persistence'])\n\nlayout.addWidget(QLabel('Lacunarity:', parent))\nwidgets['lacunarity'] = QDoubleSpinBox(parent)\nwidgets['lacunarity'].setRange(1.0, 4.0)\nwidgets['lacunarity'].setValue(2.0)\nwidgets['lacunarity'].setSingleStep(0.1)\nlayout.addWidget(widgets['lacunarity'])",
16+
"gui_get_values_code": "def get_values(widgets):\n return {\n 'seed': widgets['seed'].value(),\n 'scale': widgets['scale'].value(),\n 'octaves': widgets['octaves'].value(),\n 'persistence': widgets['persistence'].value(),\n 'lacunarity': widgets['lacunarity'].value()\n }\n\ndef set_initial_state(widgets, state):\n if 'seed' in state: widgets['seed'].setValue(state['seed'])\n if 'scale' in state: widgets['scale'].setValue(state['scale'])\n if 'octaves' in state: widgets['octaves'].setValue(state['octaves'])\n if 'persistence' in state: widgets['persistence'].setValue(state['persistence'])\n if 'lacunarity' in state: widgets['lacunarity'].setValue(state['lacunarity'])",
17+
"gui_state": {
18+
"seed": 42,
19+
"scale": 50.0,
20+
"octaves": 4,
21+
"persistence": 0.5,
22+
"lacunarity": 2.0
23+
},
24+
"colors": {
25+
"title": "#2a2a2a",
26+
"body": "#141414"
27+
}
28+
},
29+
{
30+
"uuid": "color-palette",
31+
"title": "2. Color Palette",
32+
"pos": [
33+
411.9682499999998,
34+
432.89262500000007
35+
],
36+
"size": [
37+
250,
38+
258
39+
],
40+
"code": "@node_entry\ndef get_color_palette(color1: str, color2: str, color3: str) -> str:\n # Pass colors as a delimited string\n return f'{color1};{color2};{color3}'",
41+
"gui_code": "from PySide6.QtWidgets import QLabel, QPushButton, QColorDialog\nfrom PySide6.QtGui import QColor\n\n# Helper to update button color\ndef update_color(button, color_str):\n button.setStyleSheet(f'background-color: {color_str};')\n\n# --- BUG FIX: Use lambdas to correctly capture scope for signal handlers ---\nlayout.addWidget(QLabel('Gradient Colors:', parent))\n\n# Color 1\nbtn1 = QPushButton('#0D3B66', parent)\nbtn1.clicked.connect(lambda: (lambda b=btn1: (color := QColorDialog.getColor(QColor(b.text()))).isValid() and (b.setText(color.name()), update_color(b, color.name())))())\nupdate_color(btn1, '#0D3B66')\nwidgets['color1_button'] = btn1\nlayout.addWidget(btn1)\n\n# Color 2\nbtn2 = QPushButton('#FAF0CA', parent)\nbtn2.clicked.connect(lambda: (lambda b=btn2: (color := QColorDialog.getColor(QColor(b.text()))).isValid() and (b.setText(color.name()), update_color(b, color.name())))())\nupdate_color(btn2, '#FAF0CA')\nwidgets['color2_button'] = btn2\nlayout.addWidget(btn2)\n\n# Color 3\nbtn3 = QPushButton('#F95738', parent)\nbtn3.clicked.connect(lambda: (lambda b=btn3: (color := QColorDialog.getColor(QColor(b.text()))).isValid() and (b.setText(color.name()), update_color(b, color.name())))())\nupdate_color(btn3, '#F95738')\nwidgets['color3_button'] = btn3\nlayout.addWidget(btn3)",
42+
"gui_get_values_code": "def get_values(widgets):\n return {\n 'color1': widgets['color1_button'].text(),\n 'color2': widgets['color2_button'].text(),\n 'color3': widgets['color3_button'].text()\n }\n\ndef set_initial_state(widgets, state):\n # Helper to update button color\n def update_color(button, color_str):\n button.setStyleSheet(f'background-color: {color_str};')\n if 'color1' in state: \n widgets['color1_button'].setText(state['color1'])\n update_color(widgets['color1_button'], state['color1'])\n if 'color2' in state: \n widgets['color2_button'].setText(state['color2'])\n update_color(widgets['color2_button'], state['color2'])\n if 'color3' in state: \n widgets['color3_button'].setText(state['color3'])\n update_color(widgets['color3_button'], state['color3'])",
43+
"gui_state": {
44+
"color1": "#0D3B66",
45+
"color2": "#FAF0CA",
46+
"color3": "#F95738"
47+
},
48+
"colors": {
49+
"title": "#2a2a2a",
50+
"body": "#141414"
51+
}
52+
},
53+
{
54+
"uuid": "noise-generator",
55+
"title": "3. Generate Noise Map",
56+
"pos": [
57+
434.79125,
58+
88.97612500000002
59+
],
60+
"size": [
61+
250,
62+
192
63+
],
64+
"code": "import numpy as np\nimport json\n\n# Helper function for linear interpolation\ndef lerp(a, b, x):\n return a + x * (b - a)\n\n# Perlin noise implementation (for portability)\ndef perlin(x, y, seed=0):\n np.random.seed(seed)\n p = np.arange(256, dtype=int)\n np.random.shuffle(p)\n p = np.stack([p, p]).flatten()\n xi, yi = x.astype(int), y.astype(int)\n xf, yf = x - xi, y - yi\n u, v = xf * xf * xf * (xf * (xf * 6 - 15) + 10), yf * yf * yf * (yf * (yf * 6 - 15) + 10)\n # This is a simplified noise function for demonstration\n n00 = np.sin(xi) + np.cos(yi)\n n10 = np.sin(xi+1) + np.cos(yi)\n n01 = np.sin(xi) + np.cos(yi+1)\n n11 = np.sin(xi+1) + np.cos(yi+1)\n x1 = lerp(n00, n10, u)\n x2 = lerp(n01, n11, u)\n return lerp(x1, x2, v)\n\n@node_entry\ndef generate_noise(seed: int, scale: float, octaves: int, persistence: float, lacunarity: float) -> str:\n width, height = 256, 256\n shape = (width, height)\n \n noise_map = np.zeros(shape)\n frequency = 1\n amplitude = 1\n \n for i in range(octaves):\n lin_x = np.linspace(0, width / scale * frequency, width, endpoint=False)\n lin_y = np.linspace(0, height / scale * frequency, height, endpoint=False)\n x, y = np.meshgrid(lin_x, lin_y)\n noise_map += perlin(x, y, seed + i) * amplitude\n \n amplitude *= persistence\n frequency *= lacunarity\n\n # Normalize to 0-1 range\n if np.max(noise_map) != np.min(noise_map):\n noise_map = (noise_map - np.min(noise_map)) / (np.max(noise_map) - np.min(noise_map))\n \n # Return as a JSON string of a list\n return json.dumps(noise_map.tolist())",
65+
"gui_code": "",
66+
"gui_get_values_code": "",
67+
"gui_state": {},
68+
"colors": {
69+
"title": "#006a4e",
70+
"body": "#004b38"
71+
}
72+
},
73+
{
74+
"uuid": "colorize-node",
75+
"title": "4. Colorize Noise Map",
76+
"pos": [
77+
751.143125,
78+
257.22662500000007
79+
],
80+
"size": [
81+
250,
82+
117
83+
],
84+
"code": "import numpy as np\nfrom PIL import Image\nimport json\nimport base64\nimport io\n\n@node_entry\ndef colorize_noise(noise_map_json: str, palette_str: str) -> str:\n try:\n noise_map = np.array(json.loads(noise_map_json))\n colors_hex = palette_str.split(';')\n colors_rgb = [tuple(int(h.lstrip('#')[i:i+2], 16) for i in (0, 2, 4)) for h in colors_hex]\n\n width, height = noise_map.shape\n color_image = np.zeros((height, width, 3), dtype=np.uint8)\n\n # Create a simple linear gradient from the colors\n for i in range(width):\n for j in range(height):\n val = noise_map[i, j]\n if val < 0.5:\n t = val * 2\n color = (int((1-t)*colors_rgb[0][c] + t*colors_rgb[1][c]) for c in range(3))\n else:\n t = (val - 0.5) * 2\n color = (int((1-t)*colors_rgb[1][c] + t*colors_rgb[2][c]) for c in range(3))\n color_image[j, i] = tuple(color)\n\n img = Image.fromarray(color_image, 'RGB')\n \n # Convert image to base64 string to pass to the GUI without saving a file\n buffered = io.BytesIO()\n img.save(buffered, format=\"PNG\")\n img_str = base64.b64encode(buffered.getvalue()).decode('utf-8')\n return img_str\n except Exception as e:\n return f'ERROR: {e}'",
85+
"gui_code": "",
86+
"gui_get_values_code": "",
87+
"gui_state": {},
88+
"colors": {
89+
"title": "#5c2a9d",
90+
"body": "#3c1d63"
91+
}
92+
},
93+
{
94+
"uuid": "image-preview",
95+
"title": "5. Texture Preview",
96+
"pos": [
97+
1110.0795000000003,
98+
211.60037500000004
99+
],
100+
"size": [
101+
250,
102+
348
103+
],
104+
"code": "@node_entry\ndef show_image(image_base64: str) -> str:\n # Pass the data through for the GUI logic to use\n return image_base64",
105+
"gui_code": "from PySide6.QtWidgets import QLabel\nfrom PySide6.QtGui import QPixmap\nfrom PySide6.QtCore import Qt\n\nwidgets['image_label'] = QLabel('Execute graph to generate texture...', parent)\nwidgets['image_label'].setMinimumSize(256, 256)\nwidgets['image_label'].setAlignment(Qt.AlignCenter)\nwidgets['image_label'].setStyleSheet('border: 1px solid #555;')\nlayout.addWidget(widgets['image_label'])",
106+
"gui_get_values_code": "import base64\n\ndef get_values(widgets):\n return {}\n\ndef set_values(widgets, outputs):\n from PySide6.QtGui import QPixmap\n from PySide6.QtCore import Qt\n img_str = outputs.get('output_1')\n if img_str and not img_str.startswith('ERROR'):\n try:\n img_data = base64.b64decode(img_str)\n pixmap = QPixmap()\n pixmap.loadFromData(img_data)\n if not pixmap.isNull():\n widgets['image_label'].setPixmap(pixmap)\n else:\n widgets['image_label'].setText('Error: Could not load image data')\n except Exception as e:\n widgets['image_label'].setText(f'Error decoding image:\\n{e}')\n else:\n widgets['image_label'].setText(f'Execution failed:\\n{img_str}')",
107+
"gui_state": {},
108+
"colors": {
109+
"title": "#2a2a2a",
110+
"body": "#141414"
111+
}
112+
}
113+
],
114+
"connections": [
115+
{
116+
"start_node_uuid": "noise-params",
117+
"start_pin_name": "output_1",
118+
"end_node_uuid": "noise-generator",
119+
"end_pin_name": "seed"
120+
},
121+
{
122+
"start_node_uuid": "noise-params",
123+
"start_pin_name": "output_2",
124+
"end_node_uuid": "noise-generator",
125+
"end_pin_name": "scale"
126+
},
127+
{
128+
"start_node_uuid": "noise-params",
129+
"start_pin_name": "output_3",
130+
"end_node_uuid": "noise-generator",
131+
"end_pin_name": "octaves"
132+
},
133+
{
134+
"start_node_uuid": "noise-params",
135+
"start_pin_name": "output_4",
136+
"end_node_uuid": "noise-generator",
137+
"end_pin_name": "persistence"
138+
},
139+
{
140+
"start_node_uuid": "noise-params",
141+
"start_pin_name": "output_5",
142+
"end_node_uuid": "noise-generator",
143+
"end_pin_name": "lacunarity"
144+
},
145+
{
146+
"start_node_uuid": "noise-generator",
147+
"start_pin_name": "output_1",
148+
"end_node_uuid": "colorize-node",
149+
"end_pin_name": "noise_map_json"
150+
},
151+
{
152+
"start_node_uuid": "color-palette",
153+
"start_pin_name": "output_1",
154+
"end_node_uuid": "colorize-node",
155+
"end_pin_name": "palette_str"
156+
},
157+
{
158+
"start_node_uuid": "colorize-node",
159+
"start_pin_name": "output_1",
160+
"end_node_uuid": "image-preview",
161+
"end_pin_name": "image_base64"
162+
}
163+
],
164+
"requirements": [
165+
"numpy",
166+
"Pillow"
167+
]
168+
}

file_stats.py

Lines changed: 0 additions & 78 deletions
This file was deleted.

images/environment_manager.png

222 KB
Loading

images/gui.png

39.2 KB
Loading

images/python_editor.png

193 KB
Loading

images/text_adventure_graph.png

37.9 KB
Loading

main.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,47 @@
11
# main.py
22
# Entry point for the Node Editor application.
3-
# Now loads a QSS stylesheet for a consistent dark theme.
3+
# Now loads a QSS stylesheet and the Font Awesome font.
44

55
import sys
66
import os
77
from PySide6.QtWidgets import QApplication
8+
from PySide6.QtCore import qInstallMessageHandler, QtMsgType
9+
from PySide6.QtGui import QFontDatabase
810
from node_editor_window import NodeEditorWindow
911

1012

11-
def load_stylesheet(app, filename="dark_theme.qss"):
12-
"""Loads an external QSS file and applies it to the application."""
13-
try:
14-
with open(filename, "r") as f:
15-
style = f.read()
16-
app.setStyleSheet(style)
17-
except FileNotFoundError:
18-
print(f"Warning: Stylesheet '{filename}' not found. Using default style.")
13+
def custom_message_handler(mode, context, message):
14+
"""
15+
A custom message handler to filter out specific, non-critical warnings.
16+
"""
17+
warnings_to_ignore = [
18+
"qt.qpa.wayland.textinput",
19+
]
20+
if any(warning in message for warning in warnings_to_ignore):
21+
return
22+
print(message, file=sys.stderr)
1923

2024

2125
if __name__ == "__main__":
22-
# Create the Qt Application
26+
qInstallMessageHandler(custom_message_handler)
2327
app = QApplication(sys.argv)
2428

29+
# --- Load Font Awesome ---
30+
font_path = os.path.join(os.path.dirname(__file__), "resources", "Font Awesome 6 Free-Solid-900.otf")
31+
if os.path.exists(font_path):
32+
font_id = QFontDatabase.addApplicationFont(font_path)
33+
if font_id == -1:
34+
print("Warning: Failed to load Font Awesome font.", file=sys.stderr)
35+
else:
36+
print("Warning: Font Awesome font file not found at 'resources/Font Awesome 6 Free-Solid-900.otf'", file=sys.stderr)
37+
2538
# Load the dark theme stylesheet
26-
load_stylesheet(app)
39+
try:
40+
with open("dark_theme.qss", "r") as f:
41+
style = f.read()
42+
app.setStyleSheet(style)
43+
except FileNotFoundError:
44+
print("Warning: 'dark_theme.qss' not found. Using default style.", file=sys.stderr)
2745

2846
# Create and show the main window
2947
window = NodeEditorWindow()

0 commit comments

Comments
 (0)