Source code for simvx.editor.panels.inspector_sections._particle_section

"""ParticlePreviewSection -- emit/reset controls + emission shape preview.

Registered with the section registry via @register_inspector_section at
import time.
"""

from simvx.core import (
    Button,
    Control,
    GPUParticles2D,
    GPUParticles3D,
    HBoxContainer,
    Label,
    Node,
    Vec2,
)
from simvx.core.particles import ParticleEmitter
from simvx.core.ui.theme import get_theme

from ._base import (
    InspectorSection,
    _font_size,
    _row_h,
    register_inspector_section,
)

_SHAPE_SIZE = 60.0
_SHAPE_BG = (0.08, 0.08, 0.12, 1.0)
_SHAPE_BORDER = (0.35, 0.45, 0.6, 1.0)

class _EmissionShapePreview(Control):
    """60x60 widget that draws a schematic outline of the emission shape."""

    _draw_caching = False

    def __init__(self, particle_node: Node, **kwargs):
        super().__init__(**kwargs)
        self._node = particle_node
        self.size = Vec2(_SHAPE_SIZE, _SHAPE_SIZE)

    def on_draw(self, renderer):
        import math

        x, y, w, h = self.get_global_rect()
        theme = get_theme()
        outline = getattr(theme, "accent", _SHAPE_BORDER) if theme else _SHAPE_BORDER

        # Background
        renderer.draw_rect((x, y), (w, h), colour=_SHAPE_BG, filled=True)
        renderer.draw_rect((x, y), (w, h), colour=_SHAPE_BORDER)

        cx, cy = x + w / 2, y + h / 2
        shape = str(getattr(self._node, "emission_shape", "point"))

        if shape == "sphere":
            radius = float(getattr(self._node, "emission_radius", 1.0))
            # Scale to fit inside the preview (max radius maps to ~25px)
            r = min(max(radius * 5.0, 4.0), 25.0)
            # Approximate circle with line segments
            segs = 24
            for i in range(segs):
                a0 = 2 * math.pi * i / segs
                a1 = 2 * math.pi * (i + 1) / segs
                x0 = cx + r * math.cos(a0)
                y0 = cy + r * math.sin(a0)
                x1 = cx + r * math.cos(a1)
                y1 = cy + r * math.sin(a1)
                renderer.draw_line((x0, y0), (x1, y1), colour=outline)
        elif shape == "box":
            box = getattr(self._node, "emission_box", (1.0, 1.0, 1.0))
            bx = float(box[0]) if hasattr(box, "__getitem__") else float(box)
            by = float(box[1]) if hasattr(box, "__getitem__") and len(box) > 1 else bx
            # Scale to fit (max half-extent maps to ~25px)
            scale = min(25.0 / max(bx, by, 0.01), 25.0)
            rw = max(bx * scale * 2, 4.0)
            rh = max(by * scale * 2, 4.0)
            renderer.draw_rect((cx - rw / 2, cy - rh / 2), (rw, rh), colour=outline)
        else:
            # Point -- small filled dot
            dot_r = 3.0
            renderer.draw_rect((cx - dot_r, cy - dot_r), (dot_r * 2, dot_r * 2), colour=outline, filled=True)

_PARTICLE_TYPES = (ParticleEmitter, GPUParticles2D, GPUParticles3D)

[docs] @register_inspector_section class ParticlePreviewSection(InspectorSection): """Compact control bar and emission shape preview for particle emitter nodes.""" section_title = "Particle Controls" priority = 3
[docs] def can_handle(self, node): return isinstance(node, _PARTICLE_TYPES)
[docs] def handled_properties(self, node): return set()
[docs] def build_rows(self, node, ctx): rows: list[Control] = [] # -- Control bar: Emit | Reset | particle count label -- bar = HBoxContainer() bar.separation = 4 bar.size = Vec2(250, _row_h()) emit_btn = Button("Emit") emit_btn.size = Vec2(50, _row_h()) emit_btn.font_size = _font_size() def _do_emit(c=ctx, n=node): old = n.emitting c.on_callable_command( lambda: setattr(n, "emitting", True), lambda: setattr(n, "emitting", old), description=f"Start {n.name} emitting", ) emit_btn.pressed.connect(_do_emit) bar.add_child(emit_btn) ctx.register_widget("particle_emit", emit_btn) reset_btn = Button("Reset") reset_btn.size = Vec2(50, _row_h()) reset_btn.font_size = _font_size() def _do_reset(c=ctx, n=node): old = n.emitting c.on_callable_command( lambda: setattr(n, "emitting", False), lambda: setattr(n, "emitting", old), description=f"Stop {n.name} emitting", ) reset_btn.pressed.connect(_do_reset) bar.add_child(reset_btn) ctx.register_widget("particle_reset", reset_btn) # Particle count (read-only) count_label = Label("") count_label.font_size = _font_size() count_label.size = Vec2(100, _row_h()) if hasattr(node, "alive_count"): count_label.text = f"Alive: {node.alive_count}" elif hasattr(node, "amount"): count_label.text = f"Max: {node.amount}" bar.add_child(count_label) ctx.register_widget("particle_count", count_label) rows.append(bar) # -- Emission shape preview -- shape_label = Label("Emission Shape") shape_label.font_size = _font_size() shape_label.size = Vec2(120, _row_h()) rows.append(shape_label) preview = _EmissionShapePreview(node) rows.append(preview) ctx.register_widget("particle_shape_preview", preview) return rows