"""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