1use crate::engine::ecs::SignalEmitter;
2use crate::engine::ecs::component::AssetPayloadComponent;
3use crate::engine::ecs::component::style::VerticalAlign;
4use 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 unsafe { f(&mut *ptr) }
187 })
188}
189
190pub 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 for (method, args) in &ce.calls {
213 apply_call(world, id, method, args)?;
214 }
215
216 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 for val in &ce.positionals {
253 apply_positional(world, id, val)?;
254 }
255
256 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 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 }
279 }
280 }
281
282 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
291pub 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 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
381fn 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
417pub 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 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 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 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
723pub 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 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 let val = expression_to_value(value)?;
804 named.push((name.0.clone(), val));
805 }
806 }
807 _ => {
808 }
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#[allow(dead_code)]
900fn _ce_ast_helpers_used(_: BlockStatement) {}
901
902fn 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
923fn 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
991fn 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
1027pub(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
1045pub(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
1060fn 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
1070pub(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
1131fn 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
1165fn 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 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 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 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 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: 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 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 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 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
3220fn 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 "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}