8b65764fe1c2ad09295b497739908bb8f920fd61
[blender.git] / source / blender / bmesh / operators / bmo_removedoubles.c
1 /*
2  * ***** BEGIN GPL LICENSE BLOCK *****
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public License
6  * as published by the Free Software Foundation; either version 2
7  * of the License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software Foundation,
16  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17  *
18  * Contributor(s): Joseph Eagar.
19  *
20  * ***** END GPL LICENSE BLOCK *****
21  */
22
23 /** \file blender/bmesh/operators/bmo_removedoubles.c
24  *  \ingroup bmesh
25  */
26
27 #include "MEM_guardedalloc.h"
28
29 #include "BLI_math.h"
30 #include "BLI_array.h"
31
32 #include "BKE_customdata.h"
33
34 #include "bmesh.h"
35 #include "intern/bmesh_private.h"
36
37 #include "intern/bmesh_operators_private.h" /* own include */
38
39 static void remdoubles_splitface(BMFace *f, BMesh *bm, BMOperator *op, BMOpSlot *slot_targetmap)
40 {
41         BMIter liter;
42         BMLoop *l;
43         BMVert *v2, *v_double;
44         bool split = false;
45
46         BM_ITER_ELEM (l, &liter, f, BM_LOOPS_OF_FACE) {
47                 v2 = BMO_slot_map_elem_get(slot_targetmap, l->v);
48                 /* ok: if v2 is NULL (e.g. not in the map) then it's
49                  *     a target vert, otherwise it's a double */
50                 if ((v2 && BM_vert_in_face(f, v2)) &&
51                     (v2 != l->prev->v) &&
52                     (v2 != l->next->v))
53                 {
54                         v_double = l->v;
55                         split = true;
56                         break;
57                 }
58         }
59
60         if (split && v_double != v2) {
61                 BMLoop *l_new;
62                 BMFace *f2 = BM_face_split(bm, f, v_double, v2, &l_new, NULL, false);
63
64                 remdoubles_splitface(f, bm, op, slot_targetmap);
65                 remdoubles_splitface(f2, bm, op, slot_targetmap);
66         }
67 }
68
69 #define ELE_DEL         1
70 #define EDGE_COL        2
71 #define FACE_MARK       2
72
73 #if 0
74 int remdoubles_face_overlaps(BMesh *bm, BMVert **varr,
75                              int len, BMFace *exclude,
76                              BMFace **overlapface)
77 {
78         BMIter vertfaces;
79         BMFace *f;
80         int i, amount;
81
82         if (overlapface) *overlapface = NULL;
83
84         for (i = 0; i < len; i++) {
85                 f = BM_iter_new(&vertfaces, bm, BM_FACES_OF_VERT, varr[i]);
86                 while (f) {
87                         amount = BM_verts_in_face(bm, f, varr, len);
88                         if (amount >= len) {
89                                 if (overlapface) *overlapface = f;
90                                 return true;
91                         }
92                         f = BM_iter_step(&vertfaces);
93                 }
94         }
95         return false;
96 }
97 #endif
98
99 void bmo_weld_verts_exec(BMesh *bm, BMOperator *op)
100 {
101         BMIter iter, liter;
102         BMVert *v, *v2;
103         BMEdge *e, *e2, **edges = NULL;
104         BLI_array_declare(edges);
105         BMLoop *l, *l2, **loops = NULL;
106         BLI_array_declare(loops);
107         BMFace *f, *f2;
108         int a, b;
109         BMOpSlot *slot_targetmap = BMO_slot_get(op->slots_in, "targetmap");
110
111         /* mark merge verts for deletion */
112         BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) {
113                 if ((v2 = BMO_slot_map_elem_get(slot_targetmap, v))) {
114                         BMO_elem_flag_enable(bm, v, ELE_DEL);
115
116                         /* merge the vertex flags, else we get randomly selected/unselected verts */
117                         BM_elem_flag_merge(v, v2);
118                 }
119         }
120
121         /* check if any faces are getting their own corners merged
122          * together, split face if so */
123         BM_ITER_MESH (f, &iter, bm, BM_FACES_OF_MESH) {
124                 remdoubles_splitface(f, bm, op, slot_targetmap);
125         }
126
127         BM_ITER_MESH (e, &iter, bm, BM_EDGES_OF_MESH) {
128                 if (BMO_elem_flag_test(bm, e->v1, ELE_DEL) || BMO_elem_flag_test(bm, e->v2, ELE_DEL)) {
129                         v  = BMO_slot_map_elem_get(slot_targetmap, e->v1);
130                         v2 = BMO_slot_map_elem_get(slot_targetmap, e->v2);
131                         
132                         if (!v) v = e->v1;
133                         if (!v2) v2 = e->v2;
134
135                         if (v == v2) {
136                                 BMO_elem_flag_enable(bm, e, EDGE_COL);
137                         }
138                         else if (!BM_edge_exists(v, v2)) {
139                                 BM_edge_create(bm, v, v2, e, BM_CREATE_NO_DOUBLE);
140                         }
141
142                         BMO_elem_flag_enable(bm, e, ELE_DEL);
143                 }
144         }
145
146         /* BMESH_TODO, stop abusing face index here */
147         BM_ITER_MESH (f, &iter, bm, BM_FACES_OF_MESH) {
148                 BM_elem_index_set(f, 0); /* set_dirty! */
149                 BM_ITER_ELEM (l, &liter, f, BM_LOOPS_OF_FACE) {
150                         if (BMO_elem_flag_test(bm, l->v, ELE_DEL)) {
151                                 BMO_elem_flag_enable(bm, f, FACE_MARK | ELE_DEL);
152                         }
153                         if (BMO_elem_flag_test(bm, l->e, EDGE_COL)) {
154                                 BM_elem_index_set(f, BM_elem_index_get(f) + 1); /* set_dirty! */
155                         }
156                 }
157         }
158         bm->elem_index_dirty |= BM_FACE;
159
160         /* faces get "modified" by creating new faces here, then at the
161          * end the old faces are deleted */
162         BM_ITER_MESH (f, &iter, bm, BM_FACES_OF_MESH) {
163                 if (!BMO_elem_flag_test(bm, f, FACE_MARK))
164                         continue;
165
166                 if (f->len - BM_elem_index_get(f) < 3) {
167                         BMO_elem_flag_enable(bm, f, ELE_DEL);
168                         continue;
169                 }
170
171                 BLI_array_empty(edges);
172                 BLI_array_empty(loops);
173                 a = 0;
174                 BM_ITER_ELEM (l, &liter, f, BM_LOOPS_OF_FACE) {
175                         v = l->v;
176                         v2 = l->next->v;
177                         if (BMO_elem_flag_test(bm, v, ELE_DEL)) {
178                                 v = BMO_slot_map_elem_get(slot_targetmap, v);
179                         }
180                         if (BMO_elem_flag_test(bm, v2, ELE_DEL)) {
181                                 v2 = BMO_slot_map_elem_get(slot_targetmap, v2);
182                         }
183                         
184                         e2 = v != v2 ? BM_edge_exists(v, v2) : NULL;
185                         if (e2) {
186                                 for (b = 0; b < a; b++) {
187                                         if (edges[b] == e2) {
188                                                 break;
189                                         }
190                                 }
191                                 if (b != a) {
192                                         continue;
193                                 }
194
195                                 BLI_array_grow_one(edges);
196                                 BLI_array_grow_one(loops);
197
198                                 edges[a] = e2;
199                                 loops[a] = l;
200
201                                 a++;
202                         }
203                 }
204                 
205                 if (BLI_array_count(loops) < 3)
206                         continue;
207                 v = loops[0]->v;
208                 v2 = loops[1]->v;
209
210                 if (BMO_elem_flag_test(bm, v, ELE_DEL)) {
211                         v = BMO_slot_map_elem_get(slot_targetmap, v);
212                 }
213                 if (BMO_elem_flag_test(bm, v2, ELE_DEL)) {
214                         v2 = BMO_slot_map_elem_get(slot_targetmap, v2);
215                 }
216                 
217                 f2 = BM_face_create_ngon(bm, v, v2, edges, a, BM_CREATE_NO_DOUBLE);
218                 if (f2 && (f2 != f)) {
219                         BM_elem_attrs_copy(bm, bm, f, f2);
220
221                         a = 0;
222                         BM_ITER_ELEM (l, &liter, f2, BM_LOOPS_OF_FACE) {
223                                 l2 = loops[a];
224                                 BM_elem_attrs_copy(bm, bm, l2, l);
225
226                                 a++;
227                         }
228                 }
229         }
230
231         BMO_op_callf(bm, op->flag, "delete geom=%fvef context=%i", ELE_DEL, DEL_ONLYTAGGED);
232
233         BLI_array_free(edges);
234         BLI_array_free(loops);
235 }
236
237 static int vergaverco(const void *e1, const void *e2)
238 {
239         const BMVert *v1 = *(void **)e1, *v2 = *(void **)e2;
240         float x1 = v1->co[0] + v1->co[1] + v1->co[2];
241         float x2 = v2->co[0] + v2->co[1] + v2->co[2];
242
243         if      (x1 > x2) return  1;
244         else if (x1 < x2) return -1;
245         else return 0;
246 }
247
248 // #define VERT_TESTED  1 // UNUSED
249 #define VERT_DOUBLE     2
250 #define VERT_TARGET     4
251 #define VERT_KEEP       8
252 // #define VERT_MARK    16 // UNUSED
253 #define VERT_IN         32
254
255 #define EDGE_MARK       1
256
257 void bmo_pointmerge_facedata_exec(BMesh *bm, BMOperator *op)
258 {
259         BMOIter siter;
260         BMIter iter;
261         BMVert *v, *vert_snap;
262         BMLoop *l, *firstl = NULL;
263         float fac;
264         int i, tot;
265
266         vert_snap = BMO_slot_buffer_get_single(BMO_slot_get(op->slots_in, "vert_snap"));
267         tot = BM_vert_face_count(vert_snap);
268
269         if (!tot)
270                 return;
271
272         fac = 1.0f / tot;
273         BM_ITER_ELEM (l, &iter, vert_snap, BM_LOOPS_OF_VERT) {
274                 if (!firstl) {
275                         firstl = l;
276                 }
277                 
278                 for (i = 0; i < bm->ldata.totlayer; i++) {
279                         if (CustomData_layer_has_math(&bm->ldata, i)) {
280                                 int type = bm->ldata.layers[i].type;
281                                 void *e1, *e2;
282
283                                 e1 = CustomData_bmesh_get_layer_n(&bm->ldata, firstl->head.data, i);
284                                 e2 = CustomData_bmesh_get_layer_n(&bm->ldata, l->head.data, i);
285                                 
286                                 CustomData_data_multiply(type, e2, fac);
287
288                                 if (l != firstl)
289                                         CustomData_data_add(type, e1, e2);
290                         }
291                 }
292         }
293
294         BMO_ITER (v, &siter, op->slots_in, "verts", BM_VERT) {
295                 BM_ITER_ELEM (l, &iter, v, BM_LOOPS_OF_VERT) {
296                         if (l == firstl) {
297                                 continue;
298                         }
299
300                         CustomData_bmesh_copy_data(&bm->ldata, &bm->ldata, firstl->head.data, &l->head.data);
301                 }
302         }
303 }
304
305 void bmo_average_vert_facedata_exec(BMesh *bm, BMOperator *op)
306 {
307         BMOIter siter;
308         BMIter iter;
309         BMVert *v;
310         BMLoop *l /* , *firstl = NULL */;
311         CDBlockBytes min, max;
312         void *block;
313         int i, type;
314
315         for (i = 0; i < bm->ldata.totlayer; i++) {
316                 if (!CustomData_layer_has_math(&bm->ldata, i))
317                         continue;
318                 
319                 type = bm->ldata.layers[i].type;
320                 CustomData_data_initminmax(type, &min, &max);
321
322                 BMO_ITER (v, &siter, op->slots_in, "verts", BM_VERT) {
323                         BM_ITER_ELEM (l, &iter, v, BM_LOOPS_OF_VERT) {
324                                 block = CustomData_bmesh_get_layer_n(&bm->ldata, l->head.data, i);
325                                 CustomData_data_dominmax(type, block, &min, &max);
326                         }
327                 }
328
329                 CustomData_data_multiply(type, &min, 0.5f);
330                 CustomData_data_multiply(type, &max, 0.5f);
331                 CustomData_data_add(type, &min, &max);
332
333                 BMO_ITER (v, &siter, op->slots_in, "verts", BM_VERT) {
334                         BM_ITER_ELEM (l, &iter, v, BM_LOOPS_OF_VERT) {
335                                 block = CustomData_bmesh_get_layer_n(&bm->ldata, l->head.data, i);
336                                 CustomData_data_copy_value(type, &min, block);
337                         }
338                 }
339         }
340 }
341
342 void bmo_pointmerge_exec(BMesh *bm, BMOperator *op)
343 {
344         BMOperator weldop;
345         BMOIter siter;
346         BMVert *v, *vert_snap = NULL;
347         float vec[3];
348         BMOpSlot *slot_targetmap;
349         
350         BMO_slot_vec_get(op->slots_in, "merge_co", vec);
351
352         //BMO_op_callf(bm, op->flag, "collapse_uvs edges=%s", op, "edges");
353         BMO_op_init(bm, &weldop, op->flag, "weld_verts");
354
355         slot_targetmap = BMO_slot_get(weldop.slots_in, "targetmap");
356
357         BMO_ITER (v, &siter, op->slots_in, "verts", BM_VERT) {
358                 if (!vert_snap) {
359                         vert_snap = v;
360                         copy_v3_v3(vert_snap->co, vec);
361                 }
362                 else {
363                         BMO_slot_map_elem_insert(&weldop, slot_targetmap, v, vert_snap);
364                 }
365         }
366
367         BMO_op_exec(bm, &weldop);
368         BMO_op_finish(bm, &weldop);
369 }
370
371 void bmo_collapse_exec(BMesh *bm, BMOperator *op)
372 {
373         BMOperator weldop;
374         BMWalker walker;
375         BMIter iter;
376         BMEdge *e, **edges = NULL;
377         BLI_array_declare(edges);
378         float min[3], max[3], center[3];
379         int i, tot;
380         BMOpSlot *slot_targetmap;
381         
382         BMO_op_callf(bm, op->flag, "collapse_uvs edges=%s", op, "edges");
383         BMO_op_init(bm, &weldop, op->flag, "weld_verts");
384         slot_targetmap = BMO_slot_get(weldop.slots_in, "targetmap");
385
386         BMO_slot_buffer_flag_enable(bm, op->slots_in, "edges", BM_EDGE, EDGE_MARK);
387
388         BMW_init(&walker, bm, BMW_SHELL,
389                  BMW_MASK_NOP, EDGE_MARK, BMW_MASK_NOP,
390                  BMW_FLAG_NOP, /* no need to use BMW_FLAG_TEST_HIDDEN, already marked data */
391                  BMW_NIL_LAY);
392
393         BM_ITER_MESH (e, &iter, bm, BM_EDGES_OF_MESH) {
394                 if (!BMO_elem_flag_test(bm, e, EDGE_MARK))
395                         continue;
396
397                 BLI_array_empty(edges);
398
399                 INIT_MINMAX(min, max);
400                 for (e = BMW_begin(&walker, e->v1), tot = 0; e; e = BMW_step(&walker), tot++) {
401                         BLI_array_grow_one(edges);
402                         edges[tot] = e;
403
404                         minmax_v3v3_v3(min, max, e->v1->co);
405                         minmax_v3v3_v3(min, max, e->v2->co);
406                 }
407
408                 mid_v3_v3v3(center, min, max);
409
410                 /* snap edges to a point.  for initial testing purposes anyway */
411                 for (i = 0; i < tot; i++) {
412                         copy_v3_v3(edges[i]->v1->co, center);
413                         copy_v3_v3(edges[i]->v2->co, center);
414                         
415                         if (edges[i]->v1 != edges[0]->v1)
416                                 BMO_slot_map_elem_insert(&weldop, slot_targetmap, edges[i]->v1, edges[0]->v1);
417                         if (edges[i]->v2 != edges[0]->v1)
418                                 BMO_slot_map_elem_insert(&weldop, slot_targetmap, edges[i]->v2, edges[0]->v1);
419                 }
420         }
421         
422         BMO_op_exec(bm, &weldop);
423         BMO_op_finish(bm, &weldop);
424
425         BMW_end(&walker);
426         BLI_array_free(edges);
427 }
428
429 /* uv collapse function */
430 static void bmo_collapsecon_do_layer(BMesh *bm, BMOperator *op, int layer)
431 {
432         BMIter iter, liter;
433         BMFace *f;
434         BMLoop *l, *l2;
435         BMWalker walker;
436         void **blocks = NULL;
437         BLI_array_declare(blocks);
438         CDBlockBytes min, max;
439         int i, tot, type = bm->ldata.layers[layer].type;
440
441         /* clear all short flags */
442         BMO_mesh_flag_disable_all(bm, op, BM_ALL, (1 << 16) - 1);
443
444         BMO_slot_buffer_flag_enable(bm, op->slots_in, "edges", BM_EDGE, EDGE_MARK);
445
446         BMW_init(&walker, bm, BMW_LOOPDATA_ISLAND,
447                  BMW_MASK_NOP, EDGE_MARK, BMW_MASK_NOP,
448                  BMW_FLAG_NOP, /* no need to use BMW_FLAG_TEST_HIDDEN, already marked data */
449                  layer);
450
451         BM_ITER_MESH (f, &iter, bm, BM_FACES_OF_MESH) {
452                 BM_ITER_ELEM (l, &liter, f, BM_LOOPS_OF_FACE) {
453                         if (BMO_elem_flag_test(bm, l->e, EDGE_MARK)) {
454                                 /* walk */
455                                 BLI_array_empty(blocks);
456
457                                 CustomData_data_initminmax(type, &min, &max);
458                                 for (l2 = BMW_begin(&walker, l), tot = 0; l2; l2 = BMW_step(&walker), tot++) {
459                                         BLI_array_grow_one(blocks);
460                                         blocks[tot] = CustomData_bmesh_get_layer_n(&bm->ldata, l2->head.data, layer);
461                                         CustomData_data_dominmax(type, blocks[tot], &min, &max);
462                                 }
463
464                                 if (tot) {
465                                         CustomData_data_multiply(type, &min, 0.5f);
466                                         CustomData_data_multiply(type, &max, 0.5f);
467                                         CustomData_data_add(type, &min, &max);
468
469                                         /* snap CD (uv, vcol) points to their centroid */
470                                         for (i = 0; i < tot; i++) {
471                                                 CustomData_data_copy_value(type, &min, blocks[i]);
472                                         }
473                                 }
474                         }
475                 }
476         }
477
478         BMW_end(&walker);
479         BLI_array_free(blocks);
480 }
481
482 void bmo_collapse_uvs_exec(BMesh *bm, BMOperator *op)
483 {
484         int i;
485
486         for (i = 0; i < bm->ldata.totlayer; i++) {
487                 if (CustomData_layer_has_math(&bm->ldata, i))
488                         bmo_collapsecon_do_layer(bm, op, i);
489         }
490 }
491
492 static void bmesh_find_doubles_common(BMesh *bm, BMOperator *op,
493                                       BMOperator *optarget, BMOpSlot *optarget_slot)
494 {
495         BMVert  **verts;
496         int       verts_len;
497
498         int i, j, keepvert = 0;
499
500         const float dist  = BMO_slot_float_get(op->slots_in, "dist");
501         const float dist3 = dist * 3.0f;
502
503         /* Test whether keep_verts arg exists and is non-empty */
504         if (BMO_slot_exists(op->slots_in, "keep_verts")) {
505                 BMOIter oiter;
506                 keepvert = BMO_iter_new(&oiter, op->slots_in, "keep_verts", BM_VERT) != NULL;
507         }
508
509         /* get the verts as an array we can sort */
510         verts = BMO_slot_as_arrayN(op->slots_in, "verts", &verts_len);
511
512         /* sort by vertex coordinates added together */
513         qsort(verts, verts_len, sizeof(BMVert *), vergaverco);
514
515         /* Flag keep_verts */
516         if (keepvert) {
517                 BMO_slot_buffer_flag_enable(bm, op->slots_in, "keep_verts", BM_VERT, VERT_KEEP);
518         }
519
520         for (i = 0; i < verts_len; i++) {
521                 BMVert *v_check = verts[i];
522
523                 if (BMO_elem_flag_test(bm, v_check, VERT_DOUBLE)) {
524                         continue;
525                 }
526
527                 for (j = i + 1; j < verts_len; j++) {
528                         BMVert *v_other = verts[j];
529
530                         /* Compare sort values of the verts using 3x tolerance (allowing for the tolerance
531                          * on each of the three axes). This avoids the more expensive length comparison
532                          * for most vertex pairs. */
533                         if ((v_other->co[0] + v_other->co[1] + v_other->co[2]) -
534                             (v_check->co[0] + v_check->co[1] + v_check->co[2]) > dist3)
535                         {
536                                 break;
537                         }
538
539                         if (keepvert) {
540                                 if (BMO_elem_flag_test(bm, v_other, VERT_KEEP) == BMO_elem_flag_test(bm, v_check, VERT_KEEP))
541                                         continue;
542                         }
543
544                         if (compare_len_v3v3(v_check->co, v_other->co, dist)) {
545
546                                 /* If one vert is marked as keep, make sure it will be the target */
547                                 if (BMO_elem_flag_test(bm, v_other, VERT_KEEP)) {
548                                         SWAP(BMVert *, v_check, v_other);
549                                 }
550
551                                 BMO_elem_flag_enable(bm, v_other, VERT_DOUBLE);
552                                 BMO_elem_flag_enable(bm, v_check, VERT_TARGET);
553
554                                 BMO_slot_map_elem_insert(optarget, optarget_slot, v_other, v_check);
555                         }
556                 }
557         }
558
559         MEM_freeN(verts);
560 }
561
562 void bmo_remove_doubles_exec(BMesh *bm, BMOperator *op)
563 {
564         BMOperator weldop;
565         BMOpSlot *slot_targetmap;
566
567         BMO_op_init(bm, &weldop, op->flag, "weld_verts");
568         slot_targetmap = BMO_slot_get(weldop.slots_in, "targetmap");
569         bmesh_find_doubles_common(bm, op,
570                                   &weldop, slot_targetmap);
571         BMO_op_exec(bm, &weldop);
572         BMO_op_finish(bm, &weldop);
573 }
574
575
576 void bmo_find_doubles_exec(BMesh *bm, BMOperator *op)
577 {
578         BMOpSlot *slot_targetmap_out;
579         slot_targetmap_out = BMO_slot_get(op->slots_out, "targetmap.out");
580         bmesh_find_doubles_common(bm, op,
581                                   op, slot_targetmap_out);
582 }
583
584 void bmo_automerge_exec(BMesh *bm, BMOperator *op)
585 {
586         BMOperator findop, weldop;
587         BMIter viter;
588         BMVert *v;
589
590         /* The "verts" input sent to this op is the set of verts that
591          * can be merged away into any other verts. Mark all other verts
592          * as VERT_KEEP. */
593         BMO_slot_buffer_flag_enable(bm, op->slots_in, "verts", BM_VERT, VERT_IN);
594         BM_ITER_MESH (v, &viter, bm, BM_VERTS_OF_MESH) {
595                 if (!BMO_elem_flag_test(bm, v, VERT_IN)) {
596                         BMO_elem_flag_enable(bm, v, VERT_KEEP);
597                 }
598         }
599
600         /* Search for doubles among all vertices, but only merge non-VERT_KEEP
601          * vertices into VERT_KEEP vertices. */
602         BMO_op_initf(bm, &findop, op->flag, "find_doubles verts=%av keep_verts=%fv", VERT_KEEP);
603         BMO_slot_copy(op,      slots_in, "dist",
604                       &findop, slots_in, "dist");
605         BMO_op_exec(bm, &findop);
606
607         /* weld the vertices */
608         BMO_op_init(bm, &weldop, op->flag, "weld_verts");
609         BMO_slot_copy(&findop, slots_out, "targetmap.out",
610                       &weldop, slots_in,  "targetmap");
611         BMO_op_exec(bm, &weldop);
612
613         BMO_op_finish(bm, &findop);
614         BMO_op_finish(bm, &weldop);
615 }