autoforge 1.3.4__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {autoforge-1.3.4 → autoforge-1.4.0}/PKG-INFO +7 -2
  2. {autoforge-1.3.4 → autoforge-1.4.0}/README.md +5 -1
  3. {autoforge-1.3.4 → autoforge-1.4.0}/pyproject.toml +3 -2
  4. {autoforge-1.3.4 → autoforge-1.4.0}/src/AutoForge.egg-info/PKG-INFO +7 -2
  5. {autoforge-1.3.4 → autoforge-1.4.0}/src/AutoForge.egg-info/SOURCES.txt +7 -2
  6. {autoforge-1.3.4 → autoforge-1.4.0}/src/AutoForge.egg-info/requires.txt +1 -0
  7. autoforge-1.4.0/src/autoforge/Gui/Views/backend_database.py +205 -0
  8. autoforge-1.4.0/src/autoforge/Gui/Views/filament_editor.py +216 -0
  9. autoforge-1.4.0/src/autoforge/Gui/app.py +202 -0
  10. autoforge-1.4.0/src/autoforge/Gui/main.py +29 -0
  11. {autoforge-1.3.4 → autoforge-1.4.0}/src/autoforge/Helper/FilamentHelper.py +5 -2
  12. autoforge-1.4.0/src/autoforge/Helper/ImageHelper.py +164 -0
  13. {autoforge-1.3.4 → autoforge-1.4.0}/src/autoforge/Helper/OptimizerHelper.py +3 -3
  14. {autoforge-1.3.4 → autoforge-1.4.0}/src/autoforge/Helper/PruningHelper.py +2 -1
  15. {autoforge-1.3.4/src/autoforge → autoforge-1.4.0/src/autoforge/Helper}/filamentcolors_library.py +45 -11
  16. autoforge-1.4.0/src/autoforge/Loss/LossFunctions.py +104 -0
  17. {autoforge-1.3.4 → autoforge-1.4.0}/src/autoforge/Misc/color_fit_all.py +23 -9
  18. {autoforge-1.3.4 → autoforge-1.4.0}/src/autoforge/Modules/Optimizer.py +69 -4
  19. autoforge-1.4.0/src/autoforge/Modules/__init__.py +0 -0
  20. autoforge-1.4.0/src/autoforge/__init__.py +0 -0
  21. {autoforge-1.3.4 → autoforge-1.4.0}/src/autoforge/auto_forge.py +44 -7
  22. autoforge-1.3.4/src/autoforge/Helper/ImageHelper.py +0 -22
  23. autoforge-1.3.4/src/autoforge/Loss/LossFunctions.py +0 -100
  24. autoforge-1.3.4/src/autoforge/auto_forge_gradio.py +0 -495
  25. {autoforge-1.3.4 → autoforge-1.4.0}/setup.cfg +0 -0
  26. {autoforge-1.3.4 → autoforge-1.4.0}/src/AutoForge.egg-info/dependency_links.txt +0 -0
  27. {autoforge-1.3.4 → autoforge-1.4.0}/src/AutoForge.egg-info/entry_points.txt +0 -0
  28. {autoforge-1.3.4 → autoforge-1.4.0}/src/AutoForge.egg-info/top_level.txt +0 -0
  29. {autoforge-1.3.4/src/autoforge/Helper → autoforge-1.4.0/src/autoforge/Gui/Views}/__init__.py +0 -0
  30. {autoforge-1.3.4/src/autoforge/Loss → autoforge-1.4.0/src/autoforge/Gui}/__init__.py +0 -0
  31. {autoforge-1.3.4 → autoforge-1.4.0}/src/autoforge/Helper/OutputHelper.py +0 -0
  32. {autoforge-1.3.4/src/autoforge/Misc → autoforge-1.4.0/src/autoforge/Helper}/__init__.py +0 -0
  33. {autoforge-1.3.4 → autoforge-1.4.0}/src/autoforge/Loss/PerceptionLoss.py +0 -0
  34. {autoforge-1.3.4/src/autoforge/Modules → autoforge-1.4.0/src/autoforge/Loss}/__init__.py +0 -0
  35. {autoforge-1.3.4/src/autoforge → autoforge-1.4.0/src/autoforge/Misc}/__init__.py +0 -0
  36. {autoforge-1.3.4 → autoforge-1.4.0}/src/autoforge/__main__.py +0 -0
  37. {autoforge-1.3.4 → autoforge-1.4.0}/src/autoforge/old_jax_files.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: AutoForge
3
- Version: 1.3.4
3
+ Version: 1.4.0
4
4
  Summary: AutoForge is a Python tool for generating 3D printed layered models from an input image.
5
5
  Author-email: Hendric Voss <hvoss@techfak.uni-bielefeld.de>
6
6
  License: CC BY-NC-SA 4.0
@@ -17,6 +17,7 @@ Requires-Dist: tqdm>=4.50.0
17
17
  Requires-Dist: pandas>=1.1.0
18
18
  Requires-Dist: scikit-image
19
19
  Requires-Dist: scikit-learn
20
+ Requires-Dist: tensorboard
20
21
 
21
22
  # AutoForge
22
23
 
@@ -103,6 +104,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
103
104
  - `--layer_height`: Layer thickness in millimeters (default: 0.04).
104
105
  - `--max_layers`: Maximum number of layers (default: 75). \
105
106
  **Note:** This is about 3mm + the background height
107
+ - `--min_layers`: Minimum number of layers (default: 0). Used to limit height of pruning
106
108
  - `--background_height`: Height of the background in millimeters (default: 0.4). \
107
109
  **Note:** The background height must be divisible by the layer height.
108
110
  - `--background_color`: Background color in hexadecimal format (default: `#000000` aka Black). \
@@ -110,8 +112,11 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
110
112
  - `--output_size`: Maximum dimension for target image (default: 1024).
111
113
  - `--solver_size`: Maximum dimension for solver (fast) image (default: 128). \
112
114
  **Note:** We solve on a smaller size as this is many times faster, but also a bit less accurate. Increase if you need more accuracy.
113
- - `--decay`: Final tau value for the Gumbel-Softmax formulation (default: 0.01).
115
+ - `--init_tau`: Initial tau value for Gumbel-Softmax (default: 1.0).
116
+ - `--final_tau`: Final tau value for the Gumbel-Softmax formulation (default: 0.01).
114
117
  - `--visualize`: Flag to enable live visualization of the composite image during optimization.
118
+ - `--tensorboard`: Enable TensorBoard logging
119
+ - `--run-name`: Name of the run used for TensorBoard logging (optional).
115
120
  - `--perform_pruning`: Perform pruning after optimization (default: True). \
116
121
  **Note:** This is highly recommended even if you don't have a color/color swap limit, as it actually increases the quality of the output.
117
122
  - `--pruning_max_colors`: Max number of colors allowed after pruning (default: 100).
@@ -83,6 +83,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
83
83
  - `--layer_height`: Layer thickness in millimeters (default: 0.04).
84
84
  - `--max_layers`: Maximum number of layers (default: 75). \
85
85
  **Note:** This is about 3mm + the background height
86
+ - `--min_layers`: Minimum number of layers (default: 0). Used to limit height of pruning
86
87
  - `--background_height`: Height of the background in millimeters (default: 0.4). \
87
88
  **Note:** The background height must be divisible by the layer height.
88
89
  - `--background_color`: Background color in hexadecimal format (default: `#000000` aka Black). \
@@ -90,8 +91,11 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
90
91
  - `--output_size`: Maximum dimension for target image (default: 1024).
91
92
  - `--solver_size`: Maximum dimension for solver (fast) image (default: 128). \
92
93
  **Note:** We solve on a smaller size as this is many times faster, but also a bit less accurate. Increase if you need more accuracy.
93
- - `--decay`: Final tau value for the Gumbel-Softmax formulation (default: 0.01).
94
+ - `--init_tau`: Initial tau value for Gumbel-Softmax (default: 1.0).
95
+ - `--final_tau`: Final tau value for the Gumbel-Softmax formulation (default: 0.01).
94
96
  - `--visualize`: Flag to enable live visualization of the composite image during optimization.
97
+ - `--tensorboard`: Enable TensorBoard logging
98
+ - `--run-name`: Name of the run used for TensorBoard logging (optional).
95
99
  - `--perform_pruning`: Perform pruning after optimization (default: True). \
96
100
  **Note:** This is highly recommended even if you don't have a color/color swap limit, as it actually increases the quality of the output.
97
101
  - `--pruning_max_colors`: Max number of colors allowed after pruning (default: 100).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "AutoForge"
7
- version="1.3.4"
7
+ version="1.4.0"
8
8
  description = "AutoForge is a Python tool for generating 3D printed layered models from an input image."
9
9
  authors = [{ name = "Hendric Voss", email = "hvoss@techfak.uni-bielefeld.de" }]
10
10
  readme = "README.md"
@@ -21,7 +21,8 @@ dependencies = [
21
21
  "tqdm>=4.50.0",
22
22
  "pandas>=1.1.0",
23
23
  "scikit-image",
24
- "scikit-learn"
24
+ "scikit-learn",
25
+ "tensorboard"
25
26
  ]
26
27
 
27
28
  [tool.setuptools]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: AutoForge
3
- Version: 1.3.4
3
+ Version: 1.4.0
4
4
  Summary: AutoForge is a Python tool for generating 3D printed layered models from an input image.
5
5
  Author-email: Hendric Voss <hvoss@techfak.uni-bielefeld.de>
6
6
  License: CC BY-NC-SA 4.0
@@ -17,6 +17,7 @@ Requires-Dist: tqdm>=4.50.0
17
17
  Requires-Dist: pandas>=1.1.0
18
18
  Requires-Dist: scikit-image
19
19
  Requires-Dist: scikit-learn
20
+ Requires-Dist: tensorboard
20
21
 
21
22
  # AutoForge
22
23
 
@@ -103,6 +104,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
103
104
  - `--layer_height`: Layer thickness in millimeters (default: 0.04).
104
105
  - `--max_layers`: Maximum number of layers (default: 75). \
105
106
  **Note:** This is about 3mm + the background height
107
+ - `--min_layers`: Minimum number of layers (default: 0). Used to limit height of pruning
106
108
  - `--background_height`: Height of the background in millimeters (default: 0.4). \
107
109
  **Note:** The background height must be divisible by the layer height.
108
110
  - `--background_color`: Background color in hexadecimal format (default: `#000000` aka Black). \
@@ -110,8 +112,11 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
110
112
  - `--output_size`: Maximum dimension for target image (default: 1024).
111
113
  - `--solver_size`: Maximum dimension for solver (fast) image (default: 128). \
112
114
  **Note:** We solve on a smaller size as this is many times faster, but also a bit less accurate. Increase if you need more accuracy.
113
- - `--decay`: Final tau value for the Gumbel-Softmax formulation (default: 0.01).
115
+ - `--init_tau`: Initial tau value for Gumbel-Softmax (default: 1.0).
116
+ - `--final_tau`: Final tau value for the Gumbel-Softmax formulation (default: 0.01).
114
117
  - `--visualize`: Flag to enable live visualization of the composite image during optimization.
118
+ - `--tensorboard`: Enable TensorBoard logging
119
+ - `--run-name`: Name of the run used for TensorBoard logging (optional).
115
120
  - `--perform_pruning`: Perform pruning after optimization (default: True). \
116
121
  **Note:** This is highly recommended even if you don't have a color/color swap limit, as it actually increases the quality of the output.
117
122
  - `--pruning_max_colors`: Max number of colors allowed after pruning (default: 100).
@@ -9,15 +9,20 @@ src/AutoForge.egg-info/top_level.txt
9
9
  src/autoforge/__init__.py
10
10
  src/autoforge/__main__.py
11
11
  src/autoforge/auto_forge.py
12
- src/autoforge/auto_forge_gradio.py
13
- src/autoforge/filamentcolors_library.py
14
12
  src/autoforge/old_jax_files.py
13
+ src/autoforge/Gui/__init__.py
14
+ src/autoforge/Gui/app.py
15
+ src/autoforge/Gui/main.py
16
+ src/autoforge/Gui/Views/__init__.py
17
+ src/autoforge/Gui/Views/backend_database.py
18
+ src/autoforge/Gui/Views/filament_editor.py
15
19
  src/autoforge/Helper/FilamentHelper.py
16
20
  src/autoforge/Helper/ImageHelper.py
17
21
  src/autoforge/Helper/OptimizerHelper.py
18
22
  src/autoforge/Helper/OutputHelper.py
19
23
  src/autoforge/Helper/PruningHelper.py
20
24
  src/autoforge/Helper/__init__.py
25
+ src/autoforge/Helper/filamentcolors_library.py
21
26
  src/autoforge/Loss/LossFunctions.py
22
27
  src/autoforge/Loss/PerceptionLoss.py
23
28
  src/autoforge/Loss/__init__.py
@@ -9,3 +9,4 @@ tqdm>=4.50.0
9
9
  pandas>=1.1.0
10
10
  scikit-image
11
11
  scikit-learn
12
+ tensorboard
@@ -0,0 +1,205 @@
1
+ import json
2
+ import os
3
+ import uuid
4
+
5
+ import streamlit as st
6
+ import sqlite3
7
+
8
+
9
+ DATABASE = "swatches.db"
10
+ JSON_FILE = "../swatches.json"
11
+
12
+
13
+ def get_db_connection():
14
+ conn = sqlite3.connect(DATABASE)
15
+ conn.row_factory = sqlite3.Row
16
+ return conn
17
+
18
+
19
+ def init_db():
20
+ conn = get_db_connection()
21
+ cursor = conn.cursor()
22
+ cursor.execute("""
23
+ CREATE TABLE IF NOT EXISTS swatches (
24
+ Uuid TEXT PRIMARY KEY,
25
+ Brand TEXT,
26
+ Name TEXT,
27
+ TD REAL,
28
+ HexColor TEXT
29
+ )
30
+ """)
31
+ cursor.execute("""
32
+ CREATE TABLE IF NOT EXISTS owned_filaments (
33
+ Uuid TEXT PRIMARY KEY,
34
+ Brand TEXT,
35
+ Name TEXT,
36
+ TD REAL,
37
+ HexColor TEXT
38
+ )
39
+ """)
40
+ cursor.execute("""
41
+ CREATE TABLE IF NOT EXISTS meta (
42
+ key TEXT PRIMARY KEY,
43
+ value TEXT
44
+ )
45
+ """)
46
+ conn.commit()
47
+ conn.close()
48
+
49
+
50
+ def get_db_connection():
51
+ conn = sqlite3.connect(DATABASE)
52
+ conn.row_factory = sqlite3.Row
53
+ return conn
54
+
55
+
56
+ def get_items():
57
+ conn = get_db_connection()
58
+ cursor = conn.cursor()
59
+ cursor.execute("SELECT * FROM swatches")
60
+ rows = cursor.fetchall()
61
+ conn.close()
62
+ return [dict(row) for row in rows]
63
+
64
+
65
+ def add_item(item):
66
+ new_uuid = str(uuid.uuid4())
67
+ hex_color = (
68
+ item["HexColor"] if item["HexColor"].startswith("#") else "#" + item["HexColor"]
69
+ )
70
+ conn = get_db_connection()
71
+ cursor = conn.cursor()
72
+ cursor.execute(
73
+ "INSERT INTO swatches (Uuid, Brand, Name, TD, HexColor) VALUES (?, ?, ?, ?, ?)",
74
+ (new_uuid, item["Brand"], item["Name"], item["TD"], hex_color),
75
+ )
76
+ conn.commit()
77
+ conn.close()
78
+ return {
79
+ "Uuid": new_uuid,
80
+ "Brand": item["Brand"],
81
+ "Name": item["Name"],
82
+ "TD": item["TD"],
83
+ "HexColor": hex_color,
84
+ }
85
+
86
+
87
+ def update_item(uuid_val, update_data):
88
+ conn = get_db_connection()
89
+ cursor = conn.cursor()
90
+ cursor.execute("SELECT * FROM swatches WHERE Uuid = ?", (uuid_val,))
91
+ row = cursor.fetchone()
92
+ if not row:
93
+ conn.close()
94
+ st.error("Item not found")
95
+ return None
96
+
97
+ updated_brand = update_data.get("Brand") or row["Brand"]
98
+ updated_name = update_data.get("Name") or row["Name"]
99
+ updated_td = (
100
+ update_data.get("TD") if update_data.get("TD") is not None else row["TD"]
101
+ )
102
+ updated_hex = update_data.get("HexColor") or row["HexColor"]
103
+ if not updated_hex.startswith("#"):
104
+ updated_hex = "#" + updated_hex
105
+
106
+ cursor.execute(
107
+ "UPDATE swatches SET Brand = ?, Name = ?, TD = ?, HexColor = ? WHERE Uuid = ?",
108
+ (updated_brand, updated_name, updated_td, updated_hex, uuid_val),
109
+ )
110
+ conn.commit()
111
+ cursor.execute("SELECT * FROM swatches WHERE Uuid = ?", (uuid_val,))
112
+ updated_row = cursor.fetchone()
113
+ conn.close()
114
+ return dict(updated_row)
115
+
116
+
117
+ def delete_item(uuid_val):
118
+ conn = get_db_connection()
119
+ cursor = conn.cursor()
120
+ cursor.execute("DELETE FROM swatches WHERE Uuid = ?", (uuid_val,))
121
+ if cursor.rowcount == 0:
122
+ conn.close()
123
+ st.error("Item not found")
124
+ return False
125
+ conn.commit()
126
+ conn.close()
127
+ return True
128
+
129
+
130
+ def get_owned_filaments():
131
+ conn = get_db_connection()
132
+ cursor = conn.cursor()
133
+ cursor.execute("SELECT * FROM owned_filaments")
134
+ rows = cursor.fetchall()
135
+ conn.close()
136
+ return [dict(row) for row in rows]
137
+
138
+
139
+ def add_owned_filament(item):
140
+ new_uuid = str(uuid.uuid4())
141
+ hex_color = (
142
+ item["HexColor"] if item["HexColor"].startswith("#") else "#" + item["HexColor"]
143
+ )
144
+ conn = get_db_connection()
145
+ cursor = conn.cursor()
146
+ cursor.execute(
147
+ "INSERT INTO owned_filaments (Uuid, Brand, Name, TD, HexColor) VALUES (?, ?, ?, ?, ?)",
148
+ (new_uuid, item["Brand"], item["Name"], item["TD"], hex_color),
149
+ )
150
+ conn.commit()
151
+ conn.close()
152
+ return {
153
+ "Uuid": new_uuid,
154
+ "Brand": item["Brand"],
155
+ "Name": item["Name"],
156
+ "TD": item["TD"],
157
+ "HexColor": hex_color,
158
+ }
159
+
160
+
161
+ def delete_owned_filament(uuid_val):
162
+ conn = get_db_connection()
163
+ cursor = conn.cursor()
164
+ cursor.execute("DELETE FROM owned_filaments WHERE Uuid = ?", (uuid_val,))
165
+ if cursor.rowcount == 0:
166
+ conn.close()
167
+ st.error("Owned filament not found")
168
+ return False
169
+ conn.commit()
170
+ conn.close()
171
+ return True
172
+
173
+
174
+ def import_json_data():
175
+ if not os.path.exists(JSON_FILE):
176
+ return
177
+ mtime = os.path.getmtime(JSON_FILE)
178
+ conn = get_db_connection()
179
+ cursor = conn.cursor()
180
+ cursor.execute("SELECT value FROM meta WHERE key = ?", ("json_mtime",))
181
+ row = cursor.fetchone()
182
+ stored_mtime = float(row["value"]) if row else 0
183
+
184
+ if mtime > stored_mtime:
185
+ with open(JSON_FILE, "r", encoding="utf-8") as f:
186
+ data = json.load(f)
187
+ for item in data:
188
+ cursor.execute("SELECT 1 FROM swatches WHERE Uuid = ?", (item["Uuid"],))
189
+ if not cursor.fetchone():
190
+ cursor.execute(
191
+ "INSERT INTO swatches (Uuid, Brand, Name, TD, HexColor) VALUES (?, ?, ?, ?, ?)",
192
+ (
193
+ item["Uuid"],
194
+ item["Brand"],
195
+ item["Name"],
196
+ item["TD"],
197
+ item["HexColor"],
198
+ ),
199
+ )
200
+ cursor.execute(
201
+ "INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)",
202
+ ("json_mtime", str(mtime)),
203
+ )
204
+ conn.commit()
205
+ conn.close()
@@ -0,0 +1,216 @@
1
+ import ast
2
+
3
+ import streamlit as st
4
+ import pandas as pd
5
+ import hashlib
6
+
7
+ from st_aggrid import AgGrid, GridOptionsBuilder, GridUpdateMode, JsCode
8
+
9
+ from autoforge.Gui.Views.backend_database import (
10
+ get_items,
11
+ get_owned_filaments,
12
+ add_owned_filament,
13
+ delete_owned_filament,
14
+ update_item,
15
+ add_item,
16
+ )
17
+
18
+
19
+ def get_grid_key(df):
20
+ # Create a hash from the DataFrame contents
21
+ hash_obj = hashlib.sha256(
22
+ pd.util.hash_pandas_object(df, index=True).values.tobytes()
23
+ )
24
+ return hash_obj.hexdigest()
25
+
26
+
27
+ hex_renderer = JsCode("""
28
+ class ColorCellEditor {
29
+ init(params) {
30
+ this.eInput = document.createElement('input');
31
+ this.eInput.type = 'color';
32
+ this.eInput.value = params.value;
33
+ this.eInput.style.width = '60px';
34
+ this.eInput.style.height = '25px';
35
+ this.eInput.style.padding = '0';
36
+ this.eInput.style.margin = '0';
37
+ this.eInput.style.border = 'none';
38
+ this.eInput.style.outline = 'none';
39
+ this.eInput.style.borderRadius = '0';
40
+ this.eInput.style.boxShadow = 'none';
41
+ this.eInput.style.appearance = 'none';
42
+ this.eInput.style.webkitAppearance = 'none';
43
+ this.eInput.style.MozAppearance = 'none';
44
+ }
45
+ getGui() {
46
+ return this.eInput;
47
+ }
48
+ afterGuiAttached() {
49
+ this.eInput.focus();
50
+ }
51
+ getValue() {
52
+ return this.eInput.value;
53
+ }
54
+ destroy() {}
55
+ isPopup() {
56
+ return false;
57
+ }
58
+ }
59
+ """)
60
+
61
+
62
+ def show_filament_editor():
63
+ st.title("Filament Editor")
64
+
65
+ cols_order = ["Brand", "Name", "TD", "HexColor", "Uuid"]
66
+ # Always load fresh data from the database
67
+ swatches_data = pd.DataFrame(get_items())[cols_order]
68
+ owned_data = pd.DataFrame(get_owned_filaments())[cols_order]
69
+
70
+ # Configure AgGrid for swatches
71
+ gb_main = GridOptionsBuilder.from_dataframe(swatches_data)
72
+ gb_main.configure_default_column(editable=True)
73
+ gb_main.configure_column("Uuid", hide=True)
74
+ gb_main.configure_selection("multiple", use_checkbox=True)
75
+ gb_main.configure_column(
76
+ "HexColor", cellRenderer=hex_renderer, cellEditor=hex_renderer
77
+ )
78
+ gridOptions_main = gb_main.build()
79
+
80
+ # Configure AgGrid for owned filaments
81
+ if not owned_data.empty:
82
+ gb_owned = GridOptionsBuilder.from_dataframe(owned_data)
83
+ else:
84
+ gb_owned = GridOptionsBuilder.from_dataframe(
85
+ pd.DataFrame(columns=swatches_data.columns)
86
+ )
87
+ gb_owned.configure_selection("multiple", use_checkbox=True)
88
+ gb_owned.configure_column("Uuid", hide=True)
89
+ gb_owned.configure_column(
90
+ "HexColor", cellRenderer=hex_renderer, cellEditor=hex_renderer
91
+ )
92
+ gridOptions_owned = gb_owned.build()
93
+
94
+ # Generate a dynamic key for the AgGrid based on current swatches_data
95
+ grid_key = get_grid_key(swatches_data)
96
+
97
+ col1, col2 = st.columns(2)
98
+
99
+ with col1:
100
+ st.subheader("All Filaments")
101
+ grid_response = AgGrid(
102
+ swatches_data,
103
+ gridOptions=gridOptions_main,
104
+ update_mode=GridUpdateMode.SELECTION_CHANGED,
105
+ reload_data=True,
106
+ allow_unsafe_jscode=True,
107
+ height=400,
108
+ key=grid_key, # dynamic key forces re-render on data change
109
+ )
110
+ if st.button("Add Selected to Owned"):
111
+ selected_rows = grid_response.get("selected_rows", [])
112
+ current_owned = pd.DataFrame(get_owned_filaments())
113
+ added_count = 0
114
+ for row in selected_rows.to_dict("records"):
115
+ exists = False
116
+ if not current_owned.empty:
117
+ dup = current_owned[
118
+ (current_owned["Brand"] == row["Brand"])
119
+ & (current_owned["Name"] == row["Name"])
120
+ & (current_owned["TD"] == row["TD"])
121
+ & (current_owned["HexColor"] == row["HexColor"])
122
+ ]
123
+ if not dup.empty:
124
+ exists = True
125
+ if not exists:
126
+ add_owned_filament(
127
+ {
128
+ "Brand": row["Brand"],
129
+ "Name": row["Name"],
130
+ "TD": row["TD"],
131
+ "HexColor": row["HexColor"],
132
+ }
133
+ )
134
+ added_count += 1
135
+ if added_count > 0:
136
+ st.success(f"Added {added_count} filament(s) to owned filaments.")
137
+ st.rerun()
138
+ else:
139
+ st.warning("No new filaments were added to owned.")
140
+
141
+ with col2:
142
+ st.subheader("Owned Filaments")
143
+ if not owned_data.empty:
144
+ owned_grid = AgGrid(
145
+ owned_data,
146
+ gridOptions=gridOptions_owned,
147
+ update_mode=GridUpdateMode.SELECTION_CHANGED,
148
+ reload_data=True,
149
+ allow_unsafe_jscode=True,
150
+ height=400,
151
+ )
152
+ if st.button("Remove Selected from Owned"):
153
+ selected_owned = owned_grid.get("selected_rows", [])
154
+ removed_count = 0
155
+ for row in selected_owned.to_dict("records"):
156
+ if delete_owned_filament(row["Uuid"]):
157
+ removed_count += 1
158
+ if removed_count > 0:
159
+ st.success(
160
+ f"Removed {removed_count} filament(s) from owned filaments."
161
+ )
162
+ st.rerun()
163
+ else:
164
+ st.warning("No filaments selected for removal.")
165
+ else:
166
+ st.write("No owned filaments yet.")
167
+
168
+ st.markdown("### Save Changes")
169
+ if st.button("Save Changes"):
170
+ # The "data" returned by AgGrid can sometimes be a list of strings or a list of dicts
171
+ updated_data = grid_response["data"]
172
+ if (
173
+ isinstance(updated_data, list)
174
+ and updated_data
175
+ and isinstance(updated_data[0], str)
176
+ ):
177
+ try:
178
+ updated_data = [ast.literal_eval(row) for row in updated_data]
179
+ except Exception as e:
180
+ st.error(f"Error converting row data: {e}")
181
+ updated_df = pd.DataFrame(updated_data)
182
+ for index, row in updated_df.iterrows():
183
+ uuid_val = row["Uuid"]
184
+ update_data = {
185
+ "Brand": row["Brand"],
186
+ "Name": row["Name"],
187
+ "TD": row["TD"],
188
+ "HexColor": row["HexColor"],
189
+ }
190
+ updated = update_item(uuid_val, update_data)
191
+ if not updated:
192
+ st.error(f"Error updating row {uuid_val}")
193
+ st.success("Changes saved.")
194
+ st.rerun()
195
+
196
+ st.markdown("### Add New Filament")
197
+ with st.form("add_swatch_form"):
198
+ new_brand = st.text_input("Brand")
199
+ new_name = st.text_input("Name")
200
+ new_td = st.number_input("TD", value=0.0, step=0.1)
201
+ new_hex = st.color_picker("Hex Color", "#ffffff")
202
+ submitted = st.form_submit_button("Add filament")
203
+ if submitted:
204
+ new_data = {
205
+ "Brand": new_brand,
206
+ "Name": new_name,
207
+ "TD": new_td,
208
+ "HexColor": new_hex,
209
+ }
210
+ added = add_item(new_data)
211
+ add_owned_filament(new_data)
212
+ if added:
213
+ st.success("Filament added.")
214
+ st.rerun()
215
+ else:
216
+ st.error("Error adding filament.")