Merge branch 'dev' into lexer

This commit is contained in:
Jonathan Wagenet 2025-10-21 13:59:45 -04:00
commit 8c32e3bed3
42 changed files with 3806 additions and 398 deletions

BIN
docs/_static/spitfire_wing.glb vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,8 @@
<?xml version='1.0' encoding='utf-8'?>
<svg width="100.089989mm" height="13.093747mm" viewBox="-0.000798 -0.085182 1.001332 0.130994" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g transform="scale(1,-1)" stroke-linecap="round">
<g fill="none" stroke="rgb(0,0,0)" stroke-width="0.0009003885533792664">
<path d="M 1.0,-0.0 C 0.829823,0.035412 0.658159,0.060683 0.48501,0.075813 C 0.403802,0.083052 0.322454,0.085791 0.240967,0.084029 C 0.198485,0.08366 0.156475,0.079149 0.114938,0.070497 C 0.074523,0.05954 0.027058,0.048994 0.001793,0.012613 C -0.0025,0.000458 0.001085,-0.00896 0.012548,-0.01564 C 0.02178,-0.020765 0.031585,-0.024392 0.041962,-0.026521 C 0.062557,-0.030869 0.083358,-0.033834 0.104363,-0.035416 C 0.149789,-0.038322 0.195215,-0.041227 0.24064,-0.044133 C 0.321442,-0.046309 0.402208,-0.045216 0.482939,-0.040853 C 0.655652,-0.03182 0.828005,-0.018202 1.0,-0.0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 924 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -0,0 +1,11 @@
<?xml version='1.0' encoding='utf-8'?>
<svg width="100.09mm" height="66.151142mm" viewBox="-541.648921 -836.924186 4767.847817 3151.14975" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g transform="scale(1,-1)" stroke-linecap="round">
<g fill="none" stroke="rgb(0,0,0)" stroke-width="4.28720455097892">
<path d="M 1445.821235,834.74528 C 1353.526765,803.718243 1260.88331,770.545273 1167.994034,735.580745 C 1083.806246,703.883592 999.414296,670.712071 914.854789,636.113423 C 835.638754,603.69438 756.274425,570.022422 676.77418,534.952257 C 592.583511,497.803918 508.238612,459.095508 423.75228,418.390166 C 381.475605,398.017947 339.163641,377.140102 296.820744,355.66775 C 255.380094,334.651781 213.909574,313.067151 172.417787,290.800527 C 132.498839,269.376021 92.559713,247.323024 52.616058,224.499576 C 33.433555,213.541072 14.251486,202.395739 -4.930148,191.063577 C -23.484296,180.09936 -42.03687,168.953007 -60.583168,157.587759 C -78.431472,146.650245 -96.273964,135.510001 -114.110039,124.195809 C -131.629708,113.082326 -149.143185,101.801012 -166.62766,90.088045 C -183.749938,78.617716 -200.844402,66.733433 -217.887184,54.406354 C -249.053676,31.859239 -280.057077,7.844124 -310.705712,-17.830688 C -324.493158,-29.38285 -338.206616,-41.273839 -351.816854,-53.534273 C -364.358757,-64.832326 -376.813009,-76.4441 -389.144607,-88.405566 C -400.391943,-99.315314 -411.537246,-110.515963 -422.53694,-122.049338 C -432.452058,-132.445516 -442.248865,-143.112041 -451.870806,-154.096773 C -460.428098,-163.866068 -468.84708,-173.887048 -477.05134,-184.212339 C -484.238191,-193.257193 -491.260272,-202.535563 -498.010658,-212.099865 C -503.829085,-220.343718 -509.445657,-228.800001 -514.706406,-237.507752 C -519.174839,-244.90404 -523.386558,-252.481753 -527.120208,-260.233274 C -530.2732,-266.779278 -533.085259,-273.449232 -535.256161,-280.125931 C -537.126266,-285.877518 -538.520579,-291.634109 -539.139475,-297.089851 C -539.710024,-302.119395 -539.621572,-306.893256 -538.815367,-311.085197 C -538.009295,-315.276448 -536.485707,-318.88597 -534.41361,-321.787038 C -532.173969,-324.922679 -529.293531,-327.230668 -526.080734,-328.912927 C -522.384603,-330.848265 -518.248591,-331.955443 -513.912155,-332.549837 C -508.837989,-333.245352 -503.489405,-333.238769 -498.016517,-332.811088 C -491.653993,-332.313885 -485.123469,-331.247553 -478.514247,-329.834557 C -470.940437,-328.21534 -463.263282,-326.140884 -455.535716,-323.780678 C -446.817103,-321.117779 -438.03432,-318.091135 -429.219119,-314.830251 C -419.4158,-311.203845 -409.572388,-307.287744 -399.708184,-303.182158 C -388.876522,-298.673905 -378.01979,-293.937177 -367.149854,-289.051048 C -355.343719,-283.74409 -343.522009,-278.26089 -331.692086,-272.665067 C -318.963322,-266.644073 -306.225051,-260.492693 -293.48194,-254.263696 C -279.88052,-247.615147 -266.273587,-240.878173 -252.664091,-234.095751 C -238.238065,-226.906402 -223.809159,-219.665987 -209.379903,-212.417551 C -194.175885,-204.777738 -178.971635,-197.135641 -163.767154,-189.491259 C -147.290706,-181.209732 -130.81417,-172.933298 -114.327757,-164.445833 C -96.638051,-155.338894 -78.936973,-145.988995 -61.23183,-136.489711 C -42.83628,-126.618999 -24.437139,-116.590472 -6.034405,-106.404129 C 12.988826,-95.875361 32.015044,-85.181632 51.041971,-74.350743 C 90.663309,-51.796303 130.287184,-28.649245 169.904219,-5.058022 C 211.090446,19.468633 252.269483,44.474456 293.437331,69.839459 C 335.517614,95.767754 377.586396,122.072076 419.642854,148.660515 C 503.76059,201.847286 587.828904,256.147719 671.854879,311.120502 C 751.312767,363.113312 830.733706,415.715977 910.121501,468.777679 C 995.01224,525.524664 1079.865697,582.797171 1164.674248,640.617824 C 1192.283028,659.441198 1219.886541,678.326945 1247.484787,697.275067 C 1270.73957,713.242184 1293.996044,729.258338 1317.241888,745.321373 C 1327.107887,752.139255 1336.976715,758.968255 1346.848371,765.808374 C 1355.493823,771.798596 1364.146132,777.799924 1372.767724,783.793106 C 1380.127277,788.908999 1387.464446,794.018955 1394.889693,799.180553 C 1401.009487,803.434676 1407.189112,807.723879 1413.119965,811.887911 C 1417.7599,815.145595 1422.247573,818.326668 1427.380659,821.845665 C 1431.063255,824.370277 1435.078042,827.068818 1437.610752,828.998585 C 1439.134823,830.159833 1440.12222,831.0427 1443.76641,833.306567 C 1444.982916,834.062292 1446.495484,834.97191 1445.821235,834.74528" />
<path d="M 4088.698422,-2201.97176 C 4071.863286,-2209.539657 4054.994642,-2217.480665 4038.106206,-2225.738798 C 4022.573261,-2233.338843 4007.022586,-2241.206509 3991.458597,-2249.421198 C 3983.516432,-2253.614748 3975.570735,-2257.900919 3967.622793,-2262.30905 C 3960.261943,-2266.392612 3952.898981,-2270.579106 3945.536956,-2274.915202 C 3943.845675,-2275.912386 3942.154454,-2276.917073 3940.463292,-2277.929262 C 3938.829503,-2278.906677 3937.195728,-2279.889856 3935.56293,-2280.905309 C 3931.124503,-2283.667466 3926.692829,-2286.658395 3922.292205,-2289.898531 C 3918.777241,-2292.48891 3915.280436,-2295.235265 3911.829585,-2298.184808 C 3910.073846,-2299.687297 3908.329767,-2301.242202 3906.611091,-2302.876449 C 3905.350277,-2304.077409 3904.101247,-2305.31876 3902.891914,-2306.64651 C 3902.453922,-2307.124054 3902.026791,-2307.61984 3901.610521,-2308.133868 C 3901.299294,-2308.518314 3901.002293,-2308.919081 3900.719517,-2309.336169 C 3900.527397,-2309.621912 3900.344384,-2309.916518 3900.220419,-2310.243794 C 3900.138415,-2310.46029 3900.08225,-2310.691082 3900.113043,-2310.851579 C 3900.143825,-2311.012017 3900.2615,-2311.10221 3900.393136,-2311.146406 C 3900.591347,-2311.212954 3900.82121,-2311.175213 3901.054853,-2311.119196 C 3901.395934,-2311.034587 3901.742062,-2310.920466 3902.093238,-2310.776833 C 3902.560145,-2310.589421 3903.03076,-2310.365917 3903.501882,-2310.129577 C 3904.091767,-2309.832167 3904.682134,-2309.519273 3905.272983,-2309.190897 C 3906.803549,-2308.34235 3908.334632,-2307.429826 3909.864882,-2306.507962 C 3911.837632,-2305.316998 3913.809818,-2304.101833 3915.782074,-2302.891418 C 3918.161608,-2301.426976 3920.541478,-2299.976332 3922.921685,-2298.539486 C 3927.169463,-2295.979761 3931.419103,-2293.492922 3935.667474,-2291.12314 C 3937.263565,-2290.232961 3938.85938,-2289.359703 3940.455335,-2288.478295 C 3942.141204,-2287.546398 3943.827175,-2286.607933 3945.51325,-2285.6629 C 3952.851904,-2281.554558 3960.1915,-2277.358362 3967.530929,-2273.121841 C 3975.456854,-2268.545879 3983.382716,-2263.92158 3991.308409,-2259.279052 C 4006.85028,-2250.172017 4022.391679,-2241.00337 4037.933217,-2231.854444 C 4045.982183,-2227.115049 4054.031164,-2222.377487 4062.08016,-2217.64176 C 4066.897545,-2214.806906 4071.71493,-2211.972052 4076.532315,-2209.137199 C 4078.284341,-2208.105943 4080.036161,-2207.074566 4081.787775,-2206.043066 C 4082.482313,-2205.633843 4083.180026,-2205.222922 4083.880913,-2204.810304 C 4084.459844,-2204.469581 4085.044497,-2204.12585 4085.605526,-2203.794355 C 4086.04433,-2203.535079 4086.468682,-2203.283289 4086.954417,-2202.999576 C 4087.302828,-2202.796074 4087.682821,-2202.576147 4087.921957,-2202.429398 C 4088.065839,-2202.341102 4088.15873,-2202.279298 4088.504102,-2202.086294 C 4088.619387,-2202.02187 4088.762804,-2201.942827 4088.698422,-2201.97176" />
<path d="M -538.815367,-311.085197 L -91.009814,-565.713777 L 357.94024,-813.228178 L 802.388988,-1050.515762 L 1236.747229,-1274.592501 L 1655.552655,-1482.6405 L 2053.538547,-1672.043435 L 2425.7,-1840.419453 L 2767.356868,-1985.651129 L 3074.212618,-2105.912089 L 3342.40836,-2199.689984 L 3568.571378,-2265.8055 L 3749.85754,-2303.427198 L 3883.987068,-2312.081962 L 3969.273206,-2291.660954" />
<path d="M 3969.273206,-2291.660954 L 4106.195866,-2183.789653 L 4191.480731,-2048.45593 L 4224.055294,-1887.361685 L 4203.50991,-1702.532772 L 4130.102951,-1496.293521 L 4004.75755,-1271.237512 L 3829.05,-1030.194953 L 3605.189924,-776.197095 L 3335.992494,-512.438108 L 3024.843022,-242.234914 L 2675.654394,31.014524 L 2292.817859,303.873937 L 1881.14781,572.911958 L 1445.821235,834.74528" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before After
Before After

View file

@ -15,6 +15,7 @@ Cheat Sheet
.. grid-item-card:: 1D - BuildLine
| :class:`~objects_curve.Airfoil`
| :class:`~objects_curve.ArcArcTangentArc`
| :class:`~objects_curve.ArcArcTangentLine`
| :class:`~objects_curve.Bezier`

68
docs/heart_token.py Normal file
View file

@ -0,0 +1,68 @@
# [Code]
from build123d import *
from ocp_vscode import show
# Create the edges of one half the heart surface
l1 = JernArc((0, 0), (1, 1.4), 40, -17)
l2 = JernArc(l1 @ 1, l1 % 1, 4.5, 175)
l3 = IntersectingLine(l2 @ 1, l2 % 1, other=Edge.make_line((0, 0), (0, 20)))
l4 = ThreePointArc(l3 @ 1, (0, 0, 1.5) + (l3 @ 1 + l1 @ 0) / 2, l1 @ 0)
heart_half = Wire([l1, l2, l3, l4])
# [SurfaceEdges]
# Create a point elevated off the center
surface_pnt = l2.arc_center + (0, 0, 1.5)
# [SurfacePoint]
# Create the surface from the edges and point
top_right_surface = Pos(Z=0.5) * -Face.make_surface(heart_half, [surface_pnt])
# [Surface]
# Use the mirror method to create the other top and bottom surfaces
top_left_surface = top_right_surface.mirror(Plane.YZ)
bottom_right_surface = top_right_surface.mirror(Plane.XY)
bottom_left_surface = -top_left_surface.mirror(Plane.XY)
# [Surfaces]
# Create the left and right sides
left_wire = Wire([l3, l2, l1])
left_side = Pos(Z=-0.5) * Shell.extrude(left_wire, (0, 0, 1))
right_side = left_side.mirror(Plane.YZ)
# [Sides]
# Put all of the faces together into a Shell/Solid
heart = Solid(
Shell(
[
top_right_surface,
top_left_surface,
bottom_right_surface,
bottom_left_surface,
left_side,
right_side,
]
)
)
# [Solid]
# Build a frame around the heart
with BuildPart() as heart_token:
with BuildSketch() as outline:
with BuildLine():
add(l1)
add(l2)
add(l3)
Line(l3 @ 1, l1 @ 0)
make_face()
mirror(about=Plane.YZ)
center = outline.sketch
offset(amount=2, kind=Kind.INTERSECTION)
add(center, mode=Mode.SUBTRACT)
extrude(amount=2, both=True)
add(heart)
heart_token.part.color = "Red"
show(heart_token)
# [End]
# export_gltf(heart_token.part, "heart_token.glb", binary=True)

View file

@ -76,6 +76,13 @@ The following objects all can be used in BuildLine contexts. Note that
.. grid:: 3
.. grid-item-card:: :class:`~objects_curve.Airfoil`
.. image:: assets/example_airfoil.svg
+++
Airfoil described by 4 digit NACA profile
.. grid-item-card:: :class:`~objects_curve.Bezier`
.. image:: assets/bezier_curve_example.svg
@ -228,6 +235,7 @@ Reference
.. py:module:: objects_curve
.. autoclass:: BaseLineObject
.. autoclass:: Airfoil
.. autoclass:: Bezier
.. autoclass:: BlendCurve
.. autoclass:: CenterArc

View file

@ -0,0 +1,77 @@
"""
Supermarine Spitfire Wing
"""
# [Code]
from build123d import *
from ocp_vscode import show
wing_span = 36 * FT + 10 * IN
wing_leading = 2.5 * FT
wing_trailing = wing_span / 4 - wing_leading
wing_leading_fraction = wing_leading / (wing_leading + wing_trailing)
wing_tip_section = wing_span / 2 - 1 * IN # distance from root to last section
# Create leading and trailing edges
leading_edge = EllipticalCenterArc(
(0, 0), wing_span / 2, wing_leading, start_angle=270, end_angle=360
)
trailing_edge = EllipticalCenterArc(
(0, 0), wing_span / 2, wing_trailing, start_angle=0, end_angle=90
)
# [AirfoilSizes]
# Calculate the airfoil sizes from the leading/trailing edges
airfoil_sizes = []
for i in [0, 1]:
tip_axis = Axis(i * (wing_tip_section, 0, 0), (0, 1, 0))
leading_pnt = leading_edge.intersect(tip_axis)[0]
trailing_pnt = trailing_edge.intersect(tip_axis)[0]
airfoil_sizes.append(trailing_pnt.Y - leading_pnt.Y)
# [Airfoils]
# Create the root and tip airfoils - note that they are different NACA profiles
airfoil_root = Plane.YZ * scale(
Airfoil("2213").translate((-wing_leading_fraction, 0, 0)), airfoil_sizes[0]
)
airfoil_tip = (
Plane.YZ
* Pos(Z=wing_tip_section)
* scale(Airfoil("2205").translate((-wing_leading_fraction, 0, 0)), airfoil_sizes[1])
)
# [Profiles]
# Create the Gordon surface profiles and guides
profiles = airfoil_root.edges() + airfoil_tip.edges()
profiles.append(leading_edge @ 1) # wing tip
guides = [leading_edge, trailing_edge]
# Create the wing surface as a Gordon Surface
wing_surface = -Face.make_gordon_surface(profiles, guides)
# Create the root of the wing
wing_root = -Face(Wire(wing_surface.edges().filter_by(Edge.is_closed)))
# [Solid]
# Create the wing Solid
wing = Solid(Shell([wing_surface, wing_root]))
wing.color = 0x99A3B9 # Azure Blue
show(wing)
# [End]
# Documentation artifact generation
# wing_control_edges = Curve(
# [airfoil_root, airfoil_tip, Vertex(leading_edge @ 1), leading_edge, trailing_edge]
# )
# visible, _ = wing_control_edges.project_to_viewport((50 * FT, -50 * FT, 50 * FT))
# max_dimension = max(*Compound(children=visible).bounding_box().size)
# svg = ExportSVG(scale=100 / max_dimension)
# svg.add_shape(visible)
# svg.write("assets/surface_modeling/spitfire_wing_profiles_guides.svg")
# export_gltf(
# wing,
# "assets/surface_modeling/spitfire_wing.glb",
# binary=True,
# linear_deflection=0.1,
# angular_deflection=1,
# )

View file

@ -0,0 +1,106 @@
#############################################
Tutorial: Spitfire Wing with Gordon Surface
#############################################
In this advanced tutorial we construct a Supermarine Spitfire wing as a
:meth:`~topology.Face.make_gordon_surface`—a powerful technique for surfacing
from intersecting *profiles* and *guides*. A Gordon surface blends a grid of
curves into a smooth, coherent surface as long as the profiles and guides
intersect consistently.
.. note::
Gordon surfaces work best when *each profile intersects each guide exactly
once*, producing a wellformed curve network.
Overview
========
We will:
1. Define overall wing dimensions and elliptic leading/trailing edge guide curves
2. Sample the guides to size the root and tip airfoils (different NACA profiles)
3. Build the Gordon surface from the airfoil *profiles* and wingedge *guides*
4. Close the root with a planar face and build the final :class:`~topology.Solid`
.. raw:: html
<script type="module" src="https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js"></script>
<model-viewer poster="_images/spitfire_wing.png" src="_static/spitfire_wing.glb" alt="A tea cup modelled in build123d" auto-rotate camera-controls style="width: 100%; height: 50vh;"></model-viewer>
Step 1 — Dimensions and guide curves
====================================
We model a single wing (halfspan), with an elliptic leading and trailing edge.
These two edges act as the *guides* for the Gordon surface.
.. literalinclude:: spitfire_wing_gordon.py
:start-after: [Code]
:end-before: [AirfoilSizes]
Step 2 — Root and tip airfoil sizing
====================================
We intersect the guides with planes normal to the span to size the airfoil sections.
The resulting chord lengths define uniform scales for each airfoil curve.
.. literalinclude:: spitfire_wing_gordon.py
:start-after: [AirfoilSizes]
:end-before: [Airfoils]
Step 3 — Build airfoil profiles (root and tip)
==============================================
We place two different NACA airfoils on :data:`Plane.YZ`—with the airfoil origins
shifted so the leading edge fraction is aligned—then scale to the chord lengths
from Step 2.
.. literalinclude:: spitfire_wing_gordon.py
:start-after: [Airfoils]
:end-before: [Profiles]
Step 4 — Gordon surface construction
====================================
A Gordon surface needs *profiles* and *guides*. Here the airfoil edges are the
profiles; the elliptic edges are the guides. We also add the wing tip section
so the profile grid closes at the tip.
.. literalinclude:: spitfire_wing_gordon.py
:start-after: [Profiles]
:end-before: [Solid]
.. image:: ./assets/surface_modeling/spitfire_wing_profiles_guides.svg
:align: center
:alt: Elliptic leading/trailing guides
Step 5 — Cap the root and create the solid
==========================================
We extract the closed root edge loop, make a planar cap, and form a solid shell.
.. literalinclude:: spitfire_wing_gordon.py
:start-after: [Solid]
:end-before: [End]
.. image:: ./assets/surface_modeling/spitfire_wing.png
:align: center
:alt: Final wing solid
Tips for robust Gordon surfaces
-------------------------------
- Ensure each profile intersects each guide once and only once
- Keep the curve network coherent (no duplicated or missing intersections)
- When possible, reuse the same :class:`~topology.Edge` objects across adjacent faces
Complete listing
================
For convenience, here is the full script in one block:
.. literalinclude:: spitfire_wing_gordon.py
:start-after: [Code]
:end-before: [End]

View file

@ -0,0 +1,125 @@
##################################
Tutorial: Heart Token (Basics)
##################################
This handson tutorial introduces the fundamentals of surface modeling by building
a heartshaped token from a small set of nonplanar faces. Well create
nonplanar surfaces, mirror them, add side faces, and assemble a closed shell
into a solid.
As described in the `topology_` section, a BREP model consists of vertices, edges, faces,
and other elements that define the boundary of an object. When creating objects with
non-planar faces, it is often more convenient to explicitly create the boundary faces of
the object. To illustrate this process, we will create the following game token:
.. raw:: html
<script type="module" src="https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js"></script>
<model-viewer poster="_images/heart_token.png" src="_static/heart_token.glb" alt="Game Token" auto-rotate camera-controls style="width: 100%; height: 50vh;"></model-viewer>
Useful :class:`~topology.Face` creation methods include
:meth:`~topology.Face.make_surface`, :meth:`~topology.Face.make_bezier_surface`,
and :meth:`~topology.Face.make_surface_from_array_of_points`. See the
:doc:`surface_modeling` overview for the full list.
In this case, we'll use the ``make_surface`` method, providing it with the edges that define
the perimeter of the surface and a central point on that surface.
To create the perimeter, we'll define the perimeter edges. Since the heart is
symmetric, we'll only create half of its surface here:
.. literalinclude:: heart_token.py
:start-after: [Code]
:end-before: [SurfaceEdges]
Note that ``l4`` is not in the same plane as the other lines; it defines the center line
of the heart and archs up off ``Plane.XY``.
.. image:: ./assets/surface_modeling/token_heart_perimeter.png
:align: center
:alt: token perimeter
In preparation for creating the surface, we'll define a point on the surface:
.. literalinclude:: heart_token.py
:start-after: [SurfaceEdges]
:end-before: [SurfacePoint]
We will then use this point to create a non-planar ``Face``:
.. literalinclude:: heart_token.py
:start-after: [SurfacePoint]
:end-before: [Surface]
.. image:: ./assets/surface_modeling/token_half_surface.png
:align: center
:alt: token perimeter
Note that the surface was raised up by 0.5 using an Algebra expression with Pos. Also,
note that the ``-`` in front of ``Face`` simply flips the face normal so that the colored
side is up, which isn't necessary but helps with viewing.
Now that one half of the top of the heart has been created, the remainder of the top
and bottom can be created by mirroring:
.. literalinclude:: heart_token.py
:start-after: [Surface]
:end-before: [Surfaces]
The sides of the heart are going to be created by extruding the outside of the perimeter
as follows:
.. literalinclude:: heart_token.py
:start-after: [Surfaces]
:end-before: [Sides]
.. image:: ./assets/surface_modeling/token_sides.png
:align: center
:alt: token sides
With the top, bottom, and sides, the complete boundary of the object is defined. We can
now put them together, first into a :class:`~topology.Shell` and then into a
:class:`~topology.Solid`:
.. literalinclude:: heart_token.py
:start-after: [Sides]
:end-before: [Solid]
.. image:: ./assets/surface_modeling/token_heart_solid.png
:align: center
:alt: token heart solid
.. note::
When creating a Solid from a Shell, the Shell must be "water-tight," meaning it
should have no holes. For objects with complex Edges, it's best practice to reuse
Edges in adjoining Faces whenever possible to avoid slight mismatches that can
create openings.
Finally, we'll create the frame around the heart as a simple extrusion of a planar
shape defined by the perimeter of the heart and merge all of the components together:
.. literalinclude:: heart_token.py
:start-after: [Solid]
:end-before: [End]
Note that an additional planar line is used to close ``l1`` and ``l3`` so a ``Face``
can be created. The :func:`~operations_generic.offset` function defines the outside of
the frame as a constant distance from the heart itself.
Summary
-------
In this tutorial, we've explored surface modeling techniques to create a non-planar
heart-shaped object using build123d. By utilizing methods from the :class:`~topology.Face`
class, such as :meth:`~topology.Face.make_surface`, we constructed the perimeter and
central point of the surface. We then assembled the complete boundary of the object
by creating the top, bottom, and sides, and combined them into a :class:`~topology.Shell`
and eventually a :class:`~topology.Solid`. Finally, we added a frame around the heart
using the :func:`~operations_generic.offset` function to maintain a constant distance
from the heart.
Next steps
----------
Continue to :doc:`tutorial_heart_token` for an advanced example using
:meth:`~topology.Face.make_gordon_surface` to create a Supermarine Spitfire wing.

View file

@ -1,156 +1,55 @@
################
#################
Surface Modeling
################
#################
Surface modeling is employed to create objects with non-planar surfaces that can't be
generated using functions like :func:`~operations_part.extrude`,
:func:`~operations_generic.sweep`, or :func:`~operations_part.revolve`. Since there are no
specific builders designed to assist with the creation of non-planar surfaces or objects,
the following should be considered a more advanced technique.
As described in the `topology_` section, a BREP model consists of vertices, edges, faces,
and other elements that define the boundary of an object. When creating objects with
non-planar faces, it is often more convenient to explicitly create the boundary faces of
the object. To illustrate this process, we will create the following game token:
Surface modeling refers to the direct creation and manipulation of the skin of a 3D
object—its bounding faces—rather than starting from volumetric primitives or solid
operations.
.. raw:: html
Instead of defining a shape by extruding or revolving a 2D profile to fill a volume,
surface modeling focuses on building the individual curved or planar faces that together
define the outer boundary of a part. This approach allows for precise control of complex
freeform geometry such as aerodynamic surfaces, boat hulls, or organic transitions that
cannot easily be expressed with simple parametric solids.
<script type="module" src="https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js"></script>
<model-viewer poster="_images/heart_token.png" src="_static/heart_token.glb" alt="Game Token" auto-rotate camera-controls style="width: 100%; height: 50vh;"></model-viewer>
In build123d, as in other CAD kernels based on BREP (Boundary Representation) modeling,
all solids are ultimately defined by their boundaries: a hierarchy of faces, edges, and
vertices. Each face represents a finite patch of a geometric surface (plane, cylinder,
Bézier patch, etc.) bounded by one or more edge loops or wires. When adjacent faces share
edges consistently and close into a continuous boundary, they form a manifold
:class:`~topology.Shell`—the watertight surface of a volume. If this shell is properly
oriented and encloses a finite region of space, the model becomes a solid.
There are several methods of the :class:`~topology.Face` class that can be used to create
non-planar surfaces:
Surface modeling therefore operates at the most fundamental level of BREP construction.
Rather than relying on higher-level modeling operations to implicitly generate faces,
it allows you to construct and connect those faces explicitly. This provides a path to
build geometry that blends analytical and freeform shapes seamlessly, with full control
over continuity, tangency, and curvature across boundaries.
* :meth:`~topology.Face.make_bezier_surface`,
* :meth:`~topology.Face.make_surface`, and
* :meth:`~topology.Face.make_surface_from_array_of_points`.
This section provides:
- A concise overview of surfacebuilding tools in build123d
- Handson tutorials, from fundamentals to advanced techniques like Gordon surfaces
In this case, we'll use the ``make_surface`` method, providing it with the edges that define
the perimeter of the surface and a central point on that surface.
.. rubric:: Available surface methods
To create the perimeter, we'll use a ``BuildLine`` instance as follows. Since the heart is
symmetric, we'll only create half of its surface here:
Methods on :class:`~topology.Face` for creating nonplanar surfaces:
.. code-block:: build123d
with BuildLine() as heart_half:
l1 = JernArc((0, 0), (1, 1.4), 40, -17)
l2 = JernArc(l1 @ 1, l1 % 1, 4.5, 175)
l3 = IntersectingLine(l2 @ 1, l2 % 1, other=Edge.make_line((0, 0), (0, 20)))
l4 = ThreePointArc(l3 @ 1, Vector(0, 0, 1.5) + (l3 @ 1 + l1 @ 0) / 2, l1 @ 0)
Note that ``l4`` is not in the same plane as the other lines; it defines the center line
of the heart and archs up off ``Plane.XY``.
.. image:: ./assets/token_heart_perimeter.png
:align: center
:alt: token perimeter
In preparation for creating the surface, we'll define a point on the surface:
.. code-block:: build123d
surface_pnt = l2.edge().arc_center + Vector(0, 0, 1.5)
We will then use this point to create a non-planar ``Face``:
.. code-block:: build123d
top_right_surface = -Face.make_surface(heart_half.wire(), [surface_pnt]).locate(
Pos(Z=0.5)
)
.. image:: ./assets/token_half_surface.png
:align: center
:alt: token perimeter
Note that the surface was raised up by 0.5 using the locate method. Also, note that
the ``-`` in front of ``Face`` simply flips the face normal so that the colored side
is up, which isn't necessary but helps with viewing.
Now that one half of the top of the heart has been created, the remainder of the top
and bottom can be created by mirroring:
.. code-block:: build123d
top_left_surface = top_right_surface.mirror(Plane.YZ)
bottom_right_surface = top_right_surface.mirror(Plane.XY)
bottom_left_surface = -top_left_surface.mirror(Plane.XY)
The sides of the heart are going to be created by extruding the outside of the perimeter
as follows:
.. code-block:: build123d
left_wire = Wire([l3.edge(), l2.edge(), l1.edge()])
left_side = Face.extrude(left_wire, (0, 0, 1)).locate(Pos(Z=-0.5))
right_side = left_side.mirror(Plane.YZ)
.. image:: ./assets/token_sides.png
:align: center
:alt: token sides
With the top, bottom, and sides, the complete boundary of the object is defined. We can
now put them together, first into a :class:`~topology.Shell` and then into a
:class:`~topology.Solid`:
.. code-block:: build123d
heart = Solid(
Shell(
[
top_right_surface,
top_left_surface,
bottom_right_surface,
bottom_left_surface,
left_side,
right_side,
]
)
)
.. image:: ./assets/token_heart_solid.png
:align: center
:alt: token heart solid
* :meth:`~topology.Face.make_bezier_surface`
* :meth:`~topology.Face.make_gordon_surface`
* :meth:`~topology.Face.make_surface`
* :meth:`~topology.Face.make_surface_from_array_of_points`
* :meth:`~topology.Face.make_surface_from_curves`
* :meth:`~topology.Face.make_surface_patch`
.. note::
When creating a Solid from a Shell, the Shell must be "water-tight," meaning it
should have no holes. For objects with complex Edges, it's best practice to reuse
Edges in adjoining Faces whenever possible to avoid slight mismatches that can
create openings.
Surface modeling is an advanced technique. Robust results usually come from
reusing the same :class:`~topology.Edge` objects across adjacent faces and
ensuring the final :class:`~topology.Shell` is *watertight* or *manifold* (no gaps).
Finally, we'll create the frame around the heart as a simple extrusion of a planar
shape defined by the perimeter of the heart and merge all of the components together:
.. toctree::
:maxdepth: 1
.. code-block:: build123d
tutorial_surface_heart_token.rst
tutorial_spitfire_wing_gordon.rst
with BuildPart() as heart_token:
with BuildSketch() as outline:
with BuildLine():
add(l1)
add(l2)
add(l3)
Line(l3 @ 1, l1 @ 0)
make_face()
mirror(about=Plane.YZ)
center = outline.sketch
offset(amount=2, kind=Kind.INTERSECTION)
add(center, mode=Mode.SUBTRACT)
extrude(amount=2, both=True)
add(heart)
Note that an additional planar line is used to close ``l1`` and ``l3`` so a ``Face``
can be created. The :func:`~operations_generic.offset` function defines the outside of
the frame as a constant distance from the heart itself.
Summary
-------
In this tutorial, we've explored surface modeling techniques to create a non-planar
heart-shaped object using build123d. By utilizing methods from the :class:`~topology.Face`
class, such as :meth:`~topology.Face.make_surface`, we constructed the perimeter and
central point of the surface. We then assembled the complete boundary of the object
by creating the top, bottom, and sides, and combined them into a :class:`~topology.Shell`
and eventually a :class:`~topology.Solid`. Finally, we added a frame around the heart
using the :func:`~operations_generic.offset` function to maintain a constant distance
from the heart.