Skip to main content

mittens_engine/scripting/
component_registry.rs

1use crate::engine::ecs::SignalEmitter;
2use crate::engine::ecs::component::AssetPayloadComponent;
3use crate::engine::ecs::component::style::VerticalAlign;
4/// Component registry: maps MMS type names to engine component constructors.
5///
6/// This is the bridge between a `MaterializedCE` (fully-evaluated on the MMS thread)
7/// and live engine components (created on the main thread).
8///
9/// `spawn_tree` is the only public entry point. It creates the component from the
10/// ctor info, applies builder calls, named assignments, and positionals, then
11/// recurses into children.
12use crate::engine::ecs::component::{
13    ActionComponent, AlignItems, AmbientLightComponent, AnimationComponent, AnimationState,
14    AudioClipComponent, AudioOscillator, AudioOscillatorComponent, AudioOutputComponent,
15    AudioTriggerMode, AvatarBodyYawComponent, AvatarControlComponent, BackgroundColorComponent,
16    BackgroundComponent, BloomComponent, BlurPassComponent, BoundsComponent, BoxSizing,
17    Camera2DComponent, Camera3DComponent, CameraXRComponent, ClockComponent, CollisionComponent,
18    CollisionShape, CollisionShapeComponent, ColorComponent, ControllerHand, ControllerPoseKind,
19    DataComponent, DataValue, DirectionalLightComponent, Display, EdgeInsets, EditorComponent,
20    EditorInteractionMode, ElementType, EmissiveComponent, EmissivePassComponent,
21    FitBoundsComponent, FitBoundsMode, FitBoundsTarget, FlexDirection, FlexWrap, GLTFComponent,
22    GestureCoordTypeComponent, GravityComponent, GridComponent, HtmlElementComponent,
23    HttpClientComponent, HttpServerComponent, IKChainComponent, IKSolver, InputComponent,
24    InputTransformModeComponent, InputXRComponent, InputXRGamepadComponent, InspectLayoutComponent,
25    JustifyContent, KeyframeComponent, KineticResponseComponent, LayoutBoundsComponent,
26    LayoutComponent, LightQuantizationComponent, MeshComponent, MirrorComponent, MusicNote,
27    MusicNoteComponent, NormalVisualisationComponent, OpacityComponent, OptionComponent,
28    OscillatorType, Overflow, OverlayComponent, PointLightComponent, PointerComponent,
29    PointerEvents, PoseCaptureComponent, PoseCaptureLibraryComponent, PoseCapturePoseComponent,
30    Position, QuatTemporalFilterComponent, QuatYawFollowComponent, RayCastComponent,
31    RaycastableComponent, RaycastableShapeComponent, RaycastableShapeType, RenderGraphComponent,
32    RenderableComponent, RendererSettingsComponent, RendererStatsComponent, RouterComponent,
33    ScrollingComponent, SecondaryMotionComponent, SelectableComponent, SelectionComponent,
34    SerializeComponent, SignalObserverRouterComponent, SignalRouteUpwardComponent, SizeDimension,
35    SkinnedMeshComponent, SpringBoneComponent, SpringJointComponent, StencilClipComponent,
36    StyleComponent, TextAlign, TextComponent, TextInputComponent, TextShadowComponent,
37    TextureComponent, TextureFilteringComponent, TransformCameraSpecificComponent,
38    TransformComponent, TransformDropComponent, TransformForkTRSComponent, TransformGizmoAxis,
39    TransformGizmoComponent, TransformGizmoCoordSpace, TransformGizmoRotateComponent,
40    TransformGizmoScaleComponent, TransformGizmoTranslateComponent, TransformMapRotationComponent,
41    TransformMapScaleComponent, TransformMapTranslationComponent, TransformMergeTRSComponent,
42    TransformParentComponent, TransformSampleAncestorComponent, TransitionComponent,
43    TransitionEasing, TransitionReplacePolicy, TransparentCutoutComponent, UVComponent,
44    Vector3TemporalFilterComponent, WordWrapMode, XRHandComponent, XrComponent, XrHandPreference,
45};
46use crate::engine::ecs::{ComponentId, World};
47use crate::engine::graphics::CameraTarget;
48use crate::engine::graphics::bounds::Aabb;
49use crate::engine::graphics::render_assets::RenderAssets;
50use crate::scripting::ast::{
51    BlockStatement, ComponentExpression, Expression, Ident, Statement, UnaryOpKind,
52};
53use crate::scripting::object::{CeChild, MaterializedCE, Value};
54use crate::scripting::token::expand_component_shortform;
55use std::cell::RefCell;
56
57thread_local! {
58    static LIVE_RENDER_ASSETS: RefCell<Option<*mut RenderAssets>> = const { RefCell::new(None) };
59}
60
61pub fn with_live_render_assets<R>(render_assets: &mut RenderAssets, f: impl FnOnce() -> R) -> R {
62    LIVE_RENDER_ASSETS.with(|slot| {
63        let prev = slot.replace(Some(render_assets as *mut RenderAssets));
64        let result = f();
65        let _ = slot.replace(prev);
66        result
67    })
68}
69
70pub const SUPPORTED_COMPONENT_NAMES: &[&str] = &[
71    "Action",
72    "AmbientLight",
73    "Animation",
74    "AssetPayload",
75    "AudioClip",
76    "AudioOscillator",
77    "AudioOutput",
78    "AvatarBodyYaw",
79    "AvatarControl",
80    "Background",
81    "BackgroundColor",
82    "Bloom",
83    "BlurPass",
84    "Bounds",
85    "Camera2D",
86    "Camera3D",
87    "CameraXR",
88    "Clock",
89    "Collision",
90    "CollisionShape",
91    "Color",
92    "Data",
93    "DirectionalLight",
94    "Editor",
95    "Emissive",
96    "EmissivePass",
97    "FitBounds",
98    "GLTF",
99    "GestureCoordType",
100    "Gravity",
101    "Grid",
102    "HtmlElement",
103    "HttpClient",
104    "HttpServer",
105    "IKChain",
106    "Input",
107    "InputTransformMode",
108    "InputXR",
109    "InputXRGamepad",
110    "InspectLayout",
111    "Keyframe",
112    "KineticResponse",
113    "LayoutBounds",
114    "LayoutRoot",
115    "LightQuantization",
116    "Mesh",
117    "Mirror",
118    "MusicNote",
119    "NormalVis",
120    "ObserverRouter",
121    "Opacity",
122    "Option",
123    "Overlay",
124    "PointLight",
125    "Pointer",
126    "PoseCapture",
127    "PoseCaptureLibrary",
128    "PoseCapturePose",
129    "QuatTemporalFilter",
130    "QuatYawFollow",
131    "Raycast",
132    "Raycastable",
133    "RaycastableShape",
134    "RenderGraph",
135    "Renderable",
136    "RendererSettings",
137    "RendererStats",
138    "Router",
139    "Scrolling",
140    "SecondaryMotion",
141    "Selectable",
142    "Selection",
143    "Serialize",
144    "SignalRouteUpward",
145    "SkinnedMesh",
146    "SpringBone",
147    "SpringJoint",
148    "StencilClip",
149    "Style",
150    "Text",
151    "TextInput",
152    "TextShadow",
153    "Texture",
154    "TextureFiltering",
155    "Transform",
156    "TransformCameraSpecific",
157    "TransformDrop",
158    "TransformForkTRS",
159    "TransformGizmo",
160    "TransformGizmoRotate",
161    "TransformGizmoScale",
162    "TransformGizmoTranslate",
163    "TransformMapRotation",
164    "TransformMapScale",
165    "TransformMapTranslation",
166    "TransformMergeTRS",
167    "TransformParent",
168    "TransformSampleAncestor",
169    "Transition",
170    "TransparentCutout",
171    "UV",
172    "Vector3TemporalFilter",
173    "XR",
174    "XRHand",
175];
176
177fn with_render_assets_mut<R>(
178    f: impl FnOnce(&mut RenderAssets) -> Result<R, String>,
179) -> Result<R, String> {
180    LIVE_RENDER_ASSETS.with(|slot| {
181        let ptr = (*slot.borrow()).ok_or_else(|| {
182            "procedural Renderable constructors require live RenderAssets".to_string()
183        })?;
184        // SAFETY: callers install the pointer for the duration of spawn/materialization on the
185        // main thread, and no nested aliasing mutable access to the same RenderAssets occurs.
186        unsafe { f(&mut *ptr) }
187    })
188}
189
190// ---------------------------------------------------------------------------
191// Public entry point
192// ---------------------------------------------------------------------------
193
194/// Recursively spawn the component tree described by `ce`, attach it to `parent` (if any),
195/// and initialise it. Returns the root `ComponentId`.
196pub fn spawn_tree(
197    ce: &MaterializedCE,
198    parent: Option<ComponentId>,
199    world: &mut World,
200    emit: &mut dyn SignalEmitter,
201) -> Result<ComponentId, String> {
202    let type_name = resolve_type_name(&ce.component_type);
203    let id = create_component(world, &type_name, ce.ctor_method.as_deref(), &ce.ctor_args)?;
204
205    if let Some(block) = &ce.deferred_block {
206        if let Some(keyframe) = world.get_component_by_id_as_mut::<KeyframeComponent>(id) {
207            keyframe.callback = Some(block.clone());
208        }
209    }
210
211    // Extra ctor calls + body builder calls (already evaluated).
212    for (method, args) in &ce.calls {
213        apply_call(world, id, method, args)?;
214    }
215
216    // Named property assignments — intercept node-level fields first.
217    for (prop, val) in &ce.named {
218        match prop.as_str() {
219            "name" | "id" => {
220                if let Some(node) = world.get_component_record_mut(id) {
221                    node.name = val_as_str(val).unwrap_or("").to_string();
222                }
223            }
224            "guid" => apply_guid_named_prop(world, id, val)?,
225            "class" => {
226                if let Some(node) = world.get_component_record_mut(id) {
227                    match val {
228                        Value::String(s) => {
229                            node.classes = s.split_whitespace().map(str::to_string).collect();
230                        }
231                        Value::Array(arr) => {
232                            node.classes = arr
233                                .iter()
234                                .filter_map(|v| {
235                                    if let Value::String(s) = v {
236                                        Some(s.clone())
237                                    } else {
238                                        None
239                                    }
240                                })
241                                .collect();
242                        }
243                        _ => {}
244                    }
245                }
246            }
247            _ => apply_named_assignment(world, id, prop, val)?,
248        }
249    }
250
251    // Positional content (strings etc).
252    for val in &ce.positionals {
253        apply_positional(world, id, val)?;
254    }
255
256    // Attach to parent before recursing into children.
257    if let Some(p) = parent {
258        if let Err(e) = world.add_child(p, id) {
259            return Err(format!("attach failed: {e}"));
260        }
261    }
262
263    // Recurse into children. Spawn-children create fresh subtrees; Attach-children
264    // splice an already-Registered (detached, uninitialised) subtree into place.
265    for child in &ce.children {
266        match child {
267            CeChild::Spawn(child_ce) => {
268                spawn_tree(child_ce, Some(id), world, emit)?;
269            }
270            CeChild::Attach(existing_id) => {
271                if let Err(e) = world.add_child(id, *existing_id) {
272                    return Err(format!(
273                        "attach existing child {:?} failed: {e}",
274                        existing_id
275                    ));
276                }
277                // No init here: the init walk below covers the whole subtree.
278            }
279        }
280    }
281
282    // Initialise tree (if parent is already initialised, or this is a new root).
283    let parent_initialised = parent.map(|p| world.is_initialized(p)).unwrap_or(false);
284    if parent.is_none() || parent_initialised {
285        world.init_component_tree(id, emit);
286    }
287
288    Ok(id)
289}
290
291/// Like `spawn_tree`, but does **not** attach to a parent and does **not**
292/// run `init_component_tree`. The resulting subtree exists in the `World`'s
293/// component slotmap as a detached, uninitialised tree, addressable by the
294/// returned `ComponentId`.
295///
296/// Used by `HostCallKind::Register` so that `let x = CE` can produce a live
297/// `ComponentId` without committing the tree to the live world graph yet.
298/// A later `Attach` HostCall (or splice as a `CeChild::Attach` inside another
299/// CE body) places it and runs init.
300pub fn spawn_tree_uninitialized(
301    ce: &MaterializedCE,
302    world: &mut World,
303    emit: &mut dyn SignalEmitter,
304) -> Result<ComponentId, String> {
305    let type_name = resolve_type_name(&ce.component_type);
306    let id = create_component(world, &type_name, ce.ctor_method.as_deref(), &ce.ctor_args)?;
307
308    if let Some(block) = &ce.deferred_block {
309        if let Some(keyframe) = world.get_component_by_id_as_mut::<KeyframeComponent>(id) {
310            keyframe.callback = Some(block.clone());
311        }
312    }
313
314    for (method, args) in &ce.calls {
315        apply_call(world, id, method, args)?;
316    }
317
318    for (prop, val) in &ce.named {
319        match prop.as_str() {
320            "name" | "id" => {
321                if let Some(node) = world.get_component_record_mut(id) {
322                    node.name = val_as_str(val).unwrap_or("").to_string();
323                }
324            }
325            "guid" => apply_guid_named_prop(world, id, val)?,
326            "class" => {
327                if let Some(node) = world.get_component_record_mut(id) {
328                    match val {
329                        Value::String(s) => {
330                            node.classes = s.split_whitespace().map(str::to_string).collect();
331                        }
332                        Value::Array(arr) => {
333                            node.classes = arr
334                                .iter()
335                                .filter_map(|v| {
336                                    if let Value::String(s) = v {
337                                        Some(s.clone())
338                                    } else {
339                                        None
340                                    }
341                                })
342                                .collect();
343                        }
344                        _ => {}
345                    }
346                }
347            }
348            _ => apply_named_assignment(world, id, prop, val)?,
349        }
350    }
351
352    for val in &ce.positionals {
353        apply_positional(world, id, val)?;
354    }
355
356    // Children: spawn-children become children of this uninitialised parent
357    // (still uninitialised); attach-children splice in an already-Registered
358    // subtree without init.
359    for child in &ce.children {
360        match child {
361            CeChild::Spawn(child_ce) => {
362                let child_id = spawn_tree_uninitialized(child_ce, world, emit)?;
363                if let Err(e) = world.add_child(id, child_id) {
364                    return Err(format!("attach uninit child failed: {e}"));
365                }
366            }
367            CeChild::Attach(existing_id) => {
368                if let Err(e) = world.add_child(id, *existing_id) {
369                    return Err(format!(
370                        "attach existing child {:?} failed: {e}",
371                        existing_id
372                    ));
373                }
374            }
375        }
376    }
377
378    Ok(id)
379}
380
381// ---------------------------------------------------------------------------
382// Type name resolution
383// ---------------------------------------------------------------------------
384
385/// Resolve a raw type identifier to the canonical PascalCase name used by
386/// `create_component`.
387///
388/// Accepts these forms:
389/// - Shortform: `"T"` -> `"Transform"` (from `COMPONENT_SHORTFORMS`).
390/// - PascalCase: `"Transform"` -> `"Transform"` (already canonical).
391/// - snake_case: `"transform"` -> `"Transform"` (from `Component::name()`).
392/// - Loose case-insensitive match against canonical names: handles acronyms
393///   that don't survive a mechanical snake_to_pascal (`"camera3d"` ->
394///   `"Camera3D"`, `"gltf"` -> `"GLTF"`).
395fn resolve_type_name(raw: &str) -> String {
396    if let Some(full) = expand_component_shortform(raw) {
397        return full.to_string();
398    }
399    if raw
400        .chars()
401        .next()
402        .map(|c| c.is_uppercase())
403        .unwrap_or(false)
404    {
405        return raw.to_string();
406    }
407    let stripped: String = raw.chars().filter(|c| *c != '_').collect();
408    let lowered = stripped.to_lowercase();
409    for entry in crate::scripting::token::COMPONENT_SHORTFORMS {
410        if entry.full.to_lowercase() == lowered {
411            return entry.full.to_string();
412        }
413    }
414    snake_to_pascal(raw)
415}
416
417// ---------------------------------------------------------------------------
418// Subtree → AST encoding (the reverse direction of `spawn_tree`)
419// ---------------------------------------------------------------------------
420
421/// Walk a live component subtree and emit a `ComponentExpression` AST node
422/// (root + nested children in the body). Each component contributes its own
423/// `to_mms_ast()` for its header; children are appended as
424/// `Statement::Expression(Expression::Component(...))` in the body.
425///
426/// Used by:
427/// - `attach_clone` intent: subtree → CE → MaterializedCE → spawn_tree (clone).
428/// - REPL `dump` and scene save: subtree → CE → unparser → text.
429pub fn subtree_to_ce_ast(world: &World, root: ComponentId) -> Result<ComponentExpression, String> {
430    subtree_to_ce_ast_limited(world, root, usize::MAX)
431}
432
433pub fn filtered_world_to_ce_ast(world: &World) -> Result<Vec<ComponentExpression>, String> {
434    let roots: Vec<ComponentId> = world
435        .all_components()
436        .filter(|&cid| world.parent_of(cid).is_none())
437        .collect();
438    filtered_roots_to_ce_ast(world, &roots)
439}
440
441pub fn filtered_world_root_ids(world: &World) -> Vec<ComponentId> {
442    let roots: Vec<ComponentId> = world
443        .all_components()
444        .filter(|&cid| world.parent_of(cid).is_none())
445        .collect();
446    filtered_root_ids_for_roots(world, &roots)
447}
448
449pub fn filtered_roots_to_ce_ast(
450    world: &World,
451    roots: &[ComponentId],
452) -> Result<Vec<ComponentExpression>, String> {
453    let mut referenced_guids: std::collections::HashSet<uuid::Uuid> =
454        std::collections::HashSet::new();
455    for &root in roots {
456        collect_referenced_guids_filtered(world, root, &mut referenced_guids);
457    }
458
459    let mut out = Vec::new();
460    for &root in roots {
461        out.extend(filtered_ce_ast_inner(world, root, &referenced_guids)?);
462    }
463    Ok(out)
464}
465
466pub fn filtered_root_ids_for_roots(world: &World, roots: &[ComponentId]) -> Vec<ComponentId> {
467    let mut out = Vec::new();
468    for &root in roots {
469        collect_filtered_root_ids(world, root, &mut out);
470    }
471    out
472}
473
474fn immediate_child_serialize_override(world: &World, node: ComponentId) -> Option<bool> {
475    world.children_of(node).iter().find_map(|&child| {
476        world
477            .get_component_by_id_as::<SerializeComponent>(child)
478            .map(|serialize| serialize.enabled)
479    })
480}
481
482fn nearest_serialize_override(world: &World, node: ComponentId) -> Option<bool> {
483    let mut current = Some(node);
484    while let Some(component_id) = current {
485        if let Some(enabled) = immediate_child_serialize_override(world, component_id) {
486            return Some(enabled);
487        }
488        current = world.parent_of(component_id);
489    }
490    None
491}
492
493fn filtered_save_visibility(world: &World, node: ComponentId) -> bool {
494    nearest_serialize_override(world, node).unwrap_or(true)
495}
496
497fn collect_filtered_root_ids(world: &World, node: ComponentId, out: &mut Vec<ComponentId>) {
498    let visible = filtered_save_visibility(world, node);
499    if visible {
500        out.push(node);
501        return;
502    }
503
504    let children: Vec<ComponentId> = world.children_of(node).to_vec();
505    for child in children {
506        collect_filtered_root_ids(world, child, out);
507    }
508}
509
510fn collect_referenced_guids_filtered(
511    world: &World,
512    node: ComponentId,
513    out: &mut std::collections::HashSet<uuid::Uuid>,
514) {
515    use crate::engine::ecs::component::{
516        ActionComponent, ComponentRef, IKChainComponent, TransformParentComponent,
517    };
518
519    let visible = filtered_save_visibility(world, node);
520    if visible {
521        if let Some(action) = world.get_component_by_id_as::<ActionComponent>(node) {
522            for src in &action.target_sources {
523                if let ComponentRef::Guid(u) = src {
524                    out.insert(*u);
525                }
526            }
527        }
528        if let Some(ik) = world.get_component_by_id_as::<IKChainComponent>(node) {
529            for src in [&ik.target_source, &ik.end_effector_source]
530                .iter()
531                .copied()
532                .flatten()
533            {
534                if let ComponentRef::Guid(u) = src {
535                    out.insert(*u);
536                }
537            }
538        }
539        if let Some(tp) = world.get_component_by_id_as::<TransformParentComponent>(node) {
540            for src in [&tp.target_source, &tp.root_source]
541                .iter()
542                .copied()
543                .flatten()
544            {
545                if let ComponentRef::Guid(u) = src {
546                    out.insert(*u);
547                }
548            }
549        }
550    }
551
552    let children: Vec<ComponentId> = world.children_of(node).to_vec();
553    for child in children {
554        collect_referenced_guids_filtered(world, child, out);
555    }
556}
557
558fn filtered_ce_ast_inner(
559    world: &World,
560    root: ComponentId,
561    referenced_guids: &std::collections::HashSet<uuid::Uuid>,
562) -> Result<Vec<ComponentExpression>, String> {
563    let node = world
564        .get_component_record(root)
565        .ok_or_else(|| format!("filtered_ce_ast_inner: missing component {root:?}"))?;
566    let visible = filtered_save_visibility(world, root);
567
568    let mut child_components = Vec::new();
569    for &child_id in &node.children {
570        child_components.extend(filtered_ce_ast_inner(world, child_id, referenced_guids)?);
571    }
572
573    if !visible {
574        return Ok(child_components);
575    }
576
577    let mut ce = node.component.to_mms_ast(world);
578
579    if !node.name.is_empty() {
580        ce.body.statements.push(Statement::Reassign {
581            target: Expression::Identifier(crate::scripting::ast::Ident("name".to_string())),
582            value: Expression::String(node.name.clone()),
583        });
584    }
585
586    if referenced_guids.contains(&node.guid) {
587        ce.body.statements.push(Statement::Reassign {
588            target: Expression::Identifier(crate::scripting::ast::Ident("guid".to_string())),
589            value: Expression::String(node.guid.to_string()),
590        });
591    }
592
593    for child_ce in child_components {
594        ce.body
595            .statements
596            .push(Statement::Expression(Expression::Component(child_ce)));
597    }
598
599    Ok(vec![ce])
600}
601
602pub fn subtree_to_ce_ast_limited(
603    world: &World,
604    root: ComponentId,
605    max_depth: usize,
606) -> Result<ComponentExpression, String> {
607    // First pass: collect every GUID referenced by any ActionComponent in
608    // the subtree via `ComponentRef::Guid`. These are the targets that
609    // need their GUID preserved across save/load so the dumped
610    // `@uuid:<g>` selector still resolves on reload.
611    let mut referenced_guids: std::collections::HashSet<uuid::Uuid> =
612        std::collections::HashSet::new();
613    collect_referenced_guids_limited(world, root, 0, max_depth, &mut referenced_guids);
614
615    subtree_to_ce_ast_inner_limited(world, root, &referenced_guids, 0, max_depth)
616}
617
618fn collect_referenced_guids_limited(
619    world: &World,
620    node: ComponentId,
621    depth: usize,
622    max_depth: usize,
623    out: &mut std::collections::HashSet<uuid::Uuid>,
624) {
625    use crate::engine::ecs::component::{
626        ActionComponent, ComponentRef, IKChainComponent, TransformParentComponent,
627    };
628    if let Some(action) = world.get_component_by_id_as::<ActionComponent>(node) {
629        for src in &action.target_sources {
630            if let ComponentRef::Guid(u) = src {
631                out.insert(*u);
632            }
633        }
634    }
635    if let Some(ik) = world.get_component_by_id_as::<IKChainComponent>(node) {
636        for src in [&ik.target_source, &ik.end_effector_source]
637            .iter()
638            .copied()
639            .flatten()
640        {
641            if let ComponentRef::Guid(u) = src {
642                out.insert(*u);
643            }
644        }
645    }
646    if let Some(tp) = world.get_component_by_id_as::<TransformParentComponent>(node) {
647        for src in [&tp.target_source, &tp.root_source]
648            .iter()
649            .copied()
650            .flatten()
651        {
652            if let ComponentRef::Guid(u) = src {
653                out.insert(*u);
654            }
655        }
656    }
657    let children: Vec<ComponentId> = world
658        .get_component_record(node)
659        .map(|n| n.children.clone())
660        .unwrap_or_default();
661    if depth >= max_depth {
662        return;
663    }
664    for child in children {
665        collect_referenced_guids_limited(world, child, depth + 1, max_depth, out);
666    }
667}
668
669fn subtree_to_ce_ast_inner_limited(
670    world: &World,
671    root: ComponentId,
672    referenced_guids: &std::collections::HashSet<uuid::Uuid>,
673    depth: usize,
674    max_depth: usize,
675) -> Result<ComponentExpression, String> {
676    let node = world
677        .get_component_record(root)
678        .ok_or_else(|| format!("subtree_to_ce_ast: missing component {root:?}"))?;
679    let mut ce = node.component.to_mms_ast(world);
680
681    // Preserve `name` if the author set one. `name` lets `#name`
682    // selectors keep resolving across reload; it is independent of
683    // `guid` (which serves `@uuid:` selectors). Both are emitted when
684    // both apply.
685    if !node.name.is_empty() {
686        ce.body.statements.push(Statement::Reassign {
687            target: Expression::Identifier(crate::scripting::ast::Ident("name".to_string())),
688            value: Expression::String(node.name.clone()),
689        });
690    }
691
692    // Preserve `guid` whenever this component is referenced by some
693    // Action via `ComponentRef::Guid`. The dumped `@uuid:<g>` selector
694    // can only resolve on reload if `spawn_tree` restores the same GUID.
695    // Whether `name` is also set is irrelevant — names don't help
696    // `@uuid:` lookups.
697    if referenced_guids.contains(&node.guid) {
698        ce.body.statements.push(Statement::Reassign {
699            target: Expression::Identifier(crate::scripting::ast::Ident("guid".to_string())),
700            value: Expression::String(node.guid.to_string()),
701        });
702    }
703
704    let children: Vec<ComponentId> = node.children.clone();
705    if depth >= max_depth {
706        return Ok(ce);
707    }
708    for child_id in children {
709        let child_ce = subtree_to_ce_ast_inner_limited(
710            world,
711            child_id,
712            referenced_guids,
713            depth + 1,
714            max_depth,
715        )?;
716        ce.body
717            .statements
718            .push(Statement::Expression(Expression::Component(child_ce)));
719    }
720    Ok(ce)
721}
722
723/// Convert a *ground* `ComponentExpression` (literals only — no binops,
724/// no function calls in args other than nested component expressions) into
725/// a `MaterializedCE` that `spawn_tree` consumes. This is the synchronous
726/// bridge between `to_mms_ast` output and the live spawn path; it does not
727/// involve the MMS evaluator thread.
728pub fn ce_ast_to_materialized(ce: &ComponentExpression) -> Result<MaterializedCE, String> {
729    let component_property_assignment_only =
730        component_expr_uses_property_assignment_only(&ce.component_type.0);
731    let is_keyframe = matches!(ce.component_type.0.as_str(), "KF" | "Keyframe");
732    let mut ctor_method: Option<String> = None;
733    let mut ctor_args: Vec<Value> = Vec::new();
734    let mut calls: Vec<(String, Vec<Value>)> = Vec::new();
735
736    let mut ctor_iter = ce.constructors.iter();
737    if let Some(first) = ctor_iter.next() {
738        ctor_method = Some(first.method.0.clone());
739        ctor_args = first
740            .args
741            .iter()
742            .map(expression_to_value)
743            .collect::<Result<_, _>>()?;
744    }
745    for ctor in ctor_iter {
746        let args: Vec<Value> = ctor
747            .args
748            .iter()
749            .map(expression_to_value)
750            .collect::<Result<_, _>>()?;
751        calls.push((ctor.method.0.clone(), args));
752    }
753
754    if is_keyframe {
755        return Ok(MaterializedCE {
756            component_type: ce.component_type.0.clone(),
757            component_property_assignment_only,
758            ctor_method,
759            ctor_args,
760            calls,
761            named: Vec::new(),
762            positionals: Vec::new(),
763            deferred_block: Some(crate::scripting::object::RuntimeClosure {
764                body: ce.body.clone(),
765                captured_env: std::sync::Arc::new(std::collections::HashMap::new()),
766                heap: crate::scripting::object::HeapHandle::new(),
767                analysis: Some(
768                    crate::scripting::block_effect_analyzer::BlockEffectAnalyzer::analyze_keyframe_block(&ce.body),
769                ),
770            }),
771            children: Vec::new(),
772        });
773    }
774
775    let mut children: Vec<CeChild> = Vec::new();
776    let mut named: Vec<(String, Value)> = Vec::new();
777    for stmt in &ce.body.statements {
778        match stmt {
779            Statement::Expression(Expression::Component(child_ce)) => {
780                children.push(CeChild::Spawn(ce_ast_to_materialized(child_ce)?));
781            }
782            Statement::Expression(Expression::Call(c)) => {
783                // Body builder call, e.g. `scale(0.5, 0.5, 0.5)`.
784                if let Expression::Identifier(Ident(name)) = &*c.callee {
785                    let args: Vec<Value> = c
786                        .args
787                        .iter()
788                        .map(expression_to_value)
789                        .collect::<Result<_, _>>()?;
790                    calls.push((name.clone(), args));
791                }
792            }
793            Statement::Reassign { target, value } => {
794                let Expression::Identifier(name) = target else {
795                    continue;
796                };
797                if component_property_assignment_only || is_universal_component_named_prop(&name.0)
798                {
799                    // Named-prop in a property-bag CE body, e.g. `row_name = "hero"`.
800                    // The full evaluator handles this via builder.named.push in
801                    // evaluator.rs; replicate the same mapping here so the
802                    // ground-CE dump path preserves named props on round-trip.
803                    let val = expression_to_value(value)?;
804                    named.push((name.0.clone(), val));
805                }
806            }
807            _ => {
808                // Skip other statement kinds (control flow, lets) —
809                // `to_mms_ast` impls should not emit these. If one slips
810                // through, we drop it rather than fail the clone.
811            }
812        }
813    }
814
815    Ok(MaterializedCE {
816        component_type: ce.component_type.0.clone(),
817        component_property_assignment_only,
818        ctor_method,
819        ctor_args,
820        calls,
821        named,
822        positionals: Vec::new(),
823        deferred_block: None,
824        children,
825    })
826}
827
828pub fn component_expr_uses_property_assignment_only(raw: &str) -> bool {
829    matches!(
830        resolve_type_name(raw).as_str(),
831        "Data" | "DataComponent" | "Style" | "StyleComponent"
832    )
833}
834
835pub fn is_universal_component_named_prop(name: &str) -> bool {
836    matches!(name, "name" | "id" | "guid" | "class")
837}
838
839fn expression_to_value(e: &Expression) -> Result<Value, String> {
840    match e {
841        Expression::Number(n) => Ok(Value::Number(*n)),
842        Expression::String(s) => Ok(Value::String(s.clone())),
843        Expression::Bool(b) => Ok(Value::Bool(*b)),
844        Expression::Null => Ok(Value::Null),
845        Expression::Dimension(n, u) => Ok(Value::Dimension {
846            value: *n,
847            unit: *u,
848        }),
849        Expression::Identifier(Ident(s)) => Ok(Value::Identifier(s.clone())),
850        Expression::Array(items) => {
851            let vals: Vec<Value> = items
852                .iter()
853                .map(expression_to_value)
854                .collect::<Result<_, _>>()?;
855            Ok(Value::Array(vals))
856        }
857        Expression::Table(_) => {
858            Err("expression_to_value: table literals are not supported yet".into())
859        }
860        Expression::Index { base, index } => {
861            let base = expression_to_value(base)?;
862            let index = expression_to_value(index)?;
863            let Value::Array(items) = base else {
864                return Err(format!("index: expected array, got {:?}", base));
865            };
866            let Value::Number(n) = index else {
867                return Err(format!("index: expected numeric index, got {:?}", index));
868            };
869            if n.fract() != 0.0 || n < 0.0 {
870                return Err(format!("index: expected non-negative integer, got {n}"));
871            }
872            items
873                .get(n as usize)
874                .cloned()
875                .ok_or_else(|| format!("index: {n} out of bounds for array of {}", items.len()))
876        }
877        Expression::UnaryOp {
878            op: UnaryOpKind::Neg,
879            operand,
880        } => match expression_to_value(operand)? {
881            Value::Number(n) => Ok(Value::Number(-n)),
882            Value::Dimension { value, unit } => Ok(Value::Dimension {
883                value: -value,
884                unit,
885            }),
886            v => Err(format!("cannot negate value: {v:?}")),
887        },
888        Expression::Component(child_ce) => {
889            let m = ce_ast_to_materialized(child_ce)?;
890            Ok(Value::ComponentExpr(Box::new(m)))
891        }
892        other => Err(format!(
893            "expression_to_value: unsupported expression {other:?}"
894        )),
895    }
896}
897
898// Silence unused-import warnings if a builder cycle removes the only consumer.
899#[allow(dead_code)]
900fn _ce_ast_helpers_used(_: BlockStatement) {}
901
902/// Convert `snake_case` (the form returned by `Component::name()`) to the
903/// `PascalCase` form recognized by `create_component`.
904///
905/// `"transform"` -> `"Transform"`, `"audio_output"` -> `"AudioOutput"`,
906/// `"camera_3d"` -> `"Camera3d"` (note: GLTF uses caps, see fallback below).
907fn snake_to_pascal(s: &str) -> String {
908    let mut out = String::with_capacity(s.len());
909    let mut capitalize_next = true;
910    for ch in s.chars() {
911        if ch == '_' {
912            capitalize_next = true;
913        } else if capitalize_next {
914            out.extend(ch.to_uppercase());
915            capitalize_next = false;
916        } else {
917            out.push(ch);
918        }
919    }
920    out
921}
922
923// ---------------------------------------------------------------------------
924// Value conversion helpers
925// ---------------------------------------------------------------------------
926
927fn val_as_f32(v: &Value) -> Result<f32, String> {
928    match v {
929        Value::Number(n) => Ok(*n as f32),
930        other => Err(format!("expected number, got {other:?}")),
931    }
932}
933
934fn val_as_world_f32(v: &Value) -> Result<f32, String> {
935    use crate::scripting::token::Unit;
936    match v {
937        Value::Number(n) => Ok(*n as f32),
938        Value::Dimension {
939            value,
940            unit: Unit::WorldUnits,
941        } => Ok(*value as f32),
942        Value::Dimension { unit, .. } => {
943            Err(format!("expected number or wu dimension, got {:?}", unit))
944        }
945        other => Err(format!("expected number or wu dimension, got {other:?}")),
946    }
947}
948
949fn val_as_bool(v: &Value) -> Result<bool, String> {
950    match v {
951        Value::Bool(b) => Ok(*b),
952        other => Err(format!("expected bool, got {other:?}")),
953    }
954}
955
956fn val_as_str(v: &Value) -> Result<&str, String> {
957    match v {
958        Value::String(s) => Ok(s.as_str()),
959        Value::Identifier(s) => Ok(s.as_str()),
960        other => Err(format!("expected string/ident, got {other:?}")),
961    }
962}
963
964fn val_as_str_vec(v: &Value) -> Result<Vec<String>, String> {
965    match v {
966        Value::Array(items) => items
967            .iter()
968            .map(|it| val_as_str(it).map(|s| s.to_string()))
969            .collect(),
970        Value::String(s) | Value::Identifier(s) => Ok(vec![s.clone()]),
971        other => Err(format!("expected string/array, got {other:?}")),
972    }
973}
974
975fn val_as_f32_array<const N: usize>(v: &Value) -> Result<[f32; N], String> {
976    match v {
977        Value::Array(items) => {
978            if items.len() != N {
979                return Err(format!("expected array of {N}, got {}", items.len()));
980            }
981            let mut out = [0.0f32; N];
982            for (i, item) in items.iter().enumerate() {
983                out[i] = val_as_f32(item)?;
984            }
985            Ok(out)
986        }
987        other => Err(format!("expected array, got {other:?}")),
988    }
989}
990
991// ---------------------------------------------------------------------------
992// Argument helpers
993// ---------------------------------------------------------------------------
994
995fn arg(args: &[Value], i: usize) -> Result<&Value, String> {
996    args.get(i)
997        .ok_or_else(|| format!("expected at least {} arg(s), got {}", i + 1, args.len()))
998}
999
1000fn arg_f32(args: &[Value], i: usize) -> Result<f32, String> {
1001    val_as_f32(arg(args, i)?)
1002}
1003fn arg_u32(args: &[Value], i: usize) -> Result<u32, String> {
1004    match arg(args, i)? {
1005        Value::Number(n) if *n >= 0.0 && *n <= u32::MAX as f64 && n.fract() == 0.0 => Ok(*n as u32),
1006        other => Err(format!(
1007            "arg {i}: expected non-negative integer, got {other:?}"
1008        )),
1009    }
1010}
1011fn arg_world_f32(args: &[Value], i: usize) -> Result<f32, String> {
1012    val_as_world_f32(arg(args, i)?)
1013}
1014fn arg_bool(args: &[Value], i: usize) -> Result<bool, String> {
1015    val_as_bool(arg(args, i)?)
1016}
1017fn arg_str(args: &[Value], i: usize) -> Result<&str, String> {
1018    val_as_str(arg(args, i)?)
1019}
1020fn arg_f32_arr<const N: usize>(args: &[Value], i: usize) -> Result<[f32; N], String> {
1021    val_as_f32_array(arg(args, i)?)
1022}
1023fn arg_str_vec(args: &[Value], i: usize) -> Result<Vec<String>, String> {
1024    val_as_str_vec(arg(args, i)?)
1025}
1026
1027/// Produce an `ComponentRef` (authoring metadata) from the i-th arg.
1028///
1029/// Mapping:
1030/// - `Value::ComponentObject { id, .. }` → `Guid(world.guid_of(id))`. The
1031///   live handle case collapses to a guid so dump emits `@uuid:<g>` and
1032///   runtime resolution uses the O(1) guid index instead of a selector walk.
1033/// - String starting `@uuid:<hex>` → `Guid(parsed)`. Pre-parsing here means
1034///   runtime resolution skips selector parsing entirely for the common
1035///   guid-reference case.
1036/// - Any other string / identifier → `Query(s)` verbatim.
1037pub(crate) fn arg_component_ref(
1038    world: &World,
1039    args: &[Value],
1040    i: usize,
1041) -> Result<crate::engine::ecs::component::ComponentRef, String> {
1042    value_to_component_ref(world, arg(args, i)?)
1043}
1044
1045/// Vec form of `arg_component_ref`. Mixed handle / string elements OK.
1046pub(crate) fn arg_component_ref_vec(
1047    world: &World,
1048    args: &[Value],
1049    i: usize,
1050) -> Result<Vec<crate::engine::ecs::component::ComponentRef>, String> {
1051    match arg(args, i)? {
1052        Value::Array(items) => items
1053            .iter()
1054            .map(|v| value_to_component_ref(world, v))
1055            .collect(),
1056        other => value_to_component_ref(world, other).map(|t| vec![t]),
1057    }
1058}
1059
1060/// Handle `guid = "8c4f3e72-..."` on a component CE. Replaces the freshly
1061/// minted GUID with the authored one so `@uuid:` selectors saved against
1062/// this component still resolve across save/load.
1063fn apply_guid_named_prop(world: &mut World, id: ComponentId, val: &Value) -> Result<(), String> {
1064    let s = val_as_str(val).map_err(|e| format!("guid prop: {e}"))?;
1065    let parsed =
1066        uuid::Uuid::parse_str(s).map_err(|e| format!("guid prop: invalid uuid '{s}': {e}"))?;
1067    world.set_component_guid(id, parsed)
1068}
1069
1070/// Best-effort resolution of an `ComponentRef` to a ComponentId at
1071/// registry-call time. Returns `None` when the referent doesn't exist
1072/// yet — caller is expected to leave the resolved id as a null sentinel
1073/// and have a later system pass fill it in (e.g. the AnimationSystem
1074/// resolution path for Action; AvatarControlSystem for IKChain).
1075pub(crate) fn resolve_component_ref(
1076    world: &World,
1077    src: &crate::engine::ecs::component::ComponentRef,
1078) -> Option<ComponentId> {
1079    use crate::engine::ecs::component::ComponentRef;
1080    match src {
1081        ComponentRef::Guid(uuid) => world.component_id_by_guid(*uuid),
1082        ComponentRef::Query(selector) => {
1083            let roots: Vec<ComponentId> = world
1084                .all_components()
1085                .filter(|&cid| world.parent_of(cid).is_none())
1086                .collect();
1087            roots
1088                .into_iter()
1089                .find_map(|root| world.find_component(root, selector))
1090        }
1091    }
1092}
1093
1094fn value_to_component_ref(
1095    world: &World,
1096    v: &Value,
1097) -> Result<crate::engine::ecs::component::ComponentRef, String> {
1098    use crate::engine::ecs::component::ComponentRef;
1099    match v {
1100        Value::ComponentObject { id, .. } => {
1101            let guid = world
1102                .get_component_record(*id)
1103                .map(|n| n.guid)
1104                .ok_or_else(|| format!("component handle {id:?} not found in world"))?;
1105            Ok(ComponentRef::Guid(guid))
1106        }
1107        Value::String(s) | Value::Identifier(s) => {
1108            if let Some(hex) = s.strip_prefix("@uuid:") {
1109                let uuid = uuid::Uuid::parse_str(hex)
1110                    .map_err(|e| format!("invalid uuid in '@uuid:{hex}': {e}"))?;
1111                Ok(ComponentRef::Guid(uuid))
1112            } else {
1113                Ok(ComponentRef::Query(s.clone()))
1114            }
1115        }
1116        other => Err(format!(
1117            "expected component handle or selector string, got {other:?}"
1118        )),
1119    }
1120}
1121
1122fn parse_gizmo_axis(ctor: Option<&str>) -> TransformGizmoAxis {
1123    match ctor {
1124        Some("x") | Some("X") => TransformGizmoAxis::X,
1125        Some("y") | Some("Y") => TransformGizmoAxis::Y,
1126        Some("z") | Some("Z") => TransformGizmoAxis::Z,
1127        _ => TransformGizmoAxis::X,
1128    }
1129}
1130
1131/// Accept either a unit-literal (`50%`, `20gu`, `0.08wu`) or a bare number
1132/// (interpreted as glyph units) and produce a `SizeDimension`. Used by Style
1133/// sizing setters.
1134fn arg_size_dimension(args: &[Value], i: usize) -> Result<SizeDimension, String> {
1135    use crate::scripting::token::Unit;
1136    match arg(args, i)? {
1137        Value::Number(n) => Ok(SizeDimension::GlyphUnits(*n as f32)),
1138        Value::Dimension { value, unit } => match unit {
1139            Unit::Percent => Ok(SizeDimension::Percent(*value as f32)),
1140            Unit::GlyphUnits => Ok(SizeDimension::GlyphUnits(*value as f32)),
1141            Unit::WorldUnits => Ok(SizeDimension::WorldUnits(*value as f32)),
1142            Unit::Degrees | Unit::Radians => Err(format!(
1143                "expected length unit (gu, wu, %) for size, got {:?}",
1144                unit
1145            )),
1146        },
1147        v => Err(format!(
1148            "expected number or dimension for size, got {:?}",
1149            v
1150        )),
1151    }
1152}
1153
1154fn arg_layout_length(args: &[Value], i: usize) -> Result<SizeDimension, String> {
1155    match arg_size_dimension(args, i)? {
1156        SizeDimension::GlyphUnits(v) => Ok(SizeDimension::GlyphUnits(v)),
1157        SizeDimension::WorldUnits(v) => Ok(SizeDimension::WorldUnits(v)),
1158        SizeDimension::Percent(_) => {
1159            Err("expected gu or wu length for LayoutRoot size, got percent".into())
1160        }
1161        SizeDimension::Auto => Err("expected gu or wu length for LayoutRoot size, got auto".into()),
1162    }
1163}
1164
1165// ---------------------------------------------------------------------------
1166// Component creation
1167// ---------------------------------------------------------------------------
1168
1169fn create_component(
1170    world: &mut World,
1171    type_name: &str,
1172    ctor: Option<&str>,
1173    args: &[Value],
1174) -> Result<ComponentId, String> {
1175    macro_rules! add {
1176        ($component:expr) => {
1177            Ok(world.add_component($component))
1178        };
1179    }
1180
1181    match type_name {
1182        "Transform" => {
1183            let mut c = TransformComponent::new();
1184            if let Some(method) = ctor {
1185                c = apply_transform_builder(c, method, args)?;
1186            }
1187            add!(c)
1188        }
1189        "FitBounds" => {
1190            let mut c = FitBoundsComponent::new();
1191            if let Some(method) = ctor {
1192                apply_fit_bounds_ctor(&mut c, method, args)?;
1193            }
1194            add!(c)
1195        }
1196        "LayoutBounds" => {
1197            let mut c = LayoutBoundsComponent::new(
1198                crate::engine::graphics::bounds::Aabb {
1199                    min: [0.0, 0.0, 0.0],
1200                    max: [0.0, 0.0, 0.0],
1201                },
1202                crate::engine::graphics::bounds::Aabb {
1203                    min: [0.0, 0.0, 0.0],
1204                    max: [0.0, 0.0, 0.0],
1205                },
1206            );
1207            if let Some(method) = ctor {
1208                apply_layout_bounds_ctor(&mut c, method, args)?;
1209            }
1210            add!(c)
1211        }
1212        "Color" => match ctor {
1213            Some("rgba") => add!(ColorComponent::rgba(
1214                arg_f32(args, 0)?,
1215                arg_f32(args, 1)?,
1216                arg_f32(args, 2)?,
1217                arg_f32(args, 3)?
1218            )),
1219            _ => add!(ColorComponent::new()),
1220        },
1221        "Renderable" => match ctor {
1222            Some("cube") => add!(RenderableComponent::cube()),
1223            Some("wireframe_box") => with_render_assets_mut(|render_assets| {
1224                let thickness = arg_f32(args, 0).unwrap_or(0.02);
1225                Ok(world
1226                    .add_component(RenderableComponent::wireframe_box(render_assets, thickness)))
1227            }),
1228            Some("circle2d") => add!(RenderableComponent::circle2d()),
1229            Some("cone") => {
1230                if args.is_empty() {
1231                    add!(RenderableComponent::cone())
1232                } else {
1233                    with_render_assets_mut(|render_assets| {
1234                        let segments = arg_u32(args, 0).unwrap_or(32);
1235                        Ok(world.add_component(RenderableComponent::cone_dynamic(
1236                            render_assets,
1237                            segments,
1238                        )))
1239                    })
1240                }
1241            }
1242            Some("icosahedron") => with_render_assets_mut(|render_assets| {
1243                let tessellations = arg_u32(args, 0).unwrap_or(0);
1244                let sphericalness = arg_f32(args, 1).unwrap_or(0.0);
1245                Ok(world.add_component(RenderableComponent::icosahedron(
1246                    render_assets,
1247                    tessellations,
1248                    sphericalness,
1249                )))
1250            }),
1251            Some("sphere") => add!(RenderableComponent::sphere()),
1252            Some("triangle") => add!(RenderableComponent::triangle()),
1253            Some("square") => add!(RenderableComponent::square()),
1254            Some("plane") => add!(RenderableComponent::plane()),
1255            Some("tetrahedron") => add!(RenderableComponent::tetrahedron()),
1256            Some("partial_annulus_2d") => with_render_assets_mut(|render_assets| {
1257                let inner_radius = arg_f32(args, 0).unwrap_or(0.28);
1258                let outer_radius = arg_f32(args, 1).unwrap_or(0.52);
1259                let start_angle = arg_f32(args, 2).unwrap_or(0.0);
1260                let end_angle = arg_f32(args, 3).unwrap_or(4.71239);
1261                let segments = arg_u32(args, 4).unwrap_or(64);
1262                Ok(world.add_component(RenderableComponent::partial_annulus_2d(
1263                    render_assets,
1264                    inner_radius,
1265                    outer_radius,
1266                    start_angle,
1267                    end_angle,
1268                    segments,
1269                )))
1270            }),
1271            Some("star") => with_render_assets_mut(|render_assets| {
1272                let points = arg_u32(args, 0).unwrap_or(5);
1273                let inner_radius = arg_f32(args, 1).unwrap_or(0.45);
1274                let skip = arg_u32(args, 2).unwrap_or(2);
1275                let phase = arg_u32(args, 3).unwrap_or(1);
1276                Ok(world.add_component(RenderableComponent::star(
1277                    render_assets,
1278                    points,
1279                    inner_radius,
1280                    skip,
1281                    phase,
1282                )))
1283            }),
1284            Some("heart") => with_render_assets_mut(|render_assets| {
1285                let segments = arg_u32(args, 0).unwrap_or(64);
1286                Ok(world.add_component(RenderableComponent::heart(render_assets, segments)))
1287            }),
1288            _ => Err(format!(
1289                "Renderable: unknown constructor '{}'",
1290                ctor.unwrap_or("")
1291            )),
1292        },
1293        "Grid" => {
1294            let mut c = GridComponent::default();
1295            if let Some(method) = ctor {
1296                match method {
1297                    "spacing" => c = c.with_spacing(arg_f32(args, 0)?),
1298                    "size_x" => c = c.with_size_x(arg_f32(args, 0)? as u32),
1299                    "size_z" => c = c.with_size_z(arg_f32(args, 0)? as u32),
1300                    "hidden" => c = c.with_hidden(arg_bool(args, 0)?),
1301                    _ => return Err(format!("Grid: unknown constructor '{method}'")),
1302                }
1303            }
1304            add!(c)
1305        }
1306        "HttpClient" => {
1307            let mut c = HttpClientComponent::new();
1308            if let Some(method) = ctor {
1309                match method {
1310                    "enabled" => c = c.with_enabled(arg_bool(args, 0)?),
1311                    "timeout_ms" => c = c.with_timeout_ms(arg_f32(args, 0)? as u64),
1312                    _ => return Err(format!("HttpClient: unknown constructor '{method}'")),
1313                }
1314            }
1315            add!(c)
1316        }
1317        "HttpServer" => {
1318            let mut c = HttpServerComponent::new();
1319            if let Some(method) = ctor {
1320                match method {
1321                    "bind" => c = c.with_bind_addr(arg_str(args, 0)?),
1322                    "enabled" => c = c.with_enabled(arg_bool(args, 0)?),
1323                    _ => return Err(format!("HttpServer: unknown constructor '{method}'")),
1324                }
1325            }
1326            add!(c)
1327        }
1328        "StencilClip" => {
1329            let id = world.add_component(StencilClipComponent::new());
1330            if let Some(method) = ctor {
1331                apply_call(world, id, method, args)?;
1332            }
1333            Ok(id)
1334        }
1335        "Background" => {
1336            let id = world.add_component(BackgroundComponent::new());
1337            if let Some(method) = ctor {
1338                apply_call(world, id, method, args)?;
1339            }
1340            Ok(id)
1341        }
1342        "Overlay" => add!(OverlayComponent::new()),
1343        "InspectLayout" => add!(InspectLayoutComponent::new()),
1344        "BackgroundColor" => add!(BackgroundColorComponent::new()),
1345        "AmbientLight" => match ctor {
1346            Some("rgb") => add!(AmbientLightComponent::rgb(
1347                arg_f32(args, 0)?,
1348                arg_f32(args, 1)?,
1349                arg_f32(args, 2)?
1350            )),
1351            _ => add!(AmbientLightComponent::new()),
1352        },
1353        "RenderGraph" => match ctor {
1354            Some("off") => add!(RenderGraphComponent::off()),
1355            Some("on") => add!(RenderGraphComponent::on()),
1356            _ => add!(RenderGraphComponent::new()),
1357        },
1358        "EmissivePass" => add!(EmissivePassComponent::new()),
1359        "BlurPass" => {
1360            let id = world.add_component(BlurPassComponent::new());
1361            if let Some(method) = ctor {
1362                apply_call(world, id, method, args)?;
1363            }
1364            Ok(id)
1365        }
1366        "Bloom" => {
1367            let id = world.add_component(BloomComponent::new());
1368            if let Some(method) = ctor {
1369                apply_call(world, id, method, args)?;
1370            }
1371            Ok(id)
1372        }
1373        "DirectionalLight" => {
1374            let id = world.add_component(DirectionalLightComponent::new());
1375            if let Some(method) = ctor {
1376                apply_call(world, id, method, args)?;
1377            }
1378            Ok(id)
1379        }
1380        "PointLight" => {
1381            let id = world.add_component(PointLightComponent::new());
1382            if let Some(method) = ctor {
1383                apply_call(world, id, method, args)?;
1384            }
1385            Ok(id)
1386        }
1387        "Emissive" => match ctor {
1388            Some("on") => add!(EmissiveComponent::on()),
1389            Some("off") => add!(EmissiveComponent::off()),
1390            _ => add!(EmissiveComponent::on()),
1391        },
1392        "Opacity" => {
1393            let id = world.add_component(OpacityComponent::new());
1394            if let Some(method) = ctor {
1395                apply_call(world, id, method, args)?;
1396            }
1397            Ok(id)
1398        }
1399        "Input" => {
1400            let mut c = InputComponent::new();
1401            if let Some("speed") = ctor {
1402                c = c.with_speed(arg_f32(args, 0)?);
1403            }
1404            add!(c)
1405        }
1406        "InputXR" | "InputVR" => match ctor {
1407            Some("on") => add!(InputXRComponent::on()),
1408            Some("off") => add!(InputXRComponent::off()),
1409            _ => add!(InputXRComponent::on()),
1410        },
1411        "InputXRGamepad" | "InputXrGamepad" | "InputVRGamepad" | "InputVrGamepad" => {
1412            let id = world.add_component(InputXRGamepadComponent::new());
1413            if let Some(method) = ctor {
1414                if method != "new" {
1415                    apply_call(world, id, method, args)?;
1416                }
1417            }
1418            Ok(id)
1419        }
1420        "InputTransformMode" => {
1421            let c = match ctor {
1422                Some("forward_y") => InputTransformModeComponent::forward_y(),
1423                Some("forward_z") => InputTransformModeComponent::forward_z(),
1424                _ => InputTransformModeComponent::forward_z(),
1425            };
1426            let id = world.add_component(c);
1427            // Remaining builder calls (e.g. roll_axis_y, fps_rotation) get applied
1428            // by spawn_tree's normal call-list pass via apply_call.
1429            Ok(id)
1430        }
1431        "Camera3D" => {
1432            let id = world.add_component(Camera3DComponent::new());
1433            if let Some(method) = ctor {
1434                apply_call(world, id, method, args)?;
1435            }
1436            Ok(id)
1437        }
1438        "Camera2D" => {
1439            let id = world.add_component(Camera2DComponent::new());
1440            if let Some(method) = ctor {
1441                apply_call(world, id, method, args)?;
1442            }
1443            Ok(id)
1444        }
1445        "CameraXR" => match ctor {
1446            Some("off") => add!(CameraXRComponent::off()),
1447            Some("on") => add!(CameraXRComponent::on()),
1448            _ => add!(CameraXRComponent::on()),
1449        },
1450        "Pointer" => match ctor {
1451            Some("disabled") => add!(PointerComponent::disabled()),
1452            _ => add!(PointerComponent::new()),
1453        },
1454        "XR" | "VR" | "OpenXR" => match ctor {
1455            Some("off") => add!(XrComponent::off()),
1456            Some("on") | Some("auto") | Some("openxr") => add!(XrComponent::on()),
1457            _ => add!(XrComponent::on()),
1458        },
1459        "XRHand" | "XrHand" | "VRHand" | "VrHand" => match ctor {
1460            Some("new") => {
1461                let _enabled = arg_bool(args, 0)?;
1462                let hand = match arg_str(args, 1)? {
1463                    "Left" => ControllerHand::Left,
1464                    "Right" => ControllerHand::Right,
1465                    s => return Err(format!("unknown ControllerHand: {s}")),
1466                };
1467                let pose = match arg_str(args, 2)? {
1468                    "Aim" => ControllerPoseKind::Aim,
1469                    "Grip" => ControllerPoseKind::Grip,
1470                    s => return Err(format!("unknown ControllerPoseKind: {s}")),
1471                };
1472                add!(XRHandComponent::new(true, hand, pose))
1473            }
1474            _ => Err("XRHand requires .new(enabled, hand, pose)".into()),
1475        },
1476        "TransformParent" => match ctor {
1477            Some("target") => add!(
1478                TransformParentComponent::new()
1479                    .with_target_source(arg_component_ref(world, args, 0)?)
1480            ),
1481            _ => add!(TransformParentComponent::new()),
1482        },
1483        "TransformForkTRS" => add!(TransformForkTRSComponent::new()),
1484        "TransformCameraSpecific" => add!(match ctor {
1485            Some("active_stereoscopic") => TransformCameraSpecificComponent::active_stereoscopic(),
1486            _ => TransformCameraSpecificComponent::active_monoscopic(),
1487        }),
1488        "TransformMapTranslation" => add!(TransformMapTranslationComponent::new()),
1489        "TransformMapRotation" => add!(TransformMapRotationComponent::new()),
1490        "TransformMapScale" => add!(TransformMapScaleComponent::new()),
1491        "TransformMergeTRS" => add!(TransformMergeTRSComponent::new()),
1492        "TransformDrop" => add!(TransformDropComponent::new()),
1493        "TransformSampleAncestor" => {
1494            let mut c = TransformSampleAncestorComponent::new();
1495            if let Some("skip") = ctor {
1496                c = c.with_skip(arg_f32(args, 0)? as usize);
1497            }
1498            add!(c)
1499        }
1500        "QuatTemporalFilter" => {
1501            let mut c = QuatTemporalFilterComponent::new();
1502            if let Some("smoothing_factor") = ctor {
1503                c = c.with_smoothing_factor(arg_f32(args, 0)?);
1504            }
1505            add!(c)
1506        }
1507        "GLTF" => match ctor {
1508            Some("new") => add!(GLTFComponent::new(arg_str(args, 0)?)),
1509            _ => Err("GLTF requires .new(\"uri\")".into()),
1510        },
1511        "SecondaryMotion" => add!(SecondaryMotionComponent::new()),
1512        "SpringBone" => match ctor {
1513            Some("new") => add!(SpringBoneComponent::new(arg_str(args, 0)?)),
1514            _ => Err("SpringBone requires .new(\"stable_name\")".into()),
1515        },
1516        "SpringJoint" => match ctor {
1517            Some("new") => add!(SpringJointComponent::new(arg_component_ref(
1518                world, args, 0
1519            )?)),
1520            _ => Err("SpringJoint requires .new(\"node/path[0]\")".into()),
1521        },
1522        "RendererSettings" => {
1523            let c = match ctor {
1524                Some("msaa_off") => RendererSettingsComponent::msaa_off(),
1525                _ => RendererSettingsComponent::new(),
1526            };
1527            let id = world.add_component(c);
1528            // Builder calls (e.g. `.window_size(...)`) chained after the ctor go
1529            // through apply_call in spawn_tree's call-list pass. The non-`msaa_off`
1530            // path may pass through a builder name as the first ctor; route it.
1531            if let Some(method) = ctor {
1532                if method != "msaa_off" {
1533                    apply_call(world, id, method, args)?;
1534                }
1535            }
1536            Ok(id)
1537        }
1538        "RendererStats" => {
1539            let id = world.add_component(RendererStatsComponent::new());
1540            if let Some(method) = ctor {
1541                apply_call(world, id, method, args)?;
1542            }
1543            Ok(id)
1544        }
1545        "Text" => add!(TextComponent::new("")),
1546        "TextInput" => add!(TextInputComponent::new("")),
1547        "TextShadow" => {
1548            let id = world.add_component(TextShadowComponent::new());
1549            if let Some(method) = ctor {
1550                apply_call(world, id, method, args)?;
1551            }
1552            Ok(id)
1553        }
1554        "AvatarControl" => {
1555            let id = world.add_component(AvatarControlComponent::new());
1556            if let Some(method) = ctor {
1557                apply_call(world, id, method, args)?;
1558            }
1559            Ok(id)
1560        }
1561        "Editor" => {
1562            let id = world.add_component(EditorComponent::new());
1563            if let Some(method) = ctor {
1564                apply_call(world, id, method, args)?;
1565            }
1566            Ok(id)
1567        }
1568        "AssetPayload" => {
1569            let id = world.add_component(match ctor {
1570                Some("new") => AssetPayloadComponent::new(arg_str(args, 0)?, arg_str(args, 1)?),
1571                _ => AssetPayloadComponent::new("", ""),
1572            });
1573            if let Some(method) = ctor {
1574                if method != "new" {
1575                    apply_call(world, id, method, args)?;
1576                }
1577            }
1578            Ok(id)
1579        }
1580        "Router" => {
1581            let id = world.add_component(RouterComponent::new());
1582            if let Some(method) = ctor {
1583                apply_call(world, id, method, args)?;
1584            }
1585            Ok(id)
1586        }
1587        "Selectable" => match ctor {
1588            Some("off") => add!(SelectableComponent::off()),
1589            _ => add!(SelectableComponent::on()),
1590        },
1591        "Selection" => {
1592            let id = world.add_component(match ctor {
1593                Some("multiple") => SelectionComponent::multiple(),
1594                Some("optional") => SelectionComponent::optional(),
1595                _ => SelectionComponent::new(),
1596            });
1597            if let Some(method) = ctor {
1598                if method != "multiple" && method != "optional" {
1599                    apply_call(world, id, method, args)?;
1600                }
1601            }
1602            Ok(id)
1603        }
1604        "Option" => add!(OptionComponent::new()),
1605        "ObserverRouter" => {
1606            let id = world.add_component(SignalObserverRouterComponent::new());
1607            if let Some(method) = ctor {
1608                apply_call(world, id, method, args)?;
1609            }
1610            Ok(id)
1611        }
1612        "Serialize" => match ctor {
1613            Some("off") => add!(SerializeComponent::off()),
1614            _ => add!(SerializeComponent::on()),
1615        },
1616        "Scrolling" => match ctor {
1617            Some("new") => add!(ScrollingComponent::new(
1618                arg_f32(args, 0)?,
1619                arg_f32(args, 1)?
1620            )),
1621            _ => add!(ScrollingComponent::new(0.1, 0.1)),
1622        },
1623        "HtmlElement" => {
1624            let c = match ctor {
1625                Some("div") => HtmlElementComponent::div(),
1626                Some("span") => HtmlElementComponent::span(),
1627                Some("body") => HtmlElementComponent::body(),
1628                Some("header") => HtmlElementComponent::header(),
1629                Some("p") => HtmlElementComponent::p(),
1630                Some("section") => HtmlElementComponent::new(ElementType::Section),
1631                Some("article") => HtmlElementComponent::new(ElementType::Article),
1632                Some("footer") => HtmlElementComponent::new(ElementType::Footer),
1633                Some("nav") => HtmlElementComponent::new(ElementType::Nav),
1634                Some("aside") => HtmlElementComponent::new(ElementType::Aside),
1635                Some("main") => HtmlElementComponent::new(ElementType::Main),
1636                Some("h1") => HtmlElementComponent::new(ElementType::H1),
1637                Some("h2") => HtmlElementComponent::new(ElementType::H2),
1638                Some("h3") => HtmlElementComponent::new(ElementType::H3),
1639                Some("h4") => HtmlElementComponent::new(ElementType::H4),
1640                Some("h5") => HtmlElementComponent::new(ElementType::H5),
1641                Some("h6") => HtmlElementComponent::new(ElementType::H6),
1642                _ => HtmlElementComponent::new(ElementType::Element),
1643            };
1644            add!(c)
1645        }
1646        "Style" => add!(StyleComponent::new()),
1647        "LayoutRoot" => {
1648            let mut layout = LayoutComponent::new(80.0);
1649            if !args.is_empty() {
1650                layout.set_available_width_dimension(arg_layout_length(args, 0)?);
1651            }
1652            add!(layout)
1653        }
1654        "Raycastable" => match ctor {
1655            Some("disabled") => add!(RaycastableComponent::disabled()),
1656            Some("drag_only") => add!(RaycastableComponent::drag_only()),
1657            Some("click_only") => add!(RaycastableComponent::click_only()),
1658            Some("enabled") => add!(RaycastableComponent::enabled()),
1659            _ => add!(RaycastableComponent::enabled()),
1660        },
1661        "PoseCapture" => add!(PoseCaptureComponent::new()),
1662        "PoseCaptureLibrary" => {
1663            add!(PoseCaptureLibraryComponent::new(
1664                crate::engine::ecs::component::PoseTargetRef::Query("TODO".to_string())
1665            ))
1666        }
1667        "PoseCapturePose" => {
1668            add!(PoseCapturePoseComponent::new(
1669                arg_str(args, 0)?,
1670                crate::engine::ecs::component::PoseTargetRef::Query("TODO".to_string()),
1671                Vec::new()
1672            ))
1673        }
1674        "TextureFiltering" => match ctor {
1675            Some("linear") => add!(TextureFilteringComponent::linear()),
1676            Some("nearest_magnification") => {
1677                add!(TextureFilteringComponent::nearest_magnification())
1678            }
1679            Some("nearest") => add!(TextureFilteringComponent::nearest()),
1680            _ => add!(TextureFilteringComponent::linear()),
1681        },
1682        "Texture" => match ctor {
1683            Some("render_image") => add!(TextureComponent::render_image(arg_str(args, 0)?)),
1684            Some("with_uri") | Some("uri") => add!(TextureComponent::with_uri(arg_str(args, 0)?)),
1685            Some("from_png") => add!(TextureComponent::from_png(arg_str(args, 0)?)),
1686            Some("from_dds") => add!(TextureComponent::from_dds(arg_str(args, 0)?)),
1687            _ => add!(TextureComponent::unresolved()),
1688        },
1689        "Transition" => {
1690            let id = world.add_component(TransitionComponent::new());
1691            if let Some(method) = ctor {
1692                apply_call(world, id, method, args)?;
1693            }
1694            Ok(id)
1695        }
1696        "UV" => {
1697            let id = world.add_component(UVComponent::new());
1698            if let Some(method) = ctor {
1699                apply_call(world, id, method, args)?;
1700            }
1701            Ok(id)
1702        }
1703        "Clock" => {
1704            let mut c = ClockComponent::new();
1705            if let Some("bpm") = ctor {
1706                c = c.with_bpm(arg_f32(args, 0)? as f64);
1707            }
1708            add!(c)
1709        }
1710        "Animation" => {
1711            let mut c = AnimationComponent::new();
1712            match ctor {
1713                Some("playing") => c = c.with_state(AnimationState::Playing),
1714                Some("paused") => c = c.with_state(AnimationState::Paused),
1715                Some("looping") => c = c.with_state(AnimationState::Looping),
1716                Some("length") => c = c.with_length_beats(arg_f32(args, 0)? as f64),
1717                Some("scope") => c = c.with_scope_source(arg_component_ref(world, args, 0)?),
1718                _ => {}
1719            }
1720            add!(c)
1721        }
1722        "Keyframe" => match ctor {
1723            Some("at") => add!(KeyframeComponent::new(arg_f32(args, 0)? as f64)),
1724            _ => Err("Keyframe requires .at(beat)".into()),
1725        },
1726        "Action" => {
1727            use crate::engine::ecs::IntentValue as IV;
1728            use slotmap::Key;
1729            let null_ids = |n: usize| vec![ComponentId::null(); n];
1730            match ctor {
1731                Some("noop") => add!(ActionComponent::default()),
1732                Some("print") => add!(ActionComponent::print(arg_str(args, 0)?)),
1733                Some("set_color") => {
1734                    let targets = arg_component_ref_vec(world, args, 0)?;
1735                    let rgba = arg_f32_arr::<4>(args, 1)?;
1736                    let signal = IV::SetColor {
1737                        component_ids: null_ids(targets.len()),
1738                        rgba,
1739                    };
1740                    add!(ActionComponent::new_authored(signal, targets))
1741                }
1742                Some("set_text") => {
1743                    let targets = arg_component_ref_vec(world, args, 0)?;
1744                    let text = arg_str(args, 1)?.to_string();
1745                    let signal = IV::SetText {
1746                        component_ids: null_ids(targets.len()),
1747                        text,
1748                    };
1749                    add!(ActionComponent::new_authored(signal, targets))
1750                }
1751                Some("set_emissive_intensity") => {
1752                    let targets = arg_component_ref_vec(world, args, 0)?;
1753                    let intensity = arg_f32(args, 1)?.max(0.0);
1754                    let signal = IV::SetEmissiveIntensity {
1755                        component_ids: null_ids(targets.len()),
1756                        intensity,
1757                    };
1758                    add!(ActionComponent::new_authored(signal, targets))
1759                }
1760                Some("set_position") => {
1761                    let targets = arg_component_ref_vec(world, args, 0)?;
1762                    let position = arg_f32_arr::<3>(args, 1)?;
1763                    let signal = IV::SetPosition {
1764                        component_ids: null_ids(targets.len()),
1765                        position,
1766                    };
1767                    add!(ActionComponent::new_authored(signal, targets))
1768                }
1769                Some("attach") => {
1770                    let parents = arg_component_ref_vec(world, args, 0)?;
1771                    let child = arg_component_ref(world, args, 1)?;
1772                    let mut sources = parents.clone();
1773                    sources.push(child);
1774                    let signal = IV::Attach {
1775                        parents: null_ids(parents.len()),
1776                        child: ComponentId::null(),
1777                    };
1778                    add!(ActionComponent::new_authored(signal, sources))
1779                }
1780                Some("attach_clone") => {
1781                    let parents = arg_component_ref_vec(world, args, 0)?;
1782                    let prefab = arg_component_ref(world, args, 1)?;
1783                    let mut sources = parents.clone();
1784                    sources.push(prefab);
1785                    let signal = IV::AttachClone {
1786                        parents: null_ids(parents.len()),
1787                        prefab_root: ComponentId::null(),
1788                    };
1789                    add!(ActionComponent::new_authored(signal, sources))
1790                }
1791                Some("detach") => {
1792                    let targets = arg_component_ref_vec(world, args, 0)?;
1793                    let signal = IV::Detach {
1794                        component_ids: null_ids(targets.len()),
1795                    };
1796                    add!(ActionComponent::new_authored(signal, targets))
1797                }
1798                Some("remove_subtree") => {
1799                    let targets = arg_component_ref_vec(world, args, 0)?;
1800                    let signal = IV::RemoveSubtree {
1801                        component_ids: null_ids(targets.len()),
1802                    };
1803                    add!(ActionComponent::new_authored(signal, targets))
1804                }
1805                Some("request_raycast") => {
1806                    let targets = arg_component_ref_vec(world, args, 0)?;
1807                    let signal = IV::RequestRaycast {
1808                        component_ids: null_ids(targets.len()),
1809                    };
1810                    add!(ActionComponent::new_authored(signal, targets))
1811                }
1812                Some("update_transform") => {
1813                    let target = arg_component_ref(world, args, 0)?;
1814                    let translation = arg_f32_arr::<3>(args, 1)?;
1815                    let rotation_euler = arg_f32_arr::<3>(args, 2)?;
1816                    let scale = arg_f32_arr::<3>(args, 3)?;
1817                    let transform = TransformComponent::new()
1818                        .with_position(translation[0], translation[1], translation[2])
1819                        .with_rotation_euler(
1820                            rotation_euler[0],
1821                            rotation_euler[1],
1822                            rotation_euler[2],
1823                        )
1824                        .with_scale(scale[0], scale[1], scale[2]);
1825                    let signal = IV::UpdateTransform {
1826                        component_ids: null_ids(1),
1827                        translation: transform.transform.translation,
1828                        rotation_quat_xyzw: transform.transform.rotation,
1829                        scale: transform.transform.scale,
1830                    };
1831                    add!(ActionComponent::new_authored(signal, vec![target]))
1832                }
1833                Some("update_transform_quat") => {
1834                    let target = arg_component_ref(world, args, 0)?;
1835                    let translation = arg_f32_arr::<3>(args, 1)?;
1836                    let rotation_quat = arg_f32_arr::<4>(args, 2)?;
1837                    let scale = arg_f32_arr::<3>(args, 3)?;
1838                    let signal = IV::UpdateTransform {
1839                        component_ids: null_ids(1),
1840                        translation,
1841                        rotation_quat_xyzw: rotation_quat,
1842                        scale,
1843                    };
1844                    add!(ActionComponent::new_authored(signal, vec![target]))
1845                }
1846                _ => add!(ActionComponent::default()),
1847            }
1848        }
1849        "NormalVis" => {
1850            let mut c = NormalVisualisationComponent::new();
1851            if let Some("thickness") = ctor {
1852                c = c.with_thickness(arg_f32(args, 0)?);
1853            }
1854            add!(c)
1855        }
1856        "TransparentCutout" => match ctor {
1857            Some("disabled") => add!(TransparentCutoutComponent::new().with_enabled(false)),
1858            _ => add!(TransparentCutoutComponent::new()),
1859        },
1860        "LightQuantization" => match ctor {
1861            Some("steps") => add!(LightQuantizationComponent::steps(arg_f32(args, 0)?)),
1862            _ => add!(LightQuantizationComponent::new()),
1863        },
1864        "Bounds" => {
1865            let (min, max) = match ctor {
1866                Some("aabb") => (arg_f32_arr::<3>(args, 0)?, arg_f32_arr::<3>(args, 1)?),
1867                _ => ([0.0; 3], [0.0; 3]),
1868            };
1869            add!(BoundsComponent::new(Aabb { min, max }))
1870        }
1871        "Mesh" => match ctor {
1872            Some("new") => add!(MeshComponent::new(arg_str(args, 0)?)),
1873            _ => add!(MeshComponent::new("")),
1874        },
1875        "Mirror" => {
1876            let mut c = MirrorComponent::default();
1877            if let Some("quality") = ctor {
1878                c = MirrorComponent::new(arg_f32(args, 0)? as i32);
1879            }
1880            add!(c)
1881        }
1882        "GestureCoordType" => match ctor {
1883            Some("screen_space_1d_slider") => {
1884                add!(GestureCoordTypeComponent::screen_space_1d_slider())
1885            }
1886            Some("world_plane") => add!(GestureCoordTypeComponent::world_plane()),
1887            _ => add!(GestureCoordTypeComponent::world_plane()),
1888        },
1889        "CollisionShape" => match ctor {
1890            Some("cube") => {
1891                let half_extents = arg_f32_arr::<3>(args, 0)?;
1892                add!(CollisionShapeComponent::new(
1893                    CollisionShape::cube_half_extents(half_extents)
1894                ))
1895            }
1896            Some("sphere") => add!(CollisionShapeComponent::new(CollisionShape::sphere_radius(
1897                arg_f32(args, 0)?
1898            ))),
1899            _ => add!(CollisionShapeComponent::cube()),
1900        },
1901        "RaycastableShape" => {
1902            let shape = match ctor {
1903                Some("aabb") => RaycastableShapeType::Aabb,
1904                Some("cone") => RaycastableShapeType::Cone,
1905                Some("ring_2d") => RaycastableShapeType::Ring2D,
1906                Some("quad_2d") => RaycastableShapeType::Quad2D,
1907                Some("triangle_2d") => RaycastableShapeType::Triangle2D,
1908                Some("tetrahedron") => RaycastableShapeType::Tetrahedron,
1909                Some("box") => RaycastableShapeType::Box,
1910                _ => RaycastableShapeType::InferFromBaseMesh,
1911            };
1912            add!(RaycastableShapeComponent::new(shape))
1913        }
1914        "Collision" => match ctor {
1915            Some("static") => add!(CollisionComponent::STATIC()),
1916            Some("kinematic") => add!(CollisionComponent::KINEMATIC()),
1917            Some("rigged") => add!(CollisionComponent::RIGGED()),
1918            _ => add!(CollisionComponent::STATIC()),
1919        },
1920        "Gravity" => {
1921            let id = world.add_component(GravityComponent::new());
1922            if let Some(method) = ctor {
1923                apply_call(world, id, method, args)?;
1924            }
1925            Ok(id)
1926        }
1927        "SkinnedMesh" => match ctor {
1928            Some("new") => add!(SkinnedMeshComponent::new(arg_f32(args, 0)? as usize)),
1929            _ => add!(SkinnedMeshComponent::new(0)),
1930        },
1931        "Vector3TemporalFilter" => {
1932            let mut c = Vector3TemporalFilterComponent::new();
1933            if let Some("smoothing_factor") = ctor {
1934                c = c.with_smoothing_factor(arg_f32(args, 0)?);
1935            }
1936            add!(c)
1937        }
1938        "QuatYawFollow" => match ctor {
1939            Some("new") => add!(QuatYawFollowComponent::new(
1940                arg_f32(args, 0)?,
1941                arg_f32(args, 1)?
1942            )),
1943            _ => add!(QuatYawFollowComponent::default()),
1944        },
1945        "SignalRouteUpward" => match ctor {
1946            Some("new") => add!(SignalRouteUpwardComponent::new(
1947                arg_str(args, 0)?,
1948                arg_str(args, 1)?
1949            )),
1950            _ => add!(SignalRouteUpwardComponent::default()),
1951        },
1952        "AvatarBodyYaw" => {
1953            let id = world.add_component(AvatarBodyYawComponent::new());
1954            if let Some(method) = ctor {
1955                apply_call(world, id, method, args)?;
1956            }
1957            Ok(id)
1958        }
1959        "Raycast" => match ctor {
1960            Some("continuous") => add!(RayCastComponent::continuous()),
1961            Some("event_driven") => add!(RayCastComponent::event_driven()),
1962            _ => add!(RayCastComponent::event_driven()),
1963        },
1964        "MusicNote" => {
1965            let pitch = match ctor {
1966                Some("a") | Some("b") | Some("c") | Some("d") | Some("e") | Some("f")
1967                | Some("g") => ctor.unwrap(),
1968                _ => "a",
1969            };
1970            let octave = arg_f32(args, 0)? as u16;
1971            let duration = arg_f32(args, 1)?;
1972            let note = match pitch {
1973                "a" => MusicNote::a(octave, duration),
1974                "b" => MusicNote::b(octave, duration),
1975                "c" => MusicNote::c(octave, duration),
1976                "d" => MusicNote::d(octave, duration),
1977                "e" => MusicNote::e(octave, duration),
1978                "f" => MusicNote::f(octave, duration),
1979                "g" => MusicNote::g(octave, duration),
1980                _ => MusicNote::default(),
1981            };
1982            let mut mn = MusicNoteComponent::new(note);
1983            // Optional 3rd positional arg: target audio source ref/query.
1984            if let Ok(v) = arg(args, 2) {
1985                if let Ok(src) = value_to_component_ref(world, v) {
1986                    mn.target_source = Some(src);
1987                }
1988            }
1989            add!(mn)
1990        }
1991        "AudioOutput" => {
1992            add!(AudioOutputComponent::new())
1993        }
1994        "AudioOscillator" => {
1995            let kind = match ctor {
1996                Some("sin") => OscillatorType::Sin,
1997                Some("triangle") => OscillatorType::Triangle,
1998                Some("square") => OscillatorType::Square,
1999                Some("square_3") => OscillatorType::Square3,
2000                Some("saw") => OscillatorType::Saw,
2001                Some("noise") => OscillatorType::Noise,
2002                Some("drum") => OscillatorType::Drum,
2003                _ => OscillatorType::Sin,
2004            };
2005            add!(AudioOscillatorComponent::single(AudioOscillator::new(kind)))
2006        }
2007        "AudioClip" => {
2008            // Cloning is a *method* on a live AudioClip handle, not a
2009            // ctor — see `eval_method_call` + `HostCallKind::AudioClipInstance`.
2010            //
2011            // Constructor variants accept the URI as positional arg 0. The
2012            // ctor name (`wav`/`opus`/`ogg`/`mp3`/`flac`/`new`) is purely
2013            // documentation — codec is detected from the file by the
2014            // decode pipeline. `latched`/`one_shot` flip the trigger mode.
2015            let uri = arg_str(args, 0).unwrap_or("").to_string();
2016            let mut c = AudioClipComponent::new(uri);
2017            match ctor {
2018                Some("one_shot") => c = c.with_trigger_mode(AudioTriggerMode::OneShot),
2019                Some("latched") => c = c.with_trigger_mode(AudioTriggerMode::Latched),
2020                _ => {}
2021            }
2022            add!(c)
2023        }
2024        "IKChain" => {
2025            let solver = match ctor {
2026                Some("aim_constraint") => IKSolver::AimConstraint {
2027                    offset_yaw: arg_f32(args, 0)?,
2028                    copy_position: arg_bool(args, 1).unwrap_or(false),
2029                    target_position_offset: arg_f32_arr::<3>(args, 2).unwrap_or([0.0, 0.0, 0.0]),
2030                },
2031                Some("two_bone_ik") => {
2032                    use slotmap::Key;
2033                    IKSolver::TwoBoneIK {
2034                        // root_joint_id and mid_joint_id are runtime-wired by
2035                        // AvatarControlSystem; MMS-authored TwoBoneIK chains are
2036                        // not currently supported (would need name resolution).
2037                        root_joint_id: ComponentId::null(),
2038                        mid_joint_id: ComponentId::null(),
2039                        pole_direction: arg_f32_arr::<3>(args, 0)?,
2040                        copy_end_rotation: arg_bool(args, 1)?,
2041                    }
2042                }
2043                Some("fabrik") => IKSolver::Fabrik {
2044                    max_iterations: arg_f32(args, 0)? as u32,
2045                    tolerance: arg_f32(args, 1)?,
2046                    target_position_offset: arg_f32_arr::<3>(args, 2).unwrap_or([0.0, 0.0, 0.0]),
2047                },
2048                _ => IKSolver::AimConstraint {
2049                    offset_yaw: 0.0,
2050                    copy_position: false,
2051                    target_position_offset: [0.0, 0.0, 0.0],
2052                },
2053            };
2054            // target_id and end_effector_id are runtime-wired by AvatarControlSystem;
2055            // pass a sentinel for now.
2056            use slotmap::Key;
2057            let sentinel = ComponentId::null();
2058            add!(IKChainComponent::new(solver, sentinel, sentinel))
2059        }
2060        "TransformGizmo" => {
2061            let id = world.add_component(TransformGizmoComponent::new());
2062            if let Some(method) = ctor {
2063                apply_call(world, id, method, args)?;
2064            }
2065            Ok(id)
2066        }
2067        "TransformGizmoTranslate" => add!(TransformGizmoTranslateComponent::new(parse_gizmo_axis(
2068            ctor
2069        ))),
2070        "TransformGizmoRotate" => add!(TransformGizmoRotateComponent::new(parse_gizmo_axis(ctor))),
2071        "TransformGizmoScale" => add!(TransformGizmoScaleComponent::new(parse_gizmo_axis(ctor))),
2072        "KineticResponse" => {
2073            let c = match ctor {
2074                Some("push") => KineticResponseComponent::push(),
2075                Some("slide") => KineticResponseComponent::slide(),
2076                _ => KineticResponseComponent::slide(),
2077            };
2078            let id = world.add_component(c);
2079            Ok(id)
2080        }
2081        "Data" => {
2082            add!(DataComponent::new())
2083        }
2084        other => Err(format!("unknown component type: '{other}'")),
2085    }
2086}
2087
2088fn apply_named_assignment(
2089    world: &mut World,
2090    id: ComponentId,
2091    name: &str,
2092    val: &Value,
2093) -> Result<(), String> {
2094    if let Some(style) = world.get_component_by_id_as_mut::<StyleComponent>(id) {
2095        match name {
2096            "background_color" => {
2097                style.background_color = Some(val_as_f32_array::<4>(val)?);
2098                return Ok(());
2099            }
2100            "background_z" => {
2101                style.background_z = Some(val_as_f32(val)?);
2102                return Ok(());
2103            }
2104            "color" => {
2105                style.color = Some(val_as_f32_array::<4>(val)?);
2106                return Ok(());
2107            }
2108            _ => {}
2109        }
2110    }
2111
2112    if let Some(data) = world.get_component_by_id_as_mut::<DataComponent>(id) {
2113        data.insert(
2114            name.to_string(),
2115            match val {
2116                Value::String(s) => DataValue::Text(s.clone()),
2117                Value::Bool(b) => DataValue::Bool(*b),
2118                Value::Number(n) => DataValue::Integer(*n as i64),
2119                _ => DataValue::Text(format!("{val:?}")),
2120            },
2121        );
2122        return Ok(());
2123    }
2124
2125    if name == "root"
2126        && world
2127            .get_component_by_id_as::<SelectionComponent>(id)
2128            .is_some()
2129    {
2130        let src = value_to_component_ref(world, val)?;
2131        if let Some(selection) = world.get_component_by_id_as_mut::<SelectionComponent>(id) {
2132            selection.target_root_source = Some(src);
2133        }
2134        return Ok(());
2135    }
2136
2137    if let Some(router) = world.get_component_by_id_as_mut::<RouterComponent>(id) {
2138        match name {
2139            "target" => {
2140                router.target_name = Some(val_as_str(val)?.to_string());
2141            }
2142            "ignore" => {
2143                let Value::Array(items) = val else {
2144                    return Err(format!("expected array for Router.ignore, got {val:?}"));
2145                };
2146                router.ignore_names = items
2147                    .iter()
2148                    .map(val_as_str)
2149                    .collect::<Result<Vec<_>, _>>()?
2150                    .into_iter()
2151                    .map(str::to_string)
2152                    .collect();
2153            }
2154            _ => {}
2155        }
2156        return Ok(());
2157    }
2158
2159    if let Some(t) = world.get_component_by_id_as_mut::<TransformComponent>(id) {
2160        if name == "rotation" {
2161            let arr = val_as_f32_array::<3>(val)?;
2162            *t = t.clone().with_rotation_euler(arr[0], arr[1], arr[2]);
2163        }
2164        return Ok(());
2165    }
2166    println!("[registry] unhandled named assignment '{name}' on component {id:?}");
2167    Ok(())
2168}
2169
2170fn apply_call(
2171    world: &mut World,
2172    id: ComponentId,
2173    method: &str,
2174    args: &[Value],
2175) -> Result<(), String> {
2176    if let Some(fit_bounds) = world.get_component_by_id_as_mut::<FitBoundsComponent>(id) {
2177        match method {
2178            "renderable_only" => fit_bounds.mode = FitBoundsMode::RenderableOnly,
2179            "layout_aware" => fit_bounds.mode = FitBoundsMode::LayoutAware,
2180            "to" => {
2181                fit_bounds.target = FitBoundsTarget::ExplicitBounds;
2182                fit_bounds.target_bounds = val_as_f32_array::<6>(&Value::Array(args.to_vec()))?;
2183            }
2184            "to_container" => fit_bounds.target = FitBoundsTarget::ParentPaddingBox,
2185            _ => {}
2186        }
2187        return Ok(());
2188    }
2189
2190    if let Some(layout_bounds) = world.get_component_by_id_as_mut::<LayoutBoundsComponent>(id) {
2191        match method {
2192            "content_box" => {
2193                layout_bounds.content_local = crate::engine::graphics::bounds::Aabb {
2194                    min: val_as_f32_array::<3>(&args[0])?,
2195                    max: val_as_f32_array::<3>(&args[1])?,
2196                }
2197            }
2198            "padding_box" => {
2199                layout_bounds.padding_local = crate::engine::graphics::bounds::Aabb {
2200                    min: val_as_f32_array::<3>(&args[0])?,
2201                    max: val_as_f32_array::<3>(&args[1])?,
2202                }
2203            }
2204            _ => {}
2205        }
2206        return Ok(());
2207    }
2208
2209    if let Some(pc) = world.get_component_by_id_as_mut::<PoseCaptureComponent>(id) {
2210        match method {
2211            "with_label" | "label" => pc.label = Some(arg_str(args, 0)?.to_string()),
2212            _ => {}
2213        }
2214        return Ok(());
2215    }
2216
2217    if let Some(pose) = world.get_component_by_id_as_mut::<PoseCapturePoseComponent>(id) {
2218        match method {
2219            "joint" => {
2220                let entry = crate::engine::ecs::component::PoseBoneEntry {
2221                    query: arg_str(args, 0)?.to_string(),
2222                    translation: val_as_f32_array::<3>(arg(args, 1)?)?,
2223                    rotation: val_as_f32_array::<4>(arg(args, 2)?)?,
2224                    scale: val_as_f32_array::<3>(arg(args, 3)?)?,
2225                };
2226                pose.push_joint(entry)?;
2227            }
2228            _ => {}
2229        }
2230        return Ok(());
2231    }
2232
2233    // Transform builders
2234    if let Some(t) = world.get_component_by_id_as_mut::<TransformComponent>(id) {
2235        match method {
2236            "position" => {
2237                *t = t.clone().with_position(
2238                    arg_world_f32(args, 0)?,
2239                    arg_world_f32(args, 1)?,
2240                    arg_world_f32(args, 2)?,
2241                )
2242            }
2243            "scale" => {
2244                *t = t.clone().with_scale(
2245                    arg_world_f32(args, 0)?,
2246                    arg_world_f32(args, 1)?,
2247                    arg_world_f32(args, 2)?,
2248                )
2249            }
2250            "rotation" | "rotation_euler" => {
2251                *t = t.clone().with_rotation_euler(
2252                    arg_f32(args, 0)?,
2253                    arg_f32(args, 1)?,
2254                    arg_f32(args, 2)?,
2255                )
2256            }
2257            "rotation_quat" | "quaternion" | "quat" => {
2258                *t = t.clone().with_rotation_quat(arg_f32_quat(args)?)
2259            }
2260            "looking_at" => *t = t.clone().with_looking_at(arg_f32_arr::<3>(args, 0)?),
2261            _ => {}
2262        }
2263        return Ok(());
2264    }
2265    if let Some(l) = world.get_component_by_id_as_mut::<LayoutComponent>(id) {
2266        match method {
2267            "width" | "available_width" => {
2268                l.set_available_width_dimension(arg_layout_length(args, 0)?);
2269            }
2270            "height" | "available_height" => {
2271                l.set_available_height_dimension(arg_layout_length(args, 0)?);
2272            }
2273            "unit_scale" => l.set_unit_scale(arg_f32(args, 0)?),
2274            _ => {}
2275        }
2276        return Ok(());
2277    }
2278    if let Some(text_input) = world.get_component_by_id_as_mut::<TextInputComponent>(id) {
2279        match method {
2280            "read_only" => text_input.read_only = arg_bool(args, 0)?,
2281            _ => {}
2282        }
2283        return Ok(());
2284    }
2285    if let Some(http_client) = world.get_component_by_id_as_mut::<HttpClientComponent>(id) {
2286        match method {
2287            "enabled" => http_client.enabled = arg_bool(args, 0)?,
2288            "timeout_ms" => http_client.timeout_ms = Some(arg_f32(args, 0)? as u64),
2289            _ => {}
2290        }
2291        return Ok(());
2292    }
2293    if let Some(http_server) = world.get_component_by_id_as_mut::<HttpServerComponent>(id) {
2294        match method {
2295            "bind" => http_server.bind_addr = arg_str(args, 0)?.to_string(),
2296            "enabled" => http_server.enabled = arg_bool(args, 0)?,
2297            _ => {}
2298        }
2299        return Ok(());
2300    }
2301    if let Some(asset_payload) = world.get_component_by_id_as_mut::<AssetPayloadComponent>(id) {
2302        match method {
2303            "asset_key" => asset_payload.asset_key = arg_str(args, 0)?.to_string(),
2304            "title" => asset_payload.title = arg_str(args, 0)?.to_string(),
2305            _ => {}
2306        }
2307        return Ok(());
2308    }
2309    if world
2310        .get_component_by_id_as::<SelectionComponent>(id)
2311        .is_some()
2312    {
2313        match method {
2314            "optional" => {
2315                if let Some(selection) = world.get_component_by_id_as_mut::<SelectionComponent>(id)
2316                {
2317                    selection.allow_empty_single = true;
2318                }
2319            }
2320            "root" => {
2321                let src = arg_component_ref(world, args, 0)?;
2322                if let Some(selection) = world.get_component_by_id_as_mut::<SelectionComponent>(id)
2323                {
2324                    selection.target_root_source = Some(src);
2325                }
2326            }
2327            _ => {}
2328        }
2329        return Ok(());
2330    }
2331    if let Some(router) = world.get_component_by_id_as_mut::<SignalObserverRouterComponent>(id) {
2332        match method {
2333            "blacklist" => {
2334                router.blacklist = arg_str_vec(args, 0)?;
2335            }
2336            "whitelist" => {
2337                router.whitelist = arg_str_vec(args, 0)?;
2338            }
2339            _ => {}
2340        }
2341        return Ok(());
2342    }
2343    if let Some(dl) = world.get_component_by_id_as_mut::<DirectionalLightComponent>(id) {
2344        match method {
2345            "intensity" => *dl = dl.clone().with_intensity(arg_f32(args, 0)?),
2346            "color" => {
2347                *dl = dl
2348                    .clone()
2349                    .with_color(arg_f32(args, 0)?, arg_f32(args, 1)?, arg_f32(args, 2)?)
2350            }
2351            _ => {}
2352        }
2353        return Ok(());
2354    }
2355    if let Some(pl) = world.get_component_by_id_as_mut::<PointLightComponent>(id) {
2356        match method {
2357            "intensity" => *pl = pl.clone().with_intensity(arg_f32(args, 0)?),
2358            "distance" => *pl = pl.clone().with_distance(arg_f32(args, 0)?),
2359            "color" => {
2360                *pl = pl
2361                    .clone()
2362                    .with_color(arg_f32(args, 0)?, arg_f32(args, 1)?, arg_f32(args, 2)?)
2363            }
2364            _ => {}
2365        }
2366        return Ok(());
2367    }
2368    if let Some(render_graph) = world.get_component_by_id_as_mut::<RenderGraphComponent>(id) {
2369        match method {
2370            "on" => *render_graph = render_graph.clone().with_enabled(true),
2371            "off" => *render_graph = render_graph.clone().with_enabled(false),
2372            "enabled" => *render_graph = render_graph.clone().with_enabled(arg_bool(args, 0)?),
2373            _ => {}
2374        }
2375        return Ok(());
2376    }
2377    if let Some(blur_pass) = world.get_component_by_id_as_mut::<BlurPassComponent>(id) {
2378        match method {
2379            "on" => *blur_pass = blur_pass.clone().with_enabled(true),
2380            "off" => *blur_pass = blur_pass.clone().with_enabled(false),
2381            "enabled" => *blur_pass = blur_pass.clone().with_enabled(arg_bool(args, 0)?),
2382            "radius_ndc" => *blur_pass = blur_pass.clone().with_radius_ndc(arg_f32(args, 0)?),
2383            "half_res" => *blur_pass = blur_pass.clone().with_half_res(arg_bool(args, 0)?),
2384            _ => {}
2385        }
2386        return Ok(());
2387    }
2388    if let Some(bloom) = world.get_component_by_id_as_mut::<BloomComponent>(id) {
2389        match method {
2390            "on" => *bloom = bloom.clone().with_enabled(true),
2391            "off" => *bloom = bloom.clone().with_enabled(false),
2392            "enabled" => *bloom = bloom.clone().with_enabled(arg_bool(args, 0)?),
2393            "intensity" => *bloom = bloom.clone().with_intensity(arg_f32(args, 0)?),
2394            "radius_ndc" => *bloom = bloom.clone().with_radius_ndc(arg_f32(args, 0)?),
2395            "emissive_scale" => *bloom = bloom.clone().with_emissive_scale(arg_f32(args, 0)?),
2396            "half_res" => *bloom = bloom.clone().with_half_res(arg_bool(args, 0)?),
2397            "output_texture" => {
2398                *bloom = bloom.clone().with_output_texture(arg_str(args, 0)?);
2399            }
2400            _ => {}
2401        }
2402        return Ok(());
2403    }
2404    if let Some(sc) = world.get_component_by_id_as_mut::<StencilClipComponent>(id) {
2405        if method == "stencil_ref" {
2406            sc.stencil_ref = arg_f32(args, 0)? as u8;
2407        }
2408        return Ok(());
2409    }
2410    if let Some(g) = world.get_component_by_id_as_mut::<GravityComponent>(id) {
2411        match method {
2412            "enabled" => g.enabled = arg_bool(args, 0)?,
2413            "coefficient" => g.coefficient = arg_f32(args, 0)?,
2414            _ => {}
2415        }
2416        return Ok(());
2417    }
2418    if let Some(aby) = world.get_component_by_id_as_mut::<AvatarBodyYawComponent>(id) {
2419        match method {
2420            "threshold" => *aby = aby.clone().with_threshold(arg_f32(args, 0)?),
2421            "rate" => *aby = aby.clone().with_rate(arg_f32(args, 0)?),
2422            "forward_plus_z" => *aby = aby.clone().with_forward_plus_z(),
2423            _ => {}
2424        }
2425        return Ok(());
2426    }
2427    if let Some(gz) = world.get_component_by_id_as_mut::<TransformGizmoComponent>(id) {
2428        if method == "scale" {
2429            *gz = gz.clone().with_scale(arg_f32(args, 0)?);
2430        }
2431        return Ok(());
2432    }
2433    if let Some(kr) = world.get_component_by_id_as_mut::<KineticResponseComponent>(id) {
2434        match method {
2435            "enabled" => kr.enabled = arg_bool(args, 0)?,
2436            "max_iterations" => kr.max_iterations = arg_f32(args, 0)? as u32,
2437            "push_out_epsilon" => kr.push_out_epsilon = arg_f32(args, 0)?,
2438            "push_strength" => kr.push_strength = arg_f32(args, 0)?,
2439            "friction" => kr.friction = arg_f32(args, 0)?,
2440            "friction_y" => kr.friction_y = arg_f32(args, 0)?,
2441            "max_speed" => kr.max_speed = arg_f32(args, 0)?,
2442            _ => {}
2443        }
2444        return Ok(());
2445    }
2446    if let Some(rs) = world.get_component_by_id_as_mut::<RendererStatsComponent>(id) {
2447        match method {
2448            "enabled" => rs.enabled = arg_bool(args, 0)?,
2449            "update_interval_sec" => rs.update_interval_sec = arg_f32(args, 0)?,
2450            "smoothing" => rs.smoothing = arg_f32(args, 0)?,
2451            "color" => rs.color = arg_f32_arr::<4>(args, 0)?,
2452            "emissive" => rs.emissive = arg_bool(args, 0)?,
2453            "camera_target" => {
2454                let target = match arg_str(args, 0)? {
2455                    "Xr" | "xr" => CameraTarget::Xr,
2456                    _ => CameraTarget::Window,
2457                };
2458                *rs = rs.clone().with_camera_target(target);
2459            }
2460            _ => {}
2461        }
2462        return Ok(());
2463    }
2464    if world
2465        .get_component_by_id_as::<MusicNoteComponent>(id)
2466        .is_some()
2467    {
2468        match method {
2469            "velocity" => {
2470                if let Some(mn) = world.get_component_by_id_as_mut::<MusicNoteComponent>(id) {
2471                    mn.note = mn.note.with_velocity(arg_f32(args, 0)?);
2472                }
2473            }
2474            "play_on_attach" => {
2475                if let Some(mn) = world.get_component_by_id_as_mut::<MusicNoteComponent>(id) {
2476                    mn.play_on_attach = true;
2477                }
2478            }
2479            "at_beat" => {
2480                let b = arg_f32(args, 0)? as f64;
2481                if let Some(mn) = world.get_component_by_id_as_mut::<MusicNoteComponent>(id) {
2482                    mn.scheduled_beat = Some(b);
2483                }
2484            }
2485            "target" => {
2486                // target(ref) — explicit ComponentRef override.
2487                let src = arg_component_ref(world, args, 0)?;
2488                if let Some(mn) = world.get_component_by_id_as_mut::<MusicNoteComponent>(id) {
2489                    mn.target_source = Some(src);
2490                }
2491            }
2492            _ => {}
2493        }
2494        return Ok(());
2495    }
2496    if world
2497        .get_component_by_id_as::<AudioOscillatorComponent>(id)
2498        .is_some()
2499    {
2500        match method {
2501            "frequency" => {
2502                let hz = arg_f32(args, 0)?;
2503                if let Some(c) = world.get_component_by_id_as_mut::<AudioOscillatorComponent>(id) {
2504                    for o in c.oscillators.iter_mut() {
2505                        o.frequency = hz;
2506                    }
2507                }
2508            }
2509            "amplitude" => {
2510                let a = arg_f32(args, 0)?;
2511                if let Some(c) = world.get_component_by_id_as_mut::<AudioOscillatorComponent>(id) {
2512                    for o in c.oscillators.iter_mut() {
2513                        o.amplitude = a;
2514                    }
2515                }
2516            }
2517            "enabled" => {
2518                let en = arg_bool(args, 0)?;
2519                if let Some(c) = world.get_component_by_id_as_mut::<AudioOscillatorComponent>(id) {
2520                    for o in c.oscillators.iter_mut() {
2521                        o.enabled = en;
2522                    }
2523                }
2524            }
2525            _ => {}
2526        }
2527        return Ok(());
2528    }
2529    if world
2530        .get_component_by_id_as::<AudioClipComponent>(id)
2531        .is_some()
2532    {
2533        match method {
2534            "one_shot" => {
2535                if let Some(c) = world.get_component_by_id_as_mut::<AudioClipComponent>(id) {
2536                    c.trigger_mode = AudioTriggerMode::OneShot;
2537                }
2538            }
2539            "latched" => {
2540                if let Some(c) = world.get_component_by_id_as_mut::<AudioClipComponent>(id) {
2541                    c.trigger_mode = AudioTriggerMode::Latched;
2542                }
2543            }
2544            "retrigger" => {
2545                if let Some(c) = world.get_component_by_id_as_mut::<AudioClipComponent>(id) {
2546                    c.trigger_mode = AudioTriggerMode::Retrigger;
2547                }
2548            }
2549            _ => {}
2550        }
2551        return Ok(());
2552    }
2553    if world
2554        .get_component_by_id_as::<IKChainComponent>(id)
2555        .is_some()
2556    {
2557        match method {
2558            "weight" => {
2559                let w = arg_f32(args, 0)?;
2560                if let Some(ik) = world.get_component_by_id_as_mut::<IKChainComponent>(id) {
2561                    *ik = ik.clone().with_weight(w);
2562                }
2563            }
2564            "target" => {
2565                let src = arg_component_ref(world, args, 0)?;
2566                let resolved = resolve_component_ref(world, &src);
2567                if let Some(ik) = world.get_component_by_id_as_mut::<IKChainComponent>(id) {
2568                    ik.target_source = Some(src);
2569                    if let Some(r) = resolved {
2570                        ik.target_id = r;
2571                    }
2572                }
2573            }
2574            "end_effector" => {
2575                let src = arg_component_ref(world, args, 0)?;
2576                let resolved = resolve_component_ref(world, &src);
2577                if let Some(ik) = world.get_component_by_id_as_mut::<IKChainComponent>(id) {
2578                    ik.end_effector_source = Some(src);
2579                    if let Some(r) = resolved {
2580                        ik.end_effector_id = r;
2581                    }
2582                }
2583            }
2584            _ => {}
2585        }
2586        return Ok(());
2587    }
2588    if world
2589        .get_component_by_id_as::<TransformParentComponent>(id)
2590        .is_some()
2591    {
2592        match method {
2593            "target" => {
2594                let src = arg_component_ref(world, args, 0)?;
2595                if let Some(tp) = world.get_component_by_id_as_mut::<TransformParentComponent>(id) {
2596                    tp.target_source = Some(src);
2597                }
2598            }
2599            "root" => {
2600                let src = arg_component_ref(world, args, 0)?;
2601                if let Some(tp) = world.get_component_by_id_as_mut::<TransformParentComponent>(id) {
2602                    tp.root_source = Some(src);
2603                }
2604            }
2605            _ => {}
2606        }
2607        return Ok(());
2608    }
2609    if let Some(rc) = world.get_component_by_id_as_mut::<RayCastComponent>(id) {
2610        if method == "max_distance" {
2611            *rc = rc.with_max_distance(arg_f32(args, 0)?);
2612        }
2613        return Ok(());
2614    }
2615    if let Some(yf) = world.get_component_by_id_as_mut::<QuatYawFollowComponent>(id) {
2616        match method {
2617            "forward_plus_z" => *yf = yf.with_forward_plus_z(),
2618            "initial_yaw" => *yf = yf.with_initial_yaw(arg_f32(args, 0)?),
2619            _ => {}
2620        }
2621        return Ok(());
2622    }
2623    if let Some(v3f) = world.get_component_by_id_as_mut::<Vector3TemporalFilterComponent>(id) {
2624        if method == "smoothing_factor" {
2625            *v3f = v3f.with_smoothing_factor(arg_f32(args, 0)?);
2626        }
2627        return Ok(());
2628    }
2629    if let Some(c3) = world.get_component_by_id_as_mut::<Camera3DComponent>(id) {
2630        match method {
2631            "enabled" => *c3 = c3.clone().with_enabled(arg_bool(args, 0)?),
2632            "fov" => *c3 = c3.clone().with_fov(arg_f32(args, 0)?),
2633            "near" => *c3 = c3.clone().with_near(arg_f32(args, 0)?),
2634            "far" => *c3 = c3.clone().with_far(arg_f32(args, 0)?),
2635            "target" => {
2636                c3.target = match arg_str(args, 0)? {
2637                    "xr" => CameraTarget::Xr,
2638                    _ => CameraTarget::Window,
2639                };
2640            }
2641            _ => {}
2642        }
2643        return Ok(());
2644    }
2645    if let Some(c2) = world.get_component_by_id_as_mut::<Camera2DComponent>(id) {
2646        if method == "target" {
2647            c2.target = match arg_str(args, 0)? {
2648                "xr" => CameraTarget::Xr,
2649                _ => CameraTarget::Window,
2650            };
2651        }
2652        return Ok(());
2653    }
2654    if let Some(cxr) = world.get_component_by_id_as_mut::<CameraXRComponent>(id) {
2655        match method {
2656            "enabled" => cxr.enabled = arg_bool(args, 0)?,
2657            "target" => {
2658                cxr.target = match arg_str(args, 0)? {
2659                    "window" => CameraTarget::Window,
2660                    _ => CameraTarget::Xr,
2661                };
2662            }
2663            _ => {}
2664        }
2665        return Ok(());
2666    }
2667    if let Some(bg) = world.get_component_by_id_as_mut::<BackgroundComponent>(id) {
2668        match method {
2669            "occlusion_and_lighting" => bg.occlusion_and_lighting = true,
2670            "ray_casting" => bg.ray_casting = true,
2671            _ => {}
2672        }
2673        return Ok(());
2674    }
2675    if let Some(ed) = world.get_component_by_id_as_mut::<EditorComponent>(id) {
2676        match method {
2677            "active" => ed.active = true,
2678            "interaction_mode" => {
2679                ed.interaction_mode = match arg_str(args, 0)? {
2680                    "cursor_3d" => EditorInteractionMode::Cursor3d,
2681                    "select_cursor" => EditorInteractionMode::SelectAndCursor,
2682                    _ => EditorInteractionMode::Select,
2683                };
2684            }
2685            "translation_space" => {
2686                let space = match arg_str(args, 0)? {
2687                    "local" => TransformGizmoCoordSpace::Local,
2688                    _ => TransformGizmoCoordSpace::World,
2689                };
2690                ed.transform_gizmo_translation_space = space;
2691            }
2692            "rotation_space" => {
2693                let space = match arg_str(args, 0)? {
2694                    "local" => TransformGizmoCoordSpace::Local,
2695                    _ => TransformGizmoCoordSpace::World,
2696                };
2697                ed.transform_gizmo_rotation_space = space;
2698            }
2699            _ => {}
2700        }
2701        return Ok(());
2702    }
2703    if let Some(rc) = world.get_component_by_id_as_mut::<RaycastableComponent>(id) {
2704        if method == "pointer_events" {
2705            rc.pointer_events = match arg_str(args, 0)? {
2706                "drag_only" => PointerEvents::DragOnly,
2707                "click_only" => PointerEvents::ClickOnly,
2708                "pass_through" => PointerEvents::PassThrough,
2709                _ => PointerEvents::All,
2710            };
2711        } else if method == "interaction_priority" {
2712            rc.interaction_priority = arg_f32(args, 0)?.clamp(0.0, u8::MAX as f32) as u8;
2713        }
2714        return Ok(());
2715    }
2716    if let Some(op) = world.get_component_by_id_as_mut::<OpacityComponent>(id) {
2717        match method {
2718            "opacity" => *op = op.with_opacity(arg_f32(args, 0)?),
2719            "multiple_layers" => *op = op.with_multiple_layers(),
2720            _ => {}
2721        }
2722        return Ok(());
2723    }
2724    if let Some(em) = world.get_component_by_id_as_mut::<EmissiveComponent>(id) {
2725        if method == "intensity" {
2726            em.intensity = arg_f32(args, 0)?.max(0.0);
2727        }
2728        return Ok(());
2729    }
2730    if let Some(gltf) = world.get_component_by_id_as_mut::<GLTFComponent>(id) {
2731        if method == "with_visualized_transforms" {
2732            *gltf = gltf.clone().with_visualized_transforms(arg_bool(args, 0)?);
2733        }
2734        return Ok(());
2735    }
2736    let spring_center = if method == "center" {
2737        Some(arg_component_ref(world, args, 0)?)
2738    } else {
2739        None
2740    };
2741    if let Some(chain) = world.get_component_by_id_as_mut::<SpringBoneComponent>(id) {
2742        match method {
2743            "center" => chain.center = spring_center,
2744            "enabled" => chain.enabled = arg_bool(args, 0)?,
2745            "virtual_end_length_ratio" => {
2746                chain.virtual_end_length_ratio = Some(arg_f32(args, 0)?.max(0.0))
2747            }
2748            _ => {}
2749        }
2750        return Ok(());
2751    }
2752    if let Some(joint) = world.get_component_by_id_as_mut::<SpringJointComponent>(id) {
2753        match method {
2754            "stiffness" => joint.stiffness = arg_f32(args, 0)?.max(0.0),
2755            "drag_force" => joint.drag_force = arg_f32(args, 0)?.clamp(0.0, 1.0),
2756            "gravity" => {
2757                joint.gravity_power = arg_f32(args, 0)?;
2758                joint.gravity_dir = if args.len() == 2 {
2759                    val_as_f32_array::<3>(&args[1])?
2760                } else {
2761                    [arg_f32(args, 1)?, arg_f32(args, 2)?, arg_f32(args, 3)?]
2762                };
2763            }
2764            _ => {}
2765        }
2766        return Ok(());
2767    }
2768    if let Some(inp) = world.get_component_by_id_as_mut::<InputComponent>(id) {
2769        if method == "speed" {
2770            inp.speed = arg_f32(args, 0)?;
2771        }
2772        return Ok(());
2773    }
2774    if let Some(inp) = world.get_component_by_id_as_mut::<InputXRGamepadComponent>(id) {
2775        match method {
2776            "enabled" => inp.enabled = arg_bool(args, 0)?,
2777            "hand" => {
2778                inp.hand = match arg_str(args, 0)?.to_ascii_lowercase().as_str() {
2779                    "default" => XrHandPreference::Default,
2780                    "left" => XrHandPreference::Left,
2781                    "right" => XrHandPreference::Right,
2782                    "either" => XrHandPreference::Either,
2783                    s => return Err(format!("unknown XrHandPreference: {s}")),
2784                }
2785            }
2786            "locomotion" => {
2787                inp.locomotion = if args.is_empty() {
2788                    true
2789                } else {
2790                    arg_bool(args, 0)?
2791                };
2792            }
2793            "speed" => inp.speed = arg_f32(args, 0)?,
2794            "deadzone" => inp.deadzone = arg_f32(args, 0)?,
2795            _ => {}
2796        }
2797        return Ok(());
2798    }
2799    if world
2800        .get_component_by_id_as::<InputTransformModeComponent>(id)
2801        .is_some()
2802    {
2803        let translation_basis_src = if method == "translation_basis" {
2804            Some(arg_component_ref(world, args, 0)?)
2805        } else {
2806            None
2807        };
2808        let updated = {
2809            let itm = world
2810                .get_component_by_id_as::<InputTransformModeComponent>(id)
2811                .expect("checked above")
2812                .clone();
2813            match method {
2814                "fps_rotation" => itm.with_fps_rotation(),
2815                "roll_axis_y" => itm.with_roll_axis_y(),
2816                "rotation_disabled" => itm.with_rotation_disabled(),
2817                "translation_basis" => itm
2818                    .with_translation_basis_source(translation_basis_src.expect("computed above")),
2819                _ => itm,
2820            }
2821        };
2822        if let Some(itm) = world.get_component_by_id_as_mut::<InputTransformModeComponent>(id) {
2823            *itm = updated;
2824        }
2825        return Ok(());
2826    }
2827    if let Some(s) = world.get_component_by_id_as_mut::<RendererSettingsComponent>(id) {
2828        if method == "window_size" {
2829            *s = s
2830                .clone()
2831                .with_window_size(arg_f32(args, 0)? as u32, arg_f32(args, 1)? as u32);
2832        }
2833        return Ok(());
2834    }
2835    if let Some(ts) = world.get_component_by_id_as_mut::<TextShadowComponent>(id) {
2836        match method {
2837            "offset_xy" => {
2838                let arr = arg_f32_arr::<2>(args, 0)?;
2839                *ts = ts.clone().with_offset_xy(arr);
2840            }
2841            "z_offset" => *ts = ts.clone().with_z_offset(arg_f32(args, 0)?),
2842            "offset" => {
2843                let arr = arg_f32_arr::<3>(args, 0)?;
2844                *ts = ts.clone().with_offset(arr);
2845            }
2846            "rgba" => {
2847                let arr = arg_f32_arr::<4>(args, 0)?;
2848                *ts = ts.clone().with_rgba(arr);
2849            }
2850            "scale" => *ts = ts.clone().with_scale(arg_f32(args, 0)?),
2851            _ => {}
2852        }
2853        return Ok(());
2854    }
2855    if let Some(router) = world.get_component_by_id_as_mut::<RouterComponent>(id) {
2856        match method {
2857            "target" => router.target_name = Some(arg_str(args, 0)?.to_string()),
2858            "ignore" => router.ignore_names = arg_str_vec(args, 0)?,
2859            _ => {}
2860        }
2861        return Ok(());
2862    }
2863    if let Some(text) = world.get_component_by_id_as_mut::<TextComponent>(id) {
2864        match method {
2865            "font_size" => text.set_font_size(arg_f32(args, 0)?),
2866            _ => {}
2867        }
2868        return Ok(());
2869    }
2870    if let Some(editor) = world.get_component_by_id_as_mut::<EditorComponent>(id) {
2871        match method {
2872            "panels" => editor.spawn_panels = arg_bool(args, 0)?,
2873            "serialize_editor_panels" => editor.serialize_editor_panels = arg_bool(args, 0)?,
2874            _ => {}
2875        }
2876        return Ok(());
2877    }
2878    if let Some(rs) = world.get_component_by_id_as_mut::<RendererStatsComponent>(id) {
2879        if method == "camera_target" {
2880            let target = match arg_str(args, 0)? {
2881                "Xr" => CameraTarget::Xr,
2882                _ => CameraTarget::Window,
2883            };
2884            *rs = rs.clone().with_camera_target(target);
2885        }
2886        return Ok(());
2887    }
2888    if let Some(qtf) = world.get_component_by_id_as_mut::<QuatTemporalFilterComponent>(id) {
2889        if method == "smoothing_factor" {
2890            *qtf = qtf.clone().with_smoothing_factor(arg_f32(args, 0)?);
2891        }
2892        return Ok(());
2893    }
2894    if let Some(avc) = world.get_component_by_id_as_mut::<AvatarControlComponent>(id) {
2895        match method {
2896            "head_bone" => *avc = avc.clone().with_head_bone(arg_str(args, 0)?),
2897            "left_hand_bone" => *avc = avc.clone().with_left_hand_bone(arg_str(args, 0)?),
2898            "right_hand_bone" => *avc = avc.clone().with_right_hand_bone(arg_str(args, 0)?),
2899            "left_upper_arm_bone" => *avc = avc.clone().with_left_upper_arm_bone(arg_str(args, 0)?),
2900            "left_lower_arm_bone" => *avc = avc.clone().with_left_lower_arm_bone(arg_str(args, 0)?),
2901            "right_upper_arm_bone" => {
2902                *avc = avc.clone().with_right_upper_arm_bone(arg_str(args, 0)?)
2903            }
2904            "right_lower_arm_bone" => {
2905                *avc = avc.clone().with_right_lower_arm_bone(arg_str(args, 0)?)
2906            }
2907            "left_arm_pole_direction" => {
2908                *avc = avc
2909                    .clone()
2910                    .with_left_arm_pole_direction(arg_f32_arr::<3>(args, 0)?)
2911            }
2912            "right_arm_pole_direction" => {
2913                *avc = avc
2914                    .clone()
2915                    .with_right_arm_pole_direction(arg_f32_arr::<3>(args, 0)?)
2916            }
2917            "initial_yaw" => *avc = avc.clone().with_initial_yaw(arg_f32(args, 0)?),
2918            "forward_plus_z" => *avc = avc.clone().with_forward_plus_z(),
2919            "ik_debug" => *avc = avc.clone().with_ik_debug(),
2920            "calibrate_hand_transforms" => *avc = avc.clone().with_calibrate_hand_transforms(),
2921            "body_yaw_threshold" => *avc = avc.clone().with_body_yaw_threshold(arg_f32(args, 0)?),
2922            "body_yaw_rate" => *avc = avc.clone().with_body_yaw_rate(arg_f32(args, 0)?),
2923            "hand_rotation_smoothing" => {
2924                *avc = avc.clone().with_hand_rotation_smoothing(arg_f32(args, 0)?)
2925            }
2926            "camera_bone" => *avc = avc.clone().with_camera_bone(arg_str(args, 0)?),
2927            "avatar_height" => *avc = avc.clone().with_avatar_height(arg_f32(args, 0)?),
2928            "eye_height_from_head_bone" => {
2929                *avc = avc
2930                    .clone()
2931                    .with_eye_height_from_head_bone(arg_f32(args, 0)?)
2932            }
2933            "head_ik_eye_height" => *avc = avc.clone().with_head_ik_eye_height(arg_f32(args, 0)?),
2934            "hand_grip_rotation_left" => {
2935                *avc = avc
2936                    .clone()
2937                    .with_hand_grip_rotation_left(arg_f32_arr::<4>(args, 0)?)
2938            }
2939            "hand_grip_rotation_right" => {
2940                *avc = avc
2941                    .clone()
2942                    .with_hand_grip_rotation_right(arg_f32_arr::<4>(args, 0)?)
2943            }
2944            "hips_bone" => *avc = avc.clone().with_hips_bone(arg_str(args, 0)?),
2945            _ => {}
2946        }
2947        return Ok(());
2948    }
2949
2950    if let Some(tex) = world.get_component_by_id_as_mut::<TextureComponent>(id) {
2951        match method {
2952            "render_image" => *tex = TextureComponent::render_image(arg_str(args, 0)?),
2953            "uri" | "with_uri" => *tex = TextureComponent::with_uri(arg_str(args, 0)?),
2954            "from_png" => *tex = TextureComponent::from_png(arg_str(args, 0)?),
2955            "from_dds" => *tex = TextureComponent::from_dds(arg_str(args, 0)?),
2956            _ => {}
2957        }
2958        return Ok(());
2959    }
2960    if let Some(grid) = world.get_component_by_id_as_mut::<GridComponent>(id) {
2961        match method {
2962            "spacing" => *grid = grid.with_spacing(arg_f32(args, 0)?),
2963            "size_x" => *grid = grid.with_size_x(arg_f32(args, 0)? as u32),
2964            "size_z" => *grid = grid.with_size_z(arg_f32(args, 0)? as u32),
2965            "enabled" => *grid = grid.with_enabled(arg_bool(args, 0)?),
2966            "hidden" => *grid = grid.with_hidden(arg_bool(args, 0)?),
2967            "selectable" => *grid = grid.with_selectable(arg_bool(args, 0)?),
2968            _ => {}
2969        }
2970        return Ok(());
2971    }
2972    if let Some(transition) = world.get_component_by_id_as_mut::<TransitionComponent>(id) {
2973        match method {
2974            "on" => *transition = transition.on(),
2975            "off" => *transition = transition.off(),
2976            "enabled" => *transition = transition.enabled(arg_bool(args, 0)?),
2977            "duration_beats" => {
2978                *transition = transition.with_duration_beats(arg_f32(args, 0)? as f64)
2979            }
2980            "capture_from_current" => {
2981                *transition = transition.with_capture_from_current(arg_bool(args, 0)?)
2982            }
2983            "step" => *transition = transition.with_easing(TransitionEasing::Step),
2984            "linear" => *transition = transition.with_easing(TransitionEasing::Linear),
2985            "ease_in_quad" => *transition = transition.with_easing(TransitionEasing::EaseInQuad),
2986            "ease_out_quad" => *transition = transition.with_easing(TransitionEasing::EaseOutQuad),
2987            "ease_in_out_quad" => {
2988                *transition = transition.with_easing(TransitionEasing::EaseInOutQuad)
2989            }
2990            "ease_in_cubic" => *transition = transition.with_easing(TransitionEasing::EaseInCubic),
2991            "ease_out_cubic" => {
2992                *transition = transition.with_easing(TransitionEasing::EaseOutCubic)
2993            }
2994            "ease_in_out_cubic" => {
2995                *transition = transition.with_easing(TransitionEasing::EaseInOutCubic)
2996            }
2997            "ease_in_out_sine" => {
2998                *transition = transition.with_easing(TransitionEasing::EaseInOutSine)
2999            }
3000            "replace_same_target" => {
3001                *transition = transition.with_replace(TransitionReplacePolicy::ReplaceSameTarget)
3002            }
3003            "allow_parallel" => {
3004                *transition = transition.with_replace(TransitionReplacePolicy::AllowParallel)
3005            }
3006            _ => {}
3007        }
3008        return Ok(());
3009    }
3010    if let Some(uv) = world.get_component_by_id_as_mut::<UVComponent>(id) {
3011        if method == "uv" {
3012            *uv = uv.clone().with_uv(arg_f32(args, 0)?, arg_f32(args, 1)?);
3013        }
3014        return Ok(());
3015    }
3016    if let Some(ck) = world.get_component_by_id_as_mut::<ClockComponent>(id) {
3017        if method == "bpm" {
3018            *ck = ck.clone().with_bpm(arg_f32(args, 0)? as f64);
3019        }
3020        return Ok(());
3021    }
3022    if world
3023        .get_component_by_id_as::<AnimationComponent>(id)
3024        .is_some()
3025    {
3026        use crate::engine::ecs::component::ResolveTargetsMode;
3027        let scope_src = if method == "scope" {
3028            Some(arg_component_ref(world, args, 0)?)
3029        } else {
3030            None
3031        };
3032        let Some(anim) = world.get_component_by_id_as_mut::<AnimationComponent>(id) else {
3033            return Ok(());
3034        };
3035        match method {
3036            "playing" => *anim = anim.clone().with_state(AnimationState::Playing),
3037            "looping" => *anim = anim.clone().with_state(AnimationState::Looping),
3038            "paused" => *anim = anim.clone().with_state(AnimationState::Paused),
3039            "length" => {
3040                let n = arg_f32(args, 0)? as f64;
3041                *anim = anim.clone().with_length_beats(n);
3042            }
3043            "scope" => {
3044                *anim = anim
3045                    .clone()
3046                    .with_scope_source(scope_src.expect("scope arg pre-parsed"));
3047            }
3048            "resolve_targets" => {
3049                let mode = match arg_str(args, 0)? {
3050                    "on_attach" => ResolveTargetsMode::OnAttach,
3051                    "on_play" => ResolveTargetsMode::OnPlay,
3052                    other => {
3053                        return Err(format!(
3054                            "Animation.resolve_targets: expected 'on_attach' or 'on_play', got {other:?}"
3055                        ));
3056                    }
3057                };
3058                *anim = anim.clone().with_resolve_targets(mode);
3059            }
3060            _ => {}
3061        }
3062        return Ok(());
3063    }
3064    if let Some(st) = world.get_component_by_id_as_mut::<StyleComponent>(id) {
3065        match method {
3066            "display" => {
3067                st.display = match arg_str(args, 0)? {
3068                    "block" => Some(Display::Block),
3069                    "inline" => Some(Display::Inline),
3070                    "inline_block" | "inline-block" => Some(Display::InlineBlock),
3071                    "flex" => Some(Display::Flex),
3072                    "none" => Some(Display::None),
3073                    _ => None,
3074                };
3075            }
3076            "width" => st.width = arg_size_dimension(args, 0)?,
3077            "height" => st.height = arg_size_dimension(args, 0)?,
3078            "box_sizing" => {
3079                st.box_sizing = match arg_str(args, 0)? {
3080                    "border_box" | "border-box" => BoxSizing::BorderBox,
3081                    "content_box" | "content-box" => BoxSizing::ContentBox,
3082                    _ => return Ok(()),
3083                };
3084            }
3085            "padding" => st.padding = EdgeInsets::all_dim(arg_size_dimension(args, 0)?),
3086            "padding_xy" => {
3087                st.padding =
3088                    EdgeInsets::axes_dim(arg_size_dimension(args, 0)?, arg_size_dimension(args, 1)?)
3089            }
3090            "margin" => st.margin = EdgeInsets::all_dim(arg_size_dimension(args, 0)?),
3091            "margin_xy" => {
3092                st.margin =
3093                    EdgeInsets::axes_dim(arg_size_dimension(args, 0)?, arg_size_dimension(args, 1)?)
3094            }
3095            "background_color" => st.background_color = Some(arg_f32_arr::<4>(args, 0)?),
3096            "background_z" => st.background_z = Some(arg_f32(args, 0)?),
3097            "color" => st.color = Some(arg_f32_arr::<4>(args, 0)?),
3098            "flex_direction" => {
3099                st.flex_direction = match arg_str(args, 0)? {
3100                    "row" | "Row" => FlexDirection::Row,
3101                    "column" | "Column" => FlexDirection::Column,
3102                    "row_reverse" | "RowReverse" => FlexDirection::RowReverse,
3103                    "column_reverse" | "ColumnReverse" => FlexDirection::ColumnReverse,
3104                    _ => return Ok(()),
3105                };
3106            }
3107            "justify_content" => {
3108                st.justify_content = match arg_str(args, 0)? {
3109                    "flex_start" | "start" => JustifyContent::FlexStart,
3110                    "flex_end" | "end" => JustifyContent::FlexEnd,
3111                    "center" => JustifyContent::Center,
3112                    "space_between" => JustifyContent::SpaceBetween,
3113                    "space_around" => JustifyContent::SpaceAround,
3114                    "space_evenly" => JustifyContent::SpaceEvenly,
3115                    _ => return Ok(()),
3116                };
3117            }
3118            "align_items" => {
3119                st.align_items = match arg_str(args, 0)? {
3120                    "stretch" => AlignItems::Stretch,
3121                    "flex_start" | "start" => AlignItems::FlexStart,
3122                    "flex_end" | "end" => AlignItems::FlexEnd,
3123                    "center" => AlignItems::Center,
3124                    "baseline" => AlignItems::Baseline,
3125                    _ => return Ok(()),
3126                };
3127            }
3128            "text_align" => {
3129                st.text_align = match arg_str(args, 0)? {
3130                    "left" => TextAlign::Left,
3131                    "center" => TextAlign::Center,
3132                    "right" => TextAlign::Right,
3133                    "auto" | "none" => TextAlign::Auto,
3134                    _ => return Ok(()),
3135                };
3136            }
3137            "font_size" => st.font_size = arg_size_dimension(args, 0)?,
3138            "vertical_align" => {
3139                st.vertical_align = match arg_str(args, 0)? {
3140                    "top" => VerticalAlign::Top,
3141                    "middle" | "center" => VerticalAlign::Middle,
3142                    "bottom" => VerticalAlign::Bottom,
3143                    "auto" | "none" => VerticalAlign::Auto,
3144                    _ => return Ok(()),
3145                };
3146            }
3147            "flex_grow" => st.flex_grow = arg_f32(args, 0)?,
3148            "flex_shrink" => st.flex_shrink = arg_f32(args, 0)?,
3149            "gap" => {
3150                st.row_gap = arg_f32(args, 0)?;
3151                st.column_gap = st.row_gap;
3152            }
3153            "row_gap" => st.row_gap = arg_f32(args, 0)?,
3154            "column_gap" => st.column_gap = arg_f32(args, 0)?,
3155            "position" => {
3156                st.position = match arg_str(args, 0)? {
3157                    "static" => Position::Static,
3158                    "relative" => Position::Relative,
3159                    "absolute" => Position::Absolute,
3160                    "fixed" => Position::Fixed,
3161                    _ => return Ok(()),
3162                };
3163            }
3164            "top" => st.top = Some(arg_size_dimension(args, 0)?),
3165            "right" => st.right = Some(arg_size_dimension(args, 0)?),
3166            "bottom" => st.bottom = Some(arg_size_dimension(args, 0)?),
3167            "left" => st.left = Some(arg_size_dimension(args, 0)?),
3168            "overflow" => {
3169                st.overflow = match arg_str(args, 0)? {
3170                    "visible" => Overflow::Visible,
3171                    "hidden" => Overflow::Hidden,
3172                    "scroll" => Overflow::Scroll,
3173                    "auto" => Overflow::Auto,
3174                    _ => return Ok(()),
3175                };
3176            }
3177            "z_index" => st.z_index = Some(arg_f32(args, 0)? as i32),
3178            "flex_wrap" => {
3179                st.flex_wrap = match arg_str(args, 0)? {
3180                    "nowrap" | "no_wrap" => FlexWrap::NoWrap,
3181                    "wrap" => FlexWrap::Wrap,
3182                    "wrap_reverse" => FlexWrap::WrapReverse,
3183                    _ => return Ok(()),
3184                };
3185            }
3186            "word_wrap" => {
3187                st.word_wrap = match arg_str(args, 0)? {
3188                    "normal" => Some(WordWrapMode::Normal),
3189                    "break_word" | "break-word" => Some(WordWrapMode::BreakWord),
3190                    "break_all" | "break-all" => Some(WordWrapMode::BreakAll),
3191                    _ => None,
3192                };
3193            }
3194            "word_wrap_tokens" => {
3195                st.word_wrap_tokens = Some(arg_str_vec(args, 0)?);
3196            }
3197            _ => {}
3198        }
3199        return Ok(());
3200    }
3201    println!("[registry] unhandled call '{method}' on component {id:?}");
3202    Ok(())
3203}
3204
3205fn apply_positional(world: &mut World, id: ComponentId, val: &Value) -> Result<(), String> {
3206    if let Value::String(s) = val {
3207        if let Some(t) = world.get_component_by_id_as_mut::<TextComponent>(id) {
3208            t.text = s.clone();
3209            return Ok(());
3210        }
3211        if let Some(ti) = world.get_component_by_id_as_mut::<TextInputComponent>(id) {
3212            ti.set_text(s.clone());
3213            return Ok(());
3214        }
3215    }
3216    println!("[registry] unhandled positional on component {id:?}");
3217    Ok(())
3218}
3219
3220// ---------------------------------------------------------------------------
3221// Transform builder helper
3222// ---------------------------------------------------------------------------
3223
3224fn apply_transform_builder(
3225    c: TransformComponent,
3226    method: &str,
3227    args: &[Value],
3228) -> Result<TransformComponent, String> {
3229    match method {
3230        "position" => Ok(c.with_position(
3231            arg_world_f32(args, 0)?,
3232            arg_world_f32(args, 1)?,
3233            arg_world_f32(args, 2)?,
3234        )),
3235        "scale" => Ok(c.with_scale(
3236            arg_world_f32(args, 0)?,
3237            arg_world_f32(args, 1)?,
3238            arg_world_f32(args, 2)?,
3239        )),
3240        "rotation" | "rotation_euler" => {
3241            Ok(c.with_rotation_euler(arg_f32(args, 0)?, arg_f32(args, 1)?, arg_f32(args, 2)?))
3242        }
3243        "looking_at" => Ok(c.with_looking_at(arg_f32_arr::<3>(args, 0)?)),
3244        // Lossless quaternion form — used by `to_mms_ast` so saved/cloned
3245        // transforms reproduce exactly, including arbitrary axis rotations.
3246        "rotation_quat" | "quaternion" | "quat" => Ok(c.with_rotation_quat(arg_f32_quat(args)?)),
3247        other => {
3248            println!("[registry] unknown Transform builder: '{other}'");
3249            Ok(c)
3250        }
3251    }
3252}
3253
3254fn arg_f32_quat(args: &[Value]) -> Result<[f32; 4], String> {
3255    match args {
3256        [Value::Array(_)] => val_as_f32_array::<4>(&args[0]),
3257        [_, _, _, _] => Ok([
3258            arg_f32(args, 0)?,
3259            arg_f32(args, 1)?,
3260            arg_f32(args, 2)?,
3261            arg_f32(args, 3)?,
3262        ]),
3263        other => Err(format!(
3264            "expected quaternion as four numeric args or one vec4 array, got {:?}",
3265            other
3266        )),
3267    }
3268}
3269
3270fn apply_fit_bounds_ctor(
3271    c: &mut FitBoundsComponent,
3272    method: &str,
3273    args: &[Value],
3274) -> Result<(), String> {
3275    match method {
3276        "renderable_only" => c.mode = FitBoundsMode::RenderableOnly,
3277        "layout_aware" => c.mode = FitBoundsMode::LayoutAware,
3278        "to" => {
3279            c.target = FitBoundsTarget::ExplicitBounds;
3280            c.target_bounds = val_as_f32_array::<6>(&Value::Array(args.to_vec()))?;
3281        }
3282        "to_container" => c.target = FitBoundsTarget::ParentPaddingBox,
3283        other => {
3284            println!("[registry] unknown FitBounds builder: '{other}'");
3285        }
3286    }
3287    Ok(())
3288}
3289
3290fn apply_layout_bounds_ctor(
3291    c: &mut LayoutBoundsComponent,
3292    method: &str,
3293    args: &[Value],
3294) -> Result<(), String> {
3295    match method {
3296        "content_box" => {
3297            c.content_local = crate::engine::graphics::bounds::Aabb {
3298                min: val_as_f32_array::<3>(&args[0])?,
3299                max: val_as_f32_array::<3>(&args[1])?,
3300            };
3301        }
3302        "padding_box" => {
3303            c.padding_local = crate::engine::graphics::bounds::Aabb {
3304                min: val_as_f32_array::<3>(&args[0])?,
3305                max: val_as_f32_array::<3>(&args[1])?,
3306            };
3307        }
3308        other => {
3309            println!("[registry] unknown LayoutBounds builder: '{other}'");
3310        }
3311    }
3312    Ok(())
3313}
3314
3315#[cfg(test)]
3316mod tests {
3317    use super::*;
3318    use crate::scripting::ast::BlockStatement;
3319    use crate::scripting::object::RuntimeClosure;
3320    use std::collections::HashMap;
3321    use std::sync::Arc;
3322
3323    #[test]
3324    fn spawn_tree_installs_keyframe_callback() {
3325        let ce = MaterializedCE {
3326            component_type: "Keyframe".to_string(),
3327            component_property_assignment_only: false,
3328            ctor_method: Some("at".to_string()),
3329            ctor_args: vec![Value::Number(1.0)],
3330            calls: vec![],
3331            named: vec![],
3332            positionals: vec![],
3333            deferred_block: Some(RuntimeClosure {
3334                body: BlockStatement { statements: vec![] },
3335                captured_env: Arc::new(HashMap::new()),
3336                heap: crate::scripting::object::HeapHandle::new(),
3337                analysis: None,
3338            }),
3339            children: vec![],
3340        };
3341
3342        let mut world = World::default();
3343        let mut emit = crate::engine::ecs::RxWorld::default();
3344        let id = spawn_tree(&ce, None, &mut world, &mut emit).expect("spawn keyframe");
3345
3346        let keyframe = world
3347            .get_component_by_id_as::<KeyframeComponent>(id)
3348            .expect("spawned keyframe exists");
3349        assert!(keyframe.callback.is_some());
3350    }
3351}