New operator for copying (hair) particle systems from one object to

another, including edit data (grooming).

This uses basically the same method as the existing connect/disconnect
feature. The main difference is that it allows working with multiple
objects and transferring the //particle/hair data// instead of the
//mesh// data (which is what connect/disconnect expects). This is a much
more realistic workflow when rigging, topology etc. changes and
groomed hair has to be transferred to the changed model.
This commit is contained in:
Lukas Tönne 2015-01-15 11:51:30 +01:00
parent 8f9f55498e
commit 91b70d3c56
5 changed files with 323 additions and 64 deletions

@ -3041,6 +3041,7 @@ void object_remove_particle_system(Scene *UNUSED(scene), Object *ob)
DAG_relations_tag_update(G.main); DAG_relations_tag_update(G.main);
DAG_id_tag_update(&ob->id, OB_RECALC_DATA); DAG_id_tag_update(&ob->id, OB_RECALC_DATA);
} }
static void default_particle_settings(ParticleSettings *part) static void default_particle_settings(ParticleSettings *part)
{ {
part->type = PART_EMITTER; part->type = PART_EMITTER;

@ -83,9 +83,11 @@
#include "physics_intern.h" #include "physics_intern.h"
static void PE_create_particle_edit(Scene *scene, Object *ob, PointCache *cache, ParticleSystem *psys); void PE_create_particle_edit(Scene *scene, Object *ob, PointCache *cache, ParticleSystem *psys);
static void PTCacheUndo_clear(PTCacheEdit *edit); void PTCacheUndo_clear(PTCacheEdit *edit);
static void recalc_emitter_field(Object *ob, ParticleSystem *psys); void recalc_lengths(PTCacheEdit *edit);
void recalc_emitter_field(Object *ob, ParticleSystem *psys);
void update_world_cos(Object *ob, PTCacheEdit *edit);
#define KEY_K PTCacheEditKey *key; int k #define KEY_K PTCacheEditKey *key; int k
#define POINT_P PTCacheEditPoint *point; int p #define POINT_P PTCacheEditPoint *point; int p
@ -1074,7 +1076,7 @@ static void pe_iterate_lengths(Scene *scene, PTCacheEdit *edit)
} }
} }
/* set current distances to be kept between neighbouting keys */ /* set current distances to be kept between neighbouting keys */
static void recalc_lengths(PTCacheEdit *edit) void recalc_lengths(PTCacheEdit *edit)
{ {
POINT_P; KEY_K; POINT_P; KEY_K;
@ -1090,7 +1092,7 @@ static void recalc_lengths(PTCacheEdit *edit)
} }
/* calculate a tree for finding nearest emitter's vertice */ /* calculate a tree for finding nearest emitter's vertice */
static void recalc_emitter_field(Object *ob, ParticleSystem *psys) void recalc_emitter_field(Object *ob, ParticleSystem *psys)
{ {
DerivedMesh *dm=psys_get_modifier(ob, psys)->dm; DerivedMesh *dm=psys_get_modifier(ob, psys)->dm;
PTCacheEdit *edit= psys->edit; PTCacheEdit *edit= psys->edit;
@ -1178,7 +1180,7 @@ static void PE_update_selection(Scene *scene, Object *ob, int useflag)
point->flag &= ~PEP_EDIT_RECALC; point->flag &= ~PEP_EDIT_RECALC;
} }
static void update_world_cos(Object *ob, PTCacheEdit *edit) void update_world_cos(Object *ob, PTCacheEdit *edit)
{ {
ParticleSystem *psys = edit->psys; ParticleSystem *psys = edit->psys;
ParticleSystemModifierData *psmd= psys_get_modifier(ob, psys); ParticleSystemModifierData *psmd= psys_get_modifier(ob, psys);
@ -4452,7 +4454,7 @@ int PE_undo_valid(Scene *scene)
return 0; return 0;
} }
static void PTCacheUndo_clear(PTCacheEdit *edit) void PTCacheUndo_clear(PTCacheEdit *edit)
{ {
PTCacheUndo *undo; PTCacheUndo *undo;
@ -4553,7 +4555,7 @@ int PE_minmax(Scene *scene, float min[3], float max[3])
/************************ particle edit toggle operator ************************/ /************************ particle edit toggle operator ************************/
/* initialize needed data for bake edit */ /* initialize needed data for bake edit */
static void PE_create_particle_edit(Scene *scene, Object *ob, PointCache *cache, ParticleSystem *psys) void PE_create_particle_edit(Scene *scene, Object *ob, PointCache *cache, ParticleSystem *psys)
{ {
PTCacheEdit *edit; PTCacheEdit *edit;
ParticleSystemModifierData *psmd = (psys) ? psys_get_modifier(ob, psys) : NULL; ParticleSystemModifierData *psmd = (psys) ? psys_get_modifier(ob, psys) : NULL;

@ -40,18 +40,22 @@
#include "BLI_math.h" #include "BLI_math.h"
#include "BLI_listbase.h" #include "BLI_listbase.h"
#include "BLI_utildefines.h" #include "BLI_utildefines.h"
#include "BLI_string.h"
#include "BKE_context.h" #include "BKE_context.h"
#include "BKE_depsgraph.h" #include "BKE_depsgraph.h"
#include "BKE_DerivedMesh.h" #include "BKE_DerivedMesh.h"
#include "BKE_cdderivedmesh.h" #include "BKE_cdderivedmesh.h"
#include "BKE_effect.h"
#include "BKE_global.h" #include "BKE_global.h"
#include "BKE_library.h"
#include "BKE_main.h" #include "BKE_main.h"
#include "BKE_modifier.h"
#include "BKE_object.h"
#include "BKE_particle.h" #include "BKE_particle.h"
#include "BKE_pointcache.h" #include "BKE_pointcache.h"
#include "BKE_report.h" #include "BKE_report.h"
#include "RNA_access.h" #include "RNA_access.h"
#include "RNA_define.h" #include "RNA_define.h"
@ -62,8 +66,31 @@
#include "ED_screen.h" #include "ED_screen.h"
#include "ED_object.h" #include "ED_object.h"
#include "UI_resources.h"
#include "physics_intern.h" #include "physics_intern.h"
extern void PE_create_particle_edit(Scene *scene, Object *ob, PointCache *cache, ParticleSystem *psys);
extern void PTCacheUndo_clear(PTCacheEdit *edit);
extern void recalc_lengths(PTCacheEdit *edit);
extern void recalc_emitter_field(Object *ob, ParticleSystem *psys);
extern void update_world_cos(Object *ob, PTCacheEdit *edit);
#define KEY_K PTCacheEditKey *key; int k
#define POINT_P PTCacheEditPoint *point; int p
#define LOOP_POINTS for (p=0, point=edit->points; p<edit->totpoint; p++, point++)
#define LOOP_VISIBLE_POINTS for (p=0, point=edit->points; p<edit->totpoint; p++, point++) if (!(point->flag & PEP_HIDE))
#define LOOP_SELECTED_POINTS for (p=0, point=edit->points; p<edit->totpoint; p++, point++) if (point_is_selected(point))
#define LOOP_UNSELECTED_POINTS for (p=0, point=edit->points; p<edit->totpoint; p++, point++) if (!point_is_selected(point))
#define LOOP_EDITED_POINTS for (p=0, point=edit->points; p<edit->totpoint; p++, point++) if (point->flag & PEP_EDIT_RECALC)
#define LOOP_TAGGED_POINTS for (p=0, point=edit->points; p<edit->totpoint; p++, point++) if (point->flag & PEP_TAG)
#define LOOP_KEYS for (k=0, key=point->keys; k<point->totkey; k++, key++)
#define LOOP_VISIBLE_KEYS for (k=0, key=point->keys; k<point->totkey; k++, key++) if (!(key->flag & PEK_HIDE))
#define LOOP_SELECTED_KEYS for (k=0, key=point->keys; k<point->totkey; k++, key++) if ((key->flag & PEK_SELECT) && !(key->flag & PEK_HIDE))
#define LOOP_TAGGED_KEYS for (k=0, key=point->keys; k<point->totkey; k++, key++) if (key->flag & PEK_TAG)
#define KEY_WCO (key->flag & PEK_USE_WCO ? key->world_co : key->co)
/********************** particle system slot operators *********************/ /********************** particle system slot operators *********************/
static int particle_system_add_exec(bContext *C, wmOperator *UNUSED(op)) static int particle_system_add_exec(bContext *C, wmOperator *UNUSED(op))
@ -623,37 +650,32 @@ void PARTICLE_OT_disconnect_hair(wmOperatorType *ot)
RNA_def_boolean(ot->srna, "all", 0, "All hair", "Disconnect all hair systems from the emitter mesh"); RNA_def_boolean(ot->srna, "all", 0, "All hair", "Disconnect all hair systems from the emitter mesh");
} }
static bool connect_hair(Scene *scene, Object *ob, ParticleSystem *psys) static bool remap_hair_emitter(Scene *scene, Object *UNUSED(ob), ParticleSystem *psys,
Object *target_ob, DerivedMesh *target_dm, ParticleSystem *target_psys, PTCacheEdit *target_edit,
bool from_world_space, bool to_world_space)
{ {
ParticleSystemModifierData *psmd = psys_get_modifier(ob, psys); ParticleData *pa, *tpa;
ParticleData *pa; PTCacheEditPoint *edit_point;
PTCacheEdit *edit; PTCacheEditKey *ekey;
PTCacheEditPoint *point;
PTCacheEditKey *ekey = NULL;
HairKey *key;
BVHTreeFromMesh bvhtree= {NULL}; BVHTreeFromMesh bvhtree= {NULL};
BVHTreeNearest nearest;
MFace *mface = NULL, *mf; MFace *mface = NULL, *mf;
MEdge *medge = NULL, *me; MEdge *medge = NULL, *me;
MVert *mvert; MVert *mvert;
DerivedMesh *dm = NULL; DerivedMesh *dm;
int numverts; int numverts;
int i, k; int i, k;
float hairmat[4][4], imat[4][4];
float v[4][3], vec[3];
if (!psys || !psys->part || psys->part->type != PART_HAIR || !psmd->dm) if (!psys->part || psys->part->type != PART_HAIR || !target_psys->part || target_psys->part->type != PART_HAIR)
return false; return false;
edit= psys->edit; edit_point = target_edit ? target_edit->points : NULL;
point= edit ? edit->points : NULL;
if (psmd->dm->deformedOnly) { if (target_dm->deformedOnly) {
/* we don't want to mess up psmd->dm when converting to global coordinates below */ /* we don't want to mess up target_dm when converting to global coordinates below */
dm = psmd->dm; dm = target_dm;
} }
else { else {
dm = mesh_get_derived_deform(scene, ob, CD_MASK_BAREMESH); dm = mesh_get_derived_deform(scene, target_ob, CD_MASK_BAREMESH);
} }
/* don't modify the original vertices */ /* don't modify the original vertices */
dm = CDDM_copy(dm); dm = CDDM_copy(dm);
@ -662,12 +684,11 @@ static bool connect_hair(Scene *scene, Object *ob, ParticleSystem *psys)
DM_ensure_tessface(dm); DM_ensure_tessface(dm);
numverts = dm->getNumVerts(dm); numverts = dm->getNumVerts(dm);
mvert = dm->getVertArray(dm); mvert = dm->getVertArray(dm);
/* convert to global coordinates */ /* convert to global coordinates */
for (i=0; i<numverts; i++) for (i=0; i<numverts; i++)
mul_m4_v3(ob->obmat, mvert[i].co); mul_m4_v3(target_ob->obmat, mvert[i].co);
if (dm->getNumTessFaces(dm) != 0) { if (dm->getNumTessFaces(dm) != 0) {
mface = dm->getTessFaceArray(dm); mface = dm->getTessFaceArray(dm);
@ -682,13 +703,17 @@ static bool connect_hair(Scene *scene, Object *ob, ParticleSystem *psys)
return false; return false;
} }
for (i=0, pa= psys->particles; i<psys->totpart; i++, pa++) { for (i = 0, tpa = target_psys->particles, pa = psys->particles;
key = pa->hair; i < target_psys->totpart;
i++, tpa++, pa++) {
const float *co = from_world_space ? pa->hair[0].co : pa->hair[0].world_co;
BVHTreeNearest nearest;
nearest.index = -1; nearest.index = -1;
nearest.dist_sq = FLT_MAX; nearest.dist_sq = FLT_MAX;
BLI_bvhtree_find_nearest(bvhtree.tree, key->co, &nearest, bvhtree.nearest_callback, &bvhtree); BLI_bvhtree_find_nearest(bvhtree.tree, co, &nearest, bvhtree.nearest_callback, &bvhtree);
if (nearest.index == -1) { if (nearest.index == -1) {
if (G.debug & G_DEBUG) if (G.debug & G_DEBUG)
@ -697,6 +722,8 @@ static bool connect_hair(Scene *scene, Object *ob, ParticleSystem *psys)
} }
if (mface) { if (mface) {
float v[4][3];
mf = &mface[nearest.index]; mf = &mface[nearest.index];
copy_v3_v3(v[0], mvert[mf->v1].co); copy_v3_v3(v[0], mvert[mf->v1].co);
@ -704,44 +731,71 @@ static bool connect_hair(Scene *scene, Object *ob, ParticleSystem *psys)
copy_v3_v3(v[2], mvert[mf->v3].co); copy_v3_v3(v[2], mvert[mf->v3].co);
if (mf->v4) { if (mf->v4) {
copy_v3_v3(v[3], mvert[mf->v4].co); copy_v3_v3(v[3], mvert[mf->v4].co);
interp_weights_poly_v3(pa->fuv, v, 4, nearest.co); interp_weights_poly_v3(tpa->fuv, v, 4, nearest.co);
} }
else else
interp_weights_poly_v3(pa->fuv, v, 3, nearest.co); interp_weights_poly_v3(tpa->fuv, v, 3, nearest.co);
tpa->foffset = 0.0f;
pa->num = nearest.index; tpa->num = nearest.index;
pa->num_dmcache = psys_particle_dm_face_lookup(ob, psmd->dm, pa->num, pa->fuv, NULL); tpa->num_dmcache = psys_particle_dm_face_lookup(target_ob, dm, tpa->num, tpa->fuv, NULL);
} }
else { else {
me = &medge[nearest.index]; me = &medge[nearest.index];
pa->fuv[1] = line_point_factor_v3(nearest.co, tpa->fuv[1] = line_point_factor_v3(nearest.co,
mvert[me->v2].co, mvert[me->v1].co,
mvert[me->v2].co); mvert[me->v2].co);
pa->fuv[0] = 1.0f - pa->fuv[1]; tpa->fuv[0] = 1.0f - tpa->fuv[1];
pa->fuv[2] = pa->fuv[3] = 0.0f; tpa->fuv[2] = tpa->fuv[3] = 0.0f;
tpa->foffset = 0.0f;
pa->num = nearest.index; tpa->num = nearest.index;
pa->num_dmcache = -1; tpa->num_dmcache = -1;
} }
psys_mat_hair_to_global(ob, psmd->dm, psys->part->from, pa, hairmat); /* translate hair keys */
invert_m4_m4(imat, hairmat); {
HairKey *key, *tkey;
sub_v3_v3v3(vec, nearest.co, key->co); float hairmat[4][4], imat[4][4];
float offset[3];
if (point) {
ekey = point->keys; /* note: using target_dm here, which is in target_ob object space and has full modifiers */
point++; psys_mat_hair_to_global(target_ob, target_dm, target_psys->part->from, tpa, hairmat);
} invert_m4_m4(imat, hairmat);
for (k=0, key=pa->hair; k<pa->totkey; k++, key++) { /* offset in world space */
add_v3_v3(key->co, vec); sub_v3_v3v3(offset, nearest.co, co);
mul_m4_v3(imat, key->co); {
SimDebugData *dd = psys->clmd ? psys->clmd->debug_data : NULL;
if (ekey) { BKE_sim_debug_data_add_dot(dd, nearest.co, 1,1,1, "particle matrix", 689, i);
ekey->flag |= PEK_USE_WCO;
ekey++; BKE_sim_debug_data_add_vector(dd, hairmat[3], hairmat[0], 1,0,0, "particle matrix", 222, i);
BKE_sim_debug_data_add_vector(dd, hairmat[3], hairmat[1], 0,1,0, "particle matrix", 333, i);
BKE_sim_debug_data_add_vector(dd, hairmat[3], hairmat[2], 0,0,1, "particle matrix", 444, i);
}
if (edit_point) {
for (k=0, key=pa->hair, tkey=tpa->hair, ekey = edit_point->keys; k<tpa->totkey; k++, key++, tkey++, ekey++) {
const float *co_orig = from_world_space ? key->co : key->world_co;
add_v3_v3v3(tkey->co, co_orig, offset);
if (!to_world_space)
mul_m4_v3(imat, tkey->co);
ekey->flag |= PEK_USE_WCO;
}
edit_point++;
}
else {
for (k=0, key=pa->hair, tkey=tpa->hair; k<tpa->totkey; k++, key++, tkey++) {
const float *co_orig = from_world_space ? key->co : key->world_co;
add_v3_v3v3(tkey->co, co_orig, offset);
if (!to_world_space)
mul_m4_v3(imat, tkey->co);
}
} }
} }
} }
@ -749,15 +803,30 @@ static bool connect_hair(Scene *scene, Object *ob, ParticleSystem *psys)
free_bvhtree_from_mesh(&bvhtree); free_bvhtree_from_mesh(&bvhtree);
dm->release(dm); dm->release(dm);
psys_free_path_cache(psys, psys->edit); psys_free_path_cache(target_psys, target_edit);
psys->flag &= ~PSYS_GLOBAL_HAIR; PE_update_object(scene, target_ob, 0);
PE_update_object(scene, ob, 0);
return true; return true;
} }
static bool connect_hair(Scene *scene, Object *ob, ParticleSystem *psys)
{
ParticleSystemModifierData *psmd;
const bool from_global = psys->flag & PSYS_GLOBAL_HAIR;
const bool to_global = false;
if (!psys)
return false;
psmd = psys_get_modifier(ob, psys);
if (!psmd->dm)
return false;
psys->flag &= ~PSYS_GLOBAL_HAIR;
return remap_hair_emitter(scene, ob, psys, ob, psmd->dm, psys, psys->edit, from_global, to_global);
}
static int connect_hair_exec(bContext *C, wmOperator *op) static int connect_hair_exec(bContext *C, wmOperator *op)
{ {
Scene *scene= CTX_data_scene(C); Scene *scene= CTX_data_scene(C);
@ -805,3 +874,188 @@ void PARTICLE_OT_connect_hair(wmOperatorType *ot)
RNA_def_boolean(ot->srna, "all", 0, "All hair", "Connect all hair systems to the emitter mesh"); RNA_def_boolean(ot->srna, "all", 0, "All hair", "Connect all hair systems to the emitter mesh");
} }
/************************ particle system copy operator *********************/
static bool find_object_pair(bContext *C, ReportList *reports, Object **r_ob_to, Object **r_ob_from)
{
ListBase selected;
CollectionPointerLink *link;
Object *ob_from, *ob_to;
ob_to = CTX_data_active_object(C);
if (!ob_to) {
BKE_report(reports, RPT_ERROR, "No active object");
return false;
}
ob_from = NULL;
CTX_data_selected_objects(C, &selected);
for (link = selected.first; link; link = link->next) {
Object *ob = link->ptr.data;
if (ob != ob_to) {
if (ob_from)
break; /* indicates multiple selected objects */
else
ob_from = ob;
}
}
BLI_freelistN(&selected);
if (!ob_from) {
BKE_report(reports, RPT_ERROR, "No non-active selected object");
return false;
}
else if (link) {
BKE_report(reports, RPT_ERROR, "Multiple non-active selected objects");
return false;
}
*r_ob_to = ob_to;
*r_ob_from = ob_from;
return true;
}
static void copy_particle_edit(Scene *scene, Object *ob, ParticleSystem *psys, ParticleSystem *psys_from)
{
PTCacheEdit *edit_from = psys_from->edit, *edit;
ParticleData *pa;
KEY_K;
POINT_P;
if (!edit_from)
return;
edit = MEM_dupallocN(edit_from);
edit->psys = psys;
psys->edit = edit;
edit->pathcache = NULL;
BLI_listbase_clear(&edit->pathcachebufs);
edit->emitter_field = NULL;
edit->emitter_cosnos = NULL;
BLI_listbase_clear(&edit->undo);
edit->curundo = NULL;
edit->points = MEM_dupallocN(edit_from->points);
pa = psys->particles;
LOOP_POINTS {
HairKey *hkey = pa->hair;
point->keys= MEM_dupallocN(point->keys);
LOOP_KEYS {
key->co = hkey->co;
key->time = &hkey->time;
key->flag = hkey->editflag;
if (!(psys->flag & PSYS_GLOBAL_HAIR)) {
key->flag |= PEK_USE_WCO;
hkey->editflag |= PEK_USE_WCO;
}
hkey++;
}
pa++;
}
update_world_cos(ob, edit);
UI_GetThemeColor3ubv(TH_EDGE_SELECT, edit->sel_col);
UI_GetThemeColor3ubv(TH_WIRE, edit->nosel_col);
recalc_lengths(edit);
recalc_emitter_field(ob, psys);
PE_update_object(scene, ob, true);
PTCacheUndo_clear(edit);
PE_undo_push(scene, "Original");
}
static int copy_particle_systems_exec(bContext *C, wmOperator *op)
{
Scene *scene = CTX_data_scene(C);
Object *ob_from, *ob_to;
ParticleSystem *psys, *psys_from;
ModifierData *md, *md_next;
DerivedMesh *final_dm;
int i;
if (!find_object_pair(C, op->reports, &ob_to, &ob_from))
return OPERATOR_CANCELLED;
if (BLI_listbase_is_empty(&ob_from->particlesystem)) {
BKE_reportf(op->reports, RPT_ERROR, "Object %200s has no particle systems", ob_from->id.name+2);
return OPERATOR_CANCELLED;
}
/* XXX in theory it could be nice to not delete existing particle systems,
* but the current code for copying assumes that the target object list is empty ...
*/
for (md = ob_to->modifiers.first; md; md = md_next) {
md_next = md->next;
/* remove all particle system modifiers as well,
* these need to sync to the particle system list
*/
if (ELEM(md->type, eModifierType_ParticleSystem, eModifierType_DynamicPaint, eModifierType_Smoke)) {
BLI_remlink(&ob_to->modifiers, md);
modifier_free(md);
}
}
BKE_object_free_particlesystems(ob_to);
/* For remapping we need a valid DM.
* Because the modifier is appended at the end it's safe to use
* the final DM of the object without particles
*/
if (ob_to->derivedFinal)
final_dm = ob_to->derivedFinal;
else
final_dm = mesh_get_derived_final(scene, ob_to, CD_MASK_BAREMESH);
BKE_object_copy_particlesystems(ob_to, ob_from);
for (psys = ob_to->particlesystem.first, psys_from = ob_from->particlesystem.first, i = 0;
psys;
psys = psys->next, psys_from = psys_from->next, ++i) {
ParticleSystemModifierData *psmd;
/* add a particle system modifier for each system */
md = modifier_new(eModifierType_ParticleSystem);
psmd = (ParticleSystemModifierData *)md;
/* push on top of the stack, no use trying to reproduce old stack order */
BLI_addtail(&ob_to->modifiers, md);
BLI_snprintf(md->name, sizeof(md->name), "ParticleSystem %i", i);
modifier_unique_name(&ob_to->modifiers, (ModifierData *)psmd);
psmd->psys = psys;
psmd->dm = CDDM_copy(final_dm);
CDDM_calc_normals(psmd->dm);
if (psys_from->edit)
copy_particle_edit(scene, ob_to, psys, psys_from);
remap_hair_emitter(scene, ob_from, psys_from, ob_to, psmd->dm, psys, psys->edit, false, false);
/* tag for recalc */
psys->recalc |= PSYS_RECALC_RESET;
}
DAG_id_tag_update(&ob_to->id, OB_RECALC_DATA);
WM_main_add_notifier(NC_OBJECT | ND_PARTICLE | NA_EDITED, ob_to);
return OPERATOR_FINISHED;
}
void PARTICLE_OT_copy_particle_systems(wmOperatorType *ot)
{
ot->name = "Copy Particle Systems";
ot->description = "Copy particle systems to the active object";
ot->idname = "PARTICLE_OT_copy_particle_systems";
ot->exec = copy_particle_systems_exec;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}

@ -72,6 +72,7 @@ void PARTICLE_OT_target_move_up(struct wmOperatorType *ot);
void PARTICLE_OT_target_move_down(struct wmOperatorType *ot); void PARTICLE_OT_target_move_down(struct wmOperatorType *ot);
void PARTICLE_OT_connect_hair(struct wmOperatorType *ot); void PARTICLE_OT_connect_hair(struct wmOperatorType *ot);
void PARTICLE_OT_disconnect_hair(struct wmOperatorType *ot); void PARTICLE_OT_disconnect_hair(struct wmOperatorType *ot);
void PARTICLE_OT_copy_particle_systems(struct wmOperatorType *ot);
void PARTICLE_OT_dupliob_copy(struct wmOperatorType *ot); void PARTICLE_OT_dupliob_copy(struct wmOperatorType *ot);
void PARTICLE_OT_dupliob_remove(struct wmOperatorType *ot); void PARTICLE_OT_dupliob_remove(struct wmOperatorType *ot);

@ -80,6 +80,7 @@ static void operatortypes_particle(void)
WM_operatortype_append(PARTICLE_OT_target_move_down); WM_operatortype_append(PARTICLE_OT_target_move_down);
WM_operatortype_append(PARTICLE_OT_connect_hair); WM_operatortype_append(PARTICLE_OT_connect_hair);
WM_operatortype_append(PARTICLE_OT_disconnect_hair); WM_operatortype_append(PARTICLE_OT_disconnect_hair);
WM_operatortype_append(PARTICLE_OT_copy_particle_systems);
WM_operatortype_append(PARTICLE_OT_dupliob_copy); WM_operatortype_append(PARTICLE_OT_dupliob_copy);
WM_operatortype_append(PARTICLE_OT_dupliob_remove); WM_operatortype_append(PARTICLE_OT_dupliob_remove);