--- jupyter: description: 'From a 2D profile to a 3D part and a STEP file in Python with planex2d: draw the profile, pad it into a solid with named faces, sketch holes on its faces and pocket them through, check the volume and write STEP.' --- # From 2D profile to 3D part and STEP A prismatic part starts as a 2D profile. planex2d pads the profile into an OpenCASCADE solid whose side faces carry the names of the profile's edges, sketches on those faces, pockets the sketches into the solid and writes the result as STEP for any CAD program. The profile can be drawn in code, or read from a DXF or SVG drawing, a planar STEP or BREP file, a sketch from a .CATPart file or a Shapely polygon. The 3D part needs the OpenCASCADE bindings, the optional package planex2d-occ (`pip install "planex2d[occ]"`). The first cell is the complete workflow, ready to copy and run, and the section Step by step runs it in parts, with the plots one looks at along the way. The example is an aluminum angle bracket of 40 × 40 × 5 mm with an inner fillet of radius 5, 60 mm long, with two holes of diameter 6.6 in each leg for M6 screws. ```python import math import netgen.occ as occ from ngsolve_webgpu.jupyter import Draw import planex as px profile = px.QuickSketch(px.XZ) profile.Line(40, name="base").Turn(90).Line(5).Turn(90).Line(35) profile.Turn(-90).Line(35).Turn(90).Line(5).Turn(90).Close(name="back") section = profile.Face() section.Fillet(section.vertices("Line.3.end"), 5) bracket = px.Pad(section, 60) base = px.QuickSketch(bracket.faces["base"][0]).Circle((20, 15), 3.3).Circle((20, 45), 3.3) back = px.QuickSketch(bracket.faces["back"][0]).Circle((-15, -22.5), 3.3).Circle((-45, -22.5), 3.3) bracket = px.Pocket(bracket, base, until=px.last) bracket = px.Pocket(bracket, back, until=px.last) print("volume", bracket.mass, "| profile area × 60 less the holes", section.mass * 60 - 4 * math.pi * 3.3**2 * 5) bracket.WriteStep("angle-bracket.step") print("read back", occ.OCCGeometry("angle-bracket.step").shape.mass) Draw(bracket) ``` ## Step by step ### The profile The profile is drawn with the pen on the front plane `px.XZ`, in millimeter numbers, which OpenCASCADE takes as millimeters. The outer bottom edge is named `base` and the outer back edge `back`, so that the faces they become can be found by name later (see [Names and colors](../core/basics/properties.md)). `Fillet` rounds the inner corner. The lines are numbered in the order they are drawn, so the corner is the end of the third, `"Line.3.end"`, and `profile.Plot(backend="plotly")`, the plot below, shows every id on hover (see [Fillets and chamfers](../core/sketch/fillets-chamfers.md) and [Picking what to measure](../core/measuring/analysis.md#picking-what-to-measure)). The area is the two legs less their overlap plus the fillet's corner, $40 \cdot 5 + 35 \cdot 5 + (1 - \pi/4) \cdot 5^2$. ```python import math import netgen.occ as occ from ngsolve_webgpu.jupyter import Draw import planex as px profile = px.QuickSketch(px.XZ) profile.Line(40, name="base").Turn(90).Line(5).Turn(90).Line(35) profile.Turn(-90).Line(35).Turn(90).Line(5).Turn(90).Close(name="back") section = profile.Face() section.Fillet(section.vertices("Line.3.end"), 5) print("area", section.mass, "exact", 40 * 5 + 35 * 5 + (1 - math.pi / 4) * 5**2) profile.Plot(backend="plotly") ``` ### Padding `px.Pad` extrudes the profile into a solid along the normal of its plane, 60 mm, toward $-y$ for `px.XZ`. Every side face carries the name of the profile edge it comes from, so the outer faces of the legs are `base` and `back` (see [Pad and Pocket](../core/3d/pad-pocket.md)). ```python bracket = px.Pad(section, 60) print(sorted({f.name for f in bracket.faces if f.name}), "| volume", bracket.mass) ``` ### Sketching on the faces A sketch on a face takes the face's own plane, and its coordinates follow that plane's axes. `px.Plane(face)` shows them before anything is drawn. On `base`, $h$ runs along $x$ and $v$ along $-y$, so a hole 15 mm into the bracket is at (20, 15). On `back`, $h$ runs along $y$ and $v$ along $-z$, so a hole 15 mm into the bracket and 22.5 mm up is at (−15, −22.5). See [Placing a sketch](../core/sketch/placement.md) for how a face's axes are chosen. ```python print("base | h", px.Plane(bracket.faces["base"][0]).h, "| v", px.Plane(bracket.faces["base"][0]).v) print("back | h", px.Plane(bracket.faces["back"][0]).h, "| v", px.Plane(bracket.faces["back"][0]).v) base = px.QuickSketch(bracket.faces["base"][0]).Circle((20, 15), 3.3).Circle((20, 45), 3.3) back = px.QuickSketch(bracket.faces["back"][0]).Circle((-15, -22.5), 3.3).Circle((-45, -22.5), 3.3) ``` ### Pocketing the holes `px.Pocket` cuts each sketch into the solid, and `until=px.last` takes it through to the last face it meets, so the holes go through the 5 mm leg whatever its thickness. The volume is the profile's area times 60 less the four holes. ```python bracket = px.Pocket(bracket, base, until=px.last) bracket = px.Pocket(bracket, back, until=px.last) print("volume", bracket.mass, "| profile area × 60 less the holes", section.mass * 60 - 4 * math.pi * 3.3**2 * 5) Draw(bracket) ``` ### Writing STEP `WriteStep` writes the solid as a STEP file, in the millimeters it is drawn in, and OpenCASCADE reads it back with the same volume. The file is [angle-bracket.step](angle-bracket.step). See [To OCC shapes](../integrations/occ/export.md) for the other exports, and [From DXF drawing to mesh](dxf-to-mesh.md) for meshing. ```python bracket.WriteStep("angle-bracket.step") print("read back", occ.OCCGeometry("angle-bracket.step").shape.mass) ``` ## Where the profile comes from | Source | Call | See | |---|---|---| | Code | `px.QuickSketch(plane)`, `px.Sketch(plane)` | [Placing a sketch](../core/sketch/placement.md) | | DXF | `px.FromDXF(path, plane=...)` | [Files](../core/files.md) | | SVG | `px.FromSVG(path, plane=...)` | [SVG files](../core/files/svg.md) | | STEP, BREP (planar) | `px.FromStep(path)`, `px.FromBrep(path)` | [STEP and BREP](../core/files/step-brep.md) | | .CATPart sketches | `px.FromCATPart(path)` | [CATIA V5](../integrations/catia.md) | | Shapely | `px.FromShapely(geometry)` | [Shapely](../integrations/shapely.md) | ## Pitfalls - **Units.** The solid is in the numbers it is drawn in, and OpenCASCADE writes them as millimeters. A profile read from a file comes in meters, and `scale=1` on the reader keeps the file's millimeter numbers instead. - **The direction of the pad.** A pad runs along the normal of the profile's plane, toward $-y$ for `px.XZ`, whose normal points to $-y$ so that $x$ runs to the right and $z$ up. - **A face's axes.** A sketch on a face uses that face's plane, so print `px.Plane(face)` before placing anything. - **Names.** Only named profile edges give named faces, so name the edges whose faces are needed later.