Thursday, November 12, 2026

Building an Intelligent Constructive Solid Geometry System with LLM Integration


 


When I studied  Computer Science aeons ago, I was mainly interested in Compiler Construction, AI and Computer Graphics. The latter one nicely combined math with software engineering. We dealt with Constructive Solid Geometry (CSG) in the classroom using Digitalk Smalltalk. Nowadays, LLMs could be a nice companion of CSG, right?


Introduction

Constructive Solid Geometry (CSG) represents a powerful paradigm for modeling complex geometric shapes through boolean operations on primitive solids. When combined with Large Language Models, we can create an intuitive system where users describe scenes in natural language, and the system automatically generates corresponding CSG trees with visual representations. This tutorial explores the complete architecture, implementation details, and user experience considerations for building such a system.


The core challenge lies in bridging the gap between human language and geometric representation. A user might say "create a house with a triangular roof and two windows," and our system must parse this description, construct an appropriate CSG tree with primitives like boxes and cylinders, apply boolean operations such as union and subtraction, and render the result in either 2D or 3D. Furthermore, the system must allow intuitive tree editing, persistent storage through JSON serialization, and flexible LLM backend support across different hardware platforms.


Architectural Overview

The system architecture consists of several interconnected components that work together to provide a seamless experience. At the highest level, we have the user interface layer that handles input and display. Below that sits the LLM integration layer, which communicates with either local or remote language models. The CSG engine forms the computational core, managing tree structures and geometric operations. Finally, the rendering pipeline transforms CSG trees into visual representations.


The LLM integration layer deserves special attention because it must support multiple backends. For local execution, we need to handle different GPU architectures including NVIDIA CUDA, AMD ROCm, Intel GPUs, and Apple Metal Performance Shaders. For remote execution, we interface with API-based services. This flexibility ensures users can choose their preferred deployment model based on available hardware and privacy requirements.


CSG Tree Fundamentals

Before diving into implementation, understanding CSG tree structure proves essential. A CSG tree is a binary tree where leaf nodes represent primitive shapes and internal nodes represent boolean operations. The three fundamental operations are union, intersection, and difference. Union combines two shapes, intersection produces only the overlapping region, and difference subtracts one shape from another.


Consider a simple example of creating a window. We start with a rectangular wall primitive. We then create a smaller rectangle representing the window opening. By applying a difference operation with the wall as the left operand and the window as the right operand, we obtain a wall with a hole. This tree structure has a difference node at the root, with the wall rectangle as the left child and the window rectangle as the right child.


The tree representation provides several advantages. It maintains the construction history, allowing users to modify individual primitives or operations later. It enables efficient rendering through spatial subdivision techniques. It also provides a natural serialization format that captures both geometry and construction logic.


Data Structures and Core Classes

Let us examine the fundamental data structures that form the foundation of our system. We begin with the primitive shape classes, then move to the CSG node representation, and finally explore the tree management structure.


A primitive shape needs to store its type, dimensions, position, and orientation. For 2D shapes, we support rectangles, circles, and polygons. For 3D shapes, we include boxes, spheres, cylinders, and cones. Each primitive type requires specific parameters.


import numpy as np

from enum import Enum

from typing import List, Tuple, Optional, Dict, Any

import json

import hashlib

import time


class ShapeType(Enum):

    # 2D primitives

    RECTANGLE = "rectangle"

    CIRCLE = "circle"

    POLYGON = "polygon"

    

    # 3D primitives

    BOX = "box"

    SPHERE = "sphere"

    CYLINDER = "cylinder"

    CONE = "cone"


class Primitive:

    def __init__(self, shape_type: ShapeType, position: np.ndarray,

                 dimensions: Dict[str, float], rotation: np.ndarray = None,

                 name: str = None):

        self.shape_type = shape_type

        self.position = np.array(position, dtype=np.float64)

        

        # Validate and copy dimensions

        self.dimensions = {}

        for key, value in dimensions.items():

            if not isinstance(value, (int, float, list)):

                raise ValueError(f"Dimension {key} must be numeric or list, got {type(value)}")

            if isinstance(value, (int, float)) and value <= 0:

                raise ValueError(f"Dimension {key} must be positive, got {value}")

            self.dimensions[key] = value

        

        # Validate polygon vertices if present

        if "vertices" in self.dimensions:

            vertices = np.array(self.dimensions["vertices"])

            if len(vertices) < 3:

                raise ValueError("Polygon must have at least 3 vertices")

            self.dimensions["vertices"] = vertices.tolist()

        

        # Ensure rotation is always a 3D vector with valid values

        if rotation is not None:

            self.rotation = np.array(rotation, dtype=np.float64)

            if len(self.rotation) < 3:

                self.rotation = np.pad(self.rotation, (0, 3 - len(self.rotation)))

            # Normalize rotation angles to [-2Ï€, 2Ï€]

            self.rotation = np.fmod(self.rotation, 2 * np.pi)

        else:

            self.rotation = np.zeros(3, dtype=np.float64)

        

        # Generate unique name based on content hash if not provided

        if name:

            self.name = name

        else:

            hash_input = f"{shape_type.value}_{time.time_ns()}_{id(self)}"

            hash_digest = hashlib.md5(hash_input.encode()).hexdigest()[:8]

            self.name = f"{shape_type.value}_{hash_digest}"

        

    def to_dict(self) -> Dict[str, Any]:

        return {

            "type": "primitive",

            "shape_type": self.shape_type.value,

            "position": self.position.tolist(),

            "dimensions": self.dimensions,

            "rotation": self.rotation.tolist(),

            "name": self.name

        }

    

    @staticmethod

    def from_dict(data: Dict[str, Any]) -> 'Primitive':

        # Validate data types in dimensions

        dimensions = {}

        for key, value in data["dimensions"].items():

            if isinstance(value, list):

                dimensions[key] = value

            elif isinstance(value, (int, float)):

                dimensions[key] = float(value)

            else:

                raise ValueError(f"Invalid dimension type for {key}: {type(value)}")

        

        return Primitive(

            shape_type=ShapeType(data["shape_type"]),

            position=np.array(data["position"], dtype=np.float64),

            dimensions=dimensions,

            rotation=np.array(data["rotation"], dtype=np.float64),

            name=data["name"]

        )


The Primitive class encapsulates all information needed to define a basic geometric shape. The position vector specifies the center point in 2D or 3D space. The dimensions dictionary stores shape-specific parameters such as width and height for rectangles, radius for circles and spheres, or radius and height for cylinders. The rotation vector uses Euler angles to specify orientation in 3D space. We ensure that all numeric arrays use float64 for precision and that rotation is always a 3D vector even for 2D shapes to maintain consistency. We validate all dimension values to ensure they are positive numbers, and we normalize rotation angles to prevent overflow issues. The name generation uses a hash-based approach to ensure uniqueness across sessions.


The serialization methods to_dict and from_dict enable JSON persistence. We convert NumPy arrays to lists for JSON compatibility and reconstruct them during deserialization with proper type validation. The name field provides human-readable identification, which proves valuable during tree editing operations.


Next, we define the CSG operation types and the node structure that combines primitives and operations into a tree.


class OperationType(Enum):

    UNION = "union"

    INTERSECTION = "intersection"

    DIFFERENCE = "difference"


class CSGNode:

    def __init__(self, node_type: str, primitive: Primitive = None,

                 operation: OperationType = None, left: 'CSGNode' = None,

                 right: 'CSGNode' = None, name: str = None):

        if node_type not in ["primitive", "operation"]:

            raise ValueError(f"Invalid node_type: {node_type}")

        

        self.node_type = node_type

        self.primitive = primitive

        self.operation = operation

        self.left = left

        self.right = right

        

        # Generate unique name if not provided

        if name:

            self.name = name

        else:

            hash_input = f"{node_type}_{time.time_ns()}_{id(self)}"

            hash_digest = hashlib.md5(hash_input.encode()).hexdigest()[:8]

            self.name = f"node_{hash_digest}"

        

        # Validation

        if self.node_type == "primitive" and self.primitive is None:

            raise ValueError("Primitive node must have a primitive")

        if self.node_type == "operation":

            if self.operation is None:

                raise ValueError("Operation node must have an operation")

            if self.left is None or self.right is None:

                raise ValueError("Operation node must have both children")

        

    def is_leaf(self) -> bool:

        return self.node_type == "primitive"

    

    def to_dict(self) -> Dict[str, Any]:

        if self.is_leaf():

            return {

                "node_type": "primitive",

                "primitive": self.primitive.to_dict(),

                "name": self.name

            }

        else:

            return {

                "node_type": "operation",

                "operation": self.operation.value,

                "left": self.left.to_dict(),

                "right": self.right.to_dict(),

                "name": self.name

            }

    

    @staticmethod

    def from_dict(data: Dict[str, Any]) -> 'CSGNode':

        if data["node_type"] == "primitive":

            return CSGNode(

                node_type="primitive",

                primitive=Primitive.from_dict(data["primitive"]),

                name=data["name"]

            )

        else:

            return CSGNode(

                node_type="operation",

                operation=OperationType(data["operation"]),

                left=CSGNode.from_dict(data["left"]),

                right=CSGNode.from_dict(data["right"]),

                name=data["name"]

            )


The CSGNode class implements a classic binary tree structure with polymorphic behavior. Leaf nodes contain primitives, while internal nodes contain operations and references to child nodes. The is_leaf method provides a convenient way to distinguish between these two cases without explicit type checking. We added validation in the constructor to ensure nodes are created with valid configurations, preventing invalid tree states. The name generation uses a hash-based approach combining timestamp and object ID to ensure uniqueness even across different sessions.


The recursive serialization in to_dict traverses the entire tree, converting each node to a dictionary representation. This recursive structure naturally mirrors the tree hierarchy in the JSON output. The from_dict method performs the inverse operation, reconstructing the tree from serialized data.


Now we create a tree manager class that provides high-level operations for tree manipulation, validation, and persistence.


class CSGTree:

    def __init__(self, root: CSGNode = None, dimension: int = 3):

        if dimension not in [2, 3]:

            raise ValueError(f"Dimension must be 2 or 3, got {dimension}")

        

        self.root = root

        self.dimension = dimension

        self.metadata = {

            "created": datetime.datetime.now().isoformat(),

            "modified": datetime.datetime.now().isoformat(),

            "description": ""

        }

        

        # Calculate bounding box if tree exists

        self.bounds = None

        if self.root:

            self._calculate_bounds()

    

    def _calculate_bounds(self):

        """Calculate axis-aligned bounding box of the tree"""

        min_coords = np.full(self.dimension, float('inf'))

        max_coords = np.full(self.dimension, float('-inf'))

        

        def update_bounds(node: CSGNode):

            if node.is_leaf():

                pos = node.primitive.position[:self.dimension]

                

                # Estimate bounds based on primitive type

                if node.primitive.shape_type == ShapeType.RECTANGLE:

                    w = node.primitive.dimensions.get("width", 1)

                    h = node.primitive.dimensions.get("height", 1)

                    extent = np.array([w/2, h/2])

                elif node.primitive.shape_type == ShapeType.CIRCLE:

                    r = node.primitive.dimensions.get("radius", 1)

                    extent = np.array([r, r])

                elif node.primitive.shape_type == ShapeType.BOX:

                    w = node.primitive.dimensions.get("width", 1)

                    h = node.primitive.dimensions.get("height", 1)

                    d = node.primitive.dimensions.get("depth", 1)

                    extent = np.array([w/2, h/2, d/2])

                elif node.primitive.shape_type == ShapeType.SPHERE:

                    r = node.primitive.dimensions.get("radius", 1)

                    extent = np.array([r, r, r])

                elif node.primitive.shape_type == ShapeType.CYLINDER:

                    r = node.primitive.dimensions.get("radius", 1)

                    h = node.primitive.dimensions.get("height", 1)

                    extent = np.array([r, h/2, r])

                elif node.primitive.shape_type == ShapeType.CONE:

                    r = node.primitive.dimensions.get("radius", 1)

                    h = node.primitive.dimensions.get("height", 1)

                    extent = np.array([r, h/2, r])

                else:

                    extent = np.ones(self.dimension)

                

                nonlocal min_coords, max_coords

                min_coords = np.minimum(min_coords, pos - extent[:self.dimension])

                max_coords = np.maximum(max_coords, pos + extent[:self.dimension])

            else:

                update_bounds(node.left)

                update_bounds(node.right)

        

        try:

            update_bounds(self.root)

            self.bounds = (min_coords, max_coords)

        except Exception:

            self.bounds = None

    

    def validate(self) -> Tuple[bool, str]:

        if self.root is None:

            return False, "Tree is empty"

        

        def validate_node(node: CSGNode, depth: int = 0) -> Tuple[bool, str]:

            if depth > 1000:

                return False, "Tree depth exceeds maximum (possible cycle)"

            

            if node.is_leaf():

                # Check if primitive dimension matches tree dimension

                if self.dimension == 2:

                    valid_types = {ShapeType.RECTANGLE, ShapeType.CIRCLE, 

                                   ShapeType.POLYGON}

                else:

                    valid_types = {ShapeType.BOX, ShapeType.SPHERE, 

                                   ShapeType.CYLINDER, ShapeType.CONE}

                

                if node.primitive.shape_type not in valid_types:

                    return False, f"Invalid shape type {node.primitive.shape_type.value} for {self.dimension}D tree"

                

                # Validate position dimensions

                expected_dims = self.dimension

                actual_dims = len(node.primitive.position)

                if actual_dims < expected_dims:

                    return False, f"Position has {actual_dims} dimensions, expected at least {expected_dims}"

                

                # Validate dimensions dictionary

                if not node.primitive.dimensions:

                    return False, f"Primitive {node.name} has empty dimensions"

                

                # Validate required dimensions for each shape type

                if node.primitive.shape_type == ShapeType.RECTANGLE:

                    if "width" not in node.primitive.dimensions or "height" not in node.primitive.dimensions:

                        return False, f"Rectangle {node.name} missing width or height"

                elif node.primitive.shape_type == ShapeType.CIRCLE:

                    if "radius" not in node.primitive.dimensions:

                        return False, f"Circle {node.name} missing radius"

                elif node.primitive.shape_type == ShapeType.BOX:

                    if not all(k in node.primitive.dimensions for k in ["width", "height", "depth"]):

                        return False, f"Box {node.name} missing dimensions"

                elif node.primitive.shape_type == ShapeType.SPHERE:

                    if "radius" not in node.primitive.dimensions:

                        return False, f"Sphere {node.name} missing radius"

                elif node.primitive.shape_type == ShapeType.CYLINDER:

                    if "radius" not in node.primitive.dimensions or "height" not in node.primitive.dimensions:

                        return False, f"Cylinder {node.name} missing dimensions"

                elif node.primitive.shape_type == ShapeType.CONE:

                    if "radius" not in node.primitive.dimensions or "height" not in node.primitive.dimensions:

                        return False, f"Cone {node.name} missing dimensions"

                

                return True, "Valid"

            else:

                if node.left is None or node.right is None:

                    return False, f"Operation node {node.name} missing children"

                

                left_valid, left_msg = validate_node(node.left, depth + 1)

                if not left_valid:

                    return False, left_msg

                

                right_valid, right_msg = validate_node(node.right, depth + 1)

                if not right_valid:

                    return False, right_msg

                

                return True, "Valid"

        

        return validate_node(self.root)

    

    def save(self, filepath: str) -> bool:

        try:

            self.metadata["modified"] = datetime.datetime.now().isoformat()

            if self.metadata["created"] is None:

                self.metadata["created"] = self.metadata["modified"]

            

            data = {

                "version": "1.0",

                "dimension": self.dimension,

                "metadata": self.metadata,

                "root": self.root.to_dict() if self.root else None

            }

            

            with open(filepath, 'w', encoding='utf-8') as f:

                json.dump(data, f, indent=2, ensure_ascii=False)

            return True

        except Exception as e:

            print(f"Error saving tree: {e}")

            return False

    

    @staticmethod

    def load(filepath: str) -> Optional['CSGTree']:

        try:

            with open(filepath, 'r', encoding='utf-8') as f:

                data = json.load(f)

            

            if "dimension" not in data:

                print("Invalid file format: missing dimension")

                return None

            

            # Check version compatibility

            version = data.get("version", "1.0")

            if version != "1.0":

                print(f"Warning: File version {version} may not be fully compatible")

            

            tree = CSGTree(dimension=data["dimension"])

            tree.metadata = data.get("metadata", tree.metadata)

            if data.get("root"):

                tree.root = CSGNode.from_dict(data["root"])

                tree._calculate_bounds()

            

            return tree

        except FileNotFoundError:

            print(f"File not found: {filepath}")

            return None

        except json.JSONDecodeError as e:

            print(f"Invalid JSON in file: {e}")

            return None

        except Exception as e:

            print(f"Error loading tree: {e}")

            return None



A CSG tree creates complex graphics from simple elements using union, intersection and difference operations. Image (c): Wikipedia.


The CSGTree class serves as the main interface for tree operations. The validate method performs recursive tree traversal to ensure structural integrity and dimensional consistency. It checks that all primitives match the tree's dimensionality, that all operation nodes have exactly two children, and that each primitive has the required dimensions for its shape type. We added depth checking to prevent infinite recursion from circular references, and we validate that each shape has its required dimension parameters.

The save and load methods handle persistence. We include metadata such as creation and modification timestamps to track tree history, and we added a version field for future compatibility. The JSON format provides human-readable storage that users can inspect and potentially edit manually if needed. We added proper encoding specification and better error handling for file operations.


The calculate_bounds method computes an axis-aligned bounding box for the entire tree, which helps with rendering optimization and validation. This provides useful information about the spatial extent of the geometry.


LLM Integration Architecture

Integrating language models requires careful abstraction to support multiple backends while maintaining a consistent interface. We design an adapter pattern that encapsulates backend-specific details behind a common API.


The base LLM interface defines the contract that all implementations must fulfill. This includes text generation, conversation management, and resource cleanup.


from abc import ABC, abstractmethod

from typing import List, Dict


class LLMBackend(ABC):

    @abstractmethod

    def generate(self, prompt: str, max_tokens: int = 2048, 

                 temperature: float = 0.7) -> str:

        pass

    

    @abstractmethod

    def generate_with_history(self, messages: List[Dict[str, str]], 

                               max_tokens: int = 2048,

                               temperature: float = 0.7) -> str:

        pass

    

    @abstractmethod

    def cleanup(self):

        pass

    

    @abstractmethod

    def is_available(self) -> bool:

        pass


The generate method handles single-turn interactions, while generate_with_history supports multi-turn conversations. The cleanup method releases resources, which becomes particularly important for local models that consume significant GPU memory. The is_available method allows runtime detection of backend availability.


For local execution, we need to detect and utilize available GPU hardware. Different platforms require different libraries and initialization procedures.


import platform

import subprocess

import sys


class HardwareDetector:

    @staticmethod

    def detect_gpu() -> str:

        system = platform.system()

        

        # Check for NVIDIA CUDA

        try:

            result = subprocess.run(

                ['nvidia-smi'], 

                capture_output=True, 

                check=True,

                timeout=5

            )

            return "cuda"

        except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):

            pass

        

        # Check for AMD ROCm

        try:

            result = subprocess.run(

                ['rocm-smi'], 

                capture_output=True, 

                check=True,

                timeout=5

            )

            return "rocm"

        except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):

            pass

        

        # Check for Apple Silicon

        if system == "Darwin":

            try:

                result = subprocess.run(

                    ['sysctl', '-n', 'machdep.cpu.brand_string'],

                    capture_output=True, 

                    text=True,

                    timeout=5

                )

                if 'Apple' in result.stdout:

                    return "mps"

            except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):

                pass

        

        # Check for Intel GPU

        if system == "Linux":

            try:

                with open('/proc/cpuinfo', 'r') as f:

                    content = f.read()

                    if 'Intel' in content:

                        try:

                            import intel_extension_for_pytorch

                            return "intel"

                        except ImportError:

                            pass

            except (IOError, OSError):

                pass

        

        return "cpu"


The HardwareDetector class attempts to identify available GPU hardware through system-specific commands and file inspection. For NVIDIA GPUs, we check for the nvidia-smi utility. For AMD GPUs, we look for rocm-smi. On macOS, we examine the CPU brand string to detect Apple Silicon. This detection informs which acceleration library to use when initializing local models. We added timeout parameters to prevent hanging on unresponsive commands and better exception handling.


Now we implement a local LLM backend using the transformers library, which supports multiple hardware platforms through PyTorch.


class LocalLLMBackend(LLMBackend):

    def __init__(self, model_name: str = "gpt2", device: str = None):

        self.model_name = model_name

        self.device = device or self._detect_device()

        self.model = None

        self.tokenizer = None

        self.generation_timeout = 300  # 5 minutes

        self._initialize()

    

    def _detect_device(self) -> str:

        gpu_type = HardwareDetector.detect_gpu()

        

        if gpu_type == "cuda":

            return "cuda"

        elif gpu_type == "rocm":

            return "cuda"

        elif gpu_type == "mps":

            return "mps"

        elif gpu_type == "intel":

            return "xpu"

        else:

            return "cpu"

    

    def _initialize(self):

        try:

            from transformers import AutoModelForCausalLM, AutoTokenizer

            import torch

            

            self.tokenizer = AutoTokenizer.from_pretrained(

                self.model_name,

                trust_remote_code=False

            )

            

            if self.device == "cuda":

                if not torch.cuda.is_available():

                    print("Warning: CUDA requested but not available, falling back to CPU")

                    self.device = "cpu"

                    self.model = AutoModelForCausalLM.from_pretrained(

                        self.model_name,

                        trust_remote_code=False

                    )

                else:

                    self.model = AutoModelForCausalLM.from_pretrained(

                        self.model_name,

                        torch_dtype=torch.float16,

                        device_map="auto",

                        trust_remote_code=False

                    )

            elif self.device == "mps":

                if not torch.backends.mps.is_available():

                    print("Warning: MPS requested but not available, falling back to CPU")

                    self.device = "cpu"

                    self.model = AutoModelForCausalLM.from_pretrained(

                        self.model_name,

                        trust_remote_code=False

                    )

                else:

                    self.model = AutoModelForCausalLM.from_pretrained(

                        self.model_name,

                        torch_dtype=torch.float16,

                        trust_remote_code=False

                    )

                    self.model = self.model.to("mps")

            elif self.device == "xpu":

                try:

                    import intel_extension_for_pytorch as ipex

                    self.model = AutoModelForCausalLM.from_pretrained(

                        self.model_name,

                        trust_remote_code=False

                    )

                    self.model = self.model.to("xpu")

                    self.model = ipex.optimize(self.model)

                except ImportError:

                    print("Warning: Intel extension not available, falling back to CPU")

                    self.device = "cpu"

                    self.model = AutoModelForCausalLM.from_pretrained(

                        self.model_name,

                        trust_remote_code=False

                    )

            else:

                self.model = AutoModelForCausalLM.from_pretrained(

                    self.model_name,

                    trust_remote_code=False

                )

            

            if self.tokenizer.pad_token is None:

                self.tokenizer.pad_token = self.tokenizer.eos_token

            

        except Exception as e:

            print(f"Error initializing local LLM: {e}")

            raise

    

    def generate(self, prompt: str, max_tokens: int = 2048,

                 temperature: float = 0.7) -> str:

        if not self.is_available():

            return ""

        

        try:

            import torch

            import signal

            

            # Set up timeout handler for generation

            class TimeoutError(Exception):

                pass

            

            def timeout_handler(signum, frame):

                raise TimeoutError("Generation timeout")

            

            inputs = self.tokenizer(

                prompt, 

                return_tensors="pt",

                padding=True, 

                truncation=True, 

                max_length=2048

            )

            

            inputs = {k: v.to(self.model.device) for k, v in inputs.items()}

            

            # Set timeout for Unix-like systems

            if hasattr(signal, 'SIGALRM'):

                old_handler = signal.signal(signal.SIGALRM, timeout_handler)

                signal.alarm(self.generation_timeout)

            

            try:

                with torch.no_grad():

                    outputs = self.model.generate(

                        **inputs,

                        max_new_tokens=min(max_tokens, 2048),

                        temperature=max(0.1, min(temperature, 2.0)),

                        do_sample=temperature > 0,

                        pad_token_id=self.tokenizer.pad_token_id,

                        eos_token_id=self.tokenizer.eos_token_id

                    )

                

                generated_text = self.tokenizer.decode(

                    outputs[0], 

                    skip_special_tokens=True

                )

                

                if generated_text.startswith(prompt):

                    generated_text = generated_text[len(prompt):].strip()

                

                return generated_text

            finally:

                if hasattr(signal, 'SIGALRM'):

                    signal.alarm(0)

                    signal.signal(signal.SIGALRM, old_handler)

                

        except TimeoutError:

            print("Error: Generation timed out")

            return ""

        except Exception as e:

            print(f"Error generating text: {e}")

            return ""

    

    def generate_with_history(self, messages: List[Dict[str, str]],

                               max_tokens: int = 2048,

                               temperature: float = 0.7) -> str:

        prompt = ""

        for msg in messages:

            role = msg.get("role", "user")

            content = msg.get("content", "")

            prompt += f"{role}: {content}\n"

        

        prompt += "assistant: "

        return self.generate(prompt, max_tokens, temperature)

    

    def cleanup(self):

        try:

            import torch

            

            if self.model is not None:

                del self.model

                self.model = None

            if self.tokenizer is not None:

                del self.tokenizer

                self.tokenizer = None

            

            if torch.cuda.is_available():

                torch.cuda.empty_cache()

        except Exception as e:

            print(f"Error during cleanup: {e}")

    

    def is_available(self) -> bool:

        return self.model is not None and self.tokenizer is not None


The LocalLLMBackend implementation handles the complexities of multi-platform GPU support. The initialization process differs based on detected hardware. For CUDA-compatible devices including AMD ROCm, we use automatic device mapping and half-precision floating point for memory efficiency. For Apple Metal Performance Shaders, we explicitly move the model to the MPS device. For Intel GPUs, we apply Intel's optimization extensions. We added availability checks for each device type and graceful fallback to CPU when the requested device is not available. We also added trust_remote_code=False for security, parameter validation to prevent invalid values, and a timeout mechanism for generation to prevent hanging.


The generate method tokenizes input text, moves tensors to the appropriate device, performs inference with timeout protection, and decodes the output. We strip the original prompt from the generated text to return only the model's response. The generate_with_history method formats conversation history into a single prompt string, enabling multi-turn interactions.


For remote LLM services, we implement a different backend that communicates via HTTP APIs.


import requests

from requests.adapters import HTTPAdapter

try:

    from urllib3.util.retry import Retry

except ImportError:

    from requests.packages.urllib3.util.retry import Retry


class RemoteLLMBackend(LLMBackend):

    def __init__(self, api_url: str, api_key: str = None, 

                 model_name: str = "gpt-3.5-turbo"):

        if not api_url:

            raise ValueError("API URL cannot be empty")

        

        self.api_url = api_url.rstrip('/')

        self.api_key = api_key

        self.model_name = model_name

        self.session = requests.Session()

        

        # Configure retry strategy

        retry_strategy = Retry(

            total=3,

            backoff_factor=1,

            status_forcelist=[429, 500, 502, 503, 504],

        )

        adapter = HTTPAdapter(max_retries=retry_strategy)

        self.session.mount("http://", adapter)

        self.session.mount("https://", adapter)

        

        if api_key:

            self.session.headers.update({

                "Authorization": f"Bearer {api_key}",

                "Content-Type": "application/json"

            })

        else:

            self.session.headers.update({

                "Content-Type": "application/json"

            })

    

    def generate(self, prompt: str, max_tokens: int = 2048,

                 temperature: float = 0.7) -> str:

        try:

            response = self.session.post(

                f"{self.api_url}/completions",

                json={

                    "model": self.model_name,

                    "prompt": prompt,

                    "max_tokens": min(max_tokens, 4096),

                    "temperature": max(0.0, min(temperature, 2.0))

                },

                timeout=60

            )

            response.raise_for_status()

            data = response.json()

            

            if "choices" in data and len(data["choices"]) > 0:

                return data["choices"][0].get("text", "")

            return ""

        except requests.exceptions.Timeout:

            print("Error: Request timed out")

            return ""

        except requests.exceptions.RequestException as e:

            print(f"Error calling remote LLM: {e}")

            return ""

        except (KeyError, IndexError, ValueError) as e:

            print(f"Error parsing response: {e}")

            return ""

    

    def generate_with_history(self, messages: List[Dict[str, str]],

                               max_tokens: int = 2048,

                               temperature: float = 0.7) -> str:

        try:

            response = self.session.post(

                f"{self.api_url}/chat/completions",

                json={

                    "model": self.model_name,

                    "messages": messages,

                    "max_tokens": min(max_tokens, 4096),

                    "temperature": max(0.0, min(temperature, 2.0))

                },

                timeout=60

            )

            response.raise_for_status()

            data = response.json()

            

            if "choices" in data and len(data["choices"]) > 0:

                message = data["choices"][0].get("message", {})

                return message.get("content", "")

            return ""

        except requests.exceptions.Timeout:

            print("Error: Request timed out")

            return ""

        except requests.exceptions.RequestException as e:

            print(f"Error calling remote LLM: {e}")

            return ""

        except (KeyError, IndexError, ValueError) as e:

            print(f"Error parsing response: {e}")

            return ""

    

    def cleanup(self):

        try:

            self.session.close()

        except Exception as e:

            print(f"Error closing session: {e}")

    

    def is_available(self) -> bool:

        try:

            response = self.session.get(

                f"{self.api_url}/models", 

                timeout=5

            )

            return response.status_code == 200

        except:

            return False


The RemoteLLMBackend uses HTTP requests to communicate with API endpoints. It maintains a session object for connection pooling and header management. The implementation follows common API patterns like OpenAI's interface, making it compatible with various hosted services. We added retry logic for transient failures, better error handling with specific exception types, parameter validation, URL normalization, and proper import handling for the Retry class to support both newer and older versions of the requests library.


Natural Language to CSG Translation

The most challenging aspect of the system involves translating natural language descriptions into CSG trees. This requires the LLM to understand spatial relationships, identify geometric primitives, and determine appropriate boolean operations.


We design a prompt engineering strategy that guides the LLM to produce structured output. The prompt includes examples of successful translations, specifies the expected JSON format, and provides constraints to ensure valid geometry.


class CSGTranslator:

    def __init__(self, llm_backend: LLMBackend, dimension: int = 3):

        if dimension not in [2, 3]:

            raise ValueError(f"Dimension must be 2 or 3, got {dimension}")

        

        self.llm = llm_backend

        self.dimension = dimension

        self.conversation_history = []

    

    def _build_system_prompt(self) -> str:

        dim_str = "2D" if self.dimension == 2 else "3D"

        

        if self.dimension == 2:

            primitives = "rectangles, circles, and polygons"

            example_shape = "rectangle"

            example_dims = '{"width": 10, "height": 5}'

            example_pos = "[0, 0]"

        else:

            primitives = "boxes, spheres, cylinders, and cones"

            example_shape = "box"

            example_dims = '{"width": 10, "height": 5, "depth": 3}'

            example_pos = "[0, 0, 0]"

        

        prompt = f"""You are a {dim_str} geometry expert that converts natural language descriptions into Constructive Solid Geometry (CSG) trees.


Available primitives: {primitives}

Available operations: union, intersection, difference


Your output must be valid JSON representing a CSG tree. Each node is either:

1. A primitive with shape_type, position, dimensions, rotation, and name

2. An operation with operation type, left child, and right child


Example for a single {example_shape}:

{{

  "node_type": "primitive",

  "primitive": {{

    "type": "primitive",

    "shape_type": "{example_shape}",

    "position": {example_pos},

    "dimensions": {example_dims},

    "rotation": [0, 0, 0],

    "name": "{example_shape}_1"

  }},

  "name": "root"

}}


For multiple objects, use operations:

{{

  "node_type": "operation",

  "operation": "union",

  "left": {{...primitive or operation...}},

  "right": {{...primitive or operation...}},

  "name": "union_1"

}}


Guidelines:

- Use "union" to combine objects

- Use "intersection" for overlapping regions

- Use "difference" to subtract (left - right)

- Provide reasonable positions and dimensions

- All numeric values must be numbers, not strings

- All dimension values must be positive

- Name nodes descriptively

- Output ONLY valid JSON, no explanations


Respond with complete, valid JSON only."""

        

        return prompt

    

    def translate(self, description: str) -> Optional[CSGTree]:

        if not description or not description.strip():

            print("Error: Empty description")

            return None

        

        system_prompt = self._build_system_prompt()

        

        messages = [

            {"role": "system", "content": system_prompt},

            {"role": "user", "content": f"Create a CSG tree for: {description}"}

        ]

        

        response = self.llm.generate_with_history(

            messages, 

            max_tokens=4096,

            temperature=0.3

        )

        

        if not response:

            print("Error: Empty response from LLM")

            return None

        

        json_str = self._extract_json(response)

        if not json_str:

            print("Failed to extract JSON from LLM response")

            print(f"Response was: {response[:200]}...")

            return None

        

        try:

            tree_data = json.loads(json_str)

            root = CSGNode.from_dict(tree_data)

            tree = CSGTree(root=root, dimension=self.dimension)

            

            valid, msg = tree.validate()

            if not valid:

                print(f"Generated tree is invalid: {msg}")

                return None

            

            return tree

        except json.JSONDecodeError as e:

            print(f"Invalid JSON from LLM: {e}")

            return None

        except (KeyError, ValueError, TypeError) as e:

            print(f"Error parsing LLM output: {e}")

            return None

        except Exception as e:

            print(f"Unexpected error: {e}")

            return None

    

    def _extract_json(self, text: str) -> Optional[str]:

        if not text:

            return None

        

        # Find JSON object in text

        start = text.find('{')

        if start == -1:

            return None

        

        # Find matching closing brace

        brace_count = 0

        for i in range(start, len(text)):

            if text[i] == '{':

                brace_count += 1

            elif text[i] == '}':

                brace_count -= 1

                if brace_count == 0:

                    return text[start:i+1]

        

        return None


The CSGTranslator class manages the translation process. The system prompt provides comprehensive instructions and examples to guide the LLM's output. We use a low temperature value of 0.3 to encourage deterministic, structured responses rather than creative variations. We added input validation, better error messages with context, and more specific exception handling. The prompt explicitly states that all dimension values must be positive to prevent invalid geometry.


The extract_json method handles cases where the LLM includes explanatory text alongside the JSON output. It searches for the first opening brace and finds the matching closing brace, extracting only the JSON portion. This robustness helps handle variations in LLM output formatting.


After receiving the LLM's response, we parse the JSON and construct a CSGTree object. The validation step ensures the generated tree meets all structural and dimensional requirements before returning it to the user.


Rendering Pipeline

Transforming CSG trees into visual representations requires different approaches for 2D and 3D rendering. For 2D, we can use straightforward rasterization techniques. For 3D, we employ ray marching or mesh generation algorithms.


We start with a 2D renderer that evaluates the CSG tree at each pixel to determine whether it lies inside or outside the final shape.


import numpy as np

from PIL import Image, ImageDraw


class Renderer2D:

    def __init__(self, width: int = 800, height: int = 600):

        if width <= 0 or height <= 0:

            raise ValueError("Width and height must be positive")

        

        self.width = width

        self.height = height

        self.scale = 20

        self.origin = np.array([width / 2.0, height / 2.0])

    

    def render(self, tree: CSGTree) -> Image.Image:

        if tree.dimension != 2:

            raise ValueError("Can only render 2D trees")

        

        if tree.root is None:

            raise ValueError("Cannot render empty tree")

        

        # Check if tree has reasonable bounds

        if tree.bounds is not None:

            min_coords, max_coords = tree.bounds

            extent = np.max(np.abs(max_coords - min_coords))

            if extent > 1000:

                print(f"Warning: Tree has large extent ({extent}), rendering may be slow")

        

        img = Image.new('RGB', (self.width, self.height), 'white')

        draw = ImageDraw.Draw(img)

        

        samples = 4

        coverage = np.zeros((self.height * samples, self.width * samples), dtype=np.float32)

        

        for y in range(self.height * samples):

            for x in range(self.width * samples):

                world_x = (x / samples - self.width / 2.0) / self.scale

                world_y = (self.height / 2.0 - y / samples) / self.scale

                point = np.array([world_x, world_y])

                

                try:

                    if self._evaluate_node(tree.root, point):

                        coverage[y, x] = 1.0

                except Exception:

                    pass

        

        for y in range(self.height):

            for x in range(self.width):

                sample_sum = np.sum(

                    coverage[y*samples:(y+1)*samples, x*samples:(x+1)*samples]

                )

                intensity = int(255 * (1.0 - sample_sum / (samples * samples)))

                intensity = max(0, min(255, intensity))

                draw.point((x, y), fill=(intensity, intensity, intensity))

        

        # Explicitly delete large array to free memory

        del coverage

        

        return img

    

    def _evaluate_node(self, node: CSGNode, point: np.ndarray) -> bool:

        if node.is_leaf():

            return self._point_in_primitive(node.primitive, point)

        else:

            left_result = self._evaluate_node(node.left, point)

            right_result = self._evaluate_node(node.right, point)

            

            if node.operation == OperationType.UNION:

                return left_result or right_result

            elif node.operation == OperationType.INTERSECTION:

                return left_result and right_result

            elif node.operation == OperationType.DIFFERENCE:

                return left_result and not right_result

            

            return False

    

    def _point_in_primitive(self, primitive: Primitive, point: np.ndarray) -> bool:

        relative_pos = point - primitive.position[:2]

        

        if len(primitive.rotation) >= 3 and abs(primitive.rotation[2]) > 1e-6:

            angle = -primitive.rotation[2]

            cos_a, sin_a = np.cos(angle), np.sin(angle)

            rotation_matrix = np.array([[cos_a, -sin_a], [sin_a, cos_a]])

            relative_pos = rotation_matrix @ relative_pos

        

        if primitive.shape_type == ShapeType.RECTANGLE:

            if "width" not in primitive.dimensions or "height" not in primitive.dimensions:

                return False

            half_width = primitive.dimensions["width"] / 2.0

            half_height = primitive.dimensions["height"] / 2.0

            return (abs(relative_pos[0]) <= half_width and

                    abs(relative_pos[1]) <= half_height)

        

        elif primitive.shape_type == ShapeType.CIRCLE:

            if "radius" not in primitive.dimensions:

                return False

            radius = primitive.dimensions["radius"]

            return np.linalg.norm(relative_pos) <= radius

        

        elif primitive.shape_type == ShapeType.POLYGON:

            if "vertices" not in primitive.dimensions:

                return False

            vertices = np.array(primitive.dimensions["vertices"])

            if len(vertices) < 3:

                return False

            return self._point_in_polygon(relative_pos, vertices)

        

        return False

    

    def _point_in_polygon(self, point: np.ndarray, vertices: np.ndarray) -> bool:

        n = len(vertices)

        inside = False

        

        p1x, p1y = vertices[0]

        for i in range(1, n + 1):

            p2x, p2y = vertices[i % n]

            if point[1] > min(p1y, p2y):

                if point[1] <= max(p1y, p2y):

                    if point[0] <= max(p1x, p2x):

                        if abs(p1y - p2y) > 1e-10:

                            xinters = (point[1] - p1y) * (p2x - p1x) / (p2y - p1y) + p1x

                            if abs(p1x - p2x) < 1e-10 or point[0] <= xinters:

                                inside = not inside

            p1x, p1y = p2x, p2y

        

        return inside


The Renderer2D class implements a pixel-by-pixel evaluation approach with supersampling for anti-aliasing. For each pixel, we sample multiple points and average the results to produce smooth edges. The evaluate_node method recursively traverses the CSG tree, applying boolean operations to combine primitive evaluations. We added validation for empty trees, dimension checking, error handling during evaluation, intensity clamping, checks for required dimension keys, bounds checking with warnings for large extents, and explicit memory cleanup for the large coverage array.


The point_in_primitive method determines whether a given point lies inside a primitive shape. It handles coordinate transformation to account for position and rotation, then applies shape-specific containment tests. For rectangles, we check axis-aligned bounding box containment. For circles, we compute distance from center. For polygons, we use the ray casting algorithm. We added epsilon comparisons for floating-point stability.


For 3D rendering, we implement a ray marching approach that traces rays through the scene and evaluates the CSG tree using signed distance functions.


class Renderer3D:

    def __init__(self, width: int = 800, height: int = 600):

        if width <= 0 or height <= 0:

            raise ValueError("Width and height must be positive")

        

        self.width = width

        self.height = height

        self.camera_pos = np.array([0.0, 0.0, -10.0])

        self.camera_target = np.array([0.0, 0.0, 0.0])

        self.camera_up = np.array([0.0, 1.0, 0.0])

        self.fov = 60

    

    def render(self, tree: CSGTree) -> Image.Image:

        if tree.dimension != 3:

            raise ValueError("Can only render 3D trees")

        

        if tree.root is None:

            raise ValueError("Cannot render empty tree")

        

        # Check if tree has reasonable bounds

        if tree.bounds is not None:

            min_coords, max_coords = tree.bounds

            extent = np.max(np.abs(max_coords - min_coords))

            if extent > 1000:

                print(f"Warning: Tree has large extent ({extent}), rendering may be slow")

        

        img = Image.new('RGB', (self.width, self.height), 'white')

        pixels = np.zeros((self.height, self.width, 3), dtype=np.uint8)

        pixels.fill(255)

        

        forward = self.camera_target - self.camera_pos

        forward_norm = np.linalg.norm(forward)

        if forward_norm < 1e-6:

            raise ValueError("Camera position and target are too close")

        forward = forward / forward_norm

        

        right = np.cross(forward, self.camera_up)

        right_norm = np.linalg.norm(right)

        if right_norm < 1e-6:

            raise ValueError("Camera up vector is parallel to view direction")

        right = right / right_norm

        

        up = np.cross(right, forward)

        

        aspect_ratio = self.width / self.height

        fov_rad = np.radians(self.fov)

        viewport_height = 2.0 * np.tan(fov_rad / 2.0)

        viewport_width = viewport_height * aspect_ratio

        

        for y in range(self.height):

            for x in range(self.width):

                u = (x / self.width - 0.5) * viewport_width

                v = (0.5 - y / self.height) * viewport_height

                

                ray_dir = forward + u * right + v * up

                ray_dir_norm = np.linalg.norm(ray_dir)

                if ray_dir_norm > 1e-6:

                    ray_dir = ray_dir / ray_dir_norm

                else:

                    continue

                

                try:

                    color = self._ray_march(tree.root, self.camera_pos, ray_dir)

                    pixels[y, x] = color

                except Exception:

                    pixels[y, x] = [255, 255, 255]

        

        return Image.fromarray(pixels, mode='RGB')

    

    def _ray_march(self, node: CSGNode, origin: np.ndarray,

                   direction: np.ndarray, max_steps: int = 100,

                   max_distance: float = 100.0) -> np.ndarray:

        t = 0.0

        epsilon = 0.001

        

        for step in range(max_steps):

            pos = origin + t * direction

            dist = self._sdf_node(node, pos)

            

            if not np.isfinite(dist):

                break

            

            if abs(dist) < epsilon:

                try:

                    normal = self._calculate_normal(node, pos)

                    

                    light_dir = np.array([1.0, 1.0, -1.0])

                    light_dir = light_dir / np.linalg.norm(light_dir)

                    

                    diffuse = max(0.0, np.dot(normal, light_dir))

                    ambient = 0.2

                    

                    intensity = ambient + (1.0 - ambient) * diffuse

                    color_value = int(255 * max(0.0, min(1.0, intensity)))

                    

                    return np.array([color_value, color_value, color_value], dtype=np.uint8)

                except Exception:

                    return np.array([128, 128, 128], dtype=np.uint8)

            

            if dist < 0:

                t += epsilon

            else:

                t += max(dist * 0.9, epsilon)

            

            if t > max_distance:

                break

        

        return np.array([255, 255, 255], dtype=np.uint8)

    

    def _sdf_node(self, node: CSGNode, point: np.ndarray) -> float:

        if node.is_leaf():

            return self._sdf_primitive(node.primitive, point)

        else:

            left_dist = self._sdf_node(node.left, point)

            right_dist = self._sdf_node(node.right, point)

            

            if not np.isfinite(left_dist) or not np.isfinite(right_dist):

                return float('inf')

            

            if node.operation == OperationType.UNION:

                return min(left_dist, right_dist)

            elif node.operation == OperationType.INTERSECTION:

                return max(left_dist, right_dist)

            elif node.operation == OperationType.DIFFERENCE:

                return max(left_dist, -right_dist)

            

            return left_dist

    

    def _sdf_primitive(self, primitive: Primitive, point: np.ndarray) -> float:

        relative_pos = point - primitive.position

        

        if np.any(np.abs(primitive.rotation) > 1e-6):

            rx, ry, rz = -primitive.rotation

            

            Rx = np.array([[1, 0, 0],

                           [0, np.cos(rx), -np.sin(rx)],

                           [0, np.sin(rx), np.cos(rx)]])

            

            Ry = np.array([[np.cos(ry), 0, np.sin(ry)],

                           [0, 1, 0],

                           [-np.sin(ry), 0, np.cos(ry)]])

            

            Rz = np.array([[np.cos(rz), -np.sin(rz), 0],

                           [np.sin(rz), np.cos(rz), 0],

                           [0, 0, 1]])

            

            rotation_matrix = Rz @ Ry @ Rx

            relative_pos = rotation_matrix @ relative_pos

        

        if primitive.shape_type == ShapeType.BOX:

            if "width" not in primitive.dimensions or \

               "height" not in primitive.dimensions or \

               "depth" not in primitive.dimensions:

                return float('inf')

            

            half_size = np.array([

                primitive.dimensions["width"] / 2.0,

                primitive.dimensions["height"] / 2.0,

                primitive.dimensions["depth"] / 2.0

            ])

            q = np.abs(relative_pos) - half_size

            return np.linalg.norm(np.maximum(q, 0)) + min(max(q[0], q[1], q[2]), 0.0)

        

        elif primitive.shape_type == ShapeType.SPHERE:

            if "radius" not in primitive.dimensions:

                return float('inf')

            radius = primitive.dimensions["radius"]

            return np.linalg.norm(relative_pos) - radius

        

        elif primitive.shape_type == ShapeType.CYLINDER:

            if "radius" not in primitive.dimensions or "height" not in primitive.dimensions:

                return float('inf')

            

            radius = primitive.dimensions["radius"]

            height = primitive.dimensions["height"]

            

            d = np.array([

                np.linalg.norm(relative_pos[[0, 2]]) - radius,

                abs(relative_pos[1]) - height / 2.0

            ])

            return min(max(d[0], d[1]), 0.0) + np.linalg.norm(np.maximum(d, 0))

        

        elif primitive.shape_type == ShapeType.CONE:

            if "radius" not in primitive.dimensions or "height" not in primitive.dimensions:

                return float('inf')

            

            radius = primitive.dimensions["radius"]

            height = primitive.dimensions["height"]

            

            if abs(height) < 1e-6:

                return float('inf')

            

            q = np.array([np.linalg.norm(relative_pos[[0, 2]]), relative_pos[1]])

            k = np.array([radius / height, -1.0])

            k_dot_k = np.dot(k, k)

            

            if abs(k_dot_k) < 1e-10:

                return float('inf')

            

            d1 = q - np.array([0.0, height])

            d2_proj = np.clip(np.dot(q, k) / k_dot_k, 0.0, height)

            d2 = q - d2_proj * k

            

            return np.sqrt(min(np.dot(d1, d1), np.dot(d2, d2)))

        

        return float('inf')

    

    def _calculate_normal(self, node: CSGNode, point: np.ndarray,

                          epsilon: float = 0.001) -> np.ndarray:

        dx = np.array([epsilon, 0, 0])

        dy = np.array([0, epsilon, 0])

        dz = np.array([0, 0, epsilon])

        

        normal = np.array([

            self._sdf_node(node, point + dx) - self._sdf_node(node, point - dx),

            self._sdf_node(node, point + dy) - self._sdf_node(node, point - dy),

            self._sdf_node(node, point + dz) - self._sdf_node(node, point - dz)

        ])

        

        norm = np.linalg.norm(normal)

        if norm > 1e-6:

            return normal / norm

        return np.array([0.0, 1.0, 0.0])


The Renderer3D class implements ray marching using signed distance functions. For each pixel, we cast a ray from the camera position through the viewport. We march along the ray, evaluating the SDF at each step. The SDF returns the distance to the nearest surface, allowing us to take large steps when far from geometry and small steps when close. We added validation for camera configuration, checks for degenerate cases, finite value checks for SDFs, required dimension validation, bounds checking with warnings, and a critical fix for the cone SDF to prevent division by zero when k dot k is near zero.


The sdf_primitive method implements signed distance functions for each 3D primitive type. These functions return negative values inside the shape, positive values outside, and zero exactly on the surface. The mathematical formulations ensure smooth, accurate distance estimates that enable efficient ray marching.


When a ray intersects the surface, we calculate the surface normal using numerical differentiation of the SDF. This normal vector enables simple diffuse lighting calculations that provide depth perception in the rendered image.


Interactive Tree Editing

Providing an intuitive editing interface requires careful design of both the data manipulation layer and the user interaction layer. Users should be able to select nodes, modify parameters, add new primitives, change operations, and delete subtrees.

We create an editor class that maintains the current tree state and provides high-level editing operations.


class CSGTreeEditor:

    def __init__(self, tree: CSGTree):

        if tree is None:

            raise ValueError("Tree cannot be None")

        

        self.tree = tree

        self.selected_node = None

        self.node_map = {}

        self._build_node_map()

    

    def _build_node_map(self):

        self.node_map.clear()

        if self.tree.root:

            self._map_nodes(self.tree.root)

    

    def _map_nodes(self, node: CSGNode):

        if node.name in self.node_map:

            base_name = node.name

            counter = 1

            while f"{base_name}_{counter}" in self.node_map:

                counter += 1

            node.name = f"{base_name}_{counter}"

        

        self.node_map[node.name] = node

        if not node.is_leaf():

            self._map_nodes(node.left)

            self._map_nodes(node.right)

    

    def select_node(self, node_name: str) -> bool:

        if not node_name:

            return False

        

        if node_name in self.node_map:

            self.selected_node = self.node_map[node_name]

            return True

        return False

    

    def get_node_info(self, node_name: str) -> Optional[Dict[str, Any]]:

        if not node_name or node_name not in self.node_map:

            return None

        

        node = self.node_map[node_name]

        info = {

            "name": node.name,

            "type": node.node_type

        }

        

        if node.is_leaf():

            info["primitive"] = {

                "shape_type": node.primitive.shape_type.value,

                "position": node.primitive.position.tolist(),

                "dimensions": node.primitive.dimensions.copy(),

                "rotation": node.primitive.rotation.tolist()

            }

        else:

            info["operation"] = node.operation.value

            info["left_child"] = node.left.name if node.left else None

            info["right_child"] = node.right.name if node.right else None

        

        return info

    

    def update_primitive(self, node_name: str, position: np.ndarray = None,

                         dimensions: Dict[str, float] = None,

                         rotation: np.ndarray = None) -> bool:

        if not node_name or node_name not in self.node_map:

            return False

        

        node = self.node_map[node_name]

        if not node.is_leaf():

            return False

        

        try:

            if position is not None:

                pos_array = np.array(position, dtype=np.float64)

                if len(pos_array) >= self.tree.dimension:

                    node.primitive.position = pos_array

                else:

                    return False

            

            if dimensions is not None:

                for key, value in dimensions.items():

                    if not isinstance(value, (int, float)) or value <= 0:

                        return False

                node.primitive.dimensions.update(dimensions)

            

            if rotation is not None:

                rot_array = np.array(rotation, dtype=np.float64)

                if len(rot_array) < 3:

                    rot_array = np.pad(rot_array, (0, 3 - len(rot_array)))

                # Normalize rotation angles

                rot_array = np.fmod(rot_array, 2 * np.pi)

                node.primitive.rotation = rot_array

            

            # Recalculate tree bounds after update

            self.tree._calculate_bounds()

            

            return True

        except (ValueError, TypeError):

            return False

    

    def change_operation(self, node_name: str,

                         new_operation: OperationType) -> bool:

        if not node_name or node_name not in self.node_map:

            return False

        

        node = self.node_map[node_name]

        if node.is_leaf():

            return False

        

        node.operation = new_operation

        return True

    

    def add_primitive(self, parent_name: str, side: str,

                      primitive: Primitive,

                      operation: OperationType = OperationType.UNION) -> bool:

        if not parent_name or parent_name not in self.node_map:

            return False

        

        if side not in ["left", "right"]:

            return False

        

        if primitive is None:

            return False

        

        try:

            parent = self.node_map[parent_name]

            new_leaf = CSGNode(node_type="primitive", primitive=primitive)

            

            new_op = CSGNode(

                node_type="operation",

                operation=operation,

                left=parent if side == "left" else new_leaf,

                right=new_leaf if side == "left" else parent

            )

            

            if parent == self.tree.root:

                self.tree.root = new_op

            else:

                parent_parent = self._find_parent(self.tree.root, parent)

                if parent_parent:

                    if parent_parent.left == parent:

                        parent_parent.left = new_op

                    else:

                        parent_parent.right = new_op

                else:

                    return False

            

            self._build_node_map()

            self.tree._calculate_bounds()

            return True

        except Exception:

            return False

    

    def delete_subtree(self, node_name: str) -> bool:

        if not node_name or node_name not in self.node_map:

            return False

        

        try:

            node = self.node_map[node_name]

            

            if node == self.tree.root:

                self.tree.root = None

                self._build_node_map()

                self.tree.bounds = None

                return True

            

            parent = self._find_parent(self.tree.root, node)

            if not parent:

                return False

            

            sibling = parent.right if parent.left == node else parent.left

            

            if parent == self.tree.root:

                self.tree.root = sibling

            else:

                grandparent = self._find_parent(self.tree.root, parent)

                if grandparent:

                    if grandparent.left == parent:

                        grandparent.left = sibling

                    else:

                        grandparent.right = sibling

                else:

                    return False

            

            self._build_node_map()

            self.tree._calculate_bounds()

            return True

        except Exception:

            return False

    

    def _find_parent(self, current: CSGNode, target: CSGNode) -> Optional[CSGNode]:

        if current is None or current.is_leaf():

            return None

        

        if current.left == target or current.right == target:

            return current

        

        left_result = self._find_parent(current.left, target)

        if left_result:

            return left_result

        

        return self._find_parent(current.right, target)

    

    def get_tree_structure(self) -> List[Dict[str, Any]]:

        structure = []

        if self.tree.root:

            self._collect_structure(self.tree.root, structure, 0)

        return structure

    

    def _collect_structure(self, node: CSGNode, structure: List[Dict[str, Any]],

                           depth: int):

        if depth > 100:

            return

        

        indent = "  " * depth

        

        if node.is_leaf():

            structure.append({

                "name": node.name,

                "display": f"{indent}[P] {node.primitive.shape_type.value}",

                "depth": depth,

                "type": "primitive"

            })

        else:

            structure.append({

                "name": node.name,

                "display": f"{indent}[O] {node.operation.value}",

                "depth": depth,

                "type": "operation"

            })

            if node.left:

                self._collect_structure(node.left, structure, depth + 1)

            if node.right:

                self._collect_structure(node.right, structure, depth + 1)


The CSGTreeEditor class maintains a mapping from node names to node objects for efficient lookup during editing operations. The build_node_map method traverses the tree and populates this mapping whenever the tree structure changes. We added handling for duplicate node names, validation throughout, protection against infinite recursion, comprehensive error handling, rotation angle normalization, and bounds recalculation after tree modifications.


The update_primitive method allows modification of primitive parameters without changing tree structure. This enables users to adjust positions, sizes, and orientations interactively. The change_operation method modifies boolean operations, letting users experiment with different combinations. We added validation for dimension values, proper type checking, and rotation normalization.


The add_primitive method demonstrates more complex tree manipulation. It creates a new primitive node and a new operation node, then rewires the tree to insert these nodes at the specified location. The method handles the special case where we are adding to the root, as well as the general case of adding to an internal node.


The delete_subtree method removes a node and all its descendants. When deleting an operation node, we promote its sibling to take its place, maintaining tree validity. The method uses the find_parent helper to locate the parent of the node being deleted.


User Interface Design

Creating an excellent user experience requires thoughtful interface design that balances power with simplicity. We implement a command-line interface with rich text formatting and interactive prompts, suitable for both novice and expert users.


import sys

import os


class CSGInterface:

    def __init__(self, llm_backend: LLMBackend, dimension: int = 3):

        if dimension not in [2, 3]:

            raise ValueError(f"Dimension must be 2 or 3, got {dimension}")

        

        if llm_backend is None:

            raise ValueError("LLM backend cannot be None")

        

        self.dimension = dimension

        self.translator = CSGTranslator(llm_backend, dimension)

        self.current_tree = None

        self.editor = None

        self.renderer_2d = Renderer2D()

        self.renderer_3d = Renderer3D()

        

        self.commands = {

            "create": self.cmd_create,

            "edit": self.cmd_edit,

            "render": self.cmd_render,

            "save": self.cmd_save,

            "load": self.cmd_load,

            "show": self.cmd_show,

            "help": self.cmd_help,

            "quit": self.cmd_quit,

            "exit": self.cmd_quit

        }

    

    def run(self):

        print("=" * 60)

        print(f"CSG System - {self.dimension}D Mode")

        print("=" * 60)

        print("Type 'help' for available commands")

        print()

        

        while True:

            try:

                user_input = input("> ").strip()

                if not user_input:

                    continue

                

                parts = user_input.split(maxsplit=1)

                command = parts[0].lower()

                args = parts[1] if len(parts) > 1 else ""

                

                if command in self.commands:

                    try:

                        self.commands[command](args)

                    except Exception as e:

                        print(f"Error executing command: {e}")

                else:

                    print(f"Unknown command: {command}")

                    print("Type 'help' for available commands")

            

            except KeyboardInterrupt:

                print("\nUse 'quit' to exit")

            except EOFError:

                print("\nExiting...")

                break

            except Exception as e:

                print(f"Error: {e}")

    

    def cmd_create(self, args: str):

        if not args or not args.strip():

            print("Usage: create <description>")

            print("Example: create a house with a triangular roof")

            return

        

        print(f"Generating CSG tree from description...")

        try:

            tree = self.translator.translate(args)

            

            if tree:

                self.current_tree = tree

                self.editor = CSGTreeEditor(tree)

                print("Tree created successfully!")

                self.cmd_show("")

            else:

                print("Failed to create tree from description")

        except Exception as e:

            print(f"Error creating tree: {e}")

    

    def cmd_edit(self, args: str):

        if not self.current_tree:

            print("No tree loaded. Use 'create' or 'load' first.")

            return

        

        print("\nTree Structure:")

        try:

            structure = self.editor.get_tree_structure()

            for item in structure:

                print(item["display"])

        except Exception as e:

            print(f"Error displaying tree: {e}")

            return

        

        print("\nEdit Commands:")

        print("  select <node_name>  - Select a node for editing")

        print("  update              - Update selected primitive")

        print("  change <operation>  - Change operation type")

        print("  delete <node_name>  - Delete subtree")

        print("  done                - Finish editing")

        

        while True:

            try:

                edit_input = input("edit> ").strip()

                if not edit_input:

                    continue

                

                if edit_input == "done":

                    break

                

                parts = edit_input.split(maxsplit=1)

                edit_cmd = parts[0].lower()

                edit_args = parts[1] if len(parts) > 1 else ""

                

                if edit_cmd == "select":

                    if not edit_args:

                        print("Usage: select <node_name>")

                        continue

                    

                    if self.editor.select_node(edit_args):

                        info = self.editor.get_node_info(edit_args)

                        print(f"Selected: {edit_args}")

                        if info:

                            print(f"Type: {info['type']}")

                            if info['type'] == 'primitive':

                                print(f"Shape: {info['primitive']['shape_type']}")

                                print(f"Position: {info['primitive']['position']}")

                                print(f"Dimensions: {info['primitive']['dimensions']}")

                    else:

                        print(f"Node not found: {edit_args}")

                

                elif edit_cmd == "update":

                    if not self.editor.selected_node:

                        print("No node selected. Use 'select <node_name>' first.")

                        continue

                    

                    if not self.editor.selected_node.is_leaf():

                        print("Can only update primitive nodes")

                        continue

                    

                    print("Enter new values (or press Enter to keep current):")

                    node_name = self.editor.selected_node.name

                    

                    try:

                        pos_str = input(f"Position (current: {self.editor.selected_node.primitive.position.tolist()}): ")

                        if pos_str.strip():

                            pos = np.array([float(x) for x in pos_str.split()])

                            if not self.editor.update_primitive(node_name, position=pos):

                                print("Failed to update position")

                        

                        print("Dimensions:")

                        dims = {}

                        for key, value in self.editor.selected_node.primitive.dimensions.items():

                            if isinstance(value, list):

                                continue  # Skip vertices for now

                            dim_str = input(f"  {key} (current: {value}): ")

                            if dim_str.strip():

                                try:

                                    dims[key] = float(dim_str)

                                except ValueError:

                                    print(f"Invalid value for {key}, skipping")

                        

                        if dims:

                            if self.editor.update_primitive(node_name, dimensions=dims):

                                print("Updated successfully")

                            else:

                                print("Failed to update dimensions")

                    except ValueError as e:

                        print(f"Invalid input: {e}")

                

                elif edit_cmd == "change":

                    if not self.editor.selected_node:

                        print("No node selected. Use 'select <node_name>' first.")

                        continue

                    

                    if not edit_args:

                        print("Usage: change <operation>")

                        print("Valid operations: union, intersection, difference")

                        continue

                    

                    try:

                        new_op = OperationType(edit_args.lower())

                        if self.editor.change_operation(self.editor.selected_node.name, new_op):

                            print(f"Operation changed to {edit_args}")

                        else:

                            print("Failed to change operation (node may be a primitive)")

                    except ValueError:

                        print(f"Invalid operation: {edit_args}")

                        print("Valid operations: union, intersection, difference")

                

                elif edit_cmd == "delete":

                    if not edit_args:

                        print("Usage: delete <node_name>")

                        continue

                    

                    confirm = input(f"Delete subtree '{edit_args}'? (y/n): ")

                    if confirm.lower() == 'y':

                        if self.editor.delete_subtree(edit_args):

                            print(f"Deleted subtree: {edit_args}")

                            structure = self.editor.get_tree_structure()

                            if structure:

                                print("\nUpdated tree structure:")

                                for item in structure:

                                    print(item["display"])

                            else:

                                print("Tree is now empty")

                        else:

                            print(f"Failed to delete: {edit_args}")

                

                else:

                    print(f"Unknown edit command: {edit_cmd}")

            

            except KeyboardInterrupt:

                print("\nReturning to main menu...")

                break

            except EOFError:

                print("\nReturning to main menu...")

                break

            except Exception as e:

                print(f"Error: {e}")

    

    def cmd_render(self, args: str):

        if not self.current_tree:

            print("No tree loaded. Use 'create' or 'load' first.")

            return

        

        output_file = args.strip() if args.strip() else "output.png"

        

        print(f"Rendering {self.dimension}D scene...")

        

        try:

            if self.dimension == 2:

                img = self.renderer_2d.render(self.current_tree)

            else:

                img = self.renderer_3d.render(self.current_tree)

            

            img.save(output_file)

            print(f"Rendered to {output_file}")

        except Exception as e:

            print(f"Rendering failed: {e}")

    

    def cmd_save(self, args: str):

        if not self.current_tree:

            print("No tree loaded. Use 'create' or 'load' first.")

            return

        

        filepath = args.strip() if args.strip() else "tree.json"

        

        try:

            if self.current_tree.save(filepath):

                print(f"Tree saved to {filepath}")

            else:

                print("Failed to save tree")

        except Exception as e:

            print(f"Error saving tree: {e}")

    

    def cmd_load(self, args: str):

        if not args or not args.strip():

            print("Usage: load <filepath>")

            return

        

        filepath = args.strip()

        

        if not os.path.exists(filepath):

            print(f"File not found: {filepath}")

            return

        

        try:

            tree = CSGTree.load(filepath)

            if tree:

                if tree.dimension != self.dimension:

                    print(f"Warning: Tree dimension ({tree.dimension}D) does not match current mode ({self.dimension}D)")

                    response = input("Continue anyway? (y/n): ")

                    if response.lower() != 'y':

                        return

                    self.dimension = tree.dimension

                

                self.current_tree = tree

                self.editor = CSGTreeEditor(tree)

                print(f"Tree loaded from {filepath}")

                self.cmd_show("")

            else:

                print(f"Failed to load tree from {filepath}")

        except Exception as e:

            print(f"Error loading tree: {e}")

    

    def cmd_show(self, args: str):

        if not self.current_tree:

            print("No tree loaded. Use 'create' or 'load' first.")

            return

        

        try:

            print("\nCurrent Tree Structure:")

            print("-" * 40)

            structure = self.editor.get_tree_structure()

            if structure:

                for item in structure:

                    print(item["display"])

            else:

                print("(empty tree)")

            print("-" * 40)

            

            valid, msg = self.current_tree.validate()

            if valid:

                print("Tree is valid")

            else:

                print(f"Tree validation failed: {msg}")

            

            if self.current_tree.bounds:

                min_coords, max_coords = self.current_tree.bounds

                print(f"Bounds: min={min_coords}, max={max_coords}")

        except Exception as e:

            print(f"Error displaying tree: {e}")

    

    def cmd_help(self, args: str):

        print("\nAvailable Commands:")

        print("-" * 60)

        print("create <description>  - Create CSG tree from natural language")

        print("edit                  - Enter interactive editing mode")

        print("render [output.png]   - Render current tree to image")

        print("save [filepath]       - Save tree to JSON file")

        print("load <filepath>       - Load tree from JSON file")

        print("show                  - Display current tree structure")

        print("help                  - Show this help message")

        print("quit                  - Exit the program")

        print("-" * 60)

        print("\nExamples:")

        print("  create a sphere on top of a box")

        print("  create two overlapping circles")

        print("  create a house with windows and a door")

        print("  render scene.png")

        print("  save my_scene.json")

    

    def cmd_quit(self, args: str):

        print("Goodbye!")

        sys.exit(0)


The CSGInterface class provides a command-driven interface with contextual help and error handling. Each command method validates input, performs the requested operation, and provides clear feedback. The edit command enters a sub-mode with its own command loop, allowing focused tree manipulation without cluttering the main interface. We added comprehensive input validation, better error messages, confirmation for destructive operations, file existence checks, handling for EOF and keyboard interrupts, bounds display in the show command, and special handling for list-type dimensions like polygon vertices.


The interface maintains state including the current tree and editor instance. This allows commands to operate on the active tree without requiring users to specify it repeatedly. The show command provides visual feedback about tree structure using indentation to represent hierarchy, and now also displays the bounding box of the geometry.


Complete Running Example

Now we present the complete, production-ready implementation that integrates all components into a working system with all corrections applied.


#!/usr/bin/env python3

"""

CSG System with LLM Integration

Complete implementation for creating, editing, and rendering CSG trees

Supports both 2D and 3D geometry with multiple LLM backends

"""


import numpy as np

from PIL import Image, ImageDraw

import json

import datetime

import platform

import subprocess

import requests

from requests.adapters import HTTPAdapter

try:

    from urllib3.util.retry import Retry

except ImportError:

    from requests.packages.urllib3.util.retry import Retry

from enum import Enum

from typing import List, Tuple, Optional, Dict, Any

from abc import ABC, abstractmethod

import logging

import sys

import os

import hashlib

import time

import signal

import gc


# Configure logging

logging.basicConfig(

    level=logging.INFO, 

    format='%(asctime)s - %(levelname)s - %(message)s',

    handlers=[

        logging.StreamHandler(sys.stdout)

    ]

)

logger = logging.getLogger(__name__)



# ============================================================================

# Core Data Structures

# ============================================================================


class ShapeType(Enum):

    """Enumeration of supported primitive shapes"""

    RECTANGLE = "rectangle"

    CIRCLE = "circle"

    POLYGON = "polygon"

    BOX = "box"

    SPHERE = "sphere"

    CYLINDER = "cylinder"

    CONE = "cone"



class OperationType(Enum):

    """Enumeration of CSG boolean operations"""

    UNION = "union"

    INTERSECTION = "intersection"

    DIFFERENCE = "difference"



class Primitive:

    """Represents a geometric primitive with position, dimensions, and orientation"""

    

    def __init__(self, shape_type: ShapeType, position: np.ndarray,

                 dimensions: Dict[str, Any], rotation: Optional[np.ndarray] = None,

                 name: Optional[str] = None):

        self.shape_type = shape_type

        self.position = np.array(position, dtype=np.float64)

        

        # Validate and copy dimensions

        self.dimensions = {}

        for key, value in dimensions.items():

            if not isinstance(value, (int, float, list)):

                raise ValueError(f"Dimension {key} must be numeric or list, got {type(value)}")

            if isinstance(value, (int, float)):

                if value <= 0:

                    raise ValueError(f"Dimension {key} must be positive, got {value}")

                self.dimensions[key] = float(value)

            else:

                self.dimensions[key] = value

        

        # Validate polygon vertices if present

        if "vertices" in self.dimensions:

            vertices = np.array(self.dimensions["vertices"])

            if len(vertices) < 3:

                raise ValueError("Polygon must have at least 3 vertices")

            self.dimensions["vertices"] = vertices.tolist()

        

        # Ensure rotation is always a 3D vector with valid values

        if rotation is not None:

            self.rotation = np.array(rotation, dtype=np.float64)

            if len(self.rotation) < 3:

                self.rotation = np.pad(self.rotation, (0, 3 - len(self.rotation)))

            # Normalize rotation angles to [-Ï€, Ï€]

            self.rotation = np.arctan2(np.sin(self.rotation), np.cos(self.rotation))

        else:

            self.rotation = np.zeros(3, dtype=np.float64)

        

        # Generate unique name based on content hash if not provided

        if name:

            self.name = name

        else:

            hash_input = f"{shape_type.value}_{time.time_ns()}_{id(self)}"

            hash_digest = hashlib.md5(hash_input.encode()).hexdigest()[:8]

            self.name = f"{shape_type.value}_{hash_digest}"

        

        logger.debug(f"Created primitive: {self.name} of type {shape_type.value}")

    

    def to_dict(self) -> Dict[str, Any]:

        """Serialize primitive to dictionary for JSON storage"""

        return {

            "type": "primitive",

            "shape_type": self.shape_type.value,

            "position": self.position.tolist(),

            "dimensions": self.dimensions,

            "rotation": self.rotation.tolist(),

            "name": self.name

        }

    

    @staticmethod

    def from_dict(data: Dict[str, Any]) -> 'Primitive':

        """Deserialize primitive from dictionary"""

        # Validate data types in dimensions

        dimensions = {}

        for key, value in data["dimensions"].items():

            if isinstance(value, list):

                dimensions[key] = value

            elif isinstance(value, (int, float)):

                dimensions[key] = float(value)

            else:

                raise ValueError(f"Invalid dimension type for {key}: {type(value)}")

        

        return Primitive(

            shape_type=ShapeType(data["shape_type"]),

            position=np.array(data["position"], dtype=np.float64),

            dimensions=dimensions,

            rotation=np.array(data["rotation"], dtype=np.float64),

            name=data["name"]

        )



class CSGNode:

    """Binary tree node representing either a primitive or a boolean operation"""

    

    def __init__(self, node_type: str, primitive: Optional[Primitive] = None,

                 operation: Optional[OperationType] = None, left: Optional['CSGNode'] = None,

                 right: Optional['CSGNode'] = None, name: Optional[str] = None):

        if node_type not in ["primitive", "operation"]:

            raise ValueError(f"Invalid node_type: {node_type}")

        

        self.node_type = node_type

        self.primitive = primitive

        self.operation = operation

        self.left = left

        self.right = right

        

        # Generate unique name if not provided

        if name:

            self.name = name

        else:

            hash_input = f"{node_type}_{time.time_ns()}_{id(self)}"

            hash_digest = hashlib.md5(hash_input.encode()).hexdigest()[:8]

            self.name = f"node_{hash_digest}"

        

        # Validation

        if self.node_type == "primitive" and self.primitive is None:

            raise ValueError("Primitive node must have a primitive")

        if self.node_type == "operation":

            if self.operation is None:

                raise ValueError("Operation node must have an operation")

            if self.left is None or self.right is None:

                raise ValueError("Operation node must have both children")

        

        logger.debug(f"Created CSG node: {self.name} of type {node_type}")

    

    def is_leaf(self) -> bool:

        """Check if this node is a leaf (primitive) node"""

        return self.node_type == "primitive"

    

    def to_dict(self) -> Dict[str, Any]:

        """Serialize node to dictionary for JSON storage"""

        if self.is_leaf():

            return {

                "node_type": "primitive",

                "primitive": self.primitive.to_dict(),

                "name": self.name

            }

        else:

            return {

                "node_type": "operation",

                "operation": self.operation.value,

                "left": self.left.to_dict(),

                "right": self.right.to_dict(),

                "name": self.name

            }

    

    @staticmethod

    def from_dict(data: Dict[str, Any]) -> 'CSGNode':

        """Deserialize node from dictionary"""

        if data["node_type"] == "primitive":

            return CSGNode(

                node_type="primitive",

                primitive=Primitive.from_dict(data["primitive"]),

                name=data["name"]

            )

        else:

            return CSGNode(

                node_type="operation",

                operation=OperationType(data["operation"]),

                left=CSGNode.from_dict(data["left"]),

                right=CSGNode.from_dict(data["right"]),

                name=data["name"]

            )



class CSGTree:

    """Container for CSG tree with validation and persistence"""

    

    MAX_TREE_DEPTH = 1000

    

    def __init__(self, root: Optional[CSGNode] = None, dimension: int = 3):

        if dimension not in [2, 3]:

            raise ValueError(f"Dimension must be 2 or 3, got {dimension}")

        

        self.root = root

        self.dimension = dimension

        self.metadata = {

            "created": datetime.datetime.now().isoformat(),

            "modified": datetime.datetime.now().isoformat(),

            "description": ""

        }

        

        # Calculate bounding box if tree exists

        self.bounds: Optional[Tuple[np.ndarray, np.ndarray]] = None

        if self.root:

            self._calculate_bounds()

        

        logger.info(f"Created {dimension}D CSG tree")

    

    def _calculate_bounds(self) -> None:

        """Calculate axis-aligned bounding box of the tree"""

        min_coords = np.full(self.dimension, float('inf'))

        max_coords = np.full(self.dimension, float('-inf'))

        

        def update_bounds(node: CSGNode) -> None:

            if node.is_leaf():

                pos = node.primitive.position[:self.dimension]

                

                # Estimate bounds based on primitive type with rotation consideration

                # For simplicity, use maximum extent in all directions

                if node.primitive.shape_type == ShapeType.RECTANGLE:

                    w = node.primitive.dimensions.get("width", 1.0)

                    h = node.primitive.dimensions.get("height", 1.0)

                    # Account for rotation by using diagonal

                    extent_val = max(w, h) * 1.5

                    extent = np.array([extent_val, extent_val])

                elif node.primitive.shape_type == ShapeType.CIRCLE:

                    r = node.primitive.dimensions.get("radius", 1.0)

                    extent = np.array([r, r])

                elif node.primitive.shape_type == ShapeType.BOX:

                    w = node.primitive.dimensions.get("width", 1.0)

                    h = node.primitive.dimensions.get("height", 1.0)

                    d = node.primitive.dimensions.get("depth", 1.0)

                    # Account for rotation by using diagonal

                    extent_val = max(w, h, d) * 1.5

                    extent = np.array([extent_val, extent_val, extent_val])

                elif node.primitive.shape_type == ShapeType.SPHERE:

                    r = node.primitive.dimensions.get("radius", 1.0)

                    extent = np.array([r, r, r])

                elif node.primitive.shape_type == ShapeType.CYLINDER:

                    r = node.primitive.dimensions.get("radius", 1.0)

                    h = node.primitive.dimensions.get("height", 1.0)

                    extent_val = max(r, h/2) * 1.5

                    extent = np.array([extent_val, extent_val, extent_val])

                elif node.primitive.shape_type == ShapeType.CONE:

                    r = node.primitive.dimensions.get("radius", 1.0)

                    h = node.primitive.dimensions.get("height", 1.0)

                    extent_val = max(r, h/2) * 1.5

                    extent = np.array([extent_val, extent_val, extent_val])

                else:

                    extent = np.ones(self.dimension)

                

                nonlocal min_coords, max_coords

                min_coords = np.minimum(min_coords, pos - extent[:self.dimension])

                max_coords = np.maximum(max_coords, pos + extent[:self.dimension])

            else:

                update_bounds(node.left)

                update_bounds(node.right)

        

        try:

            update_bounds(self.root)

            self.bounds = (min_coords, max_coords)

        except Exception as e:

            logger.warning(f"Could not calculate bounds: {e}")

            self.bounds = None

    

    def validate(self) -> Tuple[bool, str]:

        """Validate tree structure and dimensional consistency"""

        if self.root is None:

            return False, "Tree is empty"

        

        def validate_node(node: CSGNode, depth: int = 0) -> Tuple[bool, str]:

            if depth > self.MAX_TREE_DEPTH:

                return False, f"Tree depth exceeds maximum ({self.MAX_TREE_DEPTH}, possible cycle)"

            

            if node.is_leaf():

                if self.dimension == 2:

                    valid_types = {ShapeType.RECTANGLE, ShapeType.CIRCLE, ShapeType.POLYGON}

                else:

                    valid_types = {ShapeType.BOX, ShapeType.SPHERE, ShapeType.CYLINDER, ShapeType.CONE}

                

                if node.primitive.shape_type not in valid_types:

                    return False, f"Invalid shape type {node.primitive.shape_type.value} for {self.dimension}D tree"

                

                # Validate position dimensions

                expected_dims = self.dimension

                actual_dims = len(node.primitive.position)

                if actual_dims < expected_dims:

                    return False, f"Position has {actual_dims} dimensions, expected at least {expected_dims}"

                

                # Validate dimensions dictionary

                if not node.primitive.dimensions:

                    return False, f"Primitive {node.name} has empty dimensions"

                

                # Validate required dimensions for each shape type

                if node.primitive.shape_type == ShapeType.RECTANGLE:

                    if "width" not in node.primitive.dimensions or "height" not in node.primitive.dimensions:

                        return False, f"Rectangle {node.name} missing width or height"

                elif node.primitive.shape_type == ShapeType.CIRCLE:

                    if "radius" not in node.primitive.dimensions:

                        return False, f"Circle {node.name} missing radius"

                elif node.primitive.shape_type == ShapeType.BOX:

                    if not all(k in node.primitive.dimensions for k in ["width", "height", "depth"]):

                        return False, f"Box {node.name} missing dimensions"

                elif node.primitive.shape_type == ShapeType.SPHERE:

                    if "radius" not in node.primitive.dimensions:

                        return False, f"Sphere {node.name} missing radius"

                elif node.primitive.shape_type == ShapeType.CYLINDER:

                    if "radius" not in node.primitive.dimensions or "height" not in node.primitive.dimensions:

                        return False, f"Cylinder {node.name} missing dimensions"

                elif node.primitive.shape_type == ShapeType.CONE:

                    if "radius" not in node.primitive.dimensions or "height" not in node.primitive.dimensions:

                        return False, f"Cone {node.name} missing dimensions"

                

                return True, "Valid"

            else:

                if node.left is None or node.right is None:

                    return False, f"Operation node {node.name} missing children"

                

                left_valid, left_msg = validate_node(node.left, depth + 1)

                if not left_valid:

                    return False, left_msg

                

                right_valid, right_msg = validate_node(node.right, depth + 1)

                if not right_valid:

                    return False, right_msg

                

                return True, "Valid"

        

        return validate_node(self.root)

    

    def save(self, filepath: str) -> bool:

        """Save tree to JSON file"""

        try:

            self.metadata["modified"] = datetime.datetime.now().isoformat()

            if self.metadata.get("created") is None:

                self.metadata["created"] = self.metadata["modified"]

            

            data = {

                "version": "1.0",

                "dimension": self.dimension,

                "metadata": self.metadata,

                "root": self.root.to_dict() if self.root else None

            }

            

            with open(filepath, 'w', encoding='utf-8') as f:

                json.dump(data, f, indent=2, ensure_ascii=False)

            

            logger.info(f"Saved tree to {filepath}")

            return True

        except Exception as e:

            logger.error(f"Error saving tree: {e}")

            return False

    

    @staticmethod

    def load(filepath: str) -> Optional['CSGTree']:

        """Load tree from JSON file"""

        try:

            with open(filepath, 'r', encoding='utf-8') as f:

                data = json.load(f)

            

            if "dimension" not in data:

                logger.error("Invalid file format: missing dimension")

                return None

            

            # Check version compatibility

            version = data.get("version", "1.0")

            if version != "1.0":

                logger.warning(f"File version {version} may not be fully compatible")

            

            tree = CSGTree(dimension=data["dimension"])

            tree.metadata = data.get("metadata", tree.metadata)

            if data.get("root"):

                tree.root = CSGNode.from_dict(data["root"])

                tree._calculate_bounds()

            

            logger.info(f"Loaded tree from {filepath}")

            return tree

        except FileNotFoundError:

            logger.error(f"File not found: {filepath}")

            return None

        except json.JSONDecodeError as e:

            logger.error(f"Invalid JSON in file: {e}")

            return None

        except Exception as e:

            logger.error(f"Error loading tree: {e}")

            return None



# ============================================================================

# LLM Backend Infrastructure

# ============================================================================


class LLMBackend(ABC):

    """Abstract base class for LLM backends"""

    

    @abstractmethod

    def generate(self, prompt: str, max_tokens: int = 2048,

                 temperature: float = 0.7) -> str:

        """Generate text from a single prompt"""

        pass

    

    @abstractmethod

    def generate_with_history(self, messages: List[Dict[str, str]],

                               max_tokens: int = 2048,

                               temperature: float = 0.7) -> str:

        """Generate text with conversation history"""

        pass

    

    @abstractmethod

    def cleanup(self) -> None:

        """Release resources"""

        pass

    

    @abstractmethod

    def is_available(self) -> bool:

        """Check if backend is available"""

        pass



class HardwareDetector:

    """Detect available GPU hardware for local LLM execution"""

    

    @staticmethod

    def detect_gpu() -> str:

        """Detect GPU type and return appropriate device string"""

        system = platform.system()

        

        # Check for NVIDIA CUDA

        try:

            result = subprocess.run(

                ['nvidia-smi'], 

                capture_output=True, 

                check=True,

                timeout=5

            )

            logger.info("Detected NVIDIA CUDA GPU")

            return "cuda"

        except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):

            pass

        

        # Check for AMD ROCm

        try:

            result = subprocess.run(

                ['rocm-smi'], 

                capture_output=True, 

                check=True,

                timeout=5

            )

            logger.info("Detected AMD ROCm GPU")

            return "rocm"

        except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):

            pass

        

        # Check for Apple Silicon

        if system == "Darwin":

            try:

                result = subprocess.run(

                    ['sysctl', '-n', 'machdep.cpu.brand_string'],

                    capture_output=True, 

                    text=True,

                    timeout=5

                )

                if 'Apple' in result.stdout:

                    logger.info("Detected Apple Silicon (MPS)")

                    return "mps"

            except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):

                pass

        

        # Check for Intel GPU

        if system == "Linux":

            try:

                with open('/proc/cpuinfo', 'r') as f:

                    content = f.read()

                    if 'Intel' in content:

                        try:

                            import intel_extension_for_pytorch

                            logger.info("Detected Intel GPU")

                            return "intel"

                        except ImportError:

                            pass

            except (IOError, OSError):

                pass

        

        logger.info("No GPU detected, using CPU")

        return "cpu"



class LocalLLMBackend(LLMBackend):

    """Local LLM backend using transformers library with multi-GPU support"""

    

    def __init__(self, model_name: str = "gpt2", device: Optional[str] = None):

        self.model_name = model_name

        self.device = device or self._detect_device()

        self.model = None

        self.tokenizer = None

        self.generation_timeout = 300  # 5 minutes

        self._initialize()

    

    def _detect_device(self) -> str:

        """Detect and return appropriate device string"""

        gpu_type = HardwareDetector.detect_gpu()

        

        if gpu_type == "cuda":

            return "cuda"

        elif gpu_type == "rocm":

            return "cuda"

        elif gpu_type == "mps":

            return "mps"

        elif gpu_type == "intel":

            return "xpu"

        else:

            return "cpu"

    

    def _initialize(self) -> None:

        """Initialize model and tokenizer with appropriate device settings"""

        try:

            from transformers import AutoModelForCausalLM, AutoTokenizer

            import torch

            

            logger.info(f"Initializing local LLM: {self.model_name} on {self.device}")

            

            self.tokenizer = AutoTokenizer.from_pretrained(

                self.model_name,

                trust_remote_code=False

            )

            

            if self.device == "cuda":

                if not torch.cuda.is_available():

                    logger.warning("CUDA requested but not available, falling back to CPU")

                    self.device = "cpu"

                    self.model = AutoModelForCausalLM.from_pretrained(

                        self.model_name,

                        trust_remote_code=False

                    )

                else:

                    self.model = AutoModelForCausalLM.from_pretrained(

                        self.model_name,

                        torch_dtype=torch.float16,

                        device_map="auto",

                        trust_remote_code=False

                    )

            elif self.device == "mps":

                if not torch.backends.mps.is_available():

                    logger.warning("MPS requested but not available, falling back to CPU")

                    self.device = "cpu"

                    self.model = AutoModelForCausalLM.from_pretrained(

                        self.model_name,

                        trust_remote_code=False

                    )

                else:

                    self.model = AutoModelForCausalLM.from_pretrained(

                        self.model_name,

                        torch_dtype=torch.float16,

                        trust_remote_code=False

                    )

                    self.model = self.model.to("mps")

            elif self.device == "xpu":

                try:

                    import intel_extension_for_pytorch as ipex

                    self.model = AutoModelForCausalLM.from_pretrained(

                        self.model_name,

                        trust_remote_code=False

                    )

                    self.model = self.model.to("xpu")

                    self.model = ipex.optimize(self.model)

                except ImportError:

                    logger.warning("Intel extension not available, falling back to CPU")

                    self.device = "cpu"

                    self.model = AutoModelForCausalLM.from_pretrained(

                        self.model_name,

                        trust_remote_code=False

                    )

            else:

                self.model = AutoModelForCausalLM.from_pretrained(

                    self.model_name,

                    trust_remote_code=False

                )

            

            if self.tokenizer.pad_token is None:

                self.tokenizer.pad_token = self.tokenizer.eos_token

            

            logger.info("Local LLM initialized successfully")

        except Exception as e:

            logger.error(f"Error initializing local LLM: {e}")

            raise

    

    def generate(self, prompt: str, max_tokens: int = 2048,

                 temperature: float = 0.7) -> str:

        """Generate text from prompt"""

        if not self.is_available():

            return ""

        

        try:

            import torch

            

            # Custom timeout exception to avoid shadowing built-in

            class GenerationTimeout(Exception):

                pass

            

            def timeout_handler(signum, frame):

                raise GenerationTimeout("Generation timeout")

            

            inputs = self.tokenizer(

                prompt, 

                return_tensors="pt",

                padding=True, 

                truncation=True, 

                max_length=2048

            )

            

            inputs = {k: v.to(self.model.device) for k, v in inputs.items()}

            

            # Set timeout for Unix-like systems only

            old_handler = None

            if hasattr(signal, 'SIGALRM'):

                old_handler = signal.signal(signal.SIGALRM, timeout_handler)

                signal.alarm(self.generation_timeout)

            

            try:

                with torch.no_grad():

                    outputs = self.model.generate(

                        **inputs,

                        max_new_tokens=min(max_tokens, 2048),

                        temperature=max(0.1, min(temperature, 2.0)),

                        do_sample=temperature > 0,

                        pad_token_id=self.tokenizer.pad_token_id,

                        eos_token_id=self.tokenizer.eos_token_id

                    )

                

                generated_text = self.tokenizer.decode(

                    outputs[0], 

                    skip_special_tokens=True

                )

                

                if generated_text.startswith(prompt):

                    generated_text = generated_text[len(prompt):].strip()

                

                return generated_text

            finally:

                if hasattr(signal, 'SIGALRM') and old_handler is not None:

                    signal.alarm(0)

                    signal.signal(signal.SIGALRM, old_handler)

                

        except Exception as e:

            if "GenerationTimeout" in str(type(e).__name__):

                logger.error("Generation timed out")

            else:

                logger.error(f"Error generating text: {e}")

            return ""

    

    def generate_with_history(self, messages: List[Dict[str, str]],

                               max_tokens: int = 2048,

                               temperature: float = 0.7) -> str:

        """Generate text with conversation history"""

        prompt = ""

        for msg in messages:

            role = msg.get("role", "user")

            content = msg.get("content", "")

            prompt += f"{role}: {content}\n"

        

        prompt += "assistant: "

        return self.generate(prompt, max_tokens, temperature)

    

    def cleanup(self) -> None:

        """Release model resources"""

        try:

            import torch

            

            logger.info("Cleaning up local LLM resources")

            

            if self.model is not None:

                del self.model

                self.model = None

            if self.tokenizer is not None:

                del self.tokenizer

                self.tokenizer = None

            

            if torch.cuda.is_available():

                torch.cuda.empty_cache()

            

            gc.collect()

        except Exception as e:

            logger.error(f"Error during cleanup: {e}")

    

    def is_available(self) -> bool:

        """Check if backend is ready"""

        return self.model is not None and self.tokenizer is not None



class RemoteLLMBackend(LLMBackend):

    """Remote LLM backend using HTTP API"""

    

    def __init__(self, api_url: str, api_key: Optional[str] = None,

                 model_name: str = "gpt-3.5-turbo"):

        if not api_url:

            raise ValueError("API URL cannot be empty")

        

        self.api_url = api_url.rstrip('/')

        self.api_key = api_key

        self.model_name = model_name

        self.session = requests.Session()

        

        # Configure retry strategy

        retry_strategy = Retry(

            total=3,

            backoff_factor=1,

            status_forcelist=[429, 500, 502, 503, 504],

        )

        adapter = HTTPAdapter(max_retries=retry_strategy)

        self.session.mount("http://", adapter)

        self.session.mount("https://", adapter)

        

        if api_key:

            self.session.headers.update({

                "Authorization": f"Bearer {api_key}",

                "Content-Type": "application/json"

            })

        else:

            self.session.headers.update({

                "Content-Type": "application/json"

            })

        

        logger.info(f"Initialized remote LLM backend: {api_url}")

    

    def generate(self, prompt: str, max_tokens: int = 2048,

                 temperature: float = 0.7) -> str:

        """Generate text from prompt via API"""

        try:

            response = self.session.post(

                f"{self.api_url}/completions",

                json={

                    "model": self.model_name,

                    "prompt": prompt,

                    "max_tokens": min(max_tokens, 4096),

                    "temperature": max(0.0, min(temperature, 2.0))

                },

                timeout=60

            )

            response.raise_for_status()

            data = response.json()

            

            if "choices" in data and len(data["choices"]) > 0:

                return data["choices"][0].get("text", "")

            return ""

        except requests.exceptions.Timeout:

            logger.error("Request timed out")

            return ""

        except requests.exceptions.RequestException as e:

            logger.error(f"Error calling remote LLM: {e}")

            return ""

        except (KeyError, IndexError, ValueError) as e:

            logger.error(f"Error parsing response: {e}")

            return ""

    

    def generate_with_history(self, messages: List[Dict[str, str]],

                               max_tokens: int = 2048,

                               temperature: float = 0.7) -> str:

        """Generate text with conversation history via API"""

        try:

            response = self.session.post(

                f"{self.api_url}/chat/completions",

                json={

                    "model": self.model_name,

                    "messages": messages,

                    "max_tokens": min(max_tokens, 4096),

                    "temperature": max(0.0, min(temperature, 2.0))

                },

                timeout=60

            )

            response.raise_for_status()

            data = response.json()

            

            if "choices" in data and len(data["choices"]) > 0:

                message = data["choices"][0].get("message", {})

                return message.get("content", "")

            return ""

        except requests.exceptions.Timeout:

            logger.error("Request timed out")

            return ""

        except requests.exceptions.RequestException as e:

            logger.error(f"Error calling remote LLM: {e}")

            return ""

        except (KeyError, IndexError, ValueError) as e:

            logger.error(f"Error parsing response: {e}")

            return ""

    

    def cleanup(self) -> None:

        """Close session"""

        try:

            logger.info("Closing remote LLM session")

            self.session.close()

        except Exception as e:

            logger.error(f"Error closing session: {e}")

    

    def is_available(self) -> bool:

        """Check if API is reachable"""

        try:

            response = self.session.get(

                f"{self.api_url}/models", 

                timeout=5

            )

            return response.status_code == 200

        except:

            return False



# ============================================================================

# Natural Language to CSG Translation

# ============================================================================


class CSGTranslator:

    """Translates natural language descriptions to CSG trees using LLM"""

    

    def __init__(self, llm_backend: LLMBackend, dimension: int = 3):

        if dimension not in [2, 3]:

            raise ValueError(f"Dimension must be 2 or 3, got {dimension}")

        

        self.llm = llm_backend

        self.dimension = dimension

        self.conversation_history: List[Dict[str, str]] = []

        

        logger.info(f"Initialized CSG translator for {dimension}D")

    

    def _build_system_prompt(self) -> str:

        """Build comprehensive system prompt for LLM"""

        dim_str = "2D" if self.dimension == 2 else "3D"

        

        if self.dimension == 2:

            primitives = "rectangles, circles, and polygons"

            example_shape = "rectangle"

            example_dims = '{"width": 10, "height": 5}'

            example_pos = "[0, 0]"

        else:

            primitives = "boxes, spheres, cylinders, and cones"

            example_shape = "box"

            example_dims = '{"width": 10, "height": 5, "depth": 3}'

            example_pos = "[0, 0, 0]"

        

        prompt = f"""You are a {dim_str} geometry expert that converts natural language descriptions into Constructive Solid Geometry (CSG) trees.


Available primitives: {primitives}

Available operations: union, intersection, difference


Your output must be valid JSON representing a CSG tree. Each node is either:

1. A primitive with shape_type, position, dimensions, rotation, and name

2. An operation with operation type, left child, and right child


Example for a single {example_shape}:

{{

  "node_type": "primitive",

  "primitive": {{

    "type": "primitive",

    "shape_type": "{example_shape}",

    "position": {example_pos},

    "dimensions": {example_dims},

    "rotation": [0, 0, 0],

    "name": "{example_shape}_1"

  }},

  "name": "root"

}}


For multiple objects, use operations:

{{

  "node_type": "operation",

  "operation": "union",

  "left": {{...primitive or operation...}},

  "right": {{...primitive or operation...}},

  "name": "union_1"

}}


Guidelines:

- Use "union" to combine objects

- Use "intersection" for overlapping regions

- Use "difference" to subtract (left - right)

- Provide reasonable positions and dimensions

- All numeric values must be numbers, not strings

- All dimension values must be positive

- Name nodes descriptively

- Output ONLY valid JSON, no explanations


Respond with complete, valid JSON only."""

        

        return prompt

    

    def translate(self, description: str) -> Optional[CSGTree]:

        """Translate natural language description to CSG tree"""

        if not description or not description.strip():

            logger.error("Empty description provided")

            return None

        

        logger.info(f"Translating description: {description}")

        

        system_prompt = self._build_system_prompt()

        

        messages = [

            {"role": "system", "content": system_prompt},

            {"role": "user", "content": f"Create a CSG tree for: {description}"}

        ]

        

        response = self.llm.generate_with_history(

            messages, 

            max_tokens=4096,

            temperature=0.3

        )

        

        if not response:

            logger.error("Empty response from LLM")

            return None

        

        logger.debug(f"LLM response: {response[:500]}...")

        

        json_str = self._extract_json(response)

        if not json_str:

            logger.error("Failed to extract JSON from LLM response")

            logger.debug(f"Response was: {response[:200]}...")

            return None

        

        try:

            tree_data = json.loads(json_str)

            root = CSGNode.from_dict(tree_data)

            tree = CSGTree(root=root, dimension=self.dimension)

            

            valid, msg = tree.validate()

            if not valid:

                logger.error(f"Generated tree is invalid: {msg}")

                return None

            

            logger.info("Successfully created CSG tree from description")

            return tree

        except json.JSONDecodeError as e:

            logger.error(f"Invalid JSON from LLM: {e}")

            return None

        except (KeyError, ValueError, TypeError) as e:

            logger.error(f"Error parsing LLM output: {e}")

            return None

        except Exception as e:

            logger.error(f"Unexpected error: {e}")

            return None

    

    def _extract_json(self, text: str) -> Optional[str]:

        """Extract JSON object from text"""

        if not text:

            return None

        

        start = text.find('{')

        if start == -1:

            return None

        

        brace_count = 0

        for i in range(start, len(text)):

            if text[i] == '{':

                brace_count += 1

            elif text[i] == '}':

                brace_count -= 1

                if brace_count == 0:

                    return text[start:i+1]

        

        return None



# ============================================================================

# Rendering Engines

# ============================================================================


class Renderer2D:

    """2D renderer using rasterization with supersampling"""

    

    def __init__(self, width: int = 800, height: int = 600):

        if width <= 0 or height <= 0:

            raise ValueError("Width and height must be positive")

        

        self.width = width

        self.height = height

        self.scale = 20.0

        self.origin = np.array([width / 2.0, height / 2.0])

        

        logger.info(f"Initialized 2D renderer: {width}x{height}")

    

    def render(self, tree: CSGTree) -> Image.Image:

        """Render CSG tree to 2D image"""

        if tree.dimension != 2:

            raise ValueError("Can only render 2D trees")

        

        if tree.root is None:

            raise ValueError("Cannot render empty tree")

        

        # Check if tree has reasonable bounds

        if tree.bounds is not None:

            min_coords, max_coords = tree.bounds

            extent = np.max(np.abs(max_coords - min_coords))

            if extent > 1000:

                logger.warning(f"Tree has large extent ({extent:.1f}), rendering may be slow")

        

        logger.info("Rendering 2D scene")

        

        img = Image.new('RGB', (self.width, self.height), 'white')

        draw = ImageDraw.Draw(img)

        

        samples = 4

        coverage = np.zeros((self.height * samples, self.width * samples), dtype=np.float32)

        

        try:

            for y in range(self.height * samples):

                for x in range(self.width * samples):

                    world_x = (x / samples - self.width / 2.0) / self.scale

                    world_y = (self.height / 2.0 - y / samples) / self.scale

                    point = np.array([world_x, world_y])

                    

                    try:

                        if self._evaluate_node(tree.root, point):

                            coverage[y, x] = 1.0

                    except Exception:

                        pass

            

            for y in range(self.height):

                for x in range(self.width):

                    sample_sum = np.sum(

                        coverage[y*samples:(y+1)*samples, x*samples:(x+1)*samples]

                    )

                    intensity = int(255 * (1.0 - sample_sum / (samples * samples)))

                    intensity = max(0, min(255, intensity))

                    draw.point((x, y), fill=(intensity, intensity, intensity))

        finally:

            # Explicitly delete large array and force garbage collection

            del coverage

            gc.collect()

        

        logger.info("2D rendering complete")

        return img

    

    def _evaluate_node(self, node: CSGNode, point: np.ndarray) -> bool:

        """Recursively evaluate CSG tree at point"""

        if node.is_leaf():

            return self._point_in_primitive(node.primitive, point)

        else:

            left_result = self._evaluate_node(node.left, point)

            right_result = self._evaluate_node(node.right, point)

            

            if node.operation == OperationType.UNION:

                return left_result or right_result

            elif node.operation == OperationType.INTERSECTION:

                return left_result and right_result

            elif node.operation == OperationType.DIFFERENCE:

                return left_result and not right_result

            

            return False

    

    def _point_in_primitive(self, primitive: Primitive, point: np.ndarray) -> bool:

        """Check if point is inside primitive"""

        relative_pos = point - primitive.position[:2]

        

        if len(primitive.rotation) >= 3 and abs(primitive.rotation[2]) > 1e-6:

            angle = -primitive.rotation[2]

            cos_a, sin_a = np.cos(angle), np.sin(angle)

            rotation_matrix = np.array([[cos_a, -sin_a], [sin_a, cos_a]], dtype=np.float64)

            relative_pos = rotation_matrix @ relative_pos

        

        if primitive.shape_type == ShapeType.RECTANGLE:

            if "width" not in primitive.dimensions or "height" not in primitive.dimensions:

                return False

            half_width = primitive.dimensions["width"] / 2.0

            half_height = primitive.dimensions["height"] / 2.0

            return (abs(relative_pos[0]) <= half_width and

                    abs(relative_pos[1]) <= half_height)

        

        elif primitive.shape_type == ShapeType.CIRCLE:

            if "radius" not in primitive.dimensions:

                return False

            radius = primitive.dimensions["radius"]

            return np.linalg.norm(relative_pos) <= radius

        

        elif primitive.shape_type == ShapeType.POLYGON:

            if "vertices" not in primitive.dimensions:

                return False

            vertices = np.array(primitive.dimensions["vertices"])

            if len(vertices) < 3:

                return False

            return self._point_in_polygon(relative_pos, vertices)

        

        return False

    

    def _point_in_polygon(self, point: np.ndarray, vertices: np.ndarray) -> bool:

        """Ray casting algorithm for polygon containment"""

        n = len(vertices)

        inside = False

        

        p1x, p1y = float(vertices[0][0]), float(vertices[0][1])

        for i in range(1, n + 1):

            p2x, p2y = float(vertices[i % n][0]), float(vertices[i % n][1])

            if point[1] > min(p1y, p2y):

                if point[1] <= max(p1y, p2y):

                    if point[0] <= max(p1x, p2x):

                        if abs(p1y - p2y) > 1e-10:

                            xinters = (point[1] - p1y) * (p2x - p1x) / (p2y - p1y) + p1x

                            if abs(p1x - p2x) < 1e-10 or point[0] <= xinters:

                                inside = not inside

            p1x, p1y = p2x, p2y

        

        return inside



class Renderer3D:

    """3D renderer using ray marching and signed distance functions"""

    

    def __init__(self, width: int = 800, height: int = 600):

        if width <= 0 or height <= 0:

            raise ValueError("Width and height must be positive")

        

        self.width = width

        self.height = height

        self.camera_pos = np.array([0.0, 0.0, -10.0])

        self.camera_target = np.array([0.0, 0.0, 0.0])

        self.camera_up = np.array([0.0, 1.0, 0.0])

        self.fov = 60.0

        

        logger.info(f"Initialized 3D renderer: {width}x{height}")

    

    def render(self, tree: CSGTree) -> Image.Image:

        """Render CSG tree to 3D image using ray marching"""

        if tree.dimension != 3:

            raise ValueError("Can only render 3D trees")

        

        if tree.root is None:

            raise ValueError("Cannot render empty tree")

        

        # Check if tree has reasonable bounds

        if tree.bounds is not None:

            min_coords, max_coords = tree.bounds

            extent = np.max(np.abs(max_coords - min_coords))

            if extent > 1000:

                logger.warning(f"Tree has large extent ({extent:.1f}), rendering may be slow")

        

        logger.info("Rendering 3D scene")

        

        img = Image.new('RGB', (self.width, self.height), 'white')

        pixels = np.zeros((self.height, self.width, 3), dtype=np.uint8)

        pixels.fill(255)

        

        forward = self.camera_target - self.camera_pos

        forward_norm = np.linalg.norm(forward)

        if forward_norm < 1e-6:

            raise ValueError("Camera position and target are too close")

        forward = forward / forward_norm

        

        right = np.cross(forward, self.camera_up)

        right_norm = np.linalg.norm(right)

        if right_norm < 1e-6:

            raise ValueError("Camera up vector is parallel to view direction")

        right = right / right_norm

        

        up = np.cross(right, forward)

        

        aspect_ratio = self.width / self.height

        fov_rad = np.radians(self.fov)

        viewport_height = 2.0 * np.tan(fov_rad / 2.0)

        viewport_width = viewport_height * aspect_ratio

        

        for y in range(self.height):

            if y % 100 == 0:

                logger.debug(f"Rendering row {y}/{self.height}")

            

            for x in range(self.width):

                u = (x / self.width - 0.5) * viewport_width

                v = (0.5 - y / self.height) * viewport_height

                

                ray_dir = forward + u * right + v * up

                ray_dir_norm = np.linalg.norm(ray_dir)

                if ray_dir_norm > 1e-6:

                    ray_dir = ray_dir / ray_dir_norm

                else:

                    continue

                

                try:

                    color = self._ray_march(tree.root, self.camera_pos, ray_dir)

                    pixels[y, x] = color

                except Exception:

                    pixels[y, x] = [255, 255, 255]

        

        logger.info("3D rendering complete")

        return Image.fromarray(pixels, mode='RGB')

    

    def _ray_march(self, node: CSGNode, origin: np.ndarray,

                   direction: np.ndarray, max_steps: int = 100,

                   max_distance: float = 100.0) -> np.ndarray:

        """Ray march through scene to find intersections"""

        t = 0.0

        epsilon = 0.001

        

        for step in range(max_steps):

            pos = origin + t * direction

            dist = self._sdf_node(node, pos)

            

            if not np.isfinite(dist):

                break

            

            if abs(dist) < epsilon:

                try:

                    normal = self._calculate_normal(node, pos)

                    

                    light_dir = np.array([1.0, 1.0, -1.0])

                    light_dir = light_dir / np.linalg.norm(light_dir)

                    

                    diffuse = max(0.0, np.dot(normal, light_dir))

                    ambient = 0.2

                    

                    intensity = ambient + (1.0 - ambient) * diffuse

                    color_value = int(255 * max(0.0, min(1.0, intensity)))

                    

                    return np.array([color_value, color_value, color_value], dtype=np.uint8)

                except Exception:

                    return np.array([128, 128, 128], dtype=np.uint8)

            

            if dist < 0:

                t += epsilon

            else:

                t += max(dist * 0.9, epsilon)

            

            if t > max_distance:

                break

        

        return np.array([255, 255, 255], dtype=np.uint8)

    

    def _sdf_node(self, node: CSGNode, point: np.ndarray) -> float:

        """Evaluate signed distance function at point"""

        if node.is_leaf():

            return self._sdf_primitive(node.primitive, point)

        else:

            left_dist = self._sdf_node(node.left, point)

            right_dist = self._sdf_node(node.right, point)

            

            if not np.isfinite(left_dist) or not np.isfinite(right_dist):

                return float('inf')

            

            if node.operation == OperationType.UNION:

                return min(left_dist, right_dist)

            elif node.operation == OperationType.INTERSECTION:

                return max(left_dist, right_dist)

            elif node.operation == OperationType.DIFFERENCE:

                return max(left_dist, -right_dist)

            

            return left_dist

    

    def _sdf_primitive(self, primitive: Primitive, point: np.ndarray) -> float:

        """Calculate signed distance to primitive"""

        relative_pos = point - primitive.position

        

        if np.any(np.abs(primitive.rotation) > 1e-6):

            rx, ry, rz = -primitive.rotation

            

            # Use numerically stable rotation matrix construction

            cx, sx = np.cos(rx), np.sin(rx)

            cy, sy = np.cos(ry), np.sin(ry)

            cz, sz = np.cos(rz), np.sin(rz)

            

            Rx = np.array([[1, 0, 0],

                           [0, cx, -sx],

                           [0, sx, cx]], dtype=np.float64)

            

            Ry = np.array([[cy, 0, sy],

                           [0, 1, 0],

                           [-sy, 0, cy]], dtype=np.float64)

            

            Rz = np.array([[cz, -sz, 0],

                           [sz, cz, 0],

                           [0, 0, 1]], dtype=np.float64)

            

            rotation_matrix = Rz @ Ry @ Rx

            relative_pos = rotation_matrix @ relative_pos

        

        if primitive.shape_type == ShapeType.BOX:

            if not all(k in primitive.dimensions for k in ["width", "height", "depth"]):

                return float('inf')

            

            half_size = np.array([

                primitive.dimensions["width"] / 2.0,

                primitive.dimensions["height"] / 2.0,

                primitive.dimensions["depth"] / 2.0

            ], dtype=np.float64)

            q = np.abs(relative_pos) - half_size

            return np.linalg.norm(np.maximum(q, 0)) + min(max(q[0], q[1], q[2]), 0.0)

        

        elif primitive.shape_type == ShapeType.SPHERE:

            if "radius" not in primitive.dimensions:

                return float('inf')

            radius = float(primitive.dimensions["radius"])

            return np.linalg.norm(relative_pos) - radius

        

        elif primitive.shape_type == ShapeType.CYLINDER:

            if "radius" not in primitive.dimensions or "height" not in primitive.dimensions:

                return float('inf')

            

            radius = float(primitive.dimensions["radius"])

            height = float(primitive.dimensions["height"])

            

            d = np.array([

                np.linalg.norm(relative_pos[[0, 2]]) - radius,

                abs(relative_pos[1]) - height / 2.0

            ], dtype=np.float64)

            return min(max(d[0], d[1]), 0.0) + np.linalg.norm(np.maximum(d, 0))

        

        elif primitive.shape_type == ShapeType.CONE:

            if "radius" not in primitive.dimensions or "height" not in primitive.dimensions:

                return float('inf')

            

            radius = float(primitive.dimensions["radius"])

            height = float(primitive.dimensions["height"])

            

            # Avoid division by zero

            if abs(height) < 1e-6 or abs(radius) < 1e-6:

                return float('inf')

            

            q = np.array([np.linalg.norm(relative_pos[[0, 2]]), relative_pos[1]], dtype=np.float64)

            k = np.array([radius / height, -1.0], dtype=np.float64)

            k_dot_k = np.dot(k, k)

            

            # Additional safety check

            if abs(k_dot_k) < 1e-10:

                return float('inf')

            

            d1 = q - np.array([0.0, height], dtype=np.float64)

            d2_proj = np.clip(np.dot(q, k) / k_dot_k, 0.0, height)

            d2 = q - d2_proj * k

            

            dist_sq = min(np.dot(d1, d1), np.dot(d2, d2))

            return np.sqrt(max(0.0, dist_sq))

        

        return float('inf')

    

    def _calculate_normal(self, node: CSGNode, point: np.ndarray,

                          epsilon: float = 0.001) -> np.ndarray:

        """Calculate surface normal using numerical gradient"""

        dx = np.array([epsilon, 0, 0], dtype=np.float64)

        dy = np.array([0, epsilon, 0], dtype=np.float64)

        dz = np.array([0, 0, epsilon], dtype=np.float64)

        

        normal = np.array([

            self._sdf_node(node, point + dx) - self._sdf_node(node, point - dx),

            self._sdf_node(node, point + dy) - self._sdf_node(node, point - dy),

            self._sdf_node(node, point + dz) - self._sdf_node(node, point - dz)

        ], dtype=np.float64)

        

        norm = np.linalg.norm(normal)

        if norm > 1e-6:

            return normal / norm

        return np.array([0.0, 1.0, 0.0], dtype=np.float64)



# ============================================================================

# Tree Editing

# ============================================================================


class CSGTreeEditor:

    """Interactive editor for CSG trees"""

    

    def __init__(self, tree: CSGTree):

        if tree is None:

            raise ValueError("Tree cannot be None")

        

        self.tree = tree

        self.selected_node: Optional[CSGNode] = None

        self.node_map: Dict[str, CSGNode] = {}

        self._build_node_map()

        

        logger.info("Initialized CSG tree editor")

    

    def _build_node_map(self) -> None:

        """Build mapping from node names to node objects"""

        self.node_map.clear()

        if self.tree.root:

            self._map_nodes(self.tree.root)

        logger.debug(f"Built node map with {len(self.node_map)} nodes")

    

    def _map_nodes(self, node: CSGNode) -> None:

        """Recursively map all nodes"""

        if node.name in self.node_map:

            base_name = node.name

            counter = 1

            while f"{base_name}_{counter}" in self.node_map:

                counter += 1

            node.name = f"{base_name}_{counter}"

        

        self.node_map[node.name] = node

        if not node.is_leaf():

            self._map_nodes(node.left)

            self._map_nodes(node.right)

    

    def select_node(self, node_name: str) -> bool:

        """Select a node for editing"""

        if not node_name:

            return False

        

        if node_name in self.node_map:

            self.selected_node = self.node_map[node_name]

            logger.info(f"Selected node: {node_name}")

            return True

        logger.warning(f"Node not found: {node_name}")

        return False

    

    def get_node_info(self, node_name: str) -> Optional[Dict[str, Any]]:

        """Get detailed information about a node"""

        if not node_name or node_name not in self.node_map:

            return None

        

        node = self.node_map[node_name]

        info: Dict[str, Any] = {

            "name": node.name,

            "type": node.node_type

        }

        

        if node.is_leaf():

            info["primitive"] = {

                "shape_type": node.primitive.shape_type.value,

                "position": node.primitive.position.tolist(),

                "dimensions": node.primitive.dimensions.copy(),

                "rotation": node.primitive.rotation.tolist()

            }

        else:

            info["operation"] = node.operation.value

            info["left_child"] = node.left.name if node.left else None

            info["right_child"] = node.right.name if node.right else None

        

        return info

    

    def update_primitive(self, node_name: str, position: Optional[np.ndarray] = None,

                         dimensions: Optional[Dict[str, float]] = None,

                         rotation: Optional[np.ndarray] = None) -> bool:

        """Update primitive parameters"""

        if not node_name or node_name not in self.node_map:

            return False

        

        node = self.node_map[node_name]

        if not node.is_leaf():

            return False

        

        try:

            if position is not None:

                pos_array = np.array(position, dtype=np.float64)

                if len(pos_array) >= self.tree.dimension:

                    node.primitive.position = pos_array

                else:

                    return False

            

            if dimensions is not None:

                for key, value in dimensions.items():

                    if not isinstance(value, (int, float)) or value <= 0:

                        return False

                node.primitive.dimensions.update(dimensions)

            

            if rotation is not None:

                rot_array = np.array(rotation, dtype=np.float64)

                if len(rot_array) < 3:

                    rot_array = np.pad(rot_array, (0, 3 - len(rot_array)))

                # Normalize rotation angles to [-Ï€, Ï€]

                rot_array = np.arctan2(np.sin(rot_array), np.cos(rot_array))

                node.primitive.rotation = rot_array

            

            # Recalculate tree bounds after update

            self.tree._calculate_bounds()

            

            logger.info(f"Updated primitive {node_name}")

            return True

        except (ValueError, TypeError) as e:

            logger.error(f"Error updating primitive: {e}")

            return False

    

    def change_operation(self, node_name: str,

                         new_operation: OperationType) -> bool:

        """Change operation type of a node"""

        if not node_name or node_name not in self.node_map:

            return False

        

        node = self.node_map[node_name]

        if node.is_leaf():

            return False

        

        node.operation = new_operation

        logger.info(f"Changed operation for {node_name} to {new_operation.value}")

        return True

    

    def add_primitive(self, parent_name: str, side: str,

                      primitive: Primitive,

                      operation: OperationType = OperationType.UNION) -> bool:

        """Add a new primitive to the tree"""

        if not parent_name or parent_name not in self.node_map:

            return False

        

        if side not in ["left", "right"]:

            return False

        

        if primitive is None:

            return False

        

        try:

            parent = self.node_map[parent_name]

            new_leaf = CSGNode(node_type="primitive", primitive=primitive)

            

            new_op = CSGNode(

                node_type="operation",

                operation=operation,

                left=parent if side == "left" else new_leaf,

                right=new_leaf if side == "left" else parent

            )

            

            if parent == self.tree.root:

                self.tree.root = new_op

            else:

                parent_parent = self._find_parent(self.tree.root, parent)

                if parent_parent:

                    if parent_parent.left == parent:

                        parent_parent.left = new_op

                    else:

                        parent_parent.right = new_op

                else:

                    return False

            

            self._build_node_map()

            self.tree._calculate_bounds()

            logger.info(f"Added primitive {primitive.name} to tree")

            return True

        except Exception as e:

            logger.error(f"Error adding primitive: {e}")

            return False

    

    def delete_subtree(self, node_name: str) -> bool:

        """Delete a subtree"""

        if not node_name or node_name not in self.node_map:

            return False

        

        try:

            node = self.node_map[node_name]

            

            if node == self.tree.root:

                self.tree.root = None

                self._build_node_map()

                self.tree.bounds = None

                logger.info("Deleted entire tree")

                return True

            

            parent = self._find_parent(self.tree.root, node)

            if not parent:

                return False

            

            sibling = parent.right if parent.left == node else parent.left

            

            if parent == self.tree.root:

                self.tree.root = sibling

            else:

                grandparent = self._find_parent(self.tree.root, parent)

                if grandparent:

                    if grandparent.left == parent:

                        grandparent.left = sibling

                    else:

                        grandparent.right = sibling

                else:

                    return False

            

            self._build_node_map()

            self.tree._calculate_bounds()

            logger.info(f"Deleted subtree: {node_name}")

            return True

        except Exception as e:

            logger.error(f"Error deleting subtree: {e}")

            return False

    

    def _find_parent(self, current: CSGNode, target: CSGNode) -> Optional[CSGNode]:

        """Find parent of target node"""

        if current is None or current.is_leaf():

            return None

        

        if current.left == target or current.right == target:

            return current

        

        left_result = self._find_parent(current.left, target)

        if left_result:

            return left_result

        

        return self._find_parent(current.right, target)

    

    def get_tree_structure(self) -> List[Dict[str, Any]]:

        """Get hierarchical tree structure for display"""

        structure: List[Dict[str, Any]] = []

        if self.tree.root:

            self._collect_structure(self.tree.root, structure, 0)

        return structure

    

    def _collect_structure(self, node: CSGNode, structure: List[Dict[str, Any]],

                           depth: int) -> None:

        """Recursively collect tree structure"""

        if depth > 100:

            return

        

        indent = "  " * depth

        

        if node.is_leaf():

            structure.append({

                "name": node.name,

                "display": f"{indent}[P] {node.primitive.shape_type.value}",

                "depth": depth,

                "type": "primitive"

            })

        else:

            structure.append({

                "name": node.name,

                "display": f"{indent}[O] {node.operation.value}",

                "depth": depth,

                "type": "operation"

            })

            if node.left:

                self._collect_structure(node.left, structure, depth + 1)

            if node.right:

                self._collect_structure(node.right, structure, depth + 1)



# ============================================================================

# User Interface

# ============================================================================


class CSGInterface:

    """Command-line interface for CSG system"""

    

    def __init__(self, llm_backend: LLMBackend, dimension: int = 3):

        if dimension not in [2, 3]:

            raise ValueError(f"Dimension must be 2 or 3, got {dimension}")

        

        if llm_backend is None:

            raise ValueError("LLM backend cannot be None")

        

        self.dimension = dimension

        self.translator = CSGTranslator(llm_backend, dimension)

        self.current_tree: Optional[CSGTree] = None

        self.editor: Optional[CSGTreeEditor] = None

        self.renderer_2d = Renderer2D()

        self.renderer_3d = Renderer3D()

        

        self.commands: Dict[str, Any] = {

            "create": self.cmd_create,

            "edit": self.cmd_edit,

            "render": self.cmd_render,

            "save": self.cmd_save,

            "load": self.cmd_load,

            "show": self.cmd_show,

            "help": self.cmd_help,

            "quit": self.cmd_quit,

            "exit": self.cmd_quit

        }

        

        logger.info(f"Initialized CSG interface in {dimension}D mode")

    

    def run(self) -> None:

        """Main interface loop"""

        print("=" * 60)

        print(f"CSG System with LLM Integration - {self.dimension}D Mode")

        print("=" * 60)

        print("Type 'help' for available commands")

        print()

        

        while True:

            try:

                user_input = input("> ").strip()

                if not user_input:

                    continue

                

                parts = user_input.split(maxsplit=1)

                command = parts[0].lower()

                args = parts[1] if len(parts) > 1 else ""

                

                if command in self.commands:

                    try:

                        self.commands[command](args)

                    except Exception as e:

                        logger.error(f"Command error: {e}", exc_info=True)

                        print(f"Error executing command: {e}")

                else:

                    print(f"Unknown command: {command}")

                    print("Type 'help' for available commands")

            

            except KeyboardInterrupt:

                print("\nUse 'quit' to exit")

            except EOFError:

                print("\nExiting...")

                break

            except Exception as e:

                logger.error(f"Unexpected error: {e}", exc_info=True)

                print(f"Error: {e}")

    

    def cmd_create(self, args: str) -> None:

        """Create CSG tree from natural language description"""

        if not args or not args.strip():

            print("Usage: create <description>")

            print("Example: create a house with a triangular roof and two windows")

            return

        

        print(f"Generating CSG tree from description...")

        try:

            tree = self.translator.translate(args)

            

            if tree:

                self.current_tree = tree

                self.editor = CSGTreeEditor(tree)

                print("Tree created successfully!")

                self.cmd_show("")

            else:

                print("Failed to create tree from description")

        except Exception as e:

            logger.error(f"Error creating tree: {e}")

            print(f"Error creating tree: {e}")

    

    def cmd_edit(self, args: str) -> None:

        """Enter interactive editing mode"""

        if not self.current_tree:

            print("No tree loaded. Use 'create' or 'load' first.")

            return

        

        print("\nTree Structure:")

        try:

            structure = self.editor.get_tree_structure()

            for item in structure:

                print(item["display"])

        except Exception as e:

            print(f"Error displaying tree: {e}")

            return

        

        print("\nEdit Commands:")

        print("  select <node_name>  - Select a node for editing")

        print("  update              - Update selected primitive")

        print("  change <operation>  - Change operation type")

        print("  delete <node_name>  - Delete subtree")

        print("  done                - Finish editing")

        

        while True:

            try:

                edit_input = input("edit> ").strip()

                if not edit_input:

                    continue

                

                if edit_input == "done":

                    break

                

                parts = edit_input.split(maxsplit=1)

                edit_cmd = parts[0].lower()

                edit_args = parts[1] if len(parts) > 1 else ""

                

                if edit_cmd == "select":

                    if not edit_args:

                        print("Usage: select <node_name>")

                        continue

                    

                    if self.editor.select_node(edit_args):

                        info = self.editor.get_node_info(edit_args)

                        print(f"Selected: {edit_args}")

                        if info:

                            print(f"Type: {info['type']}")

                            if info['type'] == 'primitive':

                                print(f"Shape: {info['primitive']['shape_type']}")

                                print(f"Position: {info['primitive']['position']}")

                                print(f"Dimensions: {info['primitive']['dimensions']}")

                    else:

                        print(f"Node not found: {edit_args}")

                

                elif edit_cmd == "update":

                    if not self.editor.selected_node:

                        print("No node selected. Use 'select <node_name>' first.")

                        continue

                    

                    if not self.editor.selected_node.is_leaf():

                        print("Can only update primitive nodes")

                        continue

                    

                    print("Enter new values (or press Enter to keep current):")

                    node_name = self.editor.selected_node.name

                    

                    try:

                        pos_str = input(f"Position (current: {self.editor.selected_node.primitive.position.tolist()}): ")

                        if pos_str.strip():

                            pos = np.array([float(x) for x in pos_str.split()])

                            if not self.editor.update_primitive(node_name, position=pos):

                                print("Failed to update position")

                        

                        print("Dimensions:")

                        dims = {}

                        for key, value in self.editor.selected_node.primitive.dimensions.items():

                            if isinstance(value, list):

                                continue  # Skip vertices for now

                            dim_str = input(f"  {key} (current: {value}): ")

                            if dim_str.strip():

                                try:

                                    dims[key] = float(dim_str)

                                except ValueError:

                                    print(f"Invalid value for {key}, skipping")

                        

                        if dims:

                            if self.editor.update_primitive(node_name, dimensions=dims):

                                print("Updated successfully")

                            else:

                                print("Failed to update dimensions")

                    except ValueError as e:

                        print(f"Invalid input: {e}")

                

                elif edit_cmd == "change":

                    if not self.editor.selected_node:

                        print("No node selected. Use 'select <node_name>' first.")

                        continue

                    

                    if not edit_args:

                        print("Usage: change <operation>")

                        print("Valid operations: union, intersection, difference")

                        continue

                    

                    try:

                        new_op = OperationType(edit_args.lower())

                        if self.editor.change_operation(self.editor.selected_node.name, new_op):

                            print(f"Operation changed to {edit_args}")

                        else:

                            print("Failed to change operation (node may be a primitive)")

                    except ValueError:

                        print(f"Invalid operation: {edit_args}")

                        print("Valid operations: union, intersection, difference")

                

                elif edit_cmd == "delete":

                    if not edit_args:

                        print("Usage: delete <node_name>")

                        continue

                    

                    confirm = input(f"Delete subtree '{edit_args}'? (y/n): ")

                    if confirm.lower() == 'y':

                        if self.editor.delete_subtree(edit_args):

                            print(f"Deleted subtree: {edit_args}")

                            structure = self.editor.get_tree_structure()

                            if structure:

                                print("\nUpdated tree structure:")

                                for item in structure:

                                    print(item["display"])

                            else:

                                print("Tree is now empty")

                        else:

                            print(f"Failed to delete: {edit_args}")

                

                else:

                    print(f"Unknown edit command: {edit_cmd}")

            

            except KeyboardInterrupt:

                print("\nReturning to main menu...")

                break

            except EOFError:

                print("\nReturning to main menu...")

                break

            except Exception as e:

                logger.error(f"Edit error: {e}")

                print(f"Error: {e}")

    

    def cmd_render(self, args: str) -> None:

        """Render current tree to image"""

        if not self.current_tree:

            print("No tree loaded. Use 'create' or 'load' first.")

            return

        

        output_file = args.strip() if args.strip() else "output.png"

        

        print(f"Rendering {self.dimension}D scene...")

        

        try:

            if self.dimension == 2:

                img = self.renderer_2d.render(self.current_tree)

            else:

                img = self.renderer_3d.render(self.current_tree)

            

            img.save(output_file)

            print(f"Rendered to {output_file}")

        except Exception as e:

            logger.error(f"Rendering failed: {e}", exc_info=True)

            print(f"Rendering failed: {e}")

    

    def cmd_save(self, args: str) -> None:

        """Save current tree to JSON file"""

        if not self.current_tree:

            print("No tree loaded. Use 'create' or 'load' first.")

            return

        

        filepath = args.strip() if args.strip() else "tree.json"

        

        try:

            if self.current_tree.save(filepath):

                print(f"Tree saved to {filepath}")

            else:

                print("Failed to save tree")

        except Exception as e:

            logger.error(f"Error saving tree: {e}")

            print(f"Error saving tree: {e}")

    

    def cmd_load(self, args: str) -> None:

        """Load tree from JSON file"""

        if not args or not args.strip():

            print("Usage: load <filepath>")

            return

        

        filepath = args.strip()

        

        if not os.path.exists(filepath):

            print(f"File not found: {filepath}")

            return

        

        try:

            tree = CSGTree.load(filepath)

            if tree:

                if tree.dimension != self.dimension:

                    print(f"Warning: Tree dimension ({tree.dimension}D) does not match current mode ({self.dimension}D)")

                    response = input("Continue anyway? (y/n): ")

                    if response.lower() != 'y':

                        return

                    self.dimension = tree.dimension

                

                self.current_tree = tree

                self.editor = CSGTreeEditor(tree)

                print(f"Tree loaded from {filepath}")

                self.cmd_show("")

            else:

                print(f"Failed to load tree from {filepath}")

        except Exception as e:

            logger.error(f"Error loading tree: {e}")

            print(f"Error loading tree: {e}")

    

    def cmd_show(self, args: str) -> None:

        """Display current tree structure"""

        if not self.current_tree:

            print("No tree loaded. Use 'create' or 'load' first.")

            return

        

        try:

            print("\nCurrent Tree Structure:")

            print("-" * 40)

            structure = self.editor.get_tree_structure()

            if structure:

                for item in structure:

                    print(item["display"])

            else:

                print("(empty tree)")

            print("-" * 40)

            

            valid, msg = self.current_tree.validate()

            if valid:

                print("Tree is valid")

            else:

                print(f"Tree validation failed: {msg}")

            

            if self.current_tree.bounds:

                min_coords, max_coords = self.current_tree.bounds

                print(f"Bounds: min={min_coords}, max={max_coords}")

        except Exception as e:

            logger.error(f"Error displaying tree: {e}")

            print(f"Error displaying tree: {e}")

    

    def cmd_help(self, args: str) -> None:

        """Display help information"""

        print("\nAvailable Commands:")

        print("-" * 60)

        print("create <description>  - Create CSG tree from natural language")

        print("edit                  - Enter interactive editing mode")

        print("render [output.png]   - Render current tree to image")

        print("save [filepath]       - Save tree to JSON file")

        print("load <filepath>       - Load tree from JSON file")

        print("show                  - Display current tree structure")

        print("help                  - Show this help message")

        print("quit                  - Exit the program")

        print("-" * 60)

        print("\nExamples:")

        print("  create a sphere on top of a box")

        print("  create two overlapping circles")

        print("  create a house with windows and a door")

        print("  render scene.png")

        print("  save my_scene.json")

    

    def cmd_quit(self, args: str) -> None:

        """Exit the program"""

        print("Goodbye!")

        sys.exit(0)



# ============================================================================

# Main Entry Point

# ============================================================================


def main() -> None:

    """Main entry point for the CSG system"""

    import argparse

    

    parser = argparse.ArgumentParser(description="CSG System with LLM Integration")

    parser.add_argument("--dimension", type=int, choices=[2, 3], default=3,

                        help="Dimension mode (2D or 3D)")

    parser.add_argument("--backend", type=str, choices=["local", "remote"],

                        default="local", help="LLM backend type")

    parser.add_argument("--model", type=str, default="gpt2",

                        help="Model name for local backend")

    parser.add_argument("--api-url", type=str,

                        help="API URL for remote backend")

    parser.add_argument("--api-key", type=str,

                        help="API key for remote backend")

    parser.add_argument("--log-level", type=str, default="INFO",

                        choices=["DEBUG", "INFO", "WARNING", "ERROR"],

                        help="Logging level")

    

    args = parser.parse_args()

    

    logging.getLogger().setLevel(getattr(logging, args.log_level))

    

    logger.info("Starting CSG System")

    logger.info(f"Dimension: {args.dimension}D")

    logger.info(f"Backend: {args.backend}")

    

    llm: Optional[LLMBackend] = None

    try:

        if args.backend == "local":

            llm = LocalLLMBackend(model_name=args.model)

        else:

            if not args.api_url:

                print("Error: --api-url required for remote backend")

                return

            llm = RemoteLLMBackend(api_url=args.api_url, api_key=args.api_key)

        

        if not llm.is_available():

            print("Error: LLM backend is not available")

            return

        

        interface = CSGInterface(llm, dimension=args.dimension)

        interface.run()

    

    except KeyboardInterrupt:

        print("\nExiting...")

    except Exception as e:

        logger.error(f"Fatal error: {e}", exc_info=True)

        print(f"Fatal error: {e}")

    finally:

        if llm is not None:

            llm.cleanup()

        logger.info("CSG System shutdown complete")



if __name__ == "__main__":

    main()