diff --git a/docs/literate/src/tut_2d_geometry.jl b/docs/literate/src/tut_2d_geometry.jl
new file mode 100644
index 0000000000..d591d0f6d7
--- /dev/null
+++ b/docs/literate/src/tut_2d_geometry.jl
@@ -0,0 +1,155 @@
+# # [Setting up a 2D simulation from geometry files](@id tut_2d_geometry)
+
+# In this tutorial, we build two genuine 2D setups from geometry files:
+# 1. a curved pipe, where one geometry file defines the outer wall envelope and a second
+# one defines the empty channel cut out of it,
+# 2. a dam-break basin with a coastline profile, where one geometry file defines the
+# filled coastline wall together with the seawall on the right.
+#
+# For a real 2D setup, we use 2D geometry formats such as `.asc` or `.dxf`.
+# STL files are surface meshes and therefore naturally lead to thin 3D setups instead.
+
+# First, we import TrixiParticles.jl together with
+# [OrdinaryDiffEq.jl](https://github.com/SciML/OrdinaryDiffEq.jl)
+# and [Plots.jl](https://docs.juliaplots.org/stable/).
+using TrixiParticles
+using OrdinaryDiffEq
+using Plots
+
+# ## Resolution
+
+# We use the same particle spacing for the fluid and for the wall geometries.
+particle_spacing = 0.03
+fluid_density = 1000.0
+gravity = 9.81
+sound_speed = 10.0
+state_equation = StateEquationCole(; sound_speed, reference_density=fluid_density,
+ exponent=7)
+nothing # hide
+
+# ## Loading 2D geometry files
+
+# The following helper loads a closed 2D geometry file and samples particles in its interior:
+# 1. load the polygon with [`load_geometry`](@ref),
+# 2. fill the polygon with [`ComplexShape`](@ref).
+#
+# This creates a true 2D solid region instead of a hollow shell around the polygon edges.
+function solid_from_geometry_file(file; particle_spacing, density)
+ geometry = load_geometry(file)
+ solid = ComplexShape(geometry; particle_spacing, density,
+ grid_offset=0.5particle_spacing)
+
+ return (; geometry, solid)
+end
+
+# ## A curved pipe from two filled geometries
+
+# The pipe wall is a solid L-shaped region with a channel cut out of it:
+# 1. one geometry file describes the outer pipe envelope,
+# 2. one geometry file describes the empty channel,
+# 3. the `setdiff` operation subtracts the channel from the solid envelope.
+pipe_outer_file = pkgdir(TrixiParticles, "examples", "preprocessing", "data",
+ "curved_pipe_outer_2d.asc")
+pipe_channel_file = pkgdir(TrixiParticles, "examples", "preprocessing", "data",
+ "curved_pipe_channel_2d.asc")
+
+pipe_outer = solid_from_geometry_file(pipe_outer_file; particle_spacing,
+ density=fluid_density)
+pipe_channel = load_geometry(pipe_channel_file)
+
+pipe_setup = (; wall=setdiff(pipe_outer.solid, pipe_channel),
+ outer_geometry=pipe_outer.geometry,
+ channel_geometry=pipe_channel)
+
+# ## A dam-break basin with a coastline profile
+
+# In the second setup, a single 2D geometry file defines a filled coastline wall:
+# the beach profile on top, a finite wall thickness below it, and the seawall on the right.
+coast_file = pkgdir(TrixiParticles, "examples", "preprocessing", "data",
+ "coastline_profile_2d.asc")
+coast = solid_from_geometry_file(coast_file; particle_spacing, density=fluid_density)
+
+# The geometry file gives the coastline bed and the right wall as a solid region.
+# We add the left wall explicitly as a rectangular particle block and place a
+# 1.5x taller rectangular dam-break water column next to it.
+left_wall = RectangularShape(particle_spacing, (5, 50), (0.0, -0.12),
+ density=fluid_density)
+reservoir = RectangularShape(particle_spacing, (28, 42), (0.15, 0.03),
+ acceleration=(0.0, -gravity),
+ state_equation=state_equation)
+coast_setup = (; geometry=coast.geometry,
+ wall=union(coast.solid, left_wall),
+ fluid=setdiff(reservoir, coast.geometry))
+
+p_pipe = plot(pipe_setup.wall, label="wall", title="Curved pipe",
+ markerstrokewidth=0, markersize=4)
+plot!(p_pipe, showaxis=false, aspect_ratio=:equal,
+ xlims=(-0.03, 1.23), ylims=(-0.03, 1.23))
+
+p_coast = plot(coast_setup.fluid, coast_setup.wall,
+ labels=["fluid" "wall"], title="Coastline dam break",
+ markerstrokewidth=0, markersize=3)
+plot!(p_coast, showaxis=false, aspect_ratio=:equal,
+ xlims=(0.0, 2.75), ylims=(-0.15, 1.35))
+
+plot(p_pipe, p_coast, layout=(1, 2), size=(900, 360))
+savefig("tut_2d_geometry_plot.png"); # hide
+# 
+
+# ## Building the simulation systems
+
+# To keep the example focused, we continue with the coastline setup.
+# From this point on, the simulation setup is the same as in other 2D simulation files.
+setup = coast_setup
+tspan = (0.0, 0.03)
+nothing # hide
+
+# We define the state equation, smoothing kernel, and viscosity for a
+# weakly compressible SPH simulation.
+smoothing_length = 1.2 * particle_spacing
+smoothing_kernel = SchoenbergCubicSplineKernel{2}()
+viscosity = ArtificialViscosityMonaghan(alpha=0.02, beta=0.0)
+
+fluid_density_calculator = ContinuityDensity()
+density_diffusion = DensityDiffusionMolteniColagrossi(delta=0.1)
+
+fluid_system = WeaklyCompressibleSPHSystem(setup.fluid, fluid_density_calculator,
+ state_equation, smoothing_kernel,
+ smoothing_length, viscosity=viscosity,
+ density_diffusion=density_diffusion,
+ acceleration=(0.0, -gravity))
+nothing # hide
+
+# For the wall, we reuse the combined solid wall particles created above.
+boundary_model = BoundaryModelDummyParticles(setup.wall.density, setup.wall.mass,
+ state_equation=state_equation,
+ AdamiPressureExtrapolation(),
+ smoothing_kernel, smoothing_length)
+boundary_system = WallBoundarySystem(setup.wall, boundary_model)
+nothing # hide
+
+# ## Semidiscretization
+
+# With fluid and wall particles defined, we can build the
+# [`Semidiscretization`](@ref TrixiParticles.Semidiscretization) exactly as in other tutorials.
+semi = Semidiscretization(fluid_system, boundary_system)
+ode = semidiscretize(semi, tspan)
+nothing # hide
+
+# ## Time integration
+
+# The setup is now complete.
+# To start the simulation, run for example
+# ```julia
+# callbacks = CallbackSet(InfoCallback(interval=10))
+# sol = solve(ode, RDPK3SpFSAL35(), save_everystep=false, callback=callbacks)
+# ```
+# This is the same final step as in [the basic setup tutorial](@ref tut_setup).
+callbacks = CallbackSet(InfoCallback(interval=10))
+nothing # hide
+
+sol = solve(ode, RDPK3SpFSAL35(), save_everystep=false, callback=callbacks) #!md
+
+# For more accurate body-fitted particles around sharper features, you can also
+# apply the [particle packing workflow](@ref tut_packing) to the 2D geometry files
+# before starting the simulation.
diff --git a/docs/make.jl b/docs/make.jl
index ddffab849a..4ac1ae3709 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -40,6 +40,8 @@ Literate.markdown(joinpath("docs", "literate", "src", "tut_custom_kernel.jl"),
joinpath("docs", "src", "tutorials"))
Literate.markdown(joinpath("docs", "literate", "src", "tut_packing.jl"),
joinpath("docs", "src", "tutorials"))
+Literate.markdown(joinpath("docs", "literate", "src", "tut_2d_geometry.jl"),
+ joinpath("docs", "src", "tutorials"))
copy_file("AUTHORS.md",
"in the [LICENSE.md](LICENSE.md) file" => "under [License](@ref)")
@@ -87,6 +89,8 @@ makedocs(sitename="TrixiParticles.jl",
"tut_custom_kernel.md")
],
"Preprocessing" => [
+ "Setting up a 2D simulation from geometry files" => joinpath("tutorials",
+ "tut_2d_geometry.md"),
"Particle packing tutorial" => joinpath("tutorials",
"tut_packing.md")
]
diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md
index 1166010291..b62dbb642c 100644
--- a/docs/src/tutorial.md
+++ b/docs/src/tutorial.md
@@ -1,8 +1,68 @@
# Tutorials
-## General
-- [Setting up your simulation from scratch](tutorials/tut_setup.md)
-- [Modifying or extending components of TrixiParticles.jl within a simulation file](tutorials/tut_custom_kernel.md)
+Choose a tutorial based on the task in front of you.
-## Preprocessing
-- [Particle packing tutorial](tutorials/tut_packing.md)
+> New to TrixiParticles.jl? Start with [Setting up your simulation from scratch](tutorials/tut_setup.md).
+
+## Recommended Path
+
+1. [Setting up your simulation from scratch](tutorials/tut_setup.md): learn the structure of a simulation file and run a complete WCSPH example.
+2. [Modifying or extending components of TrixiParticles.jl within a simulation file](tutorials/tut_custom_kernel.md): replace selected parts of an existing setup without cloning the package.
+3. [Setting up a 2D simulation from geometry files](tutorials/tut_2d_geometry.md): load 2D geometry files, turn them into filled wall regions, and combine them with standard 2D fluid blocks.
+4. [Particle packing tutorial](tutorials/tut_packing.md): build a body-fitted particle configuration for complex geometries.
+
+## Tutorials
+
+### [Setting up your simulation from scratch](tutorials/tut_setup.md)
+
+```@raw html
+
+```
+
+Build a complete weakly compressible SPH dam break setup from particle spacing through semidiscretization, callbacks, and time integration.
+
+- Focus: initial conditions, systems, semidiscretization, callbacks
+- Choose this if: you want the full workflow from a minimal example
+
+### [Modifying or extending components of TrixiParticles.jl within a simulation file](tutorials/tut_custom_kernel.md)
+
+```@raw html
+
+```
+
+Start from an existing simulation and replace pieces such as the smoothing kernel directly in the file you run.
+
+- Focus: `trixi_include`, custom kernels, rapid iteration
+- Choose this if: you want to prototype changes without rewriting a full setup
+
+### [Setting up a 2D simulation from geometry files](tutorials/tut_2d_geometry.md)
+
+```@raw html
+
+```
+
+Load 2D geometry files, fill them with particles using `ComplexShape`, and build genuine 2D setups such as a curved pipe and a coastline dam break.
+
+- Focus: `load_geometry`, `ComplexShape`, `setdiff`, 2D `Polygon`s
+- Choose this if: you want a true 2D setup from line-based geometry data
+
+### [Particle packing tutorial](tutorials/tut_packing.md)
+
+```@raw html
+
+```
+
+Go from a geometry file to a packed particle distribution using signed distance fields together with boundary and interior sampling.
+
+- Focus: geometry import, signed distance fields, boundary sampling, `ParticlePackingSystem`
+- Choose this if: you need body-fitted particles for complex shapes
+
+See also [Getting started](getting_started.md) and [Examples](examples.md).
diff --git a/examples/preprocessing/data/coastline_profile_2d.asc b/examples/preprocessing/data/coastline_profile_2d.asc
new file mode 100644
index 0000000000..7986e07261
--- /dev/null
+++ b/examples/preprocessing/data/coastline_profile_2d.asc
@@ -0,0 +1,20 @@
+# ASCII
+0.18 -0.12 0
+2.68 -0.12 0
+2.68 1.08 0
+2.62 0.66 0
+2.53 0.52 0
+2.42 0.40 0
+2.30 0.42 0
+2.18 0.33 0
+2.05 0.24 0
+1.92 0.26 0
+1.78 0.18 0
+1.62 0.11 0
+1.46 0.14 0
+1.28 0.06 0
+1.05 0.02 0
+0.82 0.05 0
+0.55 0.03 0
+0.18 0.03 0
+0.18 -0.12 0
diff --git a/examples/preprocessing/data/curved_pipe_channel_2d.asc b/examples/preprocessing/data/curved_pipe_channel_2d.asc
new file mode 100644
index 0000000000..76a1d7b8fe
--- /dev/null
+++ b/examples/preprocessing/data/curved_pipe_channel_2d.asc
@@ -0,0 +1,20 @@
+# ASCII
+0.00 0.12 0
+0.60 0.12 0
+0.72423 0.13646 0
+0.84000 0.18431 0
+0.93941 0.26059 0
+1.01569 0.36000 0
+1.06354 0.47577 0
+1.08 0.60 0
+1.08 1.20 0
+0.72 1.20 0
+0.72 0.60 0
+0.71591 0.56894 0
+0.70392 0.54000 0
+0.68485 0.51515 0
+0.66000 0.49608 0
+0.63106 0.48409 0
+0.60 0.48 0
+0.00 0.48 0
+0.00 0.12 0
diff --git a/examples/preprocessing/data/curved_pipe_outer_2d.asc b/examples/preprocessing/data/curved_pipe_outer_2d.asc
new file mode 100644
index 0000000000..1b26afac92
--- /dev/null
+++ b/examples/preprocessing/data/curved_pipe_outer_2d.asc
@@ -0,0 +1,14 @@
+# ASCII
+0.00 0.00 0
+0.60 0.00 0
+0.75529 0.02044 0
+0.90000 0.08038 0
+1.02426 0.17574 0
+1.11962 0.30000 0
+1.17956 0.44471 0
+1.20 0.60 0
+1.20 1.20 0
+0.60 1.20 0
+0.60 0.60 0
+0.00 0.60 0
+0.00 0.00 0