Mesh: Move sculpt face sets to a generic attribute

Similar to the other refactors from T95965, this commit moves sculpt
face sets to use a generic integer attribute named `".sculpt_face_set"`.
This makes face sets accessible in the Python API.

The attribute is not visible in the attributes list or the spreadsheet
because it is meant for internal use, though that could be an option
in the future along with other similar attributes. Currently the change
is small, but in the future this could simplify code by allowing use
of more generic attribute APIs.

Differential Revision: https://developer.blender.org/D16045
This commit is contained in:
Hans Goudey
2022-09-23 08:19:40 -05:00
parent 35375380d7
commit 060a534141
18 changed files with 110 additions and 64 deletions

View File

@@ -17,6 +17,15 @@ struct CustomData;
struct Mesh;
struct MFace;
/**
* Move face sets to the legacy type from a generic type.
*/
void BKE_mesh_legacy_face_set_from_generic(struct Mesh *mesh);
/**
* Copy face sets to the generic data type from the legacy type.
*/
void BKE_mesh_legacy_face_set_to_generic(struct Mesh *mesh);
/**
* Copy bevel weights from separate layers into vertices and edges.
*/

View File

@@ -1967,7 +1967,7 @@ static const LayerTypeInfo LAYERTYPEINFO[CD_NUMTYPES] = {
{sizeof(short[4][3]), "", 0, nullptr, nullptr, nullptr, nullptr, layerSwap_flnor, nullptr},
/* 41: CD_CUSTOMLOOPNORMAL */
{sizeof(short[2]), "vec2s", 1, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
/* 42: CD_SCULPT_FACE_SETS */
/* 42: CD_SCULPT_FACE_SETS */ /* DEPRECATED */
{sizeof(int), "", 0, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
/* 43: CD_LOCATION */
{sizeof(float[3]), "vec3f", 1, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
@@ -2127,8 +2127,7 @@ const CustomData_MeshMasks CD_MASK_MESH = {
/* emask */ (CD_MASK_MEDGE | CD_MASK_FREESTYLE_EDGE | CD_MASK_PROP_ALL | CD_MASK_BWEIGHT),
/* fmask */ 0,
/* pmask */
(CD_MASK_MPOLY | CD_MASK_FACEMAP | CD_MASK_FREESTYLE_FACE | CD_MASK_PROP_ALL |
CD_MASK_SCULPT_FACE_SETS),
(CD_MASK_MPOLY | CD_MASK_FACEMAP | CD_MASK_FREESTYLE_FACE | CD_MASK_PROP_ALL),
/* lmask */
(CD_MASK_MLOOP | CD_MASK_MDISPS | CD_MASK_MLOOPUV | CD_MASK_CUSTOMLOOPNORMAL |
CD_MASK_GRID_PAINT_MASK | CD_MASK_PROP_ALL),
@@ -2140,8 +2139,7 @@ const CustomData_MeshMasks CD_MASK_DERIVEDMESH = {
/* emask */ (CD_MASK_ORIGINDEX | CD_MASK_FREESTYLE_EDGE | CD_MASK_BWEIGHT | CD_MASK_PROP_ALL),
/* fmask */ (CD_MASK_ORIGINDEX | CD_MASK_ORIGSPACE | CD_MASK_PREVIEW_MCOL | CD_MASK_TANGENT),
/* pmask */
(CD_MASK_ORIGINDEX | CD_MASK_FREESTYLE_FACE | CD_MASK_FACEMAP | CD_MASK_PROP_ALL |
CD_MASK_SCULPT_FACE_SETS),
(CD_MASK_ORIGINDEX | CD_MASK_FREESTYLE_FACE | CD_MASK_FACEMAP | CD_MASK_PROP_ALL),
/* lmask */
(CD_MASK_MLOOPUV | CD_MASK_CUSTOMLOOPNORMAL | CD_MASK_PREVIEW_MLOOPCOL |
CD_MASK_ORIGSPACE_MLOOP | CD_MASK_PROP_ALL), /* XXX MISSING CD_MASK_MLOOPTANGENT ? */
@@ -2152,7 +2150,7 @@ const CustomData_MeshMasks CD_MASK_BMESH = {
/* emask */ (CD_MASK_BWEIGHT | CD_MASK_CREASE | CD_MASK_FREESTYLE_EDGE | CD_MASK_PROP_ALL),
/* fmask */ 0,
/* pmask */
(CD_MASK_FREESTYLE_FACE | CD_MASK_FACEMAP | CD_MASK_PROP_ALL | CD_MASK_SCULPT_FACE_SETS),
(CD_MASK_FREESTYLE_FACE | CD_MASK_FACEMAP | CD_MASK_PROP_ALL),
/* lmask */
(CD_MASK_MDISPS | CD_MASK_MLOOPUV | CD_MASK_CUSTOMLOOPNORMAL | CD_MASK_GRID_PAINT_MASK |
CD_MASK_PROP_ALL),
@@ -2171,7 +2169,7 @@ const CustomData_MeshMasks CD_MASK_EVERYTHING = {
CD_MASK_PROP_ALL),
/* pmask */
(CD_MASK_MPOLY | CD_MASK_BM_ELEM_PYPTR | CD_MASK_ORIGINDEX | CD_MASK_FACEMAP |
CD_MASK_FREESTYLE_FACE | CD_MASK_PROP_ALL | CD_MASK_SCULPT_FACE_SETS),
CD_MASK_FREESTYLE_FACE | CD_MASK_PROP_ALL),
/* lmask */
(CD_MASK_MLOOP | CD_MASK_BM_ELEM_PYPTR | CD_MASK_MDISPS | CD_MASK_NORMAL | CD_MASK_MLOOPUV |
CD_MASK_CUSTOMLOOPNORMAL | CD_MASK_MLOOPTANGENT | CD_MASK_PREVIEW_MLOOPCOL |

View File

@@ -252,6 +252,7 @@ static void mesh_blend_write(BlendWriter *writer, ID *id, const void *id_address
BKE_mesh_legacy_convert_hide_layers_to_flags(mesh);
BKE_mesh_legacy_convert_material_indices_to_mpoly(mesh);
BKE_mesh_legacy_bevel_weight_from_layers(mesh);
BKE_mesh_legacy_face_set_from_generic(mesh);
/* When converting to the old mesh format, don't save redundant attributes. */
names_to_skip.add_multiple_new({".hide_vert", ".hide_edge", ".hide_poly", "material_index"});

View File

@@ -917,6 +917,33 @@ void BKE_mesh_add_mface_layers(CustomData *fdata, CustomData *ldata, int total)
/** \} */
/* -------------------------------------------------------------------- */
/** \name Face Set Conversion
* \{ */
void BKE_mesh_legacy_face_set_from_generic(Mesh *mesh)
{
using namespace blender;
for (CustomDataLayer &layer : MutableSpan(mesh->pdata.layers, mesh->pdata.totlayer)) {
if (StringRef(layer.name) == ".sculpt_face_set") {
layer.type = CD_SCULPT_FACE_SETS;
}
}
}
void BKE_mesh_legacy_face_set_to_generic(Mesh *mesh)
{
using namespace blender;
for (CustomDataLayer &layer : MutableSpan(mesh->pdata.layers, mesh->pdata.totlayer)) {
if (layer.type == CD_SCULPT_FACE_SETS) {
BLI_strncpy(layer.name, ".sculpt_face_set", sizeof(layer.name));
layer.type = CD_PROP_INT32;
}
}
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Bevel Weight Conversion
* \{ */

View File

@@ -24,6 +24,7 @@
#include "DNA_meshdata_types.h"
#include "BKE_attribute.h"
#include "BKE_attribute.hh"
#include "BKE_bvhutils.h"
#include "BKE_customdata.h"
#include "BKE_editmesh.h"
@@ -318,23 +319,27 @@ void BKE_mesh_remesh_reproject_paint_mask(Mesh *target, const Mesh *source)
void BKE_remesh_reproject_sculpt_face_sets(Mesh *target, const Mesh *source)
{
using namespace blender;
using namespace blender::bke;
const AttributeAccessor src_attributes = source->attributes();
MutableAttributeAccessor dst_attributes = target->attributes_for_write();
const MPoly *target_polys = (const MPoly *)CustomData_get_layer(&target->pdata, CD_MPOLY);
const MVert *target_verts = (const MVert *)CustomData_get_layer(&target->vdata, CD_MVERT);
const MLoop *target_loops = (const MLoop *)CustomData_get_layer(&target->ldata, CD_MLOOP);
const int *source_face_sets = (const int *)CustomData_get_layer(&source->pdata,
CD_SCULPT_FACE_SETS);
if (source_face_sets == nullptr) {
const VArray<int> src_face_sets = src_attributes.lookup<int>(".sculpt_face_set",
ATTR_DOMAIN_FACE);
if (!src_face_sets) {
return;
}
SpanAttributeWriter<int> dst_face_sets = dst_attributes.lookup_or_add_for_write_only_span<int>(
".sculpt_face_set", ATTR_DOMAIN_FACE);
if (!dst_face_sets) {
return;
}
int *target_face_sets;
if (CustomData_has_layer(&target->pdata, CD_SCULPT_FACE_SETS)) {
target_face_sets = (int *)CustomData_get_layer(&target->pdata, CD_SCULPT_FACE_SETS);
}
else {
target_face_sets = (int *)CustomData_add_layer(
&target->pdata, CD_SCULPT_FACE_SETS, CD_CONSTRUCT, nullptr, target->totpoly);
}
const VArraySpan<int> src(src_face_sets);
MutableSpan<int> dst = dst_face_sets.span;
const MLoopTri *looptri = BKE_mesh_runtime_looptri_ensure(source);
BVHTreeFromMesh bvhtree = {nullptr};
@@ -349,13 +354,14 @@ void BKE_remesh_reproject_sculpt_face_sets(Mesh *target, const Mesh *source)
BKE_mesh_calc_poly_center(mpoly, &target_loops[mpoly->loopstart], target_verts, from_co);
BLI_bvhtree_find_nearest(bvhtree.tree, from_co, &nearest, bvhtree.nearest_callback, &bvhtree);
if (nearest.index != -1) {
target_face_sets[i] = source_face_sets[looptri[nearest.index].poly];
dst[i] = src[looptri[nearest.index].poly];
}
else {
target_face_sets[i] = 1;
dst[i] = 1;
}
}
free_bvhtree_from_mesh(&bvhtree);
dst_face_sets.finish();
}
void BKE_remesh_reproject_vertex_paint(Mesh *target, const Mesh *source)

View File

@@ -1739,7 +1739,8 @@ static void sculpt_update_object(
/* Sculpt Face Sets. */
if (use_face_sets) {
ss->face_sets = static_cast<int *>(CustomData_get_layer(&me->pdata, CD_SCULPT_FACE_SETS));
ss->face_sets = static_cast<int *>(
CustomData_get_layer_named(&me->pdata, CD_PROP_INT32, ".sculpt_face_set"));
}
else {
ss->face_sets = nullptr;
@@ -1966,22 +1967,17 @@ int *BKE_sculpt_face_sets_ensure(Mesh *mesh)
{
using namespace blender;
using namespace blender::bke;
if (CustomData_has_layer(&mesh->pdata, CD_SCULPT_FACE_SETS)) {
return static_cast<int *>(CustomData_get_layer(&mesh->pdata, CD_SCULPT_FACE_SETS));
MutableAttributeAccessor attributes = mesh->attributes_for_write();
if (!attributes.contains(".sculpt_face_set")) {
SpanAttributeWriter<int> face_sets = attributes.lookup_or_add_for_write_only_span<int>(
".sculpt_face_set", ATTR_DOMAIN_FACE);
face_sets.span.fill(1);
mesh->face_sets_color_default = 1;
face_sets.finish();
}
const AttributeAccessor attributes = mesh->attributes_for_write();
const VArray<bool> hide_poly = attributes.lookup_or_default<bool>(
".hide_poly", ATTR_DOMAIN_FACE, false);
MutableSpan<int> face_sets = {
static_cast<int *>(CustomData_add_layer(
&mesh->pdata, CD_SCULPT_FACE_SETS, CD_CONSTRUCT, nullptr, mesh->totpoly)),
mesh->totpoly};
face_sets.fill(1);
mesh->face_sets_color_default = 1;
return face_sets.data();
return static_cast<int *>(
CustomData_get_layer_named(&mesh->pdata, CD_PROP_INT32, ".sculpt_face_set"));
}
bool *BKE_sculpt_hide_poly_ensure(Mesh *mesh)

View File

@@ -1326,16 +1326,17 @@ static void pbvh_update_draw_buffer_cb(void *__restrict userdata,
case PBVH_FACES: {
/* Pass vertices separately because they may be not be the same as the mesh's vertices,
* and pass normals separately because they are managed by the PBVH. */
GPU_pbvh_mesh_buffers_update(pbvh->vbo_id,
node->draw_buffers,
pbvh->mesh,
pbvh->verts,
CustomData_get_layer(pbvh->vdata, CD_PAINT_MASK),
CustomData_get_layer(pbvh->pdata, CD_SCULPT_FACE_SETS),
pbvh->face_sets_color_seed,
pbvh->face_sets_color_default,
update_flags,
pbvh->vert_normals);
GPU_pbvh_mesh_buffers_update(
pbvh->vbo_id,
node->draw_buffers,
pbvh->mesh,
pbvh->verts,
CustomData_get_layer(pbvh->vdata, CD_PAINT_MASK),
CustomData_get_layer_named(pbvh->pdata, CD_PROP_INT32, ".sculpt_face_set"),
pbvh->face_sets_color_seed,
pbvh->face_sets_color_default,
update_flags,
pbvh->vert_normals);
break;
}
case PBVH_BMESH:
@@ -3135,9 +3136,9 @@ bool pbvh_has_face_sets(PBVH *pbvh)
{
switch (pbvh->header.type) {
case PBVH_GRIDS:
return (pbvh->pdata && CustomData_get_layer(pbvh->pdata, CD_SCULPT_FACE_SETS));
case PBVH_FACES:
return (pbvh->pdata && CustomData_get_layer(pbvh->pdata, CD_SCULPT_FACE_SETS));
return pbvh->pdata &&
CustomData_get_layer_named(pbvh->pdata, CD_PROP_INT32, ".sculpt_face_set") != NULL;
case PBVH_BMESH:
return false;
}

View File

@@ -24,6 +24,7 @@ static void version_mesh_legacy_to_struct_of_array_format(Mesh &mesh)
BKE_mesh_legacy_convert_flags_to_hide_layers(&mesh);
BKE_mesh_legacy_convert_mpoly_to_material_indices(&mesh);
BKE_mesh_legacy_bevel_weight_to_layers(&mesh);
BKE_mesh_legacy_face_set_to_generic(&mesh);
}
void blo_do_versions_400(FileData * /*fd*/, Library * /*lib*/, Main *bmain)

View File

@@ -288,7 +288,8 @@ static void bm_log_verts_restore(BMesh *bm, BMLog *log, GHash *verts)
static void bm_log_faces_restore(BMesh *bm, BMLog *log, GHash *faces)
{
GHashIterator gh_iter;
const int cd_face_sets = CustomData_get_offset(&bm->pdata, CD_SCULPT_FACE_SETS);
const int cd_face_sets = CustomData_get_offset_named(
&bm->pdata, CD_PROP_INT32, ".sculpt_face_set");
GHASH_ITER (gh_iter, faces) {
void *key = BLI_ghashIterator_getKey(&gh_iter);

View File

@@ -44,7 +44,8 @@ static void extract_sculpt_data_init(const MeshRenderData *mr,
CustomData *cd_pdata = (mr->extract_type == MR_EXTRACT_BMESH) ? &mr->bm->pdata : &mr->me->pdata;
const float *cd_mask = (const float *)CustomData_get_layer(cd_vdata, CD_PAINT_MASK);
const int *cd_face_set = (const int *)CustomData_get_layer(cd_pdata, CD_SCULPT_FACE_SETS);
const int *cd_face_set = (const int *)CustomData_get_layer_named(
cd_pdata, CD_PROP_INT32, ".sculpt_face_set");
GPU_vertbuf_init_with_format(vbo, format);
GPU_vertbuf_data_alloc(vbo, mr->loop_len);
@@ -59,7 +60,7 @@ static void extract_sculpt_data_init(const MeshRenderData *mr,
if (mr->extract_type == MR_EXTRACT_BMESH) {
int cd_mask_ofs = CustomData_get_offset(cd_vdata, CD_PAINT_MASK);
int cd_face_set_ofs = CustomData_get_offset(cd_pdata, CD_SCULPT_FACE_SETS);
int cd_face_set_ofs = CustomData_get_offset_named(cd_pdata, CD_PROP_INT32, ".sculpt_face_set");
BMIter f_iter;
BMFace *efa;
BM_ITER_MESH (efa, &f_iter, mr->bm, BM_FACES_OF_MESH) {
@@ -171,7 +172,8 @@ static void extract_sculpt_data_init_subdiv(const DRWSubdivCache *subdiv_cache,
};
gpuFaceSet *face_sets = (gpuFaceSet *)GPU_vertbuf_get_data(face_set_vbo);
const int *cd_face_set = (const int *)CustomData_get_layer(cd_pdata, CD_SCULPT_FACE_SETS);
const int *cd_face_set = (const int *)CustomData_get_layer_named(
cd_pdata, CD_PROP_INT32, ".sculpt_face_set");
GPUVertFormat *format = get_sculpt_data_format();
GPU_vertbuf_init_build_on_device(vbo, format, subdiv_cache->num_subdiv_loops);

View File

@@ -209,7 +209,7 @@ static int geometry_extract_apply(bContext *C,
/* Remove the Face Sets as they need to be recreated when entering Sculpt Mode in the new object.
* TODO(pablodobarro): In the future we can try to preserve them from the original mesh. */
Mesh *new_ob_mesh = new_ob->data;
CustomData_free_layers(&new_ob_mesh->pdata, CD_SCULPT_FACE_SETS, new_ob_mesh->totpoly);
CustomData_free_layer_named(&new_ob_mesh->pdata, ".sculpt_face_set", new_ob_mesh->totpoly);
/* Remove the mask from the new object so it can be sculpted directly after extracting. */
CustomData_free_layers(&new_ob_mesh->vdata, CD_PAINT_MASK, new_ob_mesh->totvert);
@@ -268,7 +268,8 @@ static void geometry_extract_tag_face_set(BMesh *bm, GeometryExtractParams *para
const int tag_face_set_id = params->active_face_set;
BM_mesh_elem_hflag_disable_all(bm, BM_VERT | BM_EDGE | BM_FACE, BM_ELEM_TAG, false);
const int cd_face_sets_offset = CustomData_get_offset(&bm->pdata, CD_SCULPT_FACE_SETS);
const int cd_face_sets_offset = CustomData_get_offset_named(
&bm->pdata, CD_PROP_INT32, ".sculpt_face_set");
BMFace *f;
BMIter iter;
@@ -561,7 +562,8 @@ static int paint_mask_slice_exec(bContext *C, wmOperator *op)
if (ob->mode == OB_MODE_SCULPT) {
SculptSession *ss = ob->sculpt;
ss->face_sets = CustomData_get_layer(&((Mesh *)ob->data)->pdata, CD_SCULPT_FACE_SETS);
ss->face_sets = CustomData_get_layer_named(
&((Mesh *)ob->data)->pdata, CD_PROP_INT32, ".sculpt_face_set");
if (ss->face_sets) {
/* Assign a new Face Set ID to the new faces created by the slice operation. */
const int next_face_set_id = ED_sculpt_face_sets_find_next_available_id(ob->data);

View File

@@ -308,7 +308,8 @@ static void mesh_join_offset_face_sets_ID(const Mesh *mesh, int *face_set_offset
return;
}
int *face_sets = (int *)CustomData_get_layer(&mesh->pdata, CD_SCULPT_FACE_SETS);
int *face_sets = (int *)CustomData_get_layer_named(
&mesh->pdata, CD_PROP_INT32, ".sculpt_face_set");
if (!face_sets) {
return;
}

View File

@@ -1354,7 +1354,9 @@ static void sculpt_gesture_trim_end(bContext *UNUSED(C), SculptGestureContext *s
{
Object *object = sgcontext->vc.obact;
SculptSession *ss = object->sculpt;
ss->face_sets = CustomData_get_layer(&((Mesh *)object->data)->pdata, CD_SCULPT_FACE_SETS);
ss->face_sets = CustomData_get_layer_named(
&((Mesh *)object->data)->pdata, CD_PROP_INT32, ".sculpt_face_set");
if (ss->face_sets) {
/* Assign a new Face Set ID to the new faces created by the trim operation. */
const int next_face_set_id = ED_sculpt_face_sets_find_next_available_id(object->data);

View File

@@ -180,7 +180,7 @@ static void SCULPT_dynamic_topology_disable_ex(
BKE_sculptsession_bm_to_me(ob, true);
/* Reset Face Sets as they are no longer valid. */
CustomData_free_layers(&me->pdata, CD_SCULPT_FACE_SETS, me->totpoly);
CustomData_free_layer_named(&me->pdata, ".sculpt_face_set", me->totpoly);
me->face_sets_color_default = 1;
/* Sync the visibility to vertices manually as the pmap is still not initialized. */

View File

@@ -66,7 +66,7 @@
int ED_sculpt_face_sets_find_next_available_id(struct Mesh *mesh)
{
const int *face_sets = static_cast<const int *>(
CustomData_get_layer(&mesh->pdata, CD_SCULPT_FACE_SETS));
CustomData_get_layer_named(&mesh->pdata, CD_PROP_INT32, ".sculpt_face_set"));
if (!face_sets) {
return SCULPT_FACE_SET_NONE;
}
@@ -82,7 +82,8 @@ int ED_sculpt_face_sets_find_next_available_id(struct Mesh *mesh)
void ED_sculpt_face_sets_initialize_none_to_id(struct Mesh *mesh, const int new_id)
{
int *face_sets = static_cast<int *>(CustomData_get_layer(&mesh->pdata, CD_SCULPT_FACE_SETS));
int *face_sets = static_cast<int *>(
CustomData_get_layer_named(&mesh->pdata, CD_PROP_INT32, ".sculpt_face_set"));
if (!face_sets) {
return;
}

View File

@@ -486,8 +486,8 @@ static bool sculpt_undo_restore_face_sets(bContext *C, SculptUndoNode *unode)
BKE_view_layer_synced_ensure(scene, view_layer);
Object *ob = BKE_view_layer_active_object_get(view_layer);
Mesh *me = BKE_object_get_original_mesh(ob);
int *face_sets = CustomData_add_layer(
&me->pdata, CD_SCULPT_FACE_SETS, CD_CONSTRUCT, NULL, me->totpoly);
int *face_sets = CustomData_add_layer_named(
&me->pdata, CD_PROP_INT32, CD_CONSTRUCT, NULL, me->totpoly, ".sculpt_face_set");
for (int i = 0; i < me->totpoly; i++) {
SWAP(int, face_sets[i], unode->face_sets[i]);
}
@@ -1364,7 +1364,7 @@ static SculptUndoNode *sculpt_undo_face_sets_push(Object *ob, SculptUndoType typ
unode->face_sets = MEM_callocN(me->totpoly * sizeof(int), "sculpt face sets");
const int *face_sets = CustomData_get_layer(&me->pdata, CD_SCULPT_FACE_SETS);
const int *face_sets = CustomData_get_layer_named(&me->pdata, CD_PROP_INT32, ".sculpt_face_set");
if (face_sets) {
for (int i = 0; i < me->totpoly; i++) {
unode->face_sets[i] = face_sets[i];

View File

@@ -2405,7 +2405,6 @@ void ED_view3d_datamask(const bContext *C,
}
if ((CTX_data_mode_enum(C) == CTX_MODE_SCULPT)) {
r_cddata_masks->vmask |= CD_MASK_PAINT_MASK;
r_cddata_masks->pmask |= CD_MASK_SCULPT_FACE_SETS;
}
}

View File

@@ -206,7 +206,6 @@ typedef enum eCustomDataType {
#define CD_MASK_MLOOPTANGENT (1LL << CD_MLOOPTANGENT)
#define CD_MASK_TESSLOOPNORMAL (1LL << CD_TESSLOOPNORMAL)
#define CD_MASK_CUSTOMLOOPNORMAL (1LL << CD_CUSTOMLOOPNORMAL)
#define CD_MASK_SCULPT_FACE_SETS (1LL << CD_SCULPT_FACE_SETS)
#define CD_MASK_PROP_COLOR (1ULL << CD_PROP_COLOR)
#define CD_MASK_PROP_FLOAT3 (1ULL << CD_PROP_FLOAT3)
#define CD_MASK_PROP_FLOAT2 (1ULL << CD_PROP_FLOAT2)