From 6bf27e0ae12a80c5479c83fc381c814a7d235ccd Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 12 Mar 2021 14:51:20 -0600 Subject: [PATCH] [OpenSCAD] Reimplement surface() to match OpenSCAD The original implementation of the surface() function used a simple B-spline representation for the surface, which generated degenerate surface with several of OpenSCAD's demo input files. This commit modifies the algorithm to generate a discrete surface identical to the one generated within OpenSCAD itself. It also adds several units tests to identify future regressions. Note that PNG input is not yet supported for the surface() function. --- src/Mod/OpenSCAD/CMakeLists.txt | 3 + src/Mod/OpenSCAD/OpenSCADFeatures.py | 166 ++++++++++++++---- .../OpenSCADTest/app/test_importCSG.py | 33 +++- .../OpenSCAD/OpenSCADTest/data/Surface.dat | 10 ++ .../OpenSCAD/OpenSCADTest/data/Surface.png | Bin 0 -> 2023 bytes .../OpenSCAD/OpenSCADTest/data/Surface2.dat | 5 + 6 files changed, 186 insertions(+), 31 deletions(-) create mode 100644 src/Mod/OpenSCAD/OpenSCADTest/data/Surface.dat create mode 100644 src/Mod/OpenSCAD/OpenSCADTest/data/Surface.png create mode 100644 src/Mod/OpenSCAD/OpenSCADTest/data/Surface2.dat diff --git a/src/Mod/OpenSCAD/CMakeLists.txt b/src/Mod/OpenSCAD/CMakeLists.txt index a60d1225f0..0edc1ee1c9 100644 --- a/src/Mod/OpenSCAD/CMakeLists.txt +++ b/src/Mod/OpenSCAD/CMakeLists.txt @@ -43,6 +43,9 @@ SET(OpenSCADTestsFiles_SRCS OpenSCADTest/data/CSG.csg OpenSCADTest/data/Cube.stl OpenSCADTest/data/Square.dxf + OpenSCADTest/data/Surface.dat + OpenSCADTest/data/Surface2.dat + OpenSCADTest/data/Surface.png ) SET(OpenSCADTests_ALL diff --git a/src/Mod/OpenSCAD/OpenSCADFeatures.py b/src/Mod/OpenSCAD/OpenSCADFeatures.py index 108b15edb5..3c7aaad365 100644 --- a/src/Mod/OpenSCAD/OpenSCADFeatures.py +++ b/src/Mod/OpenSCAD/OpenSCADFeatures.py @@ -502,33 +502,139 @@ class CGALFeature: raise ValueError def makeSurfaceVolume(filename): - import FreeCAD,Part - f1=open(filename) - coords=[] - miny=1 - for line in f1.readlines(): - sline=line.strip() - if sline and not sline.startswith('#'): - ycoord=len(coords) - lcoords=[] - for xcoord, num in enumerate(sline.split()): - fnum=float(num) - lcoords.append(FreeCAD.Vector(float(xcoord),float(ycoord),fnum)) - miny=min(fnum,miny) - coords.append(lcoords) - s=Part.BSplineSurface() - s.interpolate(coords) - plane=Part.makePlane(len(coords[0])-1,len(coords)-1,FreeCAD.Vector(0,0,miny-1)) - l1=Part.makeLine(plane.Vertexes[0].Point,s.value(0,0)) - l2=Part.makeLine(plane.Vertexes[1].Point,s.value(1,0)) - l3=Part.makeLine(plane.Vertexes[2].Point,s.value(0,1)) - l4=Part.makeLine(plane.Vertexes[3].Point,s.value(1,1)) - f0=plane.Faces[0] - f0.reverse() - f1=Part.Face(Part.Wire([plane.Edges[0],l1.Edges[0],s.vIso(0).toShape(),l2.Edges[0]])) - f2=Part.Face(Part.Wire([plane.Edges[1],l3.Edges[0],s.uIso(0).toShape(),l1.Edges[0]])) - f3=Part.Face(Part.Wire([plane.Edges[2],l4.Edges[0],s.vIso(1).toShape(),l3.Edges[0]])) - f4=Part.Face(Part.Wire([plane.Edges[3],l2.Edges[0],s.uIso(1).toShape(),l4.Edges[0]])) - f5=s.toShape().Faces[0] - solid=Part.Solid(Part.Shell([f0,f1,f2,f3,f4,f5])) - return solid,(len(coords[0])-1)/2.0,(len(coords)-1)/2.0 + import FreeCAD,Part,sys + with open(filename) as f1: + coords = [] + min_z = sys.float_info.max + for line in f1.readlines(): + sline=line.strip() + if sline and not sline.startswith('#'): + ycoord=len(coords) + lcoords=[] + for xcoord, num in enumerate(sline.split()): + fnum=float(num) + lcoords.append(FreeCAD.Vector(float(xcoord),float(ycoord),fnum)) + min_z = min(fnum,min_z) + coords.append(lcoords) + + num_rows = len(coords) + num_cols = len(coords[0]) + + # OpenSCAD does not spline this surface, so neither do we: just create a bunch of faces, + # using four triangles per quadrilateral + faces = [] + for row in range(num_rows-1): + for col in range(num_cols-1): + a = coords[row+0][col+0] + b = coords[row+0][col+1] + c = coords[row+1][col+1] + d = coords[row+1][col+0] + centroid = 0.25 * (a + b + c + d) + ab = Part.makeLine(a,b) + bc = Part.makeLine(b,c) + cd = Part.makeLine(c,d) + da = Part.makeLine(d,a) + + diag_a = Part.makeLine(a, centroid) + diag_b = Part.makeLine(b, centroid) + diag_c = Part.makeLine(c, centroid) + diag_d = Part.makeLine(d, centroid) + + wire1 = Part.Wire([ab,diag_a,diag_b]) + wire2 = Part.Wire([bc,diag_b,diag_c]) + wire3 = Part.Wire([cd,diag_c,diag_d]) + wire4 = Part.Wire([da,diag_d,diag_a]) + + try: + face = Part.Face(wire1) + faces.append(face) + face = Part.Face(wire2) + faces.append(face) + face = Part.Face(wire3) + faces.append(face) + face = Part.Face(wire4) + faces.append(face) + except Exception: + print ("Failed to create the face from {},{},{},{}".format(coords[row+0][col+0],\ + coords[row+0][col+1],coords[row+1][col+1],coords[row+1][col+0])) + + last_row = num_rows-1 + last_col = num_cols-1 + + # Create the face to close off the y-min border: OpenSCAD places the lower surface of the shell + # at 1 unit below the lowest coordinate in the surface + lines = [] + corner1 = FreeCAD.Vector(coords[0][0].x, coords[0][0].y, min_z-1) + lines.append (Part.makeLine(corner1,coords[0][0])) + for col in range(num_cols-1): + a = coords[0][col] + b = coords[0][col+1] + lines.append (Part.makeLine(a, b)) + corner2 = FreeCAD.Vector(coords[0][last_col].x, coords[0][last_col].y, min_z-1) + lines.append (Part.makeLine(corner2,coords[0][last_col])) + lines.append (Part.makeLine(corner1,corner2)) + wire = Part.Wire(lines) + face = Part.Face(wire) + faces.append(face) + + # Create the face to close off the y-max border + lines = [] + corner1 = FreeCAD.Vector(coords[last_row][0].x, coords[last_row][0].y, min_z-1) + lines.append (Part.makeLine(corner1,coords[last_row][0])) + for col in range(num_cols-1): + a = coords[last_row][col] + b = coords[last_row][col+1] + lines.append (Part.makeLine(a, b)) + corner2 = FreeCAD.Vector(coords[last_row][last_col].x, coords[last_row][last_col].y, min_z-1) + lines.append (Part.makeLine(corner2,coords[last_row][last_col])) + lines.append (Part.makeLine(corner1,corner2)) + wire = Part.Wire(lines) + face = Part.Face(wire) + faces.append(face) + + # Create the face to close off the x-min border + lines = [] + corner1 = FreeCAD.Vector(coords[0][0].x, coords[0][0].y, min_z-1) + lines.append (Part.makeLine(corner1,coords[0][0])) + for row in range(num_rows-1): + a = coords[row][0] + b = coords[row+1][0] + lines.append (Part.makeLine(a, b)) + corner2 = FreeCAD.Vector(coords[last_row][0].x, coords[last_row][0].y, min_z-1) + lines.append (Part.makeLine(corner2,coords[last_row][0])) + lines.append (Part.makeLine(corner1,corner2)) + wire = Part.Wire(lines) + face = Part.Face(wire) + faces.append(face) + + # Create the face to close off the x-max border + lines = [] + corner1 = FreeCAD.Vector(coords[0][last_col].x, coords[0][last_col].y, min_z-1) + lines.append (Part.makeLine(corner1,coords[0][last_col])) + for row in range(num_rows-1): + a = coords[row][last_col] + b = coords[row+1][last_col] + lines.append (Part.makeLine(a, b)) + corner2 = FreeCAD.Vector(coords[last_row][last_col].x, coords[last_row][last_col].y, min_z-1) + lines.append (Part.makeLine(corner2,coords[last_row][last_col])) + lines.append (Part.makeLine(corner1,corner2)) + wire = Part.Wire(lines) + face = Part.Face(wire) + faces.append(face) + + # Create a bottom surface to close off the shell + a = FreeCAD.Vector(coords[0][0].x, coords[0][0].y, min_z-1) + b = FreeCAD.Vector(coords[0][last_col].x, coords[0][last_col].y, min_z-1) + c = FreeCAD.Vector(coords[last_row][last_col].x, coords[last_row][last_col].y, min_z-1) + d = FreeCAD.Vector(coords[last_row][0].x, coords[last_row][0].y, min_z-1) + ab = Part.makeLine(a,b) + bc = Part.makeLine(b,c) + cd = Part.makeLine(c,d) + da = Part.makeLine(d,a) + wire = Part.Wire([ab,bc,cd,da]) + face = Part.Face(wire) + faces.append(face) + + s = Part.Shell(faces) + solid = Part.Solid(s) + return solid,last_col,last_row diff --git a/src/Mod/OpenSCAD/OpenSCADTest/app/test_importCSG.py b/src/Mod/OpenSCAD/OpenSCADTest/app/test_importCSG.py index 2a7b0b546b..7e4e23dfb6 100644 --- a/src/Mod/OpenSCAD/OpenSCADTest/app/test_importCSG.py +++ b/src/Mod/OpenSCAD/OpenSCADTest/app/test_importCSG.py @@ -313,7 +313,38 @@ polyhedron( FreeCAD.closeDocument(doc.Name) def test_import_surface(self): - pass + testfile = join(self.test_dir, "Surface.dat").replace('\\','/') + doc = self.utility_create_scad(f"surface(file = \"{testfile}\", center = true, convexity = 5);", "surface_simple_dat") + object = doc.ActiveObject + self.assertTrue (object is not None) + self.assertAlmostEqual (object.Shape.Volume, 275.000000, 6) + self.assertAlmostEqual (object.Shape.BoundBox.XMin, -4.5, 6) + self.assertAlmostEqual (object.Shape.BoundBox.XMax, 4.5, 6) + self.assertAlmostEqual (object.Shape.BoundBox.YMin, -4.5, 6) + self.assertAlmostEqual (object.Shape.BoundBox.YMax, 4.5, 6) + FreeCAD.closeDocument(doc.Name) + + testfile = join(self.test_dir, "Surface.dat").replace('\\','/') + doc = self.utility_create_scad(f"surface(file = \"{testfile}\", convexity = 5);", "surface_uncentered_dat") + object = doc.ActiveObject + self.assertTrue (object is not None) + self.assertAlmostEqual (object.Shape.Volume, 275.000000, 6) + self.assertAlmostEqual (object.Shape.BoundBox.XMin, 0, 6) + self.assertAlmostEqual (object.Shape.BoundBox.XMax, 9, 6) + self.assertAlmostEqual (object.Shape.BoundBox.YMin, 0, 6) + self.assertAlmostEqual (object.Shape.BoundBox.YMax, 9, 6) + FreeCAD.closeDocument(doc.Name) + + testfile = join(self.test_dir, "Surface2.dat").replace('\\','/') + doc = self.utility_create_scad(f"surface(file = \"{testfile}\", center = true, convexity = 5);", "surface_rectangular_dat") + object = doc.ActiveObject + self.assertTrue (object is not None) + self.assertAlmostEqual (object.Shape.Volume, 24.5500000, 6) + self.assertAlmostEqual (object.Shape.BoundBox.XMin, -2, 6) + self.assertAlmostEqual (object.Shape.BoundBox.XMax, 2, 6) + self.assertAlmostEqual (object.Shape.BoundBox.YMin, -1.5, 6) + self.assertAlmostEqual (object.Shape.BoundBox.YMax, 1.5, 6) + FreeCAD.closeDocument(doc.Name) def test_import_projection(self): pass diff --git a/src/Mod/OpenSCAD/OpenSCADTest/data/Surface.dat b/src/Mod/OpenSCAD/OpenSCADTest/data/Surface.dat new file mode 100644 index 0000000000..079162a3c6 --- /dev/null +++ b/src/Mod/OpenSCAD/OpenSCADTest/data/Surface.dat @@ -0,0 +1,10 @@ +10 9 8 7 6 5 5 5 5 5 +9 8 7 6 6 4 3 2 1 0 +8 7 6 6 4 3 2 1 0 0 +7 6 6 4 3 2 1 0 0 0 +6 6 4 3 2 1 1 0 0 0 +6 6 3 2 1 1 1 0 0 0 +6 6 2 1 1 1 1 0 0 0 +6 6 1 0 0 0 0 0 0 0 +3 1 0 0 0 0 0 0 0 0 +3 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/src/Mod/OpenSCAD/OpenSCADTest/data/Surface.png b/src/Mod/OpenSCAD/OpenSCADTest/data/Surface.png new file mode 100644 index 0000000000000000000000000000000000000000..d71eda57953116ba449f84091b16b1c1e78d23b9 GIT binary patch literal 2023 zcmbVNZA=qq9KV*5loGar&=~gRI9{TT_Ik%_Z&oP1&Q_>)&@t>B^SC?cjkZ_s4r$#7 zY6aJ&aUxU97)0#UxWp~G!3X2qB+A5k-^Ch2{-7)ct#1egDY}=H;4Zd@3z(6`bKf8^$C!~fYMe%A(8_u_ z`!0cP+qKzE@9L#Z4AQUxULPU=fuEHqIOO*QL?UEHQoICM%iAadryx?V8L`L;;SQ$@ zw(r4i$wz{^uz7ocbIEETA6vOpc4N-#=xCu^Q2+$@l-9#(-YFZcg zG9z9|3KA&V-`{WOuQKpL4~m&gCe(y%-KiR7f8XOIH|3R-^?kC~9>WtUPwhR)!EeS$BgoS{+H$NJfTBmyj!!Gb6` zof-I3Y5o2TT9j-%fkkQGo{ART;UJ5)vLfFn(5!7I&{(=+SkTK$yy)fs!H>*w#+x8v z6-nFuxvuNojLB2}eMbr~u76NHYRV zHE%KLc1$ul6zz`j5C9agJL;JY~=I3Rfm-bir)&_CsJm8YtWuyZ0vX*k_C zZS0Qp%E!T}0wX9|?g2AG%gJXMB)!c&cl4HN`@Jk6{g;whhKYQ))K3Ylr3Xa%8Hx@C z=A&}IE_;Ih-O1G1UwV554hDw2%uFiqk(qUD01P+*OtLKnr-~s+8FG*ow>>X=#-F>o z3)(v;-Z|5v8)Gy_+vA>s{H4UnA2ic3582$@Svo&@=g7^ZX@A4g{VzYR<8M6tEspXx zAOCWj|N6YRaBgwn!LN639Jzbr(bD0!2Oj;tzv0Ql+lvE_&ksDA+4J6uPu{)%U~Z{i z8_{SgntIEU8x==qqla4Ut>l80%pE$Rx-evGn^}TNppnLrqAQ8yquHy`*xnIjckJGe zP4P;5DP$qP3)jWgcr}wQdo!_kTC0Gf<>W{F6DrAJvFOHM1NhTn>m zO(U+-g<_rBb27Wy0uwx`^Jts2k*L&pJ&|p<$6Hh%-uh~^9k(^d$vu0^c63gEvF3nU z{XS$}D9Tf*ol}#=u+aIz!3uAQ7G6s(lt(L|f+<*W@vP>kF0N5FooGzHj!k4@*Ii8P zzPBxFA8Tuz(rH{1J!<%qY6q-6FdrUmv~laai5!Jshz}06>xPFWhw2Yc7e-d^j_GoK zUY}=Cwx1b>ry}c+F?kZAd9t5j3c3)6*H$&PuZB==UK zN9WI1e>*lgG`@YGZj-&NH(w(hJ9W8WHu7n{`p$qKyLP-~>-U%BU$Misnf$zId-M;I C`LbXD literal 0 HcmV?d00001 diff --git a/src/Mod/OpenSCAD/OpenSCADTest/data/Surface2.dat b/src/Mod/OpenSCAD/OpenSCADTest/data/Surface2.dat new file mode 100644 index 0000000000..8a00f19793 --- /dev/null +++ b/src/Mod/OpenSCAD/OpenSCADTest/data/Surface2.dat @@ -0,0 +1,5 @@ +# Example comment +1.0 1.1 1.3 1.7 2.5 +1.1 1.3 1.7 2.5 2.7 +1.3 1.7 2.5 2.7 2.9 +1.7 2.5 2.7 2.9 3.0 \ No newline at end of file