Package: sdfcad
📢 NoteThis is my fork offogleman/sdf. This fork is availableon GitHub(so it's clear where I forked it from) andon GitLab, where I'll do the deployments, automatic tests and docs at some point.
Seeherefor a an effective diff since forking.📹 Video: My TalkOpenSCAD vs PythonSDF (🇩🇪 German) at the Tübix2023 Linux DayI added so many things that it doesn't really make sense to just open a PR upstream. Probably I'll just maintain my own fork.Documentation of the new features is sparse though...  😅Here is a rough list:sdf.easegot a huge revamp - easings can now be manipulated and combined easily:ease.linear.plot()# show what it looks likeease.Easing.plot(ease.linear,ease.in_cubic,ease.smoothstep)# show multiple for comparison3*ease.linear.symmetric+1# This makes a triangle starting at 1 and going as high as 4 in the centerease.smoothstep[0.2:0.8]# zoom into functionease.linear+ease.in_sine# add or multiply functionsease.linear.append(ease.in_cubic.reverse)# stitch functions together# etc, much more, try 'python -m sdf.ease' for a non-comprehensive overviewNew objects:There's nowbezier()-curves! 🥳their width can even be modulated with an easing function (https://fosstodon.org/@nobodyinperson/110367790398144919)There's nowThread()s 🔩just a twisted offset infinite cylinder, very nice to 3D print due to the smoothnesssphere(10)-Thread().dilate(0.4)makes a nice thread hole with toleranceScrew()with head for conveniencepieslice(): a vertically infinite pie slice, useful to cut out partsNew operations:union(),intersection()anddifference()now support bothfillet=andchamfer=mirror()an object at an arbitrary point into an any directionstretch()an object from here to thererotate_stretch()an object to do a kind of partial rotary sweepshear()an object from between two points along a directionmodulate_between(): modify an object's thickness between two points with an easing functiontwist_between(): twist an object between two points with a variable rotation angle specified by an easing functionchamfercut(): cut a chamfer along a plane (a wrapper aroundmodulate_between())shell()can now also do inner and outer shell, not just around boundary
Shape analysis (not very precise and reliable yet):.boundset al.: finding boundaries/closest surface points/intersections via optimization of the SDFvolume(): brute-force-approximate the volume Monte-Carlo-styleMany usability fixes here and theresave()is now properly interruptible, no more zombie worker threadssave()now optionally shows the mesh withpyvista(useful in Notebooks)docstrings are shown properly in interactive Python shellsk()now creates a copy and doesn't modify the object itselfSome things on my TODO list:[ ]  deleting   this point from the README (impossible on mobile apparently)[ ] add tests![ ] auto-generated sphinx documentation[ ] auto-generated examples with code besides renders[ ] importing SVGs 🤔[ ] exporting SVGs (with marching squares / contourplots for 2D SDFs)[ ] importing STLs 🤔sdfCADGenerate 3D meshes based on SDFs (signed distance functions) with a dirt simple Python API.Special thanks to Michael Fogleman for initializing this codebase inGitHub:fogleman/sdf.Special thanks also toInigo Quilezfor his excellent documentation on signed distance functions:3D Signed Distance Functions2D Signed Distance FunctionsExampleHere is a complete example that generates the model shown. This is the
canonicalConstructive Solid Geometryexample. Note the use of operators for union, intersection, and difference.fromsdfcadimport*f=sphere(1)&box(1.5)c=cylinder(0.5)f-=c.orient(X)|c.orient(Y)|c.orient(Z)f.save('out.stl')Yes, that's really the entire code! You can 3D print that model or use it
in a 3D application.More ExamplesHave a cool example? Submit a PR!gearlike.pyknurling.pyblobby.pyweave.pyRequirementsNote that the dependencies will be automatically installed by when following the directions below.Python 3matplotlibmeshionumpyPillowscikit-imagescipy(pyvista, trame, jupyter, ...)📥 InstallationQuick install withpip:# optionally make a virtualenvpython-mvenvsdf-venvsourcesdf-venv/bin/activate# from PyPI: together with the dependencies to run within Jupyterpipinstall'sdfcad[jupyter]'# latest development versionpipinstall'sdfcad[jupyter] @ git+https://gitlab.com/nobodyinperson/sdfCAD'# for headless operationpipinstallgit+https://gitlab.com/nobodyinperson/sdfCADTo hack on:gitclonehttps://gitlab.com/nobodyinperson/sdfCADcdsdfCAD
poetryinstall--all-extras--with=dev
poetryshell# enter a shell in the virtual environmentConfirm that it works:pythonexamples/example.py# should generate a file named out.stlFile Formatssdfnatively writes binary STL files. For other formats,meshiois used (based on your output file extension). This adds support for over 20 different 3D file formats,
including OBJ, PLY, VTK, and many more.Viewing the MeshsdfCAD is best worked with inJupyterLab:# launch jupyter lab, this should open your browser with Jupyter LabjupyterlabAlternatively, you can just.save()your object and open the resulting file in your Mesh viewer of choice, e.g.:MeshLabOpenSCAD(save(openscad=True)creates a.scadfile you can open. OpenSCAD can also auto-reload if the file changes.)Michael Fogleman'smeshviewAPIIn all of the below examples,fis any 3D SDF, such as:f=sphere()BoundsThe bounding box of the SDF is automatically estimated. Inexact SDFs such as
non-uniform scaling may cause issues with this process. In that case you can
specify the bounds to sample manually:f.save('out.stl',bounds=((-1,-1,-1),(1,1,1)))ResolutionThe resolution of the mesh is also computed automatically. There are two ways
to specify the resolution. You can set the resolution directly withstep:f.save('out.stl',step=0.01)f.save('out.stl',step=(0.01,0.02,0.03))# non-uniform resolutionOr you can specify approximately how many points to sample:f.save('out.stl',samples=2**24)# sample about 16M pointsBy default,samples=2**18is used.Tip: Use the default resolution while developing your SDF. Then when you're done,
crank up the resolution for your final output.BatchesThe SDF is sampled in batches. By default the batches have32**3 = 32768points each. This batch size can be overridden:f.save('out.stl',batch_size=64)# instead of 32The code attempts to skip any batches that are far away from the surface of
the mesh. Inexact SDFs such as non-uniform scaling may cause issues with this
process, resulting in holes in the output mesh (where batches were skipped when
they shouldn't have been). To avoid this, you can disable sparse sampling:f.save('out.stl',sparse=False)# force all batches to be completely sampledWorker ThreadsThe SDF is sampled in batches using worker threads. By default,multiprocessing.cpu_count()worker threads are used. This can be overridden:f.save('out.stl',workers=1)# only use one worker threadWithout SavingYou can of course generate a mesh without writing it to an STL file:points=f.generate()# takes the same optional arguments as `save`print(len(points))# print number of points (3x the number of triangles)print(points[:3])# print the vertices of the first triangleIf you want to save an STL aftergenerate, just use:write_binary_stl(path,points)Visualizing the SDFThesave()method automatically shows the object withpyvistaif available (e.g. when you installed with thejupyterextra as described above).# Turn automatic plot visualization off with this:sphere().save(plot=False)In Jupyter, you can try switching the backend betweenclient(faster) orserver(slower but probably more reliable).importpyvistaaspv
pv.set_jupyter_backend("client")# or pv.set_jupyter_backend("server")You can plot a visualization of a 2D slice of the SDF using matplotlib.
This can be useful for debugging purposes.f.show_slice(z=0)f.show_slice(z=0,abs=True)# show abs(f)You can specify a slice plane at any X, Y, or Z coordinate. You can
also specify the bounds to plot.Note thatmatplotlibis only imported if this function is called, so it
isn't strictly required as a dependency.How it WorksThe code simply uses theMarching Cubesalgorithm to generate a mesh from theSigned Distance Function.This would normally be abysmally slow in Python. However, numpy is used to
evaluate the SDF on entire batches of points simultaneously. Furthermore,
multiple threads are used to process batches in parallel. The result is
surprisingly fast (for marching cubes). Meshes of adequate detail can
still be quite large in terms of number of triangles.The core "engine" of thesdflibrary is very small and can be found inmesh.py.In short, there is nothing algorithmically revolutionary here. The goal is
to provide a simple, fun, and easy-to-use API for generating 3D models in our
favorite language Python.Filessdf/d2.py: 2D signed distance functionssdf/d3.py: 3D signed distance functionssdf/dn.py: Dimension-agnostic signed distance functionssdf/ease.py:Easing functionsthat operate on numpy arrays. Some SDFs take an easing function as a parameter.sdf/mesh.py: The core mesh-generation engine. Also includes code for estimating the bounding box of an SDF and for plotting a 2D slice of an SDF with matplotlib.sdf/progress.py: A console progress bar.sdf/stl.py: Code for writing a binarySTL file.sdf/text.py: Generate 2D SDFs for text (which can then be extruded)sdf/util.py: Utility constants and functions.SDF ImplementationIt is reasonable to write your own SDFs beyond those provided by the
built-in library. Browse the SDF implementations to understand how they are
implemented. Here are some simple examples:@sdf3defsphere(radius=1,center=ORIGIN):deff(p):returnnp.linalg.norm(p-center,axis=1)-radiusreturnfAn SDF is simply a function that takes a numpy array of points with shape(N, 3)for 3D SDFs or shape(N, 2)for 2D SDFs and returns the signed distance for each
of those points as an array of shape(N, 1). They are wrapped with the@sdf3decorator (or@sdf2for 2D SDFs) which make boolean operators work,
add thesavemethod, add the operators liketranslate, etc.@op3deftranslate(other,offset):deff(p):returnother(p-offset)returnfAn SDF that operates on another SDF (like the abovetranslate) should use
the@op3decorator instead. This will register the function such that SDFs
can be chained together like:f=sphere(1).translate((1,2,3))Instead of what would otherwise be required:f=translate(sphere(1),(1,2,3))Remember, it's Python!Remember, this is Python, so it's fully programmable. You can and should split up your
model into parameterized sub-components, for example. You can use for loops and
conditionals wherever applicable. The sky is the limit!See thecustomizable box examplefor some starting ideas.EasingsMany transformations in sdfCAD take an optionaleasingparametere.
This is a scalar function that takes an input between 0 and 1 and output another scalar, typically also between 0 and 1.
For convenience, there are many predefined easing functions available, the most important ones probably beingease.linearandease.smoothstep.
Easings can be scaled, added, multiplied, chained, zoomed, etc. to customize them.
An easing function can be plotted via itsplot()method (e.g.ease.in_out_cubic.plot())
Here is an example of some operations:ease.Easing.plot(ease.linear,ease.smoothstep,ease.smoothstep.chain(),0.5*ease.in_out_cubic.reverse,ease.out_elastic.symmetric,ease.smoothstep.symmetric,ease.smoothstep.between(-0.5,1),ease.smoothstep[0.1:0.75],ease.smoothstep.mirror(),-3*ease.linear+ease.smoothstep,)Function Reference3D Primitivesspheresphere(radius=1, center=ORIGIN)f=sphere()# unit spheref=sphere(2)# specify radiusf=sphere(1,(1,2,3))# translated sphereboxbox(size=1, center=ORIGIN, a=None, b=None)f=box(1)# all side lengths = 1f=box((1,2,3))# different side lengthsf=box(a=(-1,-1,-1),b=(3,4,5))# specified by boundsrounded_boxrounded_box(size, radius)f=rounded_box((1,2,3),0.25)wireframe_boxwireframe_box(size, thickness)f=wireframe_box((1,2,3),0.05)torustorus(r1, r2)f=torus(1,0.25)capsulecapsule(a, b, radius)f=capsule(-Z,Z,0.5)capped_cylindercapped_cylinder(a, b, radius)f=capped_cylinder(-Z,Z,0.5)rounded_cylinderrounded_cylinder(ra, rb, h)f=rounded_cylinder(0.5,0.1,2)capped_conecapped_cone(a, b, ra, rb)f=capped_cone(-Z,Z,1,0.5)rounded_conerounded_cone(r1, r2, h)f=rounded_cone(0.75,0.25,2)ellipsoidellipsoid(size)f=ellipsoid((1,2,3))pyramidpyramid(h)f=pyramid(1)Platonic Solidstetrahedrontetrahedron(r)f=tetrahedron(1)octahedronoctahedron(r)f=octahedron(1)dodecahedrondodecahedron(r)f=dodecahedron(1)icosahedronicosahedron(r)f=icosahedron(1)Infinite 3D PrimitivesThe following SDFs extend to infinity in some or all axes.
They can only effectively be used in combination with other shapes, as shown in the examples below.planeplane(normal=UP, point=ORIGIN)planeis an infinite plane, with one side being positive (outside) and one side being negative (inside).f=sphere()&plane()slabslab(x0=None,y0=None,z0=None,x1=None,y1=None,z1=None,r=None)slab(dx=None,dy=None,dz=None)# symmetric versionslabis useful for cutting a shape on one or more axis-aligned planes.f=sphere()&slab(dz=1,x0=0)cylindercylinder(radius)cylinderis an infinite cylinder along the Z axis.f=sphere()-cylinder(0.5)TextYes, even text is supported!text(font_name, text, width=None, height=None, pixels=PIXELS, points=512)FONT='Arial'TEXT='Hello, world!'w,h=measure_text(FONT,TEXT)f=rounded_box((w+1,h+1,0.2),0.1)f-=text(FONT,TEXT).extrude(1)Note:PIL.ImageFont,
which is used to load fonts, does not search for the font by name on all operating systems.
For example, on Ubuntu the full path to the font has to be provided.
(e.g./usr/share/fonts/truetype/freefont/FreeMono.ttf)ImagesImage masks can be extruded and incorporated into your 3D model.image(path_or_array, width=None, height=None, pixels=PIXELS)IMAGE='examples/butterfly.png'w,h=measure_image(IMAGE)f=rounded_box((w*1.1,h*1.1,0.1),0.05)f|=image(IMAGE).extrude(1)&slab(z0=0,z1=0.075)Positioningtranslatetranslate(other, offset)f=sphere().translate((0,0,2))scalescale(other, factor)Note that non-uniform scaling is an inexact SDF.f=sphere().scale(2)f=sphere().scale((1,2,3))# non-uniform scalingrotaterotate(other, angle, vector=Z)f=capped_cylinder(-Z,Z,0.5).rotate(pi/4,X)orientorient(other, axis)orientrotates the shape such that whatever was pointing in the +Z direction
is now pointing in the specified direction.c=capped_cylinder(-Z,Z,0.25)f=c.orient(X)|c.orient(Y)|c.orient(Z)Boolean OperationsThe following primitivesaandbare used in all of the following
boolean operations.a=box((3,3,0.5))b=sphere()The named versions (union,difference,intersection) can all take
one or more SDFs as input. They all take an optionalradius(r) andchamfer(c) parameter to define the amount of filleting/chamfering to apply. When
using operators (|,-,&) the smoothing can still be applied via the.fillet(1)and.chamfer(3)functions.unionf=a|bf=union(a,b)# equivalentdifferencef=a-bf=difference(a,b)# equivalentintersectionf=a&bf=intersection(a,b)# equivalentsmooth_unionf=a|b.r(0.25)f=union(a,b,r=0.25)# equivalentsmooth_differencef=a-b.r(0.25)f=difference(a,b,r=0.25)# equivalentsmooth_intersectionf=a&b.r(0.25)f=intersection(a,b,r=0.25)# equivalentRepetitionrepeatrepeat(other, spacing, count=None, padding=0)repeatcan repeat the underlying SDF infinitely or a finite number of times.
If finite, the number of repetitions must be odd, because the count specifies
the number of copies to make on each side of the origin. If the repeated
elements overlap or come close together, you may need to specify apaddinggreater than zero to compute a correct SDF.f=sphere().repeat(3,(1,1,0))circular_arraycircular_array(other, count, offset)circular_arraymakescountcopies of the underlying SDF, arranged in a
circle around the Z axis.offsetspecifies how far to translate the shape
in X before arraying it. The underlying SDF is only evaluated twice (instead
ofcounttimes), so this is more performant than instantiatingcountcopies
of a shape.f=capped_cylinder(-Z,Z,0.5).circular_array(8,4)Miscellaneousblendblend(a, *bs, r=0.5)f=sphere().blend(box())dilatedilate(other, r)f=example.dilate(0.1)erodeerode(other, r)f=example.erode(0.1)shellshell(other, thickness)f=sphere().shell(0.05)&plane(-Z)elongateelongate(other, size)f=example.elongate((0.25,0.5,0.75))twisttwist(other, k)f=box().twist(pi/2)twist_betweenslab(z0=0,dx=5,dy=20,z1=30,r=2).twist_between(a=5*Z,# start to twist hereb=25*Z,# stop twisting here# smoothstep funtion between 0° and 80°e=units("80°").to("radians").m*ease.smoothstep).save()stretchStretch an object from here to there.# stretch a sphere vertically to make a capsulesphere(10).stretch(ORIGIN,20*Z).save()# specify symmetric=True to stretch equally in the opposite directionrotate_stretchDo a kind of rotary sweep likestretch(), but for rotation.# make a quarter capsule sweep# (”grab X, stretch it around origin to Y”)capsule(ORIGIN,30*X,10).rotate_stretch(X,Y).save()# the same with angle, Z is default rotation axiscapsule(ORIGIN,30*X,10).rotate_stretch(angle=units("90°")).save()Along the way, one can move along the rotation axis, outwards or modulate the thickness:# sweep a capsule aroundcapsule(30*X,50*X,10).rotate_stretch(angle=units("330°"),shear=40*ease.smoothstep,# move smoothly up along rotation axis while rotatingthicken=-6*ease.smoothstep,# gradually get thinner along the wayfling=-20*ease.smoothstep,# gradually move closer to rotation axis).save(sparse=False)Changing shape along the way is also possible:# sweep a capsule aroundcapsule(20*X,40*X,10).rotate_stretch(shear=30*ease.smoothstep,# spiral upwards# transition to a slightly rotated boxtransition=slab(dx=30,dy=10,dz=10,r=1).rotate(-units("50°"),Y).translate(30*X),transition_ease=ease.smoothstep.chain().chain(),# transition smoothly to box).save(sparse=False)# sweep a capsule aroundcapsule(30*X,50*X,10).rotate_stretch(shear=10*(ease.smoothstep.symmetric-0.5).repeat(5),# up and down along the way).save(sparse=False)shearGrab a point and move it into a direction, keeping another point fix.
This can be used to perform a shearing operation.box([20,10,50]).shear(fix=-15*Z,grab=15*Z,move=-10*X,e=ease.smoothstep).save()The easing function can be used to scale the movement along the way, e.g. to make a notch.box([20,10,50]).shear(-15*Z,15*Z,-5*X,e=ease.smoothstep.symmetric).save()modulate_betweenModulate the thickness between two points.# make a cylinder(cylinder(10)&slab(dz=50)).modulate_between(-20*Z,# start modulating here20*Z,# stop  modulating here# easing to apply.# symmetric smoothstep function for negative dip.e=-5*ease.smoothstep.symmetric,).save()chamferThis is a wrapper aroundstretch()andmodulate_between()to facilitate a chamfer cut along a plane.sphere(10).chamfercut(2,at=ORIGIN,direction=-Z).save()bendbend(other, k)f=box().bend(1)bend_linearbend_linear(other, p0, p1, v, e=ease.linear)f=capsule(-Z*2,Z*2,0.25).bend_linear(-Z,Z,X,ease.in_out_quad)bend_radialbend_radial(other, r0, r1, dz, e=ease.linear)f=box((5,5,0.25)).bend_radial(1,2,-1,ease.in_out_quad)transition_lineartransition_linear(f0, f1, p0=-Z, p1=Z, e=ease.linear)f=box().transition_linear(sphere(),e=ease.in_out_quad)transition_radialtransition_radial(f0, f1, r0=0, r1=1, e=ease.linear)f=box().transition_radial(sphere(),e=ease.in_out_quad)wrap_aroundwrap_around(other, x0, x1, r=None, e=ease.linear)FONT='Arial'TEXT=' wrap_around '*3w,h=measure_text(FONT,TEXT)f=text(FONT,TEXT).extrude(0.1).orient(Y).wrap_around(-w/2,w/2)2D to 3D Operationsextrudeextrude(other, h)f=hexagon(1).extrude(1)extrude_toextrude_to(a, b, h, e=ease.linear)f=rectangle(2).extrude_to(circle(1),2,ease.in_out_quad)revolverevolve(other, offset=0)f=hexagon(1).revolve(3)3D to 2D Operationssliceslice(other)f=example.translate((0,0,0.55)).slice().extrude(0.1)2D Primitivescirclelinerectanglerounded_rectangleequilateral_trianglehexagonrounded_xpolygon