The Class ParticleSystem in RhinoCommon's Problem or bug

I using python to create an custom particlesystem extend from ParticleSystem in Rhino,
when I using Remove method to remove the particle in my custom particlesystem when the particle was exipre, However it not work, and the size of particle in particleSystem was not decrease . the python code is follow:

#self is reference to custom particle
    def Update(self):
        for p in self:
            if not IsDie():
                p.Update()
            else:
# it not work , the number of particle in particlesystem not decrease
                self.Remove(p)

and also, when i visit particle’s ParentSystem, It return Null

It’s super hard to tell without seeing the entire script, since it could be a scope issue.

Hi @diff-arch ,the code is post here, the propblem of the code is under MyParticleSystem 's
Update method, thanks

from scriptcontext import doc
import System
import Rhino
from Rhino import *
import random
import rhinoscriptsyntax as rs
import Eto.Forms
import math


BOUNDBOX = Geometry.Box(Geometry.BoundingBox(Geometry.Point3d(-5000,-5000,-5000),Geometry.Point3d(5000,5000,5000)))
class DrawMeshConduit(Rhino.Display.DisplayConduit):
    def __init__(self,particleSystem):
        self.particleSystem = particleSystem;
        self.IsReflesh = True
        self.Enabled = True
    def UpdateParticle(self):
        while self.IsReflesh:
            try:
                self.particleSystem = self.particleSystem.Update()
                if len(list(self.particleSystem.GetEnumerator())) == 0:
                    self.Enable = False
                    raise Exception("Run complete")                    
            except Exception as ex:
                 self.Enable = False
                 self.IsReflesh = False
                 rs.Redraw()
            rs.Sleep(10)
            rs.Redraw()        
    def CalculateBoundingBox(self,e):
          min = Geometry.Point3d(-10000,-10000,-10000)
          max = Geometry.Point3d(10000,10000,10000)
          box = Geometry.BoundingBox(min,max)
          e.IncludeBoundingBox(box)
    def PreDrawObjects(self, drawEventArgs):
        bm = System.Drawing.Bitmap("D:\pic.jpg");
        dbm = Rhino.Display.DisplayBitmap(bm);
        drawEventArgs.Display.DrawParticles(self.particleSystem,dbm)
        drawEventArgs.Display.DrawBox(BOUNDBOX,System.Drawing.Color.Blue)
        
class MyParticleSystem(Rhino.Geometry.ParticleSystem):
    def __init__(self,startPt):
        self.pt = startPt
    def Init(self):
        self.Clear()
        for i in range(0,2000):
            random.seed = System.DateTime.Millisecond
            live = 50000
            size = 6.0
            G = 255 if i%255>255 else i%255
            B = 255 if (i*2)%255>255 else (i*2)%255
            color = System.Drawing.Color.FromArgb(255,255,G,B)
            speed = random.randint(100,500)
            direction = Geometry.Vector3d(random.randint(-10,10),random.randint(-10,10),random.randint(-10,10))
            direction.Unitize()
            p = MyParticle(self.pt,color,speed,live,direction,size)
            self.Add(p) 
    def Reset(self):
        self.Init()   
    def Update(self):
        
        #because the Remove method  not useful, so i must to crate an new particlesystem to return
        tempSys = MyParticleSystem(self.pt)
        for p in self:
            if not p.IsDie():
                parent = p.ParentSystem # will get null
                parent.Remove(p) # not work
                p.Update()
                tempSys.Add(p)
        self.Clear()
        return tempSys
                
class MyParticle(Rhino.Geometry.Particle):
    def __init__(self,location,color,speed,live,direction,size):
        self.Speed = speed
        self.live = live
        self.Color = color
        self.Location = location
        self.createTime = System.DateTime.Now
        self.direction = direction
        self.Size = size
    def Update(self):
        x = self.Location.X + self.direction.X*self.Speed
        y = self.Location.Y + self.direction.Y*self.Speed
        z = self.Location.Z + self.direction.Z*self.Speed    
        self.Location = Geometry.Point3d(x,y,z)  
    def IsDie(self):
        if (System.DateTime.Now - self.createTime).TotalMilliseconds > self.live:
            return True
        else:
            return False 
def Run():
    ps = MyParticleSystem(Geometry.Point3d(0,0,0))
    ps.Init()
    rs.Sleep(100)
    conduit = DrawMeshConduit(ps)
    conduit.UpdateParticle()
if __name__ == "__main__":
    Run()

Try this instead:

import Rhino.Geometry as rg
import random


class Particle:
    def __init__(self, position, direction, acceleration, lifetime, size, color):
        self.position = position
        self.direction = direction
        self.acceleration = acceleration
        self.lifetime = lifetime
        self.size = size
        self.color = color
    
    def update(self):
        self.position += self.direction * self.acceleration
        lifetime -= 1
    
    def is_dead(self):
        if self.lifetime > 0:
            return True
        return False


class ParticleSystem:
    def __init__(self, start_pt):
        self.particles = []
        self.setup(start_pt)
    
    def setup(self, start_pt):
        for i in xrange(0, 2000):
            direction = rg.Vector3d(
                random.randint(-10,10), 
                random.randint(-10,10), 
                random.randint(-10,10)
            )
            direction.Unitize()
            color = (
                255, 
                255, 
                255 if i % 255 > 255 else i % 255, 
                255 if (i * 2) % 255 > 255 else (i * 2) % 255
            )  # ARGB
            p = Particle(
                start_pt, 
                direction, 
                random.randint(100, 500),
                5000,
                6,
                color
            )
            self.add(p)

    def update(self):
        for i in xrange(len(self.particles)-1, -1, -1):
            if not self.particles[i].is_dead():
                self.particles[i].update()
                continue
            self.particles.pop(i)
                
    
    def add(self, particle):
        self.particles.append(particle)
    
    def clear(self):
        self.particles = []
    
    def is_empty(self):
        return len(self.particles) == 0

    
if __name__ == "__main__":
    ps = ParticleSystem(rg.Point3d.Origin)
    print ps.is_empty()
    
    for i in range(5000):
        ps.update()
    
    print ps.is_empty()

The code actually can work well, But In your code, you create an new Particle and ParticleSystem Class instead inherit from RhinoCommon’s, thus, some feature in RhinoCommon such as DrawParticle in DisplayClass will not work for particle in your code. @stevebaer, @dale

I see, but you could probably just add a method that returns a Rhino.Geometry.ParticleSystem from the particles property of the Pythonic ParticleSystem class. In terms of performance, this won’t be much worse than instantiating a new particle system, each time you merely want to remove a dead member.

You could probably also adapt the display pipeline to render the custom particles, but I haven’t attempted something like this in Rhino before.
@AndersDeleuran, I remember you posting custom adaptations of the DrawConduit some time ago. Do you think it would be possible to render a custom, Pythonic list of particle, instead of the Rhino.Geometry.ParticleSystem, like OP does?

Rhino.Geometry.ParticleSystem is just so limited and its documentation seems incomplete. I experimented with it a while back, and never got a satisfying result.
I think one issue is inheritance, since you’re not inheriting from a Python class but effectively from a C# struct or class from the API. I found things to be very unpredictable at best.

1 Like

Yeap, your are right,I have search Rhinocommon 's source code on Github,and the code is here rhino3dm/rhinosdkdisplay.cs at b8fc13e509002792149a1d8672952ce12e0f4be3 · mcneel/rhino3dm (github.com)). that actually an bug of RhinoCommon’s ParticalSystem and Particle。 If Create Custom ParticleSystem and not inherit from RhinoCommon, The only one question is How to let Rhino Draw Particle in hight perference

I think so, I’ve not implemented it, but there’s the DisplayPipeline.DrawParticles method, which be implemented in GHPython using the draw overrides in SDK mode:

1 Like

Here’s the revised code, where each Particle instance also keeps a Rhino.Geometry.Particle updated. And the ParticleSystem instance can now return a Rhino.Geometry.ParticleSystem if any particles are still alive.

import Rhino.Geometry as rg
from System.Drawing import Color
import random


class Particle:
    def __init__(self, position, direction, acceleration, lifetime, size, color):
        self.position = position
        self.direction = direction
        self.acceleration = acceleration
        self.lifetime = lifetime
        self.size = size
        self.color = color

        self.__api_particle = rg.Particle()
        self.__api_particle.Location = position
        self.__api_particle.Size = size
        self.__api_particle.Color = color
    
    def update(self):
        self.position += self.direction * self.acceleration
        self.__api_particle.Location = self.position
        lifetime -= 1
    
    def is_dead(self):
        if self.lifetime > 0:
            return True
        return False
	
    def get_api_particle(self):
        return self.__api_particle


class ParticleSystem:
    def __init__(self, start_pt):
        self.particles = []
        self.setup(start_pt)
    
    def setup(self, start_pt):
        for i in xrange(0, 2000):
            direction = rg.Vector3d(
                random.randint(-10,10), 
                random.randint(-10,10), 
                random.randint(-10,10)
            )
            direction.Unitize()
            color = Color.FromArgb(
                255, 
                255, 
                255 if i % 255 > 255 else i % 255, 
                255 if (i * 2) % 255 > 255 else (i * 2) % 255
            )
            p = Particle(
                start_pt, 
                direction, 
                random.randint(100, 500),
                5000,
                6,
                color
            )
            self.add(p)

    def update(self):
        for i in xrange(len(self.particles)-1, -1, -1):
            if not self.particles[i].is_dead():
                self.particles[i].update()
                continue
            self.particles.pop(i)
    
    def add(self, particle):
        self.particles.append(particle)
    
    def clear(self):
        self.particles = []
    
    def is_empty(self):
        return len(self.particles) == 0
	
    def get_api_particle_system(self):
        if self.is_empty():
            return None
        api_particle_system = rg.ParticleSystem()
        for particle in self.particles:
            api_particle = particle.get_api_particle()
            if api_particle:
                api_particle_system.Add(api_particle)
        return api_particle_system
		

if __name__ == "__main__":
    ps = ParticleSystem(rg.Point3d.Origin)
    print ps.get_api_particle_system()
    
    for i in range(5000):
        ps.update()

    print ps.get_api_particle_system()