/*******************************************************************************
 * ALBERTA:  an Adaptive multi Level finite element toolbox using
 *           Bisectioning refinement and Error control by Residual
 *           Techniques for scientific Applications
 *
 * file:     parametric_3d.c
 *
 * description:  Support for parametric elements in 3D
 *
 *******************************************************************************
 *
 *  authors:   Daniel Koester           
 *             Institut fuer Mathematik 
 *             Universitaet Augsburg    
 *             Universitaetsstr. 14     
 *             D-86159 Augsburg, Germany
 *
 *             Claus-Justus Heine
 *             Numerische Mathematik fuer Hoechstleistungsrechner
 *             IANS / Universitaet Stuttgart
 *             Pfaffenwaldring 57
 *             70569 Stuttgart, Germany
 *
 *  http://www.alberta-fem.de
 *
 *  (c) by D. Koester (2005)       
 *         C.-J. Heine (2006-20012).
 *
 ******************************************************************************/

#ifdef MESH_DIM
# undef MESH_DIM
#endif
#define MESH_DIM 3

#ifdef N_BAS_MAX
# undef N_BAS_MAX
#endif

#define N_BAS_MAX  N_BAS_LAGRANGE(LAGRANGE_DEG_MAX, MESH_DIM)

static const REAL_B bary2_3d[N_BAS_LAG_3D(2)] = {
  {1.0, 0.0, 0.0, 0.0},
  {0.0, 1.0, 0.0, 0.0},
  {0.0, 0.0, 1.0, 0.0},
  {0.0, 0.0, 0.0, 1.0},
  {0.5, 0.5, 0.0, 0.0},
  {0.5, 0.0, 0.5, 0.0},
  {0.5, 0.0, 0.0, 0.5},
  {0.0, 0.5, 0.5, 0.0},
  {0.0, 0.5, 0.0, 0.5},
  {0.0, 0.0, 0.5, 0.5}
};

static const REAL_B middle_bary = {0.5, 0.5, 0.0, 0.0};

static const int edge_of_face[N_FACES_3D][N_EDGES_2D] = {
  { 5, 4, 3 }, { 5, 2, 1 }, { 4, 2, 0 }, { 3, 1, 0 }
};
static const int face_of_edge[N_EDGES_3D][2] = {
  { 2, 3 }, { 1, 3 }, { 1, 2 }, { 0, 3 }, { 0, 2 }, { 0, 1 }
};
static const int edge_of_vertex_3d[N_VERTICES_3D][3] = {
  { 0, 1, 2 }, { 0, 3, 4 }, { 1, 3, 5 }, { 2, 4, 5 }
};

/*--------------------------------------------------------------------------*/
/* Functions for affine elements as parametric elements. (suffix 1_3d)      */
/*--------------------------------------------------------------------------*/

static bool param_init_element1_3d(const EL_INFO *el_info,
				   const PARAMETRIC *parametric)
{
  LAGRANGE_PARAM_DATA *data = (LAGRANGE_PARAM_DATA *)parametric->data;
  DOF_REAL_D_VEC *coords = data->coords;
  int node_v, n0_v, i;
  EL *el = el_info->el;
  EL_INFO *mod_el_info = (EL_INFO *)el_info; /* modifyable pointer reference */

  data->el = el_info->el;
    
  node_v = el_info->mesh->node[VERTEX];
  n0_v   = coords->fe_space->admin->n0_dof[VERTEX];

  if (!parametric->use_reference_mesh) {
    data->local_coords = mod_el_info->coord;
    mod_el_info->fill_flag |= FILL_COORDS;
  } else {
    data->local_coords = data->param_local_coords;
  }
  for (i = 0; i < N_VERTICES_3D; i++) {
    COPY_DOW(coords->vec[el->dof[node_v+i][n0_v]], data->local_coords[i]);
  }

  return false; /* not really parametric */
}

static void det1_3d(const EL_INFO *el_info, const QUAD *quad, int N,
		    const REAL_B lambda[], REAL dets[])
{
  REAL det;
  int  n;

  det = el_det_1d(el_info);

  if (quad) {
    N = quad->n_points;
  }

  for (n = 0; n < N; n++) {
    dets[n] = det;
  }
 
  return;
}


static void grd_lambda1_3d(const EL_INFO *el_info, const QUAD *quad,
			   int N, const REAL_B lambda[],
			   REAL_BD grd_lam[], REAL_BDD D2_lam[], REAL dets[])
{
  int i, n;

  dets[0] = el_grd_lambda_3d(el_info, grd_lam[0]);

  if (quad) {
    N = quad->n_points;
  }

  for (n = 1; n < N; n++) {
    for (i = 0; i < N_LAMBDA_3D; i++) {
      COPY_DOW( grd_lam[0][i], grd_lam[n][i]);
    }
    for (; i < N_LAMBDA_MAX; i++) {
      SET_DOW(0.0, grd_lam[n][i]);
    }
    if (dets) {
      dets[n] = dets[0];
    }
  }

  if (D2_lam) {
    for (n = 0; n < N; n++) {
      for (i = 0; i < N_LAMBDA_MAX;i++) {
	MSET_DOW(0.0, D2_lam[n][i]);
      }
    }
  }
}

static void
grd_world1_3d(const EL_INFO *el_info, const QUAD *quad,
	      int N, const REAL_B lambda[],
	      REAL_BD grd_Xtr[], REAL_BDB D2_Xtr[], REAL_BDBB D3_Xtr[])
{
  int i;

  if (quad) {
    N = quad->n_points;
  }

  for (i = 0; i < N_LAMBDA_3D; i++) {
    COPY_DOW(el_info->coord[i], grd_Xtr[0][i]);
  }
  for (; i < N_LAMBDA_MAX; i++) {
    SET_DOW(0.0, grd_Xtr[0][i]);
  }
  
  memcpy(grd_Xtr+1, grd_Xtr[0], (N-1)*sizeof(REAL_BD));

  if (D2_Xtr) {
    memset(D2_Xtr, 0, N*sizeof(REAL_BDB));
  }

  if (D3_Xtr) {
    memset(D3_Xtr, 0, N*sizeof(REAL_BDBB));
  }
}

/* Compute the co-normal for WALL at the barycentric coordinates
 * specified by LAMBDA. LAMBDA are 3d barycentric coordinates,
 * i.e. lambda[wall] == 0.0.
 */
static void
wall_normal1_3d(const EL_INFO *el_info, int wall,
		const QUAD *quad, int n, const REAL_B lambda[],
		REAL_D normals[], REAL_DB grd_normals[], REAL_DBB D2_normals[],
		REAL dets[])
{
  int i;

  if (quad) {
    n = quad->n_points;
  }

  if (grd_normals) {
    memset(grd_normals, 0, n*sizeof(REAL_DB));
  }

  if (D2_normals) {
    memset(D2_normals, 0, n*sizeof(REAL_DBB));
  }

  if (normals) {
    REAL detsbuffer[n];
    
    if (!dets) {
      dets = detsbuffer;
    }
    dets[0] = get_wall_normal_3d(el_info, wall, normals[0]);

    for (i = 1; i < n; i++) {
      dets[i] = dets[0];
      COPY_DOW(normals[0], normals[i]);
    }
  } else {
    dets[0] = get_wall_normal_3d(el_info, wall, NULL);
    for (i = 1; i < n; i++) {
      dets[i] = dets[0];
    }
  }
}

/****************************************************************************/
/* fill_coords1_3d(data): initialize the DOF_REAL_D_VEC coords containing   */
/* the position data of the parametric elements (coordinates of vertices in */
/* this case).                                                              */
/****************************************************************************/

static void fill_coords1_3d(LAGRANGE_PARAM_DATA *data)
{
  DOF_REAL_D_VEC  *coords  = data->coords;
  NODE_PROJECTION *n_proj  = data->n_proj;
  bool            selective = n_proj != NULL;
  const NODE_PROJECTION *act_proj;
  FLAGS           fill_flag = CALL_LEAF_EL|FILL_COORDS|FILL_PROJECTION;
  MESH            *mesh;
  const DOF_ADMIN *admin;
  const BAS_FCTS  *bas_fcts;
  int             i;
  DOF             dof[N_VERTICES_3D];

  mesh     = coords->fe_space->mesh;
  admin    = coords->fe_space->admin;
  bas_fcts = coords->fe_space->bas_fcts;

  TRAVERSE_FIRST(mesh, -1, fill_flag) {
    REAL      *vec;

    GET_DOF_INDICES(bas_fcts, el_info->el, admin, dof);

    for (i = 0; i < N_VERTICES_3D; i++) {
      vec = coords->vec[dof[i]];

      COPY_DOW(el_info->coord[i], vec);

      /* Look for a projection function that applies to vertex[i].
       * Apply this projection if found.                         
       */

      if (!selective || n_proj->func) {
	act_proj = wall_proj(el_info, (i+1) % N_WALLS_3D);
	if (!act_proj) {
	  act_proj = wall_proj(el_info, (i+2) % N_WALLS_3D);
	}
	if (!act_proj) {
	  act_proj = wall_proj(el_info, (i+3) % N_WALLS_3D);
	}
	if (!act_proj) {
	  act_proj = wall_proj(el_info, -1);
	}
	if (act_proj && act_proj->func && (!selective || n_proj == act_proj)) {
	  act_proj->func(vec, el_info, bary2_3d[i]);
	}
      }
    }
  } TRAVERSE_NEXT();
  
  return;
}


/****************************************************************************/
/* refine_interpol1_3d(drdv,list,n): update coords vector during refinement.*/
/****************************************************************************/

static void refine_interpol1_3d(DOF_REAL_D_VEC *drdv, RC_LIST_EL *list, int n)
{
  /*FUNCNAME("refine_interpol1_3d");*/
  MESH                *mesh    = drdv->fe_space->mesh;
  LAGRANGE_PARAM_DATA *data    = (LAGRANGE_PARAM_DATA *)mesh->parametric->data;
  REAL_D              *vec     = drdv->vec;
  NODE_PROJECTION     *n_proj  = data->n_proj;
  bool                selective = n_proj != NULL;
  const NODE_PROJECTION *act_proj;
  EL                  *el;
  DOF                 dof_new, dof0, dof1;
  int                 n0_v, node_v, i;

  node_v = mesh->node[VERTEX];
  n0_v   = drdv->fe_space->admin->n0_dof[VERTEX];
  el = list->el_info.el;

  dof0 = el->dof[node_v+0][n0_v];     /* 1st endpoint of refinement edge */
  dof1 = el->dof[node_v+1][n0_v];     /* 2nd endpoint of refinement edge */
  dof_new = el->child[0]->dof[node_v+3][n0_v]; /* vertex[3] is newest vertex */

  AXPBY_DOW(0.5, vec[dof0], 0.5, vec[dof1], vec[dof_new]);

  /* It might be that not all elements define projections for this
   * edge; we really have to loop around the edge to be sure not to
   * miss a projection.
   */
  if (!selective || n_proj->func) {
    for (i = 0; i < n; i++) {
      act_proj = list[i].el_info.active_projection;
      if (act_proj && act_proj->func && (!selective || n_proj == act_proj)) {
	act_proj->func(vec[dof_new], &list->el_info, middle_bary);
	_AI_refine_update_bbox(mesh, vec[dof_new]);
	break; /* stop after the first matching projection */
      }
    }
  }
}

static void vertex_coordsY_3d(EL_INFO *el_info)
{
  PARAMETRIC          *parametric = el_info->mesh->parametric;
  LAGRANGE_PARAM_DATA *data       = (LAGRANGE_PARAM_DATA *)parametric->data;
  DOF_REAL_D_VEC      *coords     = data->coords;
  EL                  *el         = el_info->el;
  int node_v, n0_v, i;

  node_v = el_info->mesh->node[VERTEX];
  n0_v   = coords->fe_space->admin->n0_dof[VERTEX];
    
  el_info->fill_flag |= FILL_COORDS;
  for (i = 0; i < N_VERTICES_3D; i++) {
    COPY_DOW(coords->vec[el->dof[node_v+i][n0_v]], el_info->coord[i]);
  }
}

/*--------------------------------------------------------------------------*/
/* Common functions for higher order elements. (suffix Y_3d)                */
/*--------------------------------------------------------------------------*/

static bool param_init_elementY_3d(const EL_INFO *el_info,
				   const PARAMETRIC *parametric)
{
  LAGRANGE_PARAM_DATA *data    = (LAGRANGE_PARAM_DATA *)parametric->data;
  DOF_PTR_VEC         *edge_pr = data->edge_projections;
  DOF_REAL_D_VEC      *coords  = data->coords;
  EL                  *el      = el_info->el;
  int i;

  if (data->el != el) {

    data->el = el;

    if (data->strategy == PARAM_ALL) {
      /* full-featured parametric element */
      const BAS_FCTS *bas_fcts = data->coords->fe_space->bas_fcts;

      bas_fcts->get_real_d_vec(data->local_coords, el_info->el, coords);

      return true;
    } else {
      int node_e = el_info->mesh->node[EDGE];
      int n0_edge_pr = edge_pr->fe_space->admin->n0_dof[EDGE];
    
      data->i_am_affine = true;
      for (i = 0; i < N_EDGES_3D; i++) {
	/* An element is affine iff none of its edges has suffered a
	 * projection.
	 */
	if (edge_pr->vec[el->dof[node_e+i][n0_edge_pr]] != NULL) {
	  /* full-featured parametric element */
	  const BAS_FCTS *bas_fcts = data->coords->fe_space->bas_fcts;
	  
	  data->i_am_affine = false;
	  data->local_coords = data->param_local_coords;
	  bas_fcts->get_real_d_vec(data->local_coords, el, coords);

	  return true;
	}
      }
      if (parametric->use_reference_mesh) {
	const BAS_FCTS *bas_fcts = data->coords->fe_space->bas_fcts;
	data->local_coords = data->param_local_coords;
	bas_fcts->get_real_d_vec(data->local_coords, el, coords);
      }
    }
  }

  if (!parametric->use_reference_mesh) {
    EL_INFO *mod_el_info = (EL_INFO *)el_info;

    if (data->i_am_affine) { /* Need only the vertices. */
      int node_v, n0_v;

      node_v = el_info->mesh->node[VERTEX];
      n0_v   = coords->fe_space->admin->n0_dof[VERTEX];
    
      data->local_coords = (REAL_D *)el_info->coord;
      mod_el_info->fill_flag |= FILL_COORDS;
      for (i = 0; i < N_VERTICES_3D; i++) {
	COPY_DOW(coords->vec[el->dof[node_v+i][n0_v]], data->local_coords[i]);
      }
    } else {
      mod_el_info->fill_flag &= ~FILL_COORDS;
    }
  }

  return !data->i_am_affine;
}

/* Much of the parametric stuff is actually dimension independent,
 * therefore a bunch of functions shared between (at least)
 * parametric_2d.c and parametric_3d.c is implemented in the following
 * header-file:
 */
#include "parametric_intern.h"

static inline
void WEDGE_3D(REAL res[], const REAL a[MESH_DIM], const REAL b[MESH_DIM])
{
  res[0] = a[1]*b[2] - a[2]*b[1];
  res[1] = a[2]*b[0] - a[0]*b[2];
  res[2] = a[0]*b[1] - a[1]*b[0];
}

/* Compute the co-normal for WALL at a given quadrature point. Helper
 * routine for wall_normalY_3d().
 *
 * On request we compute also the barycentric gradient of the normal,
 * this is needed by vector-valued basis function which point into
 * normal direction.
 */
static inline REAL
wall_normal_iq_3d(REAL_D *const coords,
		  REAL DD[][MESH_DIM],
		  REAL dDD[][MESH_DIM][MESH_DIM],
		  REAL ddDD[][MESH_DIM][MESH_DIM][MESH_DIM],
		  int n_bas, int wall,
		  REAL_D normal, REAL_DB grd_normal, REAL_DBB D2_normal)
{
  FUNCNAME("wall_normal_iq_3d");
  REAL detg, det;
  REAL_D e[MESH_DIM];
  REAL g[MESH_DIM][MESH_DIM];
  REAL ncoeff[MESH_DIM], w1[MESH_DIM], w2[MESH_DIM];
  int i, j;

  detg = Dt_and_DtD_3d(coords, DD, n_bas, e, g);

  for (i = 0; i < MESH_DIM; i++) {
    w1[i] = g[i][0] - g[i][2];
    w2[i] = g[i][1] - g[i][2];
  }
  
  /* The coefficient vector of the normal must be orthogonal to the
   * coefficient vectors w_i of (e_i - e_2) w.r.t. to g, so we can use
   * the cross-product here.
   */
  WEDGE_3D(ncoeff, w1, w2);
    
  AXEY_DOW(ncoeff[0], e[0], normal);
  for (j = 1; j < MESH_DIM; j++) {
    AXPY_DOW(ncoeff[j], e[j], normal);
  }

  /* compute the gradient before scaling the normal to its proper
   * size; this make things much easier.
   */
  if (grd_normal || D2_normal) {
    REAL_D De[MESH_DIM][MESH_DIM];
    REAL   Dg[MESH_DIM][MESH_DIM][MESH_DIM];
    REAL   Dw1[MESH_DIM][MESH_DIM], Dw2[MESH_DIM][MESH_DIM];
    REAL   Dw1w2[MESH_DIM][MESH_DIM], w1Dw2[MESH_DIM][MESH_DIM];
    REAL   Dncoeff[MESH_DIM][MESH_DIM];
    REAL   nuDnu[MESH_DIM];
    REAL_D grd_tmp[MESH_DIM], grd_loc[MESH_DIM];
    REAL inv_nrm2, inv_norm;
    int alpha, beta, gamma, i;

    dDt_and_dDtD_3d(coords, dDD, e, n_bas, De, Dg);

    /* Taking above formula into account we simply have to use the
     * proper product rules ...
     *
     * We extend the normal field s.t. it is constant on the
     * {\lambda_wall = const} surfaces.
     *
     * Greek indices enumerate barycentric co-ordinates, Latin indices
     * enumerate DIM_OF_WORLD stuff.
     */

    /* first part: differentiate the basis vectors of the tangent
     * space. This means just to take the proper offset into De[].
     *
     * Note that De is a "block" Hessian while grd_normal is a proper
     * Jacobian, where the column indices run over the independent
     * variables and the row indices over the components of the
     * vector-field.
     */
    for (alpha = 0; alpha < MESH_DIM; alpha++) {
      SET_DOW(0.0, grd_tmp[alpha]);
      for (beta = 0; beta < MESH_DIM; beta++) {
	AXPY_DOW(ncoeff[beta], De[alpha][beta], grd_tmp[alpha]);
      }
    }

    /* Second part: differentiate the coefficients w.r.t. to the
     * barycentric co-ordinates. ncoeff is a cross-product; we need to
     * take the proper offsets in to Dg, and then form sums of
     * cross-products.
     *
     * Dg, Dw1, Dw2 are block-Jacobians.
     */
    for (alpha = 0; alpha < MESH_DIM; alpha++) {
      for (beta = 0; beta < MESH_DIM; beta++) {
	Dw1[alpha][beta] = Dg[alpha][beta][0] - Dg[alpha][beta][2];
	Dw2[alpha][beta] = Dg[alpha][beta][1] - Dg[alpha][beta][2];
      }
      WEDGE_3D(Dw1w2[alpha], Dw1[alpha], w2);
      WEDGE_3D(w1Dw2[alpha], w1, Dw2[alpha]);
      for (beta = 0; beta < MESH_DIM; beta++) {
	Dncoeff[alpha][beta] = (Dw1w2[alpha][beta] + w1Dw2[alpha][beta]);
	AXPY_DOW(Dncoeff[alpha][beta], e[beta], grd_tmp[alpha]);
      }
    }

    /* Now it remains to take the normalization into account. Here we
     * must also re-order the barycentric co-ordinates to match the
     * ordering of the bulk element.
     *
     * We have for any vector field v that
     *
     * D_a v/|\nu| = 1/|nu| (D_a v - 1/|nu|^2 v\otimes\nu D_a\nu)
     *
     * D_a \nu is stored in grd_tmp.
     */
    
    inv_nrm2 =
      grd_normalize_3d(grd_loc, nuDnu, (const REAL_D *)grd_tmp, normal);
    inv_norm = sqrt(inv_nrm2);

    if (grd_normal) {
      /* Sort the stuff s.t. it uses the proper order of the
       * barycentric co-ordinates
       */
      for (i = 0; i < DIM_OF_WORLD; i++) {
	/* constant extension along \lambda_wall co-ordinate lines. */
	grd_normal[i][wall] = 0.0;
	for (alpha = 0; alpha < MESH_DIM; alpha++) {
	  grd_normal[i][(wall+alpha+1) % N_LAMBDA(MESH_DIM)] =
	    inv_norm * grd_loc[alpha][i];
	}
      }
    }
    
    if (D2_normal) { /* sigh */
      REAL_D       D2e[MESH_DIM][MESH_DIM][MESH_DIM];
      REAL_DDDD_3D D2g;
      REAL_D_3D    D2w1, D2w2, D2w1w2, w1D2w2;
      REAL_D       D2_tmp[MESH_DIM][MESH_DIM], D2_loc[MESH_DIM][MESH_DIM];

      ddDt_and_ddDtD_3d(coords, ddDD, e, De, Dg, n_bas, D2e, D2g);

      /* First part: take the second derivatives of the tangent vectors */
      for (alpha = 0; alpha < MESH_DIM; alpha++) {
	for (gamma = 0; gamma < MESH_DIM; gamma++) {
	  AXPY_DOW(ncoeff[gamma], D2e[alpha][alpha][gamma],
		   D2_tmp[alpha][alpha]);
	}
	for (beta = alpha+1; beta < MESH_DIM; beta++) {
	  for (gamma = 0; gamma < MESH_DIM; gamma++) {
	    AXPY_DOW(ncoeff[gamma], D2e[alpha][beta][gamma],
		     D2_tmp[alpha][beta]);
	  }
	}
      }

      /* Second part: first derivatives of coefficients, with first
       * derivatives of the tangent vectors.
       */
      for (alpha = 0; alpha < MESH_DIM; alpha++) {
	for (gamma = 0; gamma < MESH_DIM; gamma++) {
	  AXPY_DOW(2.0*Dncoeff[alpha][gamma], De[alpha][gamma],
		   D2_tmp[alpha][beta]);
	}
	for (beta = alpha+1; beta < MESH_DIM; beta++) {
	  for (gamma = 0; gamma < MESH_DIM; gamma++) {
	    AXPY_DOW(Dncoeff[alpha][gamma], De[beta][gamma],
		     D2_tmp[alpha][beta]);
	    AXPY_DOW(Dncoeff[beta][gamma], De[alpha][gamma],
		     D2_tmp[alpha][beta]);
	  }
	}
      }

      /* Third part: second derivatives of coefficients, with values
       * of the tangent vectors.
       */
      for (alpha = 0; alpha < MESH_DIM; alpha++) {
	REAL Dw1Dw2[MESH_DIM];
	for (gamma = 0; gamma < MESH_DIM; gamma++) {
	  D2w1[gamma] =
	    D2g[alpha][alpha][gamma][0] - D2g[alpha][alpha][gamma][2];
	  D2w2[gamma] =
	    D2g[alpha][alpha][gamma][1] - D2g[alpha][alpha][gamma][2];
	}
	WEDGE_3D(D2w1w2, D2w1, w2);
	WEDGE_3D(w1D2w2, w1, D2w2);
	WEDGE_3D(Dw1Dw2, Dw1[alpha], Dw2[alpha]);

	for (gamma = 0; gamma < MESH_DIM; gamma++) {
	  AXPY_DOW((2.0*Dw1Dw2[gamma] + D2w1w2[gamma] + w1D2w2[gamma]),
		   e[gamma],
		   D2_tmp[alpha][beta]);
	}
	for (beta = alpha+1; beta < MESH_DIM; beta++) {
	  REAL Dw1Dw2_ab[MESH_DIM], Dw1Dw2_ba[MESH_DIM];
	  for (gamma = 0; gamma < MESH_DIM; gamma++) {
	    D2w1[gamma] =
	      D2g[alpha][beta][gamma][0] - D2g[alpha][beta][gamma][2];
	    D2w2[gamma] =
	      D2g[alpha][beta][gamma][1] - D2g[alpha][beta][gamma][2];
	  }
	  WEDGE_3D(D2w1w2, D2w1, w2);
	  WEDGE_3D(w1D2w2, w1, D2w2);
	  WEDGE_3D(Dw1Dw2_ab, Dw1[alpha], Dw2[beta]);
	  WEDGE_3D(Dw1Dw2_ba, Dw1[beta], Dw2[alpha]);

	  for (gamma = 0; gamma < MESH_DIM; gamma++) {
	    AXPY_DOW((Dw1Dw2_ab[gamma] + Dw1Dw2_ba[gamma]
		      +
		      D2w1w2[gamma] + w1D2w2[gamma]),
		     e[gamma],
		     D2_tmp[alpha][beta]);
	  }
	  COPY_DOW(D2_tmp[alpha][beta], D2_tmp[beta][alpha]);
	}
      }

      /* Now we have computed the second derivatives of the non-unit
       * normals. We have to take the normalization into account.
       */
      D2_normalize_3d(D2_loc, normal,
		      (const REAL_D *)grd_tmp,
		      (const REAL_D (*)[MESH_DIM])D2_tmp, inv_nrm2, nuDnu);
      
      /* Sort the stuff s.t. it uses the proper order of the
       * barycentric co-ordinates
       */
      for (i = 0; i < DIM_OF_WORLD; i++) {
	D2_normal[i][wall][wall] = 0.0;
	for (alpha = 0; alpha < MESH_DIM; alpha++) {
	  int a = (wall + alpha + 1) % N_LAMBDA(MESH_DIM);
	  D2_normal[i][a][a] = inv_norm * D2_loc[alpha][alpha][i];
	  D2_normal[i][wall][a] = D2_normal[i][a][wall] = 0.0;
	  for (beta = alpha + 1; beta < MESH_DIM; beta++) {
	    int b = (wall + beta + 1) % N_LAMBDA(MESH_DIM);

	    D2_normal[i][a][b] =
	      D2_normal[i][b][a] = inv_norm * D2_loc[alpha][beta][i];
	  }
	}
      }
    }
  }

  /* The normal has the "correct" length up to scaling with 1.0/sqrt(detg) */
  SCAL_DOW(1.0/sqrt(detg), normal);

#if 1
  /* In principle the orientation should be fixed ... */
  if (SCP_DOW(e[2], normal) <= 0.0) {
    if (ALBERTA_DEBUG) {
      WARNING("Wrong orientation?\n");
    }
    SCAL_DOW(-1.0, normal);
  }
#endif

  det = NORM_DOW(normal);

  /* cH: shouldn't the following test the relative size of det w.r.t.,
   * e.g. the edge length?
   */
  TEST_EXIT(det > 1.e-30, "face det = 0 on face %d.\n", wall);

  return det;
}

/*--------------------------------------------------------------------------*/
/* Functions for quadratic elements. (suffix 2_3d)                          */
/*--------------------------------------------------------------------------*/

static void refine_interpol2_3d(DOF_REAL_D_VEC *drdv, RC_LIST_EL *list, int n)
{
  /* FUNCNAME("refine_interpol2_3d"); */
  MESH                *mesh     = drdv->fe_space->mesh;
  LAGRANGE_PARAM_DATA *data     = (LAGRANGE_PARAM_DATA *)mesh->parametric->data;
  const BAS_FCTS      *bas_fcts = drdv->fe_space->bas_fcts;
  DOF_PTR_VEC         *edge_pr  = data->edge_projections;
  REAL_D              *vec      = drdv->vec;
  EL                  *el;
  int                 i, j, node_e, n0_e, node_v, n0_v, elem;
  DOF                 cdof_e[N_EDGES_3D],cdof_v[N_VERTICES_3D];
  REAL                *x[4];
  NODE_PROJECTION     *n_proj   = data->n_proj;
  bool                selective = n_proj != NULL;
  const NODE_PROJECTION *act_proj;
  const EL_INFO       *el_info;
  int                 n0_edge_pr = -1;
  DOF                 cdof_edge_pr[4] = { -1, };
  static const REAL_B lambda[4] = { INIT_BARY_3D(0.25, 0.25, 0.5, 0.0),
				    INIT_BARY_3D(0.25, 0.25, 0.0, 0.5),
				    INIT_BARY_3D(0.75, 0.25, 0.0, 0.0),
				    INIT_BARY_3D(0.25, 0.75, 0.0, 0.0) };

  if (data->strategy != PARAM_STRAIGHT_CHILDS) {
    /* Just call the default interpolation routine */
    bas_fcts->real_d_refine_inter(drdv, list, n);
  }

  node_e = mesh->node[EDGE];
  node_v = mesh->node[VERTEX];
  n0_e   = drdv->fe_space->admin->n0_dof[EDGE];
  n0_v   = drdv->fe_space->admin->n0_dof[VERTEX];

/****************************************************************************/
/* Loop over all patch elements.                                            */
/****************************************************************************/
  for (elem = 0; elem < n; elem++) {
    el_info = &list[elem].el_info;
    el = el_info->el;

/****************************************************************************/
/* Step 1: Now we need to determine four points: The midpoint on the two    */
/* edges between the two children and the two midpoints of the children     */
/* along the current refinement edge.                                       */
/* If strategy is PARAM_ALL or PARAM_CURVED_CHILDS we use the (nonlinear)   */
/* barycentric coords. For strategy PARAM_STRAIGHT_CHILDS we use geometric  */
/* midpoints.                                                               */
/****************************************************************************/

    /* TODO: the code below should adjust the edge DOF between the
     * children only once; ATM each edge DOF is adjusted twice (if we
     * do not hit the boundary of the domain).
     */
    x[0] = vec[el->child[0]->dof[node_e+4][n0_e]];
    x[1] = vec[el->child[0]->dof[node_e+5][n0_e]];
    x[2] = vec[el->child[0]->dof[node_e+2][n0_e]];
    x[3] = vec[el->child[1]->dof[node_e+2][n0_e]];

    if (data->strategy == PARAM_STRAIGHT_CHILDS) {

      cdof_e[0] = el->dof[node_e+0][n0_e]; /* midpoint of ref-edge */
      for (i = 0; i < N_VERTICES_3D; i++) {
	cdof_v[i] = el->dof[node_v+i][n0_v];
      }

      for (i = 0; i < DIM_OF_WORLD; i++) {
	x[0][i] = 0.5 * (vec[cdof_e[0]][i] + vec[cdof_v[2]][i]);
	x[1][i] = 0.5 * (vec[cdof_e[0]][i] + vec[cdof_v[3]][i]);
      }
      if (elem == 0) {
	for (i = 0; i < DIM_OF_WORLD; i++) {
	  x[2][i] = 0.5 * (vec[cdof_e[0]][i] + vec[cdof_v[0]][i]);
	  x[3][i] = 0.5 * (vec[cdof_e[0]][i] + vec[cdof_v[1]][i]);
	}
      }
    }

/****************************************************************************/
/* Step 2: We check if any projections need to be done on these four        */
/* points. While doing this, we keep track of projected coordinates.         */
/****************************************************************************/
    if (edge_pr) {
      n0_edge_pr = edge_pr->fe_space->admin->n0_dof[EDGE];

      cdof_edge_pr[0] = el->child[0]->dof[node_e+4][n0_edge_pr];
      cdof_edge_pr[1] = el->child[0]->dof[node_e+5][n0_edge_pr];

      edge_pr->vec[cdof_edge_pr[0]] =
	edge_pr->vec[cdof_edge_pr[1]] = NULL;
      
      cdof_edge_pr[2] = el->child[0]->dof[node_e+2][n0_edge_pr];
      cdof_edge_pr[3] = el->child[1]->dof[node_e+2][n0_edge_pr];

      if (elem == 0) {
	/* invalidate only once */
	edge_pr->vec[cdof_edge_pr[2]] =
	  edge_pr->vec[cdof_edge_pr[3]] = NULL;
      }

      /* Update edge_projections even if there is no function pointer; */
      act_proj = wall_proj(el_info, 3);
      if (act_proj == NULL) {
	act_proj = wall_proj(el_info, -1);
      }
      if (act_proj && (!selective || n_proj == act_proj)) {
	edge_pr->vec[cdof_edge_pr[0]] = (void *)act_proj;
      }
      act_proj = wall_proj(el_info, 2);
      if (act_proj == NULL) {
	act_proj = wall_proj(el_info, -1);
      }
      if (act_proj && (!selective || n_proj == act_proj)) {
	edge_pr->vec[cdof_edge_pr[1]] = (void *)act_proj;
      }
      act_proj = el_info->active_projection;
      if (act_proj && (!selective || n_proj == act_proj)) {
	edge_pr->vec[cdof_edge_pr[2]] =
	  edge_pr->vec[cdof_edge_pr[3]] = (void *)act_proj;
      }
    }
    
    if (!selective || n_proj->func) {
      act_proj = wall_proj(el_info, 3);
      if (act_proj == NULL) {
	act_proj = wall_proj(el_info, -1);
      }
      if (act_proj && act_proj->func && (!selective || n_proj == act_proj)) {
	act_proj->func(x[0], el_info, lambda[0]);
	_AI_refine_update_bbox(mesh, x[0]);
      }
      act_proj = wall_proj(el_info, 2);
      if (act_proj == NULL) {
	act_proj = wall_proj(el_info, -1);
      }
      if (act_proj && act_proj->func && (!selective || n_proj == act_proj)) {
	act_proj->func(x[1], el_info, lambda[1]);
	_AI_refine_update_bbox(mesh, x[1]);
      }
      act_proj = el_info->active_projection;
      if (act_proj && act_proj->func && (!selective || n_proj == act_proj)) {
	/* we have to check this on every element; it can be that not
	 * all elements of the patch define projections for the
	 * refinement edge.
	 */
	act_proj->func(x[2], el_info, lambda[2]);
	_AI_refine_update_bbox(mesh, x[2]);
	act_proj->func(x[3], el_info, lambda[3]);
	_AI_refine_update_bbox(mesh, x[3]);
      }
    }
    
/****************************************************************************/
/* Step 3: We hand down the data corresponding to the parent edge vertex    */
/* in the refinement edge (its DOF could be deleted!). This concerns coords.*/
/* This only needs to be done once.                                         */
/****************************************************************************/
    if (elem == 0) {
      DOF pdof = el->dof[node_e+0][n0_e];
      DOF cdof = el->child[0]->dof[node_v+3][n0_v];
    
      COPY_DOW(vec[pdof], vec[cdof]);
    }

/****************************************************************************/
/* Step 4: Correct the children, if not all elements are to be parametric.  */
/* If the parent element was already affine, then the children will already */
/* have the correct coordinates.                                            */
/****************************************************************************/
    if (edge_pr) {
      int parent_affine = true;
      int i_child;
    
      for (j = 0; j < N_EDGES_3D; j++) {
	DOF edge_dof = el->dof[node_e+j][n0_edge_pr];
	
	if (edge_pr->vec[edge_dof] != NULL) {
	  parent_affine = false;
	  break;
	}
      }
      if (!parent_affine) for (i_child = 0; i_child < 2; i_child++) {
	EL *child = el->child[i_child];
	int child_affine = true;
      
	for (j = 0; j < N_EDGES_3D; j++) {
	  DOF edge_dof = child->dof[node_e+j][n0_edge_pr];
	
	  if (edge_pr->vec[edge_dof] != NULL) {
	    child_affine = false;
	    break;
	  }
	}
	if (child_affine) {
	  for (j = 0; j < N_EDGES_3D; j++) {
	    cdof_e[j] = child->dof[node_e+j][n0_e];
	  }
	  for (j = 0; j < N_VERTICES_3D; j++) {
	    cdof_v[j] = child->dof[node_v+j][n0_v];
	  }
	  for (j = 0; j < N_EDGES_3D; j++) {
	    AXPBY_DOW(0.5, vec[cdof_v[vertex_of_edge_3d[j][0]]],
		      0.5, vec[cdof_v[vertex_of_edge_3d[j][1]]],
		      vec[cdof_e[j]]);
	  }
	}
      }
    }
  }

  return;
}

static void coarse_interpol2_3d(DOF_REAL_D_VEC *drdv, RC_LIST_EL *list, int n)
{
  FUNCNAME("coarse_interpol2_3d");
  EL                  *el;
  REAL_D              *vec = NULL;
  int                  node_v, node_e, n0_v, n0_e, j;
  DOF                  cdof, pdof;
  const MESH          *mesh    = drdv->fe_space->mesh;
  LAGRANGE_PARAM_DATA *data    = (LAGRANGE_PARAM_DATA *)mesh->parametric->data;
  DOF_PTR_VEC         *edge_pr = data->edge_projections;

  GET_DOF_VEC(vec, drdv);
  el = list->el_info.el;

  node_v = drdv->fe_space->mesh->node[VERTEX];        
  node_e = drdv->fe_space->mesh->node[EDGE]; 
  n0_v = drdv->fe_space->admin->n0_dof[VERTEX];
  n0_e = drdv->fe_space->admin->n0_dof[EDGE];

/****************************************************************************/
/*  copy values at refinement vertex to the parent refinement edge.         */
/****************************************************************************/

  cdof = el->child[0]->dof[node_v+3][n0_v];      /* newest vertex is dim */
  pdof = el->dof[node_e][n0_e];

  for (j = 0; j < DIM_OF_WORLD; j++)
    vec[pdof][j] = vec[cdof][j];

  if (edge_pr) {
    /* ref-edge projected status is equal for all childs on the patch */
    int n0_edge_pr = edge_pr->fe_space->admin->n0_dof[EDGE];
    DOF pdof       = el->dof[node_e+2][n0_edge_pr];
    DOF cdof       = el->child[0]->dof[node_e+0][n0_edge_pr];
    
    edge_pr->vec[pdof] = edge_pr->vec[cdof];
  }

  return;
}

static void fill_coords2_3d(LAGRANGE_PARAM_DATA *data)
{
  DOF_REAL_D_VEC  *coords   = data->coords;
  MESH            *mesh     = coords->fe_space->mesh;
  const DOF_ADMIN *admin    = coords->fe_space->admin;
  const BAS_FCTS  *bas_fcts = coords->fe_space->bas_fcts;
  DOF_PTR_VEC     *edge_pr  = data->edge_projections;
  NODE_PROJECTION *n_proj   = data->n_proj;
  bool            selective = n_proj != NULL;
  const NODE_PROJECTION *act_proj;
  FLAGS           fill_flag = CALL_LEAF_EL|FILL_COORDS|FILL_PROJECTION;
  DOF             dof[bas_fcts->n_bas_fcts];
  int             i, j;
  int             node_e = -1, n0_edge_pr = -1;

  if (edge_pr) {
    node_e     = mesh->node[EDGE];
    n0_edge_pr = edge_pr->fe_space->admin->n0_dof[EDGE];
  }

  TRAVERSE_FIRST(mesh, -1, fill_flag) {
    void   *edge_pr_loc[N_EDGES_3D] = { NULL, };
    DOF    edge_pr_dof[N_EDGES_3D] = { 0, };
    REAL *vec, *vec0, *vec1;

    GET_DOF_INDICES(bas_fcts, el_info->el, admin, dof);

    if (edge_pr) {
      for (i = 0; i < N_EDGES_3D; i++) {
	edge_pr_dof[i] = el_info->el->dof[node_e+i][n0_edge_pr];
	edge_pr_loc[i] = edge_pr->vec[edge_pr_dof[i]];
      }
    }

    for (i = 0; i < N_VERTICES_3D; i++) {

      if (edge_pr) {
	for (j = 0; j < 3; j++) {
	  if (edge_pr_loc[edge_of_vertex_3d[i][j]]) {
	    break;
	  }
	}
	if (j < 3) {
	  continue; /* no need to call the projections twice. */
	}
      }

      vec = coords->vec[dof[i]];

      COPY_DOW(el_info->coord[i], vec);

      /* Look for a projection function that applies to vertex[i].
       * Apply this projection if found.
       */
      if (!selective || n_proj->func) {
	act_proj = wall_proj(el_info, (i+1) % N_WALLS_3D);
	if (!act_proj) {
	  act_proj = wall_proj(el_info, (i+2) % N_WALLS_3D);
	}
	if (!act_proj) {
	  act_proj = wall_proj(el_info, (i+3) % N_WALLS_3D);
	}
	if (!act_proj) {
	  act_proj = wall_proj(el_info, -1);
	}

	if (act_proj && act_proj->func && (!selective || n_proj == act_proj)) {
	  act_proj->func(vec, el_info, bary2_3d[i]);
	}
      }
    }

    for (i = 0; i < N_EDGES_3D; i++) {
      const int *voe;

      if (edge_pr && edge_pr_loc[i]) {
	/* coordinates of edges MUST NOT be set twice: there might be
	 * elements with no projections attached to their walls, but
	 * which are curved nevertheless; edges are shared by elements
	 * which are not neighbours.
	 */
	continue;
      }

      voe = vertex_of_edge_3d[i];

      vec = coords->vec[dof[N_VERTICES_3D+i]];
      vec0 = coords->vec[dof[voe[0]]];
      vec1 = coords->vec[dof[voe[1]]];

      AXPBY_DOW(0.5, vec0, 0.5, vec1, vec);

      /* Look for a projection function that applies to edge[i].   */
      /* Apply this projection if found.                           */

      act_proj = wall_proj(el_info, face_of_edge[i][0]);
      if (!act_proj) {
	act_proj = wall_proj(el_info, face_of_edge[i][1]);
      }
      if (!act_proj) {
	act_proj = wall_proj(el_info, -1);
      }
      if (act_proj && (!selective || n_proj == act_proj)) {
	if (act_proj->func) {
	  act_proj->func(vec, el_info, bary2_3d[N_VERTICES(3) + i]);
	}
	if (edge_pr) {
	  edge_pr->vec[edge_pr_dof[i]] = (void *)act_proj;
	}
      }
    }
  } TRAVERSE_NEXT();

  return;
}

/* Generic degree */

/* SIMPLE_HIGH_DEGREE works up to degree 3 but no more. ;) */
#define SIMPLE_HIGH_DEGREE 0

/* In order to obtain optimal error estimates for higher-order
 * boundary approximations we need to be careful with the choice of
 * the interior interpolation points. We do this only for strategy !=
 * PARAM_ALL.
 *
 * We follow the procedure described in: M. Lenoir, Optimal
 * Isoparametric Finite Elements and Error Estimates for Domains
 * Involving Curved Bounaries, SIAM JNA 23(3) 1986.
 *
 * Lenoir's procedure works for the case when at most one face _or_
 * one edge and no face intersects the boundary. This condition is
 * satisfied after refining the macro triangulation once (3
 * bisections).
 *
 * The procedure is recursive: to define the node-positions for degree
 * d, we must first compute the position for degree (d-1), ...,
 * 2. This is unfortunate. However, the generalization of the 2d
 * strategy works only up to degree 3; as soon as nodes appear in the
 * center of the simplexes that simple approach doesn't work any
 * longer.
 */

/* We need the values of the lower-degree basis functions at certain
 * points, but only on the boundary. More specific, given an interior
 * Lagrange node with barycentric co-ordinates (l_0, ..., l_3) and a
 * curved wall w = 0 we need the values of all lower degree basis
 * functions at the point
 *
 * (0, l_1/(1-l_0), l_2/(1-l_0), l_3/(1-l_0))
 *
 * We cache those values;
 * wall_cache[wall][deg][low_deg][nbf][centernode] gives those values
 * where "centernode" is the local center-node index of the
 * Lagrange-basis functions of degree "deg", 1 <= low_deg <= deg, nbf
 * is local number of the 1d basis functions.
 */
typedef struct param_face_cache
{
  int  n_nodes;   /* Number of nodes for which the values are
		   * cached.
		   */
  REAL ***values; /* values of basis functions, values[sub_deg][nbf][nodenum]
		   * values[0] is used for the maximum degree.
		   */
  REAL *scale;    /* The scale factor for each center-node number */
} PARAM_FACE_CACHE;

/* A cache structure for sharing data on all patch elements.
 */
typedef struct paramY_data_3d {
  MESH            *mesh;  
  const BAS_FCTS  *bas_fcts;
  const DOF_ADMIN *admin;
  int             degree;
  int             n_v, node_v, n0_v;
  int             n_e, node_e, n0_e;
  int             n_f, node_f, n0_f;
  int             n_c, node_c, n0_c;
  PARAM_STRATEGY  strategy;
  REAL_D          *vec;
  NODE_PROJECTION *n_proj;
  int             ref_edge_untouched;
  int             n0_edge_pr;
  int             *is_affine;
  NODE_PROJECTION **edge_pr;
  DOF             (*child_dofs)[2][N_BAS_MAX];
  const REAL_B    *const*nodes;
  const REAL_B    *const(*child_nodes)[3];

 /* projected edge-nodes for all intermediate basis-function degrees:
  * edge_nodes[edge][degree][node] We need this for != PARAM_ALL to
  * construct iso-parametric parameterisations of higher degree.
  */
  REAL_D          (*edge_nodes)[DEGREE_MAX][DEGREE_MAX-1];
  /* The vertex DOF of vertex 0 for each edge in edge_nodes. */
  DOF             *edge_v0;
  /* The size of the edge_nodes array. */
  int             n_edges;
  /* Offset into edges_nodes for each child of each element in the
   * patch
   */
  int             (*edge_no)[2][N_EDGES_3D];

  /* We cache the values of the basis functions at the projection
   * points at the boundary.
   */
  const PARAM_FACE_CACHE (*wall_edge_cache)[N_EDGES_2D];
  const PARAM_FACE_CACHE *center_wall_cache; /* N_WALLS_3D */
  const PARAM_FACE_CACHE *center_edge_cache; /* N_EDGES_3D */

} PARAMY_DATA_3D;

/* local numbering of face/center DOFs given the row and column of the
 * Lagrange node on the standard reference simplex.
 */
#define FACE_DOF(row, col) ((row)*((row)+1)/2+(row)*(row)+(col))
#define CENTER_DOF(height, row, col)					\
  (2*((height)*(height+1)*(height+2))/6					\
   +									\
   (height)*(height)*(height)						\
   +									\
   FACE_DOF(row, col))

/* We enumerate all edges of the refinement patch to be able to define
 * a cache for values belonging to edges. Maybe one could even define
 * a global cache; the memory tradeof would be moderate because only
 * boundary elements would be affected.
 */
#define NEW_PATCH_EDGE(elem, i_child, edge_loc, v0dof)		\
    Ydata->edge_no[elem][i_child][edge_loc] = Ydata->n_edges;	\
    Ydata->edge_nodes[Ydata->n_edges][0][0][0] = HUGE_VAL;	\
    Ydata->edge_v0[Ydata->n_edges++] = (v0dof)

/* Fill the cache, if necessary. We use the 1d ordering for the cache
 * values: first come the two vertices, then the edge DOFs.
 */

/* Cache to speed up relocation of nodes located on a face with one
 * curved edge.
 */
static const PARAM_FACE_CACHE (*face_edge_valuesY_3d(int degree))[N_EDGES_2D]
{
  static PARAM_FACE_CACHE (*face_cache)[N_FACES_3D][N_EDGES_2D];
  static int max_deg;
  PARAM_FACE_CACHE *cache;
  int i, j, d;

  if (max_deg < degree) {
    if (!max_deg) { 
      face_cache = (PARAM_FACE_CACHE (*)[N_FACES_3D][N_EDGES_2D])
	MEM_ALLOC((degree+1)*N_FACES_3D*N_EDGES_2D, PARAM_FACE_CACHE);
   } else {
      face_cache = (PARAM_FACE_CACHE (*)[N_FACES_3D][N_EDGES_2D])
	MEM_REALLOC(face_cache,
		    (max_deg+1)*N_FACES_3D*N_EDGES_2D,
		    (degree+1)*N_FACES_3D*N_EDGES_2D,
		    PARAM_FACE_CACHE);
    }
    for (d = max_deg+1; d <= degree; d++) {
      face_cache[d][0][0].values = NULL;
    }
    max_deg = degree;
  }
  if (!face_cache[degree][0][0].values) {
    int            n_f = (degree-1)*(degree-2)/2;
    int            face, edge2d;
    const BAS_FCTS *bas;
    const REAL_B   *nodes;
    const REAL_B   *face_nodes;
    REAL_B         edge_nodes[n_f];
    REAL           s;

    for (face = 0; face < N_FACES_3D; face++) {
      const int *vow = vertex_of_wall_3d[face];

      for (edge2d = 0; edge2d < N_EDGES_2D; edge2d++) {
	int edge3d = edge_of_face[face][edge2d];
	const int *voe = vertex_of_edge_3d[edge3d];
	int n_e = degree-1;

	cache = &face_cache[degree][face][edge2d];
	cache->n_nodes = n_f;
	cache->values = MEM_ALLOC(degree, REAL **);
	cache->scale  = MEM_ALLOC(n_f, REAL);
	bas  = get_lagrange(MESH_DIM, degree);
	nodes = LAGRANGE_NODES(bas);
	face_nodes = nodes + N_VERTICES_3D + N_EDGES_3D*n_e + face*n_f;
	for (i = 0; i < n_f; i++) {
	  SET_BAR(MESH_DIM, 0.0, edge_nodes[i]);
	  cache->scale[i] = s = 1.0 - face_nodes[i][vow[edge2d]];
	  for (j = 0; j < N_VERTICES_1D; j++) {
	    edge_nodes[i][voe[j]] = face_nodes[i][voe[j]] / s;
	  }
	}

	/* values[0] holds the maximum degree values */
	cache->values[0] = MEM_ALLOC(N_VERTICES_1D+n_e, REAL *);
	for (i = 0; i < N_VERTICES_1D+n_e; i++) {
	  cache->values[0][i] = MEM_ALLOC(n_f, REAL);
	}
	for (i = 0; i < N_VERTICES_1D; i++) {
	  for (j = 0; j < n_f; j++) {
	    cache->values[0][i][j] = PHI(bas, voe[i], edge_nodes[j]);
	  }
	}
	for (i = 0; i < n_e; i++) {
	  int ldof = N_VERTICES_3D + edge3d*n_e + i;
	  int ci   = N_VERTICES_1D+i; /* cache index */
	  for (j = 0; j < n_f; j++) {
	    cache->values[0][ci][j] = PHI(bas, ldof, edge_nodes[j]);
	  }
	}

	for (d = 1; d < degree; d++) {
	  bas = get_lagrange(MESH_DIM, d);
	  n_e = d-1;
	  cache->values[d] = MEM_ALLOC(N_VERTICES_1D+n_e, REAL *);
	  for (i = 0; i < N_VERTICES_1D+n_e; i++) {
	    cache->values[d][i] = MEM_ALLOC(n_f, REAL);
	  }
	  for (i = 0; i < N_VERTICES_1D; i++) {
	    for (j = 0; j < n_f; j++) {
	      cache->values[d][i][j] = PHI(bas, voe[i], edge_nodes[j]);
	    }
	  }
	  for (i = 0; i < n_e; i++) {
	    int ldof = N_VERTICES_3D + edge3d*n_e + i;
	    int ci   = N_VERTICES_1D+i; /* cache index */
	    for (j = 0; j < n_f; j++) {
	      cache->values[d][ci][j] = PHI(bas, ldof, edge_nodes[j]);
	    }
	  }
	}
      }
    }
  }
  return (const PARAM_FACE_CACHE (*)[N_EDGES_2D])face_cache[degree];
}

/* Generate the cache for center nodes shifted towards a projected
 * wall
 */
static const PARAM_FACE_CACHE *center_wall_valuesY_3d(int degree)
{
  static PARAM_FACE_CACHE (*wall_cache)[N_WALLS_3D];
  static int max_deg;
  PARAM_FACE_CACHE *cache;
  int i, j, k, d;

  if (max_deg < degree) {
    if (!max_deg) {
      wall_cache = (PARAM_FACE_CACHE (*)[N_WALLS_3D])
	MEM_ALLOC((degree+1)*N_WALLS_3D, PARAM_FACE_CACHE);
    } else {
      wall_cache = (PARAM_FACE_CACHE (*)[N_WALLS_3D])
	MEM_REALLOC(wall_cache,
		    (max_deg+1)*N_WALLS_3D,
		    (degree+1)*N_WALLS_3D, PARAM_FACE_CACHE);
    }
    for (d = max_deg; d <= degree; d++) {
      wall_cache[d][0].values = NULL;
    }
    max_deg = degree;
  }
  if (!wall_cache[degree][0].values) {
    int            n_c = (degree-1)*(degree-2)*(degree-3)/6;
    REAL_B         wall_nodes[n_c];
    const BAS_FCTS *bas;
    const REAL_B   *center_nodes;
    REAL           s;
    int            wall, ci, edge2d;

    for (wall = 0; wall < N_WALLS_3D; wall++) {
      int n_f = (degree-1)*(degree-2)/2;
      int n_e = degree-1;
      int n_f_bas = (degree+1)*(degree+2)/2;

      cache = &wall_cache[degree][wall];
      cache->values = MEM_ALLOC(degree, REAL **);
      cache->scale  = MEM_ALLOC(n_c, REAL);
      cache->n_nodes = n_c;
      bas = get_lagrange(MESH_DIM, degree);
      center_nodes  = LAGRANGE_NODES(bas);
      center_nodes += bas->n_bas_fcts - n_c;
      for (i = 0; i < n_c; i++) {
	SET_BAR(MESH_DIM, 0.0, wall_nodes[i]);
	cache->scale[i] = s = 1.0 - center_nodes[i][wall];
	for (j = 0; j < N_VERTICES_2D; j++) {
	  int vj = vertex_of_wall_3d[wall][j];
	  wall_nodes[i][vj] = center_nodes[i][vj] / s;
	}
      }
      cache->values[0] = MEM_ALLOC(n_f_bas, REAL *);
      for (i = 0; i < n_f_bas; i++) {
	cache->values[0][i] = MEM_ALLOC(n_c, REAL);
      }
      for (i = 0; i < N_VERTICES_2D; i++) {
	int vi = vertex_of_wall_3d[wall][i];
	for (j = 0; j < n_c; j++) {
	  cache->values[0][i][j] = PHI(bas, vi, wall_nodes[j]);
	}
      }
      ci = i;
      for (edge2d = 0; edge2d < N_EDGES_2D; edge2d++) {
	int edge3d = edge_of_face[wall][edge2d];
	for (i = 0; i < n_e; i++) {
	  int ldof = N_VERTICES_3D + edge3d*n_e + i;
	  for (j = 0; j < n_c; j++) {
	    cache->values[0][ci++][j] = PHI(bas, ldof, wall_nodes[j]);
	  }
	}
      }
      for (i = 0; i < n_f; i++) {
	int ldof = N_VERTICES_3D + N_EDGES_3D*n_e + wall*n_f + i;
	for (j = 0; j < n_c; j++) {
	  cache->values[0][ci++][j] = PHI(bas, ldof, wall_nodes[j]);
	}
      }
      
      for (d = 1; d < degree; d++) {
	bas = get_lagrange(MESH_DIM, d);
	n_f = (d-1)*(d-2)/2;
	n_e = d-1;
	n_f_bas = (d+1)*(d+2)/2;
	cache->values[d] = MEM_ALLOC(n_f_bas, REAL *);
	for (i = 0; i < n_f_bas; i++) {
	  cache->values[d][i] = MEM_ALLOC(n_c, REAL);
	}
	for (i = 0; i < N_VERTICES_2D; i++) {
	  int vi = vertex_of_wall_3d[wall][i]; /* vertex index */
	  for (j = 0; j < n_c; j++) {
	    cache->values[d][i][j] = PHI(bas, vi, wall_nodes[j]);
	  }
	}
	ci = i;
	for (edge2d = 0; edge2d < N_EDGES_2D; edge2d++) {
	  int edge3d = edge_of_face[wall][edge2d];
	  for (j = 0; j < n_e; j++) {
	    int ldof = N_VERTICES_3D + edge3d*n_e + j;
	    for (k = 0; k < n_c; k++) {
	      cache->values[d][ci++][k] = PHI(bas, ldof, wall_nodes[k]);
	    }
	  }
	}
	for (i = 0; i < n_f; i++) {
	  int ldof = N_VERTICES_3D + N_EDGES_3D*n_e + wall*n_f + i;
	  for (j = 0; j < n_c; j++) {
	    cache->values[d][ci++][j] = PHI(bas, ldof, wall_nodes[j]);
	  }
	}
      }
    }
  }
  return wall_cache[degree];
}

/* Now for the edges (i.e. to be able to adjust center nodes of
 * tetrahedrons with a single curved edge).
 */
static const PARAM_FACE_CACHE *center_edge_valuesY_3d(int degree)
{
  static PARAM_FACE_CACHE (*edge_cache)[N_EDGES_3D];
  static int max_deg;
  PARAM_FACE_CACHE *cache;
  int i, j, d, edge;

  if (max_deg < degree) {
    if (!max_deg) {
      edge_cache = (PARAM_FACE_CACHE (*)[N_EDGES_3D])
	MEM_ALLOC((degree+1)*N_EDGES_3D, PARAM_FACE_CACHE);
    } else {
      edge_cache = (PARAM_FACE_CACHE (*)[N_EDGES_3D]) 
	MEM_REALLOC(edge_cache,
		    (max_deg+1)*N_EDGES_3D, (degree+1)*N_EDGES_3D,
		    PARAM_FACE_CACHE);
    }
    for (d = max_deg+1; d <= degree; d++) {
      edge_cache[d][0].values = NULL;
    }
    max_deg = degree;
  }
  if (!edge_cache[degree][0].values) {
    int n_c = (degree-1)*(degree-2)*(degree-3)/6;
    const BAS_FCTS *bas;
    const REAL_B   *center_nodes;
    REAL_B         edge_nodes[n_c];
    REAL           s;

    for (edge = 0; edge < N_EDGES_3D; edge++) {
      int n_e     = degree-1;
      int n_e_bas = n_e + N_VERTICES_1D;
      const int *voe = vertex_of_edge_3d[edge];
      
      cache = &edge_cache[degree][edge];
      cache->values = MEM_ALLOC(degree, REAL **);
      cache->scale  = MEM_ALLOC(n_c, REAL);
      cache->n_nodes = n_c;
      bas = get_lagrange(MESH_DIM, degree);
      center_nodes  = LAGRANGE_NODES(bas);
      center_nodes += bas->n_bas_fcts - n_c;
      for (i = 0; i < n_c; i++) {
	SET_BAR(MESH_DIM, 0.0, edge_nodes[i]);
	cache->scale[i] = s = center_nodes[i][voe[0]] + center_nodes[i][voe[1]];
	for (j = 0; j < N_VERTICES_1D; j++) {
	  edge_nodes[i][voe[j]] = center_nodes[i][voe[j]] / s;
	}
      }
      cache->values[0] = MEM_ALLOC(n_e_bas, REAL *);
      for (i = 0; i < n_e_bas; i++) {
	cache->values[0][i] = MEM_ALLOC(n_c, REAL);
      }
      for (i = 0; i < N_VERTICES_1D; i++) {
	for (j = 0; j < n_c; j++) {
	  cache->values[0][i][j] = PHI(bas, voe[i], edge_nodes[j]);
	}
      }
      for (i = 0; i < n_e; i++) {
	int ldof = N_VERTICES_3D + edge*n_e + i;
	int ci   = N_VERTICES_1D+i; /* cache index */
	for (j = 0; j < n_c; j++) {
	  cache->values[0][ci][j] = PHI(bas, ldof, edge_nodes[j]);
	}
      }

      for (d = 1; d < degree; d++) {
	bas = get_lagrange(MESH_DIM, d);
	n_e = d-1;
	n_e_bas = n_e + N_VERTICES_1D;
	cache->values[d] = MEM_ALLOC(n_e_bas, REAL *);
	for (i = 0; i < n_e_bas; i++) {
	  cache->values[d][i] = MEM_ALLOC(n_c, REAL);
	}
	for (i = 0; i < N_VERTICES_1D; i++) {
	  for (j = 0; j < n_c; j++) {
	    cache->values[d][i][j] = PHI(bas, voe[i], edge_nodes[j]);
	  }
	}
	for (i = 0; i < n_e; i++) {
	  int ldof = N_VERTICES_3D + edge*n_e + i;
	  int ci   = N_VERTICES_1D+i; /* cache index */
	  for (j = 0; j < n_c; j++) {
	    cache->values[d][ci][j] = PHI(bas, ldof, edge_nodes[j]);
	  }
	}
      }
    }
  }
  return edge_cache[degree];
}

/**Construct the Lagrange nodes for the iso-parametric
 * parameterisation described by Lenoir. This function is quite
 * simple, it just puts together all the collected data and computes
 * the proper position of the center/face nodes by means of a Horner
 * scheme. This function is for walls and elements with a single
 * curved edge.
 *
 * @param[in] degree Degree of the parameterisation
 *
 * @param[in] cache The values of all Lagrange basis-functions
 * of degree <= @a degree at all projected center nodes.
 *
 * @param[in] edge_coords The projected world coordinates of the
 * curved edge for all Lagrange basis-functions of degree <= @a
 * degree.
 *
 * @param[in] inverse If true then the projected world coordinates
 * specified by @a edge_coords are in inverse order relative to the
 * local ordering of the basis functions on this element.
 *
 * @param[in,out] center_node_adj The face or center node coordinates
 * suitable for an optimal iso-parametric parameterisation. Note that
 * this function @b adds the the correction to whatever is initially
 * stored in @a center_nodes_adj.
 */
static void center_edge_paramY_3d(int degree,
				  const PARAM_FACE_CACHE *cache,
				  REAL_D edge_coords[DEGREE_MAX][DEGREE_MAX-1],
				  int inverse,
				  REAL_D *center_nodes_adj)
{
  int i, j, d;
  const REAL *v0, *v1;
  
  if (inverse) {
    v0 = edge_coords[1][1];
    v1 = edge_coords[1][0];
  } else {
    v0 = edge_coords[1][0];
    v1 = edge_coords[1][1];
  }
    
  for (i = 0; i < cache->n_nodes; i++) {
    REAL_D x;
    REAL s = cache->scale[i];
    int n_e = degree-1;

    AXPBY_DOW(cache->values[0][0][i], v0, cache->values[0][1][i], v1, x);
    for (j = 0; j < n_e; j++) {
      int ecj = inverse ? n_e - 1 - j : j; /* edge_coord's j */
      AXPY_DOW(cache->values[0][j+N_VERTICES_1D][i], edge_coords[0][ecj], x);
    }
    SCAL_DOW(s, x);
    for (d = degree-1; d >= 2; d--) {
      REAL_D tmp;

      n_e = d - 1;
      AXPBY_DOW(cache->values[d][0][i], v0, cache->values[d][1][i], v1, tmp);
      for (j = 0; j < n_e; j++) {
	int ecj = inverse ? n_e - 1 - j : j;
	AXPY_DOW(cache->values[d][j+N_VERTICES_1D][i],
		 edge_coords[d][ecj], tmp);
      }
      AXPY_DOW(1.0-s, tmp, x);
      SCAL_DOW(s, x);
    }
    /* linear term */
    AXPBYP_DOW(-s*cache->values[1][0][i], v0, -s*cache->values[1][1][i], v1, x);

    AXPY_DOW(s, x, center_nodes_adj[i]);
  }
}

/* Same as center_edge_paramY_3d(), but for curved faces. */
static void
center_face_paramY_3d(
  int degree,
  const PARAM_FACE_CACHE *cache,
  REAL_D (*face_coords)[(DEGREE_MAX-1)*(DEGREE_MAX-2)/2],
  REAL_D (*edge_coords[N_VERTICES_2D])[DEGREE_MAX-1],
  int edge_inverted[N_VERTICES_2D],
  REAL_D *center_nodes_adj)
{
  int i, j, d, edge2d;
  const REAL *v0 = face_coords[1][0];
  const REAL *v1 = face_coords[1][1];
  const REAL *v2 = face_coords[1][2];

  for (i = 0; i < cache->n_nodes; i++) {
    REAL_D x, tmp;
    REAL s = cache->scale[i];
    int n_e = degree-1;
    int n_f = (degree-1)*(degree-2)/2;
    int cj;

    AXPBYPCZ_DOW(cache->values[0][0][i], v0,
		 cache->values[0][1][i], v1,
		 cache->values[0][2][i], v2,
		 x);
    cj = N_VERTICES_2D;
    for (edge2d = 0; edge2d < N_EDGES_2D; edge2d++) {
      int inverse = edge_inverted[edge2d];
      for (j = 0; j < n_e; j++) {
	int ecj = inverse ? n_e - 1 - j : j;
	AXPY_DOW(cache->values[0][cj++][i], edge_coords[edge2d][0][ecj], x);
      }
    }
    for (j = 0; j < n_f; j++) {
      AXPY_DOW(cache->values[0][cj++][i], face_coords[0][j], x);
    }
    SCAL_DOW(s, x);
    for (d = degree-1; d >= 2; d--) {
      n_f = (d - 1)*(d - 2)/2;
      n_e = d-1;
      
      AXPBYPCZ_DOW(cache->values[d][0][i], v0,
		   cache->values[d][1][i], v1,
		   cache->values[d][2][i], v2,
		   tmp);
      cj = N_VERTICES_2D;
      for (edge2d = 0; edge2d < N_EDGES_2D; edge2d++) {
	int inverse = edge_inverted[edge2d];
	for (j = 0; j < n_e; j++) {
	  int ecj = inverse ? n_e - 1 - j : j;
	  AXPY_DOW(cache->values[d][cj++][i], edge_coords[edge2d][d][ecj], tmp);
	}
      }
      for (j = 0; j < n_f; j++) {
	AXPY_DOW(cache->values[d][cj++][i], face_coords[d][j], tmp);
      }
      AXPY_DOW(1.0-s, tmp, x);
      SCAL_DOW(s, x);
    }
    /* linear term */
    AXPBYPCZP_DOW(-s*cache->values[1][0][i], v0,
		  -s*cache->values[1][1][i], v1,
		  -s*cache->values[1][2][i], v2,
		  x);
    AXPY_DOW(s, x, center_nodes_adj[i]);
  }
}

/* Generate the Lagrange-nodes of the children w.r.t. to the parent.
 * We compute them on-the-fly if needed.
 *
 * The return value is indexed as follows:
 *
 * child_nodes[degree][ich][dof_loc]
 *
 * where ich == 0 for child0
 *           == 1 for child1/type0
 *           == 2 for child1/type1/2
 *
 * node_ptr takes the values of the Lagrange nodes of all Lagrange
 * basis functions <= degree.
 */
static const REAL_B *const(*child_nodes_3d(
			     int degree,
			     const REAL_B *const**node_ptr))[3]
{	       
  FUNCNAME("child_nodes_3d");
  static const REAL_B child_loc[3][N_VERTICES_3D] = {
    { /* child 0 */
      { 1.0, 0.0, 0.0, 0.0 },
      { 0.0, 0.0, 1.0, 0.0 },
      { 0.0, 0.0, 0.0, 1.0 },
      { 0.5, 0.5, 0.0, 0.0 }
    },
    { /* chidl 1, type 0 */
      { 0.0, 1.0, 0.0, 0.0 },
      { 0.0, 0.0, 0.0, 1.0 },
      { 0.0, 0.0, 1.0, 0.0 },
      { 0.5, 0.5, 0.0, 0.0 }
    },
    { /* child 1, type 1/2 */
      { 0.0, 1.0, 0.0, 0.0 },
      { 0.0, 0.0, 1.0, 0.0 },
      { 0.0, 0.0, 0.0, 1.0 },
      { 0.5, 0.5, 0.0, 0.0 }
    }
  };
  static REAL_B *(*child_nodes)[3];
  static const REAL_B **lagrange_nodes;
  static int degree_max;
  int i, j, k, d;

  if (!child_nodes) {
    child_nodes = (REAL_B *(*)[3])MEM_ALLOC(3*(degree+1), REAL_B *);
    lagrange_nodes = (const REAL_B **)MEM_ALLOC(degree+1, REAL_B *);
  } else if (degree > degree_max) {
    child_nodes = (REAL_B *(*)[3])
      MEM_REALLOC(child_nodes, 3*(degree_max+1), 3*(degree+1), REAL_B *);
    lagrange_nodes = (const REAL_B **)
      MEM_REALLOC(lagrange_nodes, degree_max+1, degree+1, REAL_B *);
  }

  if (degree_max < degree) {
    for (d = MAX(degree_max, 1); d <= degree; d++) {
      const BAS_FCTS *bas_fcts = get_lagrange(MESH_DIM, d);    
      const REAL_B *nodes = LAGRANGE_NODES(bas_fcts);
      int n_bas_fcts;

      lagrange_nodes[d] = nodes;

      n_bas_fcts = (d+1)*(d+2)*(d+3)/6;
    
      child_nodes[d][0] = MEM_ALLOC(n_bas_fcts, REAL_B);
      child_nodes[d][1] = MEM_ALLOC(n_bas_fcts, REAL_B);
      child_nodes[d][2] = MEM_ALLOC(n_bas_fcts, REAL_B);

      for (i = 0; i < n_bas_fcts; i++) {
	for (j = 0; j < 3; j++) {
	  AXEY_BAR(MESH_DIM, nodes[i][0], child_loc[j][0],
		   child_nodes[d][j][i]);
	  for (k = 1; k < N_VERTICES_3D; k++) {
	    AXPY_BAR(MESH_DIM, nodes[i][k], child_loc[j][k],
		     child_nodes[d][j][i]);
	  }
	}
      }
    }
    degree_max = degree;
  }

  *node_ptr = lagrange_nodes;

  return (const REAL_B *const(*)[3])child_nodes;
}

/* We handle the refinement edge separately: it might be that only
 * some elements of the patch define projections which apply to the
 * refinement edge. But for PARAM_STRAIGHT_CHILDS we need (at least)
 * the position of the mid-point of the refinement edge to generate
 * the correct coordinates. We handle all DOFs of the refinement edge
 * here because it simplifies other stuff.
 */
static void project_ref_edgeY_3d(DOF_REAL_D_VEC *drdv, RC_LIST_EL *list, int n,
				 PARAMY_DATA_3D *Ydata)
{
  const EL_INFO   *el_info;
  PARAM_STRATEGY  strategy   = Ydata->strategy;
  NODE_PROJECTION *n_proj    = Ydata->n_proj;
  bool            selective  = n_proj != NULL;
  int             degree     = Ydata->degree;
  int             node_e     = Ydata->node_e;
  int             node_v     = Ydata->node_v;
  int             n0_e       = Ydata->n0_e;
  int             n0_v       = Ydata->n0_v;
  int             n_e        = Ydata->n_e;
  int             n0_edge_pr = Ydata->n0_edge_pr;
  NODE_PROJECTION **edge_pr  = Ydata->edge_pr;
  REAL_D          *vec       = Ydata->vec;
  const REAL_B    *nodes     = Ydata->nodes[degree];
  const REAL_B    *const*child_nodes = Ydata->child_nodes[degree];  
  const NODE_PROJECTION *act_proj;
  DOF v0dof, v1dof;
  DOF c_re_dofs[2][DEGREE_MAX-1];
  DOF c_re_edge_pr_dof[2] = { -1, };
  int voe20 = vertex_of_edge_3d[2][0], voe21 = vertex_of_edge_3d[2][1];
  int i_child;
  EL *el;
  DOF re_dof;
  int i;

  el = list->el_info.el;
  re_dof = el->child[0]->dof[node_v+3][n0_v];
  if (edge_pr) {
    Ydata->ref_edge_untouched = edge_pr[el->dof[node_e+0][n0_edge_pr]] == NULL;
    c_re_edge_pr_dof[0] = el->child[0]->dof[node_e+2][n0_edge_pr];
    c_re_edge_pr_dof[1] = el->child[1]->dof[node_e+2][n0_edge_pr];
    edge_pr[c_re_edge_pr_dof[0]] =
      edge_pr[c_re_edge_pr_dof[1]] = NULL;
  } else {
    Ydata->ref_edge_untouched = false;
  }

  v0dof = el->dof[node_v+0][n0_v];
  v1dof = el->dof[node_v+1][n0_v];

  for (i_child = 0; i_child < 2; i_child++) {
    if (el->child[i_child]->dof[voe20][0]
	<
	el->child[i_child]->dof[voe21][0]) {
      for (i = 0; i < n_e; i++) {
	c_re_dofs[i_child][i] = el->child[i_child]->dof[node_e+2][n0_e+i];
      }
    } else {
      for (i = 0; i < n_e; i++) {
	c_re_dofs[i_child][i] =
	  el->child[i_child]->dof[node_e+2][n0_e+n_e-i-1];
      }
    }
  }

  if (strategy == PARAM_STRAIGHT_CHILDS) {
    /* Handle only the new vertex here, computation of the other DOFs
     * on the refinement edge is handled further below.
     */
    AXPBY_DOW(0.5, vec[v0dof], 0.5, vec[v1dof], vec[re_dof]);
    NEW_PATCH_EDGE(0 /* el */, 0 /* i_ch */, 2, v0dof);
    NEW_PATCH_EDGE(0 /* el */, 1 /* i_ch */, 2, v1dof);
  }
  
  if (!Ydata->ref_edge_untouched) {
    const REAL_B *cnodes[2];
    int elem;

    cnodes[0] = child_nodes[0];
    cnodes[1] = child_nodes[1 + (list->el_info.el_type != 0)];

    for (elem = 0; elem < n; elem++) {
      el_info = &list[elem].el_info;
      el = el_info->el;

      act_proj = wall_proj(el_info, 2);
      if (act_proj == NULL) {
	act_proj = wall_proj(el_info, 3);
      }
      if (act_proj == NULL) {
	act_proj = wall_proj(el_info, -1);
      }
      if (act_proj && (!selective || n_proj == act_proj)) {

	if (act_proj->func) {
	  act_proj->func(vec[re_dof], el_info, middle_bary);

	  for (i = 0; i < n_e; i++) {
	    int ldof = N_VERTICES_3D+2*n_e+i;

	    AXPBY_DOW(nodes[ldof][0], vec[v0dof], nodes[ldof][3], vec[re_dof],
		      vec[c_re_dofs[0][i]]);
	    AXPBY_DOW(nodes[ldof][0], vec[v1dof], nodes[ldof][3], vec[re_dof],
		      vec[c_re_dofs[1][i]]);
	  }

	  /* Project all other edge DOFs */
	  for (i = 0; i < n_e; i++) {
	    int ldof = N_VERTICES_3D+2*n_e+i;
	    
	    act_proj->func(vec[c_re_dofs[0][i]], el_info, cnodes[0][ldof]);
	    act_proj->func(vec[c_re_dofs[1][i]], el_info, cnodes[1][ldof]);
	  }
	}
	
	if (edge_pr) {
	  edge_pr[c_re_edge_pr_dof[0]] =
            edge_pr[c_re_edge_pr_dof[1]] = (void *)act_proj;
	}
	break; /* Need only one projection */
      }
    }
    if (elem == n) {
      Ydata->ref_edge_untouched = true;
    }
  }

  /* Compute the coordinates for the other DOFs along the old
   * refinement edge if not done so already.
   */
  if (Ydata->ref_edge_untouched && strategy == PARAM_STRAIGHT_CHILDS) {
    for (i = 0; i < n_e; i++) {
      int ldof = N_VERTICES_3D+2*n_e+i;

      AXPBY_DOW(nodes[ldof][0], vec[v0dof], nodes[ldof][3], vec[re_dof],
		vec[c_re_dofs[0][i]]);
      AXPBY_DOW(nodes[ldof][0], vec[v1dof], nodes[ldof][3], vec[re_dof],
		vec[c_re_dofs[1][i]]);
    }
  }
}

/* Generate the projected Lagrange-nodes for all degrees up to the
 * target degree for the given edge. We assume that this function is
 * only called when this edge is really subject to a projection.
 *
 * @param[in] el_info EL_INFO descriptor for the parent element.
 *
 * @param[in] elem Number of element in the patch
 *
 * @param[in] i_child Number of the child (0,1)
 *
 * @param[in] edge Number of the edge w.r.t. to the child.
 *
 * @param[in,out] Ydata Patch-cache structure.
 */
static void project_edge_nodesY_3d(const EL_INFO *el_info,
				   int elem,
				   int i_child,
				   int edge,
                                   const NODE_PROJECTION *act_proj,
				   PARAMY_DATA_3D *Ydata)
{
  const DOF *dof = Ydata->child_dofs[elem][i_child];
  REAL_D (*edge_nodes)[DEGREE_MAX-1];
  int n_e;
  int i, d;
  int i_e;
  int inv;
  int v0, v1;

  n_e = Ydata->n_e;
  i_e = Ydata->edge_no[elem][i_child][edge];
  v0  = vertex_of_edge_3d[edge][0];
  v1  = vertex_of_edge_3d[edge][1];
  if ((inv = (dof[v0] != Ydata->edge_v0[i_e]))) {
    int tmp = v0;
    v0 = v1;
    v1 = tmp;
  }
  edge_nodes = Ydata->edge_nodes[i_e];
  
  /* Copy over the coordinates for the target degree */
  for (i = 0; i < n_e; i++) {
    int ldof = N_VERTICES_3D + edge*n_e + (inv ? n_e - 1 - i : i);
    
    COPY_DOW(Ydata->vec[dof[ldof]], edge_nodes[0][i]);
  }

  /* Copy over the vertex coordinates (degree 1) */
  COPY_DOW(Ydata->vec[dof[v0]], edge_nodes[1][0]);
  COPY_DOW(Ydata->vec[dof[v1]], edge_nodes[1][1]);
  
  /* Generate the projections for all other degrees */
  for (d = 2; d < Ydata->degree; d++) {
    int cn_idx = el_info->el_type == 0 ? i_child : i_child+1;
    const REAL_B *c_bary = Ydata->child_nodes[d][cn_idx];
    REAL edge_inc = 1.0/(REAL)d;
    n_e = d-1;
    for (i = 0; i < n_e; i++) {
      int ldof = N_VERTICES_3D + edge*n_e + (inv ? n_e - 1 - i : i);

      AXPBY_DOW((REAL)(n_e-i)*edge_inc, edge_nodes[1][0],
		(REAL)(i+1)*edge_inc, edge_nodes[1][1],
		edge_nodes[d][i]);

      if (act_proj && act_proj->func) {
	act_proj->func(edge_nodes[d][i], el_info, c_bary[ldof]);
      } else {
        abort();
      }
    }
  }
}

/* Generate the projected Lagrange-nodes for all degrees up to the
 * target degree for the given face. We assume that this function is
 * only called when this edge is really subject to a projection.
 *
 * @param[in] el_info EL_INFO descriptor for the parent element.
 *
 * @param[in] elem Number of element in the patch
 *
 * @param[in] i_child Number of the child (0,1)
 *
 * @param[in] face Number of the edge w.r.t. to the child.
 *
 * @param[in,out] Ydata Patch-cache structure.
 */
static void
project_face_nodesY_3d(const EL_INFO *el_info,
		       int elem,
		       int i_child,
		       int face,
                       const NODE_PROJECTION *act_proj,
		       PARAMY_DATA_3D *Ydata,
		       REAL_D (*face_nodes)[(DEGREE_MAX-1)*(DEGREE_MAX-2)/2])
{
  const DOF *dof = Ydata->child_dofs[elem][i_child];
  int n_f, n_e;
  int i, d;
  int v0, v1, v2;

  if (act_proj == NULL) {
    abort();
  }

  n_f = Ydata->n_f;
  n_e = Ydata->n_e;
  v0  = vertex_of_wall_3d[face][0];
  v1  = vertex_of_wall_3d[face][1];
  v2  = vertex_of_wall_3d[face][2];
  
  /* Copy over the coordinates for the target degree */
  for (i = 0; i < n_f; i++) {
    int ldof = N_VERTICES_3D + N_EDGES_3D*n_e + face*n_f + i;
    
    COPY_DOW(Ydata->vec[dof[ldof]], face_nodes[0][i]);
  }

  /* Copy over the vertex coordinates (degree 1) */
  COPY_DOW(Ydata->vec[dof[v0]], face_nodes[1][0]);
  COPY_DOW(Ydata->vec[dof[v1]], face_nodes[1][1]);
  COPY_DOW(Ydata->vec[dof[v2]], face_nodes[1][2]);
  
  /* Generate the projections for all other degrees */
  for (d = 2; d < Ydata->degree; d++) {
    int cn_idx = el_info->el_type == 0 ? i_child : i_child+1;
    const REAL_B *c_bary = Ydata->child_nodes[d][cn_idx];
    const REAL_B *nodes  = Ydata->nodes[d];
    
    n_f = (d-1)*(d-2)/2;
    n_e = d-1;
    for (i = 0; i < n_f; i++) {
      int ldof = N_VERTICES_3D + N_EDGES_3D*n_e + face*n_f + i;

      AXPBYPCZ_DOW(nodes[ldof][0], face_nodes[1][0],
		   nodes[ldof][0], face_nodes[1][1],
		   nodes[ldof][0], face_nodes[1][2],
		   face_nodes[d][i]);
      if (act_proj && act_proj->func) {
	act_proj->func(face_nodes[d][i], el_info, c_bary[ldof]);
      } else {
        abort();
      }
    }
  }
}
 
/* Compute the new position of the Lagrange-nodes for one face of one
 * child of a given element. This function takes care of positioning
 * the nodes s.t. optimal convergence rates are achieved.
 */
static void adjust_face_nodesY_3d(const EL_INFO *el_info,
				  int elem,
				  int i_child,
				  int face,
				  PARAMY_DATA_3D *Ydata)
{
  int is_affine       = Ydata->is_affine[elem];
  const DOF *dof      = Ydata->child_dofs[elem][i_child];
  const REAL_B *nodes = Ydata->nodes[Ydata->degree];
  REAL_D *vec         = Ydata->vec;
  NODE_PROJECTION **edge_pr = Ydata->edge_pr;
  int n_f             = Ydata->n_f;
  int n_e             = Ydata->n_e;
  int node_e          = Ydata->node_e;
  int n0_edge_pr      = Ydata->n0_edge_pr;
  DOF **cdofp         = el_info->el->child[i_child]->dof;
  const int *vow = vertex_of_wall_3d[face];
  int n_edge_adj;
  int i, j;
  int adj_edge[N_EDGES_2D];
  NODE_PROJECTION *adj_pr[N_EDGES_2D];
  REAL_D adj[n_f];
  REAL scale;
  
  for (i = 0; i < n_f; i++) {
    int local_dof = N_VERTICES_3D + N_EDGES_3D*n_e + face*n_f + i;

    AXPBYPCZ_DOW(nodes[local_dof][vow[0]], vec[dof[vow[0]]],
		 nodes[local_dof][vow[1]], vec[dof[vow[1]]],
		 nodes[local_dof][vow[2]], vec[dof[vow[2]]],
		 vec[dof[local_dof]]);
  }
  
  if (is_affine) {
    return;
  }

  n_edge_adj = 0;
  for (i = 0; i < N_EDGES_2D; i++) {
    int edge = edge_of_face[face][i];
    
    if (edge_pr[cdofp[node_e+edge][n0_edge_pr]] == NULL) {
      continue;
    }
    
    adj_pr[n_edge_adj] = edge_pr[cdofp[node_e+edge][n0_edge_pr]];
    adj_edge[n_edge_adj++] = i;
  }

  if (!n_edge_adj || n_edge_adj == N_EDGES_2D) {
    return;
  }
  
  for (j = 0; j < n_f; j++) {
    SET_DOW(0.0, adj[j]);
  }

#if SIMPLE_HIGH_DEGREE
  /* This "#if 0" part (and the corresponding "#if 0" part in
   * "adjust_center_nodesY_3d()" implements the generalisation of the
   * 2d approach. This, however, does not work. Experimentally it is
   * ok to adjust face nodes and center nodes of elements with a
   * single curved edge by this "#if 0" algorithm.
   */
  for (i = 0; i < n_edge_adj; i++) {
    int edge = edge_of_face[face][adj_edge[i]];
    REAL_B lambda;
    int v0, v1;
    
    lambda[face_of_edge[edge][0]] =
      lambda[face_of_edge[edge][1]] = 0.0;
    v0 = vertex_of_edge_3d[edge][0];
    v1 = vertex_of_edge_3d[edge][1];
    
    for (j = 0; j < n_f; j++ ) {
      int ldof = N_VERTICES_3D + N_EDGES_3D*n_e + face*n_f + j;
      int e_ldof;
      REAL_D d;

      lambda[v0] = 1.0 - nodes[ldof][v1];
      lambda[v1] = nodes[ldof][v1];

      /* This is actually a Lagrange-node on the edge, determine its number. */
      e_ldof  = (int)(lambda[v1]*(REAL)(n_e + 1) + 0.5) - 1;
      e_ldof += N_VERTICES_3D + n_e*edge;
      AXPBY_DOW(lambda[v0], vec[dof[v0]], lambda[v1], vec[dof[v1]], d);
      AXPY_DOW(-1.0, vec[dof[e_ldof]], d);
      AXPY_DOW(-0.5*nodes[ldof][v0]/lambda[v0], d, adj[j]);
      
      /* symmetrize */
      lambda[v0] = nodes[ldof][v0];
      lambda[v1] = 1.0 - nodes[ldof][v0];

      /* This is actually a Lagrange-node on the edge, determine its number. */
      e_ldof  = n_e - 1 - (int)(lambda[v0]*(REAL)(n_e + 1) + 0.5) + 1;
      e_ldof += N_VERTICES_3D + n_e*edge;
      AXPBY_DOW(lambda[v0], vec[dof[v0]], lambda[v1], vec[dof[v1]], d);
      AXPY_DOW(-1.0, vec[dof[e_ldof]], d);
      AXPY_DOW(-0.5*nodes[ldof][v1]/lambda[v1], d, adj[j]);
    }
  }
#else  
  for (i = 0; i < n_edge_adj; i++) {
    int edge = edge_of_face[face][adj_edge[i]];
    int patch_edge = Ydata->edge_no[elem][i_child][edge];
    int v0, inverse;
    
    /* Compute Lagrange nodes for all degrees if not already done */
    if (Ydata->edge_nodes[patch_edge][0][0][0] == HUGE_VAL) {
      project_edge_nodesY_3d(el_info, elem, i_child, edge, adj_pr[i], Ydata);
    }
    
    v0 = vertex_of_edge_3d[edge][0];
    inverse = dof[v0] != Ydata->edge_v0[patch_edge];

    /* Compute the adjustment by Lenoir's construction */
    center_edge_paramY_3d(Ydata->degree,
			  &Ydata->wall_edge_cache[face][adj_edge[i]],
			  Ydata->edge_nodes[patch_edge],
			  inverse, adj);
  }
#endif

  /* Apply the corrections to the unprojected coordinates */
  scale = 1.0/(REAL)n_edge_adj;
  for (j = 0; j < n_f; j++) {
    int ldof = N_VERTICES_3D + N_EDGES_3D*n_e + face*n_f + j;
    AXPY_DOW(scale, adj[j], vec[dof[ldof]]);
  }
}

/* Compute the new coordinates of the center nodes for one child of a
 * given element. We return "true" if adjustments were necessary,
 * "false" otherwise.
 */
static int adjust_center_nodesY_3d(const EL_INFO *el_info,
				   int elem,
				   int i_child,
				   PARAMY_DATA_3D *Ydata)
{
  int i, j, k;
  const REAL_B *nodes = Ydata->nodes[Ydata->degree];
  int is_affine       = Ydata->is_affine[elem];
  const DOF *dof      = Ydata->child_dofs[elem][i_child];
  int n_c             = Ydata->n_c;
  int n_e             = Ydata->n_e;
  int n_f             = Ydata->n_f;
  int node_e          = Ydata->node_e;
  int n0_edge_pr      = Ydata->n0_edge_pr;
  REAL_D *vec         = Ydata->vec;
  NODE_PROJECTION **edge_pr = Ydata->edge_pr;
  DOF **cdofp         = el_info->el->child[i_child]->dof;
  NODE_PROJECTION *edge_pr_loc[N_EDGES_3D];
  const NODE_PROJECTION *face_pr[N_FACES_3D];
  U_CHAR touched_faces[N_FACES_3D];
  U_CHAR adj_face[N_FACES_3D];
  U_CHAR adj_edge[N_EDGES_3D];
  REAL_D adj[n_c];
  int n_face_adjust, n_edge_adjust;
  int face, edge;
  REAL scale;

  for (i = 0; i < n_c; i++) {
    int loc_dof = N_VERTICES_3D + N_EDGES_3D*n_e + N_FACES_3D*n_f + i;
    for (j = 0; j < DIM_OF_WORLD; j++) {
      vec[dof[loc_dof]][j] = nodes[loc_dof][0]*vec[dof[0]][j];
      for (k = 1; k < N_VERTICES_3D; k++) {
	vec[dof[loc_dof]][j] += nodes[loc_dof][k]*vec[dof[k]][j];
      }
    }
  }

  if (is_affine) {
    return false;
  }

  /* otherwise we have to adjust all center nodes to ensure
   * optimal convergence rates.
   */

  for (edge = 0; edge < N_EDGES_3D; edge++) {
    edge_pr_loc[edge] = edge_pr[cdofp[node_e+edge][n0_edge_pr]];
  }

  n_face_adjust = 0;
  for (face = 0; face < N_WALLS_3D; face++) {
    NODE_PROJECTION *n_proj  = Ydata->n_proj;
    bool selective = n_proj != NULL;
    const NODE_PROJECTION *act_proj = NULL;

    int pf = parent_face_3d[el_info->el_type][face][i_child];
    
    if (pf >= 0) {
      act_proj = wall_proj(el_info, pf);
    }
    if (act_proj == NULL) {
      act_proj = wall_proj(el_info, -1);
    }
    if ((selective && act_proj != n_proj) ||
        (act_proj && act_proj->func == NULL)) {
      act_proj = NULL;
    }
    if ((touched_faces[face] = act_proj != NULL)) {
      face_pr[n_face_adjust] = act_proj;
      adj_face[n_face_adjust++] = face;
    }
  }

  n_edge_adjust = 0;
  for (edge = 0; edge < N_EDGES_3D; edge++) {

    if (edge_pr_loc[edge] == NULL) {
      /* This edge is not curved */
      continue;
    }

    /* determine whether this edge belongs to a curved face; in that
     * case no extra correction need to be applied.
     */
    if (touched_faces[face_of_edge[edge][0]]
	||
	touched_faces[face_of_edge[edge][1]]) {
      continue; /* the edge belongs to a curved face */
    }
    adj_edge[n_edge_adjust++] = edge;
  }

  if (!n_edge_adjust && !n_face_adjust) { /* nothing to be done */
    return false;
  }
  
  for (i = 0; i < n_c; i++) {
    SET_DOW(0.0, adj[i]);
  }

#if SIMPLE_HIGH_DEGREE
  /* See the comment in "adjust_face_nodesY_3d()". */
  for (i = 0; i < n_edge_adjust; i++) {
    int edge = adj_edge[i];
    int v0, v1;
    REAL_B lambda;
    
    v0 = vertex_of_edge_3d[edge][0];
    v1 = vertex_of_edge_3d[edge][1];
    lambda[face_of_edge[edge][0]] =
      lambda[face_of_edge[edge][1]] = 0.0;

    for (j = 0; j < n_c; j++) {
      int ldof = N_VERTICES_3D + N_EDGES_3D*n_e + N_FACES_3D*n_f + j;
      int e_ldof;
      REAL_D d;

      lambda[v0] = 1.0 - nodes[ldof][v1];
      lambda[v1] = nodes[ldof][v1];
      
      /* This is actually a Lagrange-node on the edge, determine its number. */
      e_ldof  = (int)(lambda[v1]*(REAL)(n_e + 1) + 0.5) - 1;
      e_ldof += N_VERTICES_3D + n_e*edge;
      AXPBY_DOW(lambda[v0], vec[dof[v0]], lambda[v1], vec[dof[v1]], d);
      AXPY_DOW(-1.0, vec[dof[e_ldof]], d);
      AXPY_DOW(-0.5*nodes[ldof][v0]/lambda[v0], d, adj[j]);
      
      /* symmetrize */
      lambda[v0] = nodes[ldof][v0];
      lambda[v1] = 1.0 - nodes[ldof][v0];

      /* This is actually a Lagrange-node on the wall, determine its number. */
      e_ldof  = n_e - 1 - (int)(lambda[v0]*(REAL)(n_e + 1) + 0.5) + 1;
      e_ldof += N_VERTICES_3D + n_e*edge;
      AXPBY_DOW(lambda[v0], vec[dof[v0]], lambda[v1], vec[dof[v1]], d);
      AXPY_DOW(-1.0, vec[dof[e_ldof]], d);
      AXPY_DOW(-0.5*nodes[ldof][v1]/lambda[v1], d, adj[j]);
    }
  }
#else
  for (i = 0; i < n_edge_adjust; i++) {
    int inverse, v0;
    int patch_edge;

    edge = adj_edge[i];
    patch_edge = Ydata->edge_no[elem][i_child][edge];

    if (Ydata->edge_nodes[patch_edge][0][0][0] == HUGE_VAL) {
      project_edge_nodesY_3d(el_info, elem, i_child, edge, edge_pr_loc[edge], Ydata);
    }

    v0 = vertex_of_edge_3d[edge][0];
    inverse = dof[v0] != Ydata->edge_v0[patch_edge];
    
    center_edge_paramY_3d(Ydata->degree,
			  &Ydata->center_edge_cache[edge],
			  Ydata->edge_nodes[patch_edge], inverse,
			  adj);
  }
#endif

#if SIMPLE_HIGH_DEGREE
  /* See the comment in "adjust_face_nodesY_3d()". */
  for (i = 0; i < n_face_adjust; i++) {
    int face = adj_face[i];
    const int *vow = vertex_of_wall_3d[face];
    int v0 = vow[0], v1 = vow[1], v2 = vow[2];
    REAL_B lambda;
    
    lambda[face] = 0.0;

    for (j = 0; j < n_c; j++) {
      int ldof = N_VERTICES_3D + N_EDGES_3D*n_e + N_FACES_3D*n_f + j;
      int f_ldof, row, col, tmp;
      REAL_D d;

      for (k = 0; k < N_VERTICES_2D; k++) {
	lambda[v0] = 1.0 - nodes[ldof][v1] - nodes[ldof][v2];
	lambda[v1] = nodes[ldof][v1];
	lambda[v2] = nodes[ldof][v2];

	row = (int)(lambda[vow[2]]*(REAL)(n_e + 1) + 0.5) - 1;
	col = (int)(lambda[vow[1]]*(REAL)(n_e + 1) + 0.5) - 1;
	f_ldof = FACE_DOF(row, col);
	f_ldof += N_VERTICES_3D + N_EDGES_3D*n_e + face*n_f;
	AXPBYPCZ_DOW(lambda[v0], vec[dof[v0]],
		     lambda[v1], vec[dof[v1]],
		     lambda[v2], vec[dof[v2]], d);
	AXPY_DOW(-1.0, vec[dof[f_ldof]], d);
	AXPY_DOW(-1.0/3.0*nodes[ldof][v0]/lambda[v0], d, adj[j]);

	tmp = v0; v0 = v1; v1 = v2; v2 = tmp;
      }
    }
  }
#else
  for (i = 0; i < n_face_adjust; i++) {
    REAL_D face_nodes[DEGREE_MAX][(DEGREE_MAX-1)*(DEGREE_MAX-2)/2];
    REAL_D (*edge_nodes[N_VERTICES_2D])[DEGREE_MAX-1];
    int edge_inverted[N_VERTICES_2D];
    face = adj_face[i];

    /* compute the face nodes. The general case should be that "face"
     * is at the boundary of the domain, so caching should not be
     * necessary here (we support curved interior walls, but we do not
     * optimize for that case.
     */
    project_face_nodesY_3d(
      el_info, elem, i_child, face, face_pr[i], Ydata, face_nodes);

    for (j = 0; j < N_EDGES_2D; j++) {
      int patch_edge, v0;
      
      edge = edge_of_face[face][j];
      patch_edge = Ydata->edge_no[elem][i_child][edge];

      if (Ydata->edge_nodes[patch_edge][0][0][0] == HUGE_VAL) {
	project_edge_nodesY_3d(el_info, elem, i_child, edge, edge_pr_loc[edge],
                               Ydata);
      }

      edge_nodes[j] = Ydata->edge_nodes[patch_edge];
      v0 = vertex_of_edge_3d[edge][0];
      edge_inverted[j] = dof[v0] != Ydata->edge_v0[patch_edge];
    }
    
    center_face_paramY_3d(Ydata->degree,
			  &Ydata->center_wall_cache[face],
			  face_nodes, edge_nodes, edge_inverted,
			  adj);
  }
#endif

  scale = 1.0/(REAL)(n_edge_adjust+n_face_adjust);
  for (i = 0; i < n_c; i++) {
    int loc_dof = N_VERTICES_3D + N_EDGES_3D*n_e + N_FACES_3D*n_f + i;
    AXPY_DOW(scale, adj[i], vec[dof[loc_dof]]);
  }

  return true;
}

/* Compute coordinates for all interior nodes of all children and for
 * the nodes belonging to the wall between the two children.
 */
static void
adjust_patch_center_nodesY_3d(DOF_REAL_D_VEC *drdv, RC_LIST_EL *list, int n,
			      PARAMY_DATA_3D *Ydata)
{
  int             degree    = Ydata->degree;
  int             n_c       = Ydata->n_c;
  int             n_e       = Ydata->n_e;
  int             n_f       = Ydata->n_f;
  REAL_D          *vec      = Ydata->vec;
  NODE_PROJECTION *n_proj   = Ydata->n_proj;
  bool            selective = n_proj != NULL;
  const NODE_PROJECTION *act_proj;
  const REAL_B    *const*child_nodes = Ydata->child_nodes[degree];
  DOF             (*child_dofs)[2][N_BAS_MAX] = Ydata->child_dofs;
  int             ref_edge_untouched = Ydata->ref_edge_untouched;
  const EL_INFO   *el_info;
  int             i, j;
  int             elem, i_child;

  /* Loop over all patch elements. */                                           
  for (elem = 0; elem < n; elem++) {
    el_info = &list[elem].el_info;

    for (i_child = 0; i_child < 2; i_child++) {
      const REAL_B *cnodes;
      DOF *dof = child_dofs[elem][i_child];

      if (i_child == 0) {
	cnodes = child_nodes[i_child];
      } else {
	cnodes = child_nodes[1 + (el_info->el_type != 0)];
      }
	
      if (i_child == 0) {
	int face = 0;
	
	/* Adjust nodes on the face between the children */
	
	adjust_face_nodesY_3d(el_info, elem, i_child, face, Ydata);

	/* Then possibly additionally project the coordinates */
	if (!ref_edge_untouched) {
	  act_proj = wall_proj(el_info, -1);
	  if (act_proj && act_proj->func &&
	      (!selective || n_proj == act_proj)) {
	    for (j = 0; j < n_f; j++) {
	      int ldof = N_VERTICES_3D + N_EDGES_3D*n_e + face*n_f + j;

	      act_proj->func(vec[dof[ldof]], el_info, cnodes[ldof]);
	    }
	  }
	}
      }
      
      /* Then adjust the nodes in the interior. Face and edge DOFs are
       * finished at this point.
       */
      if (n_c <= 0) {
 	break; /* child-loop */
      }
      
      if (adjust_center_nodesY_3d(el_info, elem, i_child, Ydata)) {
	/* Check for a default projection */
	if (!ref_edge_untouched) {
	  act_proj = wall_proj(el_info, -1);
	  if (act_proj && act_proj->func &&
	      (!selective || n_proj == act_proj)) {
	    for (i = 0; i < n_c; i++) {
	      int ldof = N_VERTICES_3D+N_EDGES_3D*n_e+N_FACES_3D*n_f+i;

	      act_proj->func(vec[dof[ldof]], el_info, cnodes[ldof]);
	    }
	  }
	}
      /* There is no need to call make_affine() */
      }
    }
  }
}

/* Compute coordinates for the DOFs of the new edges between the
 * children and for the DOFs located on the walls between the elements
 * of the refinement patch. The "interior" wall between the two
 * children is handled in adjust_patch_center_nodesY_3d() above, just
 * because we loop here around the refinement patch and have not
 * updated all edges until we have finished the loop.
 */
static void
adjust_patch_wall_nodesY_3d(DOF_REAL_D_VEC *drdv, RC_LIST_EL *list, int n,
			    PARAMY_DATA_3D *Ydata)
{
  MESH            *mesh      = Ydata->mesh;
  const BAS_FCTS  *bas_fcts  = Ydata->bas_fcts;
  const DOF_ADMIN *admin     = Ydata->admin;
  NODE_PROJECTION **edge_pr  = Ydata->edge_pr;
  NODE_PROJECTION *n_proj    = Ydata->n_proj;
  bool            selective  = n_proj != NULL;
  REAL_D          *vec       = Ydata->vec;
  int             degree     = Ydata->degree;
  const REAL_B    *nodes     = Ydata->nodes[degree];
  int             node_e     = Ydata->node_e;
  int             n_e        = Ydata->n_e;
  int             n_f        = Ydata->n_f;
  int             n0_edge_pr = Ydata->n0_edge_pr;
  const NODE_PROJECTION *act_proj;
  const REAL_B    *const*child_nodes = Ydata->child_nodes[degree];
  int             (*child_dofs)[2][N_BAS_MAX] = Ydata->child_dofs;
  int             ref_edge_untouched = Ydata->ref_edge_untouched;
  int             *is_affine = Ydata->is_affine;
  const EL_INFO   *el_info;
  EL              *el;
  int             i, j, wall, dir, edge;
  int             elem;
  
  for (elem = 0; elem < n; elem++) {
    const REAL_B *cnodes;
    DOF *dof;
    int i_child;
    EL *child;
    
    el_info = &list[elem].el_info;
    el = el_info->el;

    /* First determine if this element is affine, cache the result. */
    if (!ref_edge_untouched) {
      is_affine[elem] = false;
    } else {
      is_affine[elem] = true;
      for (edge = 0; edge < N_EDGES_3D; edge++) {
	if (edge_pr[el->dof[node_e+edge][n0_edge_pr]] != NULL) {
	  is_affine[elem] = false;
	  break;
	}
      }
    }

    /***************** First child **************/

    i_child = 0;
    child = el->child[i_child];

    dof = child_dofs[elem][i_child];
    GET_DOF_INDICES(bas_fcts, el->child[i_child], admin, dof);
    cnodes = child_nodes[i_child];

    /* Figure out which half of the refinemnt edge belongs to us */
    Ydata->edge_no[elem][i_child][2] = Ydata->edge_v0[0] == dof[0] ? 0 : 1;
    /* Give the "exterior" edge a new number */
    NEW_PATCH_EDGE(elem, i_child, 3, dof[1]);

    for (dir = 0; dir < 2; dir++) {
      RC_LIST_EL *neigh = list[elem].neigh[dir];
      int pface = 2+dir;
      int child_edge = 5-dir;
	
      /* Do it only once for each wall; the mesh-traversal code
       * makes sure that walls also inherit the projection from
       * the neighbour (even if this macro-element does not
       * define a projection for the wall).
       */
      if (neigh && neigh <= list+elem) {
	/* Inherit the edge numbering from the neighbour */
	int neigh_num;
	int neigh_child;

	neigh_num = ((int)(long)neigh - (int)(long)list)/(int)sizeof(*list);
	neigh_child = child_dofs[neigh_num][0][0] == dof[0] ? 0 : 1;

	Ydata->edge_no[elem][0][child_edge] =
	  child_dofs[neigh_num][neigh_child][2] == dof[2-dir]
	  ? Ydata->edge_no[neigh_num][neigh_child][5]
	  : Ydata->edge_no[neigh_num][neigh_child][4];
	Ydata->edge_no[elem][0][1-dir] =
	  child_dofs[neigh_num][neigh_child][1] == dof[2-dir]
	  ? Ydata->edge_no[neigh_num][neigh_child][0]
	  : Ydata->edge_no[neigh_num][neigh_child][1];

	continue;
      }

      /* Old edge at patch boundary between patch neighbours */
      NEW_PATCH_EDGE(elem, i_child, 1-dir, dof[2-dir]);

      /* Handle the edge between the two children */
      
      /* Assign the new edge a new number */
      NEW_PATCH_EDGE(elem, i_child, child_edge, dof[2-dir]);

      /* Generate vertex coordinates if not done so already, edge
       * nodes need not be adjusted to yield optimal convergence
       * results.
       */
      for (i = 0; i < n_e; i++) {
	int local_dof = N_VERTICES_3D+child_edge*n_e+i;

	AXPBY_DOW(nodes[local_dof][3], vec[dof[3]],
		  nodes[local_dof][2-dir], vec[dof[2-dir]],
		  vec[dof[local_dof]]);
      }

      edge_pr[child->dof[node_e+child_edge][n0_edge_pr]] = NULL;

      act_proj = wall_proj(el_info, pface);
      if (act_proj == NULL) {
	act_proj = wall_proj(el_info, -1);
      }
      if (!ref_edge_untouched &&
	  act_proj && (!selective || n_proj == act_proj)) {
	if (act_proj->func) {
	  for (i = 0; i < n_e; i++) {
	    int ldof = N_VERTICES_3D+child_edge*n_e+i;
	    
	    act_proj->func(vec[dof[ldof]], el_info, cnodes[ldof]);
	    _AI_refine_update_bbox(mesh, vec[dof[ldof]]);
	  }
	}
	edge_pr[child->dof[node_e+child_edge][n0_edge_pr]] = (void *)act_proj;
      }

      /* Now handle one of the faces to the neighbours in the
       * refinment patch.
       */
      wall = 1+dir;
      adjust_face_nodesY_3d(el_info, elem, i_child, wall, Ydata);

      /* Then possibly additionally project the coordinates */
      act_proj = wall_proj(el_info, pface);
      if (act_proj == NULL) {
	act_proj = wall_proj(el_info, -1);
      }
      if (!ref_edge_untouched &&
	  act_proj && act_proj->func && (!selective || n_proj == act_proj)) {
	for (j = 0; j < n_f; j++) {
	  int ldof = N_VERTICES_3D + N_EDGES_3D*n_e + wall*n_f + j;
	  
	  act_proj->func(vec[dof[ldof]], el_info, cnodes[ldof]);
	  _AI_refine_update_bbox(mesh, vec[dof[ldof]]);
	}
      }
    }

    /***************** Second child **************/

    i_child = 1;
    child = el->child[i_child];

    dof = child_dofs[elem][i_child];
    GET_DOF_INDICES(bas_fcts, el->child[i_child], admin, dof);
    cnodes = child_nodes[i_child + (el_info->el_type != 0)];

    /* Figure out which half of the refinemnt edge belongs to us, and
     * inherit data from the other child.
     */
    Ydata->edge_no[elem][i_child][2] = 1 - Ydata->edge_no[elem][0][2];
    Ydata->edge_no[elem][i_child][3] = Ydata->edge_no[elem][0][3];

    for (dir = 0; dir < 2; dir++) {
      RC_LIST_EL *neigh = list[elem].neigh[dir];
      int pface = 2+dir;
      int child_edge;
      int edge_v0;

      /* edges between children */
      if (el_info->el_type == 0) {
	Ydata->edge_no[elem][i_child][4+dir] = Ydata->edge_no[elem][0][5-dir];
      } else {
	Ydata->edge_no[elem][i_child][5-dir] = Ydata->edge_no[elem][0][5-dir];
      }
      Ydata->edge_no[elem][i_child][3] = Ydata->edge_no[elem][0][3];

      /* old edge to neighbour */
      child_edge = el_info->el_type == 0 ? 0+dir : 1-dir;
      edge_v0 = el_info->el_type == 0 ? 1+dir : 2 - dir;
	
      if (neigh && neigh <= list+elem) {
	/* inherit old edge to neighbour */
	int neigh_num;
	int neigh_child;

	neigh_num = ((int)(long)neigh - (int)(long)list)/(int)sizeof(*list);
	neigh_child = child_dofs[neigh_num][0][0] == dof[0] ? 0 : 1;

	Ydata->edge_no[elem][i_child][child_edge] =
	  child_dofs[neigh_num][neigh_child][1] == dof[edge_v0]
	  ? Ydata->edge_no[neigh_num][neigh_child][0]
	  : Ydata->edge_no[neigh_num][neigh_child][1];
	continue;
      }

      /* give the old edge to the neighbour a new number */
      NEW_PATCH_EDGE(elem, i_child, child_edge, dof[edge_v0]);

      wall = el_info->el_type == 0 ? 2-dir : 1+dir;
      adjust_face_nodesY_3d(el_info, elem, i_child, wall, Ydata);

      /* Then possibly additionally project the coordinates */
      act_proj = wall_proj(el_info, pface);
      if (act_proj == NULL) {
	act_proj = wall_proj(el_info, -1);
      }
      if (!ref_edge_untouched &&
	  act_proj && act_proj->func && (!selective || n_proj == act_proj)) {
	for (j = 0; j < n_f; j++) {
	  int ldof = N_VERTICES_3D + N_EDGES_3D*n_e + wall*n_f + j;
	  
	  act_proj->func(vec[dof[ldof]], el_info, cnodes[ldof]);
	  _AI_refine_update_bbox(mesh, vec[dof[ldof]]);
	}
      }
    }
  }
}

/* Make sure the given (nodes, dof) element is affine by linear
 * interpolation between its vertices.
 */
static void make_affineY_3d(const REAL_B *nodes, const DOF *dof,
			    int n_e, int n_f, int n_c, REAL_D *vec)
{
  int i, j;
  
  if (n_e > 0) {
    for (i = 0; i < N_EDGES_3D; i++) {
      int v0 = vertex_of_edge_3d[i][0];
      int v1 = vertex_of_edge_3d[i][1];
	    
      for (j = 0; j < n_e; j++) {
	int local_dof = N_VERTICES_3D + i*n_e + j;
	
	AXPBY_DOW(nodes[local_dof][v0], vec[dof[v0]],
		  nodes[local_dof][v1], vec[dof[v1]],
		  vec[dof[local_dof]]);
      }
    }
  }

  if (n_f > 0) {
    for (i = 0; i < N_FACES_3D; i++) {
      const int *vow = vertex_of_wall_3d[i];

      for (j = 0; j < n_f; j++) {
	int local_dof = N_VERTICES_3D + N_EDGES_3D*n_e + i*n_f + j;

	AXPBYPCZ_DOW(nodes[local_dof][vow[0]], vec[dof[vow[0]]],
		     nodes[local_dof][vow[1]], vec[dof[vow[1]]],
		     nodes[local_dof][vow[2]], vec[dof[vow[2]]],
		     vec[dof[local_dof]]);
      }
    }
  }
  
  /* center DOFs */
  for (i = 0; i < n_c; i++) {
    int local_dof = N_VERTICES_3D+N_EDGES_3D*n_e+N_FACES_3D*n_f+i;

    AXEY_DOW(nodes[local_dof][0], vec[dof[0]], vec[dof[local_dof]]);
    for (j = 1; j < N_VERTICES_3D; j++) {
      AXPY_DOW(nodes[local_dof][j], vec[dof[j]], vec[dof[local_dof]]);
    }
  }
}

/* For all-paremtric meshes we simply need to apply any projections
 * needed. Partially parametric meshes are more complicated.
 *
 * We handle here PARAM_CURVED_CHILDS as well
 */
static void all_projectY_3d(DOF_REAL_D_VEC *drdv, RC_LIST_EL *list, int n,
			    PARAMY_DATA_3D *Ydata)
{
  MESH            *mesh      = Ydata->mesh;
  const BAS_FCTS  *bas_fcts  = Ydata->bas_fcts;
  const DOF_ADMIN *admin     = Ydata->admin;
  NODE_PROJECTION *n_proj    = Ydata->n_proj;
  bool            selective  = n_proj != NULL;
  REAL_D          *vec       = Ydata->vec;
  int             n_e        = Ydata->n_e;
  int             n_f        = Ydata->n_f;
  int             n_c        = Ydata->n_c;
  const NODE_PROJECTION *act_proj;
  const REAL_B    *const*child_nodes = Ydata->child_nodes[Ydata->degree];
  const EL_INFO   *el_info;
  EL              *el, *child;
  int elem, i_child, dir, i;
  DOF dof[bas_fcts->n_bas_fcts];

  /* Just do the necessary projections */
  for (elem = 0; elem < n; elem++) {

    el_info = &list[elem].el_info;
    el = el_info->el;

    for (i_child = 0; i_child < 2; i_child++) {
      const REAL_B *cnodes;

      child = el->child[i_child];

      if (i_child == 0) {
	cnodes = child_nodes[i_child];
      } else {
	cnodes = child_nodes[i_child + (el_info->el_type != 0)];
      }
	
      /* Get the DOF-indices on the child */
      GET_DOF_INDICES(bas_fcts, child, admin, dof);

      /* Face DOFs of the child and the edge DOFs common to the two
       * children.
       */
      if (true /*n_f > 0 || n_e > 0*/) {
	
	/* <<< edge/face DOF stuff */
	for (dir = 0; dir < 2; dir++) {
	  RC_LIST_EL *neigh = list[elem].neigh[dir];
	
	  /* Do it only once for each wall; the mesh-traversal code
	   * makes sure that walls also inherit the projection from
	   * the neighbour (even if this macro-element does not
	   * define a projection for the wall).
	   */
	  if (!neigh || neigh > list+elem) {
	    int pface = 2+dir;
	    int cface;

	    /* Handle the edge between the two children */
	    if (i_child == 0) {
	      int edge = 5-dir;
	      cface = 1+dir;

	      act_proj = wall_proj(el_info, pface);
	      if (act_proj == NULL) {
		act_proj = wall_proj(el_info, -1);
	      }
	      if (act_proj && act_proj->func &&
		  (!selective || n_proj == act_proj)) {
		for (i = 0; i < n_e; i++) {
		  int ldof = N_VERTICES_3D+edge*n_e+i;
		    
		  act_proj->func(vec[dof[ldof]], el_info, cnodes[ldof]);
		  _AI_refine_update_bbox(mesh, vec[dof[ldof]]);
		}
	      }
	    } else { /* child 1 */
	      cface = el_info->el_type == 0 ? 2-dir : 1+dir;
	    }

	    /* Now handle one of the faces to the neighbour in the
	     * patch.
	     */
	    act_proj = wall_proj(el_info, pface);
	    if (act_proj == NULL) {
	      act_proj = wall_proj(el_info, -1);
	    }
	    if (act_proj && act_proj->func &&
		(!selective || n_proj == act_proj)) {
	      for (i = 0; i < n_f; i++) {
		int ldof = N_VERTICES_3D+N_EDGES_3D*n_e+cface*n_f+i;

		act_proj->func(vec[dof[ldof]], el_info, cnodes[ldof]);
		_AI_refine_update_bbox(mesh, vec[dof[ldof]]);
	      }
	    }
	  } /* !neigh || neigh > list+elem */
	} /* dir loop  */

	/* And finally do not forget to handle the face between the
	 * two children! This is always face 0. Obviously, this needs
	 * to be done only once.
	 */
	if (i_child == 0) {
	  act_proj = wall_proj(el_info, -1);
	  if (act_proj && act_proj->func &&
	      (!selective || n_proj == act_proj)) {
	    for (i = 0; i < n_f; i++) {
	      int ldof = N_VERTICES_3D+N_EDGES_3D*n_e+i;
	      
	      act_proj->func(vec[dof[ldof]], el_info, cnodes[ldof]);
	    }
	  }
	}
	/* >>> */
      }  /* true || n_f > 0 || n_e > 0 */

      /* Finally look at possible center DOFs */
      if (n_c > 0) {
	/* <<< center DOF stuff */
	act_proj = wall_proj(el_info, -1);
	if (act_proj && act_proj->func &&
	    (!selective || n_proj == act_proj)) {
	  for (i = 0; i < n_c; i++) {
	    int ldof = N_VERTICES_3D+N_EDGES_3D*n_e+N_FACES_3D*n_f+i;
	    
	    act_proj->func(vec[dof[ldof]], el_info, cnodes[ldof]);
	  }
	}
	/* >>> */
      } /* center DOFs */
    }
  }
}

/* Take care of edge_projections and affine elements for
 * PARAM_CURVED_CHILDS
 */
static void
adjust_projected_edgesY_3d(DOF_REAL_D_VEC *drdv, RC_LIST_EL *list, int n,
                           PARAMY_DATA_3D *Ydata)
{
  const BAS_FCTS  *bas_fcts  = Ydata->bas_fcts;
  const DOF_ADMIN *admin     = Ydata->admin;
  NODE_PROJECTION *n_proj    = Ydata->n_proj;
  REAL_D          *vec       = Ydata->vec;
  NODE_PROJECTION **edge_pr  = Ydata->edge_pr;
  int             node_e     = Ydata->node_e;
  int             n_e        = Ydata->n_e;
  int             n_f        = Ydata->n_f;
  int             n_c        = Ydata->n_c;
  int             n0_edge_pr = Ydata->n0_edge_pr;
  const REAL_B    *nodes     = Ydata->nodes[Ydata->degree];
  const EL_INFO   *el_info;
  EL              *el, *child;
  DOF dof[bas_fcts->n_bas_fcts];
  int elem, dir;

  for (elem = 0; elem < n; elem++) {

    el_info = &list[elem].el_info;
    child   = el_info->el->child[0];

    for (dir = 0; dir < 2; dir++) {
      RC_LIST_EL *neigh = list[elem].neigh[dir];
	
      /* Do it only once for each wall; the mesh-traversal code makes
       * sure that walls also inherit the projection from the
       * neighbour (even if this macro-element does not define a
       * projection for the wall).
       */
      if (!neigh || neigh > list+elem) {
	int pface = 2+dir;
	int edge = 5-dir;

	/* Handle the edge between the two children, refinement edge
	 * is already ok.
	 */
	if (wall_proj(el_info, -1) == n_proj ||
	    wall_proj(el_info, pface) == n_proj) {
	  edge_pr[child->dof[node_e+edge][n0_edge_pr]] = n_proj;
	} else {
	  edge_pr[child->dof[node_e+edge][n0_edge_pr]] = NULL;
	}
      }
    }
  } /* ref-patch loop */

  /* loop once again around the patch and make sure affine elements
   * are really affine.
   */
  for (elem = 0; elem < n; elem++) {
    int parent_affine = true;
    int i_child, j;

    el = list[elem].el_info.el;

    for (j = 0; j < N_EDGES_3D; j++) {
      DOF edge_dof = el->dof[node_e+j][n0_edge_pr];
	
      if (edge_pr[edge_dof]) {
	parent_affine = false;
	break;
      }
    }

    if (!parent_affine) {
      for (i_child = 0; i_child < 2; i_child++) {
	EL *child = el->child[i_child];
	int child_affine = true;
      
	for (j = 0; j < N_EDGES_3D; j++) {
	  DOF edge_dof = child->dof[node_e+j][n0_edge_pr];
	
	  if (edge_pr[edge_dof]) {
	    child_affine = false;
	    break;
	  }
	}
	if (child_affine) {
	  GET_DOF_INDICES(bas_fcts, child, admin, dof);
	  make_affineY_3d(nodes, dof, n_e, n_f, n_c, vec);
	}
      }
    }
  }

  return;
}

/* This routine is quite complicated for strategy != PARAM_ALL because
 * the center and face nodes have to be adjusted carefully to ensure
 * optimal convergence rates. Also, we try not to duplicate work and
 * compute each new coordinate only a single time. See all the helper
 * functions above ...................
 */
static void refine_interpolY_3d(DOF_REAL_D_VEC *drdv, RC_LIST_EL *list, int n)
{
  /*FUNCNAME("refine_interpolY_3d");*/
  const FE_SPACE      *fe_space = drdv->fe_space;
  MESH                *mesh = fe_space->mesh;
  const BAS_FCTS      *bas_fcts = fe_space->bas_fcts;
  const DOF_ADMIN     *admin = fe_space->admin;
  LAGRANGE_PARAM_DATA *data = (LAGRANGE_PARAM_DATA *)mesh->parametric->data;
  PARAMY_DATA_3D      Ydata;
  DOF                 child_dofs[n][2][N_BAS_MAX];
  int                 is_affine[n];
  int                 edge_no[n][2][N_EDGES_3D];
  DOF                 edge_v0[5+n*4];
  REAL_D              edge_nodes[5+n*4][DEGREE_MAX][DEGREE_MAX-1];

  /* Lagrange nodes for the children */
  Ydata.mesh       = mesh;
  Ydata.bas_fcts   = bas_fcts;
  Ydata.admin      = admin;
  Ydata.n_v        = admin->n_dof[VERTEX];
  Ydata.node_v     = mesh->node[VERTEX];
  Ydata.n0_v       = admin->n0_dof[VERTEX];
  Ydata.n_e        = admin->n_dof[EDGE];
  Ydata.node_e     = mesh->node[EDGE];
  Ydata.n0_e       = admin->n0_dof[EDGE];
  if ((Ydata.n_f = admin->n_dof[FACE])) {
    Ydata.node_f   = mesh->node[FACE];
    Ydata.n0_f     = admin->n0_dof[FACE];
    if ((Ydata.n_c = admin->n_dof[CENTER])) {
      Ydata.node_c   = mesh->node[CENTER];
      Ydata.n0_c     = admin->n0_dof[CENTER];
    }
  } else {
    Ydata.n_c = 0;
  }
  Ydata.strategy    = data->strategy;
  Ydata.degree      = bas_fcts->degree;
  Ydata.ref_edge_untouched = false;
  Ydata.n_proj      = data->n_proj;
  Ydata.child_nodes = child_nodes_3d(bas_fcts->degree, &Ydata.nodes);
  Ydata.vec         = drdv->vec;
  if (Ydata.strategy != PARAM_ALL) {
    Ydata.is_affine  = is_affine;
    Ydata.child_dofs = child_dofs;
    Ydata.edge_pr    = (NODE_PROJECTION **)data->edge_projections->vec;
    Ydata.n0_edge_pr = data->edge_projections->fe_space->admin->n0_dof[EDGE];
  } else {
    Ydata.edge_pr    = NULL;
    Ydata.n0_edge_pr = -1;
  }

  if (Ydata.strategy == PARAM_STRAIGHT_CHILDS) {

    Ydata.n_edges    = 0;
    Ydata.edge_v0    = edge_v0;
    Ydata.edge_no    = edge_no;
    Ydata.edge_nodes = edge_nodes;
    
    /* Fill the basis function cache */
    if (Ydata.n_f > 0) {
      Ydata.wall_edge_cache = face_edge_valuesY_3d(Ydata.degree);
    }
    if (Ydata.n_c > 0) {
      Ydata.center_wall_cache = center_wall_valuesY_3d(Ydata.degree);
      Ydata.center_edge_cache = center_edge_valuesY_3d(Ydata.degree);
    }
  }

  /* If strategy != PARAM_STRAIGHT_CHILDS first call the default
   * interpolation routine for the coordinates. Why should we
   * duplicate work. Otherwise we interpolate the vertices linearly
   * according to the Lagrange-nodes.
   */
  if (Ydata.strategy != PARAM_STRAIGHT_CHILDS) {
    bas_fcts->real_d_refine_inter(drdv, list, n);
  }
  
  /* We first handle the refinement edge: it might be that only some
   * elements of the patch define projections which apply to the
   * refinement edge. But for PARAM_STRAIGHT_CHILDS we need (at least)
   * the position of the mid-point of the refinement edge to generate
   * the correct coordinates.
   */
  project_ref_edgeY_3d(drdv, list, n, &Ydata);

  switch (Ydata.strategy) {
  case PARAM_STRAIGHT_CHILDS:
    /* First generate the new walls and eges, then loop a second time
     * over the patch and generate the coordinates for the center
     * DOFs. The new face between the children is handled in
     * adjust_center_nodes(). adjust_center_nodes() also takes care
     * that non-parametric coordinates are really affine.
     */
    if (Ydata.n_e > 0 || Ydata.n_f > 0) {
      adjust_patch_wall_nodesY_3d(drdv, list, n, &Ydata);
    }
    if (Ydata.n_f > 0|| Ydata.n_c > 0) {
      adjust_patch_center_nodesY_3d(drdv, list, n, &Ydata);
    }
    break;
  case PARAM_CURVED_CHILDS:
    /* First do all necessary projections */
    if (Ydata.n_proj == NULL || Ydata.n_proj->func) {
      all_projectY_3d(drdv, list, n, &Ydata);
    }
    /* Then promote the edge_projections information, also make sure
     * un-projected elements are really affine.
     */
    adjust_projected_edgesY_3d(drdv, list, n, &Ydata);
    break;
  case PARAM_ALL:
    /* The easy part, everything is parametric. */
    if (Ydata.n_proj == NULL || Ydata.n_proj->func) {
      all_projectY_3d(drdv, list, n, &Ydata);
    }
  }

  return;
}

static void coarse_interpolY_3d(DOF_REAL_D_VEC *drdv, RC_LIST_EL *list, int n)
{
  /* FUNCNAME("coarse_interpolY_3d"); */
  const FE_SPACE  *fe_space = drdv->fe_space;
  MESH            *mesh     = fe_space->mesh;
  const BAS_FCTS  *bas_fcts = fe_space->bas_fcts;
  LAGRANGE_PARAM_DATA *data = (LAGRANGE_PARAM_DATA *)mesh->parametric->data;
  DOF_PTR_VEC     *edge_pr  = data->edge_projections;
  int             node_e;
  int             n0_edge_pr;
  DOF             cdof, pdof;

  /* For the coordinates, just call the default coarsening
   * interpolation routine
   */
  bas_fcts->real_d_coarse_inter(drdv, list, n);
  
  if (data->strategy == PARAM_ALL)
    return; /* nothing more to do */
  
  /* we need to restore the "projected" status of the refinement edge */
  node_e     = mesh->node[EDGE]; 
  n0_edge_pr = edge_pr->fe_space->admin->n0_dof[EDGE];
  pdof       = list->el_info.el->dof[node_e+0][n0_edge_pr];
  cdof       = list->el_info.el->child[0]->dof[node_e+2][n0_edge_pr];
  edge_pr->vec[pdof] = edge_pr->vec[cdof];

  /* BIG FAT TODO: we also need to re-adjust the positions of face
   * nodes and interior nodes of all elements in the refinement
   * patch for PARAM_STRAIGHT_CHILDS.
   */

  return;
}

static void fill_coordsY_3d(LAGRANGE_PARAM_DATA *data)
{
  FUNCNAME("fill_coordsY_3d");
  DOF_REAL_D_VEC  *coords   = data->coords;
  DOF_PTR_VEC     *edge_pr  = data->edge_projections;
  NODE_PROJECTION *n_proj   = data->n_proj;
  bool            selective = n_proj != NULL;
  const NODE_PROJECTION *act_proj;
  FLAGS           fill_flag = CALL_LEAF_EL|FILL_COORDS|FILL_PROJECTION;
  const FE_SPACE  *fe_space = coords->fe_space;
  MESH            *mesh = fe_space->mesh;
  const DOF_ADMIN *admin = fe_space->admin;
  const BAS_FCTS  *bas_fcts = fe_space->bas_fcts;
  const REAL_B    *nodes = LAGRANGE_NODES(bas_fcts);
  DOF             dof[bas_fcts->n_bas_fcts];
  int             i, j, k, n_e, n_f, n_c;
  int             node_e = -1, n0_edge_pr = -1;

  DEBUG_TEST_EXIT(bas_fcts->degree <= DEGREE_MAX,
		  "degree %d unsupported, only up to degree %d.\n",
		  bas_fcts->degree, DEGREE_MAX);
  
  n_e      = admin->n_dof[EDGE];
  n_f      = admin->n_dof[FACE];
  n_c      = admin->n_dof[CENTER];

  if (edge_pr) {
    node_e     = mesh->node[EDGE];
    n0_edge_pr = edge_pr->fe_space->admin->n0_dof[EDGE];
  }

  TRAVERSE_FIRST(mesh, -1, fill_flag) {
    void *edge_pr_loc[N_EDGES_3D];
    DOF  edge_pr_dof[N_EDGES_3D];
    REAL *vec, *vec0, *vec1;
    
    GET_DOF_INDICES(bas_fcts, el_info->el, admin, dof);

    if (edge_pr) {
      for (i = 0; i < N_EDGES_3D; i++) {
	edge_pr_dof[i] = el_info->el->dof[node_e+i][n0_edge_pr];
	edge_pr_loc[i] = edge_pr->vec[edge_pr_dof[i]];
      }
    }

    for (i = 0; i < N_VERTICES_3D; i++) {

      if (edge_pr) {
	for (j = 0; j < 3; j++) {
	  if (edge_pr_loc[edge_of_vertex_3d[i][j]] != NULL) {
	    break;
	  }
	}
	if (j < 3) {
	  continue; /* no need to call the projections twice. */
	}
      }
	  
      vec = coords->vec[dof[i]];

      COPY_DOW(el_info->coord[i], vec);

      /* Look for a projection function that applies to vertex[i].
       * Apply this projection if found.
       */
      if (!selective || n_proj->func) {
	act_proj = wall_proj(el_info, (i+1) % N_WALLS_3D);
	if (!act_proj) {
	  act_proj = wall_proj(el_info, (i+2) % N_WALLS_3D);
	}
	if (!act_proj) {
	  act_proj = wall_proj(el_info, (i+3) % N_WALLS_3D);
	}
	if (!act_proj) {
	  act_proj = wall_proj(el_info, -1);
	}
	if (act_proj && act_proj->func && (!selective || act_proj == n_proj)) {
	  act_proj->func(vec, el_info, bary2_3d[i]);
	}
      }
    }

    if (true || n_e) {

      for (i = 0; i < N_EDGES_3D; i++) {
	const int *voe;

	if (edge_pr && edge_pr_loc[i]) {
	  /* coordinates of edges MUST NOT be set twice: there might
	   * be elements with no projections attached to their walls,
	   * but which are curved nevertheless; edges are shared by
	   * elements which are not neighbours.
	   */
	  continue;
	}

	voe = vertex_of_edge_3d[i];

	for (j = 0; j < n_e; j++) {
	  int local_dof = N_VERTICES_3D + i*n_e + j;

	  vec = coords->vec[dof[local_dof]];
	  vec0 = coords->vec[dof[voe[0]]];
	  vec1 = coords->vec[dof[voe[1]]];

	  AXPBY_DOW(nodes[local_dof][voe[0]], vec0,
		    nodes[local_dof][voe[1]], vec1,
		    vec);
	}

	/* Look for a projection function that applies to edge[i]. */
	act_proj = wall_proj(el_info, face_of_edge[i][0]);
	if (!act_proj) {
	  act_proj = wall_proj(el_info, face_of_edge[i][1]);
	}
	if (!act_proj) {
	  act_proj = wall_proj(el_info, -1);
	}
	if (act_proj && (!selective || act_proj == n_proj)) {
	  if (act_proj->func) {
	    for (j = 0; j < n_e; j++) {
	      int local_dof = N_VERTICES_3D + i*n_e + j;

	      vec = coords->vec[dof[local_dof]];
	      act_proj->func(vec, el_info, nodes[local_dof]);
	    }
	  }

	  if (edge_pr) {
	    edge_pr->vec[edge_pr_dof[i]] = (void *)act_proj;
	  }
	}
      }
    }

    if (n_f) {
      /* Face coordinates are computed on each element, although this
       * duplicates work.
       */
      for (i = 0; i < N_FACES_3D; i++) {
	int v0 = vertex_of_wall_3d[i][0];
	int v1 = vertex_of_wall_3d[i][1];
	int v2 = vertex_of_wall_3d[i][2];
	
	for (j = 0; j < n_f; j++) {
	  int local_dof = N_VERTICES_3D + N_EDGES_3D*n_e + i*n_f + j;

	  vec = coords->vec[dof[local_dof]];
	  AXPBYPCZ_DOW(nodes[local_dof][v0], coords->vec[dof[v0]],
		       nodes[local_dof][v1], coords->vec[dof[v1]],
		       nodes[local_dof][v2], coords->vec[dof[v2]],
		       vec);
	}
	
	/* Look for a projection function that applies to face[i]. */
	if (!selective || n_proj->func) {
	  act_proj = wall_proj(el_info, i);
	  if (!act_proj) {
	    act_proj = wall_proj(el_info, -1);
	  }

	  if (act_proj && act_proj->func &&
	      (!selective || n_proj == act_proj)) {
	    for (j = 0; j < n_f; j++) {
	      int local_dof = N_VERTICES_3D + N_EDGES_3D*n_e + i*n_f + j;
	      vec = coords->vec[dof[local_dof]];
	      act_proj->func(vec, el_info, nodes[local_dof]);
	    }
	  }
	}
      }
    }

    if (n_c) {
      for (j = 0; j < n_c; j++) {
	int local_dof = N_VERTICES_3D + N_EDGES_3D*n_e + N_FACES_3D*n_f + j;

	vec = coords->vec[dof[local_dof]];
	AXEY_DOW(nodes[local_dof][0], coords->vec[dof[0]], vec);
	for (k = 1; k < N_VERTICES_3D; k++) {
	  AXPY_DOW(nodes[local_dof][k], coords->vec[dof[k]], vec);
	}
      }

      act_proj = wall_proj(el_info, -1);
      if (act_proj && act_proj->func && (!selective || n_proj == act_proj)) {
	for (j = 0; j < n_c; j++) {
	  int local_dof = N_VERTICES_3D + N_EDGES_3D*n_e + N_FACES_3D*n_f + j;

	  vec = coords->vec[dof[local_dof]];
	  act_proj->func(vec, el_info, nodes[local_dof]);
	}
      }
    }

#if 0
    /* TODO: adjust center and face DOFs, i.e. move them towards the
     * curved facets for PARAM_STRAIGHT_CHILDS.
     */
#endif
  } TRAVERSE_NEXT();
}

static PARAMETRIC lagrange_parametric1_3d = {
  "3D Lagrange parametric elements of degree 1",
  false,  /* not_all */
  false,  /* use_reference_mesh */
  param_init_element1_3d,
  vertex_coordsY_3d,
  param_coord_to_world,
  param_world_to_coord,
  det1_3d,
  grd_lambda1_3d,
  grd_world1_3d,
  wall_normal1_3d,
  NULL       /* data */
};

static PARAMETRIC lagrange_parametric2_3d = {
  "3D Lagrange parametric elements of degree 2",
  false,  /* not_all */
  false,  /* use_reference_mesh */
  param_init_elementY_3d,
  vertex_coordsY_3d,
  param_coord_to_world,
  param_world_to_coord,
  detY_3d,
  grd_lambdaY_3d,
  grd_worldY_3d,
  wall_normalY_3d,
  NULL       /* data */
};

static PARAMETRIC lagrange_parametricY_3d = {
  "3D Lagrange parametric elements of generic degree",
  false,  /* not_all */
  false,  /* use_reference_mesh */
  param_init_elementY_3d,
  vertex_coordsY_3d,
  param_coord_to_world,
  param_world_to_coord,
  detY_3d,
  grd_lambdaY_3d,
  grd_worldY_3d,
  wall_normalY_3d,
  NULL       /* data */
};
