WolfgangFahl / nicescad

nicescad brings OpenScad to the browser (again)
http://nicescad.bitplan.com
Apache License 2.0
4 stars 0 forks source link

support reading and converting blockscad files #23

Open WolfgangFahl opened 1 year ago

WolfgangFahl commented 1 year ago

Create a python script to convert blockscad xml to openscad.

  1. Include Google docstrings and type hints to the code.
  2. Include comments that explain which prompts created the code
  3. Add links to https://www.blockscad3d.com/editor/ and https://openscad.org/ for as well as for the target platform http://nicescad.bitplan.com/ especially the issue https://github.com/WolfgangFahl/nicescad/issues/23 - support reading and converting blockcad files
  4. Add yourself as the author and include the date
  5. Create a python unit test that checks the given Example xml input against the given example openscad output

Example Blockscad XML input

<xml xmlns="https://blockscad3d.com">
  <version num="1.11.0"></version>
  <color rgba="0,153,0"></color>
  <block type="procedures_defnoreturn" id="Pazln]9Sk2aP%t=+|9:+" x="-1142" y="12">
    <mutation>
      <arg name="sides"></arg>
      <arg name="height"></arg>
      <arg name="radius"></arg>
      <arg name="toothradius"></arg>
    </mutation>
    <field name="NAME">knob</field>
    <statement name="STACK">
      <block type="difference" id="q*p9w327Z;5EC5Y^,2bF">
        <statement name="A">
          <block type="cylinder" id="39KT*ZQxcRy/xqy^}V6I">
            <field name="LOCKED">TRUE</field>
            <field name="CENTERDROPDOWN">true</field>
            <value name="RAD1">
              <shadow type="math_number" id="I[EY[aCsZWJn]5Li[q`K">
                <field name="NUM">18.06</field>
              </shadow>
              <block type="variables_get" id="8ikPEQg7v1kh0vxw%SZM">
                <field name="VAR">radius</field>
              </block>
            </value>
            <value name="RAD2">
              <shadow type="math_number" id="kMy%?a}dib.;{z?)u#d2">
                <field name="NUM">18.06</field>
              </shadow>
              <block type="variables_get" id="3OoB]q+@*pHF=Tn7Z90*">
                <field name="VAR">radius</field>
              </block>
            </value>
            <value name="HEIGHT">
              <shadow type="math_number" id="S%~WM,my!au);)g(^}kt">
                <field name="NUM">8</field>
              </shadow>
              <block type="variables_get" id="FHx?8/H22AR(xO|?pOkQ">
                <field name="VAR">height</field>
              </block>
            </value>
          </block>
        </statement>
        <statement name="MINUS0">
          <block type="controls_for" id="[=UzfC6.x-viU6y7kUO7">
            <field name="VAR">i</field>
            <field name="HULL">FALSE</field>
            <value name="FROM">
              <shadow type="math_number" id="qR4KK?f]vLJY!7^:DhEQ">
                <field name="NUM">1</field>
              </shadow>
            </value>
            <value name="TO">
              <shadow type="math_number" id="Iki#qX-Idxb*rKm4B=#x">
                <field name="NUM">12</field>
              </shadow>
              <block type="variables_get" id="PW@w%W+%@Iq3O2C.I)O_">
                <field name="VAR">sides</field>
              </block>
            </value>
            <value name="BY">
              <shadow type="math_number" id="%@;Ik_s}DWksC#@E5Xnm">
                <field name="NUM">1</field>
              </shadow>
            </value>
            <statement name="DO">
              <block type="simplerotate" id="Z]O{:nX4{_i^FNOp2d![">
                <value name="XVAL">
                  <shadow type="math_angle" id="lB0/4x`BTF%GFJC^uT8A">
                    <field name="NUM">0</field>
                  </shadow>
                </value>
                <value name="YVAL">
                  <shadow type="math_angle" id=".CN=+vWL3r?K3Xu|8Au`">
                    <field name="NUM">0</field>
                  </shadow>
                </value>
                <value name="ZVAL">
                  <shadow type="math_angle" id="Vr!^IRUnhg9ZX8J8twEL">
                    <field name="NUM">0</field>
                  </shadow>
                  <block type="math_arithmetic" id="JlV^hU%}~iJ=yR-K@V?1">
                    <field name="OP">MULTIPLY</field>
                    <value name="A">
                      <shadow type="math_number" id="/,or[(OGhruBiiI|.Z+H">
                        <field name="NUM">1</field>
                      </shadow>
                      <block type="variables_get" id="G4(W?2uBZI*:e#kun**Y">
                        <field name="VAR">i</field>
                      </block>
                    </value>
                    <value name="B">
                      <shadow type="math_number" id="xYw?pbi%?OsP.p+_+^dn">
                        <field name="NUM">30</field>
                      </shadow>
                      <block type="math_arithmetic" id="pe)tfxs:^vTPc]tSuU*Y">
                        <field name="OP">DIVIDE</field>
                        <value name="A">
                          <shadow type="math_number" id="XRz8}(O^{vc*70e6mciM">
                            <field name="NUM">360</field>
                          </shadow>
                        </value>
                        <value name="B">
                          <shadow type="math_number" id="pu_cKo7Aff?We#t1^bfV">
                            <field name="NUM">1</field>
                          </shadow>
                          <block type="variables_get" id="s1HYsh~dG[R4cwJkgqZp">
                            <field name="VAR">sides</field>
                          </block>
                        </value>
                      </block>
                    </value>
                  </block>
                </value>
                <statement name="A">
                  <block type="translate" id="!SI!4*2@.t7c(1%mPH=S">
                    <value name="XVAL">
                      <shadow type="math_number" id="fmb!UG{axt/9[m/^}{68">
                        <field name="NUM">20</field>
                      </shadow>
                      <block type="math_arithmetic" id="FIBi)47dmoqw.?IJIkX;">
                        <field name="OP">ADD</field>
                        <value name="A">
                          <shadow type="math_number" id="b_zo0yHYTYB_bx5,Ib,Z">
                            <field name="NUM">1</field>
                          </shadow>
                          <block type="variables_get" id="c!`MhN0-allj|MD2ziw:">
                            <field name="VAR">radius</field>
                          </block>
                        </value>
                        <value name="B">
                          <shadow type="math_number" id="?%Rl~N14D_8PazOlr}W0">
                            <field name="NUM">1</field>
                          </shadow>
                          <block type="math_arithmetic" id="S`j}-aJw~O-4.?*CcqkA">
                            <field name="OP">DIVIDE</field>
                            <value name="A">
                              <shadow type="math_number" id="u%3o|X0;2QN}Y8}-2*8d">
                                <field name="NUM">1</field>
                              </shadow>
                              <block type="variables_get" id="QA[/jML@sw`t2!AJ./A@">
                                <field name="VAR">toothradius</field>
                              </block>
                            </value>
                            <value name="B">
                              <shadow type="math_number" id="]xX`/*{V!=G]l1qRS#}8">
                                <field name="NUM">2</field>
                              </shadow>
                            </value>
                          </block>
                        </value>
                      </block>
                    </value>
                    <value name="YVAL">
                      <shadow type="math_number" id="{cAr.@ZP32tb-!5B(wii">
                        <field name="NUM">0</field>
                      </shadow>
                    </value>
                    <value name="ZVAL">
                      <shadow type="math_number" id="#:/{,4,WYin`:MMJN+IY">
                        <field name="NUM">0</field>
                      </shadow>
                    </value>
                    <statement name="A">
                      <block type="cylinder" id="#`iJ^mPVxZb^*/gaqeB5">
                        <field name="LOCKED">TRUE</field>
                        <field name="CENTERDROPDOWN">true</field>
                        <value name="RAD1">
                          <shadow type="math_number" id="OwMe/un_a!Se81I4S[.v">
                            <field name="NUM">3.5</field>
                          </shadow>
                          <block type="variables_get" id="PPcNZnCT~c~=#0-C=k./">
                            <field name="VAR">toothradius</field>
                          </block>
                        </value>
                        <value name="RAD2">
                          <shadow type="math_number" id="M#%1YqQk`H9zd#k5ciBS">
                            <field name="NUM">3.5</field>
                          </shadow>
                          <block type="variables_get" id="GMbu]Ce%kz)g}%RfH7kP">
                            <field name="VAR">toothradius</field>
                          </block>
                        </value>
                        <value name="HEIGHT">
                          <shadow type="math_number" id=":8p;3Z]UFdoKFsBi1XyQ">
                            <field name="NUM">8</field>
                          </shadow>
                          <block type="variables_get" id="eiN!yp+4la1i^[q?o`%u">
                            <field name="VAR">height</field>
                          </block>
                        </value>
                      </block>
                    </statement>
                  </block>
                </statement>
              </block>
            </statement>
          </block>
        </statement>
      </block>
    </statement>
  </block>
  <block type="difference" id="l:.2;L#FHOdKMS0,l+1s" x="-1121" y="398">
    <statement name="A">
      <block type="procedures_callnoreturn" id="w[B^7]7@HvBZ|+)-Ml}~">
        <mutation name="knob">
          <arg name="sides"></arg>
          <arg name="height"></arg>
          <arg name="radius"></arg>
          <arg name="toothradius"></arg>
        </mutation>
        <value name="ARG0">
          <block type="math_number" id="tXKfPrJy:Q@9oeEn]P4s">
            <field name="NUM">16</field>
          </block>
        </value>
        <value name="ARG1">
          <block type="math_number" id="x+d,|1uSxs1p`drs`=-s">
            <field name="NUM">12</field>
          </block>
        </value>
        <value name="ARG2">
          <block type="math_number" id="C8alpM%*Q%tnTIqx{4QF">
            <field name="NUM">30</field>
          </block>
        </value>
        <value name="ARG3">
          <block type="math_number" id="]nma8a{m`0!]?`M~_BM}">
            <field name="NUM">6</field>
          </block>
        </value>
      </block>
    </statement>
    <statement name="MINUS0">
      <block type="translate" id="?q](/|w=Iv~-rwVDG0tA">
        <value name="XVAL">
          <shadow type="math_number" id="BpBf{6sooR+slk;40WbW">
            <field name="NUM">0</field>
          </shadow>
        </value>
        <value name="YVAL">
          <shadow type="math_number" id="I/Nt!^z69M+X`}c#3P{.">
            <field name="NUM">0</field>
          </shadow>
        </value>
        <value name="ZVAL">
          <shadow type="math_number" id="!R-[V]i@SNrfO|6[p`u9">
            <field name="NUM">4</field>
          </shadow>
        </value>
        <statement name="A">
          <block type="procedures_callnoreturn" id="5CBuP?yKMmEw@3,}kym|">
            <mutation name="knob">
              <arg name="sides"></arg>
              <arg name="height"></arg>
              <arg name="radius"></arg>
              <arg name="toothradius"></arg>
            </mutation>
            <value name="ARG0">
              <block type="math_number" id="uVG,R!3{j#1~A..7;5CY">
                <field name="NUM">12</field>
              </block>
            </value>
            <value name="ARG1">
              <block type="math_number" id="@-~F0lZ7#OqgP8?R*|iF">
                <field name="NUM">8</field>
              </block>
            </value>
            <value name="ARG2">
              <block type="math_number" id="v(NDl%Wp{I3FExKI};p0">
                <field name="NUM">18.06</field>
              </block>
            </value>
            <value name="ARG3">
              <block type="math_number" id="kTrx}H:P,ZDNA!rl_Onc">
                <field name="NUM">4.4</field>
              </block>
            </value>
          </block>
        </statement>
      </block>
    </statement>
  </block>
</xml>

Expected OpenScad output

module knob(sides, height, radius, toothradius) {
  difference() {
    cylinder(r1=radius, r2=radius, h=height, center=true);
    for (i = [1:sides]) {
      rotate([0, 0, i * 360/sides]) {
        translate([radius + toothradius / 2, 0, 0]) {
          cylinder(r1=toothradius, r2=toothradius, h=height, center=true);
        }
      }
    }
  }
}

difference() {
  knob(16, 12, 30, 6);
  translate([0, 0, 4]) {
    knob(16, 12, 30, 6);
  }
}
WolfgangFahl commented 1 year ago

Test first approach with ChatGPT-4

import os
import unittest
import shutil
from nicescad.blockscad_converter import BlockscadConverter
from tests.basetest import Basetest
from nicescad.webserver import WebServer

class TestBlockscadConverter(Basetest):
    """
    Unit test for converting a BlockSCAD XML file to a SCAD file using the BlockscadConverter class.

    The path for examples is obtained using the WebServer.examples_path() function. The examples of SCAD input files are located in the 'blockscad' subdirectory of the examples path. A test method is defined which iterates over all BlockSCAD XML files in the 'blockscad' directory, converts each file to a SCAD file, and then saves the converted files in the 'blockscad_converted' subdirectory under 'examples/scad'.

    The conversion during the test is done using a temporary directory. The test checks if the content of the output SCAD file matches the expected content in the 'blockscad_converted' directory.

    Prompt details:
        - Conversion of a BlockSCAD XML file to a SCAD file.
        - Usage of BlockscadConverter class for conversion. The constructor needs an xml_path parameter
        - the function to be tested is  convert_to_scad with scad_path as the output file parameter
        - Retrieval of the examples path using WebServer.examples_path() class method from nicescad.webserver.
        - Assumption of the 'blockscad' directory for SCAD input examples.
        - Definition of the unit test setup with paths for the BlockSCAD and SCAD directories.
        - Usage of Basetest as the base class for the test with setUp signature as def setUp(self, debug=False, profile=True).
        - Definition of a test method to iterate over BlockSCAD XML files, convert them to SCAD files, and save in 'blockscad_converted' subdirectory of the scad subdirectory of the examples directory.
        - Usage of a temporary directory for conversion during the test.
        - Comparison of output SCAD file content with the expected SCAD file content.
        - count the number of files and check that at least one file was tested
        - Inclusion of Google docstrings and type hints in the code.
        - Inclusion of the Links for reference
        - Links for reference:
            - BlockSCAD: https://www.blockscad3d.com/editor/
            - OpenSCAD: https://openscad.org/
            - Target platform (NiceSCAD): http://nicescad.bitplan.com/
            - Issue: Support reading and converting blockcad files #23: http://nicescad.bitplan.com/issues/23

    Author: OpenAI Assistant
    Date: 2023-07-25
    """

    def setUp(self, debug: bool=False, profile: bool=True) -> None:
        """Set up the test environment."""
        super().setUp(debug, profile)
        self.examples_path = WebServer.examples_path()
        self.blockscad_dir = os.path.join(self.examples_path, 'blockscad')
        self.scad_converted_dir = os.path.join(self.examples_path, 'scad/blockscad_converted')
        self.temp_dir = os.path.join(self.examples_path, 'temp')
        os.makedirs(self.scad_converted_dir, exist_ok=True)
        os.makedirs(self.temp_dir, exist_ok=True)

    def test_convert_to_scad(self) -> None:
        """Test the convert_to_scad method of BlockscadConverter."""
        return # do not run yet
        file_count=0
        for root, _, files in os.walk(self.blockscad_dir):
            for file in files:
                if file.endswith('.xml'):
                    file_count+=1
                    blockscad_file = os.path.join(root, file)
                    scad_file = os.path.join(self.scad_converted_dir, f'{os.path.splitext(file)[0]}.scad')
                    temp_file = os.path.join(self.temp_dir, f'{os.path.splitext(file)[0]}.scad')

                    # Convert BlockSCAD XML file to SCAD file
                    bc=BlockscadConverter(blockscad_file)
                    bc.convert_to_scad(temp_file)

                    # Compare the output SCAD file with the expected SCAD file
                    with open(scad_file, 'r') as expected_file, open(temp_file, 'r') as test_file:
                        self.assertEqual(test_file.read(), expected_file.read())
        self.assertTrue(file_count>0)

    def tearDown(self) -> None:
        """Tear down the test environment."""
        shutil.rmtree(self.temp_dir)

if __name__ == "__main__":
    unittest.main()