UNPKG

@playcanvas/react

Version:

A React renderer for PlayCanvas – build interactive 3D applications using React's declarative paradigm.

139 lines 7.09 kB
"use client"; import { jsx as _jsx } from "react/jsx-runtime"; import React, { useEffect, useRef } from 'react'; import { ParentContext } from "../../hooks/use-parent.js"; import { ActionType } from "../types.js"; import { componentSchemaRegistry } from "../utils/schema-registry.js"; import { validatePropsPartial, applyProps, warnOnce } from "../../utils/validation.js"; export const RuleProcessor = ({ entity, rule, originalChildGUIDs }) => { // Store original component values to ensure functional updates always use original values // Map structure: componentType -> propName -> originalValue const originalValuesRef = useRef(new Map()); // Track which entity we captured originals for, so we re-capture if entity changes const capturedEntityRef = useRef(null); // Apply modifications in a batched effect useEffect(() => { // Re-capture originals if entity changed if (capturedEntityRef.current !== entity) { originalValuesRef.current.clear(); capturedEntityRef.current = entity; } // 1. Clear children if requested if (rule.clearChildren) { // Build a Map of current children by GUID for O(1) lookup const childrenByGuid = new Map(entity.children.map(c => [c.getGuid(), c])); // Find original children efficiently const originalChildren = originalChildGUIDs .map(guid => childrenByGuid.get(guid)) .filter((child) => child !== undefined); // Now, we only destroy the original children originalChildren.forEach(child => { entity.removeChild(child); child.destroy(); }); } // 2. First pass: Capture original values for all functional updates for (const [componentType, action] of rule.componentActions.entries()) { if (action.type !== ActionType.MODIFY_COMPONENT) continue; const existingComponent = entity.c?.[componentType]; if (!existingComponent) continue; const modifyAction = action; const { remove, ...mergeProps } = modifyAction.props; if (remove) continue; // Skip removal actions for capturing // Capture original values for functional updates if (!originalValuesRef.current.has(componentType)) { originalValuesRef.current.set(componentType, new Map()); } const comp = existingComponent; const originalValues = originalValuesRef.current.get(componentType); for (const propName in mergeProps) { const propValue = mergeProps[propName]; if (typeof propValue === 'function' && !originalValues.has(propName)) { originalValues.set(propName, comp[propName]); } } } // 3. Second pass: Apply component actions with prop merging using component schemas for (const [componentType, action] of rule.componentActions.entries()) { if (action.type !== ActionType.MODIFY_COMPONENT) continue; // componentType is now type-safe: SupportedComponentType const existingComponent = entity.c?.[componentType]; if (!existingComponent) continue; const modifyAction = action; const { remove, ...mergeProps } = modifyAction.props; // 'remove' prop always wins if (remove) { entity.removeComponent(componentType); continue; } // Get component definition from registry (type-safe now) const componentDef = componentSchemaRegistry[componentType]; if (!componentDef || !componentDef.schema) { throw new Error(`No component schema found for component type "${componentType}". Component must be registered in schema registry.`); } // Execute functional props and prepare for validation const propsToValidate = {}; const comp = existingComponent; const schema = componentDef.schema; const originalValues = originalValuesRef.current.get(componentType); for (const propName in mergeProps) { const propValue = mergeProps[propName]; if (typeof propValue === 'function') { // Functional update (e.g., intensity={(val) => val * 2}) // Always use original value if captured, otherwise fall back to current value const originalValue = originalValues?.get(propName); const valueToUse = originalValue !== undefined ? originalValue : comp[propName]; propsToValidate[propName] = propValue(valueToUse); } else { // Direct value propsToValidate[propName] = propValue; } } // Validate props using component schema const validatedProps = validatePropsPartial(propsToValidate, componentDef, false // Don't warn about unknown props during modification ); // Apply validated props using schema's apply functions applyProps(existingComponent, schema, validatedProps); } }, [entity, rule]); // 3. Render new children const children = []; // Add new children from actions if (rule.addChildren.length > 0) { rule.addChildren.forEach((child, index) => { if (React.isValidElement(child)) { // Check if this is a component that already exists on the entity const childType = child.type; const displayName = childType?.displayName || childType?.name; // Map component display names to component types const componentTypeMap = { 'Light': 'light', 'Render': 'render', 'Camera': 'camera' }; const componentType = componentTypeMap[displayName || '']; if (componentType && entity.c?.[componentType]) { warnOnce(`Cannot add <${displayName}> component to entity "${entity.name}". ` + `Entity already has a ${componentType} component. ` + `Use <Modify.${displayName}> to modify the existing component, or <Modify.${displayName} remove> to remove it first.`); } // Clone with unique key to ensure proper React reconciliation children.push(React.cloneElement(child, { key: `${rule.entityGuid}:add:${index}` })); } else { children.push(child); } }); } // Render children with parent context return (_jsx(ParentContext.Provider, { value: entity, children: children })); }; //# sourceMappingURL=RuleProcessor.js.map