Skip to content

nicescad API Documentation

axes_helper

AxesHelper

A Python class for creating a 3D helper for visualizing the axes in a scene. This class is designed to be used with the nicegui library (https://pypi.org/project/nicegui/) for creating 3D scenes. It creates line objects for the x, y, and z axes, each with a distinct color, and provides methods for changing these colors and toggling their visibility.

The original JavaScript code, which this Python code is based on, can be found at: https://raw.githubusercontent.com/mrdoob/three.js/master/src/helpers/AxesHelper.js The JavaScript code was refactored into this Python version by OpenAI's ChatGPT.

For the refactoring, following prompts were given: 1. Refactor a JavaScript class for handling ThreeJS scenes into a Python class using the nicegui library. 2. The class should allow to hide/show axes. 3. Usage of nicegui library's API for the colors. 4. Include Google docstrings and type hints to the code. 5. The scene should be a constructor parameter to be remembered. 6. The Axes should be named x, y and z. The name attribute should be assigned after the line object creation. 7. The set_colors method should be called in the constructor. 8. Use with self.scene as scene when drawing the lines. 9. The class should provide a method to toggle the visibility of axes. 10. The default size of the axes to be drawn should be 10.0. 11. Color information should be persisted when toggling the axes. 12. Use a dictionary to store colors by axes names.

According to nicegui's documentation (https://nicegui.io/documentation/scene), the 'scene.line()' function is used to create lines, here is an example: scene.line([-4, 0, 0], [-4, 2, 0]).material('#ff0000') To remove elements from the scene, one uses 'scene.remove()' function: scene.remove(element) where element is either the element instance or its ID.

Date: 2023-07-24 @author: Refactored into Python by OpenAI's ChatGPT

Attributes:

Name Type Description
size float

The size of the axes to be drawn.

scene scene

The scene where the axes will be drawn.

Usage

scene = ui.scene().classes('w-full h-64') axes_helper = AxesHelper(scene, size=10.0) axes_helper.set_colors(x_axis_color='#FF0000', y_axis_color='#00FF00', z_axis_color='#0000FF') # set colors for x, y, and z axes axes_helper.toggle_axes() # toggle visibility of the axes

Source code in nicescad/axes_helper.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
class AxesHelper:
    """
    A Python class for creating a 3D helper for visualizing the axes in a scene.
    This class is designed to be used with the `nicegui` library (https://pypi.org/project/nicegui/)
    for creating 3D scenes. It creates line objects for the x, y, and z axes,
    each with a distinct color, and provides methods for changing these colors and toggling their visibility.

    The original JavaScript code, which this Python code is based on, can be found at:
    https://raw.githubusercontent.com/mrdoob/three.js/master/src/helpers/AxesHelper.js
    The JavaScript code was refactored into this Python version by OpenAI's ChatGPT.

    For the refactoring, following prompts were given:
    1. Refactor a JavaScript class for handling ThreeJS scenes into a Python class using the `nicegui` library.
    2. The class should allow to hide/show axes.
    3. Usage of `nicegui` library's API for the colors.
    4. Include Google docstrings and type hints to the code.
    5. The scene should be a constructor parameter to be remembered.
    6. The Axes should be named x, y and z. The name attribute should be assigned after the line object creation.
    7. The `set_colors` method should be called in the constructor.
    8. Use `with self.scene as scene` when drawing the lines.
    9. The class should provide a method to toggle the visibility of axes.
    10. The default size of the axes to be drawn should be 10.0.
    11. Color information should be persisted when toggling the axes.
    12. Use a dictionary to store colors by axes names.

    According to nicegui's documentation (https://nicegui.io/documentation/scene), the 'scene.line()' function is used to
    create lines, here is an example:
        scene.line([-4, 0, 0], [-4, 2, 0]).material('#ff0000')
    To remove elements from the scene, one uses 'scene.remove()' function:
        scene.remove(element)
    where element is either the element instance or its ID.

    Date: 2023-07-24
    @author: Refactored into Python by OpenAI's ChatGPT

    Attributes:
        size (float): The size of the axes to be drawn.
        scene (ui.scene): The scene where the axes will be drawn.

    Usage:
        scene = ui.scene().classes('w-full h-64')
        axes_helper = AxesHelper(scene, size=10.0)
        axes_helper.set_colors(x_axis_color='#FF0000', y_axis_color='#00FF00', z_axis_color='#0000FF')  # set colors for x, y, and z axes
        axes_helper.toggle_axes()  # toggle visibility of the axes
    """

    def __init__(self, scene: "ui.scene", size: float = 10.0):
        """
        The constructor for AxesHelper class.

        Args:
            scene (ui.scene): The scene where the axes will be drawn.
            size (float): The size of the axes to be drawn.
        """
        self.scene = scene
        self.size = size
        self.vertices = [
            (0, 0, 0),
            (size, 0, 0),
            (0, 0, 0),
            (0, size, 0),
            (0, 0, 0),
            (0, 0, size),
        ]

        self.axis_names = ["x", "y", "z"]
        self.lines = []
        self.axes_visible = False
        self.color_by_name = {"x": "#FF0000", "y": "#00FF00", "z": "#0000FF"}
        self.toggle_axes()

    def set_colors(
        self,
        x_axis_color: str = "#FF0000",
        y_axis_color: str = "#00FF00",
        z_axis_color: str = "#0000FF",
    ):
        """
        A method to set colors of the axes.

        Args:
            x_axis_color (str): Color of the x-axis.
            y_axis_color (str): Color of the y-axis.
            z_axis_color (str): Color of the z-axis.
        """
        self.color_by_name = {"x": x_axis_color, "y": y_axis_color, "z": z_axis_color}
        for idx, line in enumerate(self.lines):
            line.material(self.color_by_name[self.axis_names[idx]])

    def toggle_axes(self):
        """
        A method to toggle the visibility of the axes.
        """
        if self.axes_visible:
            # Axes are currently visible, so remove them
            for line in self.lines:
                try:
                    self.scene.remove(line)
                except KeyError:
                    pass
            self.lines = []
            self.axes_visible = False
        else:
            # Axes are currently not visible, so add them
            with self.scene:
                for i in range(3):
                    line = self.scene.line(
                        self.vertices[2 * i], self.vertices[2 * i + 1]
                    )
                    line.name = self.axis_names[i]
                    line.material(self.color_by_name[line.name])
                    self.lines.append(line)
            self.axes_visible = True

__init__(scene, size=10.0)

The constructor for AxesHelper class.

Parameters:

Name Type Description Default
scene scene

The scene where the axes will be drawn.

required
size float

The size of the axes to be drawn.

10.0
Source code in nicescad/axes_helper.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def __init__(self, scene: "ui.scene", size: float = 10.0):
    """
    The constructor for AxesHelper class.

    Args:
        scene (ui.scene): The scene where the axes will be drawn.
        size (float): The size of the axes to be drawn.
    """
    self.scene = scene
    self.size = size
    self.vertices = [
        (0, 0, 0),
        (size, 0, 0),
        (0, 0, 0),
        (0, size, 0),
        (0, 0, 0),
        (0, 0, size),
    ]

    self.axis_names = ["x", "y", "z"]
    self.lines = []
    self.axes_visible = False
    self.color_by_name = {"x": "#FF0000", "y": "#00FF00", "z": "#0000FF"}
    self.toggle_axes()

set_colors(x_axis_color='#FF0000', y_axis_color='#00FF00', z_axis_color='#0000FF')

A method to set colors of the axes.

Parameters:

Name Type Description Default
x_axis_color str

Color of the x-axis.

'#FF0000'
y_axis_color str

Color of the y-axis.

'#00FF00'
z_axis_color str

Color of the z-axis.

'#0000FF'
Source code in nicescad/axes_helper.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def set_colors(
    self,
    x_axis_color: str = "#FF0000",
    y_axis_color: str = "#00FF00",
    z_axis_color: str = "#0000FF",
):
    """
    A method to set colors of the axes.

    Args:
        x_axis_color (str): Color of the x-axis.
        y_axis_color (str): Color of the y-axis.
        z_axis_color (str): Color of the z-axis.
    """
    self.color_by_name = {"x": x_axis_color, "y": y_axis_color, "z": z_axis_color}
    for idx, line in enumerate(self.lines):
        line.material(self.color_by_name[self.axis_names[idx]])

toggle_axes()

A method to toggle the visibility of the axes.

Source code in nicescad/axes_helper.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def toggle_axes(self):
    """
    A method to toggle the visibility of the axes.
    """
    if self.axes_visible:
        # Axes are currently visible, so remove them
        for line in self.lines:
            try:
                self.scene.remove(line)
            except KeyError:
                pass
        self.lines = []
        self.axes_visible = False
    else:
        # Axes are currently not visible, so add them
        with self.scene:
            for i in range(3):
                line = self.scene.line(
                    self.vertices[2 * i], self.vertices[2 * i + 1]
                )
                line.name = self.axis_names[i]
                line.material(self.color_by_name[line.name])
                self.lines.append(line)
        self.axes_visible = True

blockscad_converter

A class to convert BlockSCAD XML files to SCAD files using the OpenAI language model API.

BlockSCAD Editor: https://www.blockscad3d.com/editor/ OpenSCAD: https://openscad.org/ NiceSCAD: http://nicescad.bitplan.com/ Relevant Issue: http://nicescad.bitplan.com/issue/23

Author: OpenAI ChatGPT Date: July 25, 2023

The code was created by the OpenAI ChatGPT model in response to the following prompts: - "Create a class named 'BlockscadConverter' that converts BlockSCAD XML files to SCAD files." - "The xml_path is given in the constructor." - "The class should have a method named convert_to_scad that outputs a path to the SCAD file." - "Use the write method of a file object to output the SCAD file." - "Include Google docstrings and type hints to the code." - "Include comments that explain which prompts created the code. This helps other LLMs understand how to reproduce the same results." - "Add links to the BlockSCAD editor (https://www.blockscad3d.com/editor/), OpenSCAD (https://openscad.org/), and the target platform (NiceSCAD, http://nicescad.bitplan.com/)." - "Include the link to the relevant issue: 'support reading and converting blockscad files #23' on http://nicescad.bitplan.com/issue/23" - "Add yourself (OpenAI ChatGPT) as the author and include the date" - "keep the prompt list in the comments to be able to reproduce the results." - "If the OpenAI API key is not available in the environment variables, look for it in a JSON file at ~/.openai/openai_api_key.json." - "If the OpenAI API key is not found, throw an exception."

BlockscadConverter

Attributes

xml_path : str path to the input BlockSCAD XML file

Methods

convert_to_scad(scad_path: str) -> Union[str, None] Converts the BlockSCAD XML file to a SCAD file and returns the SCAD file path.

Source code in nicescad/blockscad_converter.py
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
class BlockscadConverter:
    """
    Attributes
    ----------
    xml_path : str
        path to the input BlockSCAD XML file

    Methods
    -------
    convert_to_scad(scad_path: str) -> Union[str, None]
        Converts the BlockSCAD XML file to a SCAD file and returns the SCAD file path.
    """

    def __init__(self, xml_path: str):
        """
        Parameters
        ----------
        xml_path : str
            path to the input BlockSCAD XML file
        """
        self.xml_path = xml_path

    def convert_to_scad(self, scad_path: str) -> Union[str, None]:
        """
        Converts the BlockSCAD XML file to a SCAD file using the OpenAI language model API.

        Parameters
        ----------
        scad_path : str
            path to the output SCAD file

        Returns
        -------
        Union[str, None]
            path to the output SCAD file if conversion is successful, None otherwise
        """
        # Load the API key from the environment or a JSON file
        openai_api_key = os.getenv("OPENAI_API_KEY")
        json_file = Path.home() / ".openai" / "openai_api_key.json"

        if openai_api_key is None and json_file.is_file():
            with open(json_file, "r") as file:
                data = json.load(file)
                openai_api_key = data.get("OPENAI_API_KEY")

        if openai_api_key is None:
            raise ValueError(
                "No OpenAI API key found. Please set the 'OPENAI_API_KEY' environment variable or store it in `~/.openai/openai_api_key.json`."
            )

        openai.api_key = openai_api_key

        # Read the XML file
        with open(self.xml_path, "r") as file:
            xml_content = file.read()

        # Check if the XML content is a BlockSCAD XML
        if '<xml xmlns="https://blockscad3d.com' not in xml_content:
            msg = f"The file at {self.xml_path} is not a valid BlockSCAD XML file."
            raise Exception(msg)

        # Use the API to convert the XML to SCAD
        response = openai.Completion.create(
            engine="text-davinci-002",
            prompt=f"""Convert the following BlockSCAD XML to OpenSCAD and make sure to add a preamble comment (verbatim):
// OpenSCAD 
// converted from BlockSCAD XML by nicescad's blockscad converter
// according to 
// https://github.com/WolfgangFahl/nicescad/issues/23
// support reading and converting blockscad files #23
//
make sure to convert as direct as possible e.g. 
translate,rotate,cylinder,sphere,cube,color which are available in OpenScad should be           
used as such. 
Use all parameters e.g. prefer cube(10,10,10,center=true) to cube(10) when the parameters are available in
the original xml file.
<field name="CENTERDROPDOWN">false</field> e.g. leads to center=false
Add the original color command using a color name when applicable e.g. color("green");.
Try high reproduceability by not making any assumptions and keeping the structure intact. So do not add an initial translate. 

Do not add extra empty comments.
Avoid any empty lines - really - never add whitespace that harms getting always the same result.
Always use // line comments never /* 
Always indent with two spaces.
Make sure commands end with a semicolon.

Here is the BlockSCAD XML:\n{xml_content}
""",
            temperature=0.5,
            max_tokens=1000,
        )

        scad_content = response.choices[0].text.strip()

        # A very basic check to see if the SCAD content seems okay
        if not scad_content.startswith("// OpenSCAD"):
            msg = f"The conversion of {self.xml_path} failed - the // OpenSCAD comment is missing in:\n {scad_content}."
            raise Exception(msg)

        # Write the SCAD code to the output file
        with open(scad_path, "w") as scad_file:
            scad_file.write(scad_content)

        return scad_path

__init__(xml_path)

Parameters

xml_path : str path to the input BlockSCAD XML file

Source code in nicescad/blockscad_converter.py
48
49
50
51
52
53
54
55
def __init__(self, xml_path: str):
    """
    Parameters
    ----------
    xml_path : str
        path to the input BlockSCAD XML file
    """
    self.xml_path = xml_path

convert_to_scad(scad_path)

Converts the BlockSCAD XML file to a SCAD file using the OpenAI language model API.

Parameters

scad_path : str path to the output SCAD file

Returns

Union[str, None] path to the output SCAD file if conversion is successful, None otherwise

Source code in nicescad/blockscad_converter.py
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
    def convert_to_scad(self, scad_path: str) -> Union[str, None]:
        """
        Converts the BlockSCAD XML file to a SCAD file using the OpenAI language model API.

        Parameters
        ----------
        scad_path : str
            path to the output SCAD file

        Returns
        -------
        Union[str, None]
            path to the output SCAD file if conversion is successful, None otherwise
        """
        # Load the API key from the environment or a JSON file
        openai_api_key = os.getenv("OPENAI_API_KEY")
        json_file = Path.home() / ".openai" / "openai_api_key.json"

        if openai_api_key is None and json_file.is_file():
            with open(json_file, "r") as file:
                data = json.load(file)
                openai_api_key = data.get("OPENAI_API_KEY")

        if openai_api_key is None:
            raise ValueError(
                "No OpenAI API key found. Please set the 'OPENAI_API_KEY' environment variable or store it in `~/.openai/openai_api_key.json`."
            )

        openai.api_key = openai_api_key

        # Read the XML file
        with open(self.xml_path, "r") as file:
            xml_content = file.read()

        # Check if the XML content is a BlockSCAD XML
        if '<xml xmlns="https://blockscad3d.com' not in xml_content:
            msg = f"The file at {self.xml_path} is not a valid BlockSCAD XML file."
            raise Exception(msg)

        # Use the API to convert the XML to SCAD
        response = openai.Completion.create(
            engine="text-davinci-002",
            prompt=f"""Convert the following BlockSCAD XML to OpenSCAD and make sure to add a preamble comment (verbatim):
// OpenSCAD 
// converted from BlockSCAD XML by nicescad's blockscad converter
// according to 
// https://github.com/WolfgangFahl/nicescad/issues/23
// support reading and converting blockscad files #23
//
make sure to convert as direct as possible e.g. 
translate,rotate,cylinder,sphere,cube,color which are available in OpenScad should be           
used as such. 
Use all parameters e.g. prefer cube(10,10,10,center=true) to cube(10) when the parameters are available in
the original xml file.
<field name="CENTERDROPDOWN">false</field> e.g. leads to center=false
Add the original color command using a color name when applicable e.g. color("green");.
Try high reproduceability by not making any assumptions and keeping the structure intact. So do not add an initial translate. 

Do not add extra empty comments.
Avoid any empty lines - really - never add whitespace that harms getting always the same result.
Always use // line comments never /* 
Always indent with two spaces.
Make sure commands end with a semicolon.

Here is the BlockSCAD XML:\n{xml_content}
""",
            temperature=0.5,
            max_tokens=1000,
        )

        scad_content = response.choices[0].text.strip()

        # A very basic check to see if the SCAD content seems okay
        if not scad_content.startswith("// OpenSCAD"):
            msg = f"The conversion of {self.xml_path} failed - the // OpenSCAD comment is missing in:\n {scad_content}."
            raise Exception(msg)

        # Write the SCAD code to the output file
        with open(scad_path, "w") as scad_file:
            scad_file.write(scad_content)

        return scad_path

nicescad_cmd

Created on 2023-07-19

@author: wf

NiceScadCmd

Bases: WebserverCmd

command line handling for nicescad

Source code in nicescad/nicescad_cmd.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class NiceScadCmd(WebserverCmd):
    """
    command line handling for nicescad
    """

    def __init__(self):
        """
        constructor
        """
        config = NiceScadWebServer.get_config()
        WebserverCmd.__init__(self, config, NiceScadWebServer, DEBUG)
        pass

    def getArgParser(self, description: str, version_msg) -> ArgumentParser:
        """
        override the default argparser call
        """
        parser = super().getArgParser(description, version_msg)
        parser.add_argument(
            "-v",
            "--verbose",
            action="store_true",
            help="show verbose output [default: %(default)s]",
        )
        parser.add_argument(
            "-rp",
            "--root_path",
            default=NiceScadWebServer.examples_path(),
            help="path to pdf files [default: %(default)s]",
        )
        return parser

__init__()

constructor

Source code in nicescad/nicescad_cmd.py
20
21
22
23
24
25
26
def __init__(self):
    """
    constructor
    """
    config = NiceScadWebServer.get_config()
    WebserverCmd.__init__(self, config, NiceScadWebServer, DEBUG)
    pass

getArgParser(description, version_msg)

override the default argparser call

Source code in nicescad/nicescad_cmd.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def getArgParser(self, description: str, version_msg) -> ArgumentParser:
    """
    override the default argparser call
    """
    parser = super().getArgParser(description, version_msg)
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="show verbose output [default: %(default)s]",
    )
    parser.add_argument(
        "-rp",
        "--root_path",
        default=NiceScadWebServer.examples_path(),
        help="path to pdf files [default: %(default)s]",
    )
    return parser

main(argv=None)

main call

Source code in nicescad/nicescad_cmd.py
48
49
50
51
52
53
54
def main(argv: list = None):
    """
    main call
    """
    cmd = NiceScadCmd()
    exit_code = cmd.cmd_main(argv)
    return exit_code

openscad

Created on 2023-07-19

@author: wf

This module contains the class OpenScad, a wrapper for OpenScad.

OpenSCADLexer

Bases: RegexLexer

Lexer for OpenSCAD, a language for creating solid 3D CAD models.

Attributes:

Name Type Description
name str

The name of the lexer.

aliases list of str

A list of strings that can be used as aliases for the lexer.

filenames list of str

A list of strings that define filename patterns that match this lexer.

Source code in nicescad/openscad.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class OpenSCADLexer(RegexLexer):
    """
    Lexer for OpenSCAD, a language for creating solid 3D CAD models.

    Attributes:
        name (str): The name of the lexer.
        aliases (list of str): A list of strings that can be used as aliases for the lexer.
        filenames (list of str): A list of strings that define filename patterns that match this lexer.
    """

    name = "OpenSCAD"
    aliases = ["openscad"]
    filenames = ["*.scad"]

    tokens = {
        "root": [
            (r"\s+", Text.Whitespace),
            (r"//.*?$", Comment.Single),
            (r"/\*.*?\*/", Comment.Multiline),
            (r"[a-z_][\w]*", Name.Variable),
            (r"\d+", Number.Integer),
            (r"\+\+|--", Operator),
            (r"[=+\-*/%&|^<>!]=?", Operator),
            (r"[\[\]{}();,.]", Punctuation),
            (r'"(\\\\|\\"|[^"])*"', String),
            (r"\b(module|if|else|for|let|echo)\b", Keyword),
            (r"\b(true|false|undef)\b", Keyword.Constant),
            (
                r"\b(cube|sphere|cylinder|polyhedron|square|circle|polygon|import|scale|resize|color|offset|minkowski|hull|render|surface|rotate|translate|mirror|multmatrix|projection|rotate_extrude|linear_extrude)\b",
                Name.Builtin,
            ),
        ],
    }

OpenScad

A wrapper for OpenScad (https://openscad.org/).

Source code in nicescad/openscad.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
class OpenScad:
    """
    A wrapper for OpenScad (https://openscad.org/).
    """

    def __init__(self, scad_prepend: str = "", **kw) -> None:
        """
        Initializes the OpenScad object.
        """
        self.scad_prepend = scad_prepend
        self.openscad_exec = None
        self.openscad_tmp_dir = None
        if "OPENSCAD_EXEC" in os.environ:
            self.openscad_exec = os.environ["OPENSCAD_EXEC"]
        if "OPENSCAD_TMP_DIR" in os.environ:
            self.openscad_tmp_dir = os.environ["OPENSCAD_TMP_DIR"]
        if self.openscad_tmp_dir is not None:
            self.tmp_dir = self.openscad_tmp_dir
        else:
            self.tmp_dir = tempfile.mkdtemp()
        if "openscad_exec" in kw:
            self.openscad_exec = kw["openscad_exec"]
        if self.openscad_exec is None:
            self._try_detect_openscad_exec()
        if self.openscad_exec is None:
            raise Exception("openscad exec not found!")

    def highlight_code(self, code: str) -> str:
        """
        Highlights the provided OpenSCAD code and returns the highlighted code in HTML format.

        Args:
            code (str): The OpenSCAD code to highlight.

        Returns:
            str: The input OpenSCAD code, highlighted and formatted as an HTML string.
        """
        html = highlight(code, OpenSCADLexer(), HtmlFormatter())
        return html

    def _try_executable(self, executable_path: str) -> None:
        """
        Checks if the specified path is a file. If it is, sets it as the OpenScad executable.

        Args:
            executable_path (str): The path to the executable file.
        """
        if os.path.isfile(executable_path):
            self.openscad_exec = executable_path

    def _try_detect_openscad_exec(self) -> None:
        """
        Tries to find the OpenScad executable on the system.

        References:
            https://github.com/nickc92/ViewSCAD/blob/d4597ff6870316dfaafa4f9ecc8ef62773081c61/viewscad/renderer.py#L206C5-L222C1
        """
        platfm = platform.system()
        if platfm == "Linux":
            self._try_executable("/usr/bin/openscad")
            if self.openscad_exec is None:
                self._try_executable("/usr/local/bin/openscad")
        elif platfm == "Darwin":
            self._try_executable("/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD")
        elif platfm == "Windows":
            self._try_executable(
                os.path.join(
                    os.environ.get("Programfiles(x86)", "C:"), "OpenSCAD\\openscad.exe"
                )
            )
            self._try_executable(
                os.path.join(
                    os.environ.get("Programfiles", "C:"), "OpenSCAD\\openscad.exe"
                )
            )

    def write_to_tmp_file(self, openscad_str: str, do_prepend: bool = True):
        """
        Writes an OpenSCAD string to a temporary file.

        The `scad_prepend` string is prepended to the OpenSCAD code before writing,
        unless the OpenSCAD code contains the string '//!OpenSCAD', or `do_prepend`
        is set to `False`. In these cases, only the OpenSCAD code is written to the file.

        Args:
            openscad_str (str): The OpenSCAD code.
            do_prepend (bool, optional): If `True`, the `scad_prepend` string is
                                          prepended to the OpenSCAD code. Defaults to `True`.

        Returns:
            str: The path to the temporary file where the OpenSCAD code (and
                 possibly the `scad_prepend` string) was written.
        """
        scad_tmp_file = os.path.join(self.tmp_dir, "tmp.scad")
        with open(scad_tmp_file, "w") as of:
            if do_prepend and "//!OpenSCAD" not in openscad_str:
                of.write(self.scad_prepend)
            of.write(openscad_str)
        return scad_tmp_file

    async def render_to_file_async(
        self, openscad_str: str, stl_path: str
    ) -> Awaitable[Subprocess]:
        """
        Asynchronously renders an OpenSCAD string to a file.

        Args:
            openscad_str (str): The OpenSCAD code.
            stl_path(str): The path to the output file.

        Returns:
            Subprocess: the openscad execution result
        """
        scad_tmp_file = self.write_to_tmp_file(openscad_str)

        # now run openscad to generate stl:
        cmd = [self.openscad_exec, "-o", stl_path, scad_tmp_file]
        self.saved_umask = os.umask(0o077)
        result = await Subprocess.run_async(cmd)
        os.umask(self.saved_umask)

        self.cleanup_tmp_file(result, scad_tmp_file)
        result.stl_path = stl_path
        return result

    def cleanup_tmp_file(self, result, scad_tmp_file):
        """
        Cleanup temporary files after subprocess execution.

        Args:
            result (Subprocess): The result of the subprocess execution.
            scad_tmp_file (str): The path to the temporary file.
        """
        if result.returncode == 0:
            if os.path.isfile(scad_tmp_file):
                os.remove(scad_tmp_file)
        else:
            result.scad_tmp_file = scad_tmp_file

    async def openscad_str_to_file(
        self, openscad_str: str, stl_path: str
    ) -> Subprocess:
        """
        Renders the OpenSCAD code to a file.

        Args:
            openscad_str (str): The OpenSCAD code.
            stl_path(str): the path to the stl file

        Returns:
            Subprocess: The result of the subprocess run, encapsulated in a Subprocess object.
        """
        result = await self.render_to_file_async(openscad_str, stl_path)
        return result

__init__(scad_prepend='', **kw)

Initializes the OpenScad object.

Source code in nicescad/openscad.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def __init__(self, scad_prepend: str = "", **kw) -> None:
    """
    Initializes the OpenScad object.
    """
    self.scad_prepend = scad_prepend
    self.openscad_exec = None
    self.openscad_tmp_dir = None
    if "OPENSCAD_EXEC" in os.environ:
        self.openscad_exec = os.environ["OPENSCAD_EXEC"]
    if "OPENSCAD_TMP_DIR" in os.environ:
        self.openscad_tmp_dir = os.environ["OPENSCAD_TMP_DIR"]
    if self.openscad_tmp_dir is not None:
        self.tmp_dir = self.openscad_tmp_dir
    else:
        self.tmp_dir = tempfile.mkdtemp()
    if "openscad_exec" in kw:
        self.openscad_exec = kw["openscad_exec"]
    if self.openscad_exec is None:
        self._try_detect_openscad_exec()
    if self.openscad_exec is None:
        raise Exception("openscad exec not found!")

cleanup_tmp_file(result, scad_tmp_file)

Cleanup temporary files after subprocess execution.

Parameters:

Name Type Description Default
result Subprocess

The result of the subprocess execution.

required
scad_tmp_file str

The path to the temporary file.

required
Source code in nicescad/openscad.py
191
192
193
194
195
196
197
198
199
200
201
202
203
def cleanup_tmp_file(self, result, scad_tmp_file):
    """
    Cleanup temporary files after subprocess execution.

    Args:
        result (Subprocess): The result of the subprocess execution.
        scad_tmp_file (str): The path to the temporary file.
    """
    if result.returncode == 0:
        if os.path.isfile(scad_tmp_file):
            os.remove(scad_tmp_file)
    else:
        result.scad_tmp_file = scad_tmp_file

highlight_code(code)

Highlights the provided OpenSCAD code and returns the highlighted code in HTML format.

Parameters:

Name Type Description Default
code str

The OpenSCAD code to highlight.

required

Returns:

Name Type Description
str str

The input OpenSCAD code, highlighted and formatted as an HTML string.

Source code in nicescad/openscad.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def highlight_code(self, code: str) -> str:
    """
    Highlights the provided OpenSCAD code and returns the highlighted code in HTML format.

    Args:
        code (str): The OpenSCAD code to highlight.

    Returns:
        str: The input OpenSCAD code, highlighted and formatted as an HTML string.
    """
    html = highlight(code, OpenSCADLexer(), HtmlFormatter())
    return html

openscad_str_to_file(openscad_str, stl_path) async

Renders the OpenSCAD code to a file.

Parameters:

Name Type Description Default
openscad_str str

The OpenSCAD code.

required
stl_path(str)

the path to the stl file

required

Returns:

Name Type Description
Subprocess Subprocess

The result of the subprocess run, encapsulated in a Subprocess object.

Source code in nicescad/openscad.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
async def openscad_str_to_file(
    self, openscad_str: str, stl_path: str
) -> Subprocess:
    """
    Renders the OpenSCAD code to a file.

    Args:
        openscad_str (str): The OpenSCAD code.
        stl_path(str): the path to the stl file

    Returns:
        Subprocess: The result of the subprocess run, encapsulated in a Subprocess object.
    """
    result = await self.render_to_file_async(openscad_str, stl_path)
    return result

render_to_file_async(openscad_str, stl_path) async

Asynchronously renders an OpenSCAD string to a file.

Parameters:

Name Type Description Default
openscad_str str

The OpenSCAD code.

required
stl_path(str)

The path to the output file.

required

Returns:

Name Type Description
Subprocess Awaitable[Subprocess]

the openscad execution result

Source code in nicescad/openscad.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
async def render_to_file_async(
    self, openscad_str: str, stl_path: str
) -> Awaitable[Subprocess]:
    """
    Asynchronously renders an OpenSCAD string to a file.

    Args:
        openscad_str (str): The OpenSCAD code.
        stl_path(str): The path to the output file.

    Returns:
        Subprocess: the openscad execution result
    """
    scad_tmp_file = self.write_to_tmp_file(openscad_str)

    # now run openscad to generate stl:
    cmd = [self.openscad_exec, "-o", stl_path, scad_tmp_file]
    self.saved_umask = os.umask(0o077)
    result = await Subprocess.run_async(cmd)
    os.umask(self.saved_umask)

    self.cleanup_tmp_file(result, scad_tmp_file)
    result.stl_path = stl_path
    return result

write_to_tmp_file(openscad_str, do_prepend=True)

Writes an OpenSCAD string to a temporary file.

The scad_prepend string is prepended to the OpenSCAD code before writing, unless the OpenSCAD code contains the string '//!OpenSCAD', or do_prepend is set to False. In these cases, only the OpenSCAD code is written to the file.

Parameters:

Name Type Description Default
openscad_str str

The OpenSCAD code.

required
do_prepend bool

If True, the scad_prepend string is prepended to the OpenSCAD code. Defaults to True.

True

Returns:

Name Type Description
str

The path to the temporary file where the OpenSCAD code (and possibly the scad_prepend string) was written.

Source code in nicescad/openscad.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def write_to_tmp_file(self, openscad_str: str, do_prepend: bool = True):
    """
    Writes an OpenSCAD string to a temporary file.

    The `scad_prepend` string is prepended to the OpenSCAD code before writing,
    unless the OpenSCAD code contains the string '//!OpenSCAD', or `do_prepend`
    is set to `False`. In these cases, only the OpenSCAD code is written to the file.

    Args:
        openscad_str (str): The OpenSCAD code.
        do_prepend (bool, optional): If `True`, the `scad_prepend` string is
                                      prepended to the OpenSCAD code. Defaults to `True`.

    Returns:
        str: The path to the temporary file where the OpenSCAD code (and
             possibly the `scad_prepend` string) was written.
    """
    scad_tmp_file = os.path.join(self.tmp_dir, "tmp.scad")
    with open(scad_tmp_file, "w") as of:
        if do_prepend and "//!OpenSCAD" not in openscad_str:
            of.write(self.scad_prepend)
        of.write(openscad_str)
    return scad_tmp_file

process

Subprocess dataclass

A class representing a subprocess execution result.

Source code in nicescad/process.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@dataclass
class Subprocess:
    """A class representing a subprocess execution result."""

    stdout: str
    stderr: str
    cmd: List[str]
    returncode: int
    exception: Optional[BaseException] = None

    @staticmethod
    async def run_async(cmd: List[str]) -> Awaitable["Subprocess"]:
        """
        Asynchronously runs a command as a subprocess and returns the result as an instance of this class.

        Args:
            cmd (List[str]): The command to run.

        Returns:
            Subprocess: An instance of this class representing the result of the subprocess execution.
        """
        try:
            proc = await asyncio.create_subprocess_exec(
                *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
            )

            stdout, stderr = await proc.communicate()

            subprocess = Subprocess(
                stdout=stdout.decode(),
                stderr=stderr.decode(),
                cmd=cmd,
                returncode=proc.returncode,
            )
        except BaseException as ex:
            subprocess = Subprocess(
                stdout="", stderr=str(ex), cmd=cmd, returncode=-1, exception=ex
            )
        return subprocess

    @staticmethod
    def run(cmd: List[str]) -> "Subprocess":
        """
        Runs a command as a subprocess and returns the result as an instance of this class.

        Args:
            cmd (List[str]): The command to run.

        Returns:
            Subprocess: An instance of this class representing the result of the subprocess execution.
        """
        subprocess = asyncio.run(Subprocess.run_async(cmd))
        return subprocess

run(cmd) staticmethod

Runs a command as a subprocess and returns the result as an instance of this class.

Parameters:

Name Type Description Default
cmd List[str]

The command to run.

required

Returns:

Name Type Description
Subprocess Subprocess

An instance of this class representing the result of the subprocess execution.

Source code in nicescad/process.py
47
48
49
50
51
52
53
54
55
56
57
58
59
@staticmethod
def run(cmd: List[str]) -> "Subprocess":
    """
    Runs a command as a subprocess and returns the result as an instance of this class.

    Args:
        cmd (List[str]): The command to run.

    Returns:
        Subprocess: An instance of this class representing the result of the subprocess execution.
    """
    subprocess = asyncio.run(Subprocess.run_async(cmd))
    return subprocess

run_async(cmd) async staticmethod

Asynchronously runs a command as a subprocess and returns the result as an instance of this class.

Parameters:

Name Type Description Default
cmd List[str]

The command to run.

required

Returns:

Name Type Description
Subprocess Awaitable[Subprocess]

An instance of this class representing the result of the subprocess execution.

Source code in nicescad/process.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@staticmethod
async def run_async(cmd: List[str]) -> Awaitable["Subprocess"]:
    """
    Asynchronously runs a command as a subprocess and returns the result as an instance of this class.

    Args:
        cmd (List[str]): The command to run.

    Returns:
        Subprocess: An instance of this class representing the result of the subprocess execution.
    """
    try:
        proc = await asyncio.create_subprocess_exec(
            *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
        )

        stdout, stderr = await proc.communicate()

        subprocess = Subprocess(
            stdout=stdout.decode(),
            stderr=stderr.decode(),
            cmd=cmd,
            returncode=proc.returncode,
        )
    except BaseException as ex:
        subprocess = Subprocess(
            stdout="", stderr=str(ex), cmd=cmd, returncode=-1, exception=ex
        )
    return subprocess

solidservice

Created on 2023-07-30

@author: wf

FastAPIServer

Class for FastAPI server.

Source code in nicescad/solidservice.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class FastAPIServer:
    """
    Class for FastAPI server.
    """

    def __init__(self):
        self.app = FastAPI()
        self.app.post("/convert/")(self.convert)
        self.app.get("/version/")(self.version)
        self.app.get("/", response_class=HTMLResponse)(self.home)

    async def home(self):
        """
        Endpoint to return the homepage with links.

        Returns:
        str -- HTML content
        """
        return """
        <html>
            <head>
                <title>Nicescad solidpython converter service</title>
            </head>
            <body>
                <h1>Welcome to the nicescad solidpython to scad converter</h1>
                <ul>
                    <li><a href="/version/">Check the version of nicescad</a></li>
                    <li><a href="https://github.com/WolfgangFahl/nicescad/issues/28">nicescad GitHub issue</a></li>
                </ul>
            </body>
        </html>
        """

    async def version(self):
        """
        Endpoint to return the version of the nicescad package.

        Returns:
        dict -- the version
        """
        return {"version": nicescad.__version__}

    async def convert(self, item: Item):
        """
        Endpoint to convert Python code to OpenSCAD code.

        Arguments:
        item: Item -- input Python code

        Returns:
        dict -- the OpenSCAD code
        """
        converter = SolidConverter(item.python_code)
        openscad_code = converter.convert_to_openscad()
        return {"openscad_code": openscad_code}

convert(item) async

Endpoint to convert Python code to OpenSCAD code.

Arguments: item: Item -- input Python code

Returns: dict -- the OpenSCAD code

Source code in nicescad/solidservice.py
84
85
86
87
88
89
90
91
92
93
94
95
96
async def convert(self, item: Item):
    """
    Endpoint to convert Python code to OpenSCAD code.

    Arguments:
    item: Item -- input Python code

    Returns:
    dict -- the OpenSCAD code
    """
    converter = SolidConverter(item.python_code)
    openscad_code = converter.convert_to_openscad()
    return {"openscad_code": openscad_code}

home() async

Endpoint to return the homepage with links.

Returns: str -- HTML content

Source code in nicescad/solidservice.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
async def home(self):
    """
    Endpoint to return the homepage with links.

    Returns:
    str -- HTML content
    """
    return """
    <html>
        <head>
            <title>Nicescad solidpython converter service</title>
        </head>
        <body>
            <h1>Welcome to the nicescad solidpython to scad converter</h1>
            <ul>
                <li><a href="/version/">Check the version of nicescad</a></li>
                <li><a href="https://github.com/WolfgangFahl/nicescad/issues/28">nicescad GitHub issue</a></li>
            </ul>
        </body>
    </html>
    """

version() async

Endpoint to return the version of the nicescad package.

Returns: dict -- the version

Source code in nicescad/solidservice.py
75
76
77
78
79
80
81
82
async def version(self):
    """
    Endpoint to return the version of the nicescad package.

    Returns:
    dict -- the version
    """
    return {"version": nicescad.__version__}

SolidConverter

Class for conversion of Python code to OpenSCAD code.

Source code in nicescad/solidservice.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class SolidConverter:
    """
    Class for conversion of Python code to OpenSCAD code.
    """

    def __init__(self, python_code):
        self.python_code = python_code

    def convert_to_openscad(self):
        """
        Function to convert the input Python code into OpenSCAD code using SolidPython

        Returns:
        str -- OpenSCAD code
        """
        d = eval(self.python_code)
        openscad_code = scad_render(d)
        return openscad_code

convert_to_openscad()

Function to convert the input Python code into OpenSCAD code using SolidPython

Returns: str -- OpenSCAD code

Source code in nicescad/solidservice.py
26
27
28
29
30
31
32
33
34
35
def convert_to_openscad(self):
    """
    Function to convert the input Python code into OpenSCAD code using SolidPython

    Returns:
    str -- OpenSCAD code
    """
    d = eval(self.python_code)
    openscad_code = scad_render(d)
    return openscad_code

version

Created on 2023-06-19

@author: wf

Version dataclass

Bases: object

Version handling for nicescad

Source code in nicescad/version.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@dataclass
class Version(object):
    """
    Version handling for nicescad
    """

    name = "nicescad"
    version = nicescad.__version__
    date = "2023-07-19"
    updated = "2024-09-09"
    description = "nicescad brings OpenScad to the browser (again)"

    authors = "Wolfgang Fahl"

    doc_url = "https://wiki.bitplan.com/index.php/nicescad"
    chat_url = "https://github.com/WolfgangFahl/nicescad/discussions"
    cm_url = "https://github.com/WolfgangFahl/nicescad"

    license = f"""Copyright 2023-2024 contributors. All rights reserved.

  Licensed under the Apache License 2.0
  http://www.apache.org/licenses/LICENSE-2.0

  Distributed on an "AS IS" basis without warranties
  or conditions of any kind, either express or implied."""
    longDescription = f"""{name} version {version}
{description}

  Created by {authors} on {date} last updated {updated}"""

webserver

Created on 2023-06-19

@author: wf

NiceScadSolution

Bases: InputWebSolution

the NiceScad solution

Source code in nicescad/webserver.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
class NiceScadSolution(InputWebSolution):
    """
    the NiceScad solution
    """

    def __init__(self, webserver: NiceScadWebServer, client: Client):
        """
        Initialize the solution

        Calls the constructor of the base solution
        Args:
            webserver (NiceScadWebServer): The webserver instance associated with this context.
            client (Client): The client instance this context is associated with.
        """
        super().__init__(webserver, client)  # Call to the superclass constructor
        self.input = "example.scad"
        self.stl_name = "result.stl"
        self.stl_color = "#57B6A9"
        self.stl_object = None
        self.do_trace = True
        self.html_view = None
        self.axes_view = None
        self.oscad = webserver.oscad
        self.code = """// nicescad example
module example() {
  translate([0,0,15]) {
     cube(30,center=true);
     sphere(20);
  }
}
example();"""

    async def render(self, _click_args=None):
        """Renders the OpenScad string and updates the 3D scene with the result.

        Args:
            click_args (object): The click event arguments.
        """
        try:
            self.progress_view.visible = True
            ui.notify("rendering ...")
            with self.scene:
                self.stl_link.visible = False
                self.color_picker_button.disable()
            openscad_str = self.code_area.value
            stl_path = stl_path = os.path.join(self.oscad.tmp_dir, self.stl_name)
            render_result = await self.oscad.openscad_str_to_file(
                openscad_str, stl_path
            )
            # show render result in log
            self.log_view.push(render_result.stderr)
            if render_result.returncode == 0:
                ui.notify("stl created ... loading into scene")
                self.stl_link.visible = True
                self.color_picker_button.enable()
                with self.scene:
                    self.stl_object = (
                        self.scene.stl(f"/stl/{self.stl_name}").move(x=0.0).scale(0.1)
                    )
                    self.stl_object.name = self.stl_name
                    self.stl_object.material(self.stl_color)
        except BaseException as ex:
            self.handle_exception(ex, self.do_trace)
        self.progress_view.visible = False

    def read_input(self, input_str: str):
        """Reads the given input and handles any exceptions.

        Args:
            input_str (str): The input string representing a URL or local file.
        """
        try:
            ui.notify(f"reading {input_str}")
            self.code = self.do_read_input(input_str)
            self.input_input.set_value(input_str)
            self.code_area.set_value(self.code)
            self.log_view.clear()
            self.error_msg = None
            self.stl_link.visible = False
        except BaseException as e:
            self.code = None
            self.handle_exception(e)

    def save_file(self):
        """Saves the current code to the last input file, if it was a local path."""
        if self.is_local and self.input:
            with open(self.input, "w") as file:
                file.write(self.code)
            ui.notify(f"{self.input} saved")
        else:
            raise Exception("No local file to save to")

    async def open_file(self) -> None:
        """Opens a Local filer picker dialog and reads the selected input file."""
        if self.is_local:
            pick_list = await LocalFilePicker("~", multiple=False)
            if len(pick_list) > 0:
                input_file = pick_list[0]
                await self.read_and_optionally_render(input_file)

    pass

    def setup_pygments(self):
        """
        prepare pygments syntax highlighting by loading style
        """
        pygments_css_file = (
            Path(__file__).parent / "web" / "static" / "css" / "pygments.css"
        )
        pygments_css = pygments_css_file.read_text()
        ui.add_head_html(f"<style>{pygments_css}</style>")

    def code_changed(self, cargs):
        """
        react on changed code
        """
        self.code = cargs.value
        pass

    async def highlight_code(self, _cargs):
        """
        highlight the code and show the html
        """
        try:
            if self.code_area.visible:
                self.code_area.visible = False
                code_html = self.oscad.highlight_code(self.code)
                self.html_view.content = code_html
                self.html_view.visible = True
            else:
                self.html_view.visible = False
                self.code_area.visible = True
            self.toggle_icon(self.highlight_button)
        except BaseException as ex:
            self.handle_exception(ex, self.do_trace)

    async def pick_color(self, e: ColorPickEventArguments):
        """
        Asynchronously picks a color based on provided event arguments.

        This function changes the color of the 'color_picker_button' and the 'stl_object'
        according to the color specified in the event arguments.

        Args:
            e (ColorPickEventArguments): An object containing event-specific arguments.
                The 'color' attribute of this object specifies the color to be applied.

        Note:
            If 'stl_object' is None, the function will only change the color of 'color_picker_button'.
            Otherwise, it changes the color of both 'color_picker_button' and 'stl_object'.
        """
        self.color_picker_button.style(f"background-color:{e.color}!important")
        if self.stl_object:
            self.stl_color = e.color
            self.stl_object.material(f"{e.color}")
        pass

    async def toggle_axes(self):
        """
        toggle the axes of my scene
        """
        self.toggle_icon(self.axes_button)
        if self.axes_view is None:
            self.axes_view = AxesHelper(self.scene)
        else:
            self.axes_view.toggle_axes()
        pass

    async def toggle_grid(self, _ea):
        """
        toogle the grid of my scene
        """
        try:
            grid = self.scene._props["grid"]
            grid_str = "off" if grid else "on"
            grid_js = "false" if grid else "true"
            # try toggling grid
            ui.notify(f"setting grid to {grid_str}")
            grid = not grid
            # workaround according to https://github.com/zauberzeug/nicegui/discussions/1246
            js_cmd = f'scene_c{self.scene.id}.children.find(c => c.type === "GridHelper").visible = {grid_js}'
            await ui.run_javascript(js_cmd, respond=False)
            self.scene._props["grid"] = grid
            self.scene.update()
            # try toggling icon
            self.toggle_icon(self.grid_button)
        except BaseException as ex:
            self.handleExeption(ex)
        pass

    def prepare_ui(self):
        """
        handle the command line arguments
        """
        InputWebSolution.prepare_ui(self)
        self.setup_pygments()

    async def home(self):
        """Generates the home page with a 3D viewer and a code editor."""

        self.setup_menu()
        with ui.column():
            with ui.splitter() as splitter:
                with splitter.before:
                    self.grid_button = self.tool_button(
                        "toggle grid",
                        handler=self.toggle_grid,
                        icon="grid_off",
                        toggle_icon="grid_on",
                    )
                    self.axes_button = self.tool_button(
                        "toggle axes",
                        icon="polyline",
                        toggle_icon="square",
                        handler=self.toggle_axes,
                    )
                    self.color_picker_button = ui.button(
                        icon="colorize", color=self.stl_color
                    )
                    with self.color_picker_button:
                        self.color_picker = ui.color_picker(on_pick=self.pick_color)
                    self.color_picker_button.disable()

                    with ui.scene(width=1024, height=768).classes("w-full") as scene:
                        self.scene = scene
                        scene.spot_light(distance=100, intensity=0.2).move(-10, 0, 10)
                    with splitter.after:
                        with ui.element("div").classes("w-full"):
                            extensions = {"scad": ".scad", "xml": ".xml"}
                            self.example_selector = FileSelector(
                                path=self.root_path,
                                handler=self.read_and_optionally_render,
                                extensions=extensions,
                            )
                            self.input_input = ui.input(
                                value=self.input, on_change=self.input_changed
                            ).props("size=100")
                            self.highlight_button = self.tool_button(
                                tooltip="highlight",
                                icon="html",
                                toggle_icon="code",
                                handler=self.highlight_code,
                            )
                            if self.is_local:
                                self.tool_button(
                                    tooltip="save", icon="save", handler=self.save_file
                                )
                            self.tool_button(
                                tooltip="reload",
                                icon="refresh",
                                handler=self.reload_file,
                            )
                            if self.is_local:
                                self.tool_button(
                                    tooltip="open",
                                    icon="file_open",
                                    handler=self.open_file,
                                )
                            self.tool_button(
                                tooltip="render",
                                icon="play_circle",
                                handler=self.render,
                            )
                            self.stl_link = ui.link(
                                "stl result", f"/stl/{self.stl_name}", new_tab=True
                            )
                            self.stl_link.visible = False
                            self.progress_view = ui.spinner(
                                "dots", size="lg", color="blue"
                            )
                            self.progress_view.visible = False
                            self.code_area = (
                                ui.textarea(
                                    value=self.code, on_change=self.code_changed
                                )
                                .props("clearable")
                                .props("rows=25")
                            )
                            self.html_view = ui.html()
                            self.html_view.visible = False
                            self.log_view = ui.log(max_lines=20).classes("w-full h-40")
        await self.setup_footer()
        if self.args.input:
            await self.read_and_optionally_render(self.args.input)

    def configure_settings(self):
        """Generates the settings page with a link to the project's GitHub page."""
        sp_input = ui.textarea("scad prepend", value=self.oscad.scad_prepend).props(
            "cols=80"
        )
        sp_input.bind_value(self.oscad, "scad_prepend")

__init__(webserver, client)

Initialize the solution

Calls the constructor of the base solution Args: webserver (NiceScadWebServer): The webserver instance associated with this context. client (Client): The client instance this context is associated with.

Source code in nicescad/webserver.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
    def __init__(self, webserver: NiceScadWebServer, client: Client):
        """
        Initialize the solution

        Calls the constructor of the base solution
        Args:
            webserver (NiceScadWebServer): The webserver instance associated with this context.
            client (Client): The client instance this context is associated with.
        """
        super().__init__(webserver, client)  # Call to the superclass constructor
        self.input = "example.scad"
        self.stl_name = "result.stl"
        self.stl_color = "#57B6A9"
        self.stl_object = None
        self.do_trace = True
        self.html_view = None
        self.axes_view = None
        self.oscad = webserver.oscad
        self.code = """// nicescad example
module example() {
  translate([0,0,15]) {
     cube(30,center=true);
     sphere(20);
  }
}
example();"""

code_changed(cargs)

react on changed code

Source code in nicescad/webserver.py
187
188
189
190
191
192
def code_changed(self, cargs):
    """
    react on changed code
    """
    self.code = cargs.value
    pass

configure_settings()

Generates the settings page with a link to the project's GitHub page.

Source code in nicescad/webserver.py
360
361
362
363
364
365
def configure_settings(self):
    """Generates the settings page with a link to the project's GitHub page."""
    sp_input = ui.textarea("scad prepend", value=self.oscad.scad_prepend).props(
        "cols=80"
    )
    sp_input.bind_value(self.oscad, "scad_prepend")

highlight_code(_cargs) async

highlight the code and show the html

Source code in nicescad/webserver.py
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
async def highlight_code(self, _cargs):
    """
    highlight the code and show the html
    """
    try:
        if self.code_area.visible:
            self.code_area.visible = False
            code_html = self.oscad.highlight_code(self.code)
            self.html_view.content = code_html
            self.html_view.visible = True
        else:
            self.html_view.visible = False
            self.code_area.visible = True
        self.toggle_icon(self.highlight_button)
    except BaseException as ex:
        self.handle_exception(ex, self.do_trace)

home() async

Generates the home page with a 3D viewer and a code editor.

Source code in nicescad/webserver.py
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
async def home(self):
    """Generates the home page with a 3D viewer and a code editor."""

    self.setup_menu()
    with ui.column():
        with ui.splitter() as splitter:
            with splitter.before:
                self.grid_button = self.tool_button(
                    "toggle grid",
                    handler=self.toggle_grid,
                    icon="grid_off",
                    toggle_icon="grid_on",
                )
                self.axes_button = self.tool_button(
                    "toggle axes",
                    icon="polyline",
                    toggle_icon="square",
                    handler=self.toggle_axes,
                )
                self.color_picker_button = ui.button(
                    icon="colorize", color=self.stl_color
                )
                with self.color_picker_button:
                    self.color_picker = ui.color_picker(on_pick=self.pick_color)
                self.color_picker_button.disable()

                with ui.scene(width=1024, height=768).classes("w-full") as scene:
                    self.scene = scene
                    scene.spot_light(distance=100, intensity=0.2).move(-10, 0, 10)
                with splitter.after:
                    with ui.element("div").classes("w-full"):
                        extensions = {"scad": ".scad", "xml": ".xml"}
                        self.example_selector = FileSelector(
                            path=self.root_path,
                            handler=self.read_and_optionally_render,
                            extensions=extensions,
                        )
                        self.input_input = ui.input(
                            value=self.input, on_change=self.input_changed
                        ).props("size=100")
                        self.highlight_button = self.tool_button(
                            tooltip="highlight",
                            icon="html",
                            toggle_icon="code",
                            handler=self.highlight_code,
                        )
                        if self.is_local:
                            self.tool_button(
                                tooltip="save", icon="save", handler=self.save_file
                            )
                        self.tool_button(
                            tooltip="reload",
                            icon="refresh",
                            handler=self.reload_file,
                        )
                        if self.is_local:
                            self.tool_button(
                                tooltip="open",
                                icon="file_open",
                                handler=self.open_file,
                            )
                        self.tool_button(
                            tooltip="render",
                            icon="play_circle",
                            handler=self.render,
                        )
                        self.stl_link = ui.link(
                            "stl result", f"/stl/{self.stl_name}", new_tab=True
                        )
                        self.stl_link.visible = False
                        self.progress_view = ui.spinner(
                            "dots", size="lg", color="blue"
                        )
                        self.progress_view.visible = False
                        self.code_area = (
                            ui.textarea(
                                value=self.code, on_change=self.code_changed
                            )
                            .props("clearable")
                            .props("rows=25")
                        )
                        self.html_view = ui.html()
                        self.html_view.visible = False
                        self.log_view = ui.log(max_lines=20).classes("w-full h-40")
    await self.setup_footer()
    if self.args.input:
        await self.read_and_optionally_render(self.args.input)

open_file() async

Opens a Local filer picker dialog and reads the selected input file.

Source code in nicescad/webserver.py
167
168
169
170
171
172
173
async def open_file(self) -> None:
    """Opens a Local filer picker dialog and reads the selected input file."""
    if self.is_local:
        pick_list = await LocalFilePicker("~", multiple=False)
        if len(pick_list) > 0:
            input_file = pick_list[0]
            await self.read_and_optionally_render(input_file)

pick_color(e) async

Asynchronously picks a color based on provided event arguments.

This function changes the color of the 'color_picker_button' and the 'stl_object' according to the color specified in the event arguments.

Parameters:

Name Type Description Default
e ColorPickEventArguments

An object containing event-specific arguments. The 'color' attribute of this object specifies the color to be applied.

required
Note

If 'stl_object' is None, the function will only change the color of 'color_picker_button'. Otherwise, it changes the color of both 'color_picker_button' and 'stl_object'.

Source code in nicescad/webserver.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
async def pick_color(self, e: ColorPickEventArguments):
    """
    Asynchronously picks a color based on provided event arguments.

    This function changes the color of the 'color_picker_button' and the 'stl_object'
    according to the color specified in the event arguments.

    Args:
        e (ColorPickEventArguments): An object containing event-specific arguments.
            The 'color' attribute of this object specifies the color to be applied.

    Note:
        If 'stl_object' is None, the function will only change the color of 'color_picker_button'.
        Otherwise, it changes the color of both 'color_picker_button' and 'stl_object'.
    """
    self.color_picker_button.style(f"background-color:{e.color}!important")
    if self.stl_object:
        self.stl_color = e.color
        self.stl_object.material(f"{e.color}")
    pass

prepare_ui()

handle the command line arguments

Source code in nicescad/webserver.py
265
266
267
268
269
270
def prepare_ui(self):
    """
    handle the command line arguments
    """
    InputWebSolution.prepare_ui(self)
    self.setup_pygments()

read_input(input_str)

Reads the given input and handles any exceptions.

Parameters:

Name Type Description Default
input_str str

The input string representing a URL or local file.

required
Source code in nicescad/webserver.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def read_input(self, input_str: str):
    """Reads the given input and handles any exceptions.

    Args:
        input_str (str): The input string representing a URL or local file.
    """
    try:
        ui.notify(f"reading {input_str}")
        self.code = self.do_read_input(input_str)
        self.input_input.set_value(input_str)
        self.code_area.set_value(self.code)
        self.log_view.clear()
        self.error_msg = None
        self.stl_link.visible = False
    except BaseException as e:
        self.code = None
        self.handle_exception(e)

render(_click_args=None) async

Renders the OpenScad string and updates the 3D scene with the result.

Parameters:

Name Type Description Default
click_args object

The click event arguments.

required
Source code in nicescad/webserver.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
async def render(self, _click_args=None):
    """Renders the OpenScad string and updates the 3D scene with the result.

    Args:
        click_args (object): The click event arguments.
    """
    try:
        self.progress_view.visible = True
        ui.notify("rendering ...")
        with self.scene:
            self.stl_link.visible = False
            self.color_picker_button.disable()
        openscad_str = self.code_area.value
        stl_path = stl_path = os.path.join(self.oscad.tmp_dir, self.stl_name)
        render_result = await self.oscad.openscad_str_to_file(
            openscad_str, stl_path
        )
        # show render result in log
        self.log_view.push(render_result.stderr)
        if render_result.returncode == 0:
            ui.notify("stl created ... loading into scene")
            self.stl_link.visible = True
            self.color_picker_button.enable()
            with self.scene:
                self.stl_object = (
                    self.scene.stl(f"/stl/{self.stl_name}").move(x=0.0).scale(0.1)
                )
                self.stl_object.name = self.stl_name
                self.stl_object.material(self.stl_color)
    except BaseException as ex:
        self.handle_exception(ex, self.do_trace)
    self.progress_view.visible = False

save_file()

Saves the current code to the last input file, if it was a local path.

Source code in nicescad/webserver.py
158
159
160
161
162
163
164
165
def save_file(self):
    """Saves the current code to the last input file, if it was a local path."""
    if self.is_local and self.input:
        with open(self.input, "w") as file:
            file.write(self.code)
        ui.notify(f"{self.input} saved")
    else:
        raise Exception("No local file to save to")

setup_pygments()

prepare pygments syntax highlighting by loading style

Source code in nicescad/webserver.py
177
178
179
180
181
182
183
184
185
def setup_pygments(self):
    """
    prepare pygments syntax highlighting by loading style
    """
    pygments_css_file = (
        Path(__file__).parent / "web" / "static" / "css" / "pygments.css"
    )
    pygments_css = pygments_css_file.read_text()
    ui.add_head_html(f"<style>{pygments_css}</style>")

toggle_axes() async

toggle the axes of my scene

Source code in nicescad/webserver.py
232
233
234
235
236
237
238
239
240
241
async def toggle_axes(self):
    """
    toggle the axes of my scene
    """
    self.toggle_icon(self.axes_button)
    if self.axes_view is None:
        self.axes_view = AxesHelper(self.scene)
    else:
        self.axes_view.toggle_axes()
    pass

toggle_grid(_ea) async

toogle the grid of my scene

Source code in nicescad/webserver.py
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
async def toggle_grid(self, _ea):
    """
    toogle the grid of my scene
    """
    try:
        grid = self.scene._props["grid"]
        grid_str = "off" if grid else "on"
        grid_js = "false" if grid else "true"
        # try toggling grid
        ui.notify(f"setting grid to {grid_str}")
        grid = not grid
        # workaround according to https://github.com/zauberzeug/nicegui/discussions/1246
        js_cmd = f'scene_c{self.scene.id}.children.find(c => c.type === "GridHelper").visible = {grid_js}'
        await ui.run_javascript(js_cmd, respond=False)
        self.scene._props["grid"] = grid
        self.scene.update()
        # try toggling icon
        self.toggle_icon(self.grid_button)
    except BaseException as ex:
        self.handleExeption(ex)
    pass

NiceScadWebServer

Bases: InputWebserver

WebServer class that manages the server and handles OpenScad operations.

Attributes:

Name Type Description
oscad OpenScad

An OpenScad object that aids in performing OpenScad operations.

Source code in nicescad/webserver.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class NiceScadWebServer(InputWebserver):
    """WebServer class that manages the server and handles OpenScad operations.

    Attributes:
        oscad (OpenScad): An OpenScad object that aids in performing OpenScad operations.
    """

    @classmethod
    def get_config(cls) -> WebserverConfig:
        copy_right = "(c)2023-2024 Wolfgang Fahl"
        config = WebserverConfig(
            copy_right=copy_right,
            version=Version(),
            default_port=9858,
            short_name="nicescad",
        )
        server_config = WebserverConfig.get(config)
        server_config.solution_class = NiceScadSolution
        return server_config

    def __init__(self):
        """Constructs all the necessary attributes for the WebServer object."""
        InputWebserver.__init__(self, config=NiceScadWebServer.get_config())
        self.oscad = OpenScad(
            scad_prepend="""//https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Other_Language_Features#$fa,_$fs_and_$fn
// default number of facets for arc generation
$fn=30;
"""
        )
        app.add_static_files("/stl", self.oscad.tmp_dir)

    def configure_run(self):
        root_path = (
            self.args.root_path
            if self.args.root_path
            else NiceScadWebServer.examples_path()
        )
        self.root_path = os.path.abspath(root_path)
        self.allowed_urls = [
            "https://raw.githubusercontent.com/WolfgangFahl/nicescad/main/examples/",
            "https://raw.githubusercontent.com/openscad/openscad/master/examples/",
            self.examples_path(),
            self.root_path,
        ]

    @classmethod
    def examples_path(cls) -> str:
        # the root directory (default: examples)
        path = os.path.join(os.path.dirname(__file__), "../nicescad_examples")
        path = os.path.abspath(path)
        return path

__init__()

Constructs all the necessary attributes for the WebServer object.

Source code in nicescad/webserver.py
42
43
44
45
46
47
48
49
50
51
    def __init__(self):
        """Constructs all the necessary attributes for the WebServer object."""
        InputWebserver.__init__(self, config=NiceScadWebServer.get_config())
        self.oscad = OpenScad(
            scad_prepend="""//https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Other_Language_Features#$fa,_$fs_and_$fn
// default number of facets for arc generation
$fn=30;
"""
        )
        app.add_static_files("/stl", self.oscad.tmp_dir)