diff --git a/__init__.py b/__init__.py
index 201080e..2378569 100644
--- a/__init__.py
+++ b/__init__.py
@@ -2,7 +2,7 @@
     "name": "Node to Python", 
     "description": "Convert Blender node groups to a Python add-on!",
     "author": "Brendan Parmer",
-    "version": (2, 2, 0),
+    "version": (3, 0, 0),
     "blender": (3, 0, 0),
     "location": "Node", 
     "category": "Node",
@@ -10,16 +10,19 @@
 
 if "bpy" in locals():
     import importlib
-    importlib.reload(materials)
-    importlib.reload(geo_nodes)
+    importlib.reload(compositor)
+    importlib.reload(geometry)
+    importlib.reload(material)
     importlib.reload(options)
 else:
-    from . import materials
-    from . import geo_nodes
+    from . import compositor
+    from . import geometry
+    from . import material
     from . import options
 
 import bpy
 
+
 class NodeToPythonMenu(bpy.types.Menu):
     bl_idname = "NODE_MT_node_to_python"
     bl_label = "Node To Python"
@@ -33,17 +36,23 @@ def draw(self, context):
         layout.operator_context = 'INVOKE_DEFAULT'
 
 
-
-
 classes = [NodeToPythonMenu,
+            #options
             options.NTPOptions,
-            geo_nodes.GeoNodesToPython,
-            geo_nodes.SelectGeoNodesMenu,
-            geo_nodes.GeoNodesToPythonPanel,
-            materials.MaterialToPython,
-            materials.SelectMaterialMenu,
-            materials.MaterialToPythonPanel,
-            options.NTPOptionsPanel
+            options.NTPOptionsPanel,
+            #compositor
+            compositor.operator.NTPCompositorOperator,
+            compositor.ui.NTPCompositorScenesMenu,
+            compositor.ui.NTPCompositorGroupsMenu,
+            compositor.ui.NTPCompositorPanel,
+            #geometry
+            geometry.operator.NTPGeoNodesOperator,
+            geometry.ui.NTPGeoNodesMenu,
+            geometry.ui.NTPGeoNodesPanel,
+            #material
+            material.operator.NTPMaterialOperator,
+            material.ui.NTPMaterialMenu,
+            material.ui.NTPMaterialPanel,
             ]
 
 def register():
diff --git a/compositor/__init__.py b/compositor/__init__.py
new file mode 100644
index 0000000..ceb3a04
--- /dev/null
+++ b/compositor/__init__.py
@@ -0,0 +1,11 @@
+if "bpy" in locals():
+    import importlib
+    importlib.reload(node_settings)
+    importlib.reload(operator)
+    importlib.reload(ui)
+else:
+    from . import node_settings
+    from . import operator
+    from . import ui
+
+import bpy
\ No newline at end of file
diff --git a/compositor/node_settings.py b/compositor/node_settings.py
new file mode 100644
index 0000000..8034b30
--- /dev/null
+++ b/compositor/node_settings.py
@@ -0,0 +1,610 @@
+from ..utils import ST, NTPNodeSetting
+
+compositor_node_settings : dict[str, list[NTPNodeSetting]] = {
+    # INPUT
+    'CompositorNodeBokehImage' : [
+        NTPNodeSetting("angle",        ST.FLOAT),
+        NTPNodeSetting("catadioptric", ST.FLOAT),
+        NTPNodeSetting("flaps",        ST.INT),
+        NTPNodeSetting("rounding",     ST.FLOAT),
+        NTPNodeSetting("shift",        ST.FLOAT)
+    ],
+
+    'CompositorNodeImage' : [
+        NTPNodeSetting("frame_duration",            ST.INT),
+        NTPNodeSetting("frame_offset",              ST.INT),
+        NTPNodeSetting("frame_start",               ST.INT),
+        NTPNodeSetting("image",                     ST.IMAGE),
+        NTPNodeSetting("layer",                     ST.ENUM),
+        NTPNodeSetting("use_auto_refresh",          ST.BOOL),
+        NTPNodeSetting("use_cyclic",                ST.BOOL),
+        NTPNodeSetting("use_straight_alpha_output", ST.BOOL),
+        NTPNodeSetting("view",                      ST.ENUM)
+    ],
+
+    'CompositorNodeMask' : [
+        NTPNodeSetting("mask",                ST.MASK),
+        NTPNodeSetting("motion_blur_samples", ST.INT),
+        NTPNodeSetting("motion_blur_shutter", ST.FLOAT),
+        NTPNodeSetting("size_source",         ST.ENUM),
+        NTPNodeSetting("size_x",              ST.INT),
+        NTPNodeSetting("size_y",              ST.INT),
+        NTPNodeSetting("use_feather",         ST.BOOL),
+        NTPNodeSetting("use_motion_blur",     ST.BOOL)
+    ],
+
+    'CompositorNodeMovieClip' : [
+        NTPNodeSetting("clip", ST.MOVIE_CLIP)
+    ],
+
+    'CompositorNodeTexture' : [
+        NTPNodeSetting("node_output", ST.INT), #TODO: ??
+        NTPNodeSetting("texture",     ST.TEXTURE)
+    ],
+
+    # Input > Constant
+    'CompositorNodeRGB' : [],
+    'CompositorNodeValue' : [],
+
+    # Input > Scene
+    'CompositorNodeRLayers' : [
+        NTPNodeSetting("layer", ST.ENUM),
+        NTPNodeSetting("scene", ST.SCENE)
+    ],
+
+    'CompositorNodeSceneTime' : [],
+
+    'CompositorNodeTime' : [
+        NTPNodeSetting("curve",       ST.CURVE_MAPPING),
+        NTPNodeSetting("frame_end",   ST.INT),
+        NTPNodeSetting("frame_start", ST.INT)
+    ],
+
+
+    # OUTPUT
+    'CompositorNodeComposite' : [
+        NTPNodeSetting("use_alpha", ST.BOOL)
+    ],
+
+    'CompositorNodeOutputFile'  : [
+        NTPNodeSetting("active_input_index", ST.INT), #TODO: probably not right at all
+        NTPNodeSetting("base_path",          ST.STRING),
+        NTPNodeSetting("file_slots",         ST.FILE_SLOTS),
+        NTPNodeSetting("format",             ST.IMAGE_FORMAT_SETTINGS),
+        NTPNodeSetting("layer_slots",        ST.LAYER_SLOTS)
+    ],
+
+    'CompositorNodeViewer' : [
+        NTPNodeSetting("center_x",   ST.FLOAT),
+        NTPNodeSetting("center_y",   ST.FLOAT),
+        NTPNodeSetting("tile_order", ST.ENUM),
+        NTPNodeSetting("use_alpha",  ST.BOOL)
+    ],    
+    
+    'CompositorNodeSplitViewer' : [
+        NTPNodeSetting("axis",   ST.ENUM),
+        NTPNodeSetting("factor", ST.INT)
+    ],
+
+
+    # COLOR
+    'CompositorNodePremulKey' : [
+        NTPNodeSetting("mapping", ST.ENUM)
+    ],
+
+    'CompositorNodeValToRGB' : [
+        NTPNodeSetting("color_ramp", ST.COLOR_RAMP)
+    ],
+
+    'CompositorNodeConvertColorSpace' : [
+        NTPNodeSetting("from_color_space", ST.ENUM, min_version=(3, 1, 0)),
+        NTPNodeSetting("to_color_space",   ST.ENUM, min_version=(3, 1, 0))
+    ],
+
+    'CompositorNodeSetAlpha' : [
+        NTPNodeSetting("mode", ST.ENUM)
+    ],
+
+    'CompositorNodeInvert' : [
+        NTPNodeSetting("invert_alpha", ST.BOOL),
+        NTPNodeSetting("invert_rgb",   ST.BOOL)
+    ],
+
+    'CompositorNodeRGBToBW' : [],
+
+    # Color > Adjust
+    'CompositorNodeBrightContrast' : [
+        NTPNodeSetting("use_premultiply", ST.BOOL)
+    ],
+
+    'CompositorNodeColorBalance' : [
+        NTPNodeSetting("correction_method", ST.ENUM),
+        NTPNodeSetting("gain",              ST.COLOR),
+        NTPNodeSetting("gamma",             ST.COLOR),
+        NTPNodeSetting("lift",              ST.COLOR),
+        NTPNodeSetting("offset",            ST.COLOR),
+        NTPNodeSetting("offset_basis",      ST.FLOAT),
+        NTPNodeSetting("power",             ST.COLOR),
+        NTPNodeSetting("slope",             ST.COLOR)
+    ],
+
+    'CompositorNodeColorCorrection' : [
+        NTPNodeSetting("red",   ST.BOOL),
+        NTPNodeSetting("green", ST.BOOL),
+        NTPNodeSetting("blue",  ST.BOOL),
+        #master
+        NTPNodeSetting("master_saturation", ST.FLOAT),
+        NTPNodeSetting("master_contrast",   ST.FLOAT),
+        NTPNodeSetting("master_gamma",      ST.FLOAT),
+        NTPNodeSetting("master_gain",       ST.FLOAT),
+        NTPNodeSetting("master_lift",       ST.FLOAT),
+        #highlights
+        NTPNodeSetting("highlights_saturation", ST.FLOAT),
+        NTPNodeSetting("highlights_contrast",   ST.FLOAT),
+        NTPNodeSetting("highlights_gamma",      ST.FLOAT),
+        NTPNodeSetting("highlights_gain",       ST.FLOAT),
+        NTPNodeSetting("highlights_lift",       ST.FLOAT),
+        #midtones
+        NTPNodeSetting("midtones_saturation", ST.FLOAT),
+        NTPNodeSetting("midtones_contrast",   ST.FLOAT),
+        NTPNodeSetting("midtones_gamma",      ST.FLOAT),
+        NTPNodeSetting("midtones_gain",       ST.FLOAT),
+        NTPNodeSetting("midtones_lift",       ST.FLOAT),
+        #shadows
+        NTPNodeSetting("shadows_saturation", ST.FLOAT),
+        NTPNodeSetting("shadows_contrast",   ST.FLOAT),
+        NTPNodeSetting("shadows_gamma",      ST.FLOAT),
+        NTPNodeSetting("shadows_gain",       ST.FLOAT),
+        NTPNodeSetting("shadows_lift",       ST.FLOAT),
+        #midtones location
+        NTPNodeSetting("midtones_start", ST.FLOAT),
+        NTPNodeSetting("midtones_end",   ST.FLOAT)
+    ],
+
+    'CompositorNodeExposure' : [],
+    'CompositorNodeGamma' : [],
+
+    'CompositorNodeHueCorrect' : [
+        NTPNodeSetting("mapping", ST.CURVE_MAPPING)
+    ],
+
+    'CompositorNodeHueSat' : [],
+
+    'CompositorNodeCurveRGB' : [
+        NTPNodeSetting("mapping", ST.CURVE_MAPPING)
+    ],
+
+    'CompositorNodeTonemap' : [
+        NTPNodeSetting("adaptation",   ST.FLOAT),
+        NTPNodeSetting("contrast",     ST.FLOAT),
+        NTPNodeSetting("correction",   ST.FLOAT),
+        NTPNodeSetting("gamma",        ST.FLOAT),
+        NTPNodeSetting("intensity",    ST.FLOAT),
+        NTPNodeSetting("key",          ST.FLOAT),
+        NTPNodeSetting("offset",       ST.FLOAT),
+        NTPNodeSetting("tonemap_type", ST.ENUM)
+    ],
+
+    
+    # Color > Mix
+    'CompositorNodeAlphaOver' : [
+        NTPNodeSetting("premul",          ST.FLOAT),
+        NTPNodeSetting("use_premultiply", ST.BOOL)
+    ],
+
+    'CompositorNodeCombineColor' : [
+        NTPNodeSetting("mode",     ST.ENUM, min_version = (3, 3, 0)),
+        NTPNodeSetting("ycc_mode", ST.ENUM, min_version = (3, 3, 0))
+    ],
+
+    'CompositorNodeSeparateColor' : [
+        NTPNodeSetting("mode",     ST.ENUM, min_version = (3, 3, 0)),
+        NTPNodeSetting("ycc_mode", ST.ENUM, min_version = (3, 3, 0))
+    ],
+
+    'CompositorNodeMixRGB' : [
+        NTPNodeSetting("blend_type", ST.ENUM),
+        NTPNodeSetting("use_alpha",  ST.BOOL),
+        NTPNodeSetting("use_clamp",  ST.BOOL)
+    ],
+
+    'CompositorNodeZcombine' : [
+        NTPNodeSetting("use_alpha",       ST.BOOL),
+        NTPNodeSetting("use_antialias_z", ST.BOOL)
+    ],
+
+
+    # FILTER
+    'CompositorNodeAntiAliasing' : [
+        NTPNodeSetting("contrast_limit",  ST.FLOAT),
+        NTPNodeSetting("corner_rounding", ST.FLOAT),
+        NTPNodeSetting("threshold",       ST.FLOAT)
+    ],
+    
+    'CompositorNodeDenoise' : [
+        NTPNodeSetting("prefilter", ST.ENUM),
+        NTPNodeSetting("use_hdr",   ST.BOOL)
+    ],
+
+    'CompositorNodeDespeckle' : [
+        NTPNodeSetting("threshold",          ST.FLOAT),
+        NTPNodeSetting("threshold_neighbor", ST.FLOAT)
+    ],
+
+    'CompositorNodeDilateErode' : [
+        NTPNodeSetting("distance", ST.INT),
+        NTPNodeSetting("edge",     ST.FLOAT),
+        NTPNodeSetting("falloff",  ST.ENUM),
+        NTPNodeSetting("mode",     ST.ENUM)
+    ],
+
+    'CompositorNodeInpaint' : [
+        NTPNodeSetting("distance", ST.INT)
+    ],
+
+    'CompositorNodeFilter' : [
+        NTPNodeSetting("filter_type", ST.ENUM)
+    ],
+
+    'CompositorNodeGlare' : [
+        NTPNodeSetting("angle_offset",     ST.FLOAT),
+        NTPNodeSetting("color_modulation", ST.FLOAT),
+        NTPNodeSetting("fade",             ST.FLOAT),
+        NTPNodeSetting("glare_type",       ST.ENUM),
+        NTPNodeSetting("iterations",       ST.INT),
+        NTPNodeSetting("mix",              ST.FLOAT),
+        NTPNodeSetting("quality",          ST.ENUM),
+        NTPNodeSetting("size",             ST.INT),
+        NTPNodeSetting("streaks",          ST.INT),
+        NTPNodeSetting("threshold",        ST.FLOAT),
+        NTPNodeSetting("use_rotate_45",    ST.BOOL)
+    ],
+    
+    'CompositorNodeKuwahara' : [
+        NTPNodeSetting("eccentricity", ST.FLOAT, min_version = (4, 0, 0)),
+        NTPNodeSetting("sharpness",    ST.FLOAT, min_version = (4, 0, 0)),
+        NTPNodeSetting("size",         ST.INT,   min_version = (4, 0, 0)),
+        NTPNodeSetting("uniformity",   ST.INT,   min_version = (4, 0, 0)),
+        NTPNodeSetting("variation",    ST.ENUM,  min_version = (4, 0, 0))
+    ],
+
+    'CompositorNodePixelate' : [],
+    'CompositorNodePosterize' : [],
+
+    'CompositorNodeSunBeams' : [
+        NTPNodeSetting("ray_length", ST.FLOAT),
+        NTPNodeSetting("source",     ST.VEC2)
+    ],
+
+    # Filter > Blur
+    'CompositorNodeBilateralblur' : [
+        NTPNodeSetting("iterations",  ST.INT),
+        NTPNodeSetting("sigma_color", ST.FLOAT),
+        NTPNodeSetting("sigma_space", ST.FLOAT)
+    ],
+
+    'CompositorNodeBlur' : [
+        NTPNodeSetting("aspect_correction",    ST.ENUM),
+        NTPNodeSetting("factor",               ST.FLOAT),
+        NTPNodeSetting("factor_x",             ST.FLOAT),
+        NTPNodeSetting("factor_y",             ST.FLOAT),
+        NTPNodeSetting("filter_type",          ST.ENUM),
+        NTPNodeSetting("size_x",               ST.INT),
+        NTPNodeSetting("size_y",               ST.INT),
+        NTPNodeSetting("use_bokeh",            ST.BOOL),
+        NTPNodeSetting("use_extended_bounds",  ST.BOOL),
+        NTPNodeSetting("use_gamma_correction", ST.BOOL),
+        NTPNodeSetting("use_relative",         ST.BOOL),
+        NTPNodeSetting("use_variable_size",    ST.BOOL)
+    ],
+
+    'CompositorNodeBokehBlur' : [
+        NTPNodeSetting("blur_max",            ST.FLOAT),
+        NTPNodeSetting("use_extended_bounds", ST.BOOL), 
+        NTPNodeSetting("use_variable_size",   ST.BOOL)
+    ],
+
+    'CompositorNodeDefocus' : [
+        NTPNodeSetting("angle",                ST.FLOAT),
+        NTPNodeSetting("blur_max",             ST.FLOAT),
+        NTPNodeSetting("bokeh",                ST.ENUM),
+        NTPNodeSetting("f_stop",               ST.FLOAT),
+        NTPNodeSetting("scene",                ST.SCENE),
+        NTPNodeSetting("threshold",            ST.FLOAT),
+        NTPNodeSetting("use_gamma_correction", ST.BOOL),
+        NTPNodeSetting("use_preview",          ST.BOOL),
+        NTPNodeSetting("use_zbuffer",          ST.BOOL),
+        NTPNodeSetting("z_scale",              ST.FLOAT)
+    ],
+
+    'CompositorNodeDBlur' : [
+        NTPNodeSetting("angle",      ST.FLOAT),
+        NTPNodeSetting("center_x",   ST.FLOAT),
+        NTPNodeSetting("center_y",   ST.FLOAT),
+        NTPNodeSetting("distance",   ST.FLOAT),
+        NTPNodeSetting("iterations", ST.INT),
+        NTPNodeSetting("spin",       ST.FLOAT),
+        NTPNodeSetting("use_wrap",   ST.BOOL, max_version = (3, 4, 0)),
+        NTPNodeSetting("zoom",       ST.FLOAT)
+    ],
+    
+    'CompositorNodeVecBlur' : [
+        NTPNodeSetting("factor",     ST.FLOAT),
+        NTPNodeSetting("samples",    ST.INT),
+        NTPNodeSetting("speed_max",  ST.INT),
+        NTPNodeSetting("speed_min",  ST.INT),
+        NTPNodeSetting("use_curved", ST.BOOL)
+    ],
+
+    
+    # KEYING
+    'CompositorNodeChannelMatte' : [
+        NTPNodeSetting("color_space",   ST.ENUM),
+        NTPNodeSetting("limit_channel", ST.ENUM),
+        NTPNodeSetting("limit_max",     ST.FLOAT),
+        NTPNodeSetting("limit_method",  ST.ENUM),
+        NTPNodeSetting("limit_min",     ST.FLOAT),
+        NTPNodeSetting("matte_channel", ST.ENUM)
+    ],
+
+    'CompositorNodeChromaMatte' : [
+        NTPNodeSetting("gain",          ST.FLOAT),
+        NTPNodeSetting("lift",          ST.FLOAT),
+        NTPNodeSetting("shadow_adjust", ST.FLOAT),
+        NTPNodeSetting("threshold",     ST.FLOAT),
+        NTPNodeSetting("tolerance",     ST.FLOAT)
+    ],
+
+    'CompositorNodeColorMatte' : [
+        NTPNodeSetting("color_hue",        ST.FLOAT),
+        NTPNodeSetting("color_saturation", ST.FLOAT),
+        NTPNodeSetting("color_value",      ST.FLOAT)
+    ],
+
+    'CompositorNodeColorSpill' : [
+        NTPNodeSetting("channel",       ST.ENUM),
+        NTPNodeSetting("limit_channel", ST.ENUM),
+        NTPNodeSetting("limit_method",  ST.ENUM),
+        NTPNodeSetting("ratio",         ST.FLOAT),
+        NTPNodeSetting("unspill_blue",  ST.FLOAT),
+        NTPNodeSetting("unspill_green", ST.FLOAT),
+        NTPNodeSetting("unspill_red",   ST.FLOAT),
+        NTPNodeSetting("use_unspill",   ST.BOOL)
+    ],
+
+    'CompositorNodeDiffMatte' : [
+        NTPNodeSetting("falloff",   ST.FLOAT),
+        NTPNodeSetting("tolerance", ST.FLOAT)
+    ],
+
+    'CompositorNodeDistanceMatte' : [
+        NTPNodeSetting("channel",   ST.ENUM),
+        NTPNodeSetting("falloff",   ST.FLOAT),
+        NTPNodeSetting("tolerance", ST.FLOAT)
+    ],
+
+    'CompositorNodeKeying' : [
+        NTPNodeSetting("blur_post",             ST.INT),
+        NTPNodeSetting("blur_pre",              ST.INT),
+        NTPNodeSetting("clip_black",            ST.FLOAT),
+        NTPNodeSetting("clip_white",            ST.FLOAT),
+        NTPNodeSetting("despill_balance",       ST.FLOAT),
+        NTPNodeSetting("despill_factor",        ST.FLOAT),
+        NTPNodeSetting("dilate_distance",       ST.INT),
+        NTPNodeSetting("edge_kernel_radius",    ST.INT),
+        NTPNodeSetting("edge_kernel_tolerance", ST.FLOAT),
+        NTPNodeSetting("feather_distance",      ST.INT),
+        NTPNodeSetting("feather_falloff",       ST.ENUM),
+        NTPNodeSetting("screen_balance",        ST.FLOAT)
+    ],
+
+    'CompositorNodeKeyingScreen' : [
+        NTPNodeSetting("clip",            ST.MOVIE_CLIP),
+        NTPNodeSetting("tracking_object", ST.STRING)
+    ],
+
+    'CompositorNodeLumaMatte' : [
+        NTPNodeSetting("limit_max", ST.FLOAT),
+        NTPNodeSetting("limit_min", ST.FLOAT)
+    ],
+
+
+    # MASK
+    'CompositorNodeCryptomatteV2' : [
+        NTPNodeSetting("add",              ST.COLOR),
+        NTPNodeSetting("entries",          ST.CRYPTOMATTE_ENTRIES),
+        NTPNodeSetting("frame_duration",   ST.INT),
+        NTPNodeSetting("frame_offset",     ST.INT),
+        NTPNodeSetting("frame_start",      ST.INT),
+        NTPNodeSetting("image",            ST.IMAGE),
+        NTPNodeSetting("layer",            ST.ENUM),
+        NTPNodeSetting("layer_name",       ST.ENUM),
+        NTPNodeSetting("matte_id",         ST.STRING),
+        NTPNodeSetting("remove",           ST.COLOR),
+        NTPNodeSetting("scene",            ST.SCENE),
+        NTPNodeSetting("source",           ST.ENUM),
+        NTPNodeSetting("use_auto_refresh", ST.BOOL),
+        NTPNodeSetting("use_cyclic",       ST.BOOL),
+        NTPNodeSetting("view",             ST.ENUM)
+    ],
+
+    'CompositorNodeCryptomatte' : [
+        NTPNodeSetting("add",      ST.COLOR), #TODO: may need special handling
+        NTPNodeSetting("matte_id", ST.STRING),
+        NTPNodeSetting("remove",   ST.COLOR)
+    ],
+
+    'CompositorNodeBoxMask' : [
+        NTPNodeSetting("height",    ST.FLOAT),
+        NTPNodeSetting("mask_type", ST.ENUM),
+        NTPNodeSetting("rotation",  ST.FLOAT),
+        NTPNodeSetting("width",     ST.FLOAT),
+        NTPNodeSetting("x",         ST.FLOAT),
+        NTPNodeSetting("y",         ST.FLOAT)
+    ],
+
+    'CompositorNodeEllipseMask' : [
+        NTPNodeSetting("height",    ST.FLOAT),
+        NTPNodeSetting("mask_type", ST.ENUM),
+        NTPNodeSetting("rotation",  ST.FLOAT),
+        NTPNodeSetting("width",     ST.FLOAT),
+        NTPNodeSetting("x",         ST.FLOAT),
+        NTPNodeSetting("y",         ST.FLOAT)
+    ],
+
+    'CompositorNodeDoubleEdgeMask' : [
+        NTPNodeSetting("edge_mode",  ST.ENUM),
+        NTPNodeSetting("inner_mode", ST.ENUM)
+    ],
+
+    'CompositorNodeIDMask' : [
+        NTPNodeSetting("index",            ST.INT),
+        NTPNodeSetting("use_antialiasing", ST.BOOL)
+    ],
+
+
+    # TRACKING
+    'CompositorNodePlaneTrackDeform' : [
+        NTPNodeSetting("clip",                ST.MOVIE_CLIP),
+        NTPNodeSetting("motion_blur_samples", ST.INT),
+        NTPNodeSetting("motion_blur_shutter", ST.FLOAT),
+        NTPNodeSetting("plane_track_name",    ST.STRING),
+        NTPNodeSetting("tracking_object",     ST.STRING),
+        NTPNodeSetting("use_motion_blur",     ST.BOOL)
+    ],
+
+    'CompositorNodeStabilize' : [
+        NTPNodeSetting("clip",        ST.MOVIE_CLIP),
+        NTPNodeSetting("filter_type", ST.ENUM),
+        NTPNodeSetting("invert",      ST.BOOL)
+    ],
+
+    'CompositorNodeTrackPos' : [
+        NTPNodeSetting("clip",            ST.MOVIE_CLIP),
+        NTPNodeSetting("frame_relative",  ST.INT),
+        NTPNodeSetting("position",        ST.ENUM),
+        NTPNodeSetting("track_name",      ST.STRING), #TODO: probably not right
+        NTPNodeSetting("tracking_object", ST.STRING)
+    ],
+
+
+    # TRANSFORM
+    'CompositorNodeRotate' : [
+        NTPNodeSetting("filter_type",  ST.ENUM)
+    ],
+
+    'CompositorNodeScale' : [
+        NTPNodeSetting("frame_method", ST.ENUM),
+        NTPNodeSetting("offset_x",     ST.FLOAT),
+        NTPNodeSetting("offset_y",     ST.FLOAT),
+        NTPNodeSetting("space",        ST.ENUM)
+    ],
+
+    'CompositorNodeTransform' : [
+        NTPNodeSetting("filter_type", ST.ENUM)
+    ],
+
+    'CompositorNodeTranslate' : [
+        NTPNodeSetting("use_relative", ST.BOOL),
+        NTPNodeSetting("wrap_axis",    ST.ENUM)
+    ],
+
+    'CompositorNodeCornerPin' : [],
+
+    'CompositorNodeCrop' : [
+        NTPNodeSetting("max_x",         ST.INT),
+        NTPNodeSetting("max_y",         ST.INT),
+        NTPNodeSetting("min_x",         ST.INT),
+        NTPNodeSetting("min_y",         ST.INT),
+        NTPNodeSetting("rel_max_x",     ST.FLOAT),
+        NTPNodeSetting("rel_max_y",     ST.FLOAT),
+        NTPNodeSetting("rel_min_x",     ST.FLOAT),
+        NTPNodeSetting("rel_min_y",     ST.FLOAT),
+        NTPNodeSetting("relative",      ST.BOOL),
+        NTPNodeSetting("use_crop_size", ST.BOOL)
+    ],
+
+    'CompositorNodeDisplace' : [],
+
+    'CompositorNodeFlip' : [
+        NTPNodeSetting("axis", ST.ENUM)
+    ],
+
+    'CompositorNodeMapUV' : [
+        NTPNodeSetting("alpha", ST.INT)
+    ],
+
+    'CompositorNodeLensdist' : [
+        NTPNodeSetting("use_fit",       ST.BOOL),
+        NTPNodeSetting("use_jitter",    ST.BOOL),
+        NTPNodeSetting("use_projector", ST.BOOL)
+    ],
+
+    'CompositorNodeMovieDistortion' : [
+        NTPNodeSetting("clip",            ST.MOVIE_CLIP),
+        NTPNodeSetting("distortion_type", ST.ENUM)
+    ],
+
+
+    # UTILITIES
+    'CompositorNodeMapRange' : [
+        NTPNodeSetting("use_clamp", ST.BOOL)
+    ],
+
+    'CompositorNodeMapValue' : [
+        NTPNodeSetting("max",     ST.VEC1),
+        NTPNodeSetting("min",     ST.VEC1),
+        NTPNodeSetting("offset",  ST.VEC1),
+        NTPNodeSetting("size",    ST.VEC1),
+        NTPNodeSetting("use_max", ST.BOOL),
+        NTPNodeSetting("use_min", ST.BOOL)
+    ],
+
+    'CompositorNodeMath' : [
+        NTPNodeSetting("operation", ST.ENUM),
+        NTPNodeSetting("use_clamp", ST.BOOL)
+    ],
+
+    'CompositorNodeLevels' : [
+        NTPNodeSetting("channel", ST.ENUM)
+    ],
+    
+    'CompositorNodeNormalize' : [],
+
+
+    'CompositorNodeSwitch' : [
+        NTPNodeSetting("check", ST.BOOL)
+    ],
+
+    'CompositorNodeSwitchView' : [],
+
+
+    # VECTOR
+    'CompositorNodeCombineXYZ'  : [],
+    'CompositorNodeSeparateXYZ' : [],
+    'CompositorNodeNormal'      : [],
+
+    'CompositorNodeCurveVec' : [
+        NTPNodeSetting("mapping", ST.CURVE_MAPPING)
+    ],
+
+
+    # MISC
+    'CompositorNodeGroup' : [
+        NTPNodeSetting("node_tree", ST.NODE_TREE)
+    ],
+
+    'NodeFrame' : [
+        NTPNodeSetting("label_size", ST.INT),
+        NTPNodeSetting("shrink", ST.BOOL),
+        NTPNodeSetting("text", ST.TEXT)
+    ],
+
+    'NodeGroupInput'  : [],
+
+    'NodeGroupOutput' : [
+        NTPNodeSetting("is_active_output", ST.BOOL)
+    ],
+
+    'NodeReroute'     : []
+}
diff --git a/compositor/operator.py b/compositor/operator.py
new file mode 100644
index 0000000..afe2d62
--- /dev/null
+++ b/compositor/operator.py
@@ -0,0 +1,229 @@
+import bpy
+
+from bpy.types import Node, CompositorNodeColorBalance, CompositorNodeTree
+
+from ..ntp_operator import NTP_Operator, INDEX
+from ..ntp_node_tree import NTP_NodeTree
+from ..utils import *
+from io import StringIO
+from .node_settings import compositor_node_settings
+
+SCENE = "scene"
+BASE_NAME = "base_name"
+END_NAME = "end_name"
+NODE = "node"
+
+COMP_OP_RESERVED_NAMES = {SCENE, BASE_NAME, END_NAME, NODE} 
+
+class NTPCompositorOperator(NTP_Operator):
+    bl_idname = "node.ntp_compositor"
+    bl_label =  "Compositor to Python"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    mode : bpy.props.EnumProperty(
+        name = "Mode",
+        items = [
+            ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"),
+            ('ADDON', "Addon", "Create a full addon")
+        ]
+    )
+    
+    compositor_name: bpy.props.StringProperty(name="Node Group")
+    is_scene : bpy.props.BoolProperty(name="Is Scene", description="Blender stores compositing node trees differently for scenes and in groups")
+
+    def __init__(self):
+        super().__init__()
+        self._settings = compositor_node_settings
+        for name in COMP_OP_RESERVED_NAMES:
+            self._used_vars[name] = 0
+
+
+    def _create_scene(self, indent: str):
+        #TODO: wrap in more general unique name util function
+        self._write(f"# Generate unique scene name", indent)
+        self._write(f"{BASE_NAME} = {str_to_py_str(self.compositor_name)}",
+                    indent)
+        self._write(f"{END_NAME} = {BASE_NAME}", indent)
+        self._write(f"if bpy.data.scenes.get({END_NAME}) != None:", indent)
+
+        indent2 = f"{indent}\t"
+        self._write(f"{INDEX} = 1", indent2)
+        self._write(f"{END_NAME} = {BASE_NAME} + f\".{{i:03d}}\"", 
+                    indent2)
+        self._write(f"while bpy.data.scenes.get({END_NAME}) != None:",
+                    indent2)
+        
+        indent3 = f"{indent}\t\t"
+        self._write(f"{END_NAME} = {BASE_NAME} + f\".{{{INDEX}:03d}}\"", indent3)
+        self._write(f"{INDEX} += 1\n", indent3)
+
+        self._write(f"{SCENE} = bpy.context.window.scene.copy()\n", indent) 
+        self._write(f"{SCENE}.name = {END_NAME}", indent)
+        self._write(f"{SCENE}.use_fake_user = True", indent)
+        self._write(f"bpy.context.window.scene = {SCENE}", indent)
+
+    def _initialize_compositor_node_tree(self, ntp_nt, nt_name):
+        #initialize node group
+        self._write(f"#initialize {nt_name} node group", self._outer)
+        self._write(f"def {ntp_nt.var}_node_group():", self._outer)
+
+        if ntp_nt.node_tree == self._base_node_tree:
+            self._write(f"{ntp_nt.var} = {SCENE}.node_tree")
+            self._write(f"#start with a clean node tree")
+            self._write(f"for {NODE} in {ntp_nt.var}.nodes:")
+            self._write(f"\t{ntp_nt.var}.nodes.remove({NODE})")
+        else:
+            self._write((f"{ntp_nt.var} = bpy.data.node_groups.new("
+                         f"type = \'CompositorNodeTree\', "
+                         f"name = {str_to_py_str(nt_name)})"))
+            self._write("")
+
+    def _set_color_balance_settings(self, node: CompositorNodeColorBalance
+                                   ) -> None:
+        """
+        Sets the color balance settings so we only set the active variables,
+        preventing conflict
+
+        node (CompositorNodeColorBalance): the color balance node
+        """
+        if node.correction_method == 'LIFT_GAMMA_GAIN':
+            lst = [NTPNodeSetting("correction_method", ST.ENUM),                 
+                   NTPNodeSetting("gain",              ST.COLOR),
+                   NTPNodeSetting("gamma",             ST.COLOR),
+                   NTPNodeSetting("lift",              ST.COLOR)]
+        else:
+            lst = [NTPNodeSetting("correction_method", ST.ENUM),
+                   NTPNodeSetting("offset",            ST.COLOR),
+                   NTPNodeSetting("offset_basis",      ST.FLOAT),
+                   NTPNodeSetting("power",             ST.COLOR),
+                   NTPNodeSetting("slope",             ST.COLOR)]
+
+        self._settings['CompositorNodeColorBalance'] = lst
+
+    def _process_node(self, node: Node, ntp_nt: NTP_NodeTree):
+        """
+        Create node and set settings, defaults, and cosmetics
+
+        Parameters:
+        node (Node): node to process
+        ntp_nt (NTP_NodeTree): the node tree that node belongs to
+        """
+        node_var: str = self._create_node(node, ntp_nt.var)
+        
+        if node.bl_idname == 'CompositorNodeColorBalance':
+            self._set_color_balance_settings(node)
+
+        self._set_settings_defaults(node)
+        self._hide_hidden_sockets(node)
+
+        if bpy.app.version < (4, 0, 0):
+            if node.bl_idname == 'NodeGroupInput' and not ntp_nt.inputs_set:
+                self._group_io_settings(node, "input", ntp_nt)
+                ntp_nt.inputs_set = True
+
+            elif node.bl_idname == 'NodeGroupOutput' and not ntp_nt.outputs_set:
+                self._group_io_settings(node, "output", ntp_nt)
+                ntp_nt.outputs_set = True
+
+        self._set_socket_defaults(node)
+    
+    def _process_node_tree(self, node_tree: CompositorNodeTree):
+        """
+        Generates a Python function to recreate a compositor node tree
+
+        Parameters:
+        node_tree (CompositorNodeTree): node tree to be recreated
+        """  
+        if node_tree == self._base_node_tree:
+            nt_var = self._create_var(self.compositor_name)
+            nt_name = self.compositor_name
+        else:
+            nt_var = self._create_var(node_tree.name)
+            nt_name = node_tree.name
+
+        self._node_tree_vars[node_tree] = nt_var
+
+        ntp_nt = NTP_NodeTree(node_tree, nt_var)
+        self._initialize_compositor_node_tree(ntp_nt, nt_name)
+
+        if bpy.app.version >= (4, 0, 0):
+            self._tree_interface_settings(ntp_nt)
+
+        #initialize nodes
+        self._write(f"#initialize {nt_var} nodes")
+
+        for node in node_tree.nodes:
+            self._process_node(node, ntp_nt)
+
+        self._set_parents(node_tree)
+        self._set_locations(node_tree)
+        self._set_dimensions(node_tree)
+        
+        self._init_links(node_tree)
+        
+        self._write(f"return {nt_var}\n")
+
+        self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer)
+    
+    def execute(self, context):
+        #find node group to replicate
+        if self.is_scene:
+            self._base_node_tree = bpy.data.scenes[self.compositor_name].node_tree
+        else:
+            self._base_node_tree = bpy.data.node_groups[self.compositor_name]
+
+        if self._base_node_tree is None:
+            #shouldn't happen
+            self.report({'ERROR'},("NodeToPython: This doesn't seem to be a "
+                                   "valid compositor node tree. Is Use Nodes "
+                                   "selected?"))
+            return {'CANCELLED'}
+
+        #set up names to use in generated addon
+        comp_var = clean_string(self.compositor_name)
+        
+        if self.mode == 'ADDON':
+            self._outer = "\t\t"
+            self._inner = "\t\t\t"
+
+            self._setup_addon_directories(context, comp_var)
+
+            self._file = open(f"{self._addon_dir}/__init__.py", "w")
+
+            self._create_header(self.compositor_name)
+            self._class_name = clean_string(self.compositor_name, lower=False)
+            self._init_operator(comp_var, self.compositor_name)
+
+            self._write("def execute(self, context):", "\t")
+        else:
+            self._file = StringIO("")
+
+        if self.is_scene:
+            if self.mode == 'ADDON':
+                self._create_scene("\t\t")
+            elif self.mode == 'SCRIPT':
+                self._create_scene("")
+
+        node_trees_to_process = self._topological_sort(self._base_node_tree)
+
+        for node_tree in node_trees_to_process:  
+            self._process_node_tree(node_tree)
+
+        if self.mode == 'ADDON':
+            self._write("return {'FINISHED'}\n", self._outer)
+
+            self._create_menu_func()
+            self._create_register_func()
+            self._create_unregister_func()
+            self._create_main_func()
+        else:
+            context.window_manager.clipboard = self._file.getvalue()
+
+        self._file.close()
+        
+        if self.mode == 'ADDON':
+            self._zip_addon()
+        
+        self._report_finished("compositor nodes")
+
+        return {'FINISHED'}
\ No newline at end of file
diff --git a/compositor/ui.py b/compositor/ui.py
new file mode 100644
index 0000000..a1003ca
--- /dev/null
+++ b/compositor/ui.py
@@ -0,0 +1,80 @@
+import bpy
+from bpy.types import Panel
+from bpy.types import Menu
+from .operator import NTPCompositorOperator
+
+class NTPCompositorPanel(Panel):
+    bl_label = "Compositor to Python"
+    bl_idname = "NODE_PT_ntp_compositor"
+    bl_space_type = 'NODE_EDITOR'
+    bl_region_type = 'UI'
+    bl_context = ''
+    bl_category = "NodeToPython"
+
+    @classmethod
+    def poll(cls, context):
+        return True
+    
+    def draw_header(self, context):
+        layout = self.layout
+
+    def draw(self, context):
+        layout = self.layout
+        scenes_row = layout.row()
+        
+        # Disables menu when there are no compositing node groups
+        scenes = [scene for scene in bpy.data.scenes if scene.node_tree]
+        scenes_exist = len(scenes) > 0
+        scenes_row.enabled = scenes_exist
+        
+        scenes_row.alignment = 'EXPAND'
+        scenes_row.operator_context = 'INVOKE_DEFAULT'
+        scenes_row.menu("NODE_MT_ntp_comp_scenes", 
+                        text="Scene Compositor Nodes")
+
+        groups_row = layout.row()
+        groups = [node_tree for node_tree in bpy.data.node_groups 
+                  if node_tree.bl_idname == 'CompositorNodeTree']
+        groups_exist = len(groups) > 0
+        groups_row.enabled = groups_exist
+
+        groups_row.alignment = 'EXPAND'
+        groups_row.operator_context = 'INVOKE_DEFAULT'
+        groups_row.menu("NODE_MT_ntp_comp_groups", 
+                        text="Group Compositor Nodes")
+
+class NTPCompositorScenesMenu(Menu):
+    bl_idname = "NODE_MT_ntp_comp_scenes"
+    bl_label = "Select "
+    
+    @classmethod
+    def poll(cls, context):
+        return True
+
+    def draw(self, context):
+        layout = self.layout.column_flow(columns=1)
+        layout.operator_context = 'INVOKE_DEFAULT'
+        for scene in bpy.data.scenes:
+            if scene.node_tree:
+                op = layout.operator(NTPCompositorOperator.bl_idname, 
+                                     text=scene.name)
+                op.compositor_name = scene.name
+                op.is_scene = True
+
+class NTPCompositorGroupsMenu(Menu):
+    bl_idname = "NODE_MT_ntp_comp_groups"
+    bl_label = "Select "
+    
+    @classmethod
+    def poll(cls, context):
+        return True
+
+    def draw(self, context):
+        layout = self.layout.column_flow(columns=1)
+        layout.operator_context = 'INVOKE_DEFAULT'
+        for node_group in bpy.data.node_groups:
+            if node_group.bl_idname == 'CompositorNodeTree':
+                op = layout.operator(NTPCompositorOperator.bl_idname, 
+                                     text=node_group.name)
+                op.compositor_name = node_group.name
+                op.is_scene = False
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
index e82bbb8..bc461c1 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -2,7 +2,7 @@
 
 ![Node To Python Logo](./img/ntp.jpg "Node To Python Logo")
 
-[![GitHub release (latest by date)](https://img.shields.io/github/v/release/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/releases) [![GitHub](https://img.shields.io/github/license/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/blob/main/LICENSE) ![](https://visitor-badge.laobi.icu/badge?page_id=BrendanParmer.NodeToPython)
+[![GitHub release (latest by date)](https://img.shields.io/github/v/release/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/releases) [![GitHub](https://img.shields.io/github/license/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/blob/main/LICENSE) ![](https://visitor-badge.laobi.icu/badge?page_id=BrendanParmer.NodeToPython) ![](https://img.shields.io/github/downloads/BrendanParmer/NodeToPython/total.svg)
  
 ## About
 A Blender add-on to create scripts and add-ons! This add-on will take your Geometry Nodes or Materials and convert them into legible Python code.
@@ -15,7 +15,7 @@ Blender's node-based editors are powerful, yet accessible tools, and I wanted to
 * interfacing with other parts of the software or properties of an object
 
 ## Supported Versions
-NodeToPython v2.2 is compatible with Blender 3.0 - 3.6 on Windows, macOS, and Linux. I generally try to update the add-on to handle new nodes around the beta release of each update.
+NodeToPython v3.0 is supported for Blender 3.0 - 4.0 on Windows, macOS, and Linux. I generally try to update the add-on to handle new nodes around the beta release of each update.
 
 ## Installation
 1. Download the `NodeToPython.zip` file from the [latest release](https://github.com/BrendanParmer/NodeToPython/releases)
@@ -35,33 +35,29 @@ Select the node group you want code for, and you'll be prompted with a **Script*
     * Doesn't include `import bpy` line
     * To keep NodeToPython cross-platform and independent of third-party libraries, to get it into your system clipboard you need to paste into the Blender text editor and recopy it currently
 * **Add-on** mode generates a zip file for you in the save directory specified in the NodeToPython menu. From here, you can install it like a regular add-on. The generated add-on comes complete with operator registration and creating a modifier/material for the node tree to be used in. 
-
-## Future
-### v3.x
-* Expansion to Compositing nodes
-* New Blender 4.0 nodes and changes
-
-### Later
-* Better asset handling
-* Auto-set handle movies and image sequences
-* Automatically format code to be PEP8 compliant
-* Automatically detect the minimum version of Blender needed to run the add-on
-
-## Potential Issues
-* As of version 2.2, the add-on will not set default values for
-    * Scripts
-    * IES files
-    * Filepaths
-    * UV maps
-* This add-on doesn't currently set default values in Geometry Nodes modifiers, just the node groups themselves
-* Currently when setting default values for the following, the add-on must be run in the same blend file as the node group was created in to set the default, otherwise it will just set it to `None`:
+    * The current default operator install location is in the Object menu
+
+## Future Plans
+* Investigate drivers and keyframes
+* A better default operator install location for generated add-ons
+* A development repository with useful scripts and tests
+* Better handling of more setting types, including
+    * Image sequences
+    * Movie clips
     * Materials
     * Objects
-    * Collections
     * Textures
-
-* In a future version, I plan on having the add-on adding all of the above to the Asset Library for reference
-* You may run into naming conflicts if your add-on shares a name with another Blender add-on or operator (see [#56](https://github.com/BrendanParmer/NodeToPython/issues/56))
+    * Text objects
+    * Scenes
+    * Particle systems
+    * Fonts
+    * Masks
+    * Cryptomatte entries
+    * Image format settings
+    * File slots
+    * Layer slots
+* Automatic detection of the minimum/maximum version of Blender compatible with a generated add-on
+* Autoformatting of generated code
 
 ## Bug Reports and Suggestions
 
diff --git a/geo_nodes.py b/geo_nodes.py
deleted file mode 100644
index c70f219..0000000
--- a/geo_nodes.py
+++ /dev/null
@@ -1,437 +0,0 @@
-import bpy
-import os
-
-from .utils import *
-from io import StringIO
-
-geo_node_settings = {
-    # Attribute nodes
-    "GeometryNodeAttributeStatistic" : ["data_type", "domain"],
-    "GeometryNodeAttributeDomainSize" : ["component"],
-
-    "GeometryNodeBlurAttribute" : ["data_type"],
-    "GeometryNodeCaptureAttribute" : ["data_type", "domain"],
-    "GeometryNodeStoreNamedAttribute" : ["data_type", "domain"],
-    "GeometryNodeAttributeTransfer" : ["data_type", "mapping"],
-
-    # Input Nodes
-    # Input > Constant
-    "FunctionNodeInputBool" : ["boolean"],
-    "FunctionNodeInputColor" : ["color"],
-    "FunctionNodeInputInt" : ["integer"],
-    "GeometryNodeInputMaterial" : ["material"],
-    "FunctionNodeInputString" : ["string"],
-    "FunctionNodeInputVector" : ["vector"],
-
-    # Input > Scene
-    "GeometryNodeCollectionInfo" : ["transform_space"],
-    "GeometryNodeObjectInfo" : ["transform_space"],
-
-    # Output Nodes
-    "GeometryNodeViewer" : ["domain"],
-
-    # Geometry Nodes
-    # Geometry > Read
-    "GeometryNodeInputNamedAttribute" : ["data_type"],
-
-    # Geometry > Sample
-    "GeometryNodeProximity" : ["target_element"],
-    "GeometryNodeRaycast" : ["data_type", "mapping"],
-    "GeometryNodeSampleIndex" : ["data_type", "domain", "clamp"],
-    "GeometryNodeSampleNearest" : ["domain"],
-
-    # Geometry > Operations
-    "GeometryNodeDeleteGeometry" : ["domain", "mode"],
-    "GeometryNodeDuplicateElements" : ["domain"],
-    "GeometryNodeMergeByDistance" : ["mode"],
-    "GeometryNodeSeparateGeometry" : ["domain"],
-
-
-    # Curve
-    # Curve > Read
-    "GeometryNodeCurveHandleTypeSelection" : ["mode", "handle_type"],
-
-    # Curve > Sample
-    "GeometryNodeSampleCurve" : ["data_type", "mode", "use_all_curves"],
-
-    # Curve > Write
-    "GeometryNodeSetCurveNormal" : ["mode"],
-    "GeometryNodeSetCurveHandlePositions" : ["mode"],
-    "GeometryNodeCurveSetHandles" : ["mode", "handle_type"],
-    "GeometryNodeCurveSplineType" : ["spline_type"],
-
-    # Curve > Operations
-    "GeometryNodeCurveToPoints" : ["mode"],
-    "GeometryNodeFillCurve" : ["mode"], 
-    "GeometryNodeFilletCurve" : ["mode"],
-    "GeometryNodeResampleCurve" : ["mode"],
-    "GeometryNodeTrimCurve" : ["mode"],
-
-    # Curve > Primitives
-    "GeometryNodeCurveArc" : ["mode"],
-    "GeometryNodeCurvePrimitiveBezierSegment" : ["mode"],
-    "GeometryNodeCurvePrimitiveCircle" : ["mode"],
-    "GeometryNodeCurvePrimitiveLine" : ["mode"],
-    "GeometryNodeCurvePrimitiveQuadrilateral" : ["mode"],
-
-
-    # Mesh Nodes
-    # Mesh > Sample
-    "GeometryNodeSampleNearestSurface" : ["data_type"],
-    "GeometryNodeSampleUVSurface" : ["data_type"],
-
-    # Mesh > Operations
-    "GeometryNodeExtrudeMesh" : ["mode"],
-    "GeometryNodeMeshBoolean" : ["operation"],
-    "GeometryNodeMeshToPoints" : ["mode"],
-    "GeometryNodeMeshToVolume" : ["resolution_mode"],
-    "GeometryNodeScaleElements" : ["domain", "scale_mode"],
-    "GeometryNodeSubdivisionSurface" : ["uv_smooth", "boundary_smooth"],
-    "GeometryNodeTriangulate" : ["quad_method", "ngon_method"],
-
-    # Mesh > Primitives
-    "GeometryNodeMeshCone" : ["fill_type"],
-    "GeometryNodeMeshCylinder" : ["fill_type"],
-    "GeometryNodeMeshCircle" : ["fill_type"],
-    "GeometryNodeMeshLine" : ["mode"],
-
-    # Mesh > UV
-    "GeometryNodeUVUnwrap" : ["method"],
-
-
-    # Point Nodes
-    "GeometryNodeDistributePointsInVolume" : ["mode"],
-    "GeometryNodeDistributePointsOnFaces" : ["distribute_method"],
-    "GeometryNodePointsToVolume" : ["resolution_mode"],
-
-    # Volume Nodes
-    "GeometryNodeVolumeToMesh" : ["resolution_mode"],
-
-
-    # Texture Nodes
-    "ShaderNodeTexBrick" : ["offset", "offset_frequency", "squash", 
-                            "squash_frequency"],
-    "ShaderNodeTexGradient" : ["gradient_type"],
-    "GeometryNodeImageTexture" : ["interpolation", "extension"],
-    "ShaderNodeTexMagic" : ["turbulence_depth"],
-    "ShaderNodeTexNoise" : ["noise_dimensions"],
-    "ShaderNodeTexVoronoi" : ["voronoi_dimensions", "feature", "distance"],
-    "ShaderNodeTexWave" : ["wave_type", "bands_direction", "wave_profile"],
-    "ShaderNodeTexWhiteNoise" : ["noise_dimensions"],
-
-
-    # Utilities
-    # Utilities > Color
-    "FunctionNodeCombineColor" : ["mode"],
-    "ShaderNodeMixRGB" : ["blend_type", "use_clamp"], #legacy
-    "FunctionNodeSeparateColor" : ["mode"],
-    
-    # Utilities > Text
-    "GeometryNodeStringToCurves" : ["overflow", "align_x", "align_y", 
-                                    "pivot_mode"],
-
-    # Utilities > Vector
-    "ShaderNodeVectorMath" : ["operation"],
-    "ShaderNodeVectorRotate" : ["rotation_type", "invert"],
-
-    # Utilities > Field
-    "GeometryNodeAccumulateField" : ["data_type", "domain"],
-    "GeometryNodeFieldAtIndex" : ["data_type", "domain"],
-    "GeometryNodeFieldOnDomain" : ["data_type", "domain" ],
-
-    # Utilities > Math
-    "FunctionNodeBooleanMath" : ["operation"],
-    "ShaderNodeClamp" : ["clamp_type"],
-    "FunctionNodeCompare" : ["data_type", "operation", "mode"],
-    "FunctionNodeFloatToInt" : ["rounding_mode"],
-    "ShaderNodeMapRange" : ["data_type", "interpolation_type", "clamp"], 
-    "ShaderNodeMath" : ["operation", "use_clamp"],
-
-    # Utilities > Rotate
-    "FunctionNodeAlignEulerToVector" : ["axis", "pivot_axis"],
-    "FunctionNodeRotateEuler" : ["type", "space"],
-
-    # Utilities > General
-    "ShaderNodeMix" : ["data_type", "blend_type", "clamp_result", 
-                       "clamp_factor", "factor_mode"],
-    "FunctionNodeRandomValue" : ["data_type"],
-    "GeometryNodeSwitch" : ["input_type"]
-}
-
-curve_nodes = {'ShaderNodeFloatCurve', 
-               'ShaderNodeVectorCurve', 
-               'ShaderNodeRGBCurve'}
-
-image_nodes = {'GeometryNodeInputImage'}
-
-class GeoNodesToPython(bpy.types.Operator):
-    bl_idname = "node.geo_nodes_to_python"
-    bl_label = "Geo Nodes to Python"
-    bl_options = {'REGISTER', 'UNDO'}
-    
-    mode : bpy.props.EnumProperty(
-        name = "Mode",
-        items = [
-            ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"),
-            ('ADDON', "Addon", "Create a full addon")
-        ]
-    )
-
-    geo_nodes_group_name: bpy.props.StringProperty(name="Node Group")
-    
-    def execute(self, context):
-        #find node group to replicate
-        nt = bpy.data.node_groups[self.geo_nodes_group_name]
-
-        #set up names to use in generated addon
-        nt_var = clean_string(nt.name)
-
-        if self.mode == 'ADDON':
-            #find base directory to save new addon
-            dir = bpy.path.abspath(context.scene.ntp_options.dir_path)
-            if not dir or dir == "":
-                self.report({'ERROR'}, 
-                            ("NodeToPython: Save your blend file before using "
-                            "NodeToPython!"))
-                return {'CANCELLED'}
-
-            #save in addons/ subdirectory
-            zip_dir = os.path.join(dir, nt_var)
-            addon_dir = os.path.join(zip_dir, nt_var)
-
-            if not os.path.exists(addon_dir):
-                os.makedirs(addon_dir)
-            file = open(f"{addon_dir}/__init__.py", "w")
-            
-            create_header(file, nt.name)
-            class_name = clean_string(nt.name.replace(" ", "").replace('.', ""), 
-                                    lower = False)
-            init_operator(file, class_name, nt_var, nt.name)
-            file.write("\tdef execute(self, context):\n")
-        else:
-            file = StringIO("")
-
-        #set to keep track of already created node trees
-        node_trees = set()
-
-        #dictionary to keep track of node->variable name pairs
-        node_vars = {}
-
-        #dictionary to keep track of variables->usage count pairs
-        used_vars = {}
-        
-        def process_geo_nodes_group(node_tree, level, node_vars, used_vars):
-            nt_var = create_var(node_tree.name, used_vars)
-                
-            outer, inner = make_indents(level)
-
-            #initialize node group
-            file.write(f"{outer}#initialize {nt_var} node group\n")
-            file.write(f"{outer}def {nt_var}_node_group():\n")
-            file.write((f"{inner}{nt_var}"
-                        f"= bpy.data.node_groups.new("
-                        f"type = \'GeometryNodeTree\', "
-                        f"name = {str_to_py_str(node_tree.name)})\n"))
-            file.write("\n")
-
-            inputs_set = False
-            outputs_set = False
-
-            #initialize nodes
-            file.write(f"{inner}#initialize {nt_var} nodes\n")
-            
-            sim_inputs = []
-
-            for node in node_tree.nodes:
-                if node.bl_idname == 'GeometryNodeGroup':
-                    node_nt = node.node_tree
-                    if node_nt is not None and node_nt not in node_trees:
-                        process_geo_nodes_group(node_nt, level + 1, node_vars, 
-                                                used_vars)
-                        node_trees.add(node_nt)
-                elif node.bl_idname == 'NodeGroupInput' and not inputs_set:
-                    group_io_settings(node, file, inner, "input", nt_var, 
-                                      node_tree)
-                    inputs_set = True
-
-                elif node.bl_idname == 'NodeGroupOutput' and not outputs_set:
-                    group_io_settings(node, file, inner, "output", nt_var, 
-                                      node_tree)
-                    outputs_set = True
-
-                #create node
-                node_var = create_node(node, file, inner, nt_var, 
-                                       node_vars, used_vars)
-                set_settings_defaults(node, geo_node_settings, file, inner, 
-                                      node_var)
-                hide_sockets(node, file, inner, node_var)
-
-                if node.bl_idname == 'GeometryNodeGroup':
-                    if node.node_tree is not None:
-                        file.write((f"{inner}{node_var}.node_tree = "
-                                    f"bpy.data.node_groups"
-                                    f"[{str_to_py_str(node.node_tree.name)}]\n"))
-
-                elif node.bl_idname == 'ShaderNodeValToRGB':
-                    color_ramp_settings(node, file, inner, node_var)
-
-                elif node.bl_idname in curve_nodes:
-                    curve_node_settings(node, file, inner, node_var)
-
-                elif node.bl_idname in image_nodes and self.mode == 'ADDON':
-                    img = node.image
-                    if img is not None and img.source in {'FILE', 'GENERATED', 'TILED'}:
-                        save_image(img, addon_dir)
-                        load_image(img, file, inner, f"{node_var}.image")
-
-                elif node.bl_idname == 'GeometryNodeSimulationInput':
-                    sim_inputs.append(node)
-
-                elif node.bl_idname == 'GeometryNodeSimulationOutput':
-                    file.write(f"{inner}#remove generated sim state items\n")
-                    file.write(f"{inner}for item in {node_var}.state_items:\n")
-                    file.write(f"{inner}\t{node_var}.state_items.remove(item)\n")
-
-                    for i, si in enumerate(node.state_items):
-                        socket_type = enum_to_py_str(si.socket_type)
-                        name = str_to_py_str(si.name)
-                        file.write(f"{inner}#create SSI {name}\n")
-                        file.write((f"{inner}{node_var}.state_items.new"
-                                    f"({socket_type}, {name})\n"))
-                        si_var = f"{node_var}.state_items[{i}]"
-                        attr_domain = enum_to_py_str(si.attribute_domain)
-                        file.write((f"{inner}{si_var}.attribute_domain = "
-                                    f"{attr_domain}\n"))
-
-                if node.bl_idname != 'GeometryNodeSimulationInput':
-                    if self.mode == 'ADDON':
-                        set_input_defaults(node, file, inner, node_var, addon_dir)
-                    else:
-                        set_input_defaults(node, file, inner, node_var)
-                    set_output_defaults(node, file, inner, node_var)
-
-            #create simulation zones
-            for sim_input in sim_inputs:
-                sim_input_var = node_vars[sim_input]
-                sim_output_var = node_vars[sim_input.paired_output]
-                file.write((f"{inner}{sim_input_var}.pair_with_output"
-                            f"({sim_output_var})\n"))
-
-                #must set defaults after paired with output
-                if self.mode == 'ADDON':
-                    set_input_defaults(node, file, inner, node_var, addon_dir)
-                else:
-                    set_input_defaults(node, file, inner, node_var)
-                set_output_defaults(sim_input, file, inner, sim_input_var)
-            
-            #set look of nodes
-            set_parents(node_tree, file, inner, node_vars)
-            set_locations(node_tree, file, inner, node_vars)
-            set_dimensions(node_tree, file, inner, node_vars)
-
-            #create connections
-            init_links(node_tree, file, inner, nt_var, node_vars)
-            
-            file.write(f"{inner}return {nt_var}\n")
-
-            #create node group
-            file.write((f"\n{outer}{nt_var} = "
-                        f"{nt_var}_node_group()\n\n"))
-            return used_vars
-        
-        if self.mode == 'ADDON':
-            level = 2
-        else:
-            level = 0
-        process_geo_nodes_group(nt, level, node_vars, used_vars)
-
-        def apply_modifier():
-            #get object
-            file.write(f"\t\tname = bpy.context.object.name\n")
-            file.write(f"\t\tobj = bpy.data.objects[name]\n")
-
-            #set modifier to the one we just created
-            mod_name = str_to_py_str(nt.name)
-            file.write((f"\t\tmod = obj.modifiers.new(name = {mod_name}, "
-                        f"type = 'NODES')\n"))
-            file.write(f"\t\tmod.node_group = {nt_var}\n")
-        if self.mode == 'ADDON':
-            apply_modifier()
-
-            file.write("\t\treturn {'FINISHED'}\n\n")
-            
-            create_menu_func(file, class_name)
-            create_register_func(file, class_name)
-            create_unregister_func(file, class_name)
-            create_main_func(file)
-        else:
-            context.window_manager.clipboard = file.getvalue()
-        file.close()
-
-        if self.mode == 'ADDON':
-            zip_addon(zip_dir)
-
-        #alert user that NTP is finished
-        if self.mode == 'SCRIPT':
-            location = "clipboard"
-        else:
-            location = dir
-        self.report({'INFO'}, 
-                    f"NodeToPython: Saved geometry nodes group to {location}")
-        return {'FINISHED'}
-    
-    def invoke(self, context, event):
-        return context.window_manager.invoke_props_dialog(self)
-
-    def draw(self, context):
-        self.layout.prop(self, "mode")
-    
-class SelectGeoNodesMenu(bpy.types.Menu):
-    bl_idname = "NODE_MT_ntp_geo_nodes_selection"
-    bl_label = "Select Geo Nodes"
-    
-    @classmethod
-    def poll(cls, context):
-        return True
-
-    def draw(self, context):
-        layout = self.layout.column_flow(columns=1)
-        layout.operator_context = 'INVOKE_DEFAULT'
-
-        geo_node_groups = [node for node in bpy.data.node_groups 
-                           if node.type == 'GEOMETRY']
-
-        for geo_ng in geo_node_groups:
-            op = layout.operator(GeoNodesToPython.bl_idname, text=geo_ng.name)
-            op.geo_nodes_group_name = geo_ng.name
-            
-class GeoNodesToPythonPanel(bpy.types.Panel):
-    bl_label = "Geometry Nodes to Python"
-    bl_idname = "NODE_PT_geo_nodes_to_python"
-    bl_space_type = 'NODE_EDITOR'
-    bl_region_type = 'UI'
-    bl_context = ''
-    bl_category = "NodeToPython"
-
-    @classmethod
-    def poll(cls, context):
-        return True
-    
-    def draw_header(self, context):
-        layout = self.layout
-
-    def draw(self, context):
-        layout = self.layout
-        col = layout.column()
-        row = col.row()
-        
-        # Disables menu when len of geometry nodes is 0
-        geo_node_groups = [node 
-                            for node in bpy.data.node_groups 
-                            if node.type == 'GEOMETRY']
-        geo_node_groups_exist = len(geo_node_groups) > 0
-        row.enabled = geo_node_groups_exist
-        
-        row.alignment = 'EXPAND'
-        row.operator_context = 'INVOKE_DEFAULT'
-        row.menu("NODE_MT_ntp_geo_nodes_selection", text="Geometry Nodes")
\ No newline at end of file
diff --git a/geometry/__init__.py b/geometry/__init__.py
new file mode 100644
index 0000000..e2707b0
--- /dev/null
+++ b/geometry/__init__.py
@@ -0,0 +1,13 @@
+if "bpy" in locals():
+    import importlib
+    importlib.reload(node_settings)
+    importlib.reload(node_tree)
+    importlib.reload(operator)
+    importlib.reload(ui)
+else:
+    from . import node_settings
+    from . import node_tree
+    from . import operator
+    from . import ui
+
+import bpy
\ No newline at end of file
diff --git a/geometry/node_settings.py b/geometry/node_settings.py
new file mode 100644
index 0000000..ffa30c6
--- /dev/null
+++ b/geometry/node_settings.py
@@ -0,0 +1,675 @@
+from ..utils import ST, NTPNodeSetting
+
+geo_node_settings : dict[str, list[NTPNodeSetting]] = {
+    # ATTRIBUTE
+    'GeometryNodeAttributeStatistic' : [
+        NTPNodeSetting("data_type", ST.ENUM), 
+        NTPNodeSetting("domain",    ST.ENUM)
+    ],
+
+    'GeometryNodeAttributeDomainSize' : [
+        NTPNodeSetting("component", ST.ENUM, min_version = (3, 1, 0))
+    ],
+
+    'GeometryNodeBlurAttribute' : [
+        NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 5, 0))
+    ],
+
+    'GeometryNodeCaptureAttribute' : [
+        NTPNodeSetting("data_type", ST.ENUM),
+        NTPNodeSetting("domain",    ST.ENUM)
+    ],
+
+    'GeometryNodeRemoveAttribute' : [],
+
+    'GeometryNodeStoreNamedAttribute' : [
+        NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 2, 0)),
+        NTPNodeSetting("domain",    ST.ENUM, min_version = (3, 2, 0))
+    ],
+
+    'GeometryNodeAttributeTransfer' : [
+        NTPNodeSetting("data_type", ST.ENUM),
+        NTPNodeSetting("domain",    ST.ENUM),
+        NTPNodeSetting("mapping",   ST.ENUM)
+    ],
+
+    # INPUT
+    # Input > Constant
+    'FunctionNodeInputBool' : [
+        NTPNodeSetting("boolean",  ST.BOOL)
+    ],
+
+    'FunctionNodeInputColor' : [
+        NTPNodeSetting("color", ST.VEC4)
+    ],
+
+    'GeometryNodeInputImage' : [
+        NTPNodeSetting("image", ST.IMAGE, min_version = (3, 5, 0))
+    ],
+
+    'FunctionNodeInputInt' : [
+        NTPNodeSetting("integer", ST.INT)
+    ],
+
+    'GeometryNodeInputMaterial' : [
+        NTPNodeSetting("material", ST.MATERIAL)
+    ],
+
+    'FunctionNodeInputString' : [
+        NTPNodeSetting("string", ST.STRING)
+    ],
+
+    'ShaderNodeValue' : [],
+
+    'FunctionNodeInputVector' : [
+        NTPNodeSetting("vector", ST.VEC3)
+    ],
+
+    #Input > Group
+    'NodeGroupInput' : [],
+    
+    # Input > Scene
+    'GeometryNodeTool3DCursor' : [],
+
+    'GeometryNodeCollectionInfo' : [
+        NTPNodeSetting("transform_space", ST.ENUM)
+    ],
+
+    'GeometryNodeImageInfo' : [],
+    'GeometryNodeIsViewport' : [],
+
+    'GeometryNodeObjectInfo' : [
+        NTPNodeSetting("transform_space", ST.ENUM)
+    ],
+
+    'GeometryNodeSelfObject' : [],
+    'GeometryNodeInputSceneTime' : [],
+
+
+    # OUTPUT
+    'GeometryNodeViewer'  : [
+        NTPNodeSetting("data_type", ST.ENUM),
+        NTPNodeSetting("domain",    ST.ENUM, min_version = (3, 4, 0))
+    ],
+
+
+    # GEOMETRY
+    'GeometryNodeJoinGeometry' : [],
+    'GeometryNodeGeometryToInstance' : [],
+
+    # Geometry > Read
+    'GeometryNodeInputID' : [],
+    'GeometryNodeInputIndex' : [],
+
+    'GeometryNodeInputNamedAttribute' : [
+        NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 2, 0))
+    ],
+
+    'GeometryNodeInputNormal' : [],
+    'GeometryNodeInputPosition' : [],
+    'GeometryNodeInputRadius' : [],
+    'GeometryNodeToolSelection' : [],
+
+    # Geometry > Sample
+    'GeometryNodeProximity' : [
+        NTPNodeSetting("target_element", ST.ENUM)
+    ],
+
+    'GeometryNodeIndexOfNearest' : [],
+
+    'GeometryNodeRaycast' : [
+        NTPNodeSetting("data_type", ST.ENUM),
+        NTPNodeSetting("mapping",   ST.ENUM)
+    ],
+
+    'GeometryNodeSampleIndex' : [
+        NTPNodeSetting("clamp",     ST.BOOL, min_version = (3, 4, 0)),
+        NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 4, 0)),
+        NTPNodeSetting("domain",    ST.ENUM, min_version = (3, 4, 0))
+    ],
+
+    'GeometryNodeSampleNearest' : [
+        NTPNodeSetting("domain", ST.ENUM, min_version = (3, 4, 0))
+    ],
+
+    # Geometry > Write
+    'GeometryNodeSetID' : [],
+    'GeometryNodeSetPosition' : [],
+    'GeometryNodeToolSetSelection' : [],
+
+    # Geometry > Operations
+    'GeometryNodeBoundBox' : [],
+    'GeometryNodeConvexHull' : [],
+
+    'GeometryNodeDeleteGeometry' : [
+        NTPNodeSetting("domain", ST.ENUM),
+        NTPNodeSetting("mode",   ST.ENUM)
+    ],
+
+    'GeometryNodeDuplicateElements' : [
+        NTPNodeSetting("domain", ST.ENUM, min_version = (3, 2, 0))
+    ],
+
+    'GeometryNodeMergeByDistance' : [
+        NTPNodeSetting("mode", ST.ENUM, min_version = (3, 1, 0))
+    ],
+
+    'GeometryNodeTransform' : [],
+    'GeometryNodeSeparateComponents' : [],
+
+    'GeometryNodeSeparateGeometry' : [
+        NTPNodeSetting("domain", ST.ENUM)
+    ],
+
+
+    # CURVE
+    # Curve > Read
+    'GeometryNodeInputCurveHandlePositions' : [],
+    'GeometryNodeCurveLength' : [],
+    'GeometryNodeInputTangent' : [],
+    'GeometryNodeInputCurveTilt' : [],
+    'GeometryNodeCurveEndpointSelection' : [],
+
+    'GeometryNodeCurveHandleTypeSelection' : [
+        NTPNodeSetting("handle_type", ST.ENUM),
+        NTPNodeSetting("mode",        ST.ENUM_SET)
+    ],
+
+    'GeometryNodeInputSplineCyclic' : [],
+    'GeometryNodeSplineLength' : [],
+    'GeometryNodeSplineParameter' : [],
+    'GeometryNodeInputSplineResolution' : [],
+
+    # Curve > Sample
+    'GeometryNodeSampleCurve' : [
+        NTPNodeSetting("data_type",      ST.ENUM, min_version = (3, 4, 0)),
+        NTPNodeSetting("mode",           ST.ENUM),
+        NTPNodeSetting("use_all_curves", ST.BOOL, min_version = (3, 4, 0))
+    ],
+
+    # Curve > Write
+    'GeometryNodeSetCurveNormal' : [
+        NTPNodeSetting("mode", ST.ENUM, min_version = (3, 4, 0))
+    ],
+
+    'GeometryNodeSetCurveRadius' : [],
+    'GeometryNodeSetCurveTilt' : [],
+
+    'GeometryNodeSetCurveHandlePositions' : [
+        NTPNodeSetting("mode", ST.ENUM)
+    ],
+
+    'GeometryNodeCurveSetHandles' : [
+        NTPNodeSetting("handle_type", ST.ENUM),
+        NTPNodeSetting("mode",        ST.ENUM_SET)
+    ],
+
+    'GeometryNodeSetSplineCyclic' : [],
+    'GeometryNodeSetSplineResolution' : [],
+
+    'GeometryNodeCurveSplineType' : [
+        NTPNodeSetting("spline_type", ST.ENUM)
+    ],
+
+    # Curve > Operations
+    'GeometryNodeCurveToMesh' : [],
+
+    'GeometryNodeCurveToPoints' : [
+        NTPNodeSetting("mode", ST.ENUM)
+    ],
+
+    'GeometryNodeDeformCurvesOnSurface' : [],
+
+    'GeometryNodeFillCurve' : [
+        NTPNodeSetting("mode", ST.ENUM)
+    ],
+
+    'GeometryNodeFilletCurve' : [
+        NTPNodeSetting("mode", ST.ENUM)
+    ],
+
+    'GeometryNodeInterpolateCurves' : [],
+
+    'GeometryNodeResampleCurve' : [
+        NTPNodeSetting("mode", ST.ENUM)
+    ],
+
+    'GeometryNodeReverseCurve' : [],
+    'GeometryNodeSubdivideCurve' : [],
+
+    'GeometryNodeTrimCurve' : [
+        NTPNodeSetting("mode", ST.ENUM)
+    ],
+
+    # Curve > Primitives
+    'GeometryNodeCurveArc' : [
+        NTPNodeSetting("mode", ST.ENUM, min_version = (3, 1, 0))
+    ],
+
+    'GeometryNodeCurvePrimitiveBezierSegment' : [
+        NTPNodeSetting("mode", ST.ENUM)
+    ],
+
+    'GeometryNodeCurvePrimitiveCircle' : [
+        NTPNodeSetting("mode", ST.ENUM)
+    ],
+
+    'GeometryNodeCurvePrimitiveLine' : [
+        NTPNodeSetting("mode", ST.ENUM)
+    ],
+
+    'GeometryNodeCurveSpiral' : [],
+    'GeometryNodeCurveQuadraticBezier' : [],
+
+    'GeometryNodeCurvePrimitiveQuadrilateral' : [
+        NTPNodeSetting("mode", ST.ENUM)
+    ],
+
+    'GeometryNodeCurveStar' : [],
+
+    # Curve > Topology
+    'GeometryNodeOffsetPointInCurve' : [],
+    'GeometryNodeCurveOfPoint'       : [],
+    'GeometryNodePointsOfCurve'      : [],
+
+
+    # INSTANCES
+    'GeometryNodeInstanceOnPoints' : [],
+    'GeometryNodeInstancesToPoints' : [],
+
+    'GeometryNodeRealizeInstances' : [
+        NTPNodeSetting("legacy_behavior", ST.BOOL, min_version = (3, 1, 0), 
+                                                   max_version = (3, 6, 0))
+    ],
+
+    'GeometryNodeRotateInstances'       : [],
+    'GeometryNodeScaleInstances'        : [],
+    'GeometryNodeTranslateInstances'    : [],
+    'GeometryNodeInputInstanceRotation' : [],
+    'GeometryNodeInputInstanceScale'    : [],
+
+
+    # MESH
+    # Mesh > Read
+    'GeometryNodeInputMeshEdgeAngle'       : [],
+    'GeometryNodeInputMeshEdgeNeighbors'   : [],
+    'GeometryNodeInputMeshEdgeVertices'    : [],
+    'GeometryNodeEdgesToFaceGroups'        : [],
+    'GeometryNodeInputMeshFaceArea'        : [],
+    'GeometryNodeInputMeshFaceNeighbors'   : [],
+    'GeometryNodeToolFaceSet'              : [],
+    'GeometryNodeMeshFaceSetBoundaries'    : [],
+    'GeometryNodeInputMeshFaceIsPlanar'    : [],
+    'GeometryNodeInputShadeSmooth'         : [],
+    'GeometryNodeInputEdgeSmooth'          : [],
+    'GeometryNodeInputMeshIsland'          : [],
+    'GeometryNodeInputShortestEdgePaths'   : [],
+    'GeometryNodeInputMeshVertexNeighbors' : [],
+
+    # Mesh > Sample
+    'GeometryNodeSampleNearestSurface' : [
+        NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 4, 0))
+    ],
+
+    'GeometryNodeSampleUVSurface' : [
+        NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 4, 0))
+    ],
+
+    # Mesh > Write
+    'GeometryNodeToolSetFaceSet' : [],
+
+    'GeometryNodeSetShadeSmooth' : [
+        NTPNodeSetting("domain", ST.ENUM, min_version = (4, 0, 0))
+    ],
+
+    # Mesh > Operations
+    'GeometryNodeDualMesh'             : [],
+    'GeometryNodeEdgePathsToCurves'    : [],
+    'GeometryNodeEdgePathsToSelection' : [],
+
+    'GeometryNodeExtrudeMesh' : [
+        NTPNodeSetting("mode", ST.ENUM, min_version = (3, 1, 0))
+    ],
+
+    'GeometryNodeFlipFaces' : [],
+
+    'GeometryNodeMeshBoolean' : [
+        NTPNodeSetting("operation", ST.ENUM)
+    ],
+
+    'GeometryNodeMeshToCurve' : [],
+
+    'GeometryNodeMeshToPoints' : [
+        NTPNodeSetting("mode", ST.ENUM)
+    ],
+
+    'GeometryNodeMeshToVolume' : [
+        NTPNodeSetting("resolution_mode", ST.ENUM, min_version = (3, 3, 0))
+    ],
+
+    'GeometryNodeScaleElements' : [
+        NTPNodeSetting("domain",     ST.ENUM, min_version = (3, 1, 0)),
+        NTPNodeSetting("scale_mode", ST.ENUM, min_version = (3, 1, 0))
+    ],
+
+    'GeometryNodeSplitEdges'    : [],
+    'GeometryNodeSubdivideMesh' : [],
+
+    'GeometryNodeSubdivisionSurface' : [
+        NTPNodeSetting("boundary_smooth", ST.ENUM),
+        NTPNodeSetting("uv_smooth",       ST.ENUM)
+    ],
+
+    'GeometryNodeTriangulate' : [
+        NTPNodeSetting("ngon_method", ST.ENUM),
+        NTPNodeSetting("quad_method", ST.ENUM)
+    ],
+
+    # Mesh > Primitives
+    'GeometryNodeMeshCone' : [
+        NTPNodeSetting("fill_type", ST.ENUM)
+    ],
+
+    'GeometryNodeMeshCube' : [],
+
+    'GeometryNodeMeshCylinder' : [
+        NTPNodeSetting("fill_type", ST.ENUM)
+    ],
+
+    'GeometryNodeMeshGrid'      : [],
+    'GeometryNodeMeshIcoSphere' : [],
+
+    'GeometryNodeMeshCircle' : [
+        NTPNodeSetting("fill_type", ST.ENUM)
+    ],
+
+    'GeometryNodeMeshLine' : [
+        NTPNodeSetting("count_mode", ST.ENUM),
+        NTPNodeSetting("mode",       ST.ENUM)
+    ],
+
+    'GeometryNodeMeshUVSphere'  : [],
+
+    # Mesh > Topology
+    'GeometryNodeCornersOfFace'      : [],
+    'GeometryNodeCornersOfVertex'    : [],
+    'GeometryNodeEdgesOfCorner'      : [],
+    'GeometryNodeEdgesOfVertex'      : [],
+    'GeometryNodeFaceOfCorner'       : [],
+    'GeometryNodeOffsetCornerInFace' : [],
+    'GeometryNodeVertexOfCorner'     : [],
+
+    # Mesh > UV
+    'GeometryNodeUVPackIslands' : [],
+
+    'GeometryNodeUVUnwrap': [
+        NTPNodeSetting("method", ST.ENUM, min_version = (3, 3, 0))
+    ],
+
+
+    # POINT
+    'GeometryNodeDistributePointsInVolume' : [
+        NTPNodeSetting("mode", ST.ENUM, min_version = (3, 4, 0))
+    ],
+
+    'GeometryNodeDistributePointsOnFaces' : [
+        NTPNodeSetting("distribute_method", ST.ENUM),
+        NTPNodeSetting("use_legacy_normal", ST.BOOL, min_version = (3, 5, 0))
+    ],
+
+    'GeometryNodePoints'           : [],
+    'GeometryNodePointsToCurves'   : [],
+    'GeometryNodePointsToVertices' : [],
+
+    'GeometryNodePointsToVolume' : [
+        NTPNodeSetting("resolution_mode", ST.ENUM)
+    ],
+
+    'GeometryNodeSetPointRadius' : [],
+
+
+    # VOLUME
+    'GeometryNodeVolumeCube'   : [],
+    'GeometryNodeVolumeToMesh' : [
+        NTPNodeSetting("resolution_mode", ST.ENUM)
+    ],
+
+    
+    # SIMULATION
+    'GeometryNodeSimulationInput'  : [],
+    'GeometryNodeSimulationOutput' : [],
+
+
+    # MATERIAL
+    'GeometryNodeReplaceMaterial'    : [],
+    'GeometryNodeInputMaterialIndex' : [],
+    'GeometryNodeMaterialSelection'  : [],
+    'GeometryNodeSetMaterial'        : [],
+    'GeometryNodeSetMaterialIndex'   : [],
+
+
+    # TEXTURE
+    'ShaderNodeTexBrick' : [
+        NTPNodeSetting("offset",           ST.FLOAT),
+        NTPNodeSetting("offset_frequency", ST.INT),
+        NTPNodeSetting("squash",           ST.FLOAT), 
+        NTPNodeSetting("squash_frequency", ST.INT)
+    ],
+
+    'ShaderNodeTexChecker' : [],
+
+    'ShaderNodeTexGradient' : [
+        NTPNodeSetting("gradient_type", ST.ENUM)
+    ],
+
+    'GeometryNodeImageTexture' : [
+        NTPNodeSetting("extension",     ST.ENUM, min_version = (3, 1, 0)),
+        NTPNodeSetting("interpolation", ST.ENUM, min_version = (3, 1, 0))
+    ],
+
+    'ShaderNodeTexMagic' : [
+        NTPNodeSetting("turbulence_depth", ST.INT)
+    ],
+
+    'ShaderNodeTexMusgrave' : [
+        NTPNodeSetting("musgrave_dimensions", ST.ENUM),
+        NTPNodeSetting("musgrave_type",       ST.ENUM)
+    ],
+
+    'ShaderNodeTexNoise' : [
+        NTPNodeSetting("noise_dimensions", ST.ENUM)
+    ],
+
+    'ShaderNodeTexVoronoi' : [
+        NTPNodeSetting("distance",           ST.ENUM),
+        NTPNodeSetting("feature",            ST.ENUM),
+        NTPNodeSetting("voronoi_dimensions", ST.ENUM)
+    ],
+
+    'ShaderNodeTexWave' : [
+        NTPNodeSetting("bands_direction", ST.ENUM),
+        NTPNodeSetting("rings_direction", ST.ENUM),
+        NTPNodeSetting("wave_profile",    ST.ENUM),
+        NTPNodeSetting("wave_type",       ST.ENUM)
+    ],
+
+    'ShaderNodeTexWhiteNoise' : [
+        NTPNodeSetting("noise_dimensions", ST.ENUM)
+    ],
+
+
+    # UTILITIES
+    'ShaderNodeMix' : [
+        NTPNodeSetting("blend_type",   ST.ENUM, min_version = (3, 4, 0)),
+        NTPNodeSetting("clamp_factor", ST.BOOL, min_version = (3, 4, 0)),
+        NTPNodeSetting("clamp_result", ST.BOOL, min_version = (3, 4, 0)),
+        NTPNodeSetting("data_type",    ST.ENUM, min_version = (3, 4, 0)),
+        NTPNodeSetting("factor_mode",  ST.ENUM, min_version = (3, 4, 0))
+    ],
+
+    'FunctionNodeRandomValue' : [
+        NTPNodeSetting("data_type", ST.ENUM)
+    ],
+
+    'GeometryNodeRepeatInput' : [],
+
+    'GeometryNodeRepeatOutput' : [
+        NTPNodeSetting("inspection_index", ST.INT, min_version = (4, 0, 0))
+    ],
+
+    'GeometryNodeSwitch' : [
+        NTPNodeSetting("input_type", ST.ENUM)
+    ],
+    
+    # Utilities > Color
+    'ShaderNodeValToRGB' : [
+        NTPNodeSetting("color_ramp", ST.COLOR_RAMP)
+    ],
+
+    'ShaderNodeRGBCurve' : [
+        NTPNodeSetting("mapping", ST.CURVE_MAPPING)
+    ],
+
+    'FunctionNodeCombineColor' : [
+        NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0))
+    ],
+
+    'ShaderNodeMixRGB' : [
+        NTPNodeSetting("blend_type", ST.ENUM),
+        NTPNodeSetting("use_alpha",  ST.BOOL),
+        NTPNodeSetting("use_clamp",  ST.BOOL)
+    ], #legacy
+
+    'FunctionNodeSeparateColor' : [
+        NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0))
+    ],
+    
+    # Utilities > Text
+    'GeometryNodeStringJoin'    : [],
+    'FunctionNodeReplaceString' : [],
+    'FunctionNodeSliceString'   : [],
+    'FunctionNodeStringLength'  : [],
+
+    'GeometryNodeStringToCurves' : [
+        NTPNodeSetting("align_x",    ST.ENUM),
+        NTPNodeSetting("align_y",    ST.ENUM),
+        NTPNodeSetting("font",       ST.FONT),
+        NTPNodeSetting("overflow",   ST.ENUM),
+        NTPNodeSetting("pivot_mode", ST.ENUM, min_version = (3, 1, 0))
+    ],
+
+    'FunctionNodeValueToString'          : [],
+    'FunctionNodeInputSpecialCharacters' : [],
+
+    # Utilities > Vector
+    'ShaderNodeVectorCurve' : [
+        NTPNodeSetting("mapping", ST.CURVE_MAPPING)
+    ],
+
+    'ShaderNodeVectorMath' : [
+        NTPNodeSetting("operation", ST.ENUM)
+    ],
+
+    'ShaderNodeVectorRotate' : [
+        NTPNodeSetting("invert",        ST.BOOL),
+        NTPNodeSetting("rotation_type", ST.ENUM)
+    ],
+
+    'ShaderNodeCombineXYZ'  : [],
+    'ShaderNodeSeparateXYZ' : [],
+
+    # Utilities > Field
+    'GeometryNodeAccumulateField' : [
+        NTPNodeSetting("data_type", ST.ENUM),
+        NTPNodeSetting("domain",    ST.ENUM)
+    ],
+
+    'GeometryNodeFieldAtIndex' : [
+        NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 1, 0)),
+        NTPNodeSetting("domain",    ST.ENUM, min_version = (3, 1, 0))
+    ],
+
+    'GeometryNodeFieldOnDomain' : [
+        NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 3, 0)),
+        NTPNodeSetting("domain",    ST.ENUM, min_version = (3, 3, 0))
+    ],
+
+    # Utilities > Math
+    'FunctionNodeBooleanMath' : [
+        NTPNodeSetting("operation", ST.ENUM)
+    ],
+
+    'ShaderNodeClamp' : [
+        NTPNodeSetting("clamp_type", ST.ENUM)
+    ],
+
+    'FunctionNodeCompare' : [
+        NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 1, 0)),
+        NTPNodeSetting("mode",      ST.ENUM, min_version = (3, 1, 0)),
+        NTPNodeSetting("operation", ST.ENUM, min_version = (3, 1, 0))
+    ],
+
+    'FunctionNodeCompareFloats' : [
+        NTPNodeSetting("operation", ST.ENUM, max_version = (3, 0, 0))
+    ],
+
+    'ShaderNodeFloatCurve' : [
+        NTPNodeSetting("mapping", ST.CURVE_MAPPING)
+    ],
+
+    'FunctionNodeFloatToInt' : [
+        NTPNodeSetting("rounding_mode", ST.ENUM)
+    ],
+
+    'ShaderNodeMapRange' : [
+        NTPNodeSetting("clamp",              ST.BOOL),
+        NTPNodeSetting("data_type",          ST.ENUM, min_version = (3, 1, 0)),
+        NTPNodeSetting("interpolation_type", ST.ENUM)
+    ],
+
+    'ShaderNodeMath' : [
+        NTPNodeSetting("operation", ST.ENUM),
+        NTPNodeSetting("use_clamp", ST.BOOL)
+    ],
+
+    # Utilities > Rotation
+    'FunctionNodeAlignEulerToVector' : [
+        NTPNodeSetting("axis",       ST.ENUM),
+        NTPNodeSetting("pivot_axis", ST.ENUM)
+    ],
+
+    'FunctionNodeAxisAngleToRotation' : [],
+    'FunctionNodeEulerToRotation'     : [],
+    'FunctionNodeInvertRotation'      : [],
+                                        
+    'FunctionNodeRotateEuler' : [
+        NTPNodeSetting("space", ST.ENUM),
+        NTPNodeSetting("type",  ST.ENUM)
+    ],
+
+    'FunctionNodeRotateVector'          : [],
+    'FunctionNodeRotationToAxisAngle'   : [],
+    'FunctionNodeRotationToEuler'       : [],
+    'FunctionNodeRotationToQuaternion'  : [],
+    'FunctionNodeQuaternionToRotation'  : [],
+
+    # MISC
+    'GeometryNodeGroup' : [
+        NTPNodeSetting("node_tree", ST.NODE_TREE)
+    ],
+
+    'NodeFrame' : [
+        NTPNodeSetting("label_size", ST.INT),
+        NTPNodeSetting("shrink", ST.BOOL),
+        NTPNodeSetting("text", ST.TEXT)
+    ],
+
+    'NodeGroupInput'  : [],
+
+    'NodeGroupOutput' : [
+        NTPNodeSetting("is_active_output", ST.BOOL)
+    ],
+
+    'NodeReroute'     : []
+
+}
\ No newline at end of file
diff --git a/geometry/node_tree.py b/geometry/node_tree.py
new file mode 100644
index 0000000..73b3565
--- /dev/null
+++ b/geometry/node_tree.py
@@ -0,0 +1,18 @@
+import bpy
+from bpy.types import GeometryNodeTree
+
+if bpy.app.version >= (3, 6, 0):
+    from bpy.types import GeometryNodeSimulationInput
+
+if bpy.app.version > (4, 0, 0):
+    from bpy.types import GeometryNodeRepeatInput
+
+from ..ntp_node_tree import NTP_NodeTree
+
+class NTP_GeoNodeTree(NTP_NodeTree):
+    def __init__(self, node_tree: GeometryNodeTree, var: str):
+        super().__init__(node_tree, var)
+        if bpy.app.version >= (3, 6, 0):
+            self.sim_inputs: list[GeometryNodeSimulationInput] = []
+        if bpy.app.version >= (4, 0, 0):
+            self.repeat_inputs: list[GeometryNodeRepeatInput] = []
diff --git a/geometry/operator.py b/geometry/operator.py
new file mode 100644
index 0000000..d88d6cc
--- /dev/null
+++ b/geometry/operator.py
@@ -0,0 +1,258 @@
+import bpy
+from bpy.types import GeometryNode, GeometryNodeTree
+from bpy.types import Node
+
+from io import StringIO
+
+from ..ntp_operator import NTP_Operator
+from ..utils import *
+from .node_tree import NTP_GeoNodeTree
+from .node_settings import geo_node_settings
+
+ITEM = "item"
+OBJECT_NAME = "name"
+OBJECT = "obj"
+MODIFIER = "mod"
+GEO_OP_RESERVED_NAMES = {ITEM, 
+                         OBJECT_NAME, 
+                         OBJECT,
+                         MODIFIER}
+
+class NTPGeoNodesOperator(NTP_Operator):
+    bl_idname = "node.ntp_geo_nodes"
+    bl_label = "Geo Nodes to Python"
+    bl_options = {'REGISTER', 'UNDO'}
+    
+    mode: bpy.props.EnumProperty(
+        name = "Mode",
+        items = [
+            ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"),
+            ('ADDON',  "Addon", "Create a full addon")
+        ]
+    )
+
+    geo_nodes_group_name: bpy.props.StringProperty(name="Node Group")
+
+    def __init__(self):
+        super().__init__()
+        self._settings = geo_node_settings
+        for name in GEO_OP_RESERVED_NAMES:
+            self._used_vars[name] = 0
+
+    if bpy.app.version >= (3, 6, 0):
+        def _process_zone_output_node(self, node: GeometryNode) -> None:
+            is_sim = False
+            if node.bl_idname == 'GeometryNodeSimulationOutput':
+                items = "state_items"
+                is_sim = True
+            elif node.bl_idname == 'GeometryNodeRepeatOutput':
+                items = "repeat_items"
+            else:
+                self.report({'WARNING'}, f"NodeToPython: {node.bl_idname} is "
+                                         f"not recognized as a valid zone output")
+
+            node_var = self._node_vars[node]
+
+            self._write(f"# Remove generated {items}")
+            self._write(f"for {ITEM} in {node_var}.{items}:")
+            self._write(f"\t{node_var}.{items}.remove(item)")
+
+            for i, item in enumerate(getattr(node, items)):
+                socket_type = enum_to_py_str(item.socket_type)
+                name = str_to_py_str(item.name)
+                self._write(f"# Create item {name}")
+                self._write(f"{node_var}.{items}.new({socket_type}, {name})")
+                if is_sim:
+                    item_var = f"{node_var}.{items}[{i}]"
+                    ad = enum_to_py_str(item.attribute_domain)
+                    self._write(f"{item_var}.attribute_domain = {ad}")
+
+    def _process_node(self, node: Node, ntp_nt: NTP_GeoNodeTree) -> None:
+        """
+        Create node and set settings, defaults, and cosmetics
+
+        Parameters:
+        node (Node): node to process
+        ntp_nt (NTP_NodeTree): the node tree that node belongs to
+        """
+        node_var: str = self._create_node(node, ntp_nt.var)
+        self._set_settings_defaults(node)
+
+        if bpy.app.version < (4, 0, 0):
+            if node.bl_idname == 'NodeGroupInput' and not ntp_nt.inputs_set:
+                self._group_io_settings(node, "input", ntp_nt)
+                ntp_nt.inputs_set = True
+
+            elif node.bl_idname == 'NodeGroupOutput' and not ntp_nt.outputs_set:
+                self._group_io_settings(node, "output", ntp_nt)
+                ntp_nt.outputs_set = True
+
+        if node.bl_idname == 'GeometryNodeSimulationInput':
+            ntp_nt.sim_inputs.append(node)
+
+        elif node.bl_idname == 'GeometryNodeSimulationOutput':
+            self._process_zone_output_node(node)
+
+        elif node.bl_idname == 'GeometryNodeRepeatInput':
+            ntp_nt.repeat_inputs.append(node)
+        
+        elif node.bl_idname == 'GeometryNodeRepeatOutput':
+            self._process_zone_output_node(node)
+        
+        self._hide_hidden_sockets(node)
+
+        if node.bl_idname not in {'GeometryNodeSimulationInput', 
+                                  'GeometryNodeRepeatInput'}:
+            self._set_socket_defaults(node)
+
+    if bpy.app.version >= (3, 6, 0):
+        def _process_zones(self, zone_inputs: list[GeometryNode]) -> None:
+            """
+            Recreates a zone
+            zone_inputs (list[GeometryNodeSimulationInput]): list of 
+                simulation input nodes
+            """
+            for zone_input in zone_inputs:
+                zone_output = zone_input.paired_output
+
+                zone_input_var = self._node_vars[zone_input]
+                zone_output_var = self._node_vars[zone_output]
+
+                self._write(f"#Process zone input {zone_input.name}")
+                self._write(f"{zone_input_var}.pair_with_output"
+                            f"({zone_output_var})")
+
+                #must set defaults after paired with output
+                self._set_socket_defaults(zone_input)
+                self._set_socket_defaults(zone_output)
+            self._write("")
+
+    if bpy.app.version >= (4, 0, 0):
+        def _set_geo_tree_properties(self, node_tree: GeometryNodeTree) -> None:
+            is_mod = node_tree.is_modifier
+            is_tool = node_tree.is_tool
+
+            nt_var = self._node_tree_vars[node_tree]
+
+            if is_mod:
+                self._write(f"{nt_var}.is_modifier = True")
+            if is_tool:
+                self._write(f"{nt_var}.is_tool = True")
+
+                tool_flags =  ["is_mode_edit", 
+                               "is_mode_sculpt",
+                               "is_type_curve",
+                               "is_type_mesh",
+                               "is_type_point_cloud"]
+            
+                for flag in tool_flags:
+                    self._write(f"{nt_var}.{flag} = {getattr(node_tree, flag)}")
+            self._write("")
+
+    def _process_node_tree(self, node_tree: GeometryNodeTree) -> None:
+        """
+        Generates a Python function to recreate a node tree
+
+        Parameters:
+        node_tree (GeometryNodeTree): geometry node tree to be recreated
+        """
+        
+        nt_var = self._create_var(node_tree.name)
+        self._node_tree_vars[node_tree] = nt_var
+
+        #initialize node group
+        self._write(f"#initialize {nt_var} node group", self._outer)
+        self._write(f"def {nt_var}_node_group():", self._outer)
+        self._write(f"{nt_var} = bpy.data.node_groups.new("
+                    f"type = \'GeometryNodeTree\', "
+                    f"name = {str_to_py_str(node_tree.name)})\n")
+
+        if bpy.app.version >= (4, 0, 0):
+            self._set_geo_tree_properties(node_tree)
+    
+        #initialize nodes
+        self._write(f"#initialize {nt_var} nodes")
+
+        ntp_nt = NTP_GeoNodeTree(node_tree, nt_var)
+
+        if bpy.app.version >= (4, 0, 0):
+            self._tree_interface_settings(ntp_nt)
+
+        for node in node_tree.nodes:
+            self._process_node(node, ntp_nt)
+
+        if bpy.app.version >= (3, 6, 0):
+            self._process_zones(ntp_nt.sim_inputs)
+        if bpy.app.version >= (4, 0, 0):
+            self._process_zones(ntp_nt.repeat_inputs)
+
+        #set look of nodes
+        self._set_parents(node_tree)
+        self._set_locations(node_tree)
+        self._set_dimensions(node_tree)
+
+        #create connections
+        self._init_links(node_tree)
+        
+        self._write(f"return {nt_var}\n")
+
+        #create node group
+        self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer)
+
+
+    def _apply_modifier(self, nt: GeometryNodeTree, nt_var: str):
+        #get object
+        self._write(f"{OBJECT_NAME} = bpy.context.object.name", self._outer)
+        self._write(f"{OBJECT} = bpy.data.objects[{OBJECT_NAME}]", self._outer)
+
+        #set modifier to the one we just created
+        mod_name = str_to_py_str(nt.name)
+        self._write(f"{MODIFIER} = obj.modifiers.new(name = {mod_name}, "
+                    f"type = 'NODES')", self._outer)
+        self._write(f"{MODIFIER}.node_group = {nt_var}", self._outer)
+
+
+    def execute(self, context):
+        #find node group to replicate
+        nt = bpy.data.node_groups[self.geo_nodes_group_name]
+
+        #set up names to use in generated addon
+        nt_var = clean_string(nt.name)
+
+        if self.mode == 'ADDON':
+            self._outer = "\t\t"
+            self._inner = "\t\t\t"
+
+            self._setup_addon_directories(context, nt_var)
+
+            self._file = open(f"{self._addon_dir}/__init__.py", "w")
+            
+            self._create_header(nt.name)
+            self._class_name = clean_string(nt.name, lower = False)
+            self._init_operator(nt_var, nt.name)
+            self._write("def execute(self, context):", "\t")
+        else:
+            self._file = StringIO("")
+
+        node_trees_to_process = self._topological_sort(nt)
+
+        for node_tree in node_trees_to_process:  
+            self._process_node_tree(node_tree)
+
+        if self.mode == 'ADDON':
+            self._apply_modifier(nt, nt_var)
+            self._write("return {'FINISHED'}\n", self._outer)
+            self._create_menu_func()
+            self._create_register_func()
+            self._create_unregister_func()
+            self._create_main_func()
+        else:
+            context.window_manager.clipboard = self._file.getvalue()
+        self._file.close()
+
+        if self.mode == 'ADDON':
+            self._zip_addon()
+
+        self._report_finished("geometry node group")
+
+        return {'FINISHED'}
\ No newline at end of file
diff --git a/geometry/ui.py b/geometry/ui.py
new file mode 100644
index 0000000..b3a8283
--- /dev/null
+++ b/geometry/ui.py
@@ -0,0 +1,55 @@
+import bpy
+from bpy.types import Panel
+from bpy.types import Menu
+
+from .operator import NTPGeoNodesOperator
+
+class NTPGeoNodesPanel(Panel):
+    bl_label = "Geometry Nodes to Python"
+    bl_idname = "NODE_PT_geo_nodes"
+    bl_space_type = 'NODE_EDITOR'
+    bl_region_type = 'UI'
+    bl_context = ''
+    bl_category = "NodeToPython"
+
+    @classmethod
+    def poll(cls, context):
+        return True
+    
+    def draw_header(self, context):
+        layout = self.layout
+
+    def draw(self, context):
+        layout = self.layout
+        col = layout.column()
+        row = col.row()
+        
+        # Disables menu when len of geometry nodes is 0
+        geo_node_groups = [node_tree for node_tree in bpy.data.node_groups 
+                            if node_tree.bl_idname == 'GeometryNodeTree']
+        geo_node_groups_exist = len(geo_node_groups) > 0
+        row.enabled = geo_node_groups_exist
+
+        row.alignment = 'EXPAND'
+        row.operator_context = 'INVOKE_DEFAULT'
+        row.menu("NODE_MT_ntp_geo_nodes", text="Geometry Nodes")
+
+class NTPGeoNodesMenu(Menu):
+    bl_idname = "NODE_MT_ntp_geo_nodes"
+    bl_label = "Select Geo Nodes"
+    
+    @classmethod
+    def poll(cls, context):
+        return True
+
+    def draw(self, context):
+        layout = self.layout.column_flow(columns=1)
+        layout.operator_context = 'INVOKE_DEFAULT'
+
+        geo_node_groups = [node_tree for node_tree in bpy.data.node_groups 
+                           if node_tree.bl_idname == 'GeometryNodeTree']
+
+        for node_tree in geo_node_groups:
+            op = layout.operator(NTPGeoNodesOperator.bl_idname, 
+                                    text=node_tree.name)
+            op.geo_nodes_group_name = node_tree.name
\ No newline at end of file
diff --git a/material/__init__.py b/material/__init__.py
new file mode 100644
index 0000000..ceb3a04
--- /dev/null
+++ b/material/__init__.py
@@ -0,0 +1,11 @@
+if "bpy" in locals():
+    import importlib
+    importlib.reload(node_settings)
+    importlib.reload(operator)
+    importlib.reload(ui)
+else:
+    from . import node_settings
+    from . import operator
+    from . import ui
+
+import bpy
\ No newline at end of file
diff --git a/material/node_settings.py b/material/node_settings.py
new file mode 100644
index 0000000..3729d84
--- /dev/null
+++ b/material/node_settings.py
@@ -0,0 +1,396 @@
+from ..utils import ST, NTPNodeSetting
+
+shader_node_settings : dict[str, list[NTPNodeSetting]] = {
+    # INPUT
+    'ShaderNodeAmbientOcclusion' : [
+        NTPNodeSetting("inside",     ST.BOOL),
+        NTPNodeSetting("only_local", ST.BOOL),
+        NTPNodeSetting("samples",    ST.INT)
+    ],
+
+    'ShaderNodeAttribute' : [
+        NTPNodeSetting("attribute_name", ST.STRING), #TODO: separate attribute type?
+        NTPNodeSetting("attribute_type", ST.ENUM)
+    ],
+
+    'ShaderNodeBevel' : [
+        NTPNodeSetting("samples", ST.INT)
+    ],
+
+    'ShaderNodeCameraData' : [],
+
+    'ShaderNodeVertexColor' : [
+        NTPNodeSetting("layer_name", ST.STRING) #TODO: separate color attribute type?
+    ],
+
+    'ShaderNodeHairInfo'     : [],
+    'ShaderNodeFresnel'      : [],
+    'ShaderNodeNewGeometry'  : [],
+    'ShaderNodeLayerWeight'  : [],
+    'ShaderNodeLightPath'    : [],
+    'ShaderNodeObjectInfo'   : [],
+    'ShaderNodeParticleInfo' : [],
+    'ShaderNodePointInfo'    : [],
+    'ShaderNodeRGB'          : [],
+
+    'ShaderNodeTangent' : [
+        NTPNodeSetting("axis",           ST.ENUM),
+        NTPNodeSetting("direction_type", ST.ENUM),
+        NTPNodeSetting("uv_map",         ST.STRING) #TODO: special UV Map type?
+    ],
+
+    'ShaderNodeTexCoord' : [
+        NTPNodeSetting("from_instancer", ST.BOOL),
+        NTPNodeSetting("object",         ST.OBJECT)
+    ],
+
+    'ShaderNodeUVAlongStroke' : [
+        NTPNodeSetting("use_tips", ST.BOOL)
+    ],
+
+    'ShaderNodeUVMap' : [
+        NTPNodeSetting("from_instancer", ST.BOOL), 
+        NTPNodeSetting("uv_map",         ST.STRING)
+    ], #TODO: see ShaderNodeTangent
+
+    'ShaderNodeValue'      : [],
+    'ShaderNodeVolumeInfo' : [],
+
+    'ShaderNodeWireframe' : [
+        NTPNodeSetting("use_pixel_size", ST.BOOL)
+    ],
+
+
+    # OUTPUT
+    'ShaderNodeOutputAOV' : [
+        NTPNodeSetting("name", ST.STRING)
+    ],
+
+    'ShaderNodeOutputLight' : [
+        NTPNodeSetting("is_active_output", ST.BOOL),
+        NTPNodeSetting("target",           ST.ENUM)
+    ],
+
+    'ShaderNodeOutputLineStyle' : [
+        NTPNodeSetting("blend_type",       ST.ENUM),
+        NTPNodeSetting("is_active_output", ST.BOOL),
+        NTPNodeSetting("target",           ST.ENUM),
+        NTPNodeSetting("use_alpha",        ST.BOOL),
+        NTPNodeSetting("use_clamp",        ST.BOOL)
+    ],
+
+    'ShaderNodeOutputMaterial' : [
+        NTPNodeSetting("is_active_output", ST.BOOL),
+        NTPNodeSetting("target",           ST.ENUM)
+    ],
+
+    'ShaderNodeOutputWorld' : [
+        NTPNodeSetting("is_active_output", ST.BOOL),
+        NTPNodeSetting("target",           ST.ENUM)
+    ],
+
+
+    # SHADER
+    'ShaderNodeAddShader' : [],
+
+    'ShaderNodeBsdfAnisotropic' : [
+        NTPNodeSetting("distribution", ST.ENUM)
+    ],
+
+    'ShaderNodeBackground'  : [],
+    'ShaderNodeBsdfDiffuse' : [],
+    'ShaderNodeEmission'    : [],
+
+    'ShaderNodeBsdfGlass' : [
+        NTPNodeSetting("distribution", ST.ENUM)
+    ],
+
+    'ShaderNodeBsdfGlossy' : [
+        NTPNodeSetting("distribution", ST.ENUM)
+    ],
+
+    'ShaderNodeBsdfHair' : [
+        NTPNodeSetting("component", ST.ENUM)
+    ],
+
+    'ShaderNodeHoldout'   : [],
+    'ShaderNodeMixShader' : [],
+
+    'ShaderNodeBsdfPrincipled' : [
+        NTPNodeSetting("distribution",      ST.ENUM),
+        NTPNodeSetting("subsurface_method", ST.ENUM)
+    ],
+
+    'ShaderNodeBsdfHairPrincipled' : [
+        NTPNodeSetting("model",           ST.ENUM),
+        NTPNodeSetting("parametrization", ST.ENUM)
+    ],
+
+    'ShaderNodeVolumePrincipled' : [],
+
+    'ShaderNodeBsdfRefraction' : [
+        NTPNodeSetting("distribution", ST.ENUM)
+    ],
+
+    'ShaderNodeBsdfSheen' : [
+        NTPNodeSetting("distribution", ST.ENUM, min_version = (4, 0, 0))
+    ],
+
+    'ShaderNodeEeveeSpecular' : [],
+
+    'ShaderNodeSubsurfaceScattering' : [
+        NTPNodeSetting("falloff", ST.ENUM)
+    ],
+
+    'ShaderNodeBsdfToon' : [
+        NTPNodeSetting("component", ST.ENUM)
+    ],
+
+    'ShaderNodeBsdfTranslucent'  : [],
+    'ShaderNodeBsdfTransparent'  : [],
+    'ShaderNodeBsdfVelvet'       : [],
+    'ShaderNodeVolumeAbsorption' : [],
+    'ShaderNodeVolumeScatter'    : [],
+
+
+    # TEXTURE
+    'ShaderNodeTexBrick' : [
+        NTPNodeSetting("offset",           ST.FLOAT), 
+        NTPNodeSetting("offset_frequency", ST.INT),
+        NTPNodeSetting("squash",           ST.FLOAT),
+        NTPNodeSetting("squash_frequency", ST.INT)
+    ],
+
+    'ShaderNodeTexChecker' : [],
+
+    'ShaderNodeTexEnvironment'  : [
+        NTPNodeSetting("image",         ST.IMAGE),
+        NTPNodeSetting("image_user",    ST.IMAGE_USER),
+        NTPNodeSetting("interpolation", ST.ENUM),
+        NTPNodeSetting("projection",    ST.ENUM)
+    ],
+
+    'ShaderNodeTexGradient' : [
+        NTPNodeSetting("gradient_type", ST.ENUM)
+    ],
+
+    'ShaderNodeTexIES' : [
+        NTPNodeSetting("filepath", ST.STRING), #TODO
+        NTPNodeSetting("ies",      ST.TEXT),
+        NTPNodeSetting("mode",     ST.ENUM)
+    ],
+
+    'ShaderNodeTexImage' : [
+        NTPNodeSetting("extension",        ST.ENUM),
+        NTPNodeSetting("image",            ST.IMAGE),
+        NTPNodeSetting("image_user",       ST.IMAGE_USER),
+        NTPNodeSetting("interpolation",    ST.ENUM),
+        NTPNodeSetting("projection",       ST.ENUM),
+        NTPNodeSetting("projection_blend", ST.FLOAT)
+    ],
+
+    'ShaderNodeTexMagic' : [
+        NTPNodeSetting("turbulence_depth", ST.INT)
+    ],
+
+    'ShaderNodeTexMusgrave' : [
+        NTPNodeSetting("musgrave_dimensions", ST.ENUM),
+        NTPNodeSetting("musgrave_type",       ST.ENUM)
+    ],
+
+    'ShaderNodeTexNoise' : [
+        NTPNodeSetting("noise_dimensions", ST.ENUM),
+        NTPNodeSetting("normalize",        ST.BOOL, min_version = (4, 0, 0))
+    ],
+
+    'ShaderNodeTexPointDensity' : [
+        NTPNodeSetting("interpolation",         ST.ENUM),
+        NTPNodeSetting("object",                ST.OBJECT),
+        NTPNodeSetting("particle_color_source", ST.ENUM),
+        NTPNodeSetting("particle_system",       ST.PARTICLE_SYSTEM),
+        NTPNodeSetting("point_source",          ST.ENUM),
+        NTPNodeSetting("radius",                ST.FLOAT),
+        NTPNodeSetting("resolution",            ST.INT),
+        NTPNodeSetting("space",                 ST.ENUM),
+        NTPNodeSetting("vertex_attribute_name", ST.STRING), #TODO
+        NTPNodeSetting("vertex_color_source",   ST.ENUM)
+    ],
+
+    'ShaderNodeTexSky' : [
+        NTPNodeSetting("air_density",   ST.FLOAT),
+        NTPNodeSetting("altitude",      ST.FLOAT),
+        NTPNodeSetting("dust_density",  ST.FLOAT),
+        NTPNodeSetting("ground_albedo", ST.FLOAT),
+        NTPNodeSetting("ozone_density", ST.FLOAT),
+        NTPNodeSetting("sky_type",      ST.ENUM),
+        NTPNodeSetting("sun_direction", ST.VEC3),
+        NTPNodeSetting("sun_disc",      ST.BOOL),
+        NTPNodeSetting("sun_elevation", ST.FLOAT),
+        NTPNodeSetting("sun_intensity", ST.FLOAT),
+        NTPNodeSetting("sun_rotation",  ST.FLOAT),
+        NTPNodeSetting("sun_size",      ST.FLOAT),
+        NTPNodeSetting("turbidity",     ST.FLOAT)
+    ],
+
+    'ShaderNodeTexVoronoi' : [
+        NTPNodeSetting("distance",           ST.ENUM),
+        NTPNodeSetting("feature",            ST.ENUM),
+        NTPNodeSetting("normalize",          ST.BOOL, min_version = (4, 0, 0)),
+        NTPNodeSetting("voronoi_dimensions", ST.ENUM)
+    ],
+
+    'ShaderNodeTexWave' : [
+        NTPNodeSetting("bands_direction", ST.ENUM),
+        NTPNodeSetting("rings_direction", ST.ENUM),
+        NTPNodeSetting("wave_profile",    ST.ENUM),
+        NTPNodeSetting("wave_type",       ST.ENUM)
+    ],
+
+    'ShaderNodeTexWhiteNoise' : [
+        NTPNodeSetting("noise_dimensions", ST.ENUM)
+    ],
+
+
+    # COLOR
+    'ShaderNodeBrightContrast' : [],
+    'ShaderNodeGamma'          : [],
+    'ShaderNodeHueSaturation'  : [],
+    'ShaderNodeInvert'         : [],
+    'ShaderNodeLightFalloff'   : [],
+
+    'ShaderNodeMix' : [
+        NTPNodeSetting("blend_type",   ST.ENUM, min_version = (3, 4, 0)),
+        NTPNodeSetting("clamp_factor", ST.BOOL, min_version = (3, 4, 0)),
+        NTPNodeSetting("clamp_result", ST.BOOL, min_version = (3, 4, 0)),
+        NTPNodeSetting("data_type",    ST.ENUM, min_version = (3, 4, 0)),
+        NTPNodeSetting("factor_mode",  ST.ENUM, min_version = (3, 4, 0))
+    ],
+
+    'ShaderNodeMixRGB' : [
+        NTPNodeSetting("blend_type", ST.ENUM),
+        NTPNodeSetting("use_alpha",  ST.BOOL),
+        NTPNodeSetting("use_clamp",  ST.BOOL)
+    ],
+
+    'ShaderNodeRGBCurve' : [
+        NTPNodeSetting("mapping", ST.CURVE_MAPPING)
+    ],
+
+
+    # VECTOR
+    'ShaderNodeBump' : [
+        NTPNodeSetting("invert", ST.BOOL)
+    ],
+
+    'ShaderNodeDisplacement' : [
+        NTPNodeSetting("space", ST.ENUM)
+    ],
+
+    'ShaderNodeMapping' : [
+        NTPNodeSetting("vector_type", ST.ENUM)
+    ],
+
+    'ShaderNodeNormal' : [],
+
+    'ShaderNodeNormalMap' : [
+        NTPNodeSetting("space",  ST.ENUM),
+        NTPNodeSetting("uv_map", ST.STRING) #TODO
+    ],
+
+    'ShaderNodeVectorCurve' : [
+        NTPNodeSetting("mapping", ST.CURVE_MAPPING)
+    ],
+
+    'ShaderNodeVectorDisplacement' : [
+        NTPNodeSetting("space", ST.ENUM)
+    ],
+
+    'ShaderNodeVectorRotate' : [
+        NTPNodeSetting("invert",        ST.BOOL),
+        NTPNodeSetting("rotation_type", ST.ENUM)
+    ],
+
+    'ShaderNodeVectorTransform' : [
+        NTPNodeSetting("convert_from", ST.ENUM),
+        NTPNodeSetting("convert_to",   ST.ENUM),
+        NTPNodeSetting("vector_type",  ST.ENUM)
+    ],
+    
+
+    # CONVERTER
+    'ShaderNodeBlackbody' : [],
+
+    'ShaderNodeClamp' : [
+        NTPNodeSetting("clamp_type", ST.ENUM)
+    ],
+
+    'ShaderNodeValToRGB' : [
+        NTPNodeSetting("color_ramp", ST.COLOR_RAMP)
+    ],
+
+    'ShaderNodeCombineColor' : [
+        NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0))
+    ],
+
+    'ShaderNodeCombineXYZ' : [],
+
+    'ShaderNodeFloatCurve' : [
+        NTPNodeSetting("mapping", ST.CURVE_MAPPING)
+    ],
+
+    'ShaderNodeMapRange' : [
+        NTPNodeSetting("clamp",              ST.BOOL),
+        NTPNodeSetting("data_type",          ST.ENUM, min_version = (3, 1, 0)),
+        NTPNodeSetting("interpolation_type", ST.ENUM)
+    ],
+
+    'ShaderNodeMath' : [
+        NTPNodeSetting("operation", ST.ENUM),
+        NTPNodeSetting("use_clamp", ST.BOOL)
+    ],
+
+    'ShaderNodeRGBToBW' : [],
+
+    'ShaderNodeSeparateColor' : [
+        NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0))
+    ],
+
+    'ShaderNodeSeparateXYZ' : [],
+    'ShaderNodeShaderToRGB' : [],
+
+    'ShaderNodeVectorMath' : [
+        NTPNodeSetting("operation", ST.ENUM)
+    ],
+
+    'ShaderNodeWavelength' : [],
+
+
+    # SCRIPT
+    'ShaderNodeScript' : [
+        NTPNodeSetting("bytecode",        ST.STRING), #TODO: test all that
+        NTPNodeSetting("bytecode_hash",   ST.STRING),
+        NTPNodeSetting("filepath",        ST.STRING),
+        NTPNodeSetting("mode",            ST.ENUM),
+        NTPNodeSetting("script",          ST.TEXT),
+        NTPNodeSetting("use_auto_update", ST.BOOL)
+    ],
+
+    # MISC
+    'ShaderNodeGroup' : [
+        NTPNodeSetting('node_tree', ST.NODE_TREE)
+    ],
+
+    'NodeFrame' : [
+        NTPNodeSetting("label_size", ST.INT),
+        NTPNodeSetting("shrink", ST.BOOL),
+        NTPNodeSetting("text", ST.TEXT)
+    ],
+
+    'NodeGroupInput' : [],
+
+    'NodeGroupOutput' : [
+        NTPNodeSetting("is_active_output", ST.BOOL)
+    ],
+
+    'NodeReroute' : []
+}
diff --git a/material/operator.py b/material/operator.py
new file mode 100644
index 0000000..0cc8461
--- /dev/null
+++ b/material/operator.py
@@ -0,0 +1,178 @@
+import bpy
+from bpy.types import Node
+from bpy.types import ShaderNodeTree
+
+from io import StringIO
+
+from ..utils import *
+from ..ntp_operator import NTP_Operator
+from ..ntp_node_tree import NTP_NodeTree
+from .node_settings import shader_node_settings
+
+MAT_VAR = "mat"
+NODE = "node"
+SHADER_OP_RESERVED_NAMES = {MAT_VAR, NODE}
+
+class NTPMaterialOperator(NTP_Operator):
+    bl_idname = "node.ntp_material"
+    bl_label =  "Material to Python"
+    bl_options = {'REGISTER', 'UNDO'}
+    
+    #TODO: add option for general shader node groups
+    material_name: bpy.props.StringProperty(name="Node Group")
+
+    def __init__(self):
+        super().__init__()
+        self._settings = shader_node_settings
+        for name in SHADER_OP_RESERVED_NAMES:
+            self._used_vars[name] = 0
+    
+    def _create_material(self, indent: str):
+        self._write(f"{MAT_VAR} = bpy.data.materials.new("
+                    f"name = {str_to_py_str(self.material_name)})", indent)
+        self._write(f"{MAT_VAR}.use_nodes = True", indent)
+
+    def _initialize_shader_node_tree(self, ntp_node_tree: NTP_NodeTree, 
+                                    nt_name: str) -> None:
+        """
+        Initialize the shader node group
+
+        Parameters:
+        ntp_node_tree (NTP_NodeTree): node tree to be generated and 
+            variable to use
+        nt_name (str): name to use for the node tree
+        """
+        self._write(f"#initialize {nt_name} node group", self._outer)
+        self._write(f"def {ntp_node_tree.var}_node_group():\n", self._outer)
+
+        if ntp_node_tree.node_tree == self._base_node_tree:
+            self._write(f"{ntp_node_tree.var} = {MAT_VAR}.node_tree")
+            self._write(f"#start with a clean node tree")
+            self._write(f"for {NODE} in {ntp_node_tree.var}.nodes:")
+            self._write(f"\t{ntp_node_tree.var}.nodes.remove({NODE})")
+        else:
+            self._write((f"{ntp_node_tree.var} = bpy.data.node_groups.new("
+                         f"type = \'ShaderNodeTree\', "
+                         f"name = {str_to_py_str(nt_name)})"))
+            self._write("")
+
+    def _process_node(self, node: Node, ntp_nt: NTP_NodeTree) -> None:
+        """
+        Create node and set settings, defaults, and cosmetics
+
+        Parameters:
+        node (Node): node to process
+        ntp_nt (NTP_NodeTree): the node tree that node belongs to
+        """
+        node_var: str = self._create_node(node, ntp_nt.var)
+        self._set_settings_defaults(node)
+        
+        if bpy.app.version < (4, 0, 0):
+            if node.bl_idname == 'NodeGroupInput' and not ntp_nt.inputs_set:
+                self._group_io_settings(node, "input", ntp_nt)
+                ntp_nt.inputs_set = True
+
+            elif node.bl_idname == 'NodeGroupOutput' and not ntp_nt.outputs_set:
+                self._group_io_settings(node, "output", ntp_nt)
+                ntp_nt.outputs_set = True
+
+        self._hide_hidden_sockets(node)
+        self._set_socket_defaults(node)
+
+    def _process_node_tree(self, node_tree: ShaderNodeTree) -> None:
+        """
+        Generates a Python function to recreate a node tree
+
+        Parameters:
+        node_tree (NodeTree): node tree to be recreated
+        level (int): number of tabs to use for each line, used with
+            node groups within node groups and script/add-on differences
+        """
+
+        if node_tree == self._base_node_tree:
+            nt_var = self._create_var(self.material_name)
+            nt_name = self.material_name #TODO: this is probably overcomplicating things if we move to a harder material vs shader node tree difference
+        else:
+            nt_var = self._create_var(node_tree.name)
+            nt_name = node_tree.name
+
+        self._node_tree_vars[node_tree] = nt_var
+
+        ntp_nt = NTP_NodeTree(node_tree, nt_var)
+
+        self._initialize_shader_node_tree(ntp_nt, nt_name)
+
+        if bpy.app.version >= (4, 0, 0):
+            self._tree_interface_settings(ntp_nt)
+
+        #initialize nodes
+        self._write(f"#initialize {nt_var} nodes")
+
+        for node in node_tree.nodes:
+            self._process_node(node, ntp_nt)
+
+        self._set_parents(node_tree)
+        self._set_locations(node_tree)
+        self._set_dimensions(node_tree)
+
+        self._init_links(node_tree)
+
+        self._write(f"return {nt_var}\n")
+
+        self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer)
+        
+
+    def execute(self, context):
+        #find node group to replicate
+        self._base_node_tree = bpy.data.materials[self.material_name].node_tree
+        if self._base_node_tree is None:
+            self.report({'ERROR'}, ("NodeToPython: This doesn't seem to be a "
+                                    "valid material. Is Use Nodes selected?"))
+            return {'CANCELLED'}
+
+        #set up names to use in generated addon
+        mat_var = clean_string(self.material_name)
+        
+        if self.mode == 'ADDON':
+            self._outer = "\t\t"
+            self._inner = "\t\t\t"
+
+            self._setup_addon_directories(context, mat_var)
+
+            self._file = open(f"{self._addon_dir}/__init__.py", "w")
+
+            self._create_header(self.material_name)
+            self._class_name = clean_string(self.material_name, lower=False)
+            self._init_operator(mat_var, self.material_name)
+
+            self._write("def execute(self, context):", "\t")
+        else:
+            self._file = StringIO("")
+
+        if self.mode == 'ADDON':
+            self._create_material("\t\t")
+        elif self.mode == 'SCRIPT':
+            self._create_material("")   
+        
+        node_trees_to_process = self._topological_sort(self._base_node_tree)
+
+        for node_tree in node_trees_to_process:
+            self._process_node_tree(node_tree)
+
+        if self.mode == 'ADDON':
+            self._write("return {'FINISHED'}", self._outer)
+            self._create_menu_func()
+            self._create_register_func()
+            self._create_unregister_func()
+            self._create_main_func()
+        else:
+            context.window_manager.clipboard = self._file.getvalue()
+
+        self._file.close()
+        
+        if self.mode == 'ADDON':
+            self._zip_addon(self._zip_dir)
+
+        self._report_finished("material")
+
+        return {'FINISHED'}
\ No newline at end of file
diff --git a/material/ui.py b/material/ui.py
new file mode 100644
index 0000000..1bf6181
--- /dev/null
+++ b/material/ui.py
@@ -0,0 +1,49 @@
+import bpy
+from bpy.types import Panel
+from bpy.types import Menu
+from .operator import NTPMaterialOperator
+
+class NTPMaterialPanel(Panel):
+    bl_label = "Material to Python"
+    bl_idname = "NODE_PT_mat_to_python"
+    bl_space_type = 'NODE_EDITOR'
+    bl_region_type = 'UI'
+    bl_context = ''
+    bl_category = "NodeToPython"
+
+    @classmethod
+    def poll(cls, context):
+        return True
+    
+    def draw_header(self, context):
+        layout = self.layout
+
+    def draw(self, context):
+        layout = self.layout
+        row = layout.row()
+        
+        # Disables menu when there are no materials
+        materials = [mat for mat in bpy.data.materials if mat.node_tree]
+        materials_exist = len(materials) > 0
+        row.enabled = materials_exist
+        
+        row.alignment = 'EXPAND'
+        row.operator_context = 'INVOKE_DEFAULT'
+        row.menu("NODE_MT_ntp_material", text="Materials")
+
+class NTPMaterialMenu(Menu):
+    bl_idname = "NODE_MT_ntp_material"
+    bl_label = "Select Material"
+    
+    @classmethod
+    def poll(cls, context):
+        return True
+
+    def draw(self, context):
+        layout = self.layout.column_flow(columns=1)
+        layout.operator_context = 'INVOKE_DEFAULT'
+        for mat in bpy.data.materials:
+            if mat.node_tree:
+                op = layout.operator(NTPMaterialOperator.bl_idname, 
+                                     text=mat.name)
+                op.material_name = mat.name
\ No newline at end of file
diff --git a/materials.py b/materials.py
deleted file mode 100644
index 466bee5..0000000
--- a/materials.py
+++ /dev/null
@@ -1,315 +0,0 @@
-import bpy
-import os
-
-from .utils import *
-from io import StringIO
-
-node_settings = {
-    #input
-    "ShaderNodeAmbientOcclusion" : ["samples", "inside", "only_local"],
-    "ShaderNodeAttribute" : ["attribute_type", "attribute_name"],
-    "ShaderNodeBevel" : ["samples"],
-    "ShaderNodeVertexColor" : ["layer_name"],
-    "ShaderNodeTangent" : ["direction_type", "axis"],
-    "ShaderNodeTexCoord" : ["object", "from_instancer"],
-    "ShaderNodeUVMap" : ["from_instancer", "uv_map"],
-    "ShaderNodeWireframe" : ["use_pixel_size"],
-
-    #output
-    "ShaderNodeOutputAOV" : ["name"],
-    "ShaderNodeOutputMaterial" : ["target"],
-
-    #shader
-    "ShaderNodeBsdfGlass" : ["distribution"],
-    "ShaderNodeBsdfGlossy" : ["distribution"],
-    "ShaderNodeBsdfPrincipled" : ["distribution", "subsurface_method"],
-    "ShaderNodeBsdfRefraction" : ["distribution"],
-    "ShaderNodeSubsurfaceScattering" : ["falloff"],
-
-    #texture
-    "ShaderNodeTexBrick" : ["offset", "offset_frequency", "squash", "squash_frequency"],
-    "ShaderNodeTexEnvironment" : ["interpolation", "projection", "image_user.frame_duration", "image_user.frame_start", "image_user.frame_offset", "image_user.use_cyclic", "image_user.use_auto_refresh"],
-    "ShaderNodeTexGradient" : ["gradient_type"],
-    "ShaderNodeTexIES" : ["mode"],
-    "ShaderNodeTexImage" : ["interpolation", "projection", "projection_blend", 
-                            "extension"],
-    "ShaderNodeTexMagic" : ["turbulence_depth"],
-    "ShaderNodeTexMusgrave" : ["musgrave_dimensions", "musgrave_type"],
-    "ShaderNodeTexNoise" : ["noise_dimensions"],
-    "ShaderNodeTexPointDensity" : ["point_source", "object", "space", "radius", 
-                                    "interpolation", "resolution", 
-                                    "vertex_color_source"],
-    "ShaderNodeTexSky" : ["sky_type", "sun_direction", "turbidity",
-                            "ground_albedo", "sun_disc", "sun_size", 
-                            "sun_intensity", "sun_elevation", 
-                            "sun_rotation", "altitude", "air_density", 
-                            "dust_density", "ozone_density"],
-    "ShaderNodeTexVoronoi" : ["voronoi_dimensions", "feature", "distance"],
-    "ShaderNodeTexWave" : ["wave_type", "rings_direction", "wave_profile"],
-    "ShaderNodeTexWhiteNoise" : ["noise_dimensions"],
-
-    #color
-    "ShaderNodeMix" : ["data_type", "clamp_factor", "factor_mode", "blend_type",
-                        "clamp_result"],
-
-    #vector
-    "ShaderNodeBump" : ["invert"],
-    "ShaderNodeDisplacement" : ["space"],
-    "ShaderNodeMapping" : ["vector_type"],
-    "ShaderNodeNormalMap" : ["space", "uv_map"],
-    "ShaderNodeVectorDisplacement" : ["space"],
-    "ShaderNodeVectorRotate" : ["rotation_type", "invert"],
-    "ShaderNodeVectorTransform" : ["vector_type", "convert_from", "convert_to"],
-    
-    #converter
-    "ShaderNodeClamp" : ["clamp_type"],
-    "ShaderNodeCombineColor" : ["mode"],
-    "ShaderNodeMapRange" : ["data_type", "interpolation_type", "clamp"],
-    "ShaderNodeMath" : ["operation", "use_clamp"],
-    "ShaderNodeSeparateColor" : ["mode"],
-    "ShaderNodeVectorMath" : ["operation"]
-}
-
-curve_nodes = {'ShaderNodeFloatCurve', 
-               'ShaderNodeVectorCurve', 
-               'ShaderNodeRGBCurve'}
-
-image_nodes = {'ShaderNodeTexEnvironment',
-               'ShaderNodeTexImage'}
-
-class MaterialToPython(bpy.types.Operator):
-    bl_idname = "node.material_to_python"
-    bl_label =  "Material to Python"
-    bl_options = {'REGISTER', 'UNDO'}
-
-    mode : bpy.props.EnumProperty(
-        name = "Mode",
-        items = [
-            ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"),
-            ('ADDON', "Addon", "Create a full addon")
-        ]
-    )
-    material_name: bpy.props.StringProperty(name="Node Group")
-
-    def execute(self, context):
-        #find node group to replicate
-        nt = bpy.data.materials[self.material_name].node_tree
-        if nt is None:
-            self.report({'ERROR'},("NodeToPython: This doesn't seem to be a "
-                                   "valid material. Is Use Nodes selected?"))
-            return {'CANCELLED'}
-
-        #set up names to use in generated addon
-        mat_var = clean_string(self.material_name)
-        
-        if self.mode == 'ADDON':
-            dir = bpy.path.abspath(context.scene.ntp_options.dir_path)
-            if not dir or dir == "":
-                self.report({'ERROR'},
-                            ("NodeToPython: Save your blender file before using "
-                            "NodeToPython!"))
-                return {'CANCELLED'}
-
-            zip_dir = os.path.join(dir, mat_var)
-            addon_dir = os.path.join(zip_dir, mat_var)
-            if not os.path.exists(addon_dir):
-                os.makedirs(addon_dir)
-            file = open(f"{addon_dir}/__init__.py", "w")
-
-            create_header(file, self.material_name)
-            class_name = clean_string(self.material_name, lower=False)
-            init_operator(file, class_name, mat_var, self.material_name)
-
-            file.write("\tdef execute(self, context):\n")
-        else:
-            file = StringIO("")
-
-        def create_material(indent: str):
-            file.write((f"{indent}mat = bpy.data.materials.new("
-                        f"name = {str_to_py_str(self.material_name)})\n"))
-            file.write(f"{indent}mat.use_nodes = True\n")
-        
-        if self.mode == 'ADDON':
-            create_material("\t\t")
-        elif self.mode == 'SCRIPT':
-            create_material("")
-        
-        #set to keep track of already created node trees
-        node_trees = set()
-
-        #dictionary to keep track of node->variable name pairs
-        node_vars = {}
-
-        #keeps track of all used variables
-        used_vars = {}
-
-        def is_outermost_node_group(level: int) -> bool:
-            if self.mode == 'ADDON' and level == 2:
-                return True
-            elif self.mode == 'SCRIPT' and level == 0:
-                return True
-            return False
-
-        def process_mat_node_group(node_tree, level, node_vars, used_vars):
-            if is_outermost_node_group(level):
-                nt_var = create_var(self.material_name, used_vars)
-                nt_name = self.material_name
-            else:
-                nt_var = create_var(node_tree.name, used_vars)
-                nt_name = node_tree.name
-
-            outer, inner = make_indents(level)
-
-            #initialize node group
-            file.write(f"{outer}#initialize {nt_var} node group\n")
-            file.write(f"{outer}def {nt_var}_node_group():\n")
-
-            if is_outermost_node_group(level): #outermost node group
-                file.write(f"{inner}{nt_var} = mat.node_tree\n")
-                file.write(f"{inner}#start with a clean node tree\n")
-                file.write(f"{inner}for node in {nt_var}.nodes:\n")
-                file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n")
-            else:
-                file.write((f"{inner}{nt_var}"
-                        f"= bpy.data.node_groups.new("
-                        f"type = \'ShaderNodeTree\', "
-                        f"name = {str_to_py_str(nt_name)})\n"))
-                file.write("\n")
-
-            inputs_set = False
-            outputs_set = False
-
-            #initialize nodes
-            file.write(f"{inner}#initialize {nt_var} nodes\n")
-
-            #dictionary to keep track of node->variable name pairs
-            node_vars = {}
-
-            for node in node_tree.nodes:
-                if node.bl_idname == 'ShaderNodeGroup':
-                    node_nt = node.node_tree
-                    if node_nt is not None and node_nt not in node_trees:
-                        process_mat_node_group(node_nt, level + 1, node_vars, 
-                                               used_vars)
-                        node_trees.add(node_nt)
-                
-                node_var = create_node(node, file, inner, nt_var, node_vars, 
-                                       used_vars)
-                
-                set_settings_defaults(node, node_settings, file, inner, node_var)
-                hide_sockets(node, file, inner, node_var)
-
-                if node.bl_idname == 'ShaderNodeGroup':
-                    if node.node_tree is not None:
-                        file.write((f"{inner}{node_var}.node_tree = "
-                                    f"bpy.data.node_groups"
-                                    f"[\"{node.node_tree.name}\"]\n"))
-                elif node.bl_idname == 'NodeGroupInput' and not inputs_set:
-                    group_io_settings(node, file, inner, "input", nt_var, node_tree)
-                    inputs_set = True
-
-                elif node.bl_idname == 'NodeGroupOutput' and not outputs_set:
-                    group_io_settings(node, file, inner, "output", nt_var, node_tree)
-                    outputs_set = True
-
-                elif node.bl_idname in image_nodes and self.mode == 'ADDON':
-                    img = node.image
-                    if img is not None and img.source in {'FILE', 'GENERATED', 'TILED'}:
-                        save_image(img, addon_dir)
-                        load_image(img, file, inner, f"{node_var}.image")
-                        image_user_settings(node, file, inner, node_var)
-
-                elif node.bl_idname == 'ShaderNodeValToRGB':
-                    color_ramp_settings(node, file, inner, node_var)
-
-                elif node.bl_idname in curve_nodes:
-                    curve_node_settings(node, file, inner, node_var)
-
-                if self.mode == 'ADDON':
-                    set_input_defaults(node, file, inner, node_var, addon_dir)
-                else:
-                    set_input_defaults(node, file, inner, node_var)
-                set_output_defaults(node, file, inner, node_var)
-
-            set_parents(node_tree, file, inner, node_vars)
-            set_locations(node_tree, file, inner, node_vars)
-            set_dimensions(node_tree, file, inner, node_vars)
-            
-            init_links(node_tree, file, inner, nt_var, node_vars)
-            
-            file.write(f"\n{outer}{nt_var}_node_group()\n\n")
-
-        if self.mode == 'ADDON':
-            level = 2
-        else:
-            level = 0        
-        process_mat_node_group(nt, level, node_vars, used_vars)
-
-        if self.mode == 'ADDON':
-            file.write("\t\treturn {'FINISHED'}\n\n")
-
-            create_menu_func(file, class_name)
-            create_register_func(file, class_name)
-            create_unregister_func(file, class_name)
-            create_main_func(file)
-        else:
-            context.window_manager.clipboard = file.getvalue()
-
-        file.close()
-        
-        if self.mode == 'ADDON':
-            zip_addon(zip_dir)
-        if self.mode == 'SCRIPT':
-            location = "clipboard"
-        else:
-            location = dir
-        self.report({'INFO'}, f"NodeToPython: Saved material to {location}")
-        return {'FINISHED'}
-    
-    def invoke(self, context, event):
-        return context.window_manager.invoke_props_dialog(self)
-    def draw(self, context):
-        self.layout.prop(self, "mode")
-
-class SelectMaterialMenu(bpy.types.Menu):
-    bl_idname = "NODE_MT_npt_mat_selection"
-    bl_label = "Select Material"
-    
-    @classmethod
-    def poll(cls, context):
-        return True
-
-    def draw(self, context):
-        layout = self.layout.column_flow(columns=1)
-        layout.operator_context = 'INVOKE_DEFAULT'
-        for mat in bpy.data.materials:
-            op = layout.operator(MaterialToPython.bl_idname, text=mat.name)
-            op.material_name = mat.name
-    
-class MaterialToPythonPanel(bpy.types.Panel):
-    bl_label = "Material to Python"
-    bl_idname = "NODE_PT_mat_to_python"
-    bl_space_type = 'NODE_EDITOR'
-    bl_region_type = 'UI'
-    bl_context = ''
-    bl_category = "NodeToPython"
-
-    @classmethod
-    def poll(cls, context):
-        return True
-    
-    def draw_header(self, context):
-        layout = self.layout
-
-    def draw(self, context):
-        layout = self.layout
-        row = layout.row()
-        
-        # Disables menu when there are no materials
-        materials = bpy.data.materials
-        materials_exist = len(materials) > 0
-        row.enabled = materials_exist
-        
-        row.alignment = 'EXPAND'
-        row.operator_context = 'INVOKE_DEFAULT'
-        row.menu("NODE_MT_npt_mat_selection", text="Materials")
\ No newline at end of file
diff --git a/ntp_node_tree.py b/ntp_node_tree.py
new file mode 100644
index 0000000..a551d01
--- /dev/null
+++ b/ntp_node_tree.py
@@ -0,0 +1,16 @@
+from bpy.types import NodeTree
+import bpy
+
+class NTP_NodeTree:
+    def __init__(self, node_tree: NodeTree, var: str):
+        # Blender node tree object being copied
+        self.node_tree: NodeTree = node_tree
+
+        # The variable named for the regenerated node tree
+        self.var: str = var
+
+        if bpy.app.version < (4, 0, 0):
+            # Keep track of if we need to set the default values for the node
+            # tree inputs and outputs
+            self.inputs_set: bool = False
+            self.outputs_set: bool = False
\ No newline at end of file
diff --git a/ntp_operator.py b/ntp_operator.py
new file mode 100644
index 0000000..732985a
--- /dev/null
+++ b/ntp_operator.py
@@ -0,0 +1,1214 @@
+import bpy
+from bpy.types import Context, Operator
+from bpy.types import Node, NodeTree
+from bpy_types import bpy_types
+
+if bpy.app.version < (4, 0, 0):
+    from bpy.types import NodeSocketInterface
+else:
+    from bpy.types import NodeTreeInterfacePanel, NodeTreeInterfaceSocket
+    from bpy.types import NodeTreeInterfaceItem
+
+import os
+from typing import TextIO
+import shutil
+
+from .ntp_node_tree import NTP_NodeTree
+from .utils import *
+
+INDEX = "i"
+IMAGE_DIR_NAME = "imgs"
+IMAGE_PATH = "image_path"
+BASE_DIR = "base_dir"
+
+RESERVED_NAMES = {
+                  INDEX,
+                  IMAGE_DIR_NAME,
+                  IMAGE_PATH,
+                  BASE_DIR
+                 }
+
+#node input sockets that are messy to set default values for
+DONT_SET_DEFAULTS = {'NodeSocketGeometry',
+                     'NodeSocketShader',
+                     'NodeSocketVirtual'}
+
+class NTP_Operator(Operator):
+    """
+    "Abstract" base class for all NTP operators. Blender types and abstraction
+    don't seem to mix well, but this should only be inherited from
+    """
+
+    bl_idname = ""
+    bl_label = ""
+
+    mode: bpy.props.EnumProperty(
+        name="Mode",
+        items=[
+            ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"),
+            ('ADDON', "Addon", "Create a full addon")
+        ]
+    )
+
+    # node tree input sockets that have default properties
+    if bpy.app.version < (4, 0, 0):
+        default_sockets_v3 = {'VALUE', 'INT', 'BOOLEAN', 'VECTOR', 'RGBA'}
+    else:
+        nondefault_sockets_v4 = {
+            bpy.types.NodeTreeInterfaceSocketCollection,
+            bpy.types.NodeTreeInterfaceSocketGeometry,
+            bpy.types.NodeTreeInterfaceSocketImage,
+            bpy.types.NodeTreeInterfaceSocketMaterial,
+            bpy.types.NodeTreeInterfaceSocketObject,
+            bpy.types.NodeTreeInterfaceSocketShader,
+            bpy.types.NodeTreeInterfaceSocketTexture
+        }
+
+    def __init__(self):
+        super().__init__()
+
+        # File (TextIO) or string (StringIO) the add-on/script is generated into
+        self._file: TextIO = None
+
+        # Path to the current directory
+        self._dir: str = None
+
+        # Path to the directory of the zip file
+        self._zip_dir: str = None
+
+        # Path to the directory for the generated addon
+        self._addon_dir: str = None
+
+        # Class named for the generated operator
+        self._class_name: str = None
+
+        # Indentation to use for the default write function
+        self._outer: str = ""
+        self._inner: str = "\t"
+
+        # Base node tree we're converting
+        self._base_node_tree: NodeTree = None
+
+        # Dictionary to keep track of node tree->variable name pairs
+        self._node_tree_vars: dict[NodeTree, str] = {}
+
+        # Dictionary to keep track of node->variable name pairs
+        self._node_vars: dict[Node, str] = {}
+
+        # Dictionary to keep track of variables->usage count pairs
+        self._used_vars: dict[str, int] = {}
+
+        # Dictionary used for setting node properties
+        self._settings: dict[str, list[(str, ST)]] = {}
+
+        for name in RESERVED_NAMES:
+            self._used_vars[name] = 0
+
+    def _write(self, string: str, indent: str = None):
+        if indent is None:
+            indent = self._inner
+        self._file.write(f"{indent}{string}\n")
+
+    def _setup_addon_directories(self, context: Context, nt_var: str) -> None:
+        """
+        Finds/creates directories to save add-on to
+        """
+        # find base directory to save new addon
+        self._dir = bpy.path.abspath(context.scene.ntp_options.dir_path)
+        if not self._dir or self._dir == "":
+            self.report({'ERROR'},
+                        ("NodeToPython: Save your blend file before using "
+                         "NodeToPython!"))  # TODO: Still valid??
+            return {'CANCELLED'}  # TODO
+
+        self._zip_dir = os.path.join(self._dir, nt_var)
+        self._addon_dir = os.path.join(self._zip_dir, nt_var)
+
+        if not os.path.exists(self._addon_dir):
+            os.makedirs(self._addon_dir)
+
+    def _create_header(self, name: str) -> None:
+        """
+        Sets up the bl_info and imports the Blender API
+
+        Parameters:
+        file (TextIO): the file for the generated add-on
+        name (str): name of the add-on
+        """
+
+        self._write("bl_info = {", "")
+        self._write(f"\t\"name\" : \"{name}\",", "")
+        self._write("\t\"author\" : \"Node To Python\",", "")
+        self._write("\t\"version\" : (1, 0, 0),", "")
+        self._write(f"\t\"blender\" : {bpy.app.version},", "")
+        self._write("\t\"location\" : \"Object\",", "")  # TODO
+        self._write("\t\"category\" : \"Node\"", "")
+        self._write("}\n", "")
+        self._write("import bpy", "")
+        self._write("import mathutils", "")
+        self._write("import os\n", "")
+
+    def _init_operator(self, idname: str, label: str) -> None:
+        """
+        Initializes the add-on's operator 
+
+        Parameters:
+        file (TextIO): the file for the generated add-on
+        name (str): name for the class
+        idname (str): name for the operator
+        label (str): appearence inside Blender
+        """
+        self._write(f"class {self._class_name}(bpy.types.Operator):", "")
+        self._write(f"\tbl_idname = \"object.{idname}\"", "")
+        self._write(f"\tbl_label = \"{label}\"", "")
+        self._write("\tbl_options = {\'REGISTER\', \'UNDO\'}", "")
+        self._write("")
+
+    def _topological_sort(self, node_tree: NodeTree) -> list[NodeTree]:
+        """
+        Perform a topological sort on the node graph to determine dependencies 
+        and which node groups need processed first
+
+        Parameters:
+        node_tree (NodeTree): the base node tree to convert
+
+        Returns:
+        (list[NodeTree]): the node trees in order of processing
+        """
+        if isinstance(node_tree, bpy.types.CompositorNodeTree):
+            group_node_type = 'CompositorNodeGroup'
+        elif isinstance(node_tree, bpy.types.GeometryNodeTree):
+            group_node_type = 'GeometryNodeGroup'
+        elif isinstance(node_tree, bpy.types.ShaderNodeTree):
+            group_node_type = 'ShaderNodeGroup'
+        
+        visited = set()
+        result: list[NodeTree] = []
+
+        def dfs(nt: NodeTree) -> None:
+            """
+            Helper function to perform depth-first search on a NodeTree
+
+            Parameters:
+            nt (NodeTree): current node tree in the dependency graph
+            """
+            if nt not in visited:
+                visited.add(nt)
+                for group_node in [node for node in nt.nodes
+                                   if node.bl_idname == group_node_type]:
+                    if group_node.node_tree not in visited:
+                        dfs(group_node.node_tree)
+                result.append(nt)
+        
+        dfs(node_tree)
+
+        return result
+
+    def _create_var(self, name: str) -> str:
+        """
+        Creates a unique variable name for a node tree
+
+        Parameters:
+        name (str): basic string we'd like to create the variable name out of
+
+        Returns:
+        clean_name (str): variable name for the node tree
+        """
+        if name == "":
+            name = "unnamed"
+        clean_name = clean_string(name)
+        var = clean_name
+        if var in self._used_vars:
+            self._used_vars[var] += 1
+            return f"{clean_name}_{self._used_vars[var]}"
+        else:
+            self._used_vars[var] = 0
+            return clean_name
+
+    def _create_node(self, node: Node, node_tree_var: str) -> str:
+        """
+        Initializes a new node with location, dimension, and label info
+
+        Parameters:
+        node (Node): node to be copied
+        node_tree_var (str): variable name for the node tree
+        Returns:
+        node_var (str): variable name for the node
+        """
+
+        self._write(f"#node {node.name}")
+
+        node_var = self._create_var(node.name)
+        self._node_vars[node] = node_var
+
+        idname = str_to_py_str(node.bl_idname)
+        self._write(f"{node_var} = {node_tree_var}.nodes.new({idname})")
+
+        # label
+        if node.label:
+            self._write(f"{node_var}.label = {str_to_py_str(node.label)}")
+
+        # name
+        self._write(f"{node_var}.name = {str_to_py_str(node.name)}")
+
+        # color
+        if node.use_custom_color:
+            self._write(f"{node_var}.use_custom_color = True")
+            self._write(f"{node_var}.color = {vec3_to_py_str(node.color)}")
+
+        # mute
+        if node.mute:
+            self._write(f"{node_var}.mute = True")
+
+        return node_var
+
+    def _set_settings_defaults(self, node: Node) -> None:
+        """
+        Sets the defaults for any settings a node may have
+
+        Parameters:
+        node (Node): the node object we're copying settings from
+        node_var (str): name of the variable we're using for the node in our add-on
+        """
+        if node.bl_idname not in self._settings:
+            self.report({'WARNING'},
+                        (f"NodeToPython: couldn't find {node.bl_idname} in "
+                         f"settings. Your Blender version may not be supported"))
+            return
+
+        node_var = self._node_vars[node]
+
+        for setting in self._settings[node.bl_idname]:
+
+            attr_name = setting.name
+            st = setting.st 
+
+            if not hasattr(node, attr_name):
+                if (bpy.app.version >= setting.min_version and 
+                    bpy.app.version <= setting.max_version):
+                    self.report({'WARNING'},
+                                f"NodeToPython: Couldn't find attribute "
+                                f"\"{attr_name}\" for node {node.name} of type "
+                                f"{node.bl_idname}")
+                continue
+            attr = getattr(node, attr_name, None)
+            if attr is None:
+                continue
+
+            setting_str = f"{node_var}.{attr_name}"
+            if st == ST.ENUM:
+                if attr != '':
+                    self._write(f"{setting_str} = {enum_to_py_str(attr)}")
+            elif st == ST.ENUM_SET:
+                self._write(f"{setting_str} = {attr}")
+            elif st == ST.STRING:
+                self._write(f"{setting_str} = {str_to_py_str(attr)}")
+            elif st == ST.BOOL or st == ST.INT or st == ST.FLOAT:
+                self._write(f"{setting_str} = {attr}")
+            elif st == ST.VEC1:
+                self._write(f"{setting_str} = {vec1_to_py_str(attr)}")
+            elif st == ST.VEC2:
+                self._write(f"{setting_str} = {vec2_to_py_str(attr)}")
+            elif st == ST.VEC3:
+                self._write(f"{setting_str} = {vec3_to_py_str(attr)}")
+            elif st == ST.VEC4:
+                self._write(f"{setting_str} = {vec4_to_py_str(attr)}")
+            elif st == ST.COLOR:
+                self._write(f"{setting_str} = {color_to_py_str(attr)}")
+            elif st == ST.MATERIAL:
+                name = str_to_py_str(attr.name)
+                self._write((f"if {name} in bpy.data.materials:"))
+                self._write((f"\t{setting_str} = bpy.data.materials[{name}]"))
+            elif st == ST.OBJECT:
+                name = str_to_py_str(attr.name)
+                self._write((f"if {name} in bpy.data.objects:"))
+                self._write((f"\t{setting_str} = bpy.data.objects[{name}]"))
+            elif st == ST.COLOR_RAMP:
+                self._color_ramp_settings(node, attr_name)
+            elif st == ST.CURVE_MAPPING:
+                self._curve_mapping_settings(node, attr_name)
+            elif st == ST.NODE_TREE:
+                self._node_tree_settings(node, attr_name)
+            elif st == ST.IMAGE:
+                if self._addon_dir is not None and attr is not None:
+                    if attr.source in {'FILE', 'GENERATED', 'TILED'}:
+                        self._save_image(attr)
+                        self._load_image(attr, f"{node_var}.{attr_name}")
+            elif st == ST.IMAGE_USER:
+                self._image_user_settings(attr, f"{node_var}.{attr_name}")
+
+    if bpy.app.version < (4, 0, 0):
+        def _set_group_socket_defaults(self, socket_interface: NodeSocketInterface,
+                                       socket_var: str) -> None:
+            """
+            Set a node group input/output's default properties if they exist
+            Helper function to _group_io_settings()
+
+            Parameters:
+            socket_interface (NodeSocketInterface): socket interface associated
+                with the input/output
+            socket_var (str): variable name for the socket
+            """
+            if socket_interface.type not in self.default_sockets_v3:
+                return
+
+            if socket_interface.type == 'RGBA':
+                dv = vec4_to_py_str(socket_interface.default_value)
+            elif socket_interface.type == 'VECTOR':
+                dv = vec3_to_py_str(socket_interface.default_value)
+            else:
+                dv = socket_interface.default_value
+            self._write(f"{socket_var}.default_value = {dv}")
+
+            # min value
+            if hasattr(socket_interface, "min_value"):
+                min_val = socket_interface.min_value
+                self._write(f"{socket_var}.min_value = {min_val}")
+            # max value
+            if hasattr(socket_interface, "min_value"):
+                max_val = socket_interface.max_value
+                self._write(f"{socket_var}.max_value = {max_val}")
+
+        def _group_io_settings(self, node: Node, 
+                               io: str,  # TODO: convert to enum
+                               ntp_node_tree: NTP_NodeTree) -> None:
+            """
+            Set the settings for group input and output sockets
+
+            Parameters:
+            node (Node) : group input/output node
+            io (str): whether we're generating the input or output settings
+            ntp_node_tree (NTP_NodeTree): node tree that we're generating 
+                input and output settings for
+            """
+            node_tree_var = ntp_node_tree.var
+            node_tree = ntp_node_tree.node_tree
+
+            if io == "input":
+                io_sockets = node.outputs
+                io_socket_interfaces = node_tree.inputs
+            else:
+                io_sockets = node.inputs
+                io_socket_interfaces = node_tree.outputs
+
+            self._write(f"#{node_tree_var} {io}s")
+            for i, inout in enumerate(io_sockets):
+                if inout.bl_idname == 'NodeSocketVirtual':
+                    continue
+                self._write(f"#{io} {inout.name}")
+                idname = enum_to_py_str(inout.bl_idname)
+                name = str_to_py_str(inout.name)
+                self._write(f"{node_tree_var}.{io}s.new({idname}, {name})")
+                socket_interface = io_socket_interfaces[i]
+                socket_var = f"{node_tree_var}.{io}s[{i}]"
+
+                self._set_group_socket_defaults(socket_interface, socket_var)
+
+                # default attribute name
+                if hasattr(socket_interface, "default_attribute_name"):
+                    if socket_interface.default_attribute_name != "":
+                        dan = str_to_py_str(socket_interface.default_attribute_name)
+                        self._write(f"{socket_var}.default_attribute_name = {dan}")
+
+                # attribute domain
+                if hasattr(socket_interface, "attribute_domain"):
+                    ad = enum_to_py_str(socket_interface.attribute_domain)
+                    self._write(f"{socket_var}.attribute_domain = {ad}")
+
+                # tooltip
+                if socket_interface.description != "":
+                    description = str_to_py_str(socket_interface.description)
+                    self._write(f"{socket_var}.description = {description}")
+
+                # hide_value
+                if socket_interface.hide_value is True:
+                    self._write(f"{socket_var}.hide_value = True")
+
+                # hide in modifier
+                if hasattr(socket_interface, "hide_in_modifier"):
+                    if socket_interface.hide_in_modifier is True:
+                        self._write(f"{socket_var}.hide_in_modifier = True")
+
+                self._write("")
+            self._write("")
+
+    elif bpy.app.version >= (4, 0, 0):
+        def _set_tree_socket_defaults(self, socket_interface: NodeTreeInterfaceSocket,
+                                      socket_var: str) -> None:
+            """
+            Set a node tree input/output's default properties if they exist
+
+            Helper function to _create_socket()
+
+            Parameters:
+            socket_interface (NodeTreeInterfaceSocket): socket interface associated
+                with the input/output
+            socket_var (str): variable name for the socket
+            """
+            if type(socket_interface) in self.nondefault_sockets_v4:
+                return
+
+            dv = socket_interface.default_value
+
+            if type(socket_interface) == bpy.types.NodeTreeInterfaceSocketColor:
+                dv = vec4_to_py_str(dv)
+            elif type(dv) in {mathutils.Vector, mathutils.Euler}:
+                dv = vec3_to_py_str(dv)
+            elif type(dv) == bpy_types.bpy_prop_array:
+                dv = array_to_py_str(dv)
+            elif type(dv) == str:
+                dv = str_to_py_str(dv)
+            self._write(f"{socket_var}.default_value = {dv}")
+
+            # min value
+            if hasattr(socket_interface, "min_value"):
+                min_val = socket_interface.min_value
+                self._write(f"{socket_var}.min_value = {min_val}")
+            # max value
+            if hasattr(socket_interface, "min_value"):
+                max_val = socket_interface.max_value
+                self._write(f"{socket_var}.max_value = {max_val}")
+
+        def _create_socket(self, socket: NodeTreeInterfaceSocket, 
+                           parent: NodeTreeInterfacePanel, 
+                           panel_dict: dict[NodeTreeInterfacePanel, str], 
+                           ntp_nt: NTP_NodeTree) -> None:
+            """
+            Initialize a new tree socket
+
+            Helper function to _process_items()
+
+            Parameters:
+            socket (NodeTreeInterfaceSocket): the socket to recreate
+            parent (NodeTreeInterfacePanel): parent panel of the socket
+                (possibly None)
+            panel_dict (dict[NodeTreeInterfacePanel, str]: panel -> variable
+            ntp_nt (NTP_NodeTree): owner of the socket
+            """
+
+            self._write(f"#Socket {socket.name}")
+            # initialization
+            socket_var = self._create_var(socket.name + "_socket") 
+            name = str_to_py_str(socket.name)
+            in_out_enum = enum_to_py_str(socket.in_out)
+
+            socket_type = enum_to_py_str(socket.bl_socket_idname)
+            """
+            I might be missing something, but the Python API's set up a bit 
+            weird here now. The new socket initialization only accepts types
+            from a list of basic ones, but there doesn't seem to be a way of
+            retrieving just this basic type without the subtype information.
+            """
+            if 'Float' in socket_type:
+                socket_type = enum_to_py_str('NodeSocketFloat')
+            elif 'Int' in socket_type:
+                socket_type = enum_to_py_str('NodeSocketInt')
+            elif 'Vector' in socket_type:
+                socket_type = enum_to_py_str('NodeSocketVector')
+
+            if parent is None:
+                optional_parent_str = ""
+            else:
+                optional_parent_str = f", parent = {panel_dict[parent]}"
+
+            self._write(f"{socket_var} = "
+                        f"{ntp_nt.var}.interface.new_socket("
+                        f"name = {name}, in_out={in_out_enum}, "
+                        f"socket_type = {socket_type}"
+                        f"{optional_parent_str})")
+
+            # subtype
+            if hasattr(socket, "subtype"):
+                subtype = enum_to_py_str(socket.subtype)
+                self._write(f"{socket_var}.subtype = {subtype}")
+
+                self._set_tree_socket_defaults(socket, socket_var)
+
+            # default attribute name
+            if socket.default_attribute_name != "":
+                dan = str_to_py_str(
+                    socket.default_attribute_name)
+                self._write(f"{socket_var}.default_attribute_name = {dan}")
+
+            # attribute domain
+            ad = enum_to_py_str(socket.attribute_domain)
+            self._write(f"{socket_var}.attribute_domain = {ad}")
+
+            # hide_value
+            if socket.hide_value is True:
+                self._write(f"{socket_var}.hide_value = True")
+
+            # hide in modifier
+            if socket.hide_in_modifier is True:
+                self._write(f"{socket_var}.hide_in_modifier = True")
+
+            # force non field
+            if socket.force_non_field is True:
+                self._write(f"{socket_var}.force_non_field = True")
+            
+            # tooltip
+            if socket.description != "":
+                description = str_to_py_str(socket.description)
+                self._write(f"{socket_var}.description = {description}")
+
+            self._write("")
+
+        def _create_panel(self, panel: NodeTreeInterfacePanel, 
+                          panel_dict: dict[NodeTreeInterfacePanel], 
+                          items_processed: set[NodeTreeInterfacePanel], 
+                          parent: NodeTreeInterfacePanel, ntp_nt: NTP_NodeTree):
+            """
+            Initialize a new tree panel and its subitems
+
+            Helper function to _process_items()
+
+            Parameters:
+            panel (NodeTreeInterfacePanel): the panel to recreate
+            panel_dict (dict[NodeTreeInterfacePanel, str]: panel -> variable
+            items_processed (set[NodeTreeInterfacePanel]): set of already
+                processed items, so none are done twice
+            parent (NodeTreeInterfacePanel): parent panel of the socket
+                (possibly None)
+            ntp_nt (NTP_NodeTree): owner of the socket
+            """
+
+            self._write(f"#Panel {panel.name}")
+
+            panel_var = self._create_var(panel.name + "_panel")
+            panel_dict[panel] = panel_var
+
+            description_str = ""
+            if panel.description != "":
+                description_str = f", description = {str_to_py_str(panel.description)}"
+
+            closed_str = ""
+            if panel.default_closed is True:
+                closed_str = f", default_closed=True"
+                
+            parent_str = ""
+            if parent is not None:
+                parent_str = f", parent = {panel_dict[parent]}"
+                
+
+            self._write(f"{panel_var} = "
+                        f"{ntp_nt.var}.interface.new_panel("
+                        f"{str_to_py_str(panel.name)}{description_str}"
+                        f"{closed_str}{parent_str})")
+
+                # tooltip
+            if panel.description != "":
+                description = str_to_py_str(panel.description)
+                self._write(f"{panel_var}.description = {description}")
+
+            panel_dict[panel] = panel_var
+
+            if len(panel.interface_items) > 0:
+                self._process_items(panel, panel_dict, items_processed, ntp_nt)
+            
+            self._write("")
+
+        def _process_items(self, parent: NodeTreeInterfacePanel, 
+                           panel_dict: dict[NodeTreeInterfacePanel], 
+                           items_processed: set[NodeTreeInterfacePanel], 
+                           ntp_nt: NTP_NodeTree) -> None:
+            """
+            Recursive function to process all node tree interface items in a 
+            given layer
+
+            Helper function to _tree_interface_settings()
+
+            Parameters:
+            parent (NodeTreeInterfacePanel): parent panel of the layer
+                (possibly None to signify the base)
+            panel_dict (dict[NodeTreeInterfacePanel, str]: panel -> variable
+            items_processed (set[NodeTreeInterfacePanel]): set of already
+                processed items, so none are done twice
+            ntp_nt (NTP_NodeTree): owner of the socket
+            """
+            if parent is None:
+                items = ntp_nt.node_tree.interface.items_tree
+            else:
+                items = parent.interface_items
+
+            for item in items:
+                if item.parent.index != -1 and item.parent not in panel_dict:
+                    continue # child of panel not processed yet
+                if item in items_processed:
+                    continue
+                
+                items_processed.add(item)
+
+                if item.item_type == 'SOCKET':
+                    self._create_socket(item, parent, panel_dict, ntp_nt)
+
+                elif item.item_type == 'PANEL':
+                    self._create_panel(item, panel_dict, items_processed,
+                                       parent, ntp_nt)
+
+        def _tree_interface_settings(self, ntp_nt: NTP_NodeTree) -> None:
+            """
+            Set the settings for group input and output sockets
+
+            Parameters:
+            ntp_nt (NTP_NodeTree): the node tree to set the interface for
+            """
+
+            self._write(f"#{ntp_nt.var} interface")
+            panel_dict: dict[NodeTreeInterfacePanel, str] = {}
+            items_processed: set[NodeTreeInterfaceItem] = set()
+
+            self._process_items(None, panel_dict, items_processed, ntp_nt)
+
+            self._write("")
+
+    def _set_input_defaults(self, node: Node) -> None:
+        """
+        Sets defaults for input sockets
+
+        Parameters:
+        node (Node): node we're setting inputs for
+        """
+        if node.bl_idname == 'NodeReroute':
+            return
+
+        node_var = self._node_vars[node]
+
+        for i, input in enumerate(node.inputs):
+            if input.bl_idname not in DONT_SET_DEFAULTS and not input.is_linked:
+                # TODO: this could be cleaner
+                socket_var = f"{node_var}.inputs[{i}]"
+
+                # colors
+                if input.bl_idname == 'NodeSocketColor':
+                    default_val = vec4_to_py_str(input.default_value)
+
+                # vector types
+                elif "Vector" in input.bl_idname:
+                    default_val = vec3_to_py_str(input.default_value)
+
+                # rotation types
+                elif input.bl_idname == 'NodeSocketRotation':
+                    default_val = vec3_to_py_str(input.default_value)
+
+                # strings
+                elif input.bl_idname == 'NodeSocketString':
+                    default_val = str_to_py_str(input.default_value)
+
+                # images
+                elif input.bl_idname == 'NodeSocketImage':
+                    img = input.default_value
+                    if img is not None and self._addon_dir != None:  # write in a better way
+                        self._save_image(img)
+                        self._load_image(img, f"{socket_var}.default_value")
+                    default_val = None
+
+                # materials
+                elif input.bl_idname == 'NodeSocketMaterial':
+                    self._in_file_inputs(input, socket_var, "materials")
+                    default_val = None
+
+                # collections
+                elif input.bl_idname == 'NodeSocketCollection':
+                    self._in_file_inputs(input, socket_var, "collections")
+                    default_val = None
+
+                # objects
+                elif input.bl_idname == 'NodeSocketObject':
+                    self._in_file_inputs(input, socket_var, "objects")
+                    default_val = None
+
+                # textures
+                elif input.bl_idname == 'NodeSocketTexture':
+                    self._in_file_inputs(input, socket_var, "textures")
+                    default_val = None
+
+                else:
+                    default_val = input.default_value
+                if default_val is not None:
+                    self._write(f"#{input.identifier}")
+                    self._write(f"{socket_var}.default_value = {default_val}")
+        self._write("")
+
+    def _set_output_defaults(self, node: Node) -> None:
+        """
+        Some output sockets need default values set. It's rather annoying
+
+        Parameters:
+        node (Node): node for the output we're setting
+        """
+        # TODO: probably should define elsewhere
+        output_default_nodes = {'ShaderNodeValue',
+                                'ShaderNodeRGB',
+                                'ShaderNodeNormal',
+                                'CompositorNodeValue',
+                                'CompositorNodeRGB',
+                                'CompositorNodeNormal'}
+
+        if node.bl_idname not in output_default_nodes:
+            return
+
+        node_var = self._node_vars[node]
+
+        dv = node.outputs[0].default_value
+        if node.bl_idname in {'ShaderNodeRGB', 'CompositorNodeRGB'}:
+            dv = vec4_to_py_str(list(dv))
+        if node.bl_idname in {'ShaderNodeNormal', 'CompositorNodeNormal'}:
+            dv = vec3_to_py_str(dv)
+        self._write(f"{node_var}.outputs[0].default_value = {dv}")
+
+    def _in_file_inputs(self, input: bpy.types.NodeSocket, socket_var: str,
+                        type: str) -> None:
+        """
+        Sets inputs for a node input if one already exists in the blend file
+
+        Parameters:
+        input (bpy.types.NodeSocket): input socket we're setting the value for
+        socket_var (str): variable name we're using for the socket
+        type (str): from what section of bpy.data to pull the default value from
+        """
+
+        if input.default_value is None:
+            return
+        name = str_to_py_str(input.default_value.name)
+        self._write(f"if {name} in bpy.data.{type}:")
+        self._write(f"\t{socket_var}.default_value = bpy.data.{type}[{name}]")
+
+    def _set_socket_defaults(self, node: Node):
+        """
+        Set input and output socket defaults
+        """
+        self._set_input_defaults(node)
+        self._set_output_defaults(node)
+
+    def _color_ramp_settings(self, node: Node, color_ramp_name: str) -> None:
+        """
+        Replicate a color ramp node
+
+        Parameters
+        node (Node): node object we're copying settings from
+        color_ramp_name (str): name of the color ramp to be copied
+        """
+
+        color_ramp: bpy.types.ColorRamp = getattr(node, color_ramp_name)
+        if not color_ramp:
+            raise ValueError(f"No color ramp named \"{color_ramp_name}\" found")
+
+        node_var = self._node_vars[node]
+
+        # settings
+        ramp_str = f"{node_var}.{color_ramp_name}"
+
+        #color mode
+        color_mode = enum_to_py_str(color_ramp.color_mode)
+        self._write(f"{ramp_str}.color_mode = {color_mode}")
+
+        #hue interpolation
+        hue_interpolation = enum_to_py_str(color_ramp.hue_interpolation)
+        self._write(f"{ramp_str}.hue_interpolation = {hue_interpolation}")
+
+        #interpolation
+        interpolation = enum_to_py_str(color_ramp.interpolation)
+        self._write(f"{ramp_str}.interpolation = {interpolation}")
+        self._write("")
+
+        # key points
+        self._write(f"#initialize color ramp elements")
+        self._write((f"{ramp_str}.elements.remove"
+                    f"({ramp_str}.elements[0])"))
+        for i, element in enumerate(color_ramp.elements):
+            element_var = self._create_var(f"{node_var}_cre_{i}")
+            if i == 0:
+                self._write(f"{element_var} = {ramp_str}.elements[{i}]")
+                self._write(f"{element_var}.position = {element.position}")
+            else:
+                self._write(f"{element_var} = {ramp_str}.elements"
+                            f".new({element.position})")
+
+            self._write(f"{element_var}.alpha = {element.alpha}")
+            color_str = vec4_to_py_str(element.color)
+            self._write(f"{element_var}.color = {color_str}\n")
+
+    def _curve_mapping_settings(self, node: Node,
+                                curve_mapping_name: str) -> None:
+        """
+        Sets defaults for Float, Vector, and Color curves
+
+        Parameters:
+        node (Node): curve node we're copying settings from
+        curve_mapping_name (str): name of the curve mapping to be set
+        """
+
+        mapping = getattr(node, curve_mapping_name)
+        if not mapping:
+            raise ValueError((f"Curve mapping \"{curve_mapping_name}\" not found "
+                              f"in node \"{node.bl_idname}\""))
+
+        node_var = self._node_vars[node]
+
+        # mapping settings
+        self._write(f"#mapping settings")
+        mapping_var = f"{node_var}.{curve_mapping_name}"
+
+        # extend
+        extend = enum_to_py_str(mapping.extend)
+        self._write(f"{mapping_var}.extend = {extend}")
+        # tone
+        tone = enum_to_py_str(mapping.tone)
+        self._write(f"{mapping_var}.tone = {tone}")
+
+        # black level
+        b_lvl_str = vec3_to_py_str(mapping.black_level)
+        self._write(f"{mapping_var}.black_level = {b_lvl_str}")
+        # white level
+        w_lvl_str = vec3_to_py_str(mapping.white_level)
+        self._write(f"{mapping_var}.white_level = {w_lvl_str}")
+
+        # minima and maxima
+        min_x = mapping.clip_min_x
+        self._write(f"{mapping_var}.clip_min_x = {min_x}")
+        min_y = mapping.clip_min_y
+        self._write(f"{mapping_var}.clip_min_y = {min_y}")
+        max_x = mapping.clip_max_x
+        self._write(f"{mapping_var}.clip_max_x = {max_x}")
+        max_y = mapping.clip_max_y
+        self._write(f"{mapping_var}.clip_max_y = {max_y}")
+
+        # use_clip
+        use_clip = mapping.use_clip
+        self._write(f"{mapping_var}.use_clip = {use_clip}")
+
+        # create curves
+        for i, curve in enumerate(mapping.curves):
+            self._create_curve_map(node, i, curve, curve_mapping_name)
+
+        # update curve
+        self._write(f"#update curve after changes")
+        self._write(f"{mapping_var}.update()")
+
+    def _create_curve_map(self, node: Node, i: int, curve: bpy.types.CurveMap,
+                          curve_mapping_name: str) -> None:
+        """
+        Helper function to create the ith curve of a node's curve mapping
+
+        Parameters:
+        node (Node): the node with a curve mapping
+        i (int): index of the CurveMap within the mapping
+        curve (bpy.types.CurveMap): the curve map to recreate
+        curve_mapping_name (str): attribute name of the recreated curve mapping
+        """
+        node_var = self._node_vars[node]
+        
+        self._write(f"#curve {i}")
+        curve_i_var = self._create_var(f"{node_var}_curve_{i}")
+        self._write(f"{curve_i_var} = "
+                    f"{node_var}.{curve_mapping_name}.curves[{i}]")
+
+        # Remove default points when CurveMap is initialized with more than
+        # two points (just CompositorNodeHueCorrect)
+        if (node.bl_idname == 'CompositorNodeHueCorrect'):
+            self._write(f"for {INDEX} in range"
+                        f"(len({curve_i_var}.points.values()) - 1, 1, -1):")
+            self._write(f"\t{curve_i_var}.points.remove("
+                        f"{curve_i_var}.points[{INDEX}])")
+
+        for j, point in enumerate(curve.points):
+            self._create_curve_map_point(j, point, curve_i_var)
+
+    def _create_curve_map_point(self, j: int, point: bpy.types.CurveMapPoint,
+                                curve_i_var: str) -> None:
+        """
+        Helper function to recreate a curve map point
+
+        Parameters:
+        j (int): index of the point within the curve map
+        point (CurveMapPoint): point to recreate
+        curve_i_var (str): variable name of the point's curve map
+        """
+        point_j_var = self._create_var(f"{curve_i_var}_point_{j}")
+
+        loc = point.location
+        loc_str = f"{loc[0]}, {loc[1]}"
+        if j < 2:
+            self._write(f"{point_j_var} = {curve_i_var}.points[{j}]")
+            self._write(f"{point_j_var}.location = ({loc_str})")
+        else:
+            self._write(f"{point_j_var} = {curve_i_var}.points.new({loc_str})")
+
+        handle = enum_to_py_str(point.handle_type)
+        self._write(f"{point_j_var}.handle_type = {handle}")
+    
+    def _node_tree_settings(self, node: Node, attr_name: str) -> None:
+        """
+        Processes node tree of group node if one is present
+
+        Parameters:
+        node (Node): the group node
+        attr_name (str): name of the node tree attribute
+        """
+        node_tree = getattr(node, attr_name)
+        if node_tree is None:
+            return
+        if node_tree in self._node_tree_vars:
+            nt_var = self._node_tree_vars[node_tree]
+            node_var = self._node_vars[node]
+            self._write(f"{node_var}.{attr_name} = {nt_var}")
+        else:
+            self.report({'WARNING'}, (f"NodeToPython: Node tree dependency graph " 
+                                    f"wasn't properly initialized"))
+
+    def _save_image(self, img: bpy.types.Image) -> None:
+        """
+        Saves an image to an image directory of the add-on
+
+        Parameters:
+        img (bpy.types.Image): image to be saved
+        """
+
+        if img is None:
+            return
+
+        # create image dir if one doesn't exist
+        img_dir = os.path.join(self._addon_dir, IMAGE_DIR_NAME)
+        if not os.path.exists(img_dir):
+            os.mkdir(img_dir)
+
+        # save the image
+        img_str = img_to_py_str(img)
+        img_path = f"{img_dir}/{img_str}"
+        if not os.path.exists(img_path):
+            img.save_render(img_path)
+
+    def _load_image(self, img: bpy.types.Image, img_var: str) -> None:
+        """
+        Loads an image from the add-on into a blend file and assigns it
+
+        Parameters:
+        img (bpy.types.Image): Blender image from the original node group
+        img_var (str): variable name to be used for the image
+        """
+
+        if img is None:
+            return
+
+        img_str = img_to_py_str(img)
+
+        # TODO: convert to special variables
+        self._write(f"#load image {img_str}")
+        self._write(f"{BASE_DIR} = "
+                    f"os.path.dirname(os.path.abspath(__file__))")
+        self._write(f"{IMAGE_PATH} = "
+                    f"os.path.join({BASE_DIR}, \"{IMAGE_DIR_NAME}\", "
+                    f"\"{img_str}\")")
+        self._write(f"{img_var} = bpy.data.images.load"
+                    f"({IMAGE_PATH}, check_existing = True)")
+
+        # copy image settings
+        self._write(f"#set image settings")
+
+        # source
+        source = enum_to_py_str(img.source)
+        self._write(f"{img_var}.source = {source}")
+
+        # color space settings
+        color_space = enum_to_py_str(img.colorspace_settings.name)
+        self._write(f"{img_var}.colorspace_settings.name = {color_space}")
+
+        # alpha mode
+        alpha_mode = enum_to_py_str(img.alpha_mode)
+        self._write(f"{img_var}.alpha_mode = {alpha_mode}")
+
+    def _image_user_settings(self, img_user: bpy.types.ImageUser,
+                             img_user_var: str) -> None:
+        """
+        Replicate the image user of an image node
+
+        Parameters
+        img_usr (bpy.types.ImageUser): image user to be copied
+        img_usr_var (str): variable name for the generated image user
+        """
+
+        img_usr_attrs = ["frame_current", "frame_duration", "frame_offset",
+                         "frame_start", "tile", "use_auto_refresh", "use_cyclic"]
+
+        for img_usr_attr in img_usr_attrs:
+            self._write(f"{img_user_var}.{img_usr_attr} = "
+                        f"{getattr(img_user, img_usr_attr)}")
+
+    def _set_parents(self, node_tree: NodeTree) -> None:
+        """
+        Sets parents for all nodes, mostly used to put nodes in frames
+
+        Parameters:
+        node_tree (NodeTree): node tree we're obtaining nodes from
+        """
+        parent_comment = False
+        for node in node_tree.nodes:
+            if node is not None and node.parent is not None:
+                if not parent_comment:
+                    self._write(f"#Set parents")
+                    parent_comment = True
+                node_var = self._node_vars[node]
+                parent_var = self._node_vars[node.parent]
+                self._write(f"{node_var}.parent = {parent_var}")
+        self._write("")
+
+    def _set_locations(self, node_tree: NodeTree) -> None:
+        """
+        Set locations for all nodes
+
+        Parameters:
+        node_tree (NodeTree): node tree we're obtaining nodes from
+        """
+
+        self._write(f"#Set locations")
+        for node in node_tree.nodes:
+            node_var = self._node_vars[node]
+            self._write(f"{node_var}.location "
+                        f"= ({node.location.x}, {node.location.y})")
+        self._write("")
+
+    def _set_dimensions(self, node_tree: NodeTree) -> None:
+        """
+        Set dimensions for all nodes
+
+        Parameters:
+        node_tree (NodeTree): node tree we're obtaining nodes from
+        """
+        self._write(f"#Set dimensions")
+        for node in node_tree.nodes:
+            node_var = self._node_vars[node]
+            self._write(f"{node_var}.width, {node_var}.height "
+                        f"= {node.width}, {node.height}")
+        self._write("")
+
+    def _init_links(self, node_tree: NodeTree) -> None:
+        """
+        Create all the links between nodes
+
+        Parameters:
+        node_tree (NodeTree): node tree to copy, with variable
+        """
+
+        nt_var = self._node_tree_vars[node_tree]
+
+        links = node_tree.links
+        if links:
+            self._write(f"#initialize {nt_var} links")
+
+        for link in links:
+            in_node_var = self._node_vars[link.from_node]
+            input_socket = link.from_socket
+
+            """
+            Blender's socket dictionary doesn't guarantee 
+            unique keys, which has caused much wailing and
+            gnashing of teeth. This is a quick fix that
+            doesn't run quick
+            """
+            # TODO: try using index() method
+            for i, item in enumerate(link.from_node.outputs.items()):
+                if item[1] == input_socket:
+                    input_idx = i
+                    break
+
+            out_node_var = self._node_vars[link.to_node]
+            output_socket = link.to_socket
+
+            for i, item in enumerate(link.to_node.inputs.items()):
+                if item[1] == output_socket:
+                    output_idx = i
+                    break
+
+            self._write(f"#{in_node_var}.{input_socket.name} "
+                        f"-> {out_node_var}.{output_socket.name}")
+            self._write(f"{nt_var}.links.new({in_node_var}"
+                        f".outputs[{input_idx}], "
+                        f"{out_node_var}.inputs[{output_idx}])")
+
+    def _hide_hidden_sockets(self, node: Node) -> None:
+        """
+        Hide hidden sockets
+
+        Parameters:
+        node (Node): node object we're copying socket settings from
+        """
+        node_var = self._node_vars[node]
+
+        for i, socket in enumerate(node.inputs):
+            if socket.hide is True:
+                self._write(f"{node_var}.inputs[{i}].hide = True")
+        for i, socket in enumerate(node.outputs):
+            if socket.hide is True:
+                self._write(f"{node_var}.outputs[{i}].hide = True")
+
+    def _create_menu_func(self) -> None:
+        """
+        Creates the menu function
+        """
+        self._write("def menu_func(self, context):", "")
+        self._write(f"self.layout.operator({self._class_name}.bl_idname)", "\t")
+        self._write("")
+
+    def _create_register_func(self) -> None:
+        """
+        Creates the register function
+        """
+        self._write("def register():", "")
+        self._write(f"bpy.utils.register_class({self._class_name})", "\t")
+        self._write("bpy.types.VIEW3D_MT_object.append(menu_func)", "\t")
+        self._write("")
+
+    def _create_unregister_func(self) -> None:
+        """
+        Creates the unregister function
+        """
+        self._write("def unregister():", "")
+        self._write(f"bpy.utils.unregister_class({self._class_name})", "\t")
+        self._write("bpy.types.VIEW3D_MT_object.remove(menu_func)", "\t")
+        self._write("")
+
+    def _create_main_func(self) -> None:
+        """
+        Creates the main function
+        """
+        self._write("if __name__ == \"__main__\":", "")
+        self._write("register()", "\t")
+
+    def _zip_addon(self) -> None:
+        """
+        Zips up the addon and removes the directory
+        """
+        shutil.make_archive(self._zip_dir, "zip", self._zip_dir)
+        shutil.rmtree(self._zip_dir)
+
+    # ABSTRACT
+    def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree) -> None:
+        return
+
+    # ABSTRACT
+    def _process_node_tree(self, node_tree: NodeTree) -> None:
+        return
+
+    def _report_finished(self, object: str):
+        """
+        Alert user that NTP is finished
+
+        Parameters:
+        object (str): the copied node tree or encapsulating structure
+            (geometry node modifier, material, scene, etc.)
+        """
+        if self.mode == 'SCRIPT':
+            location = "clipboard"
+        else:
+            location = self._dir
+        self.report({'INFO'}, f"NodeToPython: Saved {object} to {location}")
+
+    # ABSTRACT
+    def execute(self):
+        return {'FINISHED'}
+
+    def invoke(self, context, event):
+        return context.window_manager.invoke_props_dialog(self)
+
+    def draw(self, context):
+        self.layout.prop(self, "mode")
diff --git a/utils.py b/utils.py
index d66632b..08d68fb 100644
--- a/utils.py
+++ b/utils.py
@@ -1,20 +1,60 @@
 import bpy
+from bpy_types import bpy_types
 import mathutils
 
-import os
+from enum import Enum, auto
+import keyword
 import re
-import shutil
-from typing import TextIO, Tuple
-
-image_dir_name = "imgs"
-
-#node input sockets that are messy to set default values for
-dont_set_defaults = {'NodeSocketGeometry',
-                     'NodeSocketShader',
-                     'NodeSocketVirtual'}
+from typing import NamedTuple
+
+class ST(Enum):
+    """
+    Settings Types
+    """
+    # Primitives
+    ENUM = auto()
+    ENUM_SET = auto()
+    STRING = auto()
+    BOOL = auto()
+    INT = auto()
+    FLOAT = auto()
+    VEC1 = auto()
+    VEC2 = auto()
+    VEC3 = auto()
+    VEC4 = auto()
+    COLOR = auto()
+    
+    # Special settings
+    COLOR_RAMP = auto()
+    CURVE_MAPPING = auto()
+    NODE_TREE = auto()
+
+    # Asset Library
+    MATERIAL = auto() # Handle with asset library
+    OBJECT = auto() # Handle with asset library
+
+    # Image
+    IMAGE = auto() #needs refactor
+    IMAGE_USER = auto() #needs refactor
+    MOVIE_CLIP = auto() #unimplmented
+
+    TEXTURE = auto() #unimplemented
+    TEXT = auto() #unimplemented
+    SCENE = auto() #unimplemented
+    PARTICLE_SYSTEM = auto() #unimplemented
+    FONT = auto() #unimplemented
+    MASK = auto() #unimplemented
+    CRYPTOMATTE_ENTRIES = auto() #unimplemented
+    IMAGE_FORMAT_SETTINGS = auto() #unimplemented
+    FILE_SLOTS = auto() #unimplemented
+    LAYER_SLOTS = auto() #unimplemented
+
+class NTPNodeSetting(NamedTuple):
+    name: str
+    st: ST
+    min_version: tuple = (3, 0, 0)
+    max_version: tuple = (4, 1, 0)
 
-#node tree input sockets that have default properties
-default_sockets = {'VALUE', 'INT', 'BOOLEAN', 'VECTOR', 'RGBA'}
 
 def clean_string(string: str, lower: bool = True) -> str:
     """
@@ -24,13 +64,19 @@ def clean_string(string: str, lower: bool = True) -> str:
     string (str): The input string
     
     Returns:
-    clean_str: The input string with nasty characters converted to underscores
+    string (str): The input string ready to be used as a variable/file
     """
 
     if lower:
         string = string.lower()
-    clean_str = re.sub(r"[^a-zA-Z0-9_]", '_', string)
-    return clean_str
+    string = re.sub(r"[^a-zA-Z0-9_]", '_', string)
+
+    if keyword.iskeyword(string):
+        string = "_" + string
+    elif not (string[0].isalpha() or string[0] == '_'):
+        string = "_" + string
+
+    return string
 
 def enum_to_py_str(enum: str) -> str:
     """
@@ -56,740 +102,94 @@ def str_to_py_str(string: str) -> str:
     """
     return f"\"{string}\""
 
-def vec3_to_py_str(vec) -> str:
+def vec1_to_py_str(vec1) -> str:
     """
-    Converts a 3D vector to a string usable by the add-on
+    Converts a 1D vector to a string usable by the add-on
 
     Parameters:
-    vec (mathutils.Vector): a 3d vector
+    vec1: a 1d vector
 
     Returns:
-    (str): string version
+    (str): string representation of the vector
     """
-    return f"({vec[0]}, {vec[1]}, {vec[2]})"
+    return f"[{vec1[0]}]"
 
-def vec4_to_py_str(vec) -> str:
+def vec2_to_py_str(vec2) -> str:
     """
-    Converts a 4D vector to a string usable by the add-on
+    Converts a 2D vector to a string usable by the add-on
 
     Parameters:
-    vec (mathutils.Vector): a 4d vector
-
-    Returns:
-    (str): string version
-    """
-    return f"({vec[0]}, {vec[1]}, {vec[2]}, {vec[3]})"
+    vec2: a 2D vector
 
-def img_to_py_str(img) -> str:
-    """
-    Converts a Blender image into its string
-
-    Paramters:
-    img (bpy.types.Image): a Blender image
-    
     Returns:
-    (str): string version
-    """
-    name = img.name.split('.', 1)[0]
-    format = img.file_format.lower()
-    return f"{name}.{format}"
-
-def create_header(file: TextIO, name: str):
-    """
-    Sets up the bl_info and imports the Blender API
-
-    Parameters:
-    file (TextIO): the file for the generated add-on
-    name (str): name of the add-on
-    """
-
-    file.write("bl_info = {\n")
-    file.write(f"\t\"name\" : \"{name}\",\n")
-    file.write("\t\"author\" : \"Node To Python\",\n")
-    file.write("\t\"version\" : (1, 0, 0),\n")
-    file.write(f"\t\"blender\" : {bpy.app.version},\n")
-    file.write("\t\"location\" : \"Object\",\n")
-    file.write("\t\"category\" : \"Node\"\n")
-    file.write("}\n")
-    file.write("\n")
-    file.write("import bpy\n")
-    file.write("import os\n")
-    file.write("\n")
-
-def init_operator(file: TextIO, name: str, idname: str, label: str):
+    (str): string representation of the vector
     """
-    Initializes the add-on's operator 
+    return f"({vec2[0]}, {vec2[1]})"
 
-    Parameters:
-    file (TextIO): the file for the generated add-on
-    name (str): name for the class
-    idname (str): name for the operator
-    label (str): appearence inside Blender
-    """
-    file.write(f"class {name}(bpy.types.Operator):\n")
-    file.write(f"\tbl_idname = \"object.{idname}\"\n")
-    file.write(f"\tbl_label = \"{label}\"\n")
-    file.write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n")
-    file.write("\n")
-
-def create_var(name: str, used_vars: dict) -> str:
-    """
-    Creates a unique variable name for a node tree
-
-    Parameters:
-    name (str): basic string we'd like to create the variable name out of
-    used_vars (dict): dictionary containing variable names and usage counts
-
-    Returns:
-    clean_name (str): variable name for the node tree
+def vec3_to_py_str(vec3) -> str:
     """
-    if name == "":
-        name = "unnamed"
-    clean_name = clean_string(name)
-    var = clean_name
-    if var in used_vars:
-        used_vars[var] += 1
-        return f"{clean_name}_{used_vars[var]}"
-    else:
-        used_vars[var] = 0
-        return clean_name
-
-def make_indents(level: int) -> Tuple[str, str]:
-    """
-    Returns strings with the correct number of indentations 
-    given the level in the function.
-
-    Node groups need processed recursively, 
-    so there can sometimes be functions in functions.
+    Converts a 3D vector to a string usable by the add-on
 
     Parameters:
-    level (int): base number of indentations need
+    vec3: a 3d vector
 
     Returns:
-    outer (str): a basic level of indentation for a node group.
-    inner (str): a level of indentation beyond outer
+    (str): string representation of the vector
     """
-    outer = "\t"*level
-    inner = "\t"*(level + 1)
-    return outer, inner
+    return f"({vec3[0]}, {vec3[1]}, {vec3[2]})"
 
-def create_node(node, file: TextIO, inner: str, node_tree_var: str, 
-                node_vars: dict, used_vars: set) -> str:
+def vec4_to_py_str(vec4) -> str:
     """
-    Initializes a new node with location, dimension, and label info
+    Converts a 4D vector to a string usable by the add-on
 
     Parameters:
-    node (bpy.types.Node): node to be copied
-    file (TextIO): file containing the generated add-on
-    inner (str): indentation level for this logic
-    node_tree_var (str): variable name for the node tree
-    node_vars (dict): dictionary containing (bpy.types.Node, str)
-        pairs, with a Node and its corresponding variable name
-    used_vars (set): set of used variable names
+    vec4: a 4d vector
 
     Returns:
-    node_var (str): variable name for the node
-    """
-
-    file.write(f"{inner}#node {node.name}\n")
-
-    node_var = create_var(node.name, used_vars)
-    node_vars[node] = node_var
-
-    file.write((f"{inner}{node_var} "
-                f"= {node_tree_var}.nodes.new(\"{node.bl_idname}\")\n"))
-    #label
-    if node.label:
-        file.write(f"{inner}{node_var}.label = \"{node.label}\"\n")
-
-    #color
-    if node.use_custom_color:
-        file.write(f"{inner}{node_var}.use_custom_color = True\n")
-        file.write(f"{inner}{node_var}.color = {vec3_to_py_str(node.color)}\n")
-
-    #mute
-    if node.mute:
-        file.write(f"{inner}{node_var}.mute = True\n")
-        
-    return node_var
-
-def set_settings_defaults(node, settings: dict, file: TextIO, inner: str, 
-                            node_var: str):
-    """
-    Sets the defaults for any settings a node may have
-
-    Parameters:
-    node (bpy.types.Node): the node object we're copying settings from
-    settings (dict): a predefined dictionary of all settings every node has
-    file (TextIO): file we're generating the add-on into
-    inner (str): indentation
-    node_var (str): name of the variable we're using for the node in our add-on
-    """
-    if node.bl_idname in settings:
-        for setting in settings[node.bl_idname]:
-            attr = getattr(node, setting, None)
-            if attr:
-                if type(attr) == str:
-                    attr = enum_to_py_str(attr)
-                if type(attr) == mathutils.Vector:
-                    attr = vec3_to_py_str(attr)
-                if type(attr) == bpy.types.bpy_prop_array:
-                    attr = vec4_to_py_str(list(attr))
-                if type(attr) == bpy.types.Material:
-                    name = str_to_py_str(attr.name)
-                    file.write((f"{inner}if {name} in bpy.data.materials:\n"))
-                    file.write((f"{inner}\t{node_var}.{setting} = "
-                                f"bpy.data.materials[{name}]\n"))
-                    continue
-                if type(attr) == bpy.types.Object:
-                    name = str_to_py_str(attr.name)
-                    file.write((f"{inner}if {name} in bpy.data.objects:\n"))
-                    file.write((f"{inner}\t{node_var}.{setting} = "
-                                f"bpy.data.objects[{name}]\n"))
-                    continue
-                file.write((f"{inner}{node_var}.{setting} "
-                            f"= {attr}\n"))
-
-def hide_sockets(node, file: TextIO, inner: str, node_var: str):
-    """
-    Hide hidden sockets
-
-    Parameters:
-    node (bpy.types.Node): node object we're copying socket settings from
-    file (TextIO): file we're generating the add-on into
-    inner (str): indentation string
-    node_var (str): name of the variable we're using for this node
-    """
-    for i, socket in enumerate(node.inputs):
-        if socket.hide is True:
-            file.write(f"{inner}{node_var}.inputs[{i}].hide = True\n")
-    for i, socket in enumerate(node.outputs):
-        if socket.hide is True:
-            file.write(f"{inner}{node_var}.outputs[{i}].hide = True\n") 
-
-def group_io_settings(node, file: TextIO, inner: str, io: str, node_tree_var: str, node_tree):
-    if io == "input":
-        ios = node.outputs
-        ntio = node_tree.inputs
-    else:
-        ios = node.inputs
-        ntio = node_tree.outputs
-    file.write(f"{inner}#{node_tree_var} {io}s\n")
-    for i, inout in enumerate(ios):
-        if inout.bl_idname == 'NodeSocketVirtual':
-            continue
-        file.write(f"{inner}#{io} {inout.name}\n")
-        idname = enum_to_py_str(inout.bl_idname)
-        name = str_to_py_str(inout.name)
-        file.write(f"{inner}{node_tree_var}.{io}s.new({idname}, {name})\n")
-        socket = ntio[i]
-        socket_var = f"{node_tree_var}.{io}s[{i}]"
-
-        if inout.type in default_sockets:
-            #default value
-            if inout.type == 'RGBA':
-                dv = vec4_to_py_str(socket.default_value)
-            elif inout.type == 'VECTOR':
-                dv = vec3_to_py_str(socket.default_value)
-            else:
-                dv = socket.default_value
-            file.write(f"{inner}{socket_var}.default_value = {dv}\n")
-
-            #min value
-            if hasattr(socket, "min_value"):
-                file.write(f"{inner}{socket_var}.min_value = {socket.min_value}\n")
-            #max value
-            if hasattr(socket, "min_value"):
-                file.write((f"{inner}{socket_var}.max_value = {socket.max_value}\n"))
-        
-        #default attribute name
-        if hasattr(socket, "default_attribute_name"):
-            if socket.default_attribute_name != "":
-                dan = str_to_py_str(socket.default_attribute_name)
-                file.write((f"{inner}{socket_var}"
-                            f".default_attribute_name = {dan}\n"))
-
-        #attribute domain
-        if hasattr(socket, "attribute_domain"):
-            ad = enum_to_py_str(socket.attribute_domain)
-            file.write(f"{inner}{socket_var}.attribute_domain = {ad}\n")
-
-        #tooltip
-        if socket.description != "":
-            description = str_to_py_str(socket.description)
-            file.write((f"{inner}{socket_var}.description = {description}\n"))
-
-        #hide_value
-        if socket.hide_value is True:
-            file.write(f"{inner}{socket_var}.hide_value = True\n")
-
-        #hide in modifier
-        if hasattr(socket, "hide_in_modifier"):
-            if socket.hide_in_modifier is True:
-                file.write(f"{inner}{socket_var}.hide_in_modifier = True\n")
-
-        file.write("\n")
-    file.write("\n")
-
-def color_ramp_settings(node, file: TextIO, inner: str, node_var: str):
-    """
-    Replicate a color ramp node
-
-    Parameters
-    node (bpy.types.Node): node object we're copying settings from
-    file (TextIO): file we're generating the add-on into
-    inner (str): indentation
-    node_var (str): name of the variable we're using for the color ramp
-    """
-
-    color_ramp = node.color_ramp
-    #settings
-    color_mode = enum_to_py_str(color_ramp.color_mode)
-    file.write(f"{inner}{node_var}.color_ramp.color_mode = {color_mode}\n")
-
-    hue_interpolation = enum_to_py_str(color_ramp.hue_interpolation)
-    file.write((f"{inner}{node_var}.color_ramp.hue_interpolation = "
-                f"{hue_interpolation}\n"))
-    interpolation = enum_to_py_str(color_ramp.interpolation)
-    file.write((f"{inner}{node_var}.color_ramp.interpolation "
-                f"= {interpolation}\n"))
-    file.write("\n")
-
-    #key points
-    file.write((f"{inner}{node_var}.color_ramp.elements.remove"
-                f"({node_var}.color_ramp.elements[0])\n"))
-    for i, element in enumerate(color_ramp.elements):
-        element_var = f"{node_var}_cre_{i}"
-        if i == 0:
-            file.write(f"{inner}{element_var} = "
-                       f"{node_var}.color_ramp.elements[{i}]\n")
-            file.write(f"{inner}{element_var}.position = {element.position}\n")
-        else:
-            file.write((f"{inner}{element_var} = "
-                        f"{node_var}.color_ramp.elements"
-                        f".new({element.position})\n"))
-        file.write((f"{inner}{element_var}.alpha = "
-                    f"{element.alpha}\n"))
-        color_str = vec4_to_py_str(element.color)
-        file.write((f"{inner}{element_var}.color = {color_str}\n\n"))
-
-def curve_node_settings(node, file: TextIO, inner: str, node_var: str):
-    """
-    Sets defaults for Float, Vector, and Color curves
-
-    Parameters:
-    node (bpy.types.Node): curve node we're copying settings from
-    file (TextIO): file we're generating the add-on into
-    inner (str): indentation
-    node_var (str): variable name for the add-on's curve node
-    """
-
-    #mapping settings
-    file.write(f"{inner}#mapping settings\n")
-    mapping_var = f"{inner}{node_var}.mapping"
-
-    #extend
-    extend = enum_to_py_str(node.mapping.extend)
-    file.write(f"{mapping_var}.extend = {extend}\n")
-    #tone
-    tone = enum_to_py_str(node.mapping.tone)
-    file.write(f"{mapping_var}.tone = {tone}\n")
-
-    #black level
-    b_lvl_str = vec3_to_py_str(node.mapping.black_level)
-    file.write((f"{mapping_var}.black_level = {b_lvl_str}\n"))
-    #white level
-    w_lvl_str = vec3_to_py_str(node.mapping.white_level)
-    file.write((f"{mapping_var}.white_level = {w_lvl_str}\n"))
-
-    #minima and maxima
-    min_x = node.mapping.clip_min_x
-    file.write(f"{mapping_var}.clip_min_x = {min_x}\n")
-    min_y = node.mapping.clip_min_y
-    file.write(f"{mapping_var}.clip_min_y = {min_y}\n")
-    max_x = node.mapping.clip_max_x
-    file.write(f"{mapping_var}.clip_max_x = {max_x}\n")
-    max_y = node.mapping.clip_max_y
-    file.write(f"{mapping_var}.clip_max_y = {max_y}\n")
-
-    #use_clip
-    use_clip = node.mapping.use_clip
-    file.write(f"{mapping_var}.use_clip = {use_clip}\n")
-
-    #create curves
-    for i, curve in enumerate(node.mapping.curves):
-        file.write(f"{inner}#curve {i}\n")
-        curve_i = f"{node_var}_curve_{i}"
-        file.write((f"{inner}{curve_i} = {node_var}.mapping.curves[{i}]\n"))
-        for j, point in enumerate(curve.points):
-            point_j = f"{inner}{curve_i}_point_{j}"
-
-            loc = point.location
-            loc_str = f"{loc[0]}, {loc[1]}"
-            if j < 2:
-                file.write(f"{point_j} = {curve_i}.points[{j}]\n")
-                file.write(f"{point_j}.location = ({loc_str})\n")
-            else:
-                file.write((f"{point_j} = {curve_i}.points.new({loc_str})\n"))
-
-            handle = enum_to_py_str(point.handle_type)
-            file.write(f"{point_j}.handle_type = {handle}\n")
-    
-    #update curve
-    file.write(f"{inner}#update curve after changes\n")
-    file.write(f"{mapping_var}.update()\n")
-
-def set_input_defaults(node, file: TextIO, inner: str, node_var: str, 
-                       addon_dir: str = ""):
-    """
-    Sets defaults for input sockets
-
-    Parameters:
-    node (bpy.types.Node): node we're setting inputs for
-    file (TextIO): file we're generating the add-on into
-    inner (str): indentation
-    node_var (str): variable name we're using for the copied node
-    addon_dir (str): directory of the add-on, for if we need to save other
-        objects for the add-on
-    """
-    if node.bl_idname == 'NodeReroute':
-        return
-
-    for i, input in enumerate(node.inputs):
-        if input.bl_idname not in dont_set_defaults and not input.is_linked:
-            socket_var = f"{node_var}.inputs[{i}]"
-
-            #colors
-            if input.bl_idname == 'NodeSocketColor':
-                default_val = vec4_to_py_str(input.default_value)
-
-            #vector types
-            elif "Vector" in input.bl_idname:
-                default_val = vec3_to_py_str(input.default_value)
-
-            #strings
-            elif input.bl_idname == 'NodeSocketString':
-                default_val = str_to_py_str(input.default_value)
-
-            #images
-            elif input.bl_idname == 'NodeSocketImage':
-                img = input.default_value
-                if img is not None and addon_dir != "": #write in a better way
-                    save_image(img, addon_dir)
-                    load_image(img, file, inner, f"{socket_var}.default_value")
-                default_val = None
-
-            #materials 
-            elif input.bl_idname == 'NodeSocketMaterial':
-                in_file_inputs(input, file, inner, socket_var, "materials")
-                default_val = None
-
-            #collections
-            elif input.bl_idname == 'NodeSocketCollection':
-                in_file_inputs(input, file, inner, socket_var, "collections")
-                default_val = None
-
-            #objects
-            elif input.bl_idname == 'NodeSocketObject':
-                in_file_inputs(input, file, inner, socket_var, "objects")
-                default_val = None
-            
-            #textures
-            elif input.bl_idname == 'NodeSocketTexture':
-                in_file_inputs(input, file, inner, socket_var, "textures")
-                default_val = None
-
-            else:
-                default_val = input.default_value
-            if default_val is not None:
-                file.write(f"{inner}#{input.identifier}\n")
-                file.write((f"{inner}{socket_var}.default_value"
-                            f" = {default_val}\n"))
-    file.write("\n")
-
-def in_file_inputs(input, file: TextIO, inner: str, socket_var: str, type: str):
-    """
-    Sets inputs for a node input if one already exists in the blend file
-
-    Parameters:
-    input (bpy.types.NodeSocket): input socket we're setting the value for
-    file (TextIO): file we're writing the add-on into
-    inner (str): indentation string
-    socket_var (str): variable name we're using for the socket
-    type (str): from what section of bpy.data to pull the default value from
-    """
-
-    if input.default_value is not None:
-        name = str_to_py_str(input.default_value.name)
-        file.write(f"{inner}if {name} in bpy.data.{type}:\n")
-        file.write((f"{inner}\t{socket_var}.default_value = "
-                                f"bpy.data.{type}[{name}]\n"))
-
-def set_output_defaults(node, file: TextIO, inner: str, node_var: str):
-    """
-    Some output sockets need default values set. It's rather annoying
-
-    Parameters:
-    node (bpy.types.Node): node for the output we're setting
-    file (TextIO): file we're generating the add-on into
-    inner (str): indentation string
-    node_var (str): variable name for the node we're setting output defaults for
-    """
-    output_default_nodes = {'ShaderNodeValue', 
-                            'ShaderNodeRGB', 
-                            'ShaderNodeNormal'}
-
-    if node.bl_idname in output_default_nodes:
-        dv = node.outputs[0].default_value
-        if node.bl_idname == 'ShaderNodeRGB':
-            dv = vec4_to_py_str(list(dv))
-        if node.bl_idname == 'ShaderNodeNormal':
-            dv = vec3_to_py_str(dv)
-        file.write((f"{inner}{node_var}.outputs[0].default_value = {dv}\n"))
-
-def set_parents(node_tree, file: TextIO, inner: str, node_vars: dict):
-    """
-    Sets parents for all nodes, mostly used to put nodes in frames
-
-    Parameters:
-    node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from
-    file (TextIO): file for the generated add-on
-    inner (str): indentation string
-    node_vars (dict): dictionary for (node, variable) name pairs
-    """
-    parent_comment = False
-    for node in node_tree.nodes:
-        if node is not None and node.parent is not None:
-            if not parent_comment:
-                file.write(f"{inner}#Set parents\n")
-                parent_comment = True
-            node_var = node_vars[node]
-            parent_var = node_vars[node.parent]
-            file.write(f"{inner}{node_var}.parent = {parent_var}\n")
-    file.write("\n")
-
-def set_locations(node_tree, file: TextIO, inner: str, node_vars: dict):
-    """
-    Set locations for all nodes
-
-    Parameters:
-    node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from
-    file (TextIO): file for the generated add-on
-    inner (str): indentation string
-    node_vars (dict): dictionary for (node, variable) name pairs
-    """
-
-    file.write(f"{inner}#Set locations\n")
-    for node in node_tree.nodes:
-        node_var = node_vars[node]
-        file.write((f"{inner}{node_var}.location "
-                    f"= ({node.location.x}, {node.location.y})\n"))
-    file.write("\n")
-
-def set_dimensions(node_tree, file: TextIO, inner: str, node_vars: dict):
-    """
-    Set dimensions for all nodes
-
-    Parameters:
-    node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from
-    file (TextIO): file for the generated add-on
-    inner (str): indentation string
-    node_vars (dict): dictionary for (node, variable) name pairs
-    """
-
-    file.write(f"{inner}#Set dimensions\n")
-    for node in node_tree.nodes:
-        node_var = node_vars[node]
-        file.write((f"{inner}{node_var}.width, {node_var}.height "
-                        f"= {node.width}, {node.height}\n"))
-    file.write("\n")
-
-def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str, 
-                node_vars: dict):
-    """
-    Create all the links between nodes
-
-    Parameters:
-    node_tree (bpy.types.NodeTree): node tree we're copying
-    file (TextIO): file we're generating the add-on into
-    inner (str): indentation
-    node_tree_var (str): variable name we're using for the copied node tree
-    node_vars (dict): dictionary containing node to variable name pairs
-    """
-
-    if node_tree.links:
-        file.write(f"{inner}#initialize {node_tree_var} links\n")     
-    for link in node_tree.links:
-        in_node_var = node_vars[link.from_node]
-        input_socket = link.from_socket
-        
-        """
-        Blender's socket dictionary doesn't guarantee 
-        unique keys, which has caused much wailing and
-        gnashing of teeth. This is a quick fix that
-        doesn't run quick
-        """
-        for i, item in enumerate(link.from_node.outputs.items()):
-            if item[1] == input_socket:
-                input_idx = i
-                break
-        
-        out_node_var = node_vars[link.to_node]
-        output_socket = link.to_socket
-        
-        for i, item in enumerate(link.to_node.inputs.items()):
-            if item[1] == output_socket:
-                output_idx = i
-                break
-        
-        file.write((f"{inner}#{in_node_var}.{input_socket.name} "
-                    f"-> {out_node_var}.{output_socket.name}\n"))
-        file.write((f"{inner}{node_tree_var}.links.new({in_node_var}"
-                    f".outputs[{input_idx}], "
-                    f"{out_node_var}.inputs[{output_idx}])\n"))
-
-def create_menu_func(file: TextIO, name: str):
-    """
-    Creates the menu function
-
-    Parameters:
-    file (TextIO): file we're generating the add-on into
-    name (str): name of the generated operator class
-    """
-
-    file.write("def menu_func(self, context):\n")
-    file.write(f"\tself.layout.operator({name}.bl_idname)\n")
-    file.write("\n")
-
-def create_register_func(file: TextIO, name: str):
-    """
-    Creates the register function
-
-    Parameters:
-    file (TextIO): file we're generating the add-on into
-    name (str): name of the generated operator class
-    """
-    file.write("def register():\n")
-    file.write(f"\tbpy.utils.register_class({name})\n")
-    file.write("\tbpy.types.VIEW3D_MT_object.append(menu_func)\n")
-    file.write("\n")
-
-def create_unregister_func(file: TextIO, name: str):
-    """
-    Creates the unregister function
-
-    Parameters:
-    file (TextIO): file we're generating the add-on into
-    name (str): name of the generated operator class
+    (str): string version
     """
-    file.write("def unregister():\n")
-    file.write(f"\tbpy.utils.unregister_class({name})\n")
-    file.write("\tbpy.types.VIEW3D_MT_object.remove(menu_func)\n")
-    file.write("\n")
+    return f"({vec4[0]}, {vec4[1]}, {vec4[2]}, {vec4[3]})"
 
-def create_main_func(file: TextIO):
+def array_to_py_str(array: bpy_types.bpy_prop_array) -> str:
     """
-    Creates the main function
+    Converts a bpy_prop_array into a string
 
     Parameters:
-    file (TextIO): file we're generating the add-on into
-    """
-    file.write("if __name__ == \"__main__\":\n")
-    file.write("\tregister()")
+    array (bpy_prop_array): Blender Python array
 
-def save_image(img, addon_dir: str):
+    Returns:
+    (str): string version
     """
-    Saves an image to an image directory of the add-on
+    string = "("
+    for i in range(0, array.__len__()):
+        if i > 0:
+            string += ", "
+        string += f"{array[i]}"
+    string += ")"
+    return string
 
-    Parameters:
-    img (bpy.types.Image): image to be saved
-    addon_dir (str): directory of the addon
+def color_to_py_str(color: mathutils.Color) -> str:
     """
-
-    if img is None:
-        return
-
-    #create image dir if one doesn't exist
-    img_dir = os.path.join(addon_dir, image_dir_name)
-    if not os.path.exists(img_dir):
-        os.mkdir(img_dir)
-
-    #save the image
-    img_str = img_to_py_str(img)
-    img_path = f"{img_dir}/{img_str}"
-    if not os.path.exists(img_path):
-        img.save_render(img_path)
-
-def load_image(img, file: TextIO, inner: str, img_var: str):
-    """
-    Loads an image from the add-on into a blend file and assigns it
+    Converts a mathutils.Color into a string
 
     Parameters:
-    img (bpy.types.Image): Blender image from the original node group
-    file (TextIO): file for the generated add-on
-    inner (str): indentation string
-    img_var (str): variable name to be used for the image
-    """
+    color (mathutils.Color): a Blender color
 
-    if img is None:
-        return
-        
-    img_str = img_to_py_str(img)
-
-    file.write(f"{inner}#load image {img_str}\n")
-    file.write((f"{inner}base_dir = "
-                f"os.path.dirname(os.path.abspath(__file__))\n"))
-    file.write((f"{inner}image_path = "
-                f"os.path.join(base_dir, \"{image_dir_name}\", "
-                f"\"{img_str}\")\n"))
-    file.write((f"{inner}{img_var} = "
-                f"bpy.data.images.load(image_path, check_existing = True)\n"))
-
-    #copy image settings
-    file.write(f"{inner}#set image settings\n")
-
-    #source
-    source = enum_to_py_str(img.source)
-    file.write(f"{inner}{img_var}.source = {source}\n")
-
-    #color space settings
-    color_space = enum_to_py_str(img.colorspace_settings.name)
-    file.write(f"{inner}{img_var}.colorspace_settings.name = {color_space}\n")
-    
-    #alpha mode
-    alpha_mode = enum_to_py_str(img.alpha_mode)
-    file.write(f"{inner}{img_var}.alpha_mode = {alpha_mode}\n")
-
-def image_user_settings(node, file: TextIO, inner: str, node_var: str):
+    Returns:
+    (str): string version
     """
-    Replicate the image user of an image node
+    return f"mathutils.Color(({color.r}, {color.g}, {color.b}))"
 
-    Parameters
-    node (bpy.types.Node): node object we're copying settings from
-    file (TextIO): file we're generating the add-on into
-    inner (str): indentation
-    node_var (str): name of the variable we're using for the color ramp
+def img_to_py_str(img : bpy.types.Image) -> str:
     """
+    Converts a Blender image into its string
 
-    if not hasattr(node, "image_user"):
-        raise ValueError("Node must have attribute \"image_user\"")
-
-    img_usr = node.image_user
-    img_usr_var = f"{node_var}.image_user"
-
-    img_usr_attrs = ["frame_current", "frame_duration", "frame_offset",
-                     "frame_start", "tile", "use_auto_refresh", "use_cyclic"]
-    
-    for img_usr_attr in img_usr_attrs:
-        file.write((f"{inner}{img_usr_var}.{img_usr_attr} = "
-                    f"{getattr(img_usr, img_usr_attr)}\n"))
+    Paramters:
+    img (bpy.types.Image): a Blender image
     
-def zip_addon(zip_dir: str):
-    """
-    Zips up the addon and removes the directory
-
-    Parameters:
-    zip_dir (str): path to the top-level addon directory
+    Returns:
+    (str): string version
     """
-    shutil.make_archive(zip_dir, "zip", zip_dir)
-    shutil.rmtree(zip_dir)
\ No newline at end of file
+    name = img.name.split('.', 1)[0]
+    format = img.file_format.lower()
+    return f"{name}.{format}"
\ No newline at end of file