From b42a7b4e43c7f99f5eb551203892a29e00045a0e Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Mon, 1 Dec 2025 22:28:51 +0800 Subject: [PATCH] Feature/editor optimization (#251) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 编辑器/运行时架构拆分与构建系统升级 * feat(core): 层级系统重构与UI变换矩阵修复 * refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题 * fix(physics): 修复跨包组件类引用问题 * feat: 统一运行时架构与浏览器运行支持 * feat(asset): 实现浏览器运行时资产加载系统 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误 * test: 补齐核心模块测试用例,修复CI构建配置 * fix: 修复测试用例中的类型错误和断言问题 * fix: 修复 turbo build:npm 任务的依赖顺序问题 * fix: 修复 CI 构建错误并优化构建性能 --- .github/workflows/ci.yml | 107 +- .github/workflows/release-editor.yml | 96 +- .gitignore | 3 + docs/.vitepress/config.mjs | 1 + docs/guide/entity.md | 14 +- docs/guide/hierarchy.md | 437 ++++++ package.json | 26 +- packages/asset-system/package.json | 25 +- packages/asset-system/rollup.config.js | 49 - packages/asset-system/tsconfig.build.json | 22 + packages/asset-system/tsup.config.ts | 7 + packages/audio/package.json | 46 + packages/audio/src/AudioPlugin.ts | 24 + packages/audio/src/AudioSourceComponent.ts | 43 + packages/audio/src/index.ts | 2 + packages/audio/tsconfig.build.json | 12 + packages/audio/tsconfig.json | 13 + packages/audio/tsup.config.ts | 7 + packages/behavior-tree-editor/package.json | 49 + .../src}/BehaviorTreePlugin.ts | 8 +- .../src}/PluginContext.ts | 0 .../application/commands/CommandManager.ts | 0 .../src}/application/commands/ICommand.ts | 0 .../src}/application/commands/ITreeState.ts | 0 .../commands/tree/AddConnectionCommand.ts | 0 .../commands/tree/CreateNodeCommand.ts | 0 .../commands/tree/DeleteNodeCommand.ts | 0 .../commands/tree/MoveNodeCommand.ts | 0 .../commands/tree/RemoveConnectionCommand.ts | 0 .../commands/tree/UpdateNodeDataCommand.ts | 0 .../src}/application/commands/tree/index.ts | 0 .../application/interfaces/IExecutionHooks.ts | 0 .../application/services/BlackboardManager.ts | 0 .../services/ExecutionController.ts | 0 .../services/GlobalBlackboardService.ts | 2 +- .../state/BehaviorTreeDataStore.ts | 2 +- .../use-cases/AddConnectionUseCase.ts | 0 .../use-cases/CreateNodeUseCase.ts | 2 +- .../use-cases/DeleteNodeUseCase.ts | 0 .../application/use-cases/MoveNodeUseCase.ts | 0 .../use-cases/RemoveConnectionUseCase.ts | 0 .../use-cases/UpdateNodeDataUseCase.ts | 0 .../use-cases/ValidateTreeUseCase.ts | 0 .../src}/application/use-cases/index.ts | 0 .../src}/compiler/BehaviorTreeCompiler.tsx | 2 +- .../src}/components/BehaviorTreeEditor.tsx | 2 +- .../components/blackboard/BlackboardPanel.tsx | 0 .../components/canvas/BehaviorTreeCanvas.tsx | 0 .../src}/components/canvas/GridBackground.tsx | 0 .../src}/components/canvas/index.ts | 0 .../src}/components/common/DraggablePanel.tsx | 0 .../connections/ConnectionLayer.tsx | 0 .../connections/ConnectionRenderer.tsx | 0 .../src}/components/connections/index.ts | 0 .../src}/components/menu/NodeContextMenu.tsx | 0 .../src}/components/menu/QuickCreateMenu.tsx | 2 +- .../components/nodes/BehaviorTreeNode.tsx | 2 +- .../nodes/BehaviorTreeNodeRenderer.tsx | 0 .../src}/components/nodes/index.ts | 0 .../panels/BehaviorTreeEditorPanel.css | 0 .../panels/BehaviorTreeEditorPanel.tsx | 0 .../panels/BehaviorTreePropertiesPanel.css | 0 .../src}/components/toolbar/EditorToolbar.tsx | 0 .../src}/config/editorConstants.ts | 2 +- .../src}/constants/index.ts | 0 .../src}/domain/constants/RootNode.ts | 2 +- .../src}/domain/errors/DomainError.ts | 0 .../src}/domain/errors/NodeNotFoundError.ts | 0 .../src}/domain/errors/ValidationError.ts | 0 .../src}/domain/errors/index.ts | 0 .../src}/domain/index.ts | 0 .../src}/domain/interfaces/INodeFactory.ts | 2 +- .../src}/domain/interfaces/IRepository.ts | 0 .../src}/domain/interfaces/ISerializer.ts | 0 .../src}/domain/interfaces/IValidator.ts | 0 .../src}/domain/interfaces/index.ts | 0 .../src}/domain/models/BehaviorTree.ts | 0 .../src}/domain/models/Blackboard.ts | 0 .../src}/domain/models/Connection.ts | 0 .../src}/domain/models/Node.ts | 2 +- .../src}/domain/models/index.ts | 0 .../src}/domain/services/TreeValidator.ts | 0 .../src}/domain/services/index.ts | 0 .../src}/domain/value-objects/NodeType.ts | 0 .../src}/domain/value-objects/Position.ts | 0 .../src}/domain/value-objects/Size.ts | 0 .../src}/domain/value-objects/index.ts | 0 .../GlobalBlackboardTypeGenerator.ts | 2 +- .../LocalBlackboardTypeGenerator.ts | 0 .../src}/hooks/index.ts | 0 .../src}/hooks/useCanvasInteraction.ts | 0 .../src}/hooks/useCanvasMouseEvents.ts | 0 .../src}/hooks/useCommandHistory.ts | 0 .../src}/hooks/useConnectionOperations.ts | 0 .../src}/hooks/useContextMenu.ts | 0 .../src}/hooks/useDropHandler.ts | 2 +- .../src}/hooks/useEditorHandlers.ts | 0 .../src}/hooks/useEditorState.ts | 0 .../src}/hooks/useExecutionController.ts | 0 .../src}/hooks/useKeyboardShortcuts.ts | 0 .../src}/hooks/useNodeDrag.ts | 0 .../src}/hooks/useNodeOperations.ts | 2 +- .../src}/hooks/useNodeTracking.ts | 0 .../src}/hooks/usePortConnection.ts | 2 +- .../src}/hooks/useQuickCreateMenu.ts | 2 +- .../src}/index.ts | 12 +- .../infrastructure/events/EditorEventBus.ts | 0 .../infrastructure/factories/NodeFactory.ts | 2 +- .../src}/infrastructure/factories/index.ts | 0 .../src}/infrastructure/index.ts | 0 .../serialization/BehaviorTreeSerializer.ts | 2 +- .../infrastructure/serialization/index.ts | 0 .../services/NodeRegistryService.ts | 2 +- .../validation/BehaviorTreeValidator.ts | 0 .../src}/interfaces/IEditorExtensions.ts | 2 +- .../BehaviorTreeNodeInspectorProvider.tsx | 2 +- .../src}/services/BehaviorTreeService.ts | 0 .../src}/services/FileSystemService.ts | 0 .../src}/services/NotificationService.ts | 0 .../src}/services/index.ts | 0 .../src}/stores/ExecutionStatsStore.ts | 0 .../src}/stores/index.ts | 0 .../src}/stores/useUIStore.ts | 0 .../src}/styles/BehaviorTreeNode.css | 0 .../src}/styles/Toast.css | 0 .../src}/types/Breakpoint.ts | 0 .../src}/types/index.ts | 0 .../src}/utils/BehaviorTreeExecutor.ts | 8 +- .../src}/utils/DOMCache.ts | 0 .../src}/utils/RuntimeLoader.ts | 2 +- .../src}/utils/portUtils.ts | 0 .../behavior-tree-editor/tsconfig.build.json | 23 + packages/behavior-tree-editor/tsconfig.json | 11 + packages/behavior-tree-editor/tsup.config.ts | 7 + packages/behavior-tree/jest.config.cjs | 5 +- packages/behavior-tree/package.json | 144 +- .../src/BehaviorTreeRuntimeModule.ts | 58 +- packages/behavior-tree/src/constants.ts | 8 + packages/behavior-tree/src/index.ts | 12 +- .../src/loaders/BehaviorTreeLoader.ts | 2 +- packages/behavior-tree/src/runtime.ts | 36 - packages/behavior-tree/tsconfig.build.json | 23 + packages/behavior-tree/tsconfig.json | 2 +- packages/behavior-tree/tsup.config.ts | 7 + packages/behavior-tree/vite.config.ts | 112 -- packages/blueprint-editor/package.json | 49 + .../blueprint-editor/src/BlueprintPlugin.ts | 126 ++ .../src}/components/BlueprintCanvas.tsx | 9 +- .../src/components/BlueprintEditorPanel.tsx | 90 ++ .../src}/components/index.ts | 0 .../editor => blueprint-editor/src}/index.ts | 0 .../src}/stores/blueprintEditorStore.ts | 40 +- .../src}/stores/index.ts | 0 packages/blueprint-editor/tsconfig.build.json | 23 + packages/blueprint-editor/tsconfig.json | 11 + packages/blueprint-editor/tsup.config.ts | 7 + packages/blueprint/package.json | 121 +- .../blueprint/src/editor/BlueprintPlugin.ts | 180 --- .../components/BlueprintEditorPanel.tsx | 48 - packages/blueprint/tsconfig.build.json | 23 + packages/blueprint/tsup.config.ts | 7 + packages/blueprint/vite.config.ts | 106 -- packages/build-config/README.md | 215 +++ packages/build-config/package.json | 39 + packages/build-config/src/index.ts | 90 ++ .../build-config/src/plugins/block-editor.ts | 100 ++ .../build-config/src/plugins/css-inject.ts | 71 + packages/build-config/src/plugins/index.ts | 7 + .../build-config/src/presets/editor-only.ts | 109 ++ packages/build-config/src/presets/index.ts | 10 + .../build-config/src/presets/plugin-tsup.ts | 157 ++ packages/build-config/src/presets/plugin.ts | 176 +++ .../build-config/src/presets/runtime-only.ts | 78 + packages/build-config/src/types.ts | 107 ++ .../templates/plugin/package.json.template | 62 + .../templates/plugin/plugin.json.template | 22 + .../plugin/src/RuntimeModule.ts.template | 49 + .../plugin/src/editor/Plugin.ts.template | 34 + .../plugin/src/editor/index.ts.template | 13 + .../templates/plugin/src/index.ts.template | 14 + .../templates/plugin/src/runtime.ts.template | 17 + .../plugin/tsconfig.build.json.template | 23 + .../templates/plugin/tsconfig.json.template | 20 + .../templates/plugin/tsup.config.ts.template | 7 + .../runtime-only/package.json.template | 38 + .../runtime-only/src/index.ts.template | 8 + .../runtime-only/tsconfig.build.json.template | 22 + .../runtime-only/tsconfig.json.template | 19 + .../runtime-only/tsup.config.ts.template | 7 + packages/build-config/tsconfig.json | 17 + packages/camera-editor/package.json | 43 + packages/camera-editor/src/index.ts | 91 ++ packages/camera-editor/tsconfig.build.json | 13 + packages/camera-editor/tsconfig.json | 16 + packages/camera-editor/tsup.config.ts | 7 + packages/camera/package.json | 47 + .../src/CameraComponent.ts | 32 +- packages/camera/src/CameraPlugin.ts | 24 + packages/camera/src/index.ts | 2 + packages/camera/tsconfig.build.json | 12 + packages/camera/tsconfig.json | 13 + packages/camera/tsup.config.ts | 7 + packages/components/package.json | 54 - packages/components/plugin.json | 20 - .../components/src/AudioSourceComponent.ts | 39 - packages/components/src/CorePlugin.ts | 136 -- packages/components/src/TextComponent.ts | 46 - packages/components/src/TransformComponent.ts | 61 - packages/components/src/index.ts | 21 - packages/components/tsconfig.json | 52 - packages/core/src/Core.ts | 13 +- .../src/ECS/Components/HierarchyComponent.ts | 56 + packages/core/src/ECS/Components/index.ts | 1 + .../src/ECS/Core/FluentAPI/EntityBuilder.ts | 13 +- packages/core/src/ECS/Entity.ts | 311 +--- packages/core/src/ECS/EntityTags.ts | 95 ++ packages/core/src/ECS/Scene.ts | 68 +- .../src/ECS/Serialization/EntitySerializer.ts | 83 +- .../Serialization/IncrementalSerializer.ts | 38 +- .../src/ECS/Serialization/SceneSerializer.ts | 63 +- .../core/src/ECS/Systems/HierarchySystem.ts | 549 +++++++ packages/core/src/ECS/Systems/index.ts | 1 + packages/core/src/ECS/TypedEntity.ts | 4 +- packages/core/src/ECS/index.ts | 2 + packages/core/src/Types/index.ts | 2 - .../Utils/Debug/AdvancedProfilerCollector.ts | 145 +- packages/core/src/Utils/Debug/DebugManager.ts | 14 +- .../src/Utils/Debug/EntityDataCollector.ts | 99 +- .../core/src/Utils/Profiler/AutoProfiler.ts | 649 ++++++++ .../core/src/Utils/Profiler/ProfilerSDK.ts | 145 +- .../core/src/Utils/Profiler/ProfilerTypes.ts | 2 + packages/core/src/Utils/Profiler/index.ts | 2 + packages/core/tests/Core.test.ts | 465 ------ .../core/tests/ECS/Core/FluentAPI.test.ts | 691 --------- .../ECS/Core/FluentAPI/EntityBuilder.test.ts | 321 ++++ .../tests/ECS/EntityRefIntegration.test.ts | 274 ---- packages/core/tests/ECS/EntityTags.test.ts | 237 +++ packages/core/tests/ECS/Hierarchy.test.ts | 648 ++++++++ packages/core/tests/ECS/Scene.test.ts | 197 +++ .../Serialization/EntitySerializer.test.ts | 495 ++++++ .../ParentChildSerialization.test.ts | 117 -- .../ECS/Serialization/SceneSerializer.test.ts | 439 ++++++ .../ECS/Serialization/Serialization.test.ts | 629 -------- packages/core/tests/SceneQuery.test.ts | 145 -- packages/core/tests/TypeInference.test.ts | 205 --- .../Utils/Debug/EntityDataCollector.test.ts | 410 +++++ .../tests/Utils/Profiler/AutoProfiler.test.ts | 417 +++++ packages/ecs-engine-bindgen/package.json | 31 +- .../src/core/SpriteRenderHelper.ts | 8 +- packages/ecs-engine-bindgen/src/index.ts | 6 +- .../src/systems/CameraSystem.ts | 2 +- .../src/systems/EngineRenderSystem.ts | 28 +- .../ecs-engine-bindgen/tsconfig.build.json | 10 + packages/ecs-engine-bindgen/tsconfig.json | 19 +- packages/ecs-engine-bindgen/tsup.config.ts | 9 + packages/editor-app/package.json | 11 +- .../editor-app/src-tauri/src/profiler_ws.rs | 8 +- packages/editor-app/src/App.tsx | 100 +- .../src/app/managers/PluginInstaller.ts | 19 +- .../src/app/managers/ServiceRegistry.ts | 17 +- .../CreateAnimatedSpriteEntityCommand.ts | 11 +- .../entity/CreateCameraEntityCommand.ts | 11 +- .../commands/entity/CreateEntityCommand.ts | 12 +- .../entity/CreateSpriteEntityCommand.ts | 11 +- .../entity/CreateTilemapEntityCommand.ts | 10 +- .../commands/entity/DeleteEntityCommand.ts | 51 +- .../commands/entity/ReparentEntityCommand.ts | 194 +++ .../src/application/commands/entity/index.ts | 1 + .../src/components/AdvancedProfiler.tsx | 306 ++-- .../src/components/AdvancedProfilerWindow.tsx | 49 +- .../src/components/ConsolePanel.tsx | 331 ---- .../src/components/ContentBrowser.tsx | 225 ++- .../editor-app/src/components/ContextMenu.tsx | 143 +- .../editor-app/src/components/FileTree.tsx | 175 ++- .../src/components/OutputLogPanel.tsx | 223 ++- .../src/components/PluginListSetting.tsx | 111 +- .../editor-app/src/components/PortManager.tsx | 10 +- .../src/components/SceneHierarchy.tsx | 483 ++++-- .../src/components/SettingsWindow.tsx | 20 + .../editor-app/src/components/StartupPage.tsx | 14 +- .../editor-app/src/components/Viewport.tsx | 178 ++- .../src/components/inspectors/Inspector.tsx | 9 + .../TransformComponentInspector.tsx | 2 +- packages/editor-app/src/gizmos/SpriteGizmo.ts | 3 +- .../AnimationClipsFieldEditor.tsx | 2 +- packages/editor-app/src/locales/en.ts | 3 +- packages/editor-app/src/locales/zh.ts | 3 +- .../builtin/EditorAppearancePlugin.tsx | 4 +- .../src/plugins/builtin/GizmoPlugin.ts | 4 +- .../plugins/builtin/PluginConfigPlugin.tsx | 4 +- .../src/plugins/builtin/ProfilerPlugin.tsx | 32 +- .../plugins/builtin/ProjectSettingsPlugin.tsx | 4 +- .../plugins/builtin/SceneInspectorPlugin.ts | 12 +- .../editor-app/src/plugins/builtin/index.ts | 3 +- .../src/services/EditorEngineSync.ts | 3 +- .../editor-app/src/services/EngineService.ts | 878 ++++------- .../src/services/PluginSDKRegistry.ts | 25 +- .../src/services/ProfilerService.ts | 48 +- .../src/styles/AdvancedProfiler.css | 35 + .../editor-app/src/styles/ConsolePanel.css | 445 ------ .../editor-app/src/styles/ContextMenu.css | 47 +- .../editor-app/src/styles/OutputLogPanel.css | 144 +- .../editor-app/src/styles/ProfilerWindow.css | 42 + .../editor-app/src/styles/SceneHierarchy.css | 23 +- packages/editor-core/package.json | 12 +- packages/editor-core/rollup.config.cjs | 8 +- .../editor-core/src/Plugin/IPluginLoader.ts | 166 +- .../src/Plugin/PluginDescriptor.ts | 166 +- .../editor-core/src/Plugin/PluginManager.ts | 761 +++++++--- .../src/Services/AssetRegistryService.ts | 650 ++++++++ .../src/Services/EntityStoreService.ts | 10 +- .../src/Services/FileActionRegistry.ts | 6 + .../editor-core/src/Services/LogService.ts | 27 +- .../src/Services/ProjectService.ts | 74 +- .../src/Services/PropertyMetadata.ts | 13 +- .../src/Services/SettingsRegistry.ts | 1 + packages/editor-core/src/index.ts | 1 + packages/editor-runtime/package.json | 10 +- packages/editor-runtime/src/index.ts | 1 + packages/engine-core/package.json | 44 + packages/engine-core/src/EnginePlugin.ts | 227 +++ .../engine-core/src/HierarchyComponent.ts | 2 + packages/engine-core/src/HierarchySystem.ts | 2 + .../engine-core/src/TransformComponent.ts | 110 ++ packages/engine-core/src/TransformSystem.ts | 145 ++ packages/engine-core/src/index.ts | 17 + packages/engine-core/tsconfig.build.json | 12 + packages/engine-core/tsconfig.json | 13 + packages/engine-core/tsup.config.ts | 7 + packages/node-editor/package.json | 14 +- packages/physics-rapier2d-editor/package.json | 47 + .../src/Physics2DEditorModule.ts} | 30 +- .../src/Physics2DPlugin.ts | 37 + .../components/CollisionLayerSelector.tsx | 2 +- .../src}/components/CollisionMatrixEditor.tsx | 2 +- .../src}/gizmos/Physics2DGizmo.ts | 16 +- packages/physics-rapier2d-editor/src/index.ts | 11 + .../inspectors/BoxCollider2DInspector.tsx | 3 +- .../inspectors/CircleCollider2DInspector.tsx | 3 +- .../src}/inspectors/Rigidbody2DInspector.tsx | 3 +- .../tsconfig.build.json | 23 + .../physics-rapier2d-editor/tsconfig.json | 12 + .../physics-rapier2d-editor/tsup.config.ts | 7 + packages/physics-rapier2d/package.json | 44 +- .../src/Physics2DComponentsModule.ts | 4 +- .../src/PhysicsEditorPlugin.ts | 40 + .../src/PhysicsRuntimeModule.ts | 90 +- .../src/components/Collider2DBase.ts | 3 +- .../src/components/Rigidbody2DComponent.ts | 3 +- .../src/editor/Physics2DPlugin.ts | 65 - packages/physics-rapier2d/src/index.ts | 4 +- packages/physics-rapier2d/src/runtime.ts | 5 +- .../physics-rapier2d/src/runtime/index.ts | 4 +- .../src/systems/Physics2DSystem.ts | 16 +- packages/physics-rapier2d/tsconfig.build.json | 23 + packages/physics-rapier2d/tsconfig.json | 2 +- packages/physics-rapier2d/tsup.config.ts | 23 + packages/physics-rapier2d/vite.config.ts | 47 - packages/platform-common/tsconfig.json | 5 +- packages/platform-web/package.json | 24 +- .../platform-web/rollup.runtime.config.js | 13 +- packages/platform-web/src/RuntimeSystems.ts | 410 ----- packages/platform-web/src/index.ts | 34 +- packages/platform-web/src/runtime.ts | 238 ++- .../src/systems/Canvas2DRenderSystem.ts | 14 +- packages/platform-web/tsconfig.json | 5 +- packages/platform-wechat/package.json | 2 +- packages/runtime-core/package.json | 41 + packages/runtime-core/src/GameRuntime.ts | 824 ++++++++++ packages/runtime-core/src/IPlatformAdapter.ts | 152 ++ packages/runtime-core/src/PluginLoader.ts | 118 ++ packages/runtime-core/src/PluginManager.ts | 166 ++ packages/runtime-core/src/ProjectConfig.ts | 128 ++ packages/runtime-core/src/RuntimeBootstrap.ts | 66 + .../src/adapters/BrowserPlatformAdapter.ts | 143 ++ .../src/adapters/EditorPlatformAdapter.ts | 185 +++ packages/runtime-core/src/adapters/index.ts | 7 + packages/runtime-core/src/index.ts | 72 + .../src/services/BrowserFileSystemService.ts | 306 ++++ packages/runtime-core/tsconfig.build.json | 12 + packages/runtime-core/tsconfig.json | 16 + packages/runtime-core/tsup.config.ts | 7 + packages/sprite-editor/package.json | 44 + packages/sprite-editor/src/index.ts | 147 ++ packages/sprite-editor/tsconfig.build.json | 13 + packages/sprite-editor/tsconfig.json | 16 + packages/sprite-editor/tsup.config.ts | 7 + packages/sprite/package.json | 48 + .../src/SpriteAnimatorComponent.ts | 0 .../src/SpriteComponent.ts | 0 packages/sprite/src/SpriteRuntimeModule.ts | 42 + packages/sprite/src/index.ts | 5 + .../src/systems/SpriteAnimatorSystem.ts | 0 packages/sprite/tsconfig.build.json | 12 + packages/sprite/tsconfig.json | 14 + packages/sprite/tsup.config.ts | 7 + packages/tilemap-editor/package.json | 47 + .../src}/components/TilemapCanvas.tsx | 2 +- .../src}/components/TilesetPreview.tsx | 0 .../src}/components/panels/LayerPanel.tsx | 2 +- .../panels/TileSetSelectorPanel.tsx | 0 .../components/panels/TilemapDetailsPanel.tsx | 2 +- .../components/panels/TilemapEditorPanel.tsx | 2 +- .../src}/components/panels/TilesetPanel.tsx | 2 +- .../src}/gizmos/TilemapGizmo.ts | 6 +- .../editor => tilemap-editor/src}/index.ts | 39 +- .../providers/TilemapInspectorProvider.tsx | 2 +- .../src}/stores/TilemapEditorStore.ts | 0 .../src}/styles/TileSetSelectorPanel.css | 0 .../src}/styles/TilemapDetailsPanel.css | 0 .../src}/styles/TilemapEditor.css | 0 .../src}/tools/BrushTool.ts | 0 .../src}/tools/EraserTool.ts | 0 .../src}/tools/FillTool.ts | 0 .../src}/tools/ITilemapTool.ts | 2 +- packages/tilemap-editor/tsconfig.build.json | 23 + packages/tilemap-editor/tsconfig.json | 11 + packages/tilemap-editor/tsup.config.ts | 7 + packages/tilemap/package.json | 127 +- packages/tilemap/src/TilemapRuntimeModule.ts | 74 +- packages/tilemap/src/constants.ts | 9 + packages/tilemap/src/editor/TilemapPlugin.ts | 60 - packages/tilemap/src/index.ts | 13 +- packages/tilemap/src/loaders/TilemapLoader.ts | 2 +- packages/tilemap/src/loaders/TilesetLoader.ts | 2 +- .../src/physics/TilemapPhysicsSystem.ts | 2 +- packages/tilemap/src/runtime.ts | 37 - .../src/systems/TilemapRenderingSystem.ts | 2 +- packages/tilemap/tsconfig.build.json | 23 + packages/tilemap/tsconfig.json | 2 +- packages/tilemap/tsup.config.ts | 7 + packages/tilemap/vite.config.ts | 110 -- packages/ui-editor/package.json | 47 + .../src}/gizmos/UITransformGizmo.ts | 37 +- .../editor => ui-editor/src}/gizmos/index.ts | 0 .../{ui/src/editor => ui-editor/src}/index.ts | 42 +- .../src}/inspectors/UITransformInspector.tsx | 2 +- .../src}/inspectors/index.ts | 0 packages/ui-editor/tsconfig.build.json | 23 + packages/ui-editor/tsconfig.json | 11 + packages/ui-editor/tsup.config.ts | 7 + packages/ui/package.json | 51 +- packages/ui/src/UIBuilder.ts | 17 +- packages/ui/src/UIRuntimeModule.ts | 51 +- .../ui/src/components/UITransformComponent.ts | 26 + packages/ui/src/editor/UIPlugin.ts | 159 -- packages/ui/src/index.ts | 7 +- packages/ui/src/runtime.ts | 111 -- packages/ui/src/systems/UILayoutSystem.ts | 208 ++- .../systems/render/UIButtonRenderSystem.ts | 73 +- .../render/UIProgressBarRenderSystem.ts | 192 ++- .../src/systems/render/UIRectRenderSystem.ts | 100 +- .../render/UIScrollViewRenderSystem.ts | 39 +- .../systems/render/UISliderRenderSystem.ts | 55 +- .../src/systems/render/UITextRenderSystem.ts | 26 +- packages/ui/tsconfig.build.json | 23 + packages/ui/tsconfig.json | 1 - packages/ui/tsup.config.ts | 7 + packages/ui/vite.config.ts | 46 - pnpm-lock.yaml | 1342 +++++++++++++---- screenshots/about.png | Bin 29695 -> 33682 bytes screenshots/main_screetshot.png | Bin 124826 -> 179493 bytes screenshots/performance_profiler.png | Bin 42737 -> 577901 bytes screenshots/plugin_manager.png | Bin 29855 -> 62153 bytes screenshots/settings.png | Bin 45979 -> 45227 bytes scripts/create-package.mjs | 307 ++++ tsconfig.base.json | 21 + turbo.json | 48 + 468 files changed, 18301 insertions(+), 9075 deletions(-) create mode 100644 docs/guide/hierarchy.md delete mode 100644 packages/asset-system/rollup.config.js create mode 100644 packages/asset-system/tsconfig.build.json create mode 100644 packages/asset-system/tsup.config.ts create mode 100644 packages/audio/package.json create mode 100644 packages/audio/src/AudioPlugin.ts create mode 100644 packages/audio/src/AudioSourceComponent.ts create mode 100644 packages/audio/src/index.ts create mode 100644 packages/audio/tsconfig.build.json create mode 100644 packages/audio/tsconfig.json create mode 100644 packages/audio/tsup.config.ts create mode 100644 packages/behavior-tree-editor/package.json rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/BehaviorTreePlugin.ts (78%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/PluginContext.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/commands/CommandManager.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/commands/ICommand.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/commands/ITreeState.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/commands/tree/AddConnectionCommand.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/commands/tree/CreateNodeCommand.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/commands/tree/DeleteNodeCommand.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/commands/tree/MoveNodeCommand.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/commands/tree/RemoveConnectionCommand.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/commands/tree/UpdateNodeDataCommand.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/commands/tree/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/interfaces/IExecutionHooks.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/services/BlackboardManager.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/services/ExecutionController.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/services/GlobalBlackboardService.ts (98%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/state/BehaviorTreeDataStore.ts (99%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/use-cases/AddConnectionUseCase.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/use-cases/CreateNodeUseCase.ts (95%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/use-cases/DeleteNodeUseCase.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/use-cases/MoveNodeUseCase.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/use-cases/RemoveConnectionUseCase.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/use-cases/UpdateNodeDataUseCase.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/use-cases/ValidateTreeUseCase.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/application/use-cases/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/compiler/BehaviorTreeCompiler.tsx (99%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/BehaviorTreeEditor.tsx (99%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/blackboard/BlackboardPanel.tsx (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/canvas/BehaviorTreeCanvas.tsx (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/canvas/GridBackground.tsx (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/canvas/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/common/DraggablePanel.tsx (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/connections/ConnectionLayer.tsx (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/connections/ConnectionRenderer.tsx (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/connections/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/menu/NodeContextMenu.tsx (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/menu/QuickCreateMenu.tsx (99%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/nodes/BehaviorTreeNode.tsx (99%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/nodes/BehaviorTreeNodeRenderer.tsx (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/nodes/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/panels/BehaviorTreeEditorPanel.css (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/panels/BehaviorTreeEditorPanel.tsx (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/panels/BehaviorTreePropertiesPanel.css (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/components/toolbar/EditorToolbar.tsx (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/config/editorConstants.ts (94%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/constants/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/constants/RootNode.ts (91%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/errors/DomainError.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/errors/NodeNotFoundError.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/errors/ValidationError.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/errors/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/interfaces/INodeFactory.ts (91%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/interfaces/IRepository.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/interfaces/ISerializer.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/interfaces/IValidator.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/interfaces/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/models/BehaviorTree.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/models/Blackboard.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/models/Connection.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/models/Node.ts (98%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/models/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/services/TreeValidator.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/services/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/value-objects/NodeType.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/value-objects/Position.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/value-objects/Size.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/domain/value-objects/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/generators/GlobalBlackboardTypeGenerator.ts (99%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/generators/LocalBlackboardTypeGenerator.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/useCanvasInteraction.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/useCanvasMouseEvents.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/useCommandHistory.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/useConnectionOperations.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/useContextMenu.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/useDropHandler.ts (98%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/useEditorHandlers.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/useEditorState.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/useExecutionController.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/useKeyboardShortcuts.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/useNodeDrag.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/useNodeOperations.ts (97%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/useNodeTracking.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/usePortConnection.ts (99%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/hooks/useQuickCreateMenu.ts (99%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/index.ts (96%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/infrastructure/events/EditorEventBus.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/infrastructure/factories/NodeFactory.ts (97%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/infrastructure/factories/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/infrastructure/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/infrastructure/serialization/BehaviorTreeSerializer.ts (99%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/infrastructure/serialization/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/infrastructure/services/NodeRegistryService.ts (98%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/infrastructure/validation/BehaviorTreeValidator.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/interfaces/IEditorExtensions.ts (99%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/providers/BehaviorTreeNodeInspectorProvider.tsx (99%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/services/BehaviorTreeService.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/services/FileSystemService.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/services/NotificationService.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/services/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/stores/ExecutionStatsStore.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/stores/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/stores/useUIStore.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/styles/BehaviorTreeNode.css (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/styles/Toast.css (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/types/Breakpoint.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/types/index.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/utils/BehaviorTreeExecutor.ts (99%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/utils/DOMCache.ts (100%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/utils/RuntimeLoader.ts (98%) rename packages/{behavior-tree/src/editor => behavior-tree-editor/src}/utils/portUtils.ts (100%) create mode 100644 packages/behavior-tree-editor/tsconfig.build.json create mode 100644 packages/behavior-tree-editor/tsconfig.json create mode 100644 packages/behavior-tree-editor/tsup.config.ts create mode 100644 packages/behavior-tree/src/constants.ts delete mode 100644 packages/behavior-tree/src/runtime.ts create mode 100644 packages/behavior-tree/tsconfig.build.json create mode 100644 packages/behavior-tree/tsup.config.ts delete mode 100644 packages/behavior-tree/vite.config.ts create mode 100644 packages/blueprint-editor/package.json create mode 100644 packages/blueprint-editor/src/BlueprintPlugin.ts rename packages/{blueprint/src/editor => blueprint-editor/src}/components/BlueprintCanvas.tsx (97%) create mode 100644 packages/blueprint-editor/src/components/BlueprintEditorPanel.tsx rename packages/{blueprint/src/editor => blueprint-editor/src}/components/index.ts (100%) rename packages/{blueprint/src/editor => blueprint-editor/src}/index.ts (100%) rename packages/{blueprint/src/editor => blueprint-editor/src}/stores/blueprintEditorStore.ts (84%) rename packages/{blueprint/src/editor => blueprint-editor/src}/stores/index.ts (100%) create mode 100644 packages/blueprint-editor/tsconfig.build.json create mode 100644 packages/blueprint-editor/tsconfig.json create mode 100644 packages/blueprint-editor/tsup.config.ts delete mode 100644 packages/blueprint/src/editor/BlueprintPlugin.ts delete mode 100644 packages/blueprint/src/editor/components/BlueprintEditorPanel.tsx create mode 100644 packages/blueprint/tsconfig.build.json create mode 100644 packages/blueprint/tsup.config.ts delete mode 100644 packages/blueprint/vite.config.ts create mode 100644 packages/build-config/README.md create mode 100644 packages/build-config/package.json create mode 100644 packages/build-config/src/index.ts create mode 100644 packages/build-config/src/plugins/block-editor.ts create mode 100644 packages/build-config/src/plugins/css-inject.ts create mode 100644 packages/build-config/src/plugins/index.ts create mode 100644 packages/build-config/src/presets/editor-only.ts create mode 100644 packages/build-config/src/presets/index.ts create mode 100644 packages/build-config/src/presets/plugin-tsup.ts create mode 100644 packages/build-config/src/presets/plugin.ts create mode 100644 packages/build-config/src/presets/runtime-only.ts create mode 100644 packages/build-config/src/types.ts create mode 100644 packages/build-config/templates/plugin/package.json.template create mode 100644 packages/build-config/templates/plugin/plugin.json.template create mode 100644 packages/build-config/templates/plugin/src/RuntimeModule.ts.template create mode 100644 packages/build-config/templates/plugin/src/editor/Plugin.ts.template create mode 100644 packages/build-config/templates/plugin/src/editor/index.ts.template create mode 100644 packages/build-config/templates/plugin/src/index.ts.template create mode 100644 packages/build-config/templates/plugin/src/runtime.ts.template create mode 100644 packages/build-config/templates/plugin/tsconfig.build.json.template create mode 100644 packages/build-config/templates/plugin/tsconfig.json.template create mode 100644 packages/build-config/templates/plugin/tsup.config.ts.template create mode 100644 packages/build-config/templates/runtime-only/package.json.template create mode 100644 packages/build-config/templates/runtime-only/src/index.ts.template create mode 100644 packages/build-config/templates/runtime-only/tsconfig.build.json.template create mode 100644 packages/build-config/templates/runtime-only/tsconfig.json.template create mode 100644 packages/build-config/templates/runtime-only/tsup.config.ts.template create mode 100644 packages/build-config/tsconfig.json create mode 100644 packages/camera-editor/package.json create mode 100644 packages/camera-editor/src/index.ts create mode 100644 packages/camera-editor/tsconfig.build.json create mode 100644 packages/camera-editor/tsconfig.json create mode 100644 packages/camera-editor/tsup.config.ts create mode 100644 packages/camera/package.json rename packages/{components => camera}/src/CameraComponent.ts (73%) create mode 100644 packages/camera/src/CameraPlugin.ts create mode 100644 packages/camera/src/index.ts create mode 100644 packages/camera/tsconfig.build.json create mode 100644 packages/camera/tsconfig.json create mode 100644 packages/camera/tsup.config.ts delete mode 100644 packages/components/package.json delete mode 100644 packages/components/plugin.json delete mode 100644 packages/components/src/AudioSourceComponent.ts delete mode 100644 packages/components/src/CorePlugin.ts delete mode 100644 packages/components/src/TextComponent.ts delete mode 100644 packages/components/src/TransformComponent.ts delete mode 100644 packages/components/src/index.ts delete mode 100644 packages/components/tsconfig.json create mode 100644 packages/core/src/ECS/Components/HierarchyComponent.ts create mode 100644 packages/core/src/ECS/Components/index.ts create mode 100644 packages/core/src/ECS/EntityTags.ts create mode 100644 packages/core/src/ECS/Systems/HierarchySystem.ts create mode 100644 packages/core/src/Utils/Profiler/AutoProfiler.ts delete mode 100644 packages/core/tests/Core.test.ts delete mode 100644 packages/core/tests/ECS/Core/FluentAPI.test.ts create mode 100644 packages/core/tests/ECS/Core/FluentAPI/EntityBuilder.test.ts delete mode 100644 packages/core/tests/ECS/EntityRefIntegration.test.ts create mode 100644 packages/core/tests/ECS/EntityTags.test.ts create mode 100644 packages/core/tests/ECS/Hierarchy.test.ts create mode 100644 packages/core/tests/ECS/Serialization/EntitySerializer.test.ts delete mode 100644 packages/core/tests/ECS/Serialization/ParentChildSerialization.test.ts create mode 100644 packages/core/tests/ECS/Serialization/SceneSerializer.test.ts delete mode 100644 packages/core/tests/ECS/Serialization/Serialization.test.ts delete mode 100644 packages/core/tests/SceneQuery.test.ts delete mode 100644 packages/core/tests/TypeInference.test.ts create mode 100644 packages/core/tests/Utils/Debug/EntityDataCollector.test.ts create mode 100644 packages/core/tests/Utils/Profiler/AutoProfiler.test.ts create mode 100644 packages/ecs-engine-bindgen/tsconfig.build.json create mode 100644 packages/ecs-engine-bindgen/tsup.config.ts create mode 100644 packages/editor-app/src/application/commands/entity/ReparentEntityCommand.ts delete mode 100644 packages/editor-app/src/components/ConsolePanel.tsx delete mode 100644 packages/editor-app/src/styles/ConsolePanel.css create mode 100644 packages/editor-core/src/Services/AssetRegistryService.ts create mode 100644 packages/engine-core/package.json create mode 100644 packages/engine-core/src/EnginePlugin.ts create mode 100644 packages/engine-core/src/HierarchyComponent.ts create mode 100644 packages/engine-core/src/HierarchySystem.ts create mode 100644 packages/engine-core/src/TransformComponent.ts create mode 100644 packages/engine-core/src/TransformSystem.ts create mode 100644 packages/engine-core/src/index.ts create mode 100644 packages/engine-core/tsconfig.build.json create mode 100644 packages/engine-core/tsconfig.json create mode 100644 packages/engine-core/tsup.config.ts create mode 100644 packages/physics-rapier2d-editor/package.json rename packages/{physics-rapier2d/src/editor/index.ts => physics-rapier2d-editor/src/Physics2DEditorModule.ts} (94%) create mode 100644 packages/physics-rapier2d-editor/src/Physics2DPlugin.ts rename packages/{physics-rapier2d/src/editor => physics-rapier2d-editor/src}/components/CollisionLayerSelector.tsx (99%) rename packages/{physics-rapier2d/src/editor => physics-rapier2d-editor/src}/components/CollisionMatrixEditor.tsx (99%) rename packages/{physics-rapier2d/src/editor => physics-rapier2d-editor/src}/gizmos/Physics2DGizmo.ts (95%) create mode 100644 packages/physics-rapier2d-editor/src/index.ts rename packages/{physics-rapier2d/src/editor => physics-rapier2d-editor/src}/inspectors/BoxCollider2DInspector.tsx (97%) rename packages/{physics-rapier2d/src/editor => physics-rapier2d-editor/src}/inspectors/CircleCollider2DInspector.tsx (97%) rename packages/{physics-rapier2d/src/editor => physics-rapier2d-editor/src}/inspectors/Rigidbody2DInspector.tsx (98%) create mode 100644 packages/physics-rapier2d-editor/tsconfig.build.json create mode 100644 packages/physics-rapier2d-editor/tsconfig.json create mode 100644 packages/physics-rapier2d-editor/tsup.config.ts create mode 100644 packages/physics-rapier2d/src/PhysicsEditorPlugin.ts delete mode 100644 packages/physics-rapier2d/src/editor/Physics2DPlugin.ts create mode 100644 packages/physics-rapier2d/tsconfig.build.json create mode 100644 packages/physics-rapier2d/tsup.config.ts delete mode 100644 packages/physics-rapier2d/vite.config.ts delete mode 100644 packages/platform-web/src/RuntimeSystems.ts create mode 100644 packages/runtime-core/package.json create mode 100644 packages/runtime-core/src/GameRuntime.ts create mode 100644 packages/runtime-core/src/IPlatformAdapter.ts create mode 100644 packages/runtime-core/src/PluginLoader.ts create mode 100644 packages/runtime-core/src/PluginManager.ts create mode 100644 packages/runtime-core/src/ProjectConfig.ts create mode 100644 packages/runtime-core/src/RuntimeBootstrap.ts create mode 100644 packages/runtime-core/src/adapters/BrowserPlatformAdapter.ts create mode 100644 packages/runtime-core/src/adapters/EditorPlatformAdapter.ts create mode 100644 packages/runtime-core/src/adapters/index.ts create mode 100644 packages/runtime-core/src/index.ts create mode 100644 packages/runtime-core/src/services/BrowserFileSystemService.ts create mode 100644 packages/runtime-core/tsconfig.build.json create mode 100644 packages/runtime-core/tsconfig.json create mode 100644 packages/runtime-core/tsup.config.ts create mode 100644 packages/sprite-editor/package.json create mode 100644 packages/sprite-editor/src/index.ts create mode 100644 packages/sprite-editor/tsconfig.build.json create mode 100644 packages/sprite-editor/tsconfig.json create mode 100644 packages/sprite-editor/tsup.config.ts create mode 100644 packages/sprite/package.json rename packages/{components => sprite}/src/SpriteAnimatorComponent.ts (100%) rename packages/{components => sprite}/src/SpriteComponent.ts (100%) create mode 100644 packages/sprite/src/SpriteRuntimeModule.ts create mode 100644 packages/sprite/src/index.ts rename packages/{components => sprite}/src/systems/SpriteAnimatorSystem.ts (100%) create mode 100644 packages/sprite/tsconfig.build.json create mode 100644 packages/sprite/tsconfig.json create mode 100644 packages/sprite/tsup.config.ts create mode 100644 packages/tilemap-editor/package.json rename packages/{tilemap/src/editor => tilemap-editor/src}/components/TilemapCanvas.tsx (99%) rename packages/{tilemap/src/editor => tilemap-editor/src}/components/TilesetPreview.tsx (100%) rename packages/{tilemap/src/editor => tilemap-editor/src}/components/panels/LayerPanel.tsx (99%) rename packages/{tilemap/src/editor => tilemap-editor/src}/components/panels/TileSetSelectorPanel.tsx (100%) rename packages/{tilemap/src/editor => tilemap-editor/src}/components/panels/TilemapDetailsPanel.tsx (99%) rename packages/{tilemap/src/editor => tilemap-editor/src}/components/panels/TilemapEditorPanel.tsx (99%) rename packages/{tilemap/src/editor => tilemap-editor/src}/components/panels/TilesetPanel.tsx (98%) rename packages/{tilemap/src/editor => tilemap-editor/src}/gizmos/TilemapGizmo.ts (98%) rename packages/{tilemap/src/editor => tilemap-editor/src}/index.ts (92%) rename packages/{tilemap/src/editor => tilemap-editor/src}/providers/TilemapInspectorProvider.tsx (98%) rename packages/{tilemap/src/editor => tilemap-editor/src}/stores/TilemapEditorStore.ts (100%) rename packages/{tilemap/src/editor => tilemap-editor/src}/styles/TileSetSelectorPanel.css (100%) rename packages/{tilemap/src/editor => tilemap-editor/src}/styles/TilemapDetailsPanel.css (100%) rename packages/{tilemap/src/editor => tilemap-editor/src}/styles/TilemapEditor.css (100%) rename packages/{tilemap/src/editor => tilemap-editor/src}/tools/BrushTool.ts (100%) rename packages/{tilemap/src/editor => tilemap-editor/src}/tools/EraserTool.ts (100%) rename packages/{tilemap/src/editor => tilemap-editor/src}/tools/FillTool.ts (100%) rename packages/{tilemap/src/editor => tilemap-editor/src}/tools/ITilemapTool.ts (93%) create mode 100644 packages/tilemap-editor/tsconfig.build.json create mode 100644 packages/tilemap-editor/tsconfig.json create mode 100644 packages/tilemap-editor/tsup.config.ts create mode 100644 packages/tilemap/src/constants.ts delete mode 100644 packages/tilemap/src/editor/TilemapPlugin.ts delete mode 100644 packages/tilemap/src/runtime.ts create mode 100644 packages/tilemap/tsconfig.build.json create mode 100644 packages/tilemap/tsup.config.ts delete mode 100644 packages/tilemap/vite.config.ts create mode 100644 packages/ui-editor/package.json rename packages/{ui/src/editor => ui-editor/src}/gizmos/UITransformGizmo.ts (59%) rename packages/{ui/src/editor => ui-editor/src}/gizmos/index.ts (100%) rename packages/{ui/src/editor => ui-editor/src}/index.ts (94%) rename packages/{ui/src/editor => ui-editor/src}/inspectors/UITransformInspector.tsx (99%) rename packages/{ui/src/editor => ui-editor/src}/inspectors/index.ts (100%) create mode 100644 packages/ui-editor/tsconfig.build.json create mode 100644 packages/ui-editor/tsconfig.json create mode 100644 packages/ui-editor/tsup.config.ts delete mode 100644 packages/ui/src/editor/UIPlugin.ts delete mode 100644 packages/ui/src/runtime.ts create mode 100644 packages/ui/tsconfig.build.json create mode 100644 packages/ui/tsup.config.ts delete mode 100644 packages/ui/vite.config.ts create mode 100644 scripts/create-package.mjs create mode 100644 tsconfig.base.json create mode 100644 turbo.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66de7352..eaac9e8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ on: - 'package.json' - 'pnpm-lock.yaml' - 'tsconfig.json' + - 'turbo.json' - 'jest.config.*' - '.github/workflows/ci.yml' pull_request: @@ -17,11 +18,12 @@ on: - 'package.json' - 'pnpm-lock.yaml' - 'tsconfig.json' + - 'turbo.json' - 'jest.config.*' - '.github/workflows/ci.yml' jobs: - test: + ci: runs-on: ubuntu-latest steps: @@ -39,22 +41,47 @@ jobs: node-version: '20.x' cache: 'pnpm' + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + # 缓存 Rust 编译结果 + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: packages/engine + cache-on-failure: true + + # 缓存 wasm-pack + - name: Cache wasm-pack + uses: actions/cache@v4 + with: + path: ~/.cargo/bin/wasm-pack + key: wasm-pack-${{ runner.os }} + + - name: Install wasm-pack + run: | + if ! command -v wasm-pack &> /dev/null; then + cargo install wasm-pack + fi + - name: Install dependencies run: pnpm install - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable + # 缓存 Turbo + - name: Cache Turbo + uses: actions/cache@v4 + with: + path: .turbo + key: turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }} + restore-keys: | + turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}- + turbo-${{ runner.os }}- - - name: Install wasm-pack - run: cargo install wasm-pack - - - name: Build core package first - run: pnpm run build:core - - - name: Build engine WASM package - run: | - cd packages/engine - pnpm run build + # 构建所有包 + - name: Build all packages + run: pnpm run build - name: Copy WASM files to ecs-engine-bindgen run: | @@ -64,30 +91,15 @@ jobs: cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/ cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/ - - name: Build dependent packages for type declarations - run: | - cd packages/platform-common && pnpm run build - cd ../asset-system && pnpm run build - cd ../components && pnpm run build - cd ../editor-core && pnpm run build - cd ../ui && pnpm run build - cd ../editor-runtime && pnpm run build - cd ../behavior-tree && pnpm run build - cd ../tilemap && pnpm run build - cd ../physics-rapier2d && pnpm run build - cd ../node-editor && pnpm run build - cd ../blueprint && pnpm run build - - - name: Build ecs-engine-bindgen - run: | - cd packages/ecs-engine-bindgen && pnpm run build - + # 类型检查 - name: Type check run: pnpm run type-check + # Lint 检查 - name: Lint check run: pnpm run lint + # 测试 - name: Run tests with coverage run: pnpm run test:ci @@ -100,39 +112,16 @@ jobs: name: codecov-umbrella fail_ci_if_error: false - build: - runs-on: ubuntu-latest - needs: test - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20.x' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install - - - name: Build project - run: pnpm run build - - - name: Build npm package + # 构建 npm 包 + - name: Build npm packages run: pnpm run build:npm + # 上传构建产物 - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: build-artifacts path: | - bin/ - dist/ + packages/*/dist/ + packages/*/bin/ retention-days: 7 diff --git a/.github/workflows/release-editor.yml b/.github/workflows/release-editor.yml index 5129d771..560dca57 100644 --- a/.github/workflows/release-editor.yml +++ b/.github/workflows/release-editor.yml @@ -71,39 +71,13 @@ jobs: node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')" node scripts/sync-version.js - # ===== 第一层:基础包(无依赖) ===== - - name: Build core package - run: pnpm run build:core - - - name: Build math package - run: | - cd packages/math - pnpm run build - - - name: Build platform-common package - run: | - cd packages/platform-common - pnpm run build - - # ===== 第二层:依赖 core 的包 ===== - - name: Build asset-system package - run: | - cd packages/asset-system - pnpm run build - - - name: Build components package - run: | - cd packages/components - pnpm run build - - # ===== 第三层:Rust WASM 引擎 ===== - name: Install wasm-pack run: cargo install wasm-pack - - name: Build engine package (Rust WASM) - run: | - cd packages/engine - pnpm run build + # 使用 Turborepo 自动按依赖顺序构建所有包 + # 这会自动处理:core -> asset-system -> editor-core -> ui -> 等等 + - name: Build all packages with Turborepo + run: pnpm run build - name: Copy WASM files to ecs-engine-bindgen shell: bash @@ -114,60 +88,6 @@ jobs: cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/ cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/ - - name: Build ecs-engine-bindgen package - run: | - cd packages/ecs-engine-bindgen - pnpm run build - - # ===== 第四层:依赖 ecs-engine-bindgen/asset-system 的包 ===== - - name: Build editor-core package - run: | - cd packages/editor-core - pnpm run build - - # ===== 第五层:依赖 editor-core 的包 ===== - - name: Build UI package - run: | - cd packages/ui - pnpm run build - - - name: Build tilemap package - run: | - cd packages/tilemap - pnpm run build - - - name: Build editor-runtime package - run: | - cd packages/editor-runtime - pnpm run build - - - name: Build node-editor package - run: | - cd packages/node-editor - pnpm run build - - # ===== 第六层:依赖 editor-runtime 的包 ===== - - name: Build behavior-tree package - run: | - cd packages/behavior-tree - pnpm run build - - - name: Build physics-rapier2d package - run: | - cd packages/physics-rapier2d - pnpm run build - - - name: Build blueprint package - run: | - cd packages/blueprint - pnpm run build - - # ===== 第七层:平台包(依赖 ui, tilemap, behavior-tree, physics-rapier2d) ===== - - name: Build platform-web package - run: | - cd packages/platform-web - pnpm run build - - name: Bundle runtime files for Tauri run: | cd packages/editor-app @@ -220,16 +140,16 @@ jobs: delete-branch: true title: "chore(editor): Release v${{ github.event.inputs.version }}" body: | - ## 🚀 Release v${{ github.event.inputs.version }} + ## Release v${{ github.event.inputs.version }} This PR updates the editor version after successful release build. ### Changes - - ✅ Updated `packages/editor-app/package.json` → `${{ github.event.inputs.version }}` - - ✅ Updated `packages/editor-app/src-tauri/tauri.conf.json` → `${{ github.event.inputs.version }}` + - Updated `packages/editor-app/package.json` → `${{ github.event.inputs.version }}` + - Updated `packages/editor-app/src-tauri/tauri.conf.json` → `${{ github.event.inputs.version }}` ### Release - - 📦 [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/editor-v${{ github.event.inputs.version }}) + - [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/editor-v${{ github.event.inputs.version }}) --- *This PR was automatically created by the release workflow.* diff --git a/.gitignore b/.gitignore index ededeb18..bcc91689 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ dist/ .cache/ .build-cache/ +# Turborepo +.turbo/ + # IDE 配置 .idea/ .vscode/ diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index ae01353d..be45a583 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -65,6 +65,7 @@ export default defineConfig({ collapsed: false, items: [ { text: '实体类 (Entity)', link: '/guide/entity' }, + { text: '层级系统 (Hierarchy)', link: '/guide/hierarchy' }, { text: '组件系统 (Component)', link: '/guide/component' }, { text: '实体查询系统', link: '/guide/entity-query' }, { diff --git a/docs/guide/entity.md b/docs/guide/entity.md index d0a2a1aa..158bf3c7 100644 --- a/docs/guide/entity.md +++ b/docs/guide/entity.md @@ -9,6 +9,12 @@ - 提供唯一标识(ID) - 管理组件的生命周期 +::: tip 关于父子层级关系 +实体间的父子层级关系通过 `HierarchyComponent` 和 `HierarchySystem` 管理,而非 Entity 内置属性。这种设计遵循 ECS 组合原则 —— 只有需要层级关系的实体才添加此组件。 + +详见 [层级系统](./hierarchy.md) 文档。 +::: + ## 创建实体 **重要提示:实体必须通过场景创建,不支持手动创建!** @@ -285,4 +291,10 @@ entity.components.forEach(component => { }); ``` -实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。 \ No newline at end of file +实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。 + +## 下一步 + +- 了解 [层级系统](./hierarchy.md) 建立实体间的父子关系 +- 了解 [组件系统](./component.md) 为实体添加功能 +- 了解 [场景管理](./scene.md) 组织和管理实体 \ No newline at end of file diff --git a/docs/guide/hierarchy.md b/docs/guide/hierarchy.md new file mode 100644 index 00000000..4c49f704 --- /dev/null +++ b/docs/guide/hierarchy.md @@ -0,0 +1,437 @@ +# 层级系统 + +在游戏开发中,实体间的父子层级关系是常见需求。ECS Framework 采用组件化方式管理层级关系,通过 `HierarchyComponent` 和 `HierarchySystem` 实现,完全遵循 ECS 组合原则。 + +## 设计理念 + +### 为什么不在 Entity 中内置层级? + +传统的游戏对象模型(如 Unity 的 GameObject)将层级关系内置于实体中。ECS Framework 选择组件化方案的原因: + +1. **ECS 组合原则**:层级是一种"功能",应该通过组件添加,而非所有实体都具备 +2. **按需使用**:只有需要层级关系的实体才添加 `HierarchyComponent` +3. **数据与逻辑分离**:`HierarchyComponent` 存储数据,`HierarchySystem` 处理逻辑 +4. **序列化友好**:层级关系作为组件数据可以轻松序列化和反序列化 + +## 基本概念 + +### HierarchyComponent + +存储层级关系数据的组件: + +```typescript +import { HierarchyComponent } from '@esengine/ecs-framework'; + +// HierarchyComponent 的核心属性 +interface HierarchyComponent { + parentId: number | null; // 父实体 ID,null 表示根实体 + childIds: number[]; // 子实体 ID 列表 + depth: number; // 在层级中的深度(由系统维护) + bActiveInHierarchy: boolean; // 在层级中是否激活(由系统维护) +} +``` + +### HierarchySystem + +处理层级逻辑的系统,提供所有层级操作的 API: + +```typescript +import { HierarchySystem } from '@esengine/ecs-framework'; + +// 获取系统 +const hierarchySystem = scene.getEntityProcessor(HierarchySystem); +``` + +## 快速开始 + +### 添加系统到场景 + +```typescript +import { Scene, HierarchySystem } from '@esengine/ecs-framework'; + +class GameScene extends Scene { + protected initialize(): void { + // 添加层级系统 + this.addSystem(new HierarchySystem()); + + // 添加其他系统... + } +} +``` + +### 建立父子关系 + +```typescript +// 创建实体 +const parent = scene.createEntity("Parent"); +const child1 = scene.createEntity("Child1"); +const child2 = scene.createEntity("Child2"); + +// 获取层级系统 +const hierarchySystem = scene.getEntityProcessor(HierarchySystem); + +// 设置父子关系(自动添加 HierarchyComponent) +hierarchySystem.setParent(child1, parent); +hierarchySystem.setParent(child2, parent); + +// 现在 parent 有两个子实体 +``` + +### 查询层级 + +```typescript +// 获取父实体 +const parentEntity = hierarchySystem.getParent(child1); + +// 获取所有子实体 +const children = hierarchySystem.getChildren(parent); + +// 获取子实体数量 +const count = hierarchySystem.getChildCount(parent); + +// 检查是否有子实体 +const hasKids = hierarchySystem.hasChildren(parent); + +// 获取在层级中的深度 +const depth = hierarchySystem.getDepth(child1); // 返回 1 +``` + +## API 参考 + +### 父子关系操作 + +#### setParent + +设置实体的父级: + +```typescript +// 设置父级 +hierarchySystem.setParent(child, parent); + +// 移动到根级(无父级) +hierarchySystem.setParent(child, null); +``` + +#### insertChildAt + +在指定位置插入子实体: + +```typescript +// 在第一个位置插入 +hierarchySystem.insertChildAt(parent, child, 0); + +// 追加到末尾 +hierarchySystem.insertChildAt(parent, child, -1); +``` + +#### removeChild + +从父级移除子实体(子实体变为根级): + +```typescript +const success = hierarchySystem.removeChild(parent, child); +``` + +#### removeAllChildren + +移除所有子实体: + +```typescript +hierarchySystem.removeAllChildren(parent); +``` + +### 层级查询 + +#### getParent / getChildren + +```typescript +const parent = hierarchySystem.getParent(entity); +const children = hierarchySystem.getChildren(entity); +``` + +#### getRoot + +获取实体的根节点: + +```typescript +const root = hierarchySystem.getRoot(deepChild); +``` + +#### getRootEntities + +获取所有根实体(没有父级的实体): + +```typescript +const roots = hierarchySystem.getRootEntities(); +``` + +#### isAncestorOf / isDescendantOf + +检查祖先/后代关系: + +```typescript +// grandparent -> parent -> child +const isAncestor = hierarchySystem.isAncestorOf(grandparent, child); // true +const isDescendant = hierarchySystem.isDescendantOf(child, grandparent); // true +``` + +### 层级遍历 + +#### findChild + +根据名称查找子实体: + +```typescript +// 直接子级中查找 +const child = hierarchySystem.findChild(parent, "ChildName"); + +// 递归查找所有后代 +const deepChild = hierarchySystem.findChild(parent, "DeepChild", true); +``` + +#### findChildrenByTag + +根据标签查找子实体: + +```typescript +// 查找直接子级 +const tagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY); + +// 递归查找 +const allTagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY, true); +``` + +#### forEachChild + +遍历子实体: + +```typescript +// 遍历直接子级 +hierarchySystem.forEachChild(parent, (child) => { + console.log(child.name); +}); + +// 递归遍历所有后代 +hierarchySystem.forEachChild(parent, (child) => { + console.log(child.name); +}, true); +``` + +### 层级状态 + +#### isActiveInHierarchy + +检查实体在层级中是否激活(考虑所有祖先的激活状态): + +```typescript +// 如果 parent.active = false,即使 child.active = true +// isActiveInHierarchy(child) 也会返回 false +const activeInHierarchy = hierarchySystem.isActiveInHierarchy(child); +``` + +#### getDepth + +获取实体在层级中的深度(根实体深度为 0): + +```typescript +const depth = hierarchySystem.getDepth(entity); +``` + +### 扁平化层级(用于 UI 渲染) + +```typescript +// 用于实现可展开/折叠的层级树视图 +const expandedIds = new Set([parent.id]); + +const flatNodes = hierarchySystem.flattenHierarchy(expandedIds); +// 返回 [{ entity, depth, bHasChildren, bIsExpanded }, ...] +``` + +## 完整示例 + +### 创建游戏角色层级 + +```typescript +import { + Scene, + HierarchySystem, + HierarchyComponent +} from '@esengine/ecs-framework'; + +class GameScene extends Scene { + private hierarchySystem!: HierarchySystem; + + protected initialize(): void { + // 添加层级系统 + this.hierarchySystem = new HierarchySystem(); + this.addSystem(this.hierarchySystem); + + // 创建角色层级 + this.createPlayerHierarchy(); + } + + private createPlayerHierarchy(): void { + // 根实体 + const player = this.createEntity("Player"); + player.addComponent(new Transform(0, 0)); + + // 身体部件 + const body = this.createEntity("Body"); + body.addComponent(new Sprite("body.png")); + this.hierarchySystem.setParent(body, player); + + // 武器(挂载在身体上) + const weapon = this.createEntity("Weapon"); + weapon.addComponent(new Sprite("sword.png")); + this.hierarchySystem.setParent(weapon, body); + + // 特效(挂载在武器上) + const effect = this.createEntity("WeaponEffect"); + effect.addComponent(new ParticleEmitter()); + this.hierarchySystem.setParent(effect, weapon); + + // 查询层级信息 + console.log(`Player 层级深度: ${this.hierarchySystem.getDepth(player)}`); // 0 + console.log(`Weapon 层级深度: ${this.hierarchySystem.getDepth(weapon)}`); // 2 + console.log(`Effect 层级深度: ${this.hierarchySystem.getDepth(effect)}`); // 3 + } + + public equipNewWeapon(weaponName: string): void { + const body = this.findEntity("Body"); + const oldWeapon = this.hierarchySystem.findChild(body!, "Weapon"); + + if (oldWeapon) { + // 移除旧武器的所有子实体 + this.hierarchySystem.removeAllChildren(oldWeapon); + oldWeapon.destroy(); + } + + // 创建新武器 + const newWeapon = this.createEntity("Weapon"); + newWeapon.addComponent(new Sprite(`${weaponName}.png`)); + this.hierarchySystem.setParent(newWeapon, body!); + } +} +``` + +### 层级变换系统 + +结合 Transform 组件实现层级变换: + +```typescript +import { EntitySystem, Matcher, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; + +class HierarchyTransformSystem extends EntitySystem { + private hierarchySystem!: HierarchySystem; + + constructor() { + super(Matcher.empty().all(Transform, HierarchyComponent)); + } + + public onAddedToScene(): void { + // 获取层级系统引用 + this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!; + } + + protected process(entities: readonly Entity[]): void { + // 按深度排序,确保父级先更新 + const sorted = [...entities].sort((a, b) => { + return this.hierarchySystem.getDepth(a) - this.hierarchySystem.getDepth(b); + }); + + for (const entity of sorted) { + const transform = entity.getComponent(Transform)!; + const parent = this.hierarchySystem.getParent(entity); + + if (parent) { + const parentTransform = parent.getComponent(Transform); + if (parentTransform) { + // 计算世界坐标 + transform.worldX = parentTransform.worldX + transform.localX; + transform.worldY = parentTransform.worldY + transform.localY; + } + } else { + // 根实体,本地坐标即世界坐标 + transform.worldX = transform.localX; + transform.worldY = transform.localY; + } + } + } +} +``` + +## 性能优化 + +### 缓存机制 + +`HierarchySystem` 内置了缓存机制: + +- `depth` 和 `bActiveInHierarchy` 由系统自动维护 +- 使用 `bCacheDirty` 标记优化更新 +- 层级变化时自动标记所有子级缓存为脏 + +### 最佳实践 + +1. **避免深层嵌套**:系统限制最大深度为 32 层 +2. **批量操作**:构建复杂层级时,尽量一次性设置好所有父子关系 +3. **按需添加**:只有真正需要层级关系的实体才添加 `HierarchyComponent` +4. **缓存系统引用**:避免每次调用都获取 `HierarchySystem` + +```typescript +// 好的做法 +class MySystem extends EntitySystem { + private hierarchySystem!: HierarchySystem; + + onAddedToScene() { + this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!; + } + + process() { + // 使用缓存的引用 + const parent = this.hierarchySystem.getParent(entity); + } +} + +// 避免的做法 +process() { + // 每次都获取,性能较差 + const system = this.scene!.getEntityProcessor(HierarchySystem); +} +``` + +## 迁移指南 + +如果你之前使用的是旧版 Entity 内置的层级 API,请参考以下迁移指南: + +| 旧 API (已移除) | 新 API | +|----------------|--------| +| `entity.parent` | `hierarchySystem.getParent(entity)` | +| `entity.children` | `hierarchySystem.getChildren(entity)` | +| `entity.addChild(child)` | `hierarchySystem.setParent(child, entity)` | +| `entity.removeChild(child)` | `hierarchySystem.removeChild(entity, child)` | +| `entity.findChild(name)` | `hierarchySystem.findChild(entity, name)` | +| `entity.activeInHierarchy` | `hierarchySystem.isActiveInHierarchy(entity)` | + +### 迁移示例 + +```typescript +// 旧代码 +const parent = scene.createEntity("Parent"); +const child = scene.createEntity("Child"); +parent.addChild(child); +const found = parent.findChild("Child"); + +// 新代码 +const hierarchySystem = scene.getEntityProcessor(HierarchySystem); + +const parent = scene.createEntity("Parent"); +const child = scene.createEntity("Child"); +hierarchySystem.setParent(child, parent); +const found = hierarchySystem.findChild(parent, "Child"); +``` + +## 下一步 + +- 了解 [实体类](./entity.md) 的其他功能 +- 了解 [场景管理](./scene.md) 如何组织实体和系统 +- 探索 [变换系统](./transform.md) 实现空间层级变换 diff --git a/package.json b/package.json index e9096b62..02d88374 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "2.1.29", "description": "ECS Framework Monorepo - 高性能ECS框架及其网络插件", "private": true, + "packageManager": "pnpm@10.22.0", "workspaces": [ "packages/*" ], @@ -17,16 +18,18 @@ ], "scripts": { "bootstrap": "lerna bootstrap", - "clean": "lerna run clean", - "build": "npm run build:core && npm run build:math", - "build:core": "cd packages/core && npm run build", - "build:math": "cd packages/math && npm run build", - "build:npm": "npm run build:npm:core && npm run build:npm:math", + "clean": "turbo run clean", + "build": "turbo run build", + "build:filter": "turbo run build --filter", + "build:core": "turbo run build --filter=@esengine/ecs-framework", + "build:math": "turbo run build --filter=@esengine/ecs-framework-math", + "build:editor": "turbo run build --filter=@esengine/editor-app...", + "build:npm": "turbo run build:npm", "build:npm:core": "cd packages/core && npm run build:npm", "build:npm:math": "cd packages/math && npm run build:npm", - "test": "lerna run test", - "test:coverage": "lerna run test:coverage", - "test:ci": "lerna run test:ci", + "test": "turbo run test", + "test:coverage": "turbo run test:coverage", + "test:ci": "turbo run test:ci", "prepare:publish": "npm run build:npm && node scripts/pre-publish-check.cjs", "sync:versions": "node scripts/sync-versions.cjs", "publish:all": "npm run prepare:publish && npm run publish:all:dist", @@ -51,9 +54,9 @@ "copy:worker-demo": "node scripts/update-worker-demo.js", "format": "prettier --write \"packages/**/src/**/*.{ts,tsx,js,jsx}\"", "format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"", - "type-check": "lerna run type-check", - "lint": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\"", - "lint:fix": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\" --fix", + "type-check": "turbo run type-check", + "lint": "turbo run lint", + "lint:fix": "turbo run lint:fix", "build:wasm": "cd packages/engine && wasm-pack build --dev --out-dir pkg", "build:wasm:release": "cd packages/engine && wasm-pack build --release --out-dir pkg" }, @@ -92,6 +95,7 @@ "semver": "^7.6.3", "size-limit": "^11.0.2", "ts-jest": "^29.4.0", + "turbo": "^2.6.1", "typedoc": "^0.28.13", "typedoc-plugin-markdown": "^4.9.0", "typescript": "^5.8.3", diff --git a/packages/asset-system/package.json b/packages/asset-system/package.json index 84c1c8a1..60a22882 100644 --- a/packages/asset-system/package.json +++ b/packages/asset-system/package.json @@ -2,24 +2,24 @@ "name": "@esengine/asset-system", "version": "1.0.0", "description": "Asset management system for ES Engine", + "type": "module", "main": "dist/index.js", - "module": "dist/index.mjs", + "module": "dist/index.js", "types": "dist/index.d.ts", "exports": { ".": { - "import": "./dist/index.mjs", - "require": "./dist/index.js", - "types": "./dist/index.d.ts" + "types": "./dist/index.d.ts", + "import": "./dist/index.js" } }, "files": [ "dist" ], "scripts": { - "build": "rollup -c", - "build:npm": "npm run build", + "build": "tsup", + "build:watch": "tsup --watch", "clean": "rimraf dist", - "type-check": "npx tsc --noEmit" + "type-check": "tsc --noEmit" }, "keywords": [ "ecs", @@ -29,16 +29,11 @@ ], "author": "yhh", "license": "MIT", - "peerDependencies": { - "@esengine/ecs-framework": "^2.0.0" - }, "devDependencies": { - "@rollup/plugin-commonjs": "^28.0.3", - "@rollup/plugin-node-resolve": "^16.0.1", - "@rollup/plugin-typescript": "^11.1.6", + "@esengine/ecs-framework": "workspace:*", + "@esengine/build-config": "workspace:*", "rimraf": "^5.0.0", - "rollup": "^4.42.0", - "rollup-plugin-dts": "^6.2.1", + "tsup": "^8.0.0", "typescript": "^5.8.3" }, "publishConfig": { diff --git a/packages/asset-system/rollup.config.js b/packages/asset-system/rollup.config.js deleted file mode 100644 index d512c7db..00000000 --- a/packages/asset-system/rollup.config.js +++ /dev/null @@ -1,49 +0,0 @@ -import typescript from '@rollup/plugin-typescript'; -import resolve from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import dts from 'rollup-plugin-dts'; - -const external = ['@esengine/ecs-framework']; - -export default [ - // ESM and CJS builds - { - input: 'src/index.ts', - output: [ - { - file: 'dist/index.js', - format: 'cjs', - sourcemap: true, - exports: 'named' - }, - { - file: 'dist/index.mjs', - format: 'es', - sourcemap: true - } - ], - external, - plugins: [ - resolve({ - preferBuiltins: false, - browser: true - }), - commonjs(), - typescript({ - tsconfig: './tsconfig.json', - declaration: false, - declarationMap: false - }) - ] - }, - // Type definitions - { - input: 'src/index.ts', - output: { - file: 'dist/index.d.ts', - format: 'es' - }, - external, - plugins: [dts()] - } -]; \ No newline at end of file diff --git a/packages/asset-system/tsconfig.build.json b/packages/asset-system/tsconfig.build.json new file mode 100644 index 00000000..bf8abf7b --- /dev/null +++ b/packages/asset-system/tsconfig.build.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/asset-system/tsup.config.ts b/packages/asset-system/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/asset-system/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/audio/package.json b/packages/audio/package.json new file mode 100644 index 00000000..843015f2 --- /dev/null +++ b/packages/audio/package.json @@ -0,0 +1,46 @@ +{ + "name": "@esengine/audio", + "version": "1.0.0", + "description": "ECS-based audio system", + "esengine": { + "plugin": true, + "pluginExport": "AudioPlugin", + "category": "audio", + "isEnginePlugin": true + }, + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "audio", + "sound", + "music" + ], + "author": "yhh", + "license": "MIT" +} diff --git a/packages/audio/src/AudioPlugin.ts b/packages/audio/src/AudioPlugin.ts new file mode 100644 index 00000000..4e7ccb2e --- /dev/null +++ b/packages/audio/src/AudioPlugin.ts @@ -0,0 +1,24 @@ +import type { ComponentRegistry as ComponentRegistryType } from '@esengine/ecs-framework'; +import type { IRuntimeModule, IPlugin, PluginDescriptor } from '@esengine/engine-core'; +import { AudioSourceComponent } from './AudioSourceComponent'; + +class AudioRuntimeModule implements IRuntimeModule { + registerComponents(registry: typeof ComponentRegistryType): void { + registry.register(AudioSourceComponent); + } +} + +const descriptor: PluginDescriptor = { + id: '@esengine/audio', + name: 'Audio', + version: '1.0.0', + description: '音频组件', + category: 'audio', + enabledByDefault: true, + isEnginePlugin: true +}; + +export const AudioPlugin: IPlugin = { + descriptor, + runtimeModule: new AudioRuntimeModule() +}; diff --git a/packages/audio/src/AudioSourceComponent.ts b/packages/audio/src/AudioSourceComponent.ts new file mode 100644 index 00000000..b78656eb --- /dev/null +++ b/packages/audio/src/AudioSourceComponent.ts @@ -0,0 +1,43 @@ +import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework'; + +@ECSComponent('AudioSource') +@Serializable({ version: 1, typeId: 'AudioSource' }) +export class AudioSourceComponent extends Component { + @Serialize() + @Property({ type: 'asset', label: 'Audio Clip', assetType: 'audio' }) + clip: string = ''; + + /** 范围 [0, 1] */ + @Serialize() + @Property({ type: 'number', label: 'Volume', min: 0, max: 1, step: 0.01 }) + volume: number = 1; + + @Serialize() + @Property({ type: 'number', label: 'Pitch', min: 0.1, max: 3, step: 0.1 }) + pitch: number = 1; + + @Serialize() + @Property({ type: 'boolean', label: 'Loop' }) + loop: boolean = false; + + @Serialize() + @Property({ type: 'boolean', label: 'Play On Awake' }) + playOnAwake: boolean = false; + + @Serialize() + @Property({ type: 'boolean', label: 'Mute' }) + mute: boolean = false; + + /** 0 = 2D, 1 = 3D */ + @Serialize() + @Property({ type: 'number', label: 'Spatial Blend', min: 0, max: 1, step: 0.1 }) + spatialBlend: number = 0; + + @Serialize() + @Property({ type: 'number', label: 'Min Distance' }) + minDistance: number = 1; + + @Serialize() + @Property({ type: 'number', label: 'Max Distance' }) + maxDistance: number = 500; +} diff --git a/packages/audio/src/index.ts b/packages/audio/src/index.ts new file mode 100644 index 00000000..66bf81d0 --- /dev/null +++ b/packages/audio/src/index.ts @@ -0,0 +1,2 @@ +export { AudioSourceComponent } from './AudioSourceComponent'; +export { AudioPlugin } from './AudioPlugin'; diff --git a/packages/audio/tsconfig.build.json b/packages/audio/tsconfig.build.json new file mode 100644 index 00000000..f39a0594 --- /dev/null +++ b/packages/audio/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/audio/tsconfig.json b/packages/audio/tsconfig.json new file mode 100644 index 00000000..02f5f187 --- /dev/null +++ b/packages/audio/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../core" } + ] +} diff --git a/packages/audio/tsup.config.ts b/packages/audio/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/audio/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/behavior-tree-editor/package.json b/packages/behavior-tree-editor/package.json new file mode 100644 index 00000000..1ae5139e --- /dev/null +++ b/packages/behavior-tree-editor/package.json @@ -0,0 +1,49 @@ +{ + "name": "@esengine/behavior-tree-editor", + "version": "1.0.0", + "description": "Editor support for @esengine/behavior-tree - visual editor, inspectors, and tools", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "dependencies": { + "@esengine/behavior-tree": "workspace:*" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/editor-runtime": "workspace:*", + "@esengine/node-editor": "workspace:*", + "@esengine/build-config": "workspace:*", + "lucide-react": "^0.545.0", + "react": "^18.3.1", + "zustand": "^5.0.8", + "@types/react": "^18.3.12", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "behavior-tree", + "editor" + ], + "author": "", + "license": "MIT" +} diff --git a/packages/behavior-tree/src/editor/BehaviorTreePlugin.ts b/packages/behavior-tree-editor/src/BehaviorTreePlugin.ts similarity index 78% rename from packages/behavior-tree/src/editor/BehaviorTreePlugin.ts rename to packages/behavior-tree-editor/src/BehaviorTreePlugin.ts index eedbd84b..b5cdca6f 100644 --- a/packages/behavior-tree/src/editor/BehaviorTreePlugin.ts +++ b/packages/behavior-tree-editor/src/BehaviorTreePlugin.ts @@ -21,18 +21,16 @@ export const descriptor: PluginDescriptor = { { name: 'BehaviorTreeRuntime', type: 'runtime', - loadingPhase: 'default', - entry: './src/index.ts' + loadingPhase: 'default' }, { name: 'BehaviorTreeEditor', type: 'editor', - loadingPhase: 'default', - entry: './src/editor/index.ts' + loadingPhase: 'default' } ], dependencies: [ - { id: '@esengine/core', version: '>=1.0.0' } + { id: '@esengine/engine-core', version: '>=1.0.0', optional: true } ], icon: 'GitBranch' }; diff --git a/packages/behavior-tree/src/editor/PluginContext.ts b/packages/behavior-tree-editor/src/PluginContext.ts similarity index 100% rename from packages/behavior-tree/src/editor/PluginContext.ts rename to packages/behavior-tree-editor/src/PluginContext.ts diff --git a/packages/behavior-tree/src/editor/application/commands/CommandManager.ts b/packages/behavior-tree-editor/src/application/commands/CommandManager.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/CommandManager.ts rename to packages/behavior-tree-editor/src/application/commands/CommandManager.ts diff --git a/packages/behavior-tree/src/editor/application/commands/ICommand.ts b/packages/behavior-tree-editor/src/application/commands/ICommand.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/ICommand.ts rename to packages/behavior-tree-editor/src/application/commands/ICommand.ts diff --git a/packages/behavior-tree/src/editor/application/commands/ITreeState.ts b/packages/behavior-tree-editor/src/application/commands/ITreeState.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/ITreeState.ts rename to packages/behavior-tree-editor/src/application/commands/ITreeState.ts diff --git a/packages/behavior-tree/src/editor/application/commands/tree/AddConnectionCommand.ts b/packages/behavior-tree-editor/src/application/commands/tree/AddConnectionCommand.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/tree/AddConnectionCommand.ts rename to packages/behavior-tree-editor/src/application/commands/tree/AddConnectionCommand.ts diff --git a/packages/behavior-tree/src/editor/application/commands/tree/CreateNodeCommand.ts b/packages/behavior-tree-editor/src/application/commands/tree/CreateNodeCommand.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/tree/CreateNodeCommand.ts rename to packages/behavior-tree-editor/src/application/commands/tree/CreateNodeCommand.ts diff --git a/packages/behavior-tree/src/editor/application/commands/tree/DeleteNodeCommand.ts b/packages/behavior-tree-editor/src/application/commands/tree/DeleteNodeCommand.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/tree/DeleteNodeCommand.ts rename to packages/behavior-tree-editor/src/application/commands/tree/DeleteNodeCommand.ts diff --git a/packages/behavior-tree/src/editor/application/commands/tree/MoveNodeCommand.ts b/packages/behavior-tree-editor/src/application/commands/tree/MoveNodeCommand.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/tree/MoveNodeCommand.ts rename to packages/behavior-tree-editor/src/application/commands/tree/MoveNodeCommand.ts diff --git a/packages/behavior-tree/src/editor/application/commands/tree/RemoveConnectionCommand.ts b/packages/behavior-tree-editor/src/application/commands/tree/RemoveConnectionCommand.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/tree/RemoveConnectionCommand.ts rename to packages/behavior-tree-editor/src/application/commands/tree/RemoveConnectionCommand.ts diff --git a/packages/behavior-tree/src/editor/application/commands/tree/UpdateNodeDataCommand.ts b/packages/behavior-tree-editor/src/application/commands/tree/UpdateNodeDataCommand.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/tree/UpdateNodeDataCommand.ts rename to packages/behavior-tree-editor/src/application/commands/tree/UpdateNodeDataCommand.ts diff --git a/packages/behavior-tree/src/editor/application/commands/tree/index.ts b/packages/behavior-tree-editor/src/application/commands/tree/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/tree/index.ts rename to packages/behavior-tree-editor/src/application/commands/tree/index.ts diff --git a/packages/behavior-tree/src/editor/application/interfaces/IExecutionHooks.ts b/packages/behavior-tree-editor/src/application/interfaces/IExecutionHooks.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/interfaces/IExecutionHooks.ts rename to packages/behavior-tree-editor/src/application/interfaces/IExecutionHooks.ts diff --git a/packages/behavior-tree/src/editor/application/services/BlackboardManager.ts b/packages/behavior-tree-editor/src/application/services/BlackboardManager.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/services/BlackboardManager.ts rename to packages/behavior-tree-editor/src/application/services/BlackboardManager.ts diff --git a/packages/behavior-tree/src/editor/application/services/ExecutionController.ts b/packages/behavior-tree-editor/src/application/services/ExecutionController.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/services/ExecutionController.ts rename to packages/behavior-tree-editor/src/application/services/ExecutionController.ts diff --git a/packages/behavior-tree/src/editor/application/services/GlobalBlackboardService.ts b/packages/behavior-tree-editor/src/application/services/GlobalBlackboardService.ts similarity index 98% rename from packages/behavior-tree/src/editor/application/services/GlobalBlackboardService.ts rename to packages/behavior-tree-editor/src/application/services/GlobalBlackboardService.ts index b291a0e2..4a5108f3 100644 --- a/packages/behavior-tree/src/editor/application/services/GlobalBlackboardService.ts +++ b/packages/behavior-tree-editor/src/application/services/GlobalBlackboardService.ts @@ -1,4 +1,4 @@ -import { GlobalBlackboardConfig, BlackboardValueType, BlackboardVariable } from '../../..'; +import { type GlobalBlackboardConfig, BlackboardValueType, type BlackboardVariable } from '@esengine/behavior-tree'; import { createLogger } from '@esengine/ecs-framework'; const logger = createLogger('GlobalBlackboardService'); diff --git a/packages/behavior-tree/src/editor/application/state/BehaviorTreeDataStore.ts b/packages/behavior-tree-editor/src/application/state/BehaviorTreeDataStore.ts similarity index 99% rename from packages/behavior-tree/src/editor/application/state/BehaviorTreeDataStore.ts rename to packages/behavior-tree-editor/src/application/state/BehaviorTreeDataStore.ts index cdee1330..0d472621 100644 --- a/packages/behavior-tree/src/editor/application/state/BehaviorTreeDataStore.ts +++ b/packages/behavior-tree-editor/src/application/state/BehaviorTreeDataStore.ts @@ -1,7 +1,7 @@ import { createStore } from '@esengine/editor-runtime'; const create = createStore; -import { NodeTemplates, NodeTemplate } from '../../..'; +import { NodeTemplates, type NodeTemplate } from '@esengine/behavior-tree'; import { BehaviorTree } from '../../domain/models/BehaviorTree'; import { Node } from '../../domain/models/Node'; import { Connection, ConnectionType } from '../../domain/models/Connection'; diff --git a/packages/behavior-tree/src/editor/application/use-cases/AddConnectionUseCase.ts b/packages/behavior-tree-editor/src/application/use-cases/AddConnectionUseCase.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/use-cases/AddConnectionUseCase.ts rename to packages/behavior-tree-editor/src/application/use-cases/AddConnectionUseCase.ts diff --git a/packages/behavior-tree/src/editor/application/use-cases/CreateNodeUseCase.ts b/packages/behavior-tree-editor/src/application/use-cases/CreateNodeUseCase.ts similarity index 95% rename from packages/behavior-tree/src/editor/application/use-cases/CreateNodeUseCase.ts rename to packages/behavior-tree-editor/src/application/use-cases/CreateNodeUseCase.ts index 1e4e4540..86faaeeb 100644 --- a/packages/behavior-tree/src/editor/application/use-cases/CreateNodeUseCase.ts +++ b/packages/behavior-tree-editor/src/application/use-cases/CreateNodeUseCase.ts @@ -1,4 +1,4 @@ -import { NodeTemplate } from '../../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; import { Node } from '../../domain/models/Node'; import { Position } from '../../domain/value-objects/Position'; import { INodeFactory } from '../../domain/interfaces/INodeFactory'; diff --git a/packages/behavior-tree/src/editor/application/use-cases/DeleteNodeUseCase.ts b/packages/behavior-tree-editor/src/application/use-cases/DeleteNodeUseCase.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/use-cases/DeleteNodeUseCase.ts rename to packages/behavior-tree-editor/src/application/use-cases/DeleteNodeUseCase.ts diff --git a/packages/behavior-tree/src/editor/application/use-cases/MoveNodeUseCase.ts b/packages/behavior-tree-editor/src/application/use-cases/MoveNodeUseCase.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/use-cases/MoveNodeUseCase.ts rename to packages/behavior-tree-editor/src/application/use-cases/MoveNodeUseCase.ts diff --git a/packages/behavior-tree/src/editor/application/use-cases/RemoveConnectionUseCase.ts b/packages/behavior-tree-editor/src/application/use-cases/RemoveConnectionUseCase.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/use-cases/RemoveConnectionUseCase.ts rename to packages/behavior-tree-editor/src/application/use-cases/RemoveConnectionUseCase.ts diff --git a/packages/behavior-tree/src/editor/application/use-cases/UpdateNodeDataUseCase.ts b/packages/behavior-tree-editor/src/application/use-cases/UpdateNodeDataUseCase.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/use-cases/UpdateNodeDataUseCase.ts rename to packages/behavior-tree-editor/src/application/use-cases/UpdateNodeDataUseCase.ts diff --git a/packages/behavior-tree/src/editor/application/use-cases/ValidateTreeUseCase.ts b/packages/behavior-tree-editor/src/application/use-cases/ValidateTreeUseCase.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/use-cases/ValidateTreeUseCase.ts rename to packages/behavior-tree-editor/src/application/use-cases/ValidateTreeUseCase.ts diff --git a/packages/behavior-tree/src/editor/application/use-cases/index.ts b/packages/behavior-tree-editor/src/application/use-cases/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/use-cases/index.ts rename to packages/behavior-tree-editor/src/application/use-cases/index.ts diff --git a/packages/behavior-tree/src/editor/compiler/BehaviorTreeCompiler.tsx b/packages/behavior-tree-editor/src/compiler/BehaviorTreeCompiler.tsx similarity index 99% rename from packages/behavior-tree/src/editor/compiler/BehaviorTreeCompiler.tsx rename to packages/behavior-tree-editor/src/compiler/BehaviorTreeCompiler.tsx index 16ab5fb6..23cc78c9 100644 --- a/packages/behavior-tree/src/editor/compiler/BehaviorTreeCompiler.tsx +++ b/packages/behavior-tree-editor/src/compiler/BehaviorTreeCompiler.tsx @@ -10,7 +10,7 @@ import { createLogger, } from '@esengine/editor-runtime'; import { GlobalBlackboardTypeGenerator } from '../generators/GlobalBlackboardTypeGenerator'; -import { EditorFormatConverter, BehaviorTreeAssetSerializer } from '../..'; +import { EditorFormatConverter, BehaviorTreeAssetSerializer } from '@esengine/behavior-tree'; import { useBehaviorTreeDataStore } from '../application/state/BehaviorTreeDataStore'; const { File, FolderTree, FolderOpen } = Icons; diff --git a/packages/behavior-tree/src/editor/components/BehaviorTreeEditor.tsx b/packages/behavior-tree-editor/src/components/BehaviorTreeEditor.tsx similarity index 99% rename from packages/behavior-tree/src/editor/components/BehaviorTreeEditor.tsx rename to packages/behavior-tree-editor/src/components/BehaviorTreeEditor.tsx index 3c3d5679..13efddfb 100644 --- a/packages/behavior-tree/src/editor/components/BehaviorTreeEditor.tsx +++ b/packages/behavior-tree-editor/src/components/BehaviorTreeEditor.tsx @@ -1,5 +1,5 @@ import { React, useEffect, useMemo, useRef, useState, useCallback } from '@esengine/editor-runtime'; -import { NodeTemplate, BlackboardValueType } from '../..'; +import { BlackboardValueType, type NodeTemplate } from '@esengine/behavior-tree'; import { useBehaviorTreeDataStore, BehaviorTreeNode, ROOT_NODE_ID } from '../stores'; import { useUIStore } from '../stores'; import { showToast as notificationShowToast } from '../services/NotificationService'; diff --git a/packages/behavior-tree/src/editor/components/blackboard/BlackboardPanel.tsx b/packages/behavior-tree-editor/src/components/blackboard/BlackboardPanel.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/blackboard/BlackboardPanel.tsx rename to packages/behavior-tree-editor/src/components/blackboard/BlackboardPanel.tsx diff --git a/packages/behavior-tree/src/editor/components/canvas/BehaviorTreeCanvas.tsx b/packages/behavior-tree-editor/src/components/canvas/BehaviorTreeCanvas.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/canvas/BehaviorTreeCanvas.tsx rename to packages/behavior-tree-editor/src/components/canvas/BehaviorTreeCanvas.tsx diff --git a/packages/behavior-tree/src/editor/components/canvas/GridBackground.tsx b/packages/behavior-tree-editor/src/components/canvas/GridBackground.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/canvas/GridBackground.tsx rename to packages/behavior-tree-editor/src/components/canvas/GridBackground.tsx diff --git a/packages/behavior-tree/src/editor/components/canvas/index.ts b/packages/behavior-tree-editor/src/components/canvas/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/components/canvas/index.ts rename to packages/behavior-tree-editor/src/components/canvas/index.ts diff --git a/packages/behavior-tree/src/editor/components/common/DraggablePanel.tsx b/packages/behavior-tree-editor/src/components/common/DraggablePanel.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/common/DraggablePanel.tsx rename to packages/behavior-tree-editor/src/components/common/DraggablePanel.tsx diff --git a/packages/behavior-tree/src/editor/components/connections/ConnectionLayer.tsx b/packages/behavior-tree-editor/src/components/connections/ConnectionLayer.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/connections/ConnectionLayer.tsx rename to packages/behavior-tree-editor/src/components/connections/ConnectionLayer.tsx diff --git a/packages/behavior-tree/src/editor/components/connections/ConnectionRenderer.tsx b/packages/behavior-tree-editor/src/components/connections/ConnectionRenderer.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/connections/ConnectionRenderer.tsx rename to packages/behavior-tree-editor/src/components/connections/ConnectionRenderer.tsx diff --git a/packages/behavior-tree/src/editor/components/connections/index.ts b/packages/behavior-tree-editor/src/components/connections/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/components/connections/index.ts rename to packages/behavior-tree-editor/src/components/connections/index.ts diff --git a/packages/behavior-tree/src/editor/components/menu/NodeContextMenu.tsx b/packages/behavior-tree-editor/src/components/menu/NodeContextMenu.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/menu/NodeContextMenu.tsx rename to packages/behavior-tree-editor/src/components/menu/NodeContextMenu.tsx diff --git a/packages/behavior-tree/src/editor/components/menu/QuickCreateMenu.tsx b/packages/behavior-tree-editor/src/components/menu/QuickCreateMenu.tsx similarity index 99% rename from packages/behavior-tree/src/editor/components/menu/QuickCreateMenu.tsx rename to packages/behavior-tree-editor/src/components/menu/QuickCreateMenu.tsx index 98c2be9e..170d6d1b 100644 --- a/packages/behavior-tree/src/editor/components/menu/QuickCreateMenu.tsx +++ b/packages/behavior-tree-editor/src/components/menu/QuickCreateMenu.tsx @@ -1,6 +1,6 @@ import { React, useRef, useEffect, useState, useMemo, Icons } from '@esengine/editor-runtime'; import type { LucideIcon } from '@esengine/editor-runtime'; -import { NodeTemplate } from '../../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; import { NodeFactory } from '../../infrastructure/factories/NodeFactory'; const { Search, X, ChevronDown, ChevronRight } = Icons; diff --git a/packages/behavior-tree/src/editor/components/nodes/BehaviorTreeNode.tsx b/packages/behavior-tree-editor/src/components/nodes/BehaviorTreeNode.tsx similarity index 99% rename from packages/behavior-tree/src/editor/components/nodes/BehaviorTreeNode.tsx rename to packages/behavior-tree-editor/src/components/nodes/BehaviorTreeNode.tsx index d3a375d5..fe3cb6c3 100644 --- a/packages/behavior-tree/src/editor/components/nodes/BehaviorTreeNode.tsx +++ b/packages/behavior-tree-editor/src/components/nodes/BehaviorTreeNode.tsx @@ -1,6 +1,6 @@ import { React, Icons } from '@esengine/editor-runtime'; import type { LucideIcon } from '@esengine/editor-runtime'; -import { PropertyDefinition } from '../../..'; +import type { PropertyDefinition } from '@esengine/behavior-tree'; import { Node as BehaviorTreeNodeType } from '../../domain/models/Node'; import { Connection } from '../../domain/models/Connection'; diff --git a/packages/behavior-tree/src/editor/components/nodes/BehaviorTreeNodeRenderer.tsx b/packages/behavior-tree-editor/src/components/nodes/BehaviorTreeNodeRenderer.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/nodes/BehaviorTreeNodeRenderer.tsx rename to packages/behavior-tree-editor/src/components/nodes/BehaviorTreeNodeRenderer.tsx diff --git a/packages/behavior-tree/src/editor/components/nodes/index.ts b/packages/behavior-tree-editor/src/components/nodes/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/components/nodes/index.ts rename to packages/behavior-tree-editor/src/components/nodes/index.ts diff --git a/packages/behavior-tree/src/editor/components/panels/BehaviorTreeEditorPanel.css b/packages/behavior-tree-editor/src/components/panels/BehaviorTreeEditorPanel.css similarity index 100% rename from packages/behavior-tree/src/editor/components/panels/BehaviorTreeEditorPanel.css rename to packages/behavior-tree-editor/src/components/panels/BehaviorTreeEditorPanel.css diff --git a/packages/behavior-tree/src/editor/components/panels/BehaviorTreeEditorPanel.tsx b/packages/behavior-tree-editor/src/components/panels/BehaviorTreeEditorPanel.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/panels/BehaviorTreeEditorPanel.tsx rename to packages/behavior-tree-editor/src/components/panels/BehaviorTreeEditorPanel.tsx diff --git a/packages/behavior-tree/src/editor/components/panels/BehaviorTreePropertiesPanel.css b/packages/behavior-tree-editor/src/components/panels/BehaviorTreePropertiesPanel.css similarity index 100% rename from packages/behavior-tree/src/editor/components/panels/BehaviorTreePropertiesPanel.css rename to packages/behavior-tree-editor/src/components/panels/BehaviorTreePropertiesPanel.css diff --git a/packages/behavior-tree/src/editor/components/toolbar/EditorToolbar.tsx b/packages/behavior-tree-editor/src/components/toolbar/EditorToolbar.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/toolbar/EditorToolbar.tsx rename to packages/behavior-tree-editor/src/components/toolbar/EditorToolbar.tsx diff --git a/packages/behavior-tree/src/editor/config/editorConstants.ts b/packages/behavior-tree-editor/src/config/editorConstants.ts similarity index 94% rename from packages/behavior-tree/src/editor/config/editorConstants.ts rename to packages/behavior-tree-editor/src/config/editorConstants.ts index d9ab11b9..da1e7830 100644 --- a/packages/behavior-tree/src/editor/config/editorConstants.ts +++ b/packages/behavior-tree-editor/src/config/editorConstants.ts @@ -1,4 +1,4 @@ -import { NodeTemplate, NodeType } from '../..'; +import { NodeType, type NodeTemplate } from '@esengine/behavior-tree'; import { Icons } from '@esengine/editor-runtime'; import type { LucideIcon } from '@esengine/editor-runtime'; diff --git a/packages/behavior-tree/src/editor/constants/index.ts b/packages/behavior-tree-editor/src/constants/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/constants/index.ts rename to packages/behavior-tree-editor/src/constants/index.ts diff --git a/packages/behavior-tree/src/editor/domain/constants/RootNode.ts b/packages/behavior-tree-editor/src/domain/constants/RootNode.ts similarity index 91% rename from packages/behavior-tree/src/editor/domain/constants/RootNode.ts rename to packages/behavior-tree-editor/src/domain/constants/RootNode.ts index 2db45804..69f566c9 100644 --- a/packages/behavior-tree/src/editor/domain/constants/RootNode.ts +++ b/packages/behavior-tree-editor/src/domain/constants/RootNode.ts @@ -1,6 +1,6 @@ import { Node } from '../models/Node'; import { Position } from '../value-objects/Position'; -import { NodeTemplate } from '../../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; export const ROOT_NODE_ID = 'root-node'; diff --git a/packages/behavior-tree/src/editor/domain/errors/DomainError.ts b/packages/behavior-tree-editor/src/domain/errors/DomainError.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/errors/DomainError.ts rename to packages/behavior-tree-editor/src/domain/errors/DomainError.ts diff --git a/packages/behavior-tree/src/editor/domain/errors/NodeNotFoundError.ts b/packages/behavior-tree-editor/src/domain/errors/NodeNotFoundError.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/errors/NodeNotFoundError.ts rename to packages/behavior-tree-editor/src/domain/errors/NodeNotFoundError.ts diff --git a/packages/behavior-tree/src/editor/domain/errors/ValidationError.ts b/packages/behavior-tree-editor/src/domain/errors/ValidationError.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/errors/ValidationError.ts rename to packages/behavior-tree-editor/src/domain/errors/ValidationError.ts diff --git a/packages/behavior-tree/src/editor/domain/errors/index.ts b/packages/behavior-tree-editor/src/domain/errors/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/errors/index.ts rename to packages/behavior-tree-editor/src/domain/errors/index.ts diff --git a/packages/behavior-tree/src/editor/domain/index.ts b/packages/behavior-tree-editor/src/domain/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/index.ts rename to packages/behavior-tree-editor/src/domain/index.ts diff --git a/packages/behavior-tree/src/editor/domain/interfaces/INodeFactory.ts b/packages/behavior-tree-editor/src/domain/interfaces/INodeFactory.ts similarity index 91% rename from packages/behavior-tree/src/editor/domain/interfaces/INodeFactory.ts rename to packages/behavior-tree-editor/src/domain/interfaces/INodeFactory.ts index 2ac10fd6..9edb0f34 100644 --- a/packages/behavior-tree/src/editor/domain/interfaces/INodeFactory.ts +++ b/packages/behavior-tree-editor/src/domain/interfaces/INodeFactory.ts @@ -1,4 +1,4 @@ -import { NodeTemplate } from '../../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; import { Node } from '../models/Node'; import { Position } from '../value-objects'; diff --git a/packages/behavior-tree/src/editor/domain/interfaces/IRepository.ts b/packages/behavior-tree-editor/src/domain/interfaces/IRepository.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/interfaces/IRepository.ts rename to packages/behavior-tree-editor/src/domain/interfaces/IRepository.ts diff --git a/packages/behavior-tree/src/editor/domain/interfaces/ISerializer.ts b/packages/behavior-tree-editor/src/domain/interfaces/ISerializer.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/interfaces/ISerializer.ts rename to packages/behavior-tree-editor/src/domain/interfaces/ISerializer.ts diff --git a/packages/behavior-tree/src/editor/domain/interfaces/IValidator.ts b/packages/behavior-tree-editor/src/domain/interfaces/IValidator.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/interfaces/IValidator.ts rename to packages/behavior-tree-editor/src/domain/interfaces/IValidator.ts diff --git a/packages/behavior-tree/src/editor/domain/interfaces/index.ts b/packages/behavior-tree-editor/src/domain/interfaces/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/interfaces/index.ts rename to packages/behavior-tree-editor/src/domain/interfaces/index.ts diff --git a/packages/behavior-tree/src/editor/domain/models/BehaviorTree.ts b/packages/behavior-tree-editor/src/domain/models/BehaviorTree.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/models/BehaviorTree.ts rename to packages/behavior-tree-editor/src/domain/models/BehaviorTree.ts diff --git a/packages/behavior-tree/src/editor/domain/models/Blackboard.ts b/packages/behavior-tree-editor/src/domain/models/Blackboard.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/models/Blackboard.ts rename to packages/behavior-tree-editor/src/domain/models/Blackboard.ts diff --git a/packages/behavior-tree/src/editor/domain/models/Connection.ts b/packages/behavior-tree-editor/src/domain/models/Connection.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/models/Connection.ts rename to packages/behavior-tree-editor/src/domain/models/Connection.ts diff --git a/packages/behavior-tree/src/editor/domain/models/Node.ts b/packages/behavior-tree-editor/src/domain/models/Node.ts similarity index 98% rename from packages/behavior-tree/src/editor/domain/models/Node.ts rename to packages/behavior-tree-editor/src/domain/models/Node.ts index 476525af..bb1a7d50 100644 --- a/packages/behavior-tree/src/editor/domain/models/Node.ts +++ b/packages/behavior-tree-editor/src/domain/models/Node.ts @@ -1,4 +1,4 @@ -import { NodeTemplate } from '../../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; import { Position, NodeType } from '../value-objects'; import { ValidationError } from '../errors'; diff --git a/packages/behavior-tree/src/editor/domain/models/index.ts b/packages/behavior-tree-editor/src/domain/models/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/models/index.ts rename to packages/behavior-tree-editor/src/domain/models/index.ts diff --git a/packages/behavior-tree/src/editor/domain/services/TreeValidator.ts b/packages/behavior-tree-editor/src/domain/services/TreeValidator.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/services/TreeValidator.ts rename to packages/behavior-tree-editor/src/domain/services/TreeValidator.ts diff --git a/packages/behavior-tree/src/editor/domain/services/index.ts b/packages/behavior-tree-editor/src/domain/services/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/services/index.ts rename to packages/behavior-tree-editor/src/domain/services/index.ts diff --git a/packages/behavior-tree/src/editor/domain/value-objects/NodeType.ts b/packages/behavior-tree-editor/src/domain/value-objects/NodeType.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/value-objects/NodeType.ts rename to packages/behavior-tree-editor/src/domain/value-objects/NodeType.ts diff --git a/packages/behavior-tree/src/editor/domain/value-objects/Position.ts b/packages/behavior-tree-editor/src/domain/value-objects/Position.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/value-objects/Position.ts rename to packages/behavior-tree-editor/src/domain/value-objects/Position.ts diff --git a/packages/behavior-tree/src/editor/domain/value-objects/Size.ts b/packages/behavior-tree-editor/src/domain/value-objects/Size.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/value-objects/Size.ts rename to packages/behavior-tree-editor/src/domain/value-objects/Size.ts diff --git a/packages/behavior-tree/src/editor/domain/value-objects/index.ts b/packages/behavior-tree-editor/src/domain/value-objects/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/value-objects/index.ts rename to packages/behavior-tree-editor/src/domain/value-objects/index.ts diff --git a/packages/behavior-tree/src/editor/generators/GlobalBlackboardTypeGenerator.ts b/packages/behavior-tree-editor/src/generators/GlobalBlackboardTypeGenerator.ts similarity index 99% rename from packages/behavior-tree/src/editor/generators/GlobalBlackboardTypeGenerator.ts rename to packages/behavior-tree-editor/src/generators/GlobalBlackboardTypeGenerator.ts index dfb05952..7af7bc5d 100644 --- a/packages/behavior-tree/src/editor/generators/GlobalBlackboardTypeGenerator.ts +++ b/packages/behavior-tree-editor/src/generators/GlobalBlackboardTypeGenerator.ts @@ -1,4 +1,4 @@ -import { GlobalBlackboardConfig, BlackboardValueType } from '../..'; +import { BlackboardValueType, type GlobalBlackboardConfig } from '@esengine/behavior-tree'; /** * 类型生成配置选项 diff --git a/packages/behavior-tree/src/editor/generators/LocalBlackboardTypeGenerator.ts b/packages/behavior-tree-editor/src/generators/LocalBlackboardTypeGenerator.ts similarity index 100% rename from packages/behavior-tree/src/editor/generators/LocalBlackboardTypeGenerator.ts rename to packages/behavior-tree-editor/src/generators/LocalBlackboardTypeGenerator.ts diff --git a/packages/behavior-tree/src/editor/hooks/index.ts b/packages/behavior-tree-editor/src/hooks/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/index.ts rename to packages/behavior-tree-editor/src/hooks/index.ts diff --git a/packages/behavior-tree/src/editor/hooks/useCanvasInteraction.ts b/packages/behavior-tree-editor/src/hooks/useCanvasInteraction.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useCanvasInteraction.ts rename to packages/behavior-tree-editor/src/hooks/useCanvasInteraction.ts diff --git a/packages/behavior-tree/src/editor/hooks/useCanvasMouseEvents.ts b/packages/behavior-tree-editor/src/hooks/useCanvasMouseEvents.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useCanvasMouseEvents.ts rename to packages/behavior-tree-editor/src/hooks/useCanvasMouseEvents.ts diff --git a/packages/behavior-tree/src/editor/hooks/useCommandHistory.ts b/packages/behavior-tree-editor/src/hooks/useCommandHistory.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useCommandHistory.ts rename to packages/behavior-tree-editor/src/hooks/useCommandHistory.ts diff --git a/packages/behavior-tree/src/editor/hooks/useConnectionOperations.ts b/packages/behavior-tree-editor/src/hooks/useConnectionOperations.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useConnectionOperations.ts rename to packages/behavior-tree-editor/src/hooks/useConnectionOperations.ts diff --git a/packages/behavior-tree/src/editor/hooks/useContextMenu.ts b/packages/behavior-tree-editor/src/hooks/useContextMenu.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useContextMenu.ts rename to packages/behavior-tree-editor/src/hooks/useContextMenu.ts diff --git a/packages/behavior-tree/src/editor/hooks/useDropHandler.ts b/packages/behavior-tree-editor/src/hooks/useDropHandler.ts similarity index 98% rename from packages/behavior-tree/src/editor/hooks/useDropHandler.ts rename to packages/behavior-tree-editor/src/hooks/useDropHandler.ts index 43482349..a1ebe2a6 100644 --- a/packages/behavior-tree/src/editor/hooks/useDropHandler.ts +++ b/packages/behavior-tree-editor/src/hooks/useDropHandler.ts @@ -1,5 +1,5 @@ import { useState, type RefObject, React, createLogger } from '@esengine/editor-runtime'; -import { NodeTemplate, NodeType } from '../..'; +import { NodeType, type NodeTemplate } from '@esengine/behavior-tree'; import { Position } from '../domain/value-objects/Position'; import { useNodeOperations } from './useNodeOperations'; diff --git a/packages/behavior-tree/src/editor/hooks/useEditorHandlers.ts b/packages/behavior-tree-editor/src/hooks/useEditorHandlers.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useEditorHandlers.ts rename to packages/behavior-tree-editor/src/hooks/useEditorHandlers.ts diff --git a/packages/behavior-tree/src/editor/hooks/useEditorState.ts b/packages/behavior-tree-editor/src/hooks/useEditorState.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useEditorState.ts rename to packages/behavior-tree-editor/src/hooks/useEditorState.ts diff --git a/packages/behavior-tree/src/editor/hooks/useExecutionController.ts b/packages/behavior-tree-editor/src/hooks/useExecutionController.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useExecutionController.ts rename to packages/behavior-tree-editor/src/hooks/useExecutionController.ts diff --git a/packages/behavior-tree/src/editor/hooks/useKeyboardShortcuts.ts b/packages/behavior-tree-editor/src/hooks/useKeyboardShortcuts.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useKeyboardShortcuts.ts rename to packages/behavior-tree-editor/src/hooks/useKeyboardShortcuts.ts diff --git a/packages/behavior-tree/src/editor/hooks/useNodeDrag.ts b/packages/behavior-tree-editor/src/hooks/useNodeDrag.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useNodeDrag.ts rename to packages/behavior-tree-editor/src/hooks/useNodeDrag.ts diff --git a/packages/behavior-tree/src/editor/hooks/useNodeOperations.ts b/packages/behavior-tree-editor/src/hooks/useNodeOperations.ts similarity index 97% rename from packages/behavior-tree/src/editor/hooks/useNodeOperations.ts rename to packages/behavior-tree-editor/src/hooks/useNodeOperations.ts index aa52006f..6be1b5d6 100644 --- a/packages/behavior-tree/src/editor/hooks/useNodeOperations.ts +++ b/packages/behavior-tree-editor/src/hooks/useNodeOperations.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo, CommandManager } from '@esengine/editor-runtime'; -import { NodeTemplate } from '../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; import { Position } from '../domain/value-objects/Position'; import { INodeFactory } from '../domain/interfaces/INodeFactory'; import { TreeStateAdapter } from '../application/state/BehaviorTreeDataStore'; diff --git a/packages/behavior-tree/src/editor/hooks/useNodeTracking.ts b/packages/behavior-tree-editor/src/hooks/useNodeTracking.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useNodeTracking.ts rename to packages/behavior-tree-editor/src/hooks/useNodeTracking.ts diff --git a/packages/behavior-tree/src/editor/hooks/usePortConnection.ts b/packages/behavior-tree-editor/src/hooks/usePortConnection.ts similarity index 99% rename from packages/behavior-tree/src/editor/hooks/usePortConnection.ts rename to packages/behavior-tree-editor/src/hooks/usePortConnection.ts index 65c90c07..e425cc26 100644 --- a/packages/behavior-tree/src/editor/hooks/usePortConnection.ts +++ b/packages/behavior-tree-editor/src/hooks/usePortConnection.ts @@ -1,6 +1,6 @@ import { type RefObject, React } from '@esengine/editor-runtime'; import { BehaviorTreeNode, Connection, ROOT_NODE_ID, useUIStore } from '../stores'; -import { PropertyDefinition } from '../..'; +import type { PropertyDefinition } from '@esengine/behavior-tree'; import { useConnectionOperations } from './useConnectionOperations'; interface UsePortConnectionParams { diff --git a/packages/behavior-tree/src/editor/hooks/useQuickCreateMenu.ts b/packages/behavior-tree-editor/src/hooks/useQuickCreateMenu.ts similarity index 99% rename from packages/behavior-tree/src/editor/hooks/useQuickCreateMenu.ts rename to packages/behavior-tree-editor/src/hooks/useQuickCreateMenu.ts index 777de355..eac9a74b 100644 --- a/packages/behavior-tree/src/editor/hooks/useQuickCreateMenu.ts +++ b/packages/behavior-tree-editor/src/hooks/useQuickCreateMenu.ts @@ -1,5 +1,5 @@ import { useState, type RefObject } from '@esengine/editor-runtime'; -import { NodeTemplate } from '../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; import { BehaviorTreeNode, Connection, useBehaviorTreeDataStore } from '../stores'; import { Node } from '../domain/models/Node'; import { Position } from '../domain/value-objects/Position'; diff --git a/packages/behavior-tree/src/editor/index.ts b/packages/behavior-tree-editor/src/index.ts similarity index 96% rename from packages/behavior-tree/src/editor/index.ts rename to packages/behavior-tree-editor/src/index.ts index ff1d3ec1..6772feae 100644 --- a/packages/behavior-tree/src/editor/index.ts +++ b/packages/behavior-tree-editor/src/index.ts @@ -4,7 +4,7 @@ */ import type { ServiceContainer } from '@esengine/ecs-framework'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; import { type IEditorModuleLoader, type IPluginLoader, @@ -20,6 +20,7 @@ import { MessageHub, IMessageHub, FileActionRegistry, + IFileActionRegistry, IDialogService, IFileSystemService, type IDialog, @@ -28,8 +29,8 @@ import { PluginAPI, } from '@esengine/editor-runtime'; -// Runtime imports (relative paths since we're in the same package) -import { BehaviorTreeRuntimeComponent } from '../execution/BehaviorTreeRuntimeComponent'; +// Runtime imports from @esengine/behavior-tree package +import { BehaviorTreeRuntimeComponent, BehaviorTreeRuntimeModule } from '@esengine/behavior-tree'; // Editor components and services import { BehaviorTreeService } from './services/BehaviorTreeService'; @@ -41,9 +42,8 @@ import { useBehaviorTreeDataStore } from './stores'; import { createRootNode } from './domain/constants/RootNode'; import { PluginContext } from './PluginContext'; -// Import descriptor from local file, runtime module from main module +// Import descriptor from local file import { descriptor } from './BehaviorTreePlugin'; -import { BehaviorTreeRuntimeModule } from '../BehaviorTreeRuntimeModule'; // 导入编辑器 CSS 样式(会被 vite 自动处理并注入到 DOM) // Import editor CSS styles (automatically handled and injected by vite) @@ -86,7 +86,7 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader { private registerAssetCreationMappings(services: ServiceContainer): void { try { - const fileActionRegistry = services.resolve(FileActionRegistry); + const fileActionRegistry = services.resolve(IFileActionRegistry); if (fileActionRegistry) { fileActionRegistry.registerAssetCreationMapping({ extension: '.btree', diff --git a/packages/behavior-tree/src/editor/infrastructure/events/EditorEventBus.ts b/packages/behavior-tree-editor/src/infrastructure/events/EditorEventBus.ts similarity index 100% rename from packages/behavior-tree/src/editor/infrastructure/events/EditorEventBus.ts rename to packages/behavior-tree-editor/src/infrastructure/events/EditorEventBus.ts diff --git a/packages/behavior-tree/src/editor/infrastructure/factories/NodeFactory.ts b/packages/behavior-tree-editor/src/infrastructure/factories/NodeFactory.ts similarity index 97% rename from packages/behavior-tree/src/editor/infrastructure/factories/NodeFactory.ts rename to packages/behavior-tree-editor/src/infrastructure/factories/NodeFactory.ts index 4bb9228c..cc0670b2 100644 --- a/packages/behavior-tree/src/editor/infrastructure/factories/NodeFactory.ts +++ b/packages/behavior-tree-editor/src/infrastructure/factories/NodeFactory.ts @@ -1,4 +1,4 @@ -import { NodeTemplate, NodeTemplates } from '../../..'; +import { NodeTemplates, type NodeTemplate } from '@esengine/behavior-tree'; import { Node } from '../../domain/models/Node'; import { Position } from '../../domain/value-objects/Position'; import { INodeFactory } from '../../domain/interfaces/INodeFactory'; diff --git a/packages/behavior-tree/src/editor/infrastructure/factories/index.ts b/packages/behavior-tree-editor/src/infrastructure/factories/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/infrastructure/factories/index.ts rename to packages/behavior-tree-editor/src/infrastructure/factories/index.ts diff --git a/packages/behavior-tree/src/editor/infrastructure/index.ts b/packages/behavior-tree-editor/src/infrastructure/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/infrastructure/index.ts rename to packages/behavior-tree-editor/src/infrastructure/index.ts diff --git a/packages/behavior-tree/src/editor/infrastructure/serialization/BehaviorTreeSerializer.ts b/packages/behavior-tree-editor/src/infrastructure/serialization/BehaviorTreeSerializer.ts similarity index 99% rename from packages/behavior-tree/src/editor/infrastructure/serialization/BehaviorTreeSerializer.ts rename to packages/behavior-tree-editor/src/infrastructure/serialization/BehaviorTreeSerializer.ts index 9fd868e0..51f3d989 100644 --- a/packages/behavior-tree/src/editor/infrastructure/serialization/BehaviorTreeSerializer.ts +++ b/packages/behavior-tree-editor/src/infrastructure/serialization/BehaviorTreeSerializer.ts @@ -1,6 +1,6 @@ import { BehaviorTree } from '../../domain/models/BehaviorTree'; import { ISerializer, SerializationFormat } from '../../domain/interfaces/ISerializer'; -import { BehaviorTreeAssetSerializer, EditorFormatConverter } from '../../..'; +import { BehaviorTreeAssetSerializer, EditorFormatConverter } from '@esengine/behavior-tree'; /** * 序列化选项 diff --git a/packages/behavior-tree/src/editor/infrastructure/serialization/index.ts b/packages/behavior-tree-editor/src/infrastructure/serialization/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/infrastructure/serialization/index.ts rename to packages/behavior-tree-editor/src/infrastructure/serialization/index.ts diff --git a/packages/behavior-tree/src/editor/infrastructure/services/NodeRegistryService.ts b/packages/behavior-tree-editor/src/infrastructure/services/NodeRegistryService.ts similarity index 98% rename from packages/behavior-tree/src/editor/infrastructure/services/NodeRegistryService.ts rename to packages/behavior-tree-editor/src/infrastructure/services/NodeRegistryService.ts index e567f7a7..7154c304 100644 --- a/packages/behavior-tree/src/editor/infrastructure/services/NodeRegistryService.ts +++ b/packages/behavior-tree-editor/src/infrastructure/services/NodeRegistryService.ts @@ -1,4 +1,4 @@ -import { NodeTemplate, NodeMetadataRegistry, NodeMetadata, NodeType } from '../../..'; +import { NodeMetadataRegistry, NodeType, type NodeTemplate, type NodeMetadata } from '@esengine/behavior-tree'; /** * 简化的节点注册配置 diff --git a/packages/behavior-tree/src/editor/infrastructure/validation/BehaviorTreeValidator.ts b/packages/behavior-tree-editor/src/infrastructure/validation/BehaviorTreeValidator.ts similarity index 100% rename from packages/behavior-tree/src/editor/infrastructure/validation/BehaviorTreeValidator.ts rename to packages/behavior-tree-editor/src/infrastructure/validation/BehaviorTreeValidator.ts diff --git a/packages/behavior-tree/src/editor/interfaces/IEditorExtensions.ts b/packages/behavior-tree-editor/src/interfaces/IEditorExtensions.ts similarity index 99% rename from packages/behavior-tree/src/editor/interfaces/IEditorExtensions.ts rename to packages/behavior-tree-editor/src/interfaces/IEditorExtensions.ts index 0d1faa0a..fca0a475 100644 --- a/packages/behavior-tree/src/editor/interfaces/IEditorExtensions.ts +++ b/packages/behavior-tree-editor/src/interfaces/IEditorExtensions.ts @@ -1,6 +1,6 @@ import { React, createLogger } from '@esengine/editor-runtime'; import type { LucideIcon } from '@esengine/editor-runtime'; -import { NodeTemplate } from '../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; import { Node as BehaviorTreeNode } from '../domain/models/Node'; const logger = createLogger('IEditorExtensions'); diff --git a/packages/behavior-tree/src/editor/providers/BehaviorTreeNodeInspectorProvider.tsx b/packages/behavior-tree-editor/src/providers/BehaviorTreeNodeInspectorProvider.tsx similarity index 99% rename from packages/behavior-tree/src/editor/providers/BehaviorTreeNodeInspectorProvider.tsx rename to packages/behavior-tree-editor/src/providers/BehaviorTreeNodeInspectorProvider.tsx index 6ae22f38..9887c78c 100644 --- a/packages/behavior-tree/src/editor/providers/BehaviorTreeNodeInspectorProvider.tsx +++ b/packages/behavior-tree-editor/src/providers/BehaviorTreeNodeInspectorProvider.tsx @@ -10,7 +10,7 @@ import { PluginAPI, } from '@esengine/editor-runtime'; import { Node as BehaviorTreeNode } from '../domain/models/Node'; -import { PropertyDefinition } from '../..'; +import type { PropertyDefinition } from '@esengine/behavior-tree'; /** * 节点属性编辑器组件 diff --git a/packages/behavior-tree/src/editor/services/BehaviorTreeService.ts b/packages/behavior-tree-editor/src/services/BehaviorTreeService.ts similarity index 100% rename from packages/behavior-tree/src/editor/services/BehaviorTreeService.ts rename to packages/behavior-tree-editor/src/services/BehaviorTreeService.ts diff --git a/packages/behavior-tree/src/editor/services/FileSystemService.ts b/packages/behavior-tree-editor/src/services/FileSystemService.ts similarity index 100% rename from packages/behavior-tree/src/editor/services/FileSystemService.ts rename to packages/behavior-tree-editor/src/services/FileSystemService.ts diff --git a/packages/behavior-tree/src/editor/services/NotificationService.ts b/packages/behavior-tree-editor/src/services/NotificationService.ts similarity index 100% rename from packages/behavior-tree/src/editor/services/NotificationService.ts rename to packages/behavior-tree-editor/src/services/NotificationService.ts diff --git a/packages/behavior-tree/src/editor/services/index.ts b/packages/behavior-tree-editor/src/services/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/services/index.ts rename to packages/behavior-tree-editor/src/services/index.ts diff --git a/packages/behavior-tree/src/editor/stores/ExecutionStatsStore.ts b/packages/behavior-tree-editor/src/stores/ExecutionStatsStore.ts similarity index 100% rename from packages/behavior-tree/src/editor/stores/ExecutionStatsStore.ts rename to packages/behavior-tree-editor/src/stores/ExecutionStatsStore.ts diff --git a/packages/behavior-tree/src/editor/stores/index.ts b/packages/behavior-tree-editor/src/stores/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/stores/index.ts rename to packages/behavior-tree-editor/src/stores/index.ts diff --git a/packages/behavior-tree/src/editor/stores/useUIStore.ts b/packages/behavior-tree-editor/src/stores/useUIStore.ts similarity index 100% rename from packages/behavior-tree/src/editor/stores/useUIStore.ts rename to packages/behavior-tree-editor/src/stores/useUIStore.ts diff --git a/packages/behavior-tree/src/editor/styles/BehaviorTreeNode.css b/packages/behavior-tree-editor/src/styles/BehaviorTreeNode.css similarity index 100% rename from packages/behavior-tree/src/editor/styles/BehaviorTreeNode.css rename to packages/behavior-tree-editor/src/styles/BehaviorTreeNode.css diff --git a/packages/behavior-tree/src/editor/styles/Toast.css b/packages/behavior-tree-editor/src/styles/Toast.css similarity index 100% rename from packages/behavior-tree/src/editor/styles/Toast.css rename to packages/behavior-tree-editor/src/styles/Toast.css diff --git a/packages/behavior-tree/src/editor/types/Breakpoint.ts b/packages/behavior-tree-editor/src/types/Breakpoint.ts similarity index 100% rename from packages/behavior-tree/src/editor/types/Breakpoint.ts rename to packages/behavior-tree-editor/src/types/Breakpoint.ts diff --git a/packages/behavior-tree/src/editor/types/index.ts b/packages/behavior-tree-editor/src/types/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/types/index.ts rename to packages/behavior-tree-editor/src/types/index.ts diff --git a/packages/behavior-tree/src/editor/utils/BehaviorTreeExecutor.ts b/packages/behavior-tree-editor/src/utils/BehaviorTreeExecutor.ts similarity index 99% rename from packages/behavior-tree/src/editor/utils/BehaviorTreeExecutor.ts rename to packages/behavior-tree-editor/src/utils/BehaviorTreeExecutor.ts index ca7d59c7..9e73e2d6 100644 --- a/packages/behavior-tree/src/editor/utils/BehaviorTreeExecutor.ts +++ b/packages/behavior-tree-editor/src/utils/BehaviorTreeExecutor.ts @@ -1,13 +1,13 @@ import { World, Entity, Scene, createLogger, Time, Core } from '@esengine/ecs-framework'; import { - BehaviorTreeData, - BehaviorNodeData, BehaviorTreeRuntimeComponent, BehaviorTreeAssetManager, BehaviorTreeExecutionSystem, TaskStatus, - NodeType -} from '../..'; + NodeType, + type BehaviorTreeData, + type BehaviorNodeData +} from '@esengine/behavior-tree'; import type { BehaviorTreeNode } from '../stores'; import { useExecutionStatsStore } from '../stores/ExecutionStatsStore'; import type { Breakpoint } from '../types/Breakpoint'; diff --git a/packages/behavior-tree/src/editor/utils/DOMCache.ts b/packages/behavior-tree-editor/src/utils/DOMCache.ts similarity index 100% rename from packages/behavior-tree/src/editor/utils/DOMCache.ts rename to packages/behavior-tree-editor/src/utils/DOMCache.ts diff --git a/packages/behavior-tree/src/editor/utils/RuntimeLoader.ts b/packages/behavior-tree-editor/src/utils/RuntimeLoader.ts similarity index 98% rename from packages/behavior-tree/src/editor/utils/RuntimeLoader.ts rename to packages/behavior-tree-editor/src/utils/RuntimeLoader.ts index 2d096212..e4be909c 100644 --- a/packages/behavior-tree/src/editor/utils/RuntimeLoader.ts +++ b/packages/behavior-tree-editor/src/utils/RuntimeLoader.ts @@ -1,4 +1,4 @@ -import { BehaviorTreeAssetManager, BehaviorTreeData } from '../..'; +import { BehaviorTreeAssetManager, type BehaviorTreeData } from '@esengine/behavior-tree'; import { BehaviorTreeSerializer } from '../infrastructure/serialization/BehaviorTreeSerializer'; import { BehaviorTree } from '../domain/models/BehaviorTree'; diff --git a/packages/behavior-tree/src/editor/utils/portUtils.ts b/packages/behavior-tree-editor/src/utils/portUtils.ts similarity index 100% rename from packages/behavior-tree/src/editor/utils/portUtils.ts rename to packages/behavior-tree-editor/src/utils/portUtils.ts diff --git a/packages/behavior-tree-editor/tsconfig.build.json b/packages/behavior-tree-editor/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/behavior-tree-editor/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/behavior-tree-editor/tsconfig.json b/packages/behavior-tree-editor/tsconfig.json new file mode 100644 index 00000000..d099ddd8 --- /dev/null +++ b/packages/behavior-tree-editor/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/behavior-tree-editor/tsup.config.ts b/packages/behavior-tree-editor/tsup.config.ts new file mode 100644 index 00000000..b4f49f5d --- /dev/null +++ b/packages/behavior-tree-editor/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { editorOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...editorOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/behavior-tree/jest.config.cjs b/packages/behavior-tree/jest.config.cjs index a4dcf800..47414c64 100644 --- a/packages/behavior-tree/jest.config.cjs +++ b/packages/behavior-tree/jest.config.cjs @@ -1,7 +1,7 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', - roots: ['/tests'], + roots: ['/src'], testMatch: ['**/*.test.ts'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], collectCoverageFrom: [ @@ -11,5 +11,6 @@ module.exports = { ], coverageDirectory: 'coverage', verbose: true, - testTimeout: 10000 + testTimeout: 10000, + passWithNoTests: true }; diff --git a/packages/behavior-tree/package.json b/packages/behavior-tree/package.json index 4b5f4baf..54dfcbe8 100644 --- a/packages/behavior-tree/package.json +++ b/packages/behavior-tree/package.json @@ -1,99 +1,63 @@ { - "name": "@esengine/behavior-tree", - "version": "1.0.1", - "description": "ECS-based AI behavior tree system with visual editor and runtime execution", - "main": "dist/index.js", - "module": "dist/index.js", - "types": "dist/index.d.ts", - "type": "module", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "name": "@esengine/behavior-tree", + "version": "1.0.1", + "description": "ECS-based AI behavior tree system with runtime execution", + "esengine": { + "plugin": true, + "pluginExport": "BehaviorTreePlugin", + "category": "ai" }, - "./runtime": { - "types": "./dist/runtime.d.ts", - "import": "./dist/runtime.js" + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } }, - "./editor": { - "types": "./dist/editor/index.d.ts", - "import": "./dist/editor/index.js" + "files": [ + "dist" + ], + "keywords": [ + "ecs", + "behavior-tree", + "ai", + "game-ai", + "entity-component-system" + ], + "scripts": { + "clean": "rimraf dist tsconfig.tsbuildinfo", + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit" }, - "./plugin.json": "./plugin.json" - }, - "files": [ - "dist", - "plugin.json" - ], - "keywords": [ - "ecs", - "behavior-tree", - "ai", - "game-ai", - "entity-component-system" - ], - "scripts": { - "clean": "rimraf dist tsconfig.tsbuildinfo", - "build": "vite build", - "build:watch": "vite build --watch", - "type-check": "tsc --noEmit", - "test": "jest --config jest.config.cjs", - "test:watch": "jest --watch --config jest.config.cjs" - }, - "author": "yhh", - "license": "MIT", - "peerDependencies": { - "@esengine/ecs-framework": ">=2.0.0", - "@esengine/ecs-components": "workspace:*", - "@esengine/asset-system": "workspace:*", - "@esengine/editor-runtime": "workspace:*", - "lucide-react": "^0.545.0", - "react": "^18.3.1", - "zustand": "^4.5.2" - }, - "peerDependenciesMeta": { - "@esengine/ecs-components": { - "optional": true + "author": "yhh", + "license": "MIT", + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/asset-system": "workspace:*", + "@esengine/build-config": "workspace:*", + "@types/jest": "^29.5.14", + "@types/node": "^20.19.17", + "jest": "^29.7.0", + "rimraf": "^5.0.0", + "ts-jest": "^29.4.0", + "tsup": "^8.0.0", + "typescript": "^5.8.3" }, - "@esengine/asset-system": { - "optional": true + "dependencies": { + "tslib": "^2.8.1" }, - "@esengine/editor-runtime": { - "optional": true + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" }, - "react": { - "optional": true - }, - "lucide-react": { - "optional": true - }, - "zustand": { - "optional": true + "repository": { + "type": "git", + "url": "https://github.com/esengine/ecs-framework.git", + "directory": "packages/behavior-tree" } - }, - "devDependencies": { - "@tauri-apps/plugin-fs": "^2.4.2", - "@types/jest": "^29.5.14", - "@types/node": "^20.19.17", - "@types/react": "^18.3.12", - "@vitejs/plugin-react": "^4.7.0", - "jest": "^29.7.0", - "rimraf": "^5.0.0", - "ts-jest": "^29.4.0", - "typescript": "^5.8.3", - "vite": "^6.0.7", - "vite-plugin-dts": "^3.7.0" - }, - "dependencies": { - "tslib": "^2.8.1" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, - "repository": { - "type": "git", - "url": "https://github.com/esengine/ecs-framework.git", - "directory": "packages/behavior-tree" - } } diff --git a/packages/behavior-tree/src/BehaviorTreeRuntimeModule.ts b/packages/behavior-tree/src/BehaviorTreeRuntimeModule.ts index 593b1e9c..025b8543 100644 --- a/packages/behavior-tree/src/BehaviorTreeRuntimeModule.ts +++ b/packages/behavior-tree/src/BehaviorTreeRuntimeModule.ts @@ -1,11 +1,6 @@ -/** - * Behavior Tree Runtime Module (Pure runtime, no editor dependencies) - * 行为树运行时模块(纯运行时,无编辑器依赖) - */ - import type { IScene, ServiceContainer } from '@esengine/ecs-framework'; import { ComponentRegistry, Core } from '@esengine/ecs-framework'; -import type { IRuntimeModuleLoader, SystemContext } from '@esengine/ecs-components'; +import type { IRuntimeModule, IPlugin, PluginDescriptor, SystemContext } from '@esengine/engine-core'; import type { AssetManager } from '@esengine/asset-system'; import { BehaviorTreeRuntimeComponent } from './execution/BehaviorTreeRuntimeComponent'; @@ -15,11 +10,12 @@ import { GlobalBlackboardService } from './Services/GlobalBlackboardService'; import { BehaviorTreeLoader } from './loaders/BehaviorTreeLoader'; import { BehaviorTreeAssetType } from './index'; -/** - * Behavior Tree Runtime Module - * 行为树运行时模块 - */ -export class BehaviorTreeRuntimeModule implements IRuntimeModuleLoader { +export interface BehaviorTreeSystemContext extends SystemContext { + behaviorTreeSystem?: BehaviorTreeExecutionSystem; + assetManager?: AssetManager; +} + +class BehaviorTreeRuntimeModule implements IRuntimeModule { private _loaderRegistered = false; registerComponents(registry: typeof ComponentRegistry): void { @@ -36,33 +32,41 @@ export class BehaviorTreeRuntimeModule implements IRuntimeModuleLoader { } createSystems(scene: IScene, context: SystemContext): void { - // 注册行为树加载器到 AssetManager - // Register behavior tree loader to AssetManager - const assetManager = context.assetManager as AssetManager | undefined; - console.log('[BehaviorTreeRuntimeModule] createSystems called, assetManager:', assetManager ? 'exists' : 'null'); + const btContext = context as BehaviorTreeSystemContext; - if (!this._loaderRegistered && assetManager) { - assetManager.registerLoader(BehaviorTreeAssetType, new BehaviorTreeLoader()); + if (!this._loaderRegistered && btContext.assetManager) { + btContext.assetManager.registerLoader(BehaviorTreeAssetType, new BehaviorTreeLoader()); this._loaderRegistered = true; - console.log('[BehaviorTreeRuntimeModule] Registered BehaviorTreeLoader for type:', BehaviorTreeAssetType); } const behaviorTreeSystem = new BehaviorTreeExecutionSystem(Core); - // 设置 AssetManager 引用 - // Set AssetManager reference - if (assetManager) { - behaviorTreeSystem.setAssetManager(assetManager); - console.log('[BehaviorTreeRuntimeModule] Set assetManager on behaviorTreeSystem'); - } else { - console.warn('[BehaviorTreeRuntimeModule] assetManager is null, cannot set on behaviorTreeSystem'); + if (btContext.assetManager) { + behaviorTreeSystem.setAssetManager(btContext.assetManager); } - if (context.isEditor) { + if (btContext.isEditor) { behaviorTreeSystem.enabled = false; } scene.addSystem(behaviorTreeSystem); - context.behaviorTreeSystem = behaviorTreeSystem; + btContext.behaviorTreeSystem = behaviorTreeSystem; } } + +const descriptor: PluginDescriptor = { + id: '@esengine/behavior-tree', + name: 'Behavior Tree', + version: '1.0.0', + description: 'AI behavior tree system', + category: 'ai', + enabledByDefault: false, + isEnginePlugin: true +}; + +export const BehaviorTreePlugin: IPlugin = { + descriptor, + runtimeModule: new BehaviorTreeRuntimeModule() +}; + +export { BehaviorTreeRuntimeModule }; diff --git a/packages/behavior-tree/src/constants.ts b/packages/behavior-tree/src/constants.ts new file mode 100644 index 00000000..5ea246f7 --- /dev/null +++ b/packages/behavior-tree/src/constants.ts @@ -0,0 +1,8 @@ +/** + * Behavior Tree Constants + * 行为树常量 + */ + +// Asset type constant for behavior tree +// 行为树资产类型常量 +export const BehaviorTreeAssetType = 'behaviortree' as const; diff --git a/packages/behavior-tree/src/index.ts b/packages/behavior-tree/src/index.ts index dbc2177d..4e020310 100644 --- a/packages/behavior-tree/src/index.ts +++ b/packages/behavior-tree/src/index.ts @@ -7,9 +7,8 @@ * @packageDocumentation */ -// Asset type constant for behavior tree -// 行为树资产类型常量 -export const BehaviorTreeAssetType = 'behaviortree' as const; +// Constants +export { BehaviorTreeAssetType } from './constants'; // Types export * from './Types/TaskStatus'; @@ -35,8 +34,5 @@ export * from './Services/GlobalBlackboardService'; export type { BlackboardTypeDefinition } from './Blackboard/BlackboardTypes'; export { BlackboardTypes } from './Blackboard/BlackboardTypes'; -// Runtime module (no editor dependencies) -export { BehaviorTreeRuntimeModule } from './BehaviorTreeRuntimeModule'; - -// Plugin (for PluginManager - includes editor dependencies) -export { BehaviorTreePlugin } from './editor/index'; +// Runtime module and plugin +export { BehaviorTreeRuntimeModule, BehaviorTreePlugin, type BehaviorTreeSystemContext } from './BehaviorTreeRuntimeModule'; diff --git a/packages/behavior-tree/src/loaders/BehaviorTreeLoader.ts b/packages/behavior-tree/src/loaders/BehaviorTreeLoader.ts index ee6f2f45..befd7610 100644 --- a/packages/behavior-tree/src/loaders/BehaviorTreeLoader.ts +++ b/packages/behavior-tree/src/loaders/BehaviorTreeLoader.ts @@ -15,7 +15,7 @@ import { Core } from '@esengine/ecs-framework'; import { BehaviorTreeData } from '../execution/BehaviorTreeData'; import { BehaviorTreeAssetManager } from '../execution/BehaviorTreeAssetManager'; import { EditorToBehaviorTreeDataConverter } from '../Serialization/EditorToBehaviorTreeDataConverter'; -import { BehaviorTreeAssetType } from '../index'; +import { BehaviorTreeAssetType } from '../constants'; /** * 行为树资产接口 diff --git a/packages/behavior-tree/src/runtime.ts b/packages/behavior-tree/src/runtime.ts deleted file mode 100644 index 8f89ad7f..00000000 --- a/packages/behavior-tree/src/runtime.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @esengine/behavior-tree Runtime Entry Point - * - * This entry point exports only runtime-related code without any editor dependencies. - * Use this for standalone game runtime builds. - * - * 此入口点仅导出运行时相关代码,不包含任何编辑器依赖。 - * 用于独立游戏运行时构建。 - */ - -// Types -export * from './Types/TaskStatus'; - -// Execution (runtime core) -export * from './execution'; - -// Utilities -export * from './BehaviorTreeStarter'; -export * from './BehaviorTreeBuilder'; - -// Serialization -export * from './Serialization/NodeTemplates'; -export * from './Serialization/BehaviorTreeAsset'; -export * from './Serialization/EditorFormatConverter'; -export * from './Serialization/BehaviorTreeAssetSerializer'; -export * from './Serialization/EditorToBehaviorTreeDataConverter'; - -// Services -export * from './Services/GlobalBlackboardService'; - -// Blackboard types (excluding BlackboardValueType which is already exported from TaskStatus) -export type { BlackboardTypeDefinition } from './Blackboard/BlackboardTypes'; -export { BlackboardTypes } from './Blackboard/BlackboardTypes'; - -// Runtime module -export { BehaviorTreeRuntimeModule } from './BehaviorTreeRuntimeModule'; diff --git a/packages/behavior-tree/tsconfig.build.json b/packages/behavior-tree/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/behavior-tree/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/behavior-tree/tsconfig.json b/packages/behavior-tree/tsconfig.json index cbca93a9..66b54d18 100644 --- a/packages/behavior-tree/tsconfig.json +++ b/packages/behavior-tree/tsconfig.json @@ -30,7 +30,7 @@ ], "references": [ { "path": "../core" }, - { "path": "../components" }, + { "path": "../engine-core" }, { "path": "../editor-core" }, { "path": "../ui" }, { "path": "../editor-runtime" } diff --git a/packages/behavior-tree/tsup.config.ts b/packages/behavior-tree/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/behavior-tree/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/behavior-tree/vite.config.ts b/packages/behavior-tree/vite.config.ts deleted file mode 100644 index 24ab88ae..00000000 --- a/packages/behavior-tree/vite.config.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; -import dts from 'vite-plugin-dts'; -import react from '@vitejs/plugin-react'; - -/** - * 自定义插件:将 CSS 转换为自执行的样式注入代码 - * Custom plugin: Convert CSS to self-executing style injection code - * - * 当用户写 `import './styles.css'` 时,这个插件会: - * 1. 在构建时将 CSS 内容转换为 JS 代码 - * 2. JS 代码在模块导入时自动执行,将样式注入到 DOM - * 3. 使用唯一 ID 防止重复注入 - */ -function escapeUnsafeChars(str: string): string { - const charMap: Record = { - '<': '\\u003C', - '>': '\\u003E', - '/': '\\u002F', - '\\': '\\\\', - '\u2028': '\\u2028', - '\u2029': '\\u2029' - }; - return str.replace(/[<>\\/\u2028\u2029]/g, (x) => charMap[x] || x); -} - -function injectCSSPlugin(): unknown { - const cssIdMap = new Map(); - let cssCounter = 0; - - return { - name: 'inject-css-plugin', - enforce: 'post' as const, - generateBundle(_options: unknown, bundle: Record) { - const bundleKeys = Object.keys(bundle); - - // 找到所有 CSS 文件 - const cssFiles = bundleKeys.filter(key => key.endsWith('.css')); - - for (const cssFile of cssFiles) { - const cssChunk = bundle[cssFile]; - if (!cssChunk || !cssChunk.source) continue; - - const cssContent = cssChunk.source; - const styleId = `esengine-behavior-tree-style-${cssCounter++}`; - cssIdMap.set(cssFile, styleId); - - // 生成样式注入代码 - const injectCode = `(function(){if(typeof document!=='undefined'){var s=document.createElement('style');s.id='${styleId}';if(!document.getElementById(s.id)){s.textContent=${escapeUnsafeChars(JSON.stringify(cssContent))};document.head.appendChild(s);}}})();`; - - // 注入到 editor/index.js 或共享 chunk - for (const jsKey of bundleKeys) { - if (!jsKey.endsWith('.js')) continue; - const jsChunk = bundle[jsKey]; - if (!jsChunk || jsChunk.type !== 'chunk' || !jsChunk.code) continue; - - if (jsKey === 'editor/index.js' || jsKey.match(/^index-[^/]+\.js$/)) { - jsChunk.code = injectCode + '\n' + jsChunk.code; - } - } - - // 删除独立的 CSS 文件 - delete bundle[cssFile]; - } - } - }; -} - -export default defineConfig({ - plugins: [ - react(), - dts({ - include: ['src'], - outDir: 'dist', - rollupTypes: false - }), - injectCSSPlugin() - ], - esbuild: { - jsx: 'automatic', - }, - build: { - lib: { - entry: { - index: resolve(__dirname, 'src/index.ts'), - runtime: resolve(__dirname, 'src/runtime.ts'), - 'editor/index': resolve(__dirname, 'src/editor/index.ts') - }, - formats: ['es'], - fileName: (format, entryName) => `${entryName}.js` - }, - rollupOptions: { - external: [ - '@esengine/ecs-framework', - '@esengine/editor-runtime', - 'react', - 'react/jsx-runtime', - 'lucide-react', - 'zustand', - /^@esengine\//, - /^@tauri-apps\// - ], - output: { - exports: 'named', - preserveModules: false - } - }, - target: 'es2020', - minify: false, - sourcemap: true - } -}); diff --git a/packages/blueprint-editor/package.json b/packages/blueprint-editor/package.json new file mode 100644 index 00000000..a3cfd1e0 --- /dev/null +++ b/packages/blueprint-editor/package.json @@ -0,0 +1,49 @@ +{ + "name": "@esengine/blueprint-editor", + "version": "1.0.0", + "description": "Editor support for @esengine/blueprint - visual scripting editor", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "dependencies": { + "@esengine/blueprint": "workspace:*" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/node-editor": "workspace:*", + "@esengine/build-config": "workspace:*", + "lucide-react": "^0.545.0", + "react": "^18.3.1", + "zustand": "^5.0.8", + "@types/react": "^18.3.12", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "blueprint", + "editor", + "visual-scripting" + ], + "author": "", + "license": "MIT" +} diff --git a/packages/blueprint-editor/src/BlueprintPlugin.ts b/packages/blueprint-editor/src/BlueprintPlugin.ts new file mode 100644 index 00000000..8b7412cb --- /dev/null +++ b/packages/blueprint-editor/src/BlueprintPlugin.ts @@ -0,0 +1,126 @@ +/** + * Blueprint Editor Plugin + * 蓝图编辑器插件 + */ + +import { Core, type ServiceContainer } from '@esengine/ecs-framework'; +import type { IPlugin, PluginDescriptor } from '@esengine/engine-core'; +import type { IEditorModuleLoader, PanelDescriptor, FileActionHandler, FileCreationTemplate } from '@esengine/editor-core'; +import { MessageHub, PanelPosition } from '@esengine/editor-core'; + +// Re-export from @esengine/blueprint for runtime module +import { NodeRegistry, BlueprintVM, createBlueprintSystem } from '@esengine/blueprint'; + +// Store for pending file path +import { useBlueprintEditorStore } from './stores/blueprintEditorStore'; + +// Direct import of panel component (not dynamic import) +import { BlueprintEditorPanel } from './components/BlueprintEditorPanel'; + +/** + * Blueprint Editor Module Implementation + * 蓝图编辑器模块实现 + */ +class BlueprintEditorModuleImpl implements IEditorModuleLoader { + async install(_services: ServiceContainer): Promise { + // Editor module installation + } + + async uninstall(): Promise { + // Cleanup + } + + getPanels(): PanelDescriptor[] { + return [ + { + id: 'blueprint-editor', + title: 'Blueprint Editor', + position: PanelPosition.Center, + icon: 'Workflow', + closable: true, + resizable: true, + order: 50, + component: BlueprintEditorPanel, + isDynamic: true + } + ]; + } + + getFileActionHandlers(): FileActionHandler[] { + return [ + { + // 扩展名不带点号,与 FileActionRegistry.getFileExtension() 保持一致 + // Extensions without dot prefix, consistent with FileActionRegistry.getFileExtension() + extensions: ['blueprint', 'bp'], + onDoubleClick: (filePath: string) => { + // 设置待加载的文件路径到 store + // Set pending file path to store + useBlueprintEditorStore.getState().setPendingFilePath(filePath); + + // 通过 MessageHub 打开蓝图编辑器面板 + // Open blueprint editor panel via MessageHub + const messageHub = Core.services.resolve(MessageHub); + if (messageHub) { + messageHub.publish('dynamic-panel:open', { + panelId: 'blueprint-editor', + title: `Blueprint - ${filePath.split(/[\\/]/).pop()}` + }); + } + } + } + ]; + } + + getFileCreationTemplates(): FileCreationTemplate[] { + return [ + { + id: 'blueprint', + label: 'Blueprint', + // 扩展名不带点号,FileTree 会自动添加点号 + // Extension without dot, FileTree will add the dot automatically + extension: 'blueprint', + icon: 'Workflow', + getContent: (fileName: string) => { + const name = fileName.replace(/\.blueprint$/i, '') || 'NewBlueprint'; + return JSON.stringify({ + version: '1.0.0', + name, + nodes: [], + connections: [], + variables: [] + }, null, 2); + } + } + ]; + } +} + +const descriptor: PluginDescriptor = { + id: '@esengine/blueprint', + name: 'Blueprint', + version: '1.0.0', + description: 'Visual scripting system for ECS Framework', + category: 'scripting', + enabledByDefault: false, + isEnginePlugin: true, + canContainContent: true, + modules: [ + { name: 'Runtime', type: 'runtime', loadingPhase: 'default' }, + { name: 'Editor', type: 'editor', loadingPhase: 'postDefault' } + ] +}; + +/** + * Complete Blueprint plugin with both runtime and editor modules + * 完整的蓝图插件,包含运行时和编辑器模块 + */ +export const BlueprintPlugin: IPlugin = { + descriptor, + editorModule: new BlueprintEditorModuleImpl() +}; + +// Also export the editor module instance for direct use +export const BlueprintEditorModule = new BlueprintEditorModuleImpl(); + +// Re-export useful items +export { NodeRegistry, BlueprintVM, createBlueprintSystem }; diff --git a/packages/blueprint/src/editor/components/BlueprintCanvas.tsx b/packages/blueprint-editor/src/components/BlueprintCanvas.tsx similarity index 97% rename from packages/blueprint/src/editor/components/BlueprintCanvas.tsx rename to packages/blueprint-editor/src/components/BlueprintCanvas.tsx index 96b0e517..1b1c07e0 100644 --- a/packages/blueprint/src/editor/components/BlueprintCanvas.tsx +++ b/packages/blueprint-editor/src/components/BlueprintCanvas.tsx @@ -17,9 +17,8 @@ import { type PinCategory } from '@esengine/node-editor'; import { useBlueprintEditorStore } from '../stores/blueprintEditorStore'; -import { NodeRegistry } from '../../runtime/NodeRegistry'; -import type { BlueprintNode, BlueprintConnection, BlueprintNodeTemplate } from '../../types/nodes'; -import type { BlueprintPinDefinition } from '../../types/pins'; +import { NodeRegistry } from '@esengine/blueprint'; +import type { BlueprintNode, BlueprintConnection, BlueprintNodeTemplate, BlueprintPinDefinition } from '@esengine/blueprint'; interface ContextMenuState { isOpen: boolean; @@ -221,7 +220,9 @@ export const BlueprintCanvas: React.FC = () => { } } - return new Graph('blueprint', blueprint.metadata.name, graphNodes, graphConnections); + // 安全访问 metadata.name,兼容旧格式文件 + const blueprintName = blueprint.metadata?.name || (blueprint as any).name || 'Blueprint'; + return new Graph('blueprint', blueprintName, graphNodes, graphConnections); }, [blueprint]); // Handle graph changes diff --git a/packages/blueprint-editor/src/components/BlueprintEditorPanel.tsx b/packages/blueprint-editor/src/components/BlueprintEditorPanel.tsx new file mode 100644 index 00000000..64658cd0 --- /dev/null +++ b/packages/blueprint-editor/src/components/BlueprintEditorPanel.tsx @@ -0,0 +1,90 @@ +/** + * Blueprint Editor Panel - Main panel for blueprint editing + * 蓝图编辑器面板 - 蓝图编辑的主面板 + */ + +import React, { useEffect } from 'react'; +import { Core } from '@esengine/ecs-framework'; +import { IFileSystemService, type IFileSystem } from '@esengine/editor-core'; +import { BlueprintCanvas } from './BlueprintCanvas'; +import { useBlueprintEditorStore } from '../stores/blueprintEditorStore'; +import type { BlueprintAsset } from '@esengine/blueprint'; + +// Import blueprint package to register nodes +// 导入蓝图包以注册节点 +import '@esengine/blueprint'; + +/** + * Panel container styles + * 面板容器样式 + */ +const panelStyles: React.CSSProperties = { + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column', + backgroundColor: '#1a1a2e', + color: '#fff', + overflow: 'hidden' +}; + +/** + * Blueprint Editor Panel Component + * 蓝图编辑器面板组件 + */ +export const BlueprintEditorPanel: React.FC = () => { + const { + blueprint, + pendingFilePath, + createNewBlueprint, + loadBlueprint, + setPendingFilePath + } = useBlueprintEditorStore(); + + // Load blueprint from pending file path + // 从待加载的文件路径加载蓝图 + useEffect(() => { + if (!pendingFilePath) return; + + const loadBlueprintFile = async () => { + try { + const fileSystem = Core.services.tryResolve(IFileSystemService) as IFileSystem | null; + if (!fileSystem) { + console.error('[BlueprintEditorPanel] FileSystem service not available'); + setPendingFilePath(null); + createNewBlueprint('New Blueprint'); + return; + } + + const content = await fileSystem.readFile(pendingFilePath); + const asset = JSON.parse(content) as BlueprintAsset; + + loadBlueprint(asset, pendingFilePath); + setPendingFilePath(null); + + console.log('[BlueprintEditorPanel] Loaded blueprint from file:', pendingFilePath); + } catch (error) { + console.error('[BlueprintEditorPanel] Failed to load blueprint file:', error); + setPendingFilePath(null); + // 加载失败时创建新蓝图 + createNewBlueprint('New Blueprint'); + } + }; + + loadBlueprintFile(); + }, [pendingFilePath, loadBlueprint, setPendingFilePath, createNewBlueprint]); + + // Create a default blueprint if none exists and no pending file + // 如果不存在蓝图且没有待加载文件,则创建默认蓝图 + useEffect(() => { + if (!blueprint && !pendingFilePath) { + createNewBlueprint('New Blueprint'); + } + }, [blueprint, pendingFilePath, createNewBlueprint]); + + return ( +
+ +
+ ); +}; diff --git a/packages/blueprint/src/editor/components/index.ts b/packages/blueprint-editor/src/components/index.ts similarity index 100% rename from packages/blueprint/src/editor/components/index.ts rename to packages/blueprint-editor/src/components/index.ts diff --git a/packages/blueprint/src/editor/index.ts b/packages/blueprint-editor/src/index.ts similarity index 100% rename from packages/blueprint/src/editor/index.ts rename to packages/blueprint-editor/src/index.ts diff --git a/packages/blueprint/src/editor/stores/blueprintEditorStore.ts b/packages/blueprint-editor/src/stores/blueprintEditorStore.ts similarity index 84% rename from packages/blueprint/src/editor/stores/blueprintEditorStore.ts rename to packages/blueprint-editor/src/stores/blueprintEditorStore.ts index 66d5221b..e840075e 100644 --- a/packages/blueprint/src/editor/stores/blueprintEditorStore.ts +++ b/packages/blueprint-editor/src/stores/blueprintEditorStore.ts @@ -4,8 +4,8 @@ */ import { create } from 'zustand'; -import { BlueprintAsset, createEmptyBlueprint } from '../../types/blueprint'; -import { BlueprintNode, BlueprintConnection } from '../../types/nodes'; +import { createEmptyBlueprint } from '@esengine/blueprint'; +import type { BlueprintAsset, BlueprintNode, BlueprintConnection } from '@esengine/blueprint'; /** * Blueprint editor state interface @@ -30,6 +30,9 @@ interface BlueprintEditorState { /** Whether the blueprint has unsaved changes (是否有未保存的更改) */ isDirty: boolean; + /** Pending file path to load when panel opens (面板打开时待加载的文件路径) */ + pendingFilePath: string | null; + /** Current file path if saved (当前文件路径) */ filePath: string | null; @@ -75,6 +78,9 @@ interface BlueprintEditorState { /** Mark as clean (标记为未修改) */ markClean: () => void; + + /** Set pending file path (设置待加载的文件路径) */ + setPendingFilePath: (path: string | null) => void; } /** @@ -85,6 +91,19 @@ function generateId(): string { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } +/** + * 安全获取或创建 metadata + * Safely get or create metadata + */ +function getUpdatedMetadata(blueprint: BlueprintAsset): BlueprintAsset['metadata'] { + const existing = blueprint.metadata || { + name: (blueprint as any).name || 'Blueprint', + createdAt: Date.now(), + modifiedAt: Date.now() + }; + return { ...existing, modifiedAt: Date.now() }; +} + /** * Blueprint editor store * 蓝图编辑器状态存储 @@ -96,6 +115,7 @@ export const useBlueprintEditorStore = create((set, get) = panOffset: { x: 0, y: 0 }, zoom: 1, isDirty: false, + pendingFilePath: null, filePath: null, createNewBlueprint: (name: string) => { @@ -130,7 +150,7 @@ export const useBlueprintEditorStore = create((set, get) = blueprint: { ...blueprint, nodes: [...blueprint.nodes, newNode], - metadata: { ...blueprint.metadata, modifiedAt: Date.now() } + metadata: getUpdatedMetadata(blueprint) }, isDirty: true }); @@ -147,7 +167,7 @@ export const useBlueprintEditorStore = create((set, get) = connections: blueprint.connections.filter( c => c.fromNodeId !== nodeId && c.toNodeId !== nodeId ), - metadata: { ...blueprint.metadata, modifiedAt: Date.now() } + metadata: getUpdatedMetadata(blueprint) }, selectedNodeIds: get().selectedNodeIds.filter(id => id !== nodeId), isDirty: true @@ -164,7 +184,7 @@ export const useBlueprintEditorStore = create((set, get) = nodes: blueprint.nodes.map(n => n.id === nodeId ? { ...n, position: { x, y } } : n ), - metadata: { ...blueprint.metadata, modifiedAt: Date.now() } + metadata: getUpdatedMetadata(blueprint) }, isDirty: true }); @@ -180,7 +200,7 @@ export const useBlueprintEditorStore = create((set, get) = nodes: blueprint.nodes.map(n => n.id === nodeId ? { ...n, data: { ...n.data, ...data } } : n ), - metadata: { ...blueprint.metadata, modifiedAt: Date.now() } + metadata: getUpdatedMetadata(blueprint) }, isDirty: true }); @@ -210,7 +230,7 @@ export const useBlueprintEditorStore = create((set, get) = blueprint: { ...blueprint, connections: newConnections, - metadata: { ...blueprint.metadata, modifiedAt: Date.now() } + metadata: getUpdatedMetadata(blueprint) }, isDirty: true }); @@ -224,7 +244,7 @@ export const useBlueprintEditorStore = create((set, get) = blueprint: { ...blueprint, connections: blueprint.connections.filter(c => c.id !== connectionId), - metadata: { ...blueprint.metadata, modifiedAt: Date.now() } + metadata: getUpdatedMetadata(blueprint) }, isDirty: true }); @@ -252,5 +272,9 @@ export const useBlueprintEditorStore = create((set, get) = markClean: () => { set({ isDirty: false }); + }, + + setPendingFilePath: (path: string | null) => { + set({ pendingFilePath: path }); } })); diff --git a/packages/blueprint/src/editor/stores/index.ts b/packages/blueprint-editor/src/stores/index.ts similarity index 100% rename from packages/blueprint/src/editor/stores/index.ts rename to packages/blueprint-editor/src/stores/index.ts diff --git a/packages/blueprint-editor/tsconfig.build.json b/packages/blueprint-editor/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/blueprint-editor/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/blueprint-editor/tsconfig.json b/packages/blueprint-editor/tsconfig.json new file mode 100644 index 00000000..d099ddd8 --- /dev/null +++ b/packages/blueprint-editor/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/blueprint-editor/tsup.config.ts b/packages/blueprint-editor/tsup.config.ts new file mode 100644 index 00000000..b4f49f5d --- /dev/null +++ b/packages/blueprint-editor/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { editorOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...editorOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/blueprint/package.json b/packages/blueprint/package.json index 9ff7b437..748b7d01 100644 --- a/packages/blueprint/package.json +++ b/packages/blueprint/package.json @@ -1,85 +1,52 @@ { - "name": "@esengine/blueprint", - "version": "1.0.0", - "description": "Visual scripting system for ECS Framework", - "main": "dist/index.js", - "module": "dist/index.js", - "types": "dist/index.d.ts", - "type": "module", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "name": "@esengine/blueprint", + "version": "1.0.0", + "description": "Visual scripting system for ECS Framework", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } }, - "./editor": { - "types": "./dist/editor/index.d.ts", - "import": "./dist/editor/index.js" + "files": [ + "dist" + ], + "scripts": { + "clean": "rimraf dist tsconfig.tsbuildinfo", + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit" }, - "./plugin.json": "./plugin.json" - }, - "files": [ - "dist", - "plugin.json" - ], - "scripts": { - "clean": "rimraf dist tsconfig.tsbuildinfo", - "build": "vite build", - "build:watch": "vite build --watch", - "type-check": "tsc --noEmit" - }, - "keywords": [ - "ecs", - "blueprint", - "visual-scripting", - "game-engine", - "node-editor" - ], - "author": "yhh", - "license": "MIT", - "peerDependencies": { - "@esengine/ecs-framework": ">=2.0.0", - "@esengine/editor-runtime": "workspace:*", - "@esengine/node-editor": "workspace:*", - "lucide-react": "^0.545.0", - "react": "^18.3.1", - "zustand": "^4.5.2" - }, - "peerDependenciesMeta": { - "@esengine/editor-runtime": { - "optional": true + "keywords": [ + "ecs", + "blueprint", + "visual-scripting", + "game-engine" + ], + "author": "yhh", + "license": "MIT", + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/build-config": "workspace:*", + "@types/node": "^20.19.17", + "rimraf": "^5.0.0", + "tsup": "^8.0.0", + "typescript": "^5.8.3" }, - "@esengine/node-editor": { - "optional": true + "dependencies": { + "tslib": "^2.8.1" }, - "lucide-react": { - "optional": true + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" }, - "react": { - "optional": true - }, - "zustand": { - "optional": true + "repository": { + "type": "git", + "url": "https://github.com/esengine/ecs-framework.git", + "directory": "packages/blueprint" } - }, - "devDependencies": { - "@types/node": "^20.19.17", - "@types/react": "^18.3.12", - "@vitejs/plugin-react": "^4.7.0", - "rimraf": "^5.0.0", - "typescript": "^5.8.3", - "vite": "^6.0.7", - "vite-plugin-dts": "^3.7.0" - }, - "dependencies": { - "tslib": "^2.8.1" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, - "repository": { - "type": "git", - "url": "https://github.com/esengine/ecs-framework.git", - "directory": "packages/blueprint" - } } diff --git a/packages/blueprint/src/editor/BlueprintPlugin.ts b/packages/blueprint/src/editor/BlueprintPlugin.ts deleted file mode 100644 index 8add4a77..00000000 --- a/packages/blueprint/src/editor/BlueprintPlugin.ts +++ /dev/null @@ -1,180 +0,0 @@ -/** - * Blueprint Editor Plugin - Integrates blueprint editor with the editor - * 蓝图编辑器插件 - 将蓝图编辑器与编辑器集成 - */ - -import { - type ServiceContainer, - type IPluginLoader, - type IEditorModuleLoader, - type PluginDescriptor, - type PanelDescriptor, - type MenuItemDescriptor, - type FileActionHandler, - type FileCreationTemplate, - PanelPosition, - FileSystem, - createLogger, - MessageHub, - IMessageHub -} from '@esengine/editor-runtime'; -import { BlueprintEditorPanel } from './components/BlueprintEditorPanel'; -import { useBlueprintEditorStore } from './stores/blueprintEditorStore'; -import { createEmptyBlueprint, validateBlueprintAsset } from '../types/blueprint'; - -const logger = createLogger('BlueprintEditorModule'); - -/** - * Blueprint 编辑器模块 - * Blueprint editor module - */ -class BlueprintEditorModule implements IEditorModuleLoader { - private services?: ServiceContainer; - - async install(services: ServiceContainer): Promise { - this.services = services; - logger.info('Blueprint editor module installed'); - } - - async uninstall(): Promise { - logger.info('Blueprint editor module uninstalled'); - } - - getPanels(): PanelDescriptor[] { - return [ - { - id: 'panel-blueprint-editor', - title: 'Blueprint Editor', - position: PanelPosition.Center, - defaultSize: 800, - resizable: true, - closable: true, - icon: 'Workflow', - order: 20, - isDynamic: true, - component: BlueprintEditorPanel - } - ]; - } - - getMenuItems(): MenuItemDescriptor[] { - return [ - { - id: 'blueprint-new', - label: 'New Blueprint', - parentId: 'file', - shortcut: 'Ctrl+Shift+B', - execute: () => { - useBlueprintEditorStore.getState().createNewBlueprint('New Blueprint'); - } - }, - { - id: 'view-blueprint-editor', - label: 'Blueprint Editor', - parentId: 'view', - shortcut: 'Ctrl+B' - } - ]; - } - - getFileActionHandlers(): FileActionHandler[] { - const services = this.services; - return [ - { - extensions: ['bp'], - onDoubleClick: async (filePath: string) => { - try { - // 使用 FileSystem API 读取文件 - const content = await FileSystem.readTextFile(filePath); - const data = JSON.parse(content); - - if (validateBlueprintAsset(data)) { - useBlueprintEditorStore.getState().loadBlueprint(data, filePath); - logger.info('Loaded blueprint:', filePath); - - // 打开蓝图编辑器面板 - if (services) { - const messageHub = services.resolve(IMessageHub); - if (messageHub) { - const fileName = filePath.split(/[\\/]/).pop() || 'Blueprint'; - messageHub.publish('dynamic-panel:open', { - panelId: 'panel-blueprint-editor', - title: `Blueprint - ${fileName}` - }); - } - } - } else { - logger.error('Invalid blueprint file:', filePath); - } - } catch (error) { - logger.error('Failed to load blueprint:', error); - } - } - } - ]; - } - - getFileCreationTemplates(): FileCreationTemplate[] { - return [ - { - id: 'create-blueprint', - label: 'Blueprint', - extension: 'bp', - icon: 'Workflow', - category: 'scripting', - getContent: (fileName: string) => { - const name = fileName.replace('.bp', ''); - const blueprint = createEmptyBlueprint(name); - return JSON.stringify(blueprint, null, 2); - } - } - ]; - } - - async onEditorReady(): Promise { - logger.info('Editor ready'); - } - - async onProjectOpen(_projectPath: string): Promise { - logger.info('Project opened'); - } - - async onProjectClose(): Promise { - useBlueprintEditorStore.getState().createNewBlueprint('New Blueprint'); - logger.info('Project closed'); - } -} - -/** - * Plugin descriptor - * 插件描述符 - */ -const descriptor: PluginDescriptor = { - id: '@esengine/blueprint', - name: 'Blueprint Visual Scripting', - version: '1.0.0', - description: 'Visual scripting system for creating game logic without code', - category: 'scripting', - icon: 'Workflow', - enabledByDefault: true, - canContainContent: true, - isEnginePlugin: true, - isCore: false, - modules: [ - { - name: 'BlueprintEditor', - type: 'editor', - loadingPhase: 'default', - panels: ['panel-blueprint-editor'] - } - ] -}; - -/** - * Blueprint Plugin Export - * 蓝图插件导出 - */ -export const BlueprintPlugin: IPluginLoader = { - descriptor, - editorModule: new BlueprintEditorModule() -}; diff --git a/packages/blueprint/src/editor/components/BlueprintEditorPanel.tsx b/packages/blueprint/src/editor/components/BlueprintEditorPanel.tsx deleted file mode 100644 index 18ef7c0c..00000000 --- a/packages/blueprint/src/editor/components/BlueprintEditorPanel.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Blueprint Editor Panel - Main panel for blueprint editing - * 蓝图编辑器面板 - 蓝图编辑的主面板 - */ - -import React, { useEffect } from 'react'; -import { BlueprintCanvas } from './BlueprintCanvas'; -import { useBlueprintEditorStore } from '../stores/blueprintEditorStore'; - -// Import nodes to register them -// 导入节点以注册它们 -import '../../nodes'; - -/** - * Panel container styles - * 面板容器样式 - */ -const panelStyles: React.CSSProperties = { - width: '100%', - height: '100%', - display: 'flex', - flexDirection: 'column', - backgroundColor: '#1a1a2e', - color: '#fff', - overflow: 'hidden' -}; - -/** - * Blueprint Editor Panel Component - * 蓝图编辑器面板组件 - */ -export const BlueprintEditorPanel: React.FC = () => { - const { blueprint, createNewBlueprint } = useBlueprintEditorStore(); - - // Create a default blueprint if none exists - // 如果不存在则创建默认蓝图 - useEffect(() => { - if (!blueprint) { - createNewBlueprint('New Blueprint'); - } - }, [blueprint, createNewBlueprint]); - - return ( -
- -
- ); -}; diff --git a/packages/blueprint/tsconfig.build.json b/packages/blueprint/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/blueprint/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/blueprint/tsup.config.ts b/packages/blueprint/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/blueprint/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/blueprint/vite.config.ts b/packages/blueprint/vite.config.ts deleted file mode 100644 index 5fb95845..00000000 --- a/packages/blueprint/vite.config.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; -import dts from 'vite-plugin-dts'; -import react from '@vitejs/plugin-react'; - -/** - * 自定义插件:将 CSS 转换为自执行的样式注入代码 - * Custom plugin: Convert CSS to self-executing style injection code - */ -function escapeUnsafeChars(str: string): string { - const charMap: Record = { - '<': '\\u003C', - '>': '\\u003E', - '/': '\\u002F', - '\\': '\\\\', - '\u2028': '\\u2028', - '\u2029': '\\u2029' - }; - return str.replace(/[<>\\/\u2028\u2029]/g, (x) => charMap[x] || x); -} - -function injectCSSPlugin(): unknown { - const cssIdMap = new Map(); - let cssCounter = 0; - - return { - name: 'inject-css-plugin', - enforce: 'post' as const, - generateBundle(_options: unknown, bundle: Record) { - const bundleKeys = Object.keys(bundle); - - // 找到所有 CSS 文件 - const cssFiles = bundleKeys.filter(key => key.endsWith('.css')); - - for (const cssFile of cssFiles) { - const cssChunk = bundle[cssFile]; - if (!cssChunk || !cssChunk.source) continue; - - const cssContent = cssChunk.source; - const styleId = `esengine-blueprint-style-${cssCounter++}`; - cssIdMap.set(cssFile, styleId); - - // 生成样式注入代码 - const injectCode = `(function(){if(typeof document!=='undefined'){var s=document.createElement('style');s.id='${styleId}';if(!document.getElementById(s.id)){s.textContent=${escapeUnsafeChars(JSON.stringify(cssContent))};document.head.appendChild(s);}}})();`; - - // 注入到 editor/index.js 或共享 chunk - for (const jsKey of bundleKeys) { - if (!jsKey.endsWith('.js')) continue; - const jsChunk = bundle[jsKey]; - if (!jsChunk || jsChunk.type !== 'chunk' || !jsChunk.code) continue; - - if (jsKey === 'editor/index.js' || jsKey.match(/^index-[^/]+\.js$/)) { - jsChunk.code = injectCode + '\n' + jsChunk.code; - } - } - - // 删除独立的 CSS 文件 - delete bundle[cssFile]; - } - } - }; -} - -export default defineConfig({ - plugins: [ - react(), - dts({ - include: ['src'], - outDir: 'dist', - rollupTypes: false - }), - injectCSSPlugin() - ], - esbuild: { - jsx: 'automatic', - }, - build: { - lib: { - entry: { - index: resolve(__dirname, 'src/index.ts'), - 'editor/index': resolve(__dirname, 'src/editor/index.ts') - }, - formats: ['es'], - fileName: (_format, entryName) => `${entryName}.js` - }, - rollupOptions: { - external: [ - '@esengine/ecs-framework', - '@esengine/editor-runtime', - 'react', - 'react/jsx-runtime', - 'lucide-react', - 'zustand', - /^@esengine\//, - /^@tauri-apps\// - ], - output: { - exports: 'named', - preserveModules: false - } - }, - target: 'es2020', - minify: false, - sourcemap: true - } -}); diff --git a/packages/build-config/README.md b/packages/build-config/README.md new file mode 100644 index 00000000..c71438e9 --- /dev/null +++ b/packages/build-config/README.md @@ -0,0 +1,215 @@ +# @esengine/build-config + +ES Engine 统一构建配置包,提供标准化的 Vite 配置预设和共享插件。 + +## 快速开始 + +### 创建新包 + +使用脚手架工具快速创建新包: + +```bash +# 交互式创建 +node scripts/create-package.mjs + +# 或指定参数 +node scripts/create-package.mjs my-plugin --type plugin +``` + +### 包类型 + +| 类型 | 说明 | 示例 | +|------|------|------| +| `runtime-only` | 纯运行时库,不含编辑器代码 | core, math, components | +| `plugin` | 插件包,同时有 runtime 和 editor 入口 | ui, tilemap, behavior-tree | +| `editor-only` | 纯编辑器包,仅用于编辑器 | editor-core, node-editor | + +## 使用预设 + +### 1. runtime-only(纯运行时包) + +```typescript +// vite.config.ts +import { runtimeOnlyPreset } from '@esengine/build-config/presets'; + +export default runtimeOnlyPreset({ + root: __dirname +}); +``` + +目录结构: +``` +packages/my-lib/ +├── src/ +│ └── index.ts # 主入口 +├── vite.config.ts +└── package.json +``` + +### 2. plugin(插件包) + +```typescript +// vite.config.ts +import { pluginPreset } from '@esengine/build-config/presets'; + +export default pluginPreset({ + root: __dirname, + hasCSS: true // 如果有 CSS 文件 +}); +``` + +目录结构: +``` +packages/my-plugin/ +├── src/ +│ ├── index.ts # 主入口(导出全部) +│ ├── runtime.ts # 运行时入口(不含 React!) +│ ├── MyRuntimeModule.ts +│ └── editor/ +│ ├── index.ts # 编辑器模块 +│ └── MyPlugin.ts +├── plugin.json # 插件描述文件 +├── vite.config.ts +└── package.json +``` + +生成的 exports: +```json +{ + ".": "./dist/index.js", + "./runtime": "./dist/runtime.js", + "./editor": "./dist/editor/index.js", + "./plugin.json": "./plugin.json" +} +``` + +### 3. editor-only(纯编辑器包) + +```typescript +// vite.config.ts +import { editorOnlyPreset } from '@esengine/build-config/presets'; + +export default editorOnlyPreset({ + root: __dirname, + hasReact: true, + hasCSS: true +}); +``` + +## 共享插件 + +### CSS 注入插件 + +将 CSS 内联到 JS 中,避免单独的 CSS 文件: + +```typescript +import { cssInjectPlugin } from '@esengine/build-config/plugins'; + +export default defineConfig({ + plugins: [cssInjectPlugin()] +}); +``` + +### 阻止编辑器代码泄漏 + +在运行时构建中检测并阻止编辑器代码被打包: + +```typescript +import { blockEditorPlugin } from '@esengine/build-config/plugins'; + +export default defineConfig({ + plugins: [ + blockEditorPlugin({ bIsRuntimeBuild: true }) + ] +}); +``` + +## Runtime vs Editor 分离规则 + +### ✅ runtime.ts 中可以: +- 导入 @esengine/ecs-framework +- 导入 @esengine/ecs-components +- 导入其他包的 `/runtime` 路径 + +### ❌ runtime.ts 中不能: +- 导入 `react`、`react-dom` +- 导入 `@esengine/editor-core` +- 导入 `lucide-react` 等 UI 库 +- 导入任何包的 `/editor` 路径 + +### 示例 + +```typescript +// ✅ 正确 +import { Core } from '@esengine/ecs-framework'; +import { UIRuntimeModule } from '@esengine/ui/runtime'; + +// ❌ 错误 - 会把编辑器代码打包进来 +import { UIPlugin } from '@esengine/ui'; // 主入口包含编辑器 +import { UIPlugin } from '@esengine/ui/editor'; // 直接导入编辑器 +import React from 'react'; // React 不应在运行时 +``` + +## 迁移现有包 + +### 从 Rollup 迁移到 Vite 预设 + +1. 安装依赖: +```bash +pnpm add -D @esengine/build-config vite vite-plugin-dts +``` + +2. 替换 `rollup.config.js` 为 `vite.config.ts`: +```typescript +import { pluginPreset } from '@esengine/build-config/presets'; + +export default pluginPreset({ + root: __dirname, + hasCSS: true +}); +``` + +3. 更新 `package.json` 的 scripts: +```json +{ + "scripts": { + "build": "vite build", + "build:watch": "vite build --watch" + } +} +``` + +4. 删除旧的 rollup 配置和依赖。 + +## API 参考 + +### runtimeOnlyPreset(options) + +| 选项 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| root | string | - | 包根目录(必填) | +| entry | string | 'src/index.ts' | 入口文件 | +| external | (string\|RegExp)[] | [] | 额外的外部依赖 | +| viteConfig | Partial | {} | 额外的 Vite 配置 | + +### pluginPreset(options) + +| 选项 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| root | string | - | 包根目录(必填) | +| entries.main | string | 'src/index.ts' | 主入口 | +| entries.runtime | string | 'src/runtime.ts' | 运行时入口 | +| entries.editor | string | 'src/editor/index.ts' | 编辑器入口 | +| hasCSS | boolean | false | 是否包含 CSS | +| hasPluginJson | boolean | true | 是否导出 plugin.json | +| external | (string\|RegExp)[] | [] | 额外的外部依赖 | + +### editorOnlyPreset(options) + +| 选项 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| root | string | - | 包根目录(必填) | +| entry | string | 'src/index.ts' | 入口文件 | +| hasReact | boolean | true | 是否包含 React | +| hasCSS | boolean | false | 是否包含 CSS | +| external | (string\|RegExp)[] | [] | 额外的外部依赖 | diff --git a/packages/build-config/package.json b/packages/build-config/package.json new file mode 100644 index 00000000..831a2a36 --- /dev/null +++ b/packages/build-config/package.json @@ -0,0 +1,39 @@ +{ + "name": "@esengine/build-config", + "version": "1.0.0", + "description": "Shared build configuration for ES Engine packages", + "type": "module", + "main": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./presets": "./src/presets/index.ts", + "./presets/tsup": "./src/presets/plugin-tsup.ts", + "./plugins": "./src/plugins/index.ts" + }, + "scripts": { + "type-check": "tsc --noEmit" + }, + "keywords": [ + "build", + "tsup", + "config" + ], + "author": "yhh", + "license": "MIT", + "devDependencies": { + "@vitejs/plugin-react": "^4.3.4", + "tsup": "^8.0.0", + "typescript": "^5.8.3", + "vite": "^6.3.5", + "vite-plugin-dts": "^4.5.4" + }, + "peerDependencies": { + "tsup": "^8.0.0", + "vite": "^6.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } +} diff --git a/packages/build-config/src/index.ts b/packages/build-config/src/index.ts new file mode 100644 index 00000000..1b9f9512 --- /dev/null +++ b/packages/build-config/src/index.ts @@ -0,0 +1,90 @@ +/** + * @esengine/build-config + * + * 统一构建配置包,提供标准化的 Vite 配置预设和共享插件 + * Unified build configuration with standardized Vite presets and shared plugins + * + * @example + * ```typescript + * // 1. 纯运行时包 (core, math, components) + * import { runtimeOnlyPreset } from '@esengine/build-config/presets'; + * export default runtimeOnlyPreset({ root: __dirname }); + * + * // 2. 插件包 (ui, tilemap, behavior-tree) + * import { pluginPreset } from '@esengine/build-config/presets'; + * export default pluginPreset({ + * root: __dirname, + * hasCSS: true + * }); + * + * // 3. 纯编辑器包 (editor-core, node-editor) + * import { editorOnlyPreset } from '@esengine/build-config/presets'; + * export default editorOnlyPreset({ + * root: __dirname, + * hasReact: true + * }); + * ``` + * + * ## 包类型说明 + * + * | 类型 | 说明 | 示例 | + * |------|------|------| + * | RuntimeOnly | 纯运行时库,不含编辑器代码 | core, math, components | + * | Plugin | 插件包,同时有 runtime 和 editor 入口 | ui, tilemap, behavior-tree | + * | EditorOnly | 纯编辑器包,仅用于编辑器 | editor-core, node-editor | + * + * ## 目录结构约定 + * + * ### RuntimeOnly 包 + * ``` + * packages/my-lib/ + * ├── src/ + * │ └── index.ts # 主入口 + * ├── vite.config.ts + * └── package.json + * ``` + * + * ### Plugin 包 + * ``` + * packages/my-plugin/ + * ├── src/ + * │ ├── index.ts # 主入口(编辑器环境) + * │ ├── runtime.ts # 运行时入口(不含 React) + * │ └── editor/ + * │ └── index.ts # 编辑器模块 + * ├── plugin.json # 插件描述文件 + * ├── vite.config.ts + * └── package.json + * ``` + * + * ### EditorOnly 包 + * ``` + * packages/my-editor-tool/ + * ├── src/ + * │ └── index.ts # 主入口 + * ├── vite.config.ts + * └── package.json + * ``` + */ + +// Types +export { EPackageType, STANDARD_EXTERNALS, EDITOR_ONLY_EXTERNALS } from './types'; +export type { PackageBuildConfig } from './types'; + +// Presets +export { + runtimeOnlyPreset, + pluginPreset, + standaloneRuntimeConfig, + editorOnlyPreset +} from './presets'; +export type { + RuntimeOnlyOptions, + PluginPackageOptions, + StandaloneRuntimeOptions, + EditorOnlyOptions +} from './presets'; + +// Plugins +export { cssInjectPlugin, blockEditorPlugin } from './plugins'; +export type { BlockEditorOptions } from './plugins'; diff --git a/packages/build-config/src/plugins/block-editor.ts b/packages/build-config/src/plugins/block-editor.ts new file mode 100644 index 00000000..8fac291f --- /dev/null +++ b/packages/build-config/src/plugins/block-editor.ts @@ -0,0 +1,100 @@ +/** + * Block Editor Plugin + * 阻止编辑器代码泄漏插件 + * + * 在运行时构建中检测并阻止编辑器代码被打包 + * Detects and blocks editor code from being bundled in runtime builds + */ + +import type { Plugin } from 'vite'; + +export interface BlockEditorOptions { + /** 是否为运行时构建 */ + bIsRuntimeBuild: boolean; + /** 要阻止的模块模式 */ + blockedPatterns?: (string | RegExp)[]; + /** 是否只警告而不报错 */ + bWarnOnly?: boolean; +} + +const DEFAULT_BLOCKED_PATTERNS: (string | RegExp)[] = [ + // React 相关 + /^react$/, + /^react-dom$/, + /^react\/jsx-runtime$/, + /^lucide-react$/, + + // 编辑器包 + /@esengine\/editor-core/, + /@esengine\/node-editor/, + + // 编辑器子路径 + /\/editor$/, + /\/editor\//, +]; + +/** + * 创建阻止编辑器代码泄漏的插件 + * + * @example + * ```typescript + * import { blockEditorPlugin } from '@esengine/build-config/plugins'; + * + * // 在运行时构建中使用 + * export default defineConfig({ + * plugins: [ + * blockEditorPlugin({ bIsRuntimeBuild: true }) + * ] + * }); + * ``` + */ +export function blockEditorPlugin(options: BlockEditorOptions): Plugin { + const { + bIsRuntimeBuild, + blockedPatterns = DEFAULT_BLOCKED_PATTERNS, + bWarnOnly = false + } = options; + + if (!bIsRuntimeBuild) { + // 非运行时构建不需要此插件 + return { name: 'esengine:block-editor-noop' }; + } + + const isBlocked = (source: string): boolean => { + return blockedPatterns.some(pattern => { + if (typeof pattern === 'string') { + return source === pattern || source.startsWith(pattern + '/'); + } + return pattern.test(source); + }); + }; + + return { + name: 'esengine:block-editor', + enforce: 'pre', + + resolveId(source: string, importer: string | undefined) { + if (isBlocked(source)) { + const message = `[block-editor] Editor dependency detected in runtime build:\n` + + ` Source: ${source}\n` + + ` Importer: ${importer || 'entry'}\n` + + `\n` + + ` This usually means:\n` + + ` 1. A runtime module is importing from a non-/runtime path\n` + + ` 2. An editor-only dependency leaked into the dependency chain\n` + + `\n` + + ` Fix: Change the import to use /runtime subpath, e.g.:\n` + + ` import { X } from '@esengine/ui/runtime' // ✓\n` + + ` import { X } from '@esengine/ui' // ✗`; + + if (bWarnOnly) { + console.warn('\x1b[33m' + message + '\x1b[0m'); + return { id: source, external: true }; + } else { + throw new Error(message); + } + } + return null; + } + }; +} diff --git a/packages/build-config/src/plugins/css-inject.ts b/packages/build-config/src/plugins/css-inject.ts new file mode 100644 index 00000000..38b4dde3 --- /dev/null +++ b/packages/build-config/src/plugins/css-inject.ts @@ -0,0 +1,71 @@ +/** + * CSS Inject Plugin + * CSS 注入插件 + * + * 将 CSS 内联到 JS 中,避免单独的 CSS 文件 + * Inlines CSS into JS to avoid separate CSS files + */ + +import type { Plugin } from 'vite'; +import type { OutputBundle, NormalizedOutputOptions, OutputAsset, OutputChunk } from 'rollup'; + +/** + * 创建 CSS 注入插件 + * + * @example + * ```typescript + * import { cssInjectPlugin } from '@esengine/build-config/plugins'; + * + * export default defineConfig({ + * plugins: [cssInjectPlugin()] + * }); + * ``` + */ +export function cssInjectPlugin(): Plugin { + return { + name: 'esengine:css-inject', + apply: 'build', + + generateBundle(_options: NormalizedOutputOptions, bundle: OutputBundle) { + // 收集所有 CSS 内容 + const cssChunks: string[] = []; + const cssFileNames: string[] = []; + + for (const [fileName, chunk] of Object.entries(bundle)) { + if (fileName.endsWith('.css') && chunk.type === 'asset') { + cssChunks.push(chunk.source as string); + cssFileNames.push(fileName); + } + } + + if (cssChunks.length === 0) return; + + // 合并所有 CSS + const combinedCSS = cssChunks.join('\n'); + + // 创建注入代码 + const injectCode = ` +(function() { + if (typeof document === 'undefined') return; + var style = document.createElement('style'); + style.setAttribute('data-esengine', 'true'); + style.textContent = ${JSON.stringify(combinedCSS)}; + document.head.appendChild(style); +})(); +`; + + // 找到主入口 JS 文件并注入 + for (const chunk of Object.values(bundle)) { + if (chunk.type === 'chunk' && chunk.isEntry) { + chunk.code = injectCode + chunk.code; + break; + } + } + + // 删除独立的 CSS 文件 + for (const fileName of cssFileNames) { + delete bundle[fileName]; + } + } + }; +} diff --git a/packages/build-config/src/plugins/index.ts b/packages/build-config/src/plugins/index.ts new file mode 100644 index 00000000..59f9a4da --- /dev/null +++ b/packages/build-config/src/plugins/index.ts @@ -0,0 +1,7 @@ +/** + * Shared Vite Plugins + * 共享 Vite 插件 + */ + +export { cssInjectPlugin } from './css-inject'; +export { blockEditorPlugin, type BlockEditorOptions } from './block-editor'; diff --git a/packages/build-config/src/presets/editor-only.ts b/packages/build-config/src/presets/editor-only.ts new file mode 100644 index 00000000..aa3c882f --- /dev/null +++ b/packages/build-config/src/presets/editor-only.ts @@ -0,0 +1,109 @@ +/** + * Editor-Only Package Preset + * 纯编辑器包预设 + * + * 用于仅在编辑器环境使用的包 + * For packages only used in the editor environment + * + * Examples: editor-core, node-editor + */ + +import { resolve } from 'path'; +import { defineConfig, type UserConfig } from 'vite'; +import dts from 'vite-plugin-dts'; +import react from '@vitejs/plugin-react'; +import { STANDARD_EXTERNALS } from '../types'; +import { cssInjectPlugin } from '../plugins/css-inject'; + +export interface EditorOnlyOptions { + /** 包根目录 (通常是 __dirname) */ + root: string; + + /** 入口文件 (默认: src/index.ts) */ + entry?: string; + + /** 是否包含 React 组件 (默认: true) */ + hasReact?: boolean; + + /** 是否包含 CSS (默认: false) */ + hasCSS?: boolean; + + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; + + /** 额外的 Vite 配置 */ + viteConfig?: Partial; +} + +/** + * 创建纯编辑器包的 Vite 配置 + * + * @example + * ```typescript + * // vite.config.ts + * import { editorOnlyPreset } from '@esengine/build-config/presets'; + * + * export default editorOnlyPreset({ + * root: __dirname, + * hasReact: true, + * hasCSS: true + * }); + * ``` + */ +export function editorOnlyPreset(options: EditorOnlyOptions): UserConfig { + const { + root, + entry = 'src/index.ts', + hasReact = true, + hasCSS = false, + external = [], + viteConfig = {} + } = options; + + const plugins: any[] = []; + + // React 支持 + if (hasReact) { + plugins.push(react()); + } + + // DTS 生成 + plugins.push( + dts({ + include: ['src'], + outDir: 'dist', + rollupTypes: false + }) + ); + + // CSS 注入 + if (hasCSS) { + plugins.push(cssInjectPlugin()); + } + + return defineConfig({ + plugins, + esbuild: hasReact ? { jsx: 'automatic' } : undefined, + build: { + lib: { + entry: resolve(root, entry), + formats: ['es'], + fileName: () => 'index.js' + }, + rollupOptions: { + external: [ + ...STANDARD_EXTERNALS, + ...external + ], + output: { + exports: 'named', + preserveModules: false + } + }, + target: 'es2020', + minify: false, + sourcemap: true + }, + ...viteConfig + }); +} diff --git a/packages/build-config/src/presets/index.ts b/packages/build-config/src/presets/index.ts new file mode 100644 index 00000000..f2c26d4a --- /dev/null +++ b/packages/build-config/src/presets/index.ts @@ -0,0 +1,10 @@ +/** + * Build Presets + * 构建预设 + * + * 提供不同类型包的标准化 Vite 配置 + */ + +export { runtimeOnlyPreset, type RuntimeOnlyOptions } from './runtime-only'; +export { pluginPreset, standaloneRuntimeConfig, type PluginPackageOptions, type StandaloneRuntimeOptions } from './plugin'; +export { editorOnlyPreset, type EditorOnlyOptions } from './editor-only'; diff --git a/packages/build-config/src/presets/plugin-tsup.ts b/packages/build-config/src/presets/plugin-tsup.ts new file mode 100644 index 00000000..110e8eeb --- /dev/null +++ b/packages/build-config/src/presets/plugin-tsup.ts @@ -0,0 +1,157 @@ +/** + * Plugin Package Preset (tsup) + * 插件包预设 - 基于 tsup/esbuild + * + * 用于同时包含运行时和编辑器模块的插件包 + * For plugin packages with both runtime and editor modules + * + * 生成三个入口点: + * - index.js - 完整导出(编辑器环境) + * - runtime.js - 纯运行时(游戏运行时环境,不含 React) + * - editor/index.js - 编辑器模块 + * + * Examples: ui, tilemap, behavior-tree, physics-rapier2d + */ + +import type { Options } from 'tsup'; +import { STANDARD_EXTERNALS } from '../types'; + +export interface PluginPackageOptions { + /** 入口点配置 */ + entries?: { + /** 主入口 (默认: src/index.ts) */ + main?: string; + /** 运行时入口 (默认: src/runtime.ts) */ + runtime?: string; + /** 编辑器入口 (默认: src/editor/index.ts) */ + editor?: string; + }; + + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; + + /** 额外的 tsup 配置 */ + tsupConfig?: Partial; +} + +/** + * 创建插件包的 tsup 配置 + * + * @example + * ```typescript + * // tsup.config.ts + * import { defineConfig } from 'tsup'; + * import { pluginPreset } from '@esengine/build-config/presets'; + * + * export default defineConfig(pluginPreset()); + * ``` + */ +export function pluginPreset(options: PluginPackageOptions = {}): Options { + const { + entries = {}, + external = [], + tsupConfig = {} + } = options; + + const mainEntry = entries.main ?? 'src/index.ts'; + const runtimeEntry = entries.runtime ?? 'src/runtime.ts'; + const editorEntry = entries.editor ?? 'src/editor/index.ts'; + + // 合并外部依赖 + const allExternal = [ + ...STANDARD_EXTERNALS, + ...external + ]; + + return { + entry: { + index: mainEntry, + runtime: runtimeEntry, + 'editor/index': editorEntry + }, + format: ['esm'], + dts: true, + splitting: false, // 禁用代码分割 + sourcemap: true, + clean: true, + external: allExternal, + esbuildOptions(options) { + options.jsx = 'automatic'; + }, + ...tsupConfig + }; +} + +/** + * 创建纯运行时包的 tsup 配置 + */ +export interface RuntimeOnlyOptions { + /** 入口文件 (默认: src/index.ts) */ + entry?: string; + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; + /** 额外的 tsup 配置 */ + tsupConfig?: Partial; +} + +export function runtimeOnlyPreset(options: RuntimeOnlyOptions = {}): Options { + const { + entry = 'src/index.ts', + external = [], + tsupConfig = {} + } = options; + + return { + entry: [entry], + format: ['esm'], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + external: [ + ...STANDARD_EXTERNALS, + ...external + ], + ...tsupConfig + }; +} + +/** + * 创建纯编辑器包的 tsup 配置 + */ +export interface EditorOnlyOptions { + /** 入口文件 (默认: src/index.ts) */ + entry?: string; + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; + /** 额外的 tsup 配置 */ + tsupConfig?: Partial; +} + +export function editorOnlyPreset(options: EditorOnlyOptions = {}): Options { + const { + entry = 'src/index.ts', + external = [], + tsupConfig = {} + } = options; + + return { + entry: [entry], + format: ['esm'], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + // 将 CSS 内联到 JS 中,运行时自动注入到 DOM + // Inline CSS into JS, auto-inject to DOM at runtime + injectStyle: true, + external: [ + ...STANDARD_EXTERNALS, + ...external + ], + esbuildOptions(options) { + options.jsx = 'automatic'; + }, + ...tsupConfig + }; +} diff --git a/packages/build-config/src/presets/plugin.ts b/packages/build-config/src/presets/plugin.ts new file mode 100644 index 00000000..699260a0 --- /dev/null +++ b/packages/build-config/src/presets/plugin.ts @@ -0,0 +1,176 @@ +/** + * Plugin Package Preset + * 插件包预设 + * + * 用于同时包含运行时和编辑器模块的插件包 + * For plugin packages with both runtime and editor modules + * + * 生成三个入口点: + * - index.js - 完整导出(编辑器环境) + * - runtime.js - 纯运行时(游戏运行时环境,不含 React) + * - editor/index.js - 编辑器模块 + * + * Examples: ui, tilemap, behavior-tree, physics-rapier2d + */ + +import { resolve } from 'path'; +import { defineConfig, type UserConfig } from 'vite'; +import dts from 'vite-plugin-dts'; +import { STANDARD_EXTERNALS, EDITOR_ONLY_EXTERNALS } from '../types'; +import { cssInjectPlugin } from '../plugins/css-inject'; + +export interface PluginPackageOptions { + /** 包根目录 (通常是 __dirname) */ + root: string; + + /** 入口点配置 */ + entries?: { + /** 主入口 (默认: src/index.ts) */ + main?: string; + /** 运行时入口 (默认: src/runtime.ts) */ + runtime?: string; + /** 编辑器入口 (默认: src/editor/index.ts) */ + editor?: string; + }; + + /** 是否包含 CSS (默认: false) */ + hasCSS?: boolean; + + /** 是否生成 plugin.json 导出 (默认: true) */ + hasPluginJson?: boolean; + + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; + + /** 额外的 Vite 配置 */ + viteConfig?: Partial; +} + +/** + * 创建插件包的 Vite 配置 + * + * @example + * ```typescript + * // vite.config.ts + * import { pluginPreset } from '@esengine/build-config/presets'; + * + * export default pluginPreset({ + * root: __dirname, + * hasCSS: true + * }); + * ``` + */ +export function pluginPreset(options: PluginPackageOptions): UserConfig { + const { + root, + entries = {}, + hasCSS = false, + external = [], + viteConfig = {} + } = options; + + const mainEntry = entries.main ?? 'src/index.ts'; + const runtimeEntry = entries.runtime ?? 'src/runtime.ts'; + const editorEntry = entries.editor ?? 'src/editor/index.ts'; + + // 构建入口点映射 + const entryPoints: Record = { + index: resolve(root, mainEntry), + runtime: resolve(root, runtimeEntry), + 'editor/index': resolve(root, editorEntry) + }; + + const plugins: any[] = [ + dts({ + include: ['src'], + outDir: 'dist', + rollupTypes: false + }) + ]; + + // CSS 注入插件 + if (hasCSS) { + plugins.push(cssInjectPlugin()); + } + + return defineConfig({ + plugins, + esbuild: { + jsx: 'automatic', + }, + build: { + lib: { + entry: entryPoints, + formats: ['es'], + fileName: (_format: string, entryName: string) => `${entryName}.js` + }, + rollupOptions: { + external: [ + ...STANDARD_EXTERNALS, + ...external + ], + output: { + exports: 'named', + preserveModules: false, + // 禁用自动代码分割,所有共享代码内联到各入口 + manualChunks: () => undefined + } + }, + target: 'es2020', + minify: false, + sourcemap: true + }, + ...viteConfig + }); +} + +/** + * 创建独立运行时构建配置 + * 用于 platform-web 等需要生成独立 IIFE 运行时的场景 + * + * @example + * ```typescript + * // rollup.runtime.config.js + * import { standaloneRuntimeConfig } from '@esengine/build-config/presets'; + * + * export default standaloneRuntimeConfig({ + * root: __dirname, + * entry: 'src/runtime.ts', + * globalName: 'ECSRuntime' + * }); + * ``` + */ +export interface StandaloneRuntimeOptions { + /** 包根目录 */ + root: string; + /** 入口文件 */ + entry: string; + /** 全局变量名 (IIFE 格式) */ + globalName: string; + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; +} + +export function standaloneRuntimeConfig(options: StandaloneRuntimeOptions) { + const { root, entry, globalName, external = [] } = options; + + // 返回 Rollup 配置(而非 Vite,因为需要 IIFE 格式) + return { + input: resolve(root, entry), + output: { + file: 'dist/runtime.browser.js', + format: 'iife' as const, + name: globalName, + sourcemap: true, + exports: 'default' as const + }, + external: [ + ...STANDARD_EXTERNALS, + ...EDITOR_ONLY_EXTERNALS, + ...external + ], + plugins: [ + // 需要在使用时传入 rollup 插件 + ] + }; +} diff --git a/packages/build-config/src/presets/runtime-only.ts b/packages/build-config/src/presets/runtime-only.ts new file mode 100644 index 00000000..89d3daab --- /dev/null +++ b/packages/build-config/src/presets/runtime-only.ts @@ -0,0 +1,78 @@ +/** + * Runtime-Only Package Preset + * 纯运行时包预设 + * + * 用于不包含任何编辑器代码的基础库 + * For basic libraries without any editor code + * + * Examples: core, math, components, asset-system + */ + +import { resolve } from 'path'; +import { defineConfig, type UserConfig } from 'vite'; +import dts from 'vite-plugin-dts'; +import { STANDARD_EXTERNALS } from '../types'; + +export interface RuntimeOnlyOptions { + /** 包根目录 (通常是 __dirname) */ + root: string; + /** 入口文件 (默认: src/index.ts) */ + entry?: string; + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; + /** 额外的 Vite 配置 */ + viteConfig?: Partial; +} + +/** + * 创建纯运行时包的 Vite 配置 + * + * @example + * ```typescript + * // vite.config.ts + * import { runtimeOnlyPreset } from '@esengine/build-config/presets'; + * + * export default runtimeOnlyPreset({ + * root: __dirname + * }); + * ``` + */ +export function runtimeOnlyPreset(options: RuntimeOnlyOptions): UserConfig { + const { + root, + entry = 'src/index.ts', + external = [], + viteConfig = {} + } = options; + + return defineConfig({ + plugins: [ + dts({ + include: ['src'], + outDir: 'dist', + rollupTypes: false + }) + ], + build: { + lib: { + entry: resolve(root, entry), + formats: ['es'], + fileName: () => 'index.js' + }, + rollupOptions: { + external: [ + ...STANDARD_EXTERNALS, + ...external + ], + output: { + exports: 'named', + preserveModules: false + } + }, + target: 'es2020', + minify: false, + sourcemap: true + }, + ...viteConfig + }); +} diff --git a/packages/build-config/src/types.ts b/packages/build-config/src/types.ts new file mode 100644 index 00000000..9ab86776 --- /dev/null +++ b/packages/build-config/src/types.ts @@ -0,0 +1,107 @@ +/** + * Build Configuration Types + * 构建配置类型定义 + */ + +import type { UserConfig } from 'vite'; + +/** + * 包类型 + * Package types for different build configurations + */ +export const enum EPackageType { + /** + * 纯运行时库 - 不含任何编辑器代码 + * Pure runtime library - no editor dependencies + * + * Examples: core, math, components, asset-system + */ + RuntimeOnly = 'runtime-only', + + /** + * 插件包 - 同时包含运行时和编辑器模块 + * Plugin package - contains both runtime and editor modules + * + * Examples: ui, tilemap, behavior-tree, physics-rapier2d + */ + Plugin = 'plugin', + + /** + * 纯编辑器包 - 仅用于编辑器 + * Editor-only package - only used in editor + * + * Examples: editor-core, node-editor + */ + EditorOnly = 'editor-only', + + /** + * 应用包 - 最终应用(不发布到 npm) + * Application package - final app (not published) + * + * Examples: editor-app + */ + Application = 'application' +} + +/** + * 包构建配置 + */ +export interface PackageBuildConfig { + /** 包名 */ + name: string; + + /** 包类型 */ + type: EPackageType; + + /** 入口点配置 */ + entries?: { + /** 主入口 (默认: src/index.ts) */ + main?: string; + /** 运行时入口 (仅 Plugin 类型) */ + runtime?: string; + /** 编辑器入口 (Plugin 和 EditorOnly 类型) */ + editor?: string; + }; + + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; + + /** 是否包含 CSS */ + hasCSS?: boolean; + + /** 是否生成 plugin.json 导出 */ + hasPluginJson?: boolean; + + /** 额外的 Vite 配置 */ + viteConfig?: Partial; +} + +/** + * 标准外部依赖列表 + * Standard external dependencies that should never be bundled + */ +export const STANDARD_EXTERNALS = [ + // React 生态 + 'react', + 'react-dom', + 'react/jsx-runtime', + 'lucide-react', + + // 状态管理 + 'zustand', + 'immer', + + // 所有 @esengine 包 + /^@esengine\//, +] as const; + +/** + * 编辑器专用依赖(运行时构建必须排除) + * Editor-only dependencies that must be excluded from runtime builds + */ +export const EDITOR_ONLY_EXTERNALS = [ + '@esengine/editor-core', + '@esengine/node-editor', + /\/editor$/, + /\/editor\//, +] as const; diff --git a/packages/build-config/templates/plugin/package.json.template b/packages/build-config/templates/plugin/package.json.template new file mode 100644 index 00000000..ad163d91 --- /dev/null +++ b/packages/build-config/templates/plugin/package.json.template @@ -0,0 +1,62 @@ +{ + "name": "{{fullName}}", + "version": "1.0.0", + "description": "{{description}}", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./runtime": { + "types": "./dist/runtime.d.ts", + "import": "./dist/runtime.js" + }, + "./editor": { + "types": "./dist/editor/index.d.ts", + "import": "./dist/editor/index.js" + }, + "./plugin.json": "./plugin.json" + }, + "files": [ + "dist", + "plugin.json" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "clean": "rimraf dist", + "type-check": "tsc --noEmit" + }, + "peerDependencies": { + "@esengine/ecs-framework": "{{depVersion}}", + "@esengine/ecs-components": "{{depVersion}}", + "@esengine/editor-core": "{{depVersion}}", + "react": "^18.3.1" + }, + "peerDependenciesMeta": { + "@esengine/editor-core": { + "optional": true + }, + "react": { + "optional": true + } + }, + "devDependencies": { + "@esengine/build-config": "{{depVersion}}", + "@types/react": "^18.3.12", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.8.3" + }, + "keywords": [ + "ecs", + "esengine", + "plugin" + ], + "author": "", + "license": "MIT" +} diff --git a/packages/build-config/templates/plugin/plugin.json.template b/packages/build-config/templates/plugin/plugin.json.template new file mode 100644 index 00000000..a3fabdb5 --- /dev/null +++ b/packages/build-config/templates/plugin/plugin.json.template @@ -0,0 +1,22 @@ +{ + "id": "{{fullName}}", + "name": "{{displayName}}", + "version": "1.0.0", + "category": "{{category}}", + "enabledByDefault": true, + "isEnginePlugin": false, + "modules": [ + { + "name": "{{pascalName}}Runtime", + "type": "runtime", + "entry": "./src/runtime.ts" + }, + { + "name": "{{pascalName}}Editor", + "type": "editor", + "entry": "./src/editor/index.ts" + } + ], + "components": [], + "dependencies": [] +} diff --git a/packages/build-config/templates/plugin/src/RuntimeModule.ts.template b/packages/build-config/templates/plugin/src/RuntimeModule.ts.template new file mode 100644 index 00000000..475581ca --- /dev/null +++ b/packages/build-config/templates/plugin/src/RuntimeModule.ts.template @@ -0,0 +1,49 @@ +/** + * {{displayName}} Runtime Module + * + * 运行时模块 - 负责注册组件、服务和系统 + */ + +import type { + IRuntimeModuleLoader, + IComponentRegistry, + SystemContext +} from '@esengine/ecs-components'; +import type { IScene, ServiceContainer } from '@esengine/ecs-framework'; + +export class {{name}}RuntimeModule implements IRuntimeModuleLoader { + /** + * 注册组件到组件注册表 + */ + registerComponents(registry: IComponentRegistry): void { + // registry.register(MyComponent); + } + + /** + * 注册服务到服务容器 + */ + registerServices?(services: ServiceContainer): void { + // services.registerSingleton(MyService); + } + + /** + * 初始化回调 + */ + async onInitialize?(): Promise { + // 执行初始化逻辑 + } + + /** + * 为场景创建系统 + */ + createSystems?(scene: IScene, context: SystemContext): void { + // scene.addSystem(new MySystem()); + } + + /** + * 系统创建完成后的回调,用于连接跨插件依赖 + */ + onSystemsCreated?(scene: IScene, context: SystemContext): void { + // 连接跨插件依赖 + } +} diff --git a/packages/build-config/templates/plugin/src/editor/Plugin.ts.template b/packages/build-config/templates/plugin/src/editor/Plugin.ts.template new file mode 100644 index 00000000..f3975a57 --- /dev/null +++ b/packages/build-config/templates/plugin/src/editor/Plugin.ts.template @@ -0,0 +1,34 @@ +/** + * {{displayName}} Plugin + * + * 插件定义 - 注册编辑器模块(Inspector、工具等) + */ + +import type { IPluginLoader, PluginDescriptor, IEditorModuleLoader } from '@esengine/ecs-components'; +import { {{name}}RuntimeModule } from '../{{name}}RuntimeModule'; + +class {{name}}EditorModule implements IEditorModuleLoader { + registerInspectors(registry: any): void { + // 注册组件 Inspector + // registry.register('MyComponent', MyComponentInspector); + } +} + +const descriptor: PluginDescriptor = { + id: '@esengine/{{name}}', + name: '{{displayName}}', + version: '1.0.0', + category: '{{category}}', + enabledByDefault: true, + isEnginePlugin: false, + modules: [ + { name: '{{name}}Runtime', type: 'runtime', entry: './src/runtime.ts' }, + { name: '{{name}}Editor', type: 'editor', entry: './src/editor/index.ts' } + ] +}; + +export const {{name}}Plugin: IPluginLoader = { + descriptor, + runtimeModule: new {{name}}RuntimeModule(), + editorModule: new {{name}}EditorModule() +}; diff --git a/packages/build-config/templates/plugin/src/editor/index.ts.template b/packages/build-config/templates/plugin/src/editor/index.ts.template new file mode 100644 index 00000000..d5a1fd7f --- /dev/null +++ b/packages/build-config/templates/plugin/src/editor/index.ts.template @@ -0,0 +1,13 @@ +/** + * @esengine/{{name}} Editor Module + * + * 编辑器模块 - 包含 Inspector、工具等编辑器专用代码 + * Editor module - contains Inspector, tools, and other editor-specific code + * + * This module can safely import React and editor-core packages. + */ + +export { {{name}}Plugin } from './{{name}}Plugin'; + +// Inspectors +// export { MyComponentInspector } from './inspectors/MyComponentInspector'; diff --git a/packages/build-config/templates/plugin/src/index.ts.template b/packages/build-config/templates/plugin/src/index.ts.template new file mode 100644 index 00000000..dd2a6797 --- /dev/null +++ b/packages/build-config/templates/plugin/src/index.ts.template @@ -0,0 +1,14 @@ +/** + * @esengine/{{name}} + * + * {{description}} + * + * 主入口 - 导出所有内容(包括编辑器模块) + * Main entry - exports everything (including editor modules) + */ + +// Runtime exports (always available) +export * from './runtime'; + +// Editor exports (only in editor environment) +export { {{name}}Plugin } from './editor'; diff --git a/packages/build-config/templates/plugin/src/runtime.ts.template b/packages/build-config/templates/plugin/src/runtime.ts.template new file mode 100644 index 00000000..56ffb88f --- /dev/null +++ b/packages/build-config/templates/plugin/src/runtime.ts.template @@ -0,0 +1,17 @@ +/** + * @esengine/{{name}} Runtime Entry Point + * + * 运行时入口 - 仅导出运行时代码,不包含任何编辑器依赖 + * Runtime entry - exports only runtime code without any editor dependencies + * + * IMPORTANT: Do not import React or any editor packages here! + */ + +// Components +// export { MyComponent } from './components/MyComponent'; + +// Systems +// export { MySystem } from './systems/MySystem'; + +// Runtime Module +export { {{name}}RuntimeModule } from './{{name}}RuntimeModule'; diff --git a/packages/build-config/templates/plugin/tsconfig.build.json.template b/packages/build-config/templates/plugin/tsconfig.build.json.template new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/build-config/templates/plugin/tsconfig.build.json.template @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/build-config/templates/plugin/tsconfig.json.template b/packages/build-config/templates/plugin/tsconfig.json.template new file mode 100644 index 00000000..9ef4982a --- /dev/null +++ b/packages/build-config/templates/plugin/tsconfig.json.template @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/build-config/templates/plugin/tsup.config.ts.template b/packages/build-config/templates/plugin/tsup.config.ts.template new file mode 100644 index 00000000..e5589c93 --- /dev/null +++ b/packages/build-config/templates/plugin/tsup.config.ts.template @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { pluginPreset } from '@esengine/build-config/presets/tsup'; + +export default defineConfig({ + ...pluginPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/build-config/templates/runtime-only/package.json.template b/packages/build-config/templates/runtime-only/package.json.template new file mode 100644 index 00000000..d958ca47 --- /dev/null +++ b/packages/build-config/templates/runtime-only/package.json.template @@ -0,0 +1,38 @@ +{ + "name": "{{fullName}}", + "version": "1.0.0", + "description": "{{description}}", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "clean": "rimraf dist", + "type-check": "tsc --noEmit" + }, + "peerDependencies": { + "@esengine/ecs-framework": "{{depVersion}}" + }, + "devDependencies": { + "@esengine/build-config": "{{depVersion}}", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.8.3" + }, + "keywords": [ + "ecs" + ], + "author": "", + "license": "MIT" +} diff --git a/packages/build-config/templates/runtime-only/src/index.ts.template b/packages/build-config/templates/runtime-only/src/index.ts.template new file mode 100644 index 00000000..b5c2df43 --- /dev/null +++ b/packages/build-config/templates/runtime-only/src/index.ts.template @@ -0,0 +1,8 @@ +/** + * @esengine/{{name}} + * + * {{description}} + */ + +// Export your public API here +export {}; diff --git a/packages/build-config/templates/runtime-only/tsconfig.build.json.template b/packages/build-config/templates/runtime-only/tsconfig.build.json.template new file mode 100644 index 00000000..bf8abf7b --- /dev/null +++ b/packages/build-config/templates/runtime-only/tsconfig.build.json.template @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/build-config/templates/runtime-only/tsconfig.json.template b/packages/build-config/templates/runtime-only/tsconfig.json.template new file mode 100644 index 00000000..6c5476dc --- /dev/null +++ b/packages/build-config/templates/runtime-only/tsconfig.json.template @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/build-config/templates/runtime-only/tsup.config.ts.template b/packages/build-config/templates/runtime-only/tsup.config.ts.template new file mode 100644 index 00000000..08929a87 --- /dev/null +++ b/packages/build-config/templates/runtime-only/tsup.config.ts.template @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '@esengine/build-config/presets/tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/build-config/tsconfig.json b/packages/build-config/tsconfig.json new file mode 100644 index 00000000..f59e0a3a --- /dev/null +++ b/packages/build-config/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "declarationDir": "dist", + "outDir": "dist", + "rootDir": "src", + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/camera-editor/package.json b/packages/camera-editor/package.json new file mode 100644 index 00000000..f3edd467 --- /dev/null +++ b/packages/camera-editor/package.json @@ -0,0 +1,43 @@ +{ + "name": "@esengine/camera-editor", + "version": "1.0.0", + "description": "Editor components for camera system", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/camera": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "react": "^18.3.1", + "@types/react": "^18.2.0", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "camera", + "editor" + ], + "author": "yhh", + "license": "MIT" +} diff --git a/packages/camera-editor/src/index.ts b/packages/camera-editor/src/index.ts new file mode 100644 index 00000000..143f3c22 --- /dev/null +++ b/packages/camera-editor/src/index.ts @@ -0,0 +1,91 @@ +/** + * @esengine/camera-editor + * + * Editor support for @esengine/camera + * 相机编辑器支持 + */ + +import type { Entity, ServiceContainer } from '@esengine/ecs-framework'; +import { Core } from '@esengine/ecs-framework'; +import { TransformComponent } from '@esengine/engine-core'; +import type { + IEditorModuleLoader, + EntityCreationTemplate +} from '@esengine/editor-core'; +import { + EntityStoreService, + MessageHub, + ComponentRegistry +} from '@esengine/editor-core'; +import { CameraComponent } from '@esengine/camera'; + +export class CameraEditorModule implements IEditorModuleLoader { + async install(services: ServiceContainer): Promise { + const componentRegistry = services.resolve(ComponentRegistry); + if (componentRegistry) { + componentRegistry.register({ + name: 'Camera', + type: CameraComponent, + category: 'components.category.rendering', + description: 'Camera for 2D/3D rendering', + icon: 'Camera' + }); + } + } + + async uninstall(): Promise { + // Nothing to cleanup + } + + getEntityCreationTemplates(): EntityCreationTemplate[] { + return [ + { + id: 'create-camera', + label: 'Camera', + icon: 'Camera', + category: 'rendering', + order: 50, + create: (): number => { + return this.createCameraEntity('Camera'); + } + }, + ]; + } + + private createCameraEntity(baseName: string): number { + const scene = Core.scene; + if (!scene) { + throw new Error('Scene not available'); + } + + const entityStore = Core.services.resolve(EntityStoreService); + const messageHub = Core.services.resolve(MessageHub); + + if (!entityStore || !messageHub) { + throw new Error('EntityStoreService or MessageHub not available'); + } + + const existingCount = entityStore.getAllEntities() + .filter((e: Entity) => e.name.startsWith(baseName)).length; + const entityName = existingCount > 0 ? `${baseName} ${existingCount + 1}` : baseName; + + const entity = scene.createEntity(entityName); + + const transform = new TransformComponent(); + entity.addComponent(transform); + + const camera = new CameraComponent(); + entity.addComponent(camera); + + entityStore.addEntity(entity); + messageHub.publish('entity:added', { entity }); + messageHub.publish('scene:modified', {}); + entityStore.selectEntity(entity); + + return entity.id; + } +} + +export const cameraEditorModule = new CameraEditorModule(); + +export default cameraEditorModule; diff --git a/packages/camera-editor/tsconfig.build.json b/packages/camera-editor/tsconfig.build.json new file mode 100644 index 00000000..29e209e8 --- /dev/null +++ b/packages/camera-editor/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/camera-editor/tsconfig.json b/packages/camera-editor/tsconfig.json new file mode 100644 index 00000000..39e66801 --- /dev/null +++ b/packages/camera-editor/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src", + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../core" }, + { "path": "../camera" }, + { "path": "../editor-core" } + ] +} diff --git a/packages/camera-editor/tsup.config.ts b/packages/camera-editor/tsup.config.ts new file mode 100644 index 00000000..b4f49f5d --- /dev/null +++ b/packages/camera-editor/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { editorOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...editorOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/camera/package.json b/packages/camera/package.json new file mode 100644 index 00000000..916248fd --- /dev/null +++ b/packages/camera/package.json @@ -0,0 +1,47 @@ +{ + "name": "@esengine/camera", + "version": "1.0.0", + "description": "Camera component and systems for 2D/3D rendering", + "esengine": { + "plugin": true, + "pluginExport": "CameraPlugin", + "category": "core", + "isEnginePlugin": true + }, + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "camera", + "2d", + "3d", + "rendering" + ], + "author": "yhh", + "license": "MIT" +} diff --git a/packages/components/src/CameraComponent.ts b/packages/camera/src/CameraComponent.ts similarity index 73% rename from packages/components/src/CameraComponent.ts rename to packages/camera/src/CameraComponent.ts index 16ec3175..e3c02ac9 100644 --- a/packages/components/src/CameraComponent.ts +++ b/packages/camera/src/CameraComponent.ts @@ -1,77 +1,64 @@ import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework'; -/** - * 相机投影类型 - */ -export enum CameraProjection { +export enum ECameraProjection { Perspective = 'perspective', Orthographic = 'orthographic' } -/** - * 相机组件 - 管理视图和投影 - */ @ECSComponent('Camera') @Serializable({ version: 1, typeId: 'Camera' }) export class CameraComponent extends Component { - /** 投影类型 */ @Serialize() @Property({ type: 'enum', label: 'Projection', options: [ - { label: 'Orthographic', value: CameraProjection.Orthographic }, - { label: 'Perspective', value: CameraProjection.Perspective } + { label: 'Orthographic', value: ECameraProjection.Orthographic }, + { label: 'Perspective', value: ECameraProjection.Perspective } ] }) - public projection: CameraProjection = CameraProjection.Orthographic; + public projection: ECameraProjection = ECameraProjection.Orthographic; - /** 视野角度(透视模式) */ + /** 透视模式下的视野角度,范围 [1, 179] 度 */ @Serialize() @Property({ type: 'number', label: 'Field of View', min: 1, max: 179 }) public fieldOfView: number = 60; - /** 正交尺寸(正交模式) */ + /** 正交模式下的可见区域半高度(世界单位) */ @Serialize() @Property({ type: 'number', label: 'Orthographic Size', min: 0.1, step: 0.1 }) public orthographicSize: number = 5; - /** 近裁剪面 */ @Serialize() @Property({ type: 'number', label: 'Near Clip', min: 0.01, step: 0.1 }) public nearClipPlane: number = 0.1; - /** 远裁剪面 */ @Serialize() @Property({ type: 'number', label: 'Far Clip', min: 1, step: 10 }) public farClipPlane: number = 1000; - /** 视口X */ + /** 视口归一化坐标,范围 [0, 1] */ @Serialize() @Property({ type: 'number', label: 'Viewport X', min: 0, max: 1, step: 0.01 }) public viewportX: number = 0; - /** 视口Y */ @Serialize() @Property({ type: 'number', label: 'Viewport Y', min: 0, max: 1, step: 0.01 }) public viewportY: number = 0; - /** 视口宽度 */ @Serialize() @Property({ type: 'number', label: 'Viewport Width', min: 0, max: 1, step: 0.01 }) public viewportWidth: number = 1; - /** 视口高度 */ @Serialize() @Property({ type: 'number', label: 'Viewport Height', min: 0, max: 1, step: 0.01 }) public viewportHeight: number = 1; - /** 渲染优先级 */ + /** 渲染优先级,值越大越后渲染(覆盖在上层) */ @Serialize() @Property({ type: 'integer', label: 'Depth' }) public depth: number = 0; - /** 背景颜色 */ @Serialize() @Property({ type: 'color', label: 'Background Color' }) public backgroundColor: string = '#000000'; @@ -80,3 +67,6 @@ export class CameraComponent extends Component { super(); } } + +/** @deprecated 使用 ECameraProjection 代替 */ +export const CameraProjection = ECameraProjection; diff --git a/packages/camera/src/CameraPlugin.ts b/packages/camera/src/CameraPlugin.ts new file mode 100644 index 00000000..457b541b --- /dev/null +++ b/packages/camera/src/CameraPlugin.ts @@ -0,0 +1,24 @@ +import type { ComponentRegistry as ComponentRegistryType } from '@esengine/ecs-framework'; +import type { IRuntimeModule, IPlugin, PluginDescriptor } from '@esengine/engine-core'; +import { CameraComponent } from './CameraComponent'; + +class CameraRuntimeModule implements IRuntimeModule { + registerComponents(registry: typeof ComponentRegistryType): void { + registry.register(CameraComponent); + } +} + +const descriptor: PluginDescriptor = { + id: '@esengine/camera', + name: 'Camera', + version: '1.0.0', + description: '2D/3D 相机组件', + category: 'core', + enabledByDefault: true, + isEnginePlugin: true +}; + +export const CameraPlugin: IPlugin = { + descriptor, + runtimeModule: new CameraRuntimeModule() +}; diff --git a/packages/camera/src/index.ts b/packages/camera/src/index.ts new file mode 100644 index 00000000..2fa92d1c --- /dev/null +++ b/packages/camera/src/index.ts @@ -0,0 +1,2 @@ +export { CameraComponent, ECameraProjection, CameraProjection } from './CameraComponent'; +export { CameraPlugin } from './CameraPlugin'; diff --git a/packages/camera/tsconfig.build.json b/packages/camera/tsconfig.build.json new file mode 100644 index 00000000..f39a0594 --- /dev/null +++ b/packages/camera/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/camera/tsconfig.json b/packages/camera/tsconfig.json new file mode 100644 index 00000000..02f5f187 --- /dev/null +++ b/packages/camera/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../core" } + ] +} diff --git a/packages/camera/tsup.config.ts b/packages/camera/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/camera/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/components/package.json b/packages/components/package.json deleted file mode 100644 index b8736c16..00000000 --- a/packages/components/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "@esengine/ecs-components", - "version": "1.0.0", - "description": "Standard component library for ECS Framework", - "main": "bin/index.js", - "types": "bin/index.d.ts", - "exports": { - ".": { - "types": "./bin/index.d.ts", - "import": "./bin/index.js", - "development": { - "types": "./src/index.ts", - "import": "./src/index.ts" - } - }, - "./plugin.json": "./plugin.json" - }, - "files": [ - "bin/**/*", - "plugin.json" - ], - "keywords": [ - "ecs", - "components", - "game-engine", - "typescript" - ], - "scripts": { - "clean": "rimraf bin dist tsconfig.tsbuildinfo", - "build:ts": "tsc", - "prebuild": "npm run clean", - "build": "npm run build:ts", - "build:watch": "tsc --watch", - "rebuild": "npm run clean && npm run build" - }, - "author": "yhh", - "license": "MIT", - "devDependencies": { - "rimraf": "^5.0.0", - "typescript": "^5.8.3" - }, - "peerDependencies": { - "@esengine/ecs-framework": "^2.2.8", - "@esengine/asset-system": "workspace:*" - }, - "dependencies": { - "tslib": "^2.8.1" - }, - "repository": { - "type": "git", - "url": "https://github.com/esengine/ecs-framework.git", - "directory": "packages/components" - } -} diff --git a/packages/components/plugin.json b/packages/components/plugin.json deleted file mode 100644 index cb4b79d5..00000000 --- a/packages/components/plugin.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "@esengine/ecs-components", - "name": "Core Components", - "version": "1.0.0", - "description": "Transform, Sprite, Camera 等核心组件", - "category": "core", - "loadingPhase": "preDefault", - "enabledByDefault": true, - "canContainContent": false, - "isEnginePlugin": true, - "modules": [ - { - "name": "CoreRuntime", - "type": "runtime", - "entry": "./src/index.ts" - } - ], - "dependencies": [], - "icon": "Settings" -} diff --git a/packages/components/src/AudioSourceComponent.ts b/packages/components/src/AudioSourceComponent.ts deleted file mode 100644 index fd7ed525..00000000 --- a/packages/components/src/AudioSourceComponent.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework'; - -/** - * 音频源组件 - 管理音频播放 - */ -@ECSComponent('AudioSource') -@Serializable({ version: 1, typeId: 'AudioSource' }) -export class AudioSourceComponent extends Component { - /** 音频资源路径 */ - @Serialize() public clip: string = ''; - - /** 音量 (0-1) */ - @Serialize() public volume: number = 1; - - /** 音调 */ - @Serialize() public pitch: number = 1; - - /** 是否循环 */ - @Serialize() public loop: boolean = false; - - /** 是否启动时播放 */ - @Serialize() public playOnAwake: boolean = false; - - /** 是否静音 */ - @Serialize() public mute: boolean = false; - - /** 空间混合 (0=2D, 1=3D) */ - @Serialize() public spatialBlend: number = 0; - - /** 最小距离(3D音效) */ - @Serialize() public minDistance: number = 1; - - /** 最大距离(3D音效) */ - @Serialize() public maxDistance: number = 500; - - constructor() { - super(); - } -} diff --git a/packages/components/src/CorePlugin.ts b/packages/components/src/CorePlugin.ts deleted file mode 100644 index f0c61bfc..00000000 --- a/packages/components/src/CorePlugin.ts +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Core Components Plugin - * 核心组件插件 - * - * 提供基础的 Transform、Sprite、Camera 等核心组件 - * 这是一个核心插件,不可禁用 - */ - -import type { ComponentRegistry as ComponentRegistryType, IScene, ServiceContainer } from '@esengine/ecs-framework'; - -// Components -import { TransformComponent } from './TransformComponent'; -import { SpriteComponent } from './SpriteComponent'; -import { SpriteAnimatorComponent } from './SpriteAnimatorComponent'; -import { CameraComponent } from './CameraComponent'; - -// Systems -import { SpriteAnimatorSystem } from './systems/SpriteAnimatorSystem'; - - -/** - * 系统创建上下文 - */ -export interface SystemContext { - isEditor: boolean; - engineBridge?: any; - renderSystem?: any; - [key: string]: any; -} - -/** - * 插件描述符类型 - */ -export interface PluginDescriptor { - id: string; - name: string; - version: string; - description?: string; - category?: string; - loadingPhase?: string; - enabledByDefault?: boolean; - canContainContent?: boolean; - isEnginePlugin?: boolean; - modules?: Array<{ - name: string; - type: string; - entry: string; - }>; - dependencies?: Array<{ - id: string; - version?: string; - }>; - icon?: string; -} - -/** - * 运行时模块加载器接口 - */ -export interface IRuntimeModuleLoader { - registerComponents(registry: typeof ComponentRegistryType): void; - registerServices?(services: ServiceContainer): void; - createSystems?(scene: IScene, context: SystemContext): void; - /** - * 所有系统创建完成后调用 - * 用于处理跨插件的系统依赖关系 - * Called after all systems are created, used for cross-plugin system dependencies - */ - onSystemsCreated?(scene: IScene, context: SystemContext): void; - onInitialize?(): Promise; - onDestroy?(): void; -} - -/** - * 插件加载器接口 - */ -export interface IPluginLoader { - readonly descriptor: PluginDescriptor; - readonly runtimeModule?: IRuntimeModuleLoader; - readonly editorModule?: any; -} - -/** - * 核心组件运行时模块 - */ -export class CoreRuntimeModule implements IRuntimeModuleLoader { - registerComponents(registry: typeof ComponentRegistryType): void { - registry.register(TransformComponent); - registry.register(SpriteComponent); - registry.register(SpriteAnimatorComponent); - registry.register(CameraComponent); - } - - createSystems(scene: IScene, context: SystemContext): void { - const animatorSystem = new SpriteAnimatorSystem(); - - if (context.isEditor) { - animatorSystem.enabled = false; - } - - scene.addSystem(animatorSystem); - context.animatorSystem = animatorSystem; - } -} - -/** - * 插件描述符 - */ -const descriptor: PluginDescriptor = { - id: '@esengine/ecs-components', - name: 'Core Components', - version: '1.0.0', - description: 'Transform, Sprite, Camera 等核心组件', - category: 'core', - loadingPhase: 'preDefault', - enabledByDefault: true, - canContainContent: false, - isEnginePlugin: true, - modules: [ - { - name: 'CoreRuntime', - type: 'runtime', - entry: './src/index.ts' - } - ], - icon: 'Settings' -}; - -/** - * 核心组件插件 - */ -export const CorePlugin: IPluginLoader = { - descriptor, - runtimeModule: new CoreRuntimeModule(), -}; - -export default CorePlugin; diff --git a/packages/components/src/TextComponent.ts b/packages/components/src/TextComponent.ts deleted file mode 100644 index 5f075229..00000000 --- a/packages/components/src/TextComponent.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework'; - -/** - * 文本对齐方式 - */ -export enum TextAlignment { - Left = 'left', - Center = 'center', - Right = 'right' -} - -/** - * 文本组件 - 管理文本渲染 - */ -@ECSComponent('Text') -@Serializable({ version: 1, typeId: 'Text' }) -export class TextComponent extends Component { - /** 文本内容 */ - @Serialize() public text: string = ''; - - /** 字体 */ - @Serialize() public font: string = 'Arial'; - - /** 字体大小 */ - @Serialize() public fontSize: number = 16; - - /** 颜色 */ - @Serialize() public color: string = '#ffffff'; - - /** 对齐方式 */ - @Serialize() public alignment: TextAlignment = TextAlignment.Left; - - /** 行高 */ - @Serialize() public lineHeight: number = 1.2; - - /** 是否加粗 */ - @Serialize() public bold: boolean = false; - - /** 是否斜体 */ - @Serialize() public italic: boolean = false; - - constructor(text: string = '') { - super(); - this.text = text; - } -} diff --git a/packages/components/src/TransformComponent.ts b/packages/components/src/TransformComponent.ts deleted file mode 100644 index 9a01161e..00000000 --- a/packages/components/src/TransformComponent.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework'; - -/** - * 3D向量 - */ -export interface Vector3 { - x: number; - y: number; - z: number; -} - -/** - * 变换组件 - 管理实体的位置、旋转和缩放 - */ -@ECSComponent('Transform') -@Serializable({ version: 1, typeId: 'Transform' }) -export class TransformComponent extends Component { - /** 位置 */ - @Serialize() - @Property({ type: 'vector3', label: 'Position' }) - public position: Vector3 = { x: 0, y: 0, z: 0 }; - - /** 旋转(欧拉角,度) */ - @Serialize() - @Property({ type: 'vector3', label: 'Rotation' }) - public rotation: Vector3 = { x: 0, y: 0, z: 0 }; - - /** 缩放 */ - @Serialize() - @Property({ type: 'vector3', label: 'Scale' }) - public scale: Vector3 = { x: 1, y: 1, z: 1 }; - - constructor(x: number = 0, y: number = 0, z: number = 0) { - super(); - this.position = { x, y, z }; - } - - /** - * 设置位置 - */ - public setPosition(x: number, y: number, z: number = 0): this { - this.position = { x, y, z }; - return this; - } - - /** - * 设置旋转 - */ - public setRotation(x: number, y: number, z: number): this { - this.rotation = { x, y, z }; - return this; - } - - /** - * 设置缩放 - */ - public setScale(x: number, y: number, z: number = 1): this { - this.scale = { x, y, z }; - return this; - } -} diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts deleted file mode 100644 index adf75bf4..00000000 --- a/packages/components/src/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -// 变换 -export { TransformComponent, Vector3 } from './TransformComponent'; - -// 渲染 -export { SpriteComponent } from './SpriteComponent'; -export { SpriteAnimatorComponent, AnimationFrame, AnimationClip } from './SpriteAnimatorComponent'; -export { TextComponent, TextAlignment } from './TextComponent'; -export { CameraComponent, CameraProjection } from './CameraComponent'; - -// 系统 -export { SpriteAnimatorSystem } from './systems/SpriteAnimatorSystem'; - -// 物理组件已移至 @esengine/physics-rapier2d 包 -// Physics components have been moved to @esengine/physics-rapier2d package - -// 音频 -export { AudioSourceComponent } from './AudioSourceComponent'; - -// Plugin (unified plugin system) -export { CorePlugin, CoreRuntimeModule } from './CorePlugin'; -export type { SystemContext, PluginDescriptor, IRuntimeModuleLoader, IPluginLoader } from './CorePlugin'; \ No newline at end of file diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json deleted file mode 100644 index 27a8ccf8..00000000 --- a/packages/components/tsconfig.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ES2020", - "moduleResolution": "node", - "allowImportingTsExtensions": false, - "lib": ["ES2020", "DOM"], - "outDir": "./bin", - "rootDir": "./src", - "strict": true, - "composite": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "removeComments": false, - "noImplicitAny": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "exactOptionalPropertyTypes": false, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": false, - "noUncheckedIndexedAccess": false, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "importHelpers": true, - "downlevelIteration": true, - "isolatedModules": false, - "allowJs": true, - "resolveJsonModule": true - }, - "include": [ - "src/**/*", - "plugin.json" - ], - "exclude": [ - "node_modules", - "bin", - "**/*.test.ts", - "**/*.spec.ts" - ], - "references": [ - { - "path": "../core" - } - ] -} diff --git a/packages/core/src/Core.ts b/packages/core/src/Core.ts index b0ae2e04..8c6dd263 100644 --- a/packages/core/src/Core.ts +++ b/packages/core/src/Core.ts @@ -69,13 +69,6 @@ export class Core { */ private static _logger = createLogger('Core'); - /** - * 实体系统启用状态 - * - * 控制是否启用ECS实体系统功能。 - */ - public static entitySystemsEnabled: boolean; - /** * 调试模式标志 * @@ -132,7 +125,6 @@ export class Core { // 保存配置 this._config = { debug: true, - enableEntitySystems: true, ...config }; @@ -176,7 +168,6 @@ export class Core { this._pluginManager.initialize(this, this._serviceContainer); this._serviceContainer.registerInstance(PluginManager, this._pluginManager); - Core.entitySystemsEnabled = this._config.enableEntitySystems ?? true; this.debug = this._config.debug ?? true; // 初始化调试管理器 @@ -266,7 +257,6 @@ export class Core { * // 方式1:使用配置对象 * Core.create({ * debug: true, - * enableEntitySystems: true, * debugConfig: { * enabled: true, * websocketUrl: 'ws://localhost:9229' @@ -281,7 +271,7 @@ export class Core { if (this._instance == null) { // 向后兼容:如果传入boolean,转换为配置对象 const coreConfig: ICoreConfig = typeof config === 'boolean' - ? { debug: config, enableEntitySystems: true } + ? { debug: config } : config; this._instance = new Core(coreConfig); } else { @@ -614,7 +604,6 @@ export class Core { // 核心系统初始化 Core._logger.info('Core initialized', { debug: this.debug, - entitySystemsEnabled: Core.entitySystemsEnabled, debugEnabled: this._config.debugConfig?.enabled || false }); } diff --git a/packages/core/src/ECS/Components/HierarchyComponent.ts b/packages/core/src/ECS/Components/HierarchyComponent.ts new file mode 100644 index 00000000..1895a9cc --- /dev/null +++ b/packages/core/src/ECS/Components/HierarchyComponent.ts @@ -0,0 +1,56 @@ +import { Component } from '../Component'; +import { ECSComponent } from '../Decorators'; +import { Serializable, Serialize } from '../Serialization/SerializationDecorators'; + +/** + * 层级关系组件 - 用于建立实体间的父子关系 + * + * 只有需要层级关系的实体才添加此组件,遵循 ECS 组合原则。 + * 层级操作应通过 HierarchySystem 进行,而非直接修改此组件。 + * + * @example + * ```typescript + * // 通过 HierarchySystem 设置父子关系 + * const hierarchySystem = scene.getSystem(HierarchySystem); + * hierarchySystem.setParent(childEntity, parentEntity); + * + * // 查询层级信息 + * const parent = hierarchySystem.getParent(entity); + * const children = hierarchySystem.getChildren(entity); + * ``` + */ +@ECSComponent('Hierarchy') +@Serializable({ version: 1, typeId: 'Hierarchy' }) +export class HierarchyComponent extends Component { + /** + * 父实体 ID + * null 表示根实体(无父级) + */ + @Serialize() + public parentId: number | null = null; + + /** + * 子实体 ID 列表 + * 顺序即为子级的排列顺序 + */ + @Serialize() + public childIds: number[] = []; + + /** + * 在层级中的深度 + * 根实体深度为 0,由 HierarchySystem 维护 + */ + public depth: number = 0; + + /** + * 层级中是否激活 + * 考虑所有祖先的激活状态,由 HierarchySystem 维护 + */ + public bActiveInHierarchy: boolean = true; + + /** + * 层级缓存是否脏 + * 用于优化缓存更新 + */ + public bCacheDirty: boolean = true; +} diff --git a/packages/core/src/ECS/Components/index.ts b/packages/core/src/ECS/Components/index.ts new file mode 100644 index 00000000..826cbc8a --- /dev/null +++ b/packages/core/src/ECS/Components/index.ts @@ -0,0 +1 @@ +export { HierarchyComponent } from './HierarchyComponent'; diff --git a/packages/core/src/ECS/Core/FluentAPI/EntityBuilder.ts b/packages/core/src/ECS/Core/FluentAPI/EntityBuilder.ts index 8f198a94..dc74e8c5 100644 --- a/packages/core/src/ECS/Core/FluentAPI/EntityBuilder.ts +++ b/packages/core/src/ECS/Core/FluentAPI/EntityBuilder.ts @@ -2,6 +2,7 @@ import { Entity } from '../../Entity'; import { Component } from '../../Component'; import { IScene } from '../../IScene'; import { ComponentType, ComponentStorageManager } from '../ComponentStorage'; +import { HierarchySystem } from '../../Systems/HierarchySystem'; /** * 实体构建器 - 提供流式API创建和配置实体 @@ -129,7 +130,8 @@ export class EntityBuilder { */ public withChild(childBuilder: EntityBuilder): EntityBuilder { const child = childBuilder.build(); - this.entity.addChild(child); + const hierarchySystem = this.scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(child, this.entity); return this; } @@ -139,9 +141,10 @@ export class EntityBuilder { * @returns 实体构建器 */ public withChildren(...childBuilders: EntityBuilder[]): EntityBuilder { + const hierarchySystem = this.scene.getSystem(HierarchySystem); for (const childBuilder of childBuilders) { const child = childBuilder.build(); - this.entity.addChild(child); + hierarchySystem?.setParent(child, this.entity); } return this; } @@ -154,7 +157,8 @@ export class EntityBuilder { public withChildFactory(childFactory: (parent: Entity) => EntityBuilder): EntityBuilder { const childBuilder = childFactory(this.entity); const child = childBuilder.build(); - this.entity.addChild(child); + const hierarchySystem = this.scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(child, this.entity); return this; } @@ -167,7 +171,8 @@ export class EntityBuilder { public withChildIf(condition: boolean, childBuilder: EntityBuilder): EntityBuilder { if (condition) { const child = childBuilder.build(); - this.entity.addChild(child); + const hierarchySystem = this.scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(child, this.entity); } return this; } diff --git a/packages/core/src/ECS/Entity.ts b/packages/core/src/ECS/Entity.ts index 23fb4c1e..6f15f4f6 100644 --- a/packages/core/src/ECS/Entity.ts +++ b/packages/core/src/ECS/Entity.ts @@ -37,12 +37,14 @@ export class EntityComparer { * * ECS架构中的实体(Entity),作为组件的容器。 * 实体本身不包含游戏逻辑,所有功能都通过组件来实现。 - * 支持父子关系,可以构建实体层次结构。 + * + * 层级关系通过 HierarchyComponent 和 HierarchySystem 管理, + * 而非 Entity 内置属性,符合 ECS 组合原则。 * * @example * ```typescript * // 创建实体 - * const entity = new Entity("Player", 1); + * const entity = scene.createEntity("Player"); * * // 添加组件 * const healthComponent = entity.addComponent(new HealthComponent(100)); @@ -50,12 +52,9 @@ export class EntityComparer { * // 获取组件 * const health = entity.getComponent(HealthComponent); * - * // 添加位置组件 - * entity.addComponent(new PositionComponent(100, 200)); - * - * // 添加子实体 - * const weapon = new Entity("Weapon", 2); - * entity.addChild(weapon); + * // 层级关系使用 HierarchySystem + * const hierarchySystem = scene.getSystem(HierarchySystem); + * hierarchySystem.setParent(childEntity, parentEntity); * ``` */ export class Entity { @@ -89,16 +88,6 @@ export class Entity { */ private _isDestroyed: boolean = false; - /** - * 父实体引用 - */ - private _parent: Entity | null = null; - - /** - * 子实体集合 - */ - private _children: Entity[] = []; - /** * 激活状态 */ @@ -201,32 +190,6 @@ export class Entity { this._componentCache = components; } - /** - * 获取父实体 - * @returns 父实体,如果没有父实体则返回null - */ - public get parent(): Entity | null { - return this._parent; - } - - /** - * 获取子实体数组的只读副本 - * - * @returns 子实体数组的副本 - */ - public get children(): readonly Entity[] { - return [...this._children]; - } - - /** - * 获取子实体数量 - * - * @returns 子实体的数量 - */ - public get childCount(): number { - return this._children.length; - } - /** * 获取活跃状态 * @@ -239,8 +202,6 @@ export class Entity { /** * 设置活跃状态 * - * 设置实体的活跃状态,会影响子实体的有效活跃状态。 - * * @param value - 新的活跃状态 */ public set active(value: boolean) { @@ -250,19 +211,6 @@ export class Entity { } } - /** - * 获取实体的有效活跃状态 - * - * 考虑父实体的活跃状态,只有当实体本身和所有父实体都处于活跃状态时才返回true。 - * - * @returns 有效的活跃状态 - */ - public get activeInHierarchy(): boolean { - if (!this._active) return false; - if (this._parent) return this._parent.activeInHierarchy; - return true; - } - /** * 获取实体标签 * @@ -690,185 +638,6 @@ export class Entity { return null; } - /** - * 添加子实体 - * - * @param child - 要添加的子实体 - * @returns 添加的子实体 - */ - public addChild(child: Entity): Entity { - if (child === this) { - throw new Error('Entity cannot be its own child'); - } - - if (child._parent === this) { - return child; // 已经是子实体 - } - - if (child._parent) { - child._parent.removeChild(child); - } - - child._parent = this; - this._children.push(child); - - if (!child.scene && this.scene) { - child.scene = this.scene; - this.scene.addEntity(child); - } - - return child; - } - - /** - * 移除子实体 - * - * @param child - 要移除的子实体 - * @returns 是否成功移除 - */ - public removeChild(child: Entity): boolean { - const index = this._children.indexOf(child); - if (index === -1) { - return false; - } - - this._children.splice(index, 1); - child._parent = null; - - return true; - } - - /** - * 移除所有子实体 - */ - public removeAllChildren(): void { - const childrenToRemove = [...this._children]; - - for (const child of childrenToRemove) { - this.removeChild(child); - } - } - - /** - * 根据名称查找子实体 - * - * @param name - 子实体名称 - * @param recursive - 是否递归查找 - * @returns 找到的子实体或null - */ - public findChild(name: string, recursive: boolean = false): Entity | null { - for (const child of this._children) { - if (child.name === name) { - return child; - } - } - - if (recursive) { - for (const child of this._children) { - const found = child.findChild(name, true); - if (found) { - return found; - } - } - } - - return null; - } - - /** - * 根据标签查找子实体 - * - * @param tag - 标签 - * @param recursive - 是否递归查找 - * @returns 找到的子实体数组 - */ - public findChildrenByTag(tag: number, recursive: boolean = false): Entity[] { - const result: Entity[] = []; - - for (const child of this._children) { - if (child.tag === tag) { - result.push(child); - } - } - - if (recursive) { - for (const child of this._children) { - result.push(...child.findChildrenByTag(tag, true)); - } - } - - return result; - } - - /** - * 获取根实体 - * - * @returns 层次结构的根实体 - */ - public getRoot(): Entity { - if (!this._parent) { - return this; - } - return this._parent.getRoot(); - } - - /** - * 检查是否是指定实体的祖先 - * - * @param entity - 要检查的实体 - * @returns 如果是祖先则返回true - */ - public isAncestorOf(entity: Entity): boolean { - let current = entity._parent; - while (current) { - if (current === this) { - return true; - } - current = current._parent; - } - return false; - } - - /** - * 检查是否是指定实体的后代 - * - * @param entity - 要检查的实体 - * @returns 如果是后代则返回true - */ - public isDescendantOf(entity: Entity): boolean { - return entity.isAncestorOf(this); - } - - /** - * 获取层次深度 - * - * @returns 在层次结构中的深度(根实体为0) - */ - public getDepth(): number { - let depth = 0; - let current = this._parent; - while (current) { - depth++; - current = current._parent; - } - return depth; - } - - /** - * 遍历所有子实体(深度优先) - * - * @param callback - 对每个子实体执行的回调函数 - * @param recursive - 是否递归遍历 - */ - public forEachChild(callback: (child: Entity, index: number) => void, recursive: boolean = false): void { - this._children.forEach((child, index) => { - callback(child, index); - if (recursive) { - child.forEachChild(callback, true); - } - }); - } - /** * 活跃状态改变时的回调 */ @@ -882,8 +651,7 @@ export class Entity { if (this.scene && this.scene.eventSystem) { this.scene.eventSystem.emitSync('entity:activeChanged', { entity: this, - active: this._active, - activeInHierarchy: this.activeInHierarchy + active: this._active }); } } @@ -891,7 +659,8 @@ export class Entity { /** * 销毁实体 * - * 移除所有组件、子实体并标记为已销毁 + * 移除所有组件并标记为已销毁。 + * 层级关系的清理由 HierarchySystem 处理。 */ public destroy(): void { if (this._isDestroyed) { @@ -905,15 +674,6 @@ export class Entity { this.scene.referenceTracker.unregisterEntityScene(this.id); } - const childrenToDestroy = [...this._children]; - for (const child of childrenToDestroy) { - child.destroy(); - } - - if (this._parent) { - this._parent.removeChild(this); - } - this.removeAllComponents(); if (this.scene) { @@ -927,43 +687,6 @@ export class Entity { } } - /** - * 批量销毁所有子实体 - */ - public destroyAllChildren(): void { - if (this._children.length === 0) return; - - const scene = this.scene; - const toDestroy: Entity[] = []; - - const collectChildren = (entity: Entity) => { - for (const child of entity._children) { - toDestroy.push(child); - collectChildren(child); - } - }; - collectChildren(this); - - for (const entity of toDestroy) { - entity.setDestroyedState(true); - } - - for (const entity of toDestroy) { - entity.removeAllComponents(); - } - - if (scene) { - for (const entity of toDestroy) { - scene.entities.remove(entity); - scene.querySystem.removeEntity(entity); - } - - scene.clearSystemEntityCaches(); - } - - this._children.length = 0; - } - /** * 比较实体 * @@ -993,31 +716,21 @@ export class Entity { id: number; enabled: boolean; active: boolean; - activeInHierarchy: boolean; destroyed: boolean; componentCount: number; componentTypes: string[]; componentMask: string; - parentId: number | null; - childCount: number; - childIds: number[]; - depth: number; cacheBuilt: boolean; - } { + } { return { name: this.name, id: this.id, enabled: this._enabled, active: this._active, - activeInHierarchy: this.activeInHierarchy, destroyed: this._isDestroyed, componentCount: this.components.length, componentTypes: this.components.map((c) => getComponentInstanceTypeName(c)), - componentMask: BitMask64Utils.toString(this._componentMask, 2), // 二进制表示 - parentId: this._parent?.id || null, - childCount: this._children.length, - childIds: this._children.map((c) => c.id), - depth: this.getDepth(), + componentMask: BitMask64Utils.toString(this._componentMask, 2), cacheBuilt: this._componentCache !== null }; } diff --git a/packages/core/src/ECS/EntityTags.ts b/packages/core/src/ECS/EntityTags.ts new file mode 100644 index 00000000..64179c59 --- /dev/null +++ b/packages/core/src/ECS/EntityTags.ts @@ -0,0 +1,95 @@ +/** + * 实体标签常量 + * + * 用于标识特殊类型的实体,如文件夹、摄像机等。 + * 使用位掩码实现,支持多标签组合。 + * + * @example + * ```typescript + * // 创建文件夹实体 + * entity.tag = EntityTags.FOLDER; + * + * // 检查是否是文件夹 + * if ((entity.tag & EntityTags.FOLDER) !== 0) { + * // 是文件夹 + * } + * + * // 组合多个标签 + * entity.tag = EntityTags.FOLDER | EntityTags.HIDDEN; + * ``` + */ +export const EntityTags = { + /** 无标签 */ + NONE: 0x0000, + + /** 文件夹实体 - 用于场景层级中的组织分类 */ + FOLDER: 0x1000, + + /** 隐藏实体 - 在编辑器层级中不显示 */ + HIDDEN: 0x2000, + + /** 锁定实体 - 在编辑器中不可选择/编辑 */ + LOCKED: 0x4000, + + /** 编辑器专用实体 - 仅在编辑器中存在,不导出到运行时 */ + EDITOR_ONLY: 0x8000, + + /** 预制件实例 */ + PREFAB_INSTANCE: 0x0100, + + /** 预制件根节点 */ + PREFAB_ROOT: 0x0200 +} as const; + +export type EntityTagValue = (typeof EntityTags)[keyof typeof EntityTags]; + +/** + * 检查实体是否具有指定标签 + * + * @param entityTag - 实体的 tag 值 + * @param tag - 要检查的标签 + */ +export function hasEntityTag(entityTag: number, tag: EntityTagValue): boolean { + return (entityTag & tag) !== 0; +} + +/** + * 添加标签到实体 + * + * @param entityTag - 当前 tag 值 + * @param tag - 要添加的标签 + */ +export function addEntityTag(entityTag: number, tag: EntityTagValue): number { + return entityTag | tag; +} + +/** + * 从实体移除标签 + * + * @param entityTag - 当前 tag 值 + * @param tag - 要移除的标签 + */ +export function removeEntityTag(entityTag: number, tag: EntityTagValue): number { + return entityTag & ~tag; +} + +/** + * 检查实体是否是文件夹 + */ +export function isFolder(entityTag: number): boolean { + return hasEntityTag(entityTag, EntityTags.FOLDER); +} + +/** + * 检查实体是否隐藏 + */ +export function isHidden(entityTag: number): boolean { + return hasEntityTag(entityTag, EntityTags.HIDDEN); +} + +/** + * 检查实体是否锁定 + */ +export function isLocked(entityTag: number): boolean { + return hasEntityTag(entityTag, EntityTags.LOCKED); +} diff --git a/packages/core/src/ECS/Scene.ts b/packages/core/src/ECS/Scene.ts index 8921f76c..8980eab9 100644 --- a/packages/core/src/ECS/Scene.ts +++ b/packages/core/src/ECS/Scene.ts @@ -21,6 +21,9 @@ import { } from './Serialization/IncrementalSerializer'; import { ComponentPoolManager } from './Core/ComponentPool'; import { PerformanceMonitor } from '../Utils/PerformanceMonitor'; +import { ProfilerSDK } from '../Utils/Profiler/ProfilerSDK'; +import { ProfileCategory } from '../Utils/Profiler/ProfilerTypes'; +import { AutoProfiler } from '../Utils/Profiler/AutoProfiler'; import { ServiceContainer, type ServiceType, type IService } from '../Core/ServiceContainer'; import { createInstance, isInjectable, injectProperties } from '../Core/DI'; import { createLogger } from '../Utils/Logger'; @@ -334,30 +337,58 @@ export class Scene implements IScene { * 更新场景 */ public update() { - ComponentPoolManager.getInstance().update(); + // 开始性能采样帧 + ProfilerSDK.beginFrame(); + const frameHandle = ProfilerSDK.beginSample('Scene.update', ProfileCategory.ECS); - this.entities.updateLists(); + try { + ComponentPoolManager.getInstance().update(); - const systems = this.systems; + this.entities.updateLists(); - for (const system of systems) { - if (system.enabled) { - try { - system.update(); - } catch (error) { - this._handleSystemError(system, 'update', error); + const systems = this.systems; + + // Update 阶段 + const updateHandle = ProfilerSDK.beginSample('Systems.update', ProfileCategory.ECS); + try { + for (const system of systems) { + if (system.enabled) { + const systemHandle = ProfilerSDK.beginSample(system.systemName, ProfileCategory.ECS); + try { + system.update(); + } catch (error) { + this._handleSystemError(system, 'update', error); + } finally { + ProfilerSDK.endSample(systemHandle); + } + } } + } finally { + ProfilerSDK.endSample(updateHandle); } - } - for (const system of systems) { - if (system.enabled) { - try { - system.lateUpdate(); - } catch (error) { - this._handleSystemError(system, 'lateUpdate', error); + // LateUpdate 阶段 + const lateUpdateHandle = ProfilerSDK.beginSample('Systems.lateUpdate', ProfileCategory.ECS); + try { + for (const system of systems) { + if (system.enabled) { + const systemHandle = ProfilerSDK.beginSample(`${system.systemName}.late`, ProfileCategory.ECS); + try { + system.lateUpdate(); + } catch (error) { + this._handleSystemError(system, 'lateUpdate', error); + } finally { + ProfilerSDK.endSample(systemHandle); + } + } } + } finally { + ProfilerSDK.endSample(lateUpdateHandle); } + } finally { + ProfilerSDK.endSample(frameHandle); + // 结束性能采样帧 + ProfilerSDK.endFrame(); } } @@ -693,6 +724,11 @@ export class Scene implements IScene { injectProperties(system, this._services); + // 调试模式下自动包装系统方法以收集性能数据(ProfilerSDK 启用时表示调试模式) + if (ProfilerSDK.isEnabled()) { + AutoProfiler.wrapInstance(system, system.systemName, ProfileCategory.ECS); + } + system.initialize(); this.logger.debug(`System ${constructor.name} registered and initialized`); diff --git a/packages/core/src/ECS/Serialization/EntitySerializer.ts b/packages/core/src/ECS/Serialization/EntitySerializer.ts index 46aafb5f..782993fd 100644 --- a/packages/core/src/ECS/Serialization/EntitySerializer.ts +++ b/packages/core/src/ECS/Serialization/EntitySerializer.ts @@ -8,6 +8,8 @@ import { Entity } from '../Entity'; import { ComponentType } from '../Core/ComponentStorage'; import { ComponentSerializer, SerializedComponent } from './ComponentSerializer'; import { IScene } from '../IScene'; +import { HierarchyComponent } from '../Components/HierarchyComponent'; +import { HierarchySystem } from '../Systems/HierarchySystem'; /** * 序列化后的实体数据 @@ -68,9 +70,14 @@ export class EntitySerializer { * * @param entity 要序列化的实体 * @param includeChildren 是否包含子实体(默认true) + * @param hierarchySystem 层级系统(可选,用于获取层级信息) * @returns 序列化后的实体数据 */ - public static serialize(entity: Entity, includeChildren: boolean = true): SerializedEntity { + public static serialize( + entity: Entity, + includeChildren: boolean = true, + hierarchySystem?: HierarchySystem + ): SerializedEntity { const serializedComponents = ComponentSerializer.serializeComponents( Array.from(entity.components) ); @@ -86,15 +93,24 @@ export class EntitySerializer { children: [] }; - // 序列化父实体引用 - if (entity.parent) { - serializedEntity.parentId = entity.parent.id; + // 通过 HierarchyComponent 获取层级信息 + const hierarchy = entity.getComponent(HierarchyComponent); + if (hierarchy?.parentId !== null && hierarchy?.parentId !== undefined) { + serializedEntity.parentId = hierarchy.parentId; } // 序列化子实体 - if (includeChildren) { - for (const child of entity.children) { - serializedEntity.children.push(this.serialize(child, true)); + // 直接使用 HierarchyComponent.childIds 获取子实体 + if (includeChildren && hierarchy && hierarchy.childIds.length > 0) { + // 获取场景引用:优先从 hierarchySystem,否则从 entity.scene + const scene = hierarchySystem?.scene ?? entity.scene; + if (scene) { + for (const childId of hierarchy.childIds) { + const child = scene.findEntityById(childId); + if (child) { + serializedEntity.children.push(this.serialize(child, true, hierarchySystem)); + } + } } } @@ -109,6 +125,7 @@ export class EntitySerializer { * @param idGenerator 实体ID生成器(用于生成新ID或保持原ID) * @param preserveIds 是否保持原始ID(默认false) * @param scene 目标场景(可选,用于设置entity.scene以支持添加组件) + * @param hierarchySystem 层级系统(可选,用于建立层级关系) * @returns 反序列化后的实体 */ public static deserialize( @@ -116,12 +133,17 @@ export class EntitySerializer { componentRegistry: Map, idGenerator: () => number, preserveIds: boolean = false, - scene?: IScene + scene?: IScene, + hierarchySystem?: HierarchySystem | null, + allEntities?: Map ): Entity { // 创建实体(使用原始ID或新生成的ID) const entityId = preserveIds ? serializedEntity.id : idGenerator(); const entity = new Entity(serializedEntity.name, entityId); + // 将实体添加到收集 Map 中(用于后续添加到场景) + allEntities?.set(entity.id, entity); + // 如果提供了scene,先设置entity.scene以支持添加组件 if (scene) { entity.scene = scene; @@ -143,16 +165,28 @@ export class EntitySerializer { entity.addComponent(component); } - // 反序列化子实体 + // 重要:清除 HierarchyComponent 中的旧 ID + // 当 preserveIds=false 时,序列化的 parentId 和 childIds 是旧 ID,需要重新建立 + // 通过 hierarchySystem.setParent 会正确设置新的 ID + const hierarchy = entity.getComponent(HierarchyComponent); + if (hierarchy) { + hierarchy.parentId = null; + hierarchy.childIds = []; + } + + // 反序列化子实体并建立层级关系 for (const childData of serializedEntity.children) { const childEntity = this.deserialize( childData, componentRegistry, idGenerator, preserveIds, - scene + scene, + hierarchySystem, + allEntities ); - entity.addChild(childEntity); + // 使用 HierarchySystem 建立层级关系 + hierarchySystem?.setParent(childEntity, entity); } return entity; @@ -163,19 +197,23 @@ export class EntitySerializer { * * @param entities 实体数组 * @param includeChildren 是否包含子实体 + * @param hierarchySystem 层级系统(可选,用于获取层级信息) * @returns 序列化后的实体数据数组 */ public static serializeEntities( entities: Entity[], - includeChildren: boolean = true + includeChildren: boolean = true, + hierarchySystem?: HierarchySystem ): SerializedEntity[] { const result: SerializedEntity[] = []; for (const entity of entities) { // 只序列化顶层实体(没有父实体的实体) // 子实体会在父实体序列化时一并处理 - if (!entity.parent || !includeChildren) { - result.push(this.serialize(entity, includeChildren)); + const hierarchy = entity.getComponent(HierarchyComponent); + const bHasParent = hierarchy?.parentId !== null && hierarchy?.parentId !== undefined; + if (!bHasParent || !includeChildren) { + result.push(this.serialize(entity, includeChildren, hierarchySystem)); } } @@ -190,6 +228,7 @@ export class EntitySerializer { * @param idGenerator 实体ID生成器 * @param preserveIds 是否保持原始ID * @param scene 目标场景(可选,用于设置entity.scene以支持添加组件) + * @param hierarchySystem 层级系统(可选,用于建立层级关系) * @returns 反序列化后的实体数组 */ public static deserializeEntities( @@ -197,9 +236,11 @@ export class EntitySerializer { componentRegistry: Map, idGenerator: () => number, preserveIds: boolean = false, - scene?: IScene - ): Entity[] { - const result: Entity[] = []; + scene?: IScene, + hierarchySystem?: HierarchySystem | null + ): { rootEntities: Entity[]; allEntities: Map } { + const rootEntities: Entity[] = []; + const allEntities = new Map(); for (const serialized of serializedEntities) { const entity = this.deserialize( @@ -207,12 +248,14 @@ export class EntitySerializer { componentRegistry, idGenerator, preserveIds, - scene + scene, + hierarchySystem, + allEntities ); - result.push(entity); + rootEntities.push(entity); } - return result; + return { rootEntities, allEntities }; } /** diff --git a/packages/core/src/ECS/Serialization/IncrementalSerializer.ts b/packages/core/src/ECS/Serialization/IncrementalSerializer.ts index b00d2ef7..2e6a21ba 100644 --- a/packages/core/src/ECS/Serialization/IncrementalSerializer.ts +++ b/packages/core/src/ECS/Serialization/IncrementalSerializer.ts @@ -11,6 +11,8 @@ import { ComponentSerializer, SerializedComponent } from './ComponentSerializer' import { SerializedEntity } from './EntitySerializer'; import { ComponentType } from '../Core/ComponentStorage'; import { BinarySerializer } from '../../Utils/BinarySerializer'; +import { HierarchyComponent } from '../Components/HierarchyComponent'; +import { HierarchySystem } from '../Systems/HierarchySystem'; /** * 变更操作类型 @@ -196,6 +198,10 @@ export class IncrementalSerializer { for (const entity of scene.entities.buffer) { snapshot.entityIds.add(entity.id); + // 获取层级信息 + const hierarchy = entity.getComponent(HierarchyComponent); + const parentId = hierarchy?.parentId; + // 存储实体基本信息 snapshot.entities.set(entity.id, { name: entity.name, @@ -203,7 +209,7 @@ export class IncrementalSerializer { active: entity.active, enabled: entity.enabled, updateOrder: entity.updateOrder, - ...(entity.parent && { parentId: entity.parent.id }) + ...(parentId !== null && parentId !== undefined && { parentId }) }); // 快照组件 @@ -272,6 +278,10 @@ export class IncrementalSerializer { for (const entity of scene.entities.buffer) { currentEntityIds.add(entity.id); + // 获取层级信息 + const hierarchy = entity.getComponent(HierarchyComponent); + const parentId = hierarchy?.parentId; + if (!baseSnapshot.entityIds.has(entity.id)) { // 新增实体 incremental.entityChanges.push({ @@ -285,7 +295,7 @@ export class IncrementalSerializer { active: entity.active, enabled: entity.enabled, updateOrder: entity.updateOrder, - ...(entity.parent && { parentId: entity.parent.id }), + ...(parentId !== null && parentId !== undefined && { parentId }), components: [], children: [] } @@ -312,7 +322,7 @@ export class IncrementalSerializer { oldData.active !== entity.active || oldData.enabled !== entity.enabled || oldData.updateOrder !== entity.updateOrder || - oldData.parentId !== entity.parent?.id; + oldData.parentId !== parentId; if (entityChanged) { incremental.entityChanges.push({ @@ -324,7 +334,7 @@ export class IncrementalSerializer { active: entity.active, enabled: entity.enabled, updateOrder: entity.updateOrder, - ...(entity.parent && { parentId: entity.parent.id }) + ...(parentId !== null && parentId !== undefined && { parentId }) } }); } @@ -539,16 +549,20 @@ export class IncrementalSerializer { if (change.entityData.enabled !== undefined) entity.enabled = change.entityData.enabled; if (change.entityData.updateOrder !== undefined) entity.updateOrder = change.entityData.updateOrder; - if (change.entityData.parentId !== undefined) { - const newParent = scene.entities.findEntityById(change.entityData.parentId); - if (newParent && entity.parent !== newParent) { - if (entity.parent) { - entity.parent.removeChild(entity); + // 使用 HierarchySystem 更新层级关系 + const hierarchySystem = scene.getSystem(HierarchySystem); + if (hierarchySystem) { + const hierarchy = entity.getComponent(HierarchyComponent); + const currentParentId = hierarchy?.parentId; + + if (change.entityData.parentId !== undefined) { + const newParent = scene.entities.findEntityById(change.entityData.parentId); + if (newParent && currentParentId !== change.entityData.parentId) { + hierarchySystem.setParent(entity, newParent); } - newParent.addChild(entity); + } else if (currentParentId !== null && currentParentId !== undefined) { + hierarchySystem.setParent(entity, null); } - } else if (entity.parent) { - entity.parent.removeChild(entity); } } diff --git a/packages/core/src/ECS/Serialization/SceneSerializer.ts b/packages/core/src/ECS/Serialization/SceneSerializer.ts index b476b130..2991ab5f 100644 --- a/packages/core/src/ECS/Serialization/SceneSerializer.ts +++ b/packages/core/src/ECS/Serialization/SceneSerializer.ts @@ -11,6 +11,8 @@ import { EntitySerializer, SerializedEntity } from './EntitySerializer'; import { getComponentTypeName } from '../Decorators'; import { getSerializationMetadata } from './SerializationDecorators'; import { BinarySerializer } from '../../Utils/BinarySerializer'; +import { HierarchySystem } from '../Systems/HierarchySystem'; +import { HierarchyComponent } from '../Components/HierarchyComponent'; /** * 场景序列化格式 @@ -167,8 +169,11 @@ export class SceneSerializer { // 过滤实体和组件 const entities = this.filterEntities(scene, opts); - // 序列化实体 - const serializedEntities = EntitySerializer.serializeEntities(entities, true); + // 获取层级系统用于序列化子实体 + const hierarchySystem = scene.getSystem(HierarchySystem); + + // 序列化实体(传入 hierarchySystem 以正确序列化子实体) + const serializedEntities = EntitySerializer.serializeEntities(entities, true, hierarchySystem ?? undefined); // 收集组件类型信息 const componentTypeRegistry = this.buildComponentTypeRegistry(entities); @@ -258,19 +263,24 @@ export class SceneSerializer { // ID生成器 const idGenerator = () => scene.identifierPool.checkOut(); + // 获取层级系统 + const hierarchySystem = scene.getSystem(HierarchySystem); + // 反序列化实体 - const entities = EntitySerializer.deserializeEntities( + const { rootEntities, allEntities } = EntitySerializer.deserializeEntities( serializedScene.entities, componentRegistry, idGenerator, opts.preserveIds || false, - scene + scene, + hierarchySystem ); - // 将实体添加到场景 - for (const entity of entities) { + // 将所有实体添加到场景(包括子实体) + // 先添加根实体,再递归添加子实体 + for (const entity of rootEntities) { scene.addEntity(entity, true); - this.addChildrenRecursively(entity, scene); + this.addChildrenRecursively(entity, scene, hierarchySystem, allEntities); } // 统一清理缓存(批量操作完成后) @@ -285,8 +295,8 @@ export class SceneSerializer { // 调用所有组件的 onDeserialized 生命周期方法 // Call onDeserialized lifecycle method on all components const deserializedPromises: Promise[] = []; - for (const entity of entities) { - this.callOnDeserializedRecursively(entity, deserializedPromises); + for (const entity of allEntities.values()) { + this.callOnDeserializedForEntity(entity, deserializedPromises); } // 如果有异步的 onDeserialized,在后台执行 @@ -298,9 +308,12 @@ export class SceneSerializer { } /** - * 递归调用实体及其子实体所有组件的 onDeserialized 方法 + * 调用实体所有组件的 onDeserialized 方法(不递归) */ - private static callOnDeserializedRecursively(entity: Entity, promises: Promise[]): void { + private static callOnDeserializedForEntity( + entity: Entity, + promises: Promise[] + ): void { for (const component of entity.components) { try { const result = component.onDeserialized(); @@ -311,10 +324,6 @@ export class SceneSerializer { console.error(`Error calling onDeserialized on component ${component.constructor.name}:`, error); } } - - for (const child of entity.children) { - this.callOnDeserializedRecursively(child, promises); - } } /** @@ -327,11 +336,27 @@ export class SceneSerializer { * * @param entity 父实体 * @param scene 目标场景 + * @param hierarchySystem 层级系统 */ - private static addChildrenRecursively(entity: Entity, scene: IScene): void { - for (const child of entity.children) { - scene.addEntity(child, true); // 延迟缓存清理 - this.addChildrenRecursively(child, scene); // 递归处理子实体的子实体 + private static addChildrenRecursively( + entity: Entity, + scene: IScene, + hierarchySystem?: HierarchySystem | null, + childEntitiesMap?: Map + ): void { + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy || hierarchy.childIds.length === 0) return; + + // 获取子实体 + // 注意:此时子实体还没有被添加到场景,所以不能用 scene.findEntityById + // 需要从 childEntitiesMap 中查找(如果提供了的话) + for (const childId of hierarchy.childIds) { + // 尝试从 map 中获取,否则从场景获取(用于已添加的情况) + const child = childEntitiesMap?.get(childId) ?? scene.findEntityById(childId); + if (child) { + scene.addEntity(child, true); // 延迟缓存清理 + this.addChildrenRecursively(child, scene, hierarchySystem, childEntitiesMap); + } } } diff --git a/packages/core/src/ECS/Systems/HierarchySystem.ts b/packages/core/src/ECS/Systems/HierarchySystem.ts new file mode 100644 index 00000000..e10cb872 --- /dev/null +++ b/packages/core/src/ECS/Systems/HierarchySystem.ts @@ -0,0 +1,549 @@ +import { Entity } from '../Entity'; +import { EntitySystem } from './EntitySystem'; +import { Matcher } from '../Utils/Matcher'; +import { HierarchyComponent } from '../Components/HierarchyComponent'; + +/** + * 层级关系系统 - 管理实体间的父子关系 + * + * 提供层级操作的统一 API,维护层级缓存(depth、activeInHierarchy)。 + * 所有层级操作应通过此系统进行,而非直接修改 HierarchyComponent。 + * + * @example + * ```typescript + * const hierarchySystem = scene.getSystem(HierarchySystem); + * + * // 设置父子关系 + * hierarchySystem.setParent(child, parent); + * + * // 查询层级 + * const parent = hierarchySystem.getParent(entity); + * const children = hierarchySystem.getChildren(entity); + * const depth = hierarchySystem.getDepth(entity); + * ``` + */ +export class HierarchySystem extends EntitySystem { + private static readonly MAX_DEPTH = 32; + + constructor() { + super(Matcher.empty().all(HierarchyComponent)); + } + + /** + * 系统优先级,确保在其他系统之前更新层级缓存 + */ + public override get updateOrder(): number { + return -1000; + } + + protected override process(): void { + // 更新所有脏缓存 + for (const entity of this.entities) { + const hierarchy = entity.getComponent(HierarchyComponent); + if (hierarchy?.bCacheDirty) { + this.updateHierarchyCache(entity); + } + } + } + + /** + * 设置实体的父级 + * + * @param child - 子实体 + * @param parent - 父实体,null 表示移动到根级 + */ + public setParent(child: Entity, parent: Entity | null): void { + let childHierarchy = child.getComponent(HierarchyComponent); + + // 如果子实体没有 HierarchyComponent,自动添加 + if (!childHierarchy) { + childHierarchy = new HierarchyComponent(); + child.addComponent(childHierarchy); + } + + // 检查是否需要变更 + const currentParentId = childHierarchy.parentId; + const newParentId = parent?.id ?? null; + if (currentParentId === newParentId) { + return; + } + + // 防止循环引用 + if (parent && this.isAncestorOf(child, parent)) { + throw new Error('Cannot set parent: would create circular reference'); + } + + // 从旧父级移除 + if (currentParentId !== null) { + const oldParent = this.scene?.findEntityById(currentParentId); + if (oldParent) { + const oldParentHierarchy = oldParent.getComponent(HierarchyComponent); + if (oldParentHierarchy) { + const idx = oldParentHierarchy.childIds.indexOf(child.id); + if (idx !== -1) { + oldParentHierarchy.childIds.splice(idx, 1); + } + } + } + } + + // 添加到新父级 + if (parent) { + let parentHierarchy = parent.getComponent(HierarchyComponent); + if (!parentHierarchy) { + parentHierarchy = new HierarchyComponent(); + parent.addComponent(parentHierarchy); + } + childHierarchy.parentId = parent.id; + parentHierarchy.childIds.push(child.id); + } else { + childHierarchy.parentId = null; + } + + // 标记缓存脏 + this.markCacheDirty(child); + } + + /** + * 在指定位置插入子实体 + * + * @param parent - 父实体 + * @param child - 子实体 + * @param index - 插入位置索引,-1 表示追加到末尾 + */ + public insertChildAt(parent: Entity, child: Entity, index: number): void { + let childHierarchy = child.getComponent(HierarchyComponent); + let parentHierarchy = parent.getComponent(HierarchyComponent); + + // 自动添加 HierarchyComponent + if (!childHierarchy) { + childHierarchy = new HierarchyComponent(); + child.addComponent(childHierarchy); + } + if (!parentHierarchy) { + parentHierarchy = new HierarchyComponent(); + parent.addComponent(parentHierarchy); + } + + // 防止循环引用 + if (this.isAncestorOf(child, parent)) { + throw new Error('Cannot set parent: would create circular reference'); + } + + // 从旧父级移除 + if (childHierarchy.parentId !== null && childHierarchy.parentId !== parent.id) { + const oldParent = this.scene?.findEntityById(childHierarchy.parentId); + if (oldParent) { + const oldParentHierarchy = oldParent.getComponent(HierarchyComponent); + if (oldParentHierarchy) { + const idx = oldParentHierarchy.childIds.indexOf(child.id); + if (idx !== -1) { + oldParentHierarchy.childIds.splice(idx, 1); + } + } + } + } + + // 设置新父级 + childHierarchy.parentId = parent.id; + + // 从当前父级的子列表中移除(如果已存在) + const existingIdx = parentHierarchy.childIds.indexOf(child.id); + if (existingIdx !== -1) { + parentHierarchy.childIds.splice(existingIdx, 1); + } + + // 插入到指定位置 + if (index < 0 || index >= parentHierarchy.childIds.length) { + parentHierarchy.childIds.push(child.id); + } else { + parentHierarchy.childIds.splice(index, 0, child.id); + } + + // 标记缓存脏 + this.markCacheDirty(child); + } + + /** + * 移除子实体(将其移动到根级) + */ + public removeChild(parent: Entity, child: Entity): boolean { + const parentHierarchy = parent.getComponent(HierarchyComponent); + const childHierarchy = child.getComponent(HierarchyComponent); + + if (!parentHierarchy || !childHierarchy) { + return false; + } + + if (childHierarchy.parentId !== parent.id) { + return false; + } + + const idx = parentHierarchy.childIds.indexOf(child.id); + if (idx !== -1) { + parentHierarchy.childIds.splice(idx, 1); + } + + childHierarchy.parentId = null; + this.markCacheDirty(child); + + return true; + } + + /** + * 移除所有子实体 + */ + public removeAllChildren(parent: Entity): void { + const parentHierarchy = parent.getComponent(HierarchyComponent); + if (!parentHierarchy) return; + + const childIds = [...parentHierarchy.childIds]; + for (const childId of childIds) { + const child = this.scene?.findEntityById(childId); + if (child) { + this.removeChild(parent, child); + } + } + } + + /** + * 获取实体的父级 + */ + public getParent(entity: Entity): Entity | null { + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy || hierarchy.parentId === null) { + return null; + } + return this.scene?.findEntityById(hierarchy.parentId) ?? null; + } + + /** + * 获取实体的子级列表 + */ + public getChildren(entity: Entity): Entity[] { + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy) return []; + + const children: Entity[] = []; + for (const childId of hierarchy.childIds) { + const child = this.scene?.findEntityById(childId); + if (child) { + children.push(child); + } + } + return children; + } + + /** + * 获取子级数量 + */ + public getChildCount(entity: Entity): number { + const hierarchy = entity.getComponent(HierarchyComponent); + return hierarchy?.childIds.length ?? 0; + } + + /** + * 检查实体是否有子级 + */ + public hasChildren(entity: Entity): boolean { + return this.getChildCount(entity) > 0; + } + + /** + * 检查 ancestor 是否是 entity 的祖先 + */ + public isAncestorOf(ancestor: Entity, entity: Entity): boolean { + let current = this.getParent(entity); + let depth = 0; + + while (current && depth < HierarchySystem.MAX_DEPTH) { + if (current.id === ancestor.id) { + return true; + } + current = this.getParent(current); + depth++; + } + + return false; + } + + /** + * 检查 descendant 是否是 entity 的后代 + */ + public isDescendantOf(descendant: Entity, entity: Entity): boolean { + return this.isAncestorOf(entity, descendant); + } + + /** + * 获取根实体 + */ + public getRoot(entity: Entity): Entity { + let current = entity; + let parent = this.getParent(current); + let depth = 0; + + while (parent && depth < HierarchySystem.MAX_DEPTH) { + current = parent; + parent = this.getParent(current); + depth++; + } + + return current; + } + + /** + * 获取实体在层级中的深度 + */ + public getDepth(entity: Entity): number { + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy) return 0; + + // 如果缓存有效,直接返回 + if (!hierarchy.bCacheDirty) { + return hierarchy.depth; + } + + // 重新计算 + let depth = 0; + let current = this.getParent(entity); + while (current && depth < HierarchySystem.MAX_DEPTH) { + depth++; + current = this.getParent(current); + } + + hierarchy.depth = depth; + return depth; + } + + /** + * 检查实体在层级中是否激活 + */ + public isActiveInHierarchy(entity: Entity): boolean { + if (!entity.active) return false; + + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy) return entity.active; + + // 如果缓存有效,直接返回 + if (!hierarchy.bCacheDirty) { + return hierarchy.bActiveInHierarchy; + } + + // 重新计算 + const parent = this.getParent(entity); + if (!parent) { + hierarchy.bActiveInHierarchy = entity.active; + } else { + hierarchy.bActiveInHierarchy = entity.active && this.isActiveInHierarchy(parent); + } + + return hierarchy.bActiveInHierarchy; + } + + /** + * 获取所有根实体(没有父级的实体) + */ + public getRootEntities(): Entity[] { + const roots: Entity[] = []; + for (const entity of this.entities) { + const hierarchy = entity.getComponent(HierarchyComponent); + if (hierarchy && hierarchy.parentId === null) { + roots.push(entity); + } + } + return roots; + } + + /** + * 根据名称查找子实体 + * + * @param entity - 父实体 + * @param name - 子实体名称 + * @param bRecursive - 是否递归查找 + */ + public findChild(entity: Entity, name: string, bRecursive: boolean = false): Entity | null { + const children = this.getChildren(entity); + + for (const child of children) { + if (child.name === name) { + return child; + } + } + + if (bRecursive) { + for (const child of children) { + const found = this.findChild(child, name, true); + if (found) { + return found; + } + } + } + + return null; + } + + /** + * 根据标签查找子实体 + * + * @param entity - 父实体 + * @param tag - 标签值 + * @param bRecursive - 是否递归查找 + */ + public findChildrenByTag(entity: Entity, tag: number, bRecursive: boolean = false): Entity[] { + const result: Entity[] = []; + const children = this.getChildren(entity); + + for (const child of children) { + if ((child.tag & tag) !== 0) { + result.push(child); + } + + if (bRecursive) { + result.push(...this.findChildrenByTag(child, tag, true)); + } + } + + return result; + } + + /** + * 遍历所有子级 + * + * @param entity - 父实体 + * @param callback - 回调函数 + * @param bRecursive - 是否递归遍历 + */ + public forEachChild( + entity: Entity, + callback: (child: Entity) => void, + bRecursive: boolean = false + ): void { + const children = this.getChildren(entity); + + for (const child of children) { + callback(child); + + if (bRecursive) { + this.forEachChild(child, callback, true); + } + } + } + + /** + * 扁平化层级树(用于虚拟化渲染) + * + * @param expandedIds - 展开的实体 ID 集合 + * @returns 扁平化的节点列表 + */ + public flattenHierarchy(expandedIds: Set): Array<{ + entity: Entity; + depth: number; + bHasChildren: boolean; + bIsExpanded: boolean; + }> { + const result: Array<{ + entity: Entity; + depth: number; + bHasChildren: boolean; + bIsExpanded: boolean; + }> = []; + + const traverse = (entity: Entity, depth: number): void => { + const bHasChildren = this.hasChildren(entity); + const bIsExpanded = expandedIds.has(entity.id); + + result.push({ + entity, + depth, + bHasChildren, + bIsExpanded + }); + + if (bHasChildren && bIsExpanded) { + for (const child of this.getChildren(entity)) { + traverse(child, depth + 1); + } + } + }; + + for (const root of this.getRootEntities()) { + traverse(root, 0); + } + + return result; + } + + /** + * 标记缓存为脏 + */ + private markCacheDirty(entity: Entity): void { + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy) return; + + hierarchy.bCacheDirty = true; + + // 递归标记所有子级 + for (const childId of hierarchy.childIds) { + const child = this.scene?.findEntityById(childId); + if (child) { + this.markCacheDirty(child); + } + } + } + + /** + * 更新层级缓存 + */ + private updateHierarchyCache(entity: Entity): void { + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy) return; + + // 计算深度 + hierarchy.depth = this.getDepth(entity); + + // 计算激活状态 + hierarchy.bActiveInHierarchy = this.isActiveInHierarchy(entity); + + // 标记缓存有效 + hierarchy.bCacheDirty = false; + } + + /** + * 当实体被移除时清理层级关系 + */ + protected onEntityRemoved(entity: Entity): void { + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy) return; + + // 从父级移除 + if (hierarchy.parentId !== null) { + const parent = this.scene?.findEntityById(hierarchy.parentId); + if (parent) { + const parentHierarchy = parent.getComponent(HierarchyComponent); + if (parentHierarchy) { + const idx = parentHierarchy.childIds.indexOf(entity.id); + if (idx !== -1) { + parentHierarchy.childIds.splice(idx, 1); + } + } + } + } + + // 处理子级:可选择销毁或移动到根级 + // 默认将子级移动到根级 + for (const childId of hierarchy.childIds) { + const child = this.scene?.findEntityById(childId); + if (child) { + const childHierarchy = child.getComponent(HierarchyComponent); + if (childHierarchy) { + childHierarchy.parentId = null; + this.markCacheDirty(child); + } + } + } + } + + public override dispose(): void { + // 清理资源 + } +} diff --git a/packages/core/src/ECS/Systems/index.ts b/packages/core/src/ECS/Systems/index.ts index 3548a3e3..dfa1cbce 100644 --- a/packages/core/src/ECS/Systems/index.ts +++ b/packages/core/src/ECS/Systems/index.ts @@ -4,6 +4,7 @@ export { ProcessingSystem } from './ProcessingSystem'; export { PassiveSystem } from './PassiveSystem'; export { IntervalSystem } from './IntervalSystem'; export { WorkerEntitySystem } from './WorkerEntitySystem'; +export { HierarchySystem } from './HierarchySystem'; // Worker系统相关类型导出 export type { diff --git a/packages/core/src/ECS/TypedEntity.ts b/packages/core/src/ECS/TypedEntity.ts index a61d3763..8913acc7 100644 --- a/packages/core/src/ECS/TypedEntity.ts +++ b/packages/core/src/ECS/TypedEntity.ts @@ -8,6 +8,7 @@ import { Entity } from './Entity'; import type { Component } from './Component'; import type { ComponentType } from './Core/ComponentStorage'; import type { ComponentConstructor, ComponentInstance } from '../Types/TypeHelpers'; +import { HierarchySystem } from './Systems/HierarchySystem'; /** * 获取组件,如果不存在则抛出错误 @@ -277,7 +278,8 @@ export class TypedEntityBuilder { * 添加子实体 */ withChild(child: Entity): this { - this._entity.addChild(child); + const hierarchySystem = this._entity.scene?.getSystem(HierarchySystem); + hierarchySystem?.setParent(child, this._entity); return this; } diff --git a/packages/core/src/ECS/index.ts b/packages/core/src/ECS/index.ts index 4c37f576..f5f8f31d 100644 --- a/packages/core/src/ECS/index.ts +++ b/packages/core/src/ECS/index.ts @@ -4,6 +4,7 @@ export { ECSEventType, EventPriority, EVENT_TYPES, EventTypeValidator } from './ export * from './Systems'; export * from './Utils'; export * from './Decorators'; +export * from './Components'; export { Scene } from './Scene'; export { IScene, ISceneFactory, ISceneConfig } from './IScene'; export { SceneManager } from './SceneManager'; @@ -18,3 +19,4 @@ export { ReferenceTracker, getSceneByEntityId } from './Core/ReferenceTracker'; export type { EntityRefRecord } from './Core/ReferenceTracker'; export { ReactiveQuery, ReactiveQueryChangeType } from './Core/ReactiveQuery'; export type { ReactiveQueryChange, ReactiveQueryListener, ReactiveQueryConfig } from './Core/ReactiveQuery'; +export * from './EntityTags'; diff --git a/packages/core/src/Types/index.ts b/packages/core/src/Types/index.ts index f196a6e1..d51a9338 100644 --- a/packages/core/src/Types/index.ts +++ b/packages/core/src/Types/index.ts @@ -264,8 +264,6 @@ export interface IECSDebugConfig { export interface ICoreConfig { /** 是否启用调试模式 */ debug?: boolean; - /** 是否启用实体系统 */ - enableEntitySystems?: boolean; /** 调试配置 */ debugConfig?: IECSDebugConfig; /** WorldManager配置 */ diff --git a/packages/core/src/Utils/Debug/AdvancedProfilerCollector.ts b/packages/core/src/Utils/Debug/AdvancedProfilerCollector.ts index cade8938..2eea6430 100644 --- a/packages/core/src/Utils/Debug/AdvancedProfilerCollector.ts +++ b/packages/core/src/Utils/Debug/AdvancedProfilerCollector.ts @@ -30,6 +30,24 @@ export interface ILegacyPerformanceMonitor { }>; } +/** + * 热点函数项(支持递归层级) + */ +export interface IHotspotItem { + name: string; + category: string; + inclusiveTime: number; + inclusiveTimePercent: number; + exclusiveTime: number; + exclusiveTimePercent: number; + callCount: number; + avgCallTime: number; + /** 层级深度 */ + depth: number; + /** 子函数 */ + children?: IHotspotItem[] | undefined; +} + /** * 高级性能数据接口 */ @@ -63,17 +81,8 @@ export interface IAdvancedProfilerData { percentOfFrame: number; }>; }>; - /** 热点函数列表 */ - hotspots: Array<{ - name: string; - category: string; - inclusiveTime: number; - inclusiveTimePercent: number; - exclusiveTime: number; - exclusiveTimePercent: number; - callCount: number; - avgCallTime: number; - }>; + /** 热点函数列表(支持层级) */ + hotspots: IHotspotItem[]; /** 调用关系数据 */ callGraph: { /** 当前选中的函数 */ @@ -332,16 +341,91 @@ export class AdvancedProfilerCollector { private buildHotspots(report: ProfileReport): IAdvancedProfilerData['hotspots'] { const totalTime = report.hotspots.reduce((sum, h) => sum + h.inclusiveTime, 0) || 1; - return report.hotspots.slice(0, 50).map(h => ({ - name: h.name, - category: h.category, - inclusiveTime: h.inclusiveTime, - inclusiveTimePercent: (h.inclusiveTime / totalTime) * 100, - exclusiveTime: h.exclusiveTime, - exclusiveTimePercent: (h.exclusiveTime / totalTime) * 100, - callCount: h.callCount, - avgCallTime: h.averageTime - })); + // 使用 callGraph 构建层级结构 + // 找出所有根节点(没有被任何函数调用的,或者是顶层函数) + const rootFunctions = new Set(); + const childFunctions = new Set(); + + for (const [name, node] of report.callGraph) { + // 如果没有调用者,或者调用者不在 hotspots 中,则是根节点 + if (node.callers.size === 0) { + rootFunctions.add(name); + } else { + // 检查是否所有调用者都在 hotspots 之外 + let hasParentInHotspots = false; + for (const callerName of node.callers.keys()) { + if (report.callGraph.has(callerName)) { + hasParentInHotspots = true; + childFunctions.add(name); + break; + } + } + if (!hasParentInHotspots) { + rootFunctions.add(name); + } + } + } + + // 递归构建层级热点数据 + const buildHotspotItem = ( + name: string, + depth: number, + visited: Set + ): IHotspotItem | null => { + if (visited.has(name)) return null; // 避免循环 + visited.add(name); + + const stats = report.hotspots.find(h => h.name === name); + const node = report.callGraph.get(name); + + if (!stats && !node) return null; + + const inclusiveTime = stats?.inclusiveTime || node?.totalTime || 0; + const exclusiveTime = stats?.exclusiveTime || inclusiveTime; + const callCount = stats?.callCount || node?.callCount || 1; + + // 构建子节点 + const children: IHotspotItem[] = []; + if (node && depth < 5) { // 限制深度避免过深 + for (const [calleeName] of node.callees) { + const child = buildHotspotItem(calleeName, depth + 1, visited); + if (child) { + children.push(child); + } + } + // 按耗时排序 + children.sort((a, b) => b.inclusiveTime - a.inclusiveTime); + } + + return { + name, + category: stats?.category || node?.category || ProfileCategory.Custom, + inclusiveTime, + inclusiveTimePercent: (inclusiveTime / totalTime) * 100, + exclusiveTime, + exclusiveTimePercent: (exclusiveTime / totalTime) * 100, + callCount, + avgCallTime: callCount > 0 ? inclusiveTime / callCount : 0, + depth, + children: children.length > 0 ? children : undefined + }; + }; + + // 构建根节点列表 + const result: IAdvancedProfilerData['hotspots'] = []; + const visited = new Set(); + + for (const rootName of rootFunctions) { + const item = buildHotspotItem(rootName, 0, visited); + if (item) { + result.push(item); + } + } + + // 按耗时排序 + result.sort((a, b) => b.inclusiveTime - a.inclusiveTime); + + return result.slice(0, 50); } private buildHotspotsFromLegacy( @@ -363,7 +447,8 @@ export class AdvancedProfilerCollector { exclusiveTime: execTime, exclusiveTimePercent: frameTime > 0 ? (execTime / frameTime) * 100 : 0, callCount: stats?.executionCount || 1, - avgCallTime: stats?.averageTime || execTime + avgCallTime: stats?.averageTime || execTime, + depth: 0 }); } @@ -388,15 +473,27 @@ export class AdvancedProfilerCollector { }; } + // 计算所有调用者的总调用次数(用于计算调用者的百分比) + let totalCallerCount = 0; + for (const data of node.callers.values()) { + totalCallerCount += data.count; + } + + // Calling Functions(谁调用了我) + // - totalTime: 该调用者调用当前函数时的平均耗时 + // - percentOfCurrent: 该调用者的调用次数占总调用次数的百分比 const callers = Array.from(node.callers.entries()) .map(([name, data]) => ({ name, callCount: data.count, totalTime: data.totalTime, - percentOfCurrent: node.totalTime > 0 ? (data.totalTime / node.totalTime) * 100 : 0 + percentOfCurrent: totalCallerCount > 0 ? (data.count / totalCallerCount) * 100 : 0 })) - .sort((a, b) => b.totalTime - a.totalTime); + .sort((a, b) => b.callCount - a.callCount); + // Called Functions(我调用了谁) + // - totalTime: 当前函数调用该被调用者时的平均耗时 + // - percentOfCurrent: 该被调用者的耗时占当前函数耗时的百分比 const callees = Array.from(node.callees.entries()) .map(([name, data]) => ({ name, diff --git a/packages/core/src/Utils/Debug/DebugManager.ts b/packages/core/src/Utils/Debug/DebugManager.ts index 70148320..afb0f25c 100644 --- a/packages/core/src/Utils/Debug/DebugManager.ts +++ b/packages/core/src/Utils/Debug/DebugManager.ts @@ -486,6 +486,9 @@ export class DebugManager implements IService, IUpdatable { const { functionName, requestId } = message; this.advancedProfilerCollector.setSelectedFunction(functionName || null); + // 立即发送更新的数据,无需等待下一帧 + this.sendDebugData(); + this.webSocketManager.send({ type: 'set_profiler_selected_function_response', requestId, @@ -995,10 +998,19 @@ export class DebugManager implements IService, IUpdatable { try { const debugData = this.getDebugData(); + + // 收集高级性能数据(包含 callGraph) + const isProfilerEnabled = ProfilerSDK.isEnabled(); + + const advancedProfilerData = isProfilerEnabled + ? this.advancedProfilerCollector.collectAdvancedData(this.performanceMonitor) + : null; + // 包装成调试面板期望的消息格式 const message = { type: 'debug_data', - data: debugData + data: debugData, + advancedProfiler: advancedProfilerData }; this.webSocketManager.send(message); } catch (error) { diff --git a/packages/core/src/Utils/Debug/EntityDataCollector.ts b/packages/core/src/Utils/Debug/EntityDataCollector.ts index 2b8d033a..75262d62 100644 --- a/packages/core/src/Utils/Debug/EntityDataCollector.ts +++ b/packages/core/src/Utils/Debug/EntityDataCollector.ts @@ -3,6 +3,8 @@ import { Entity } from '../../ECS/Entity'; import { Component } from '../../ECS/Component'; import { getComponentInstanceTypeName } from '../../ECS/Decorators'; import { IScene } from '../../ECS/IScene'; +import { HierarchyComponent } from '../../ECS/Components/HierarchyComponent'; +import { HierarchySystem } from '../../ECS/Systems/HierarchySystem'; /** * 实体数据收集器 @@ -75,20 +77,28 @@ export class EntityDataCollector { const entityList = (scene as any).entities; if (!entityList?.buffer) return []; - return entityList.buffer.map((entity: Entity) => ({ - id: entity.id, - name: entity.name || `Entity_${entity.id}`, - active: entity.active !== false, - enabled: entity.enabled !== false, - activeInHierarchy: entity.activeInHierarchy !== false, - componentCount: entity.components.length, - componentTypes: entity.components.map((component: Component) => getComponentInstanceTypeName(component)), - parentId: entity.parent?.id || null, - childIds: entity.children?.map((child: Entity) => child.id) || [], - depth: entity.getDepth ? entity.getDepth() : 0, - tag: entity.tag || 0, - updateOrder: entity.updateOrder || 0 - })); + const hierarchySystem = scene.getSystem(HierarchySystem); + + return entityList.buffer.map((entity: Entity) => { + const hierarchy = entity.getComponent(HierarchyComponent); + const bActiveInHierarchy = hierarchySystem?.isActiveInHierarchy(entity) ?? entity.active; + const depth = hierarchySystem?.getDepth(entity) ?? 0; + + return { + id: entity.id, + name: entity.name || `Entity_${entity.id}`, + active: entity.active !== false, + enabled: entity.enabled !== false, + activeInHierarchy: bActiveInHierarchy, + componentCount: entity.components.length, + componentTypes: entity.components.map((component: Component) => getComponentInstanceTypeName(component)), + parentId: hierarchy?.parentId ?? null, + childIds: hierarchy?.childIds ?? [], + depth, + tag: entity.tag || 0, + updateOrder: entity.updateOrder || 0 + }; + }); } /** @@ -200,7 +210,7 @@ export class EntityDataCollector { pendingRemove: stats.pendingRemove || 0, entitiesPerArchetype: archetypeData.distribution, topEntitiesByComponents: archetypeData.topEntities, - entityHierarchy: this.buildEntityHierarchyTree(entityList), + entityHierarchy: this.buildEntityHierarchyTree(entityList, scene), entityDetailsMap: this.buildEntityDetailsMap(entityList, scene) }; } @@ -534,7 +544,10 @@ export class EntityDataCollector { } } - private buildEntityHierarchyTree(entityList: { buffer?: Entity[] }): Array<{ + private buildEntityHierarchyTree( + entityList: { buffer?: Entity[] }, + scene?: IScene | null + ): Array<{ id: number; name: string; active: boolean; @@ -550,11 +563,14 @@ export class EntityDataCollector { }> { if (!entityList?.buffer) return []; + const hierarchySystem = scene?.getSystem(HierarchySystem); const rootEntities: any[] = []; entityList.buffer.forEach((entity: Entity) => { - if (!entity.parent) { - const hierarchyNode = this.buildEntityHierarchyNode(entity); + const hierarchy = entity.getComponent(HierarchyComponent); + const bHasNoParent = hierarchy?.parentId === null || hierarchy?.parentId === undefined; + if (bHasNoParent) { + const hierarchyNode = this.buildEntityHierarchyNode(entity, hierarchySystem); rootEntities.push(hierarchyNode); } }); @@ -572,25 +588,32 @@ export class EntityDataCollector { /** * 构建实体层次结构节点 */ - private buildEntityHierarchyNode(entity: Entity): any { + private buildEntityHierarchyNode(entity: Entity, hierarchySystem?: HierarchySystem | null): any { + const hierarchy = entity.getComponent(HierarchyComponent); + const bActiveInHierarchy = hierarchySystem?.isActiveInHierarchy(entity) ?? entity.active; + const depth = hierarchySystem?.getDepth(entity) ?? 0; + let node = { id: entity.id, name: entity.name || `Entity_${entity.id}`, active: entity.active !== false, enabled: entity.enabled !== false, - activeInHierarchy: entity.activeInHierarchy !== false, + activeInHierarchy: bActiveInHierarchy, componentCount: entity.components.length, componentTypes: entity.components.map((component: Component) => getComponentInstanceTypeName(component)), - parentId: entity.parent?.id || null, + parentId: hierarchy?.parentId ?? null, children: [] as any[], - depth: entity.getDepth ? entity.getDepth() : 0, + depth, tag: entity.tag || 0, updateOrder: entity.updateOrder || 0 }; // 递归构建子实体节点 - if (entity.children && entity.children.length > 0) { - node.children = entity.children.map((child: Entity) => this.buildEntityHierarchyNode(child)); + if (hierarchySystem) { + const children = hierarchySystem.getChildren(entity); + if (children.length > 0) { + node.children = children.map((child: Entity) => this.buildEntityHierarchyNode(child, hierarchySystem)); + } } // 优先使用Entity的getDebugInfo方法 @@ -616,6 +639,7 @@ export class EntityDataCollector { private buildEntityDetailsMap(entityList: { buffer?: Entity[] }, scene?: IScene | null): Record { if (!entityList?.buffer) return {}; + const hierarchySystem = scene?.getSystem(HierarchySystem); const entityDetailsMap: Record = {}; const entities = entityList.buffer; const batchSize = 100; @@ -626,7 +650,7 @@ export class EntityDataCollector { batch.forEach((entity: Entity) => { const baseDebugInfo = entity.getDebugInfo ? entity.getDebugInfo() - : this.buildFallbackEntityInfo(entity, scene); + : this.buildFallbackEntityInfo(entity, scene, hierarchySystem); const componentCacheStats = (entity as any).getComponentCacheStats ? (entity as any).getComponentCacheStats() @@ -634,9 +658,13 @@ export class EntityDataCollector { const componentDetails = this.extractComponentDetails(entity.components); + // 获取父实体名称 + const parent = hierarchySystem?.getParent(entity); + const parentName = parent?.name ?? null; + entityDetailsMap[entity.id] = { ...baseDebugInfo, - parentName: entity.parent?.name || null, + parentName, components: componentDetails, componentTypes: baseDebugInfo.componentTypes || componentDetails.map((comp) => comp.typeName), cachePerformance: componentCacheStats @@ -656,15 +684,22 @@ export class EntityDataCollector { /** * 构建实体基础信息 */ - private buildFallbackEntityInfo(entity: Entity, scene?: IScene | null): any { + private buildFallbackEntityInfo( + entity: Entity, + scene?: IScene | null, + hierarchySystem?: HierarchySystem | null + ): any { const sceneInfo = this.getSceneInfo(scene); + const hierarchy = entity.getComponent(HierarchyComponent); + const bActiveInHierarchy = hierarchySystem?.isActiveInHierarchy(entity) ?? entity.active; + const depth = hierarchySystem?.getDepth(entity) ?? 0; return { name: entity.name || `Entity_${entity.id}`, id: entity.id, enabled: entity.enabled !== false, active: entity.active !== false, - activeInHierarchy: entity.activeInHierarchy !== false, + activeInHierarchy: bActiveInHierarchy, destroyed: entity.isDestroyed || false, scene: sceneInfo.name, sceneName: sceneInfo.name, @@ -672,10 +707,10 @@ export class EntityDataCollector { componentCount: entity.components.length, componentTypes: entity.components.map((component: Component) => getComponentInstanceTypeName(component)), componentMask: entity.componentMask?.toString() || '0', - parentId: entity.parent?.id || null, - childCount: entity.children?.length || 0, - childIds: entity.children.map((child: Entity) => child.id) || [], - depth: entity.getDepth ? entity.getDepth() : 0, + parentId: hierarchy?.parentId ?? null, + childCount: hierarchy?.childIds?.length ?? 0, + childIds: hierarchy?.childIds ?? [], + depth, tag: entity.tag || 0, updateOrder: entity.updateOrder || 0 }; diff --git a/packages/core/src/Utils/Profiler/AutoProfiler.ts b/packages/core/src/Utils/Profiler/AutoProfiler.ts new file mode 100644 index 00000000..ee9cbcdc --- /dev/null +++ b/packages/core/src/Utils/Profiler/AutoProfiler.ts @@ -0,0 +1,649 @@ +/** + * 自动性能分析器 + * + * 提供自动函数包装和采样分析功能,无需手动埋点即可收集性能数据。 + * + * 支持三种分析模式: + * 1. 自动包装模式 - 使用 Proxy 自动包装类的所有方法 + * 2. 采样分析模式 - 定时采样调用栈(需要浏览器支持) + * 3. 装饰器模式 - 使用 @Profile() 装饰器手动标记方法 + */ + +import { ProfilerSDK } from './ProfilerSDK'; +import { ProfileCategory } from './ProfilerTypes'; + +/** + * 自动分析配置 + */ +export interface AutoProfilerConfig { + /** 是否启用自动包装 */ + enabled: boolean; + /** 采样间隔(毫秒),用于采样分析器 */ + sampleInterval: number; + /** 最小记录耗时(毫秒),低于此值的调用不记录 */ + minDuration: number; + /** 是否追踪异步方法 */ + trackAsync: boolean; + /** 排除的方法名模式 */ + excludePatterns: RegExp[]; + /** 最大采样缓冲区大小 */ + maxBufferSize: number; +} + +const DEFAULT_CONFIG: AutoProfilerConfig = { + enabled: true, + sampleInterval: 10, + minDuration: 0.1, // 0.1ms + trackAsync: true, + excludePatterns: [ + /^_/, // 私有方法 + /^get[A-Z]/, // getter 方法 + /^set[A-Z]/, // setter 方法 + /^is[A-Z]/, // 布尔检查方法 + /^has[A-Z]/, // 存在检查方法 + ], + maxBufferSize: 10000 +}; + +/** + * 采样数据 + */ +interface SampleData { + timestamp: number; + stack: string[]; + duration?: number; +} + +/** + * 包装信息 + */ +interface WrapInfo { + className: string; + methodName: string; + category: ProfileCategory; + original: Function; +} + +/** + * 自动性能分析器 + */ +export class AutoProfiler { + private static instance: AutoProfiler | null = null; + private config: AutoProfilerConfig; + private wrappedObjects: WeakMap> = new WeakMap(); + private samplingProfiler: SamplingProfiler | null = null; + private registeredClasses: Map = new Map(); + + private constructor(config?: Partial) { + this.config = { ...DEFAULT_CONFIG, ...config }; + } + + /** + * 获取单例实例 + */ + public static getInstance(config?: Partial): AutoProfiler { + if (!AutoProfiler.instance) { + AutoProfiler.instance = new AutoProfiler(config); + } + return AutoProfiler.instance; + } + + /** + * 重置实例 + */ + public static resetInstance(): void { + if (AutoProfiler.instance) { + AutoProfiler.instance.dispose(); + AutoProfiler.instance = null; + } + } + + /** + * 启用/禁用自动分析 + */ + public static setEnabled(enabled: boolean): void { + AutoProfiler.getInstance().setEnabled(enabled); + } + + /** + * 注册类以进行自动分析 + * 该类的所有实例方法都会被自动包装 + */ + public static registerClass any>( + constructor: T, + category: ProfileCategory = ProfileCategory.Custom, + className?: string + ): T { + return AutoProfiler.getInstance().registerClass(constructor, category, className); + } + + /** + * 包装对象实例的所有方法 + */ + public static wrapInstance( + instance: T, + className: string, + category: ProfileCategory = ProfileCategory.Custom + ): T { + return AutoProfiler.getInstance().wrapInstance(instance, className, category); + } + + /** + * 包装单个函数 + */ + public static wrapFunction any>( + fn: T, + name: string, + category: ProfileCategory = ProfileCategory.Custom + ): T { + return AutoProfiler.getInstance().wrapFunction(fn, name, category); + } + + /** + * 启动采样分析器 + */ + public static startSampling(): void { + AutoProfiler.getInstance().startSampling(); + } + + /** + * 停止采样分析器 + */ + public static stopSampling(): SampleData[] { + return AutoProfiler.getInstance().stopSampling(); + } + + /** + * 设置启用状态 + */ + public setEnabled(enabled: boolean): void { + this.config.enabled = enabled; + if (!enabled && this.samplingProfiler) { + this.samplingProfiler.stop(); + } + } + + /** + * 注册类以进行自动分析 + */ + public registerClass any>( + constructor: T, + category: ProfileCategory = ProfileCategory.Custom, + className?: string + ): T { + const name = className || constructor.name; + this.registeredClasses.set(name, { constructor, category }); + + // eslint-disable-next-line @typescript-eslint/no-this-alias -- Required for Proxy construct handler + const self = this; + + // 创建代理类 + const ProxiedClass = new Proxy(constructor, { + construct(target, args, newTarget) { + const instance = Reflect.construct(target, args, newTarget); + if (self.config.enabled) { + self.wrapInstance(instance, name, category); + } + return instance; + } + }); + + return ProxiedClass; + } + + /** + * 包装对象实例的所有方法 + */ + public wrapInstance( + instance: T, + className: string, + category: ProfileCategory = ProfileCategory.Custom + ): T { + if (!this.config.enabled) { + return instance; + } + + // 检查是否已经包装过 + if (this.wrappedObjects.has(instance)) { + return instance; + } + + const wrapInfoMap = new Map(); + this.wrappedObjects.set(instance, wrapInfoMap); + + // 获取所有方法(包括原型链上的) + const methodNames = this.getAllMethodNames(instance); + + for (const methodName of methodNames) { + if (this.shouldExcludeMethod(methodName)) { + continue; + } + + const descriptor = this.getPropertyDescriptor(instance, methodName); + if (!descriptor || typeof descriptor.value !== 'function') { + continue; + } + + const original = descriptor.value as Function; + const wrapped = this.createWrappedMethod(original, className, methodName, category); + + wrapInfoMap.set(methodName, { + className, + methodName, + category, + original + }); + + try { + (instance as any)[methodName] = wrapped; + } catch { + // 某些属性可能是只读的 + } + } + + return instance; + } + + /** + * 包装单个函数 + */ + public wrapFunction any>( + fn: T, + name: string, + category: ProfileCategory = ProfileCategory.Custom + ): T { + if (!this.config.enabled) return fn; + + // eslint-disable-next-line @typescript-eslint/no-this-alias -- Required for wrapped function closure + const self = this; + + const wrapped = function(this: any, ...args: any[]): any { + const handle = ProfilerSDK.beginSample(name, category); + try { + const result = fn.apply(this, args); + + // 处理 Promise + if (self.config.trackAsync && result instanceof Promise) { + return result.finally(() => { + ProfilerSDK.endSample(handle); + }); + } + + // 同步函数,立即结束采样 + ProfilerSDK.endSample(handle); + return result; + } catch (error) { + // 发生错误时也要结束采样 + ProfilerSDK.endSample(handle); + throw error; + } + } as T; + + // 保留原函数的属性 + Object.defineProperty(wrapped, 'name', { value: fn.name || name }); + Object.defineProperty(wrapped, 'length', { value: fn.length }); + + return wrapped; + } + + /** + * 启动采样分析器 + */ + public startSampling(): void { + if (!this.samplingProfiler) { + this.samplingProfiler = new SamplingProfiler(this.config); + } + this.samplingProfiler.start(); + } + + /** + * 停止采样分析器 + */ + public stopSampling(): SampleData[] { + if (!this.samplingProfiler) { + return []; + } + return this.samplingProfiler.stop(); + } + + /** + * 释放资源 + */ + public dispose(): void { + if (this.samplingProfiler) { + this.samplingProfiler.stop(); + this.samplingProfiler = null; + } + this.registeredClasses.clear(); + } + + /** + * 创建包装后的方法 + */ + private createWrappedMethod( + original: Function, + className: string, + methodName: string, + category: ProfileCategory + ): Function { + // eslint-disable-next-line @typescript-eslint/no-this-alias -- Required for wrapped method closure + const self = this; + const fullName = `${className}.${methodName}`; + const minDuration = this.config.minDuration; + + return function(this: any, ...args: any[]): any { + if (!self.config.enabled || !ProfilerSDK.isEnabled()) { + return original.apply(this, args); + } + + const startTime = performance.now(); + const handle = ProfilerSDK.beginSample(fullName, category); + + try { + const result = original.apply(this, args); + + // 处理异步方法 + if (self.config.trackAsync && result instanceof Promise) { + return result.then( + (value) => { + const duration = performance.now() - startTime; + if (duration >= minDuration) { + ProfilerSDK.endSample(handle); + } + return value; + }, + (error) => { + ProfilerSDK.endSample(handle); + throw error; + } + ); + } + + // 同步方法,检查最小耗时后结束采样 + const duration = performance.now() - startTime; + if (duration >= minDuration) { + ProfilerSDK.endSample(handle); + } + return result; + } catch (error) { + // 发生错误时也要结束采样 + ProfilerSDK.endSample(handle); + throw error; + } + }; + } + + /** + * 获取对象的所有方法名 + */ + private getAllMethodNames(obj: object): string[] { + const methods = new Set(); + let current = obj; + + while (current && current !== Object.prototype) { + for (const name of Object.getOwnPropertyNames(current)) { + if (name !== 'constructor') { + methods.add(name); + } + } + current = Object.getPrototypeOf(current); + } + + return Array.from(methods); + } + + /** + * 获取属性描述符 + */ + private getPropertyDescriptor(obj: object, name: string): PropertyDescriptor | undefined { + let current = obj; + while (current && current !== Object.prototype) { + const descriptor = Object.getOwnPropertyDescriptor(current, name); + if (descriptor) return descriptor; + current = Object.getPrototypeOf(current); + } + return undefined; + } + + /** + * 判断是否应该排除该方法 + */ + private shouldExcludeMethod(methodName: string): boolean { + // 排除构造函数和内置方法 + if (methodName === 'constructor' || methodName.startsWith('__')) { + return true; + } + + // 检查排除模式 + for (const pattern of this.config.excludePatterns) { + if (pattern.test(methodName)) { + return true; + } + } + + return false; + } +} + +/** + * 采样分析器 + * 使用定时器定期采样调用栈信息 + */ +class SamplingProfiler { + private config: AutoProfilerConfig; + private samples: SampleData[] = []; + private intervalId: number | null = null; + private isRunning = false; + + constructor(config: AutoProfilerConfig) { + this.config = config; + } + + /** + * 开始采样 + */ + public start(): void { + if (this.isRunning) return; + + this.isRunning = true; + this.samples = []; + + // 使用 requestAnimationFrame 或 setInterval 进行采样 + const sample = () => { + if (!this.isRunning) return; + + const stack = this.captureStack(); + if (stack.length > 0) { + this.samples.push({ + timestamp: performance.now(), + stack + }); + + // 限制缓冲区大小 + if (this.samples.length > this.config.maxBufferSize) { + this.samples.shift(); + } + } + + // 继续采样 + if (this.config.sampleInterval < 16) { + // 高频采样使用 setTimeout + this.intervalId = setTimeout(sample, this.config.sampleInterval) as any; + } else { + this.intervalId = setTimeout(sample, this.config.sampleInterval) as any; + } + }; + + sample(); + } + + /** + * 停止采样并返回数据 + */ + public stop(): SampleData[] { + this.isRunning = false; + if (this.intervalId !== null) { + clearTimeout(this.intervalId); + this.intervalId = null; + } + return [...this.samples]; + } + + /** + * 捕获当前调用栈 + */ + private captureStack(): string[] { + try { + // 创建 Error 对象获取调用栈 + const error = new Error(); + const stack = error.stack || ''; + + // 解析调用栈 + const lines = stack.split('\n').slice(3); // 跳过 Error 和 captureStack/sample + const frames: string[] = []; + + for (const line of lines) { + const frame = this.parseStackFrame(line); + if (frame && !this.isInternalFrame(frame)) { + frames.push(frame); + } + } + + return frames; + } catch { + return []; + } + } + + /** + * 解析调用栈帧 + */ + private parseStackFrame(line: string): string | null { + // Chrome/Edge 格式: " at functionName (file:line:col)" + // Firefox 格式: "functionName@file:line:col" + // Safari 格式: "functionName@file:line:col" + + line = line.trim(); + + // Chrome 格式 + let match = line.match(/at\s+(.+?)\s+\(/); + if (match && match[1]) { + return match[1]; + } + + // Chrome 匿名函数格式 + match = line.match(/at\s+(.+)/); + if (match && match[1]) { + const name = match[1]; + if (!name.includes('(')) { + return name; + } + } + + // Firefox/Safari 格式 + match = line.match(/^(.+?)@/); + if (match && match[1]) { + return match[1]; + } + + return null; + } + + /** + * 判断是否是内部帧(应该过滤掉) + */ + private isInternalFrame(frame: string): boolean { + const internalPatterns = [ + 'SamplingProfiler', + 'AutoProfiler', + 'ProfilerSDK', + 'setTimeout', + 'setInterval', + 'requestAnimationFrame', + '', + 'eval' + ]; + + return internalPatterns.some(pattern => frame.includes(pattern)); + } +} + +/** + * @Profile 装饰器 + * 用于标记需要性能分析的方法 + * + * @example + * ```typescript + * class MySystem extends System { + * @Profile() + * update() { + * // 方法执行时间会被自动记录 + * } + * + * @Profile('customName', ProfileCategory.Physics) + * calculatePhysics() { + * // 使用自定义名称和分类 + * } + * } + * ``` + */ +export function Profile( + name?: string, + category: ProfileCategory = ProfileCategory.Custom +): MethodDecorator { + return function( + target: object, + propertyKey: string | symbol, + descriptor: PropertyDescriptor + ): PropertyDescriptor { + const original = descriptor.value; + const methodName = name || `${target.constructor.name}.${String(propertyKey)}`; + + descriptor.value = function(this: any, ...args: any[]): any { + if (!ProfilerSDK.isEnabled()) { + return original.apply(this, args); + } + + const handle = ProfilerSDK.beginSample(methodName, category); + try { + const result = original.apply(this, args); + + // 处理异步方法 + if (result instanceof Promise) { + return result.finally(() => { + ProfilerSDK.endSample(handle); + }); + } + + // 同步方法,立即结束采样 + ProfilerSDK.endSample(handle); + return result; + } catch (error) { + // 发生错误时也要结束采样 + ProfilerSDK.endSample(handle); + throw error; + } + }; + + return descriptor; + }; +} + +/** + * @ProfileClass 装饰器 + * 用于自动包装类的所有方法 + * + * @example + * ```typescript + * @ProfileClass(ProfileCategory.Physics) + * class PhysicsSystem extends System { + * update() { ... } // 自动被包装 + * calculate() { ... } // 自动被包装 + * } + * ``` + */ +export function ProfileClass(category: ProfileCategory = ProfileCategory.Custom): ClassDecorator { + return function(constructor: T): T { + return AutoProfiler.registerClass(constructor as any, category) as any; + }; +} diff --git a/packages/core/src/Utils/Profiler/ProfilerSDK.ts b/packages/core/src/Utils/Profiler/ProfilerSDK.ts index fd14e226..e528ada3 100644 --- a/packages/core/src/Utils/Profiler/ProfilerSDK.ts +++ b/packages/core/src/Utils/Profiler/ProfilerSDK.ts @@ -228,6 +228,9 @@ export class ProfilerSDK { const endTime = performance.now(); const duration = endTime - handle.startTime; + // 获取父级 handle(在删除当前 handle 之前) + const parentHandle = handle.parentId ? this.activeSamples.get(handle.parentId) : undefined; + const sample: ProfileSample = { id: handle.id, name: handle.name, @@ -237,6 +240,7 @@ export class ProfilerSDK { duration, selfTime: duration, parentId: handle.parentId, + parentName: parentHandle?.name, depth: handle.depth, callCount: 1 }; @@ -245,7 +249,7 @@ export class ProfilerSDK { this.currentFrame.samples.push(sample); } - this.updateCallGraph(handle.name, handle.category, duration, handle.parentId); + this.updateCallGraph(handle.name, handle.category, duration, parentHandle); this.activeSamples.delete(handle.id); const stackIndex = this.sampleStack.indexOf(handle); @@ -437,6 +441,9 @@ export class ProfilerSDK { const categoryBreakdown = this.aggregateCategoryStats(frames); + // 根据帧历史重新计算 callGraph(不使用全局累积的数据) + const callGraph = this.buildCallGraphFromFrames(frames); + const firstFrame = frames[0]; const lastFrame = frames[frames.length - 1]; @@ -450,13 +457,106 @@ export class ProfilerSDK { p95FrameTime: sortedTimes[Math.floor(sortedTimes.length * 0.95)] || 0, p99FrameTime: sortedTimes[Math.floor(sortedTimes.length * 0.99)] || 0, hotspots, - callGraph: new Map(this.callGraph), + callGraph, categoryBreakdown, memoryTrend: frames.map((f) => f.memory), longTasks: [...this.longTasks] }; } + /** + * 从帧历史构建调用图 + * 注意:totalTime 存储的是平均耗时(总耗时/调用次数),而不是累计总耗时 + */ + private buildCallGraphFromFrames(frames: ProfileFrame[]): Map { + // 临时存储累计数据 + const tempData = new Map; + callees: Map; + }>(); + + for (const frame of frames) { + for (const sample of frame.samples) { + // 获取或创建当前函数的节点 + let node = tempData.get(sample.name); + if (!node) { + node = { + category: sample.category, + callCount: 0, + totalTime: 0, + callers: new Map(), + callees: new Map() + }; + tempData.set(sample.name, node); + } + + node.callCount++; + node.totalTime += sample.duration; + + // 如果有父级,建立调用关系 + if (sample.parentName) { + // 记录当前函数被谁调用 + const callerData = node.callers.get(sample.parentName) || { count: 0, totalTime: 0 }; + callerData.count++; + callerData.totalTime += sample.duration; + node.callers.set(sample.parentName, callerData); + + // 确保父节点存在并记录它调用了谁 + let parentNode = tempData.get(sample.parentName); + if (!parentNode) { + parentNode = { + category: sample.category, + callCount: 0, + totalTime: 0, + callers: new Map(), + callees: new Map() + }; + tempData.set(sample.parentName, parentNode); + } + + const calleeData = parentNode.callees.get(sample.name) || { count: 0, totalTime: 0 }; + calleeData.count++; + calleeData.totalTime += sample.duration; + parentNode.callees.set(sample.name, calleeData); + } + } + } + + // 转换为最终结果,将 totalTime 改为平均耗时 + const callGraph = new Map(); + for (const [name, data] of tempData) { + const avgCallers = new Map(); + for (const [callerName, callerData] of data.callers) { + avgCallers.set(callerName, { + count: callerData.count, + totalTime: callerData.count > 0 ? callerData.totalTime / callerData.count : 0 + }); + } + + const avgCallees = new Map(); + for (const [calleeName, calleeData] of data.callees) { + avgCallees.set(calleeName, { + count: calleeData.count, + totalTime: calleeData.count > 0 ? calleeData.totalTime / calleeData.count : 0 + }); + } + + callGraph.set(name, { + name, + category: data.category, + callCount: data.callCount, + totalTime: data.callCount > 0 ? data.totalTime / data.callCount : 0, + callers: avgCallers, + callees: avgCallees + }); + } + + return callGraph; + } + /** * 获取调用图数据 */ @@ -631,7 +731,7 @@ export class ProfilerSDK { name: string, category: ProfileCategory, duration: number, - parentId?: string + parentHandle?: SampleHandle ): void { let node = this.callGraph.get(name); if (!node) { @@ -649,22 +749,33 @@ export class ProfilerSDK { node.callCount++; node.totalTime += duration; - if (parentId) { - const parentHandle = this.activeSamples.get(parentId); - if (parentHandle) { - const callerData = node.callers.get(parentHandle.name) || { count: 0, totalTime: 0 }; - callerData.count++; - callerData.totalTime += duration; - node.callers.set(parentHandle.name, callerData); + // 如果有父级,建立调用关系 + if (parentHandle) { + // 记录当前函数被谁调用 + const callerData = node.callers.get(parentHandle.name) || { count: 0, totalTime: 0 }; + callerData.count++; + callerData.totalTime += duration; + node.callers.set(parentHandle.name, callerData); - const parentNode = this.callGraph.get(parentHandle.name); - if (parentNode) { - const calleeData = parentNode.callees.get(name) || { count: 0, totalTime: 0 }; - calleeData.count++; - calleeData.totalTime += duration; - parentNode.callees.set(name, calleeData); - } + // 确保父节点存在 + let parentNode = this.callGraph.get(parentHandle.name); + if (!parentNode) { + parentNode = { + name: parentHandle.name, + category: parentHandle.category, + callCount: 0, + totalTime: 0, + callers: new Map(), + callees: new Map() + }; + this.callGraph.set(parentHandle.name, parentNode); } + + // 记录父函数调用了谁 + const calleeData = parentNode.callees.get(name) || { count: 0, totalTime: 0 }; + calleeData.count++; + calleeData.totalTime += duration; + parentNode.callees.set(name, calleeData); } } diff --git a/packages/core/src/Utils/Profiler/ProfilerTypes.ts b/packages/core/src/Utils/Profiler/ProfilerTypes.ts index 1397de76..d2b26773 100644 --- a/packages/core/src/Utils/Profiler/ProfilerTypes.ts +++ b/packages/core/src/Utils/Profiler/ProfilerTypes.ts @@ -56,6 +56,8 @@ export interface ProfileSample { duration: number; selfTime: number; parentId?: string | undefined; + /** 父级采样的名称(用于构建调用图) */ + parentName?: string | undefined; depth: number; callCount: number; metadata?: Record; diff --git a/packages/core/src/Utils/Profiler/index.ts b/packages/core/src/Utils/Profiler/index.ts index db530c1d..003d568c 100644 --- a/packages/core/src/Utils/Profiler/index.ts +++ b/packages/core/src/Utils/Profiler/index.ts @@ -4,3 +4,5 @@ export * from './ProfilerTypes'; export { ProfilerSDK } from './ProfilerSDK'; +export { AutoProfiler, Profile, ProfileClass } from './AutoProfiler'; +export type { AutoProfilerConfig } from './AutoProfiler'; diff --git a/packages/core/tests/Core.test.ts b/packages/core/tests/Core.test.ts deleted file mode 100644 index 1234e7df..00000000 --- a/packages/core/tests/Core.test.ts +++ /dev/null @@ -1,465 +0,0 @@ -import { Core } from '../src/Core'; -import { Scene } from '../src/ECS/Scene'; -import { SceneManager } from '../src/ECS/SceneManager'; -import { Entity } from '../src/ECS/Entity'; -import { Component } from '../src/ECS/Component'; -import { ITimer } from '../src/Utils/Timers/ITimer'; -import { Updatable } from '../src/Core/DI'; -import type { IService } from '../src/Core/ServiceContainer'; -import type { IUpdatable } from '../src/Types/IUpdatable'; - -// 测试组件 -class TestComponent extends Component { - public value: number; - - constructor(...args: unknown[]) { - super(); - const [value = 0] = args as [number?]; - this.value = value; - } -} - -// 测试场景 -class TestScene extends Scene { - public initializeCalled = false; - public beginCalled = false; - public endCalled = false; - public updateCallCount = 0; - - public override initialize(): void { - this.initializeCalled = true; - } - - public override begin(): void { - this.beginCalled = true; - } - - public override end(): void { - this.endCalled = true; - } - - public override update(): void { - this.updateCallCount++; - } -} - -// 测试可更新服务 -@Updatable() -class TestUpdatableService implements IService, IUpdatable { - public updateCallCount = 0; - - public update(): void { - this.updateCallCount++; - } - - public dispose(): void { - // 清理资源 - } -} - -describe('Core - 核心管理系统测试', () => { - let originalConsoleWarn: typeof console.warn; - - beforeEach(() => { - // 清除之前的实例 - (Core as any)._instance = null; - - // 注意:WorldManager不再是单例,无需reset - - // 模拟console.warn以避免测试输出 - originalConsoleWarn = console.warn; - console.warn = jest.fn(); - }); - - afterEach(() => { - // 恢复console.warn - console.warn = originalConsoleWarn; - - // 清理Core实例 - if (Core.Instance) { - Core.destroy(); - } - }); - - describe('实例创建和管理', () => { - test('应该能够创建Core实例', () => { - const core = Core.create(true); - - expect(core).toBeDefined(); - expect(core).toBeInstanceOf(Core); - expect(core.debug).toBe(true); - expect(Core.entitySystemsEnabled).toBe(true); - }); - - test('应该能够通过配置对象创建Core实例', () => { - const config = { - debug: false, - enableEntitySystems: false - }; - - const core = Core.create(config); - - expect(core.debug).toBe(false); - expect(Core.entitySystemsEnabled).toBe(false); - }); - - test('重复调用create应该返回同一个实例', () => { - const core1 = Core.create(true); - const core2 = Core.create(false); // 不同参数 - - expect(core1).toBe(core2); - }); - - test('应该能够获取Core实例', () => { - const core = Core.create(true); - const instance = Core.Instance; - - expect(instance).toBe(core); - }); - - test('在未创建实例时调用update应该显示警告', () => { - Core.update(0.016); - - expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("Core实例未创建,请先调用Core.create()")); - }); - }); - - // 注意:场景管理功能已移至SceneManager - // 相关测试请查看 SceneManager.test.ts - - describe('更新循环 - 可更新服务', () => { - let core: Core; - let updatableService: TestUpdatableService; - - beforeEach(() => { - core = Core.create(true); - updatableService = new TestUpdatableService(); - Core.services.registerInstance(TestUpdatableService, updatableService); - }); - - test('应该能够更新可更新服务', () => { - Core.update(0.016); - expect(updatableService.updateCallCount).toBe(1); - }); - - test('暂停状态下不应该执行更新', () => { - Core.paused = true; - Core.update(0.016); - expect(updatableService.updateCallCount).toBe(0); - - // 恢复状态 - Core.paused = false; - }); - - test('多次更新应该累积调用', () => { - Core.update(0.016); - Core.update(0.016); - Core.update(0.016); - expect(updatableService.updateCallCount).toBe(3); - }); - }); - - describe('服务容器集成', () => { - let core: Core; - let service1: TestUpdatableService; - - beforeEach(() => { - core = Core.create(true); - service1 = new TestUpdatableService(); - }); - - test('应该能够通过ServiceContainer注册可更新服务', () => { - Core.services.registerInstance(TestUpdatableService, service1); - - // 测试更新是否被调用 - Core.update(0.016); - expect(service1.updateCallCount).toBe(1); - }); - - test('应该能够注销服务', () => { - Core.services.registerInstance(TestUpdatableService, service1); - Core.services.unregister(TestUpdatableService); - - // 测试更新不应该被调用 - Core.update(0.016); - expect(service1.updateCallCount).toBe(0); - }); - - test('应该能够通过ServiceContainer解析服务', () => { - Core.services.registerInstance(TestUpdatableService, service1); - - const retrieved = Core.services.resolve(TestUpdatableService); - expect(retrieved).toBe(service1); - }); - - test('解析不存在的服务应该抛出错误', () => { - expect(() => { - Core.services.resolve(TestUpdatableService); - }).toThrow(); - }); - }); - - describe('定时器系统', () => { - let core: Core; - - beforeEach(() => { - core = Core.create(true); - }); - - test('应该能够调度定时器', () => { - let callbackExecuted = false; - let timerInstance: ITimer | null = null; - - const timer = Core.schedule(0.1, false, null, (timer) => { - callbackExecuted = true; - timerInstance = timer; - }); - - expect(timer).toBeDefined(); - - // 模拟时间推进 - for (let i = 0; i < 10; i++) { - Core.update(0.016); // 约160ms总计 - } - - expect(callbackExecuted).toBe(true); - expect(timerInstance).toBe(timer); - }); - - test('应该能够调度重复定时器', () => { - let callbackCount = 0; - - Core.schedule(0.05, true, null, () => { - callbackCount++; - }); - - // 模拟足够长的时间以触发多次回调 - for (let i = 0; i < 15; i++) { - Core.update(0.016); // 约240ms总计,应该触发4-5次 - } - - expect(callbackCount).toBeGreaterThan(1); - }); - - test('应该支持带上下文的定时器', () => { - const context = { value: 42 }; - let receivedContext: any = null; - - Core.schedule(0.05, false, context, function(this: any, timer) { - receivedContext = this; - }); - - // 模拟时间推进 - for (let i = 0; i < 5; i++) { - Core.update(0.016); - } - - expect(receivedContext).toBe(context); - }); - }); - - describe('调试功能', () => { - test('应该能够启用调试功能', () => { - const core = Core.create(true); - const debugConfig = { - enabled: true, - websocketUrl: 'ws://localhost:8080', - autoReconnect: true, - updateInterval: 1000, - channels: { - entities: true, - systems: true, - performance: true, - components: true, - scenes: true - } - }; - - Core.enableDebug(debugConfig); - - expect(Core.isDebugEnabled).toBe(true); - }); - - test('应该能够禁用调试功能', () => { - const core = Core.create(true); - const debugConfig = { - enabled: true, - websocketUrl: 'ws://localhost:8080', - autoReconnect: true, - updateInterval: 1000, - channels: { - entities: true, - systems: true, - performance: true, - components: true, - scenes: true - } - }; - - Core.enableDebug(debugConfig); - Core.disableDebug(); - - expect(Core.isDebugEnabled).toBe(false); - }); - - test('在未创建实例时启用调试应该显示警告', () => { - const debugConfig = { - enabled: true, - websocketUrl: 'ws://localhost:8080', - autoReconnect: true, - updateInterval: 1000, - channels: { - entities: true, - systems: true, - performance: true, - components: true, - scenes: true - } - }; - - Core.enableDebug(debugConfig); - - expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("Core实例未创建,请先调用Core.create()")); - }); - }); - - // ECS API 现在由 SceneManager 管理 - // 相关测试请查看 SceneManager.test.ts - - describe('性能监控集成', () => { - let core: Core; - - beforeEach(() => { - core = Core.create(true); - }); - - test('调试模式下应该启用性能监控', () => { - const performanceMonitor = (core as any)._performanceMonitor; - - expect(performanceMonitor).toBeDefined(); - // 性能监控器应该在调试模式下被启用 - expect(performanceMonitor.isEnabled).toBe(true); - }); - - test('更新循环应该包含性能监控', () => { - const performanceMonitor = (core as any)._performanceMonitor; - const startMonitoringSpy = jest.spyOn(performanceMonitor, 'startMonitoring'); - const endMonitoringSpy = jest.spyOn(performanceMonitor, 'endMonitoring'); - - Core.update(0.016); - - expect(startMonitoringSpy).toHaveBeenCalled(); - expect(endMonitoringSpy).toHaveBeenCalled(); - }); - }); - - describe('Core.destroy() 生命周期', () => { - // 测试服务类 - class TestGameService implements IService { - public disposed = false; - public value = 'test-value'; - - dispose(): void { - this.disposed = true; - } - } - - test('destroy 后应该清理所有服务', () => { - // 创建 Core 并注册服务 - Core.create({ debug: true }); - const service = new TestGameService(); - Core.services.registerInstance(TestGameService, service); - - // 验证服务已注册 - expect(Core.services.isRegistered(TestGameService)).toBe(true); - - // 销毁 Core - Core.destroy(); - - // 验证服务的 dispose 被调用 - expect(service.disposed).toBe(true); - - // 验证 Core 实例已清空 - expect(Core.Instance).toBeNull(); - }); - - test('destroy 后重新 create 应该能够成功注册服务', () => { - // 第一次:创建 Core 并注册服务 - Core.create({ debug: true }); - Core.services.registerSingleton(TestGameService); - - // 验证服务已注册 - expect(Core.services.isRegistered(TestGameService)).toBe(true); - const firstService = Core.services.resolve(TestGameService); - expect(firstService).toBeDefined(); - expect(firstService.value).toBe('test-value'); - - // 销毁 Core - Core.destroy(); - - // 第二次:重新创建 Core - Core.create({ debug: true }); - - // 应该能够重新注册相同的服务(不应该报错或 warn) - expect(() => { - Core.services.registerSingleton(TestGameService); - }).not.toThrow(); - - // 验证服务重新注册成功 - expect(Core.services.isRegistered(TestGameService)).toBe(true); - const secondService = Core.services.resolve(TestGameService); - expect(secondService).toBeDefined(); - expect(secondService.value).toBe('test-value'); - - // 两次获取的应该是不同的实例 - expect(secondService).not.toBe(firstService); - - // 第一个实例应该已经被 dispose - expect(firstService.disposed).toBe(true); - }); - - test('destroy 后旧的服务引用不应该影响新的 Core 实例', () => { - // 第一次:创建 Core 并注册服务 - Core.create({ debug: true }); - const firstService = new TestGameService(); - Core.services.registerInstance(TestGameService, firstService); - - // 销毁 Core - Core.destroy(); - - // 验证旧服务被 dispose - expect(firstService.disposed).toBe(true); - - // 第二次:重新创建 Core 并注册新的服务实例 - Core.create({ debug: true }); - const secondService = new TestGameService(); - Core.services.registerInstance(TestGameService, secondService); - - // 验证新服务注册成功 - const resolved = Core.services.resolve(TestGameService); - expect(resolved).toBe(secondService); - expect(resolved).not.toBe(firstService); - - // 新服务应该未被 dispose - expect(secondService.disposed).toBe(false); - }); - - test('多次调用 destroy 应该安全', () => { - Core.create({ debug: true }); - const service = new TestGameService(); - Core.services.registerInstance(TestGameService, service); - - // 第一次 destroy - Core.destroy(); - expect(service.disposed).toBe(true); - expect(Core.Instance).toBeNull(); - - // 第二次 destroy(应该安全,不抛出错误) - expect(() => { - Core.destroy(); - }).not.toThrow(); - - expect(Core.Instance).toBeNull(); - }); - }); -}); \ No newline at end of file diff --git a/packages/core/tests/ECS/Core/FluentAPI.test.ts b/packages/core/tests/ECS/Core/FluentAPI.test.ts deleted file mode 100644 index 2edef8f0..00000000 --- a/packages/core/tests/ECS/Core/FluentAPI.test.ts +++ /dev/null @@ -1,691 +0,0 @@ -import { - EntityBuilder, - SceneBuilder, - ComponentBuilder, - ECSFluentAPI, - EntityBatchOperator, - createECSAPI, - initializeECS, - ECS -} from '../../../src/ECS/Core/FluentAPI'; -import { Scene } from '../../../src/ECS/Scene'; -import { Entity } from '../../../src/ECS/Entity'; -import { Component } from '../../../src/ECS/Component'; -import { QuerySystem } from '../../../src/ECS/Core/QuerySystem'; -import { TypeSafeEventSystem } from '../../../src/ECS/Core/EventSystem'; -import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; -import { Matcher } from '../../../src/ECS/Utils/Matcher'; - -// 测试组件 -class TestComponent extends Component { - public value: number; - - constructor(...args: unknown[]) { - super(); - const [value = 0] = args as [number?]; - this.value = value; - } -} - -class PositionComponent extends Component { - public x: number; - public y: number; - - constructor(...args: unknown[]) { - super(); - const [x = 0, y = 0] = args as [number?, number?]; - this.x = x; - this.y = y; - } -} - -class VelocityComponent extends Component { - public vx: number; - public vy: number; - - constructor(...args: unknown[]) { - super(); - const [vx = 0, vy = 0] = args as [number?, number?]; - this.vx = vx; - this.vy = vy; - } -} - -// 测试系统 -class TestSystem extends EntitySystem { - constructor() { - super(Matcher.empty().all(TestComponent)); - } - - protected override process(entities: Entity[]): void { - // 测试系统 - } -} - -describe('FluentAPI - 流式API测试', () => { - let scene: Scene; - let querySystem: QuerySystem; - let eventSystem: TypeSafeEventSystem; - - beforeEach(() => { - scene = new Scene(); - querySystem = new QuerySystem(); - eventSystem = new TypeSafeEventSystem(); - }); - - describe('EntityBuilder - 实体构建器', () => { - let builder: EntityBuilder; - - beforeEach(() => { - builder = new EntityBuilder(scene, scene.componentStorageManager); - }); - - test('应该能够创建实体构建器', () => { - expect(builder).toBeInstanceOf(EntityBuilder); - }); - - test('应该能够设置实体名称', () => { - const entity = builder.named('TestEntity').build(); - expect(entity.name).toBe('TestEntity'); - }); - - test('应该能够设置实体标签', () => { - const entity = builder.tagged(42).build(); - expect(entity.tag).toBe(42); - }); - - test('应该能够添加组件', () => { - const component = new TestComponent(100); - const entity = builder.with(component).build(); - - expect(entity.hasComponent(TestComponent)).toBe(true); - expect(entity.getComponent(TestComponent)).toBe(component); - }); - - test('应该能够添加多个组件', () => { - const comp1 = new TestComponent(100); - const comp2 = new PositionComponent(10, 20); - const comp3 = new VelocityComponent(1, 2); - - const entity = builder.withComponents(comp1, comp2, comp3).build(); - - expect(entity.hasComponent(TestComponent)).toBe(true); - expect(entity.hasComponent(PositionComponent)).toBe(true); - expect(entity.hasComponent(VelocityComponent)).toBe(true); - }); - - test('应该能够条件性添加组件', () => { - const comp1 = new TestComponent(100); - const comp2 = new PositionComponent(10, 20); - - const entity = builder - .withIf(true, comp1) - .withIf(false, comp2) - .build(); - - expect(entity.hasComponent(TestComponent)).toBe(true); - expect(entity.hasComponent(PositionComponent)).toBe(false); - }); - - test('应该能够使用工厂函数创建组件', () => { - const entity = builder - .withFactory(() => new TestComponent(200)) - .build(); - - expect(entity.hasComponent(TestComponent)).toBe(true); - expect(entity.getComponent(TestComponent)!.value).toBe(200); - }); - - test('应该能够配置组件属性', () => { - const entity = builder - .with(new TestComponent(100)) - .configure(TestComponent, (comp) => { - comp.value = 300; - }) - .build(); - - expect(entity.getComponent(TestComponent)!.value).toBe(300); - }); - - test('配置不存在的组件应该安全处理', () => { - expect(() => { - builder.configure(TestComponent, (comp) => { - comp.value = 300; - }).build(); - }).not.toThrow(); - }); - - test('应该能够设置实体启用状态', () => { - const entity1 = builder.enabled(true).build(); - const entity2 = new EntityBuilder(scene, scene.componentStorageManager).enabled(false).build(); - - expect(entity1.enabled).toBe(true); - expect(entity2.enabled).toBe(false); - }); - - test('应该能够设置实体活跃状态', () => { - const entity1 = builder.active(true).build(); - const entity2 = new EntityBuilder(scene, scene.componentStorageManager).active(false).build(); - - expect(entity1.active).toBe(true); - expect(entity2.active).toBe(false); - }); - - test('应该能够添加子实体', () => { - const childBuilder = new EntityBuilder(scene, scene.componentStorageManager) - .named('Child') - .with(new TestComponent(50)); - - const parent = builder - .named('Parent') - .withChild(childBuilder) - .build(); - - expect(parent.children.length).toBe(1); - expect(parent.children[0].name).toBe('Child'); - }); - - test('应该能够添加多个子实体', () => { - const child1 = new EntityBuilder(scene, scene.componentStorageManager).named('Child1'); - const child2 = new EntityBuilder(scene, scene.componentStorageManager).named('Child2'); - const child3 = new EntityBuilder(scene, scene.componentStorageManager).named('Child3'); - - const parent = builder - .named('Parent') - .withChildren(child1, child2, child3) - .build(); - - expect(parent.children.length).toBe(3); - expect(parent.children[0].name).toBe('Child1'); - expect(parent.children[1].name).toBe('Child2'); - expect(parent.children[2].name).toBe('Child3'); - }); - - test('应该能够使用工厂函数创建子实体', () => { - const parent = builder - .named('Parent') - .withChildFactory((parentEntity) => { - return new EntityBuilder(scene, scene.componentStorageManager) - .named(`Child_of_${parentEntity.name}`) - .with(new TestComponent(100)); - }) - .build(); - - expect(parent.children.length).toBe(1); - expect(parent.children[0].name).toBe('Child_of_Parent'); - }); - - test('应该能够条件性添加子实体', () => { - const child1 = new EntityBuilder(scene, scene.componentStorageManager).named('Child1'); - const child2 = new EntityBuilder(scene, scene.componentStorageManager).named('Child2'); - - const parent = builder - .named('Parent') - .withChildIf(true, child1) - .withChildIf(false, child2) - .build(); - - expect(parent.children.length).toBe(1); - expect(parent.children[0].name).toBe('Child1'); - }); - - test('应该能够构建实体并添加到场景', () => { - const entity = builder - .named('SpawnedEntity') - .with(new TestComponent(100)) - .spawn(); - - expect(entity.name).toBe('SpawnedEntity'); - expect(entity.scene).toBe(scene); - }); - - test('应该能够克隆构建器', () => { - const originalBuilder = builder.named('Original').with(new TestComponent(100)); - const clonedBuilder = originalBuilder.clone(); - - expect(clonedBuilder).toBeInstanceOf(EntityBuilder); - expect(clonedBuilder).not.toBe(originalBuilder); - }); - - test('流式调用应该工作正常', () => { - const entity = builder - .named('ComplexEntity') - .tagged(42) - .with(new TestComponent(100)) - .with(new PositionComponent(10, 20)) - .enabled(true) - .active(true) - .configure(TestComponent, (comp) => { - comp.value = 200; - }) - .build(); - - expect(entity.name).toBe('ComplexEntity'); - expect(entity.tag).toBe(42); - expect(entity.enabled).toBe(true); - expect(entity.active).toBe(true); - expect(entity.hasComponent(TestComponent)).toBe(true); - expect(entity.hasComponent(PositionComponent)).toBe(true); - expect(entity.getComponent(TestComponent)!.value).toBe(200); - }); - }); - - describe('SceneBuilder - 场景构建器', () => { - let builder: SceneBuilder; - - beforeEach(() => { - builder = new SceneBuilder(); - }); - - test('应该能够创建场景构建器', () => { - expect(builder).toBeInstanceOf(SceneBuilder); - }); - - test('应该能够设置场景名称', () => { - const scene = builder.named('TestScene').build(); - expect(scene.name).toBe('TestScene'); - }); - - test('应该能够添加实体', () => { - const entity = new Entity('TestEntity', 1); - const scene = builder.withEntity(entity).build(); - - expect(scene.entities.count).toBe(1); - expect(scene.findEntity('TestEntity')).toBe(entity); - }); - - test('应该能够使用实体构建器添加实体', () => { - const scene = builder - .withEntityBuilder((builder) => { - return builder - .named('BuilderEntity') - .with(new TestComponent(100)); - }) - .build(); - - expect(scene.entities.count).toBe(1); - expect(scene.findEntity('BuilderEntity')).not.toBeNull(); - }); - - test('应该能够批量添加实体', () => { - const entity1 = new Entity('Entity1', 1); - const entity2 = new Entity('Entity2', 2); - const entity3 = new Entity('Entity3', 3); - - const scene = builder - .withEntities(entity1, entity2, entity3) - .build(); - - expect(scene.entities.count).toBe(3); - }); - - test('应该能够添加系统', () => { - const system = new TestSystem(); - const scene = builder.withSystem(system).build(); - - expect(scene.systems.length).toBe(1); - expect(scene.systems[0]).toBe(system); - }); - - test('应该能够批量添加系统', () => { - const system1 = new TestSystem(); - const system2 = new TestSystem(); - - const scene = builder - .withSystems(system1, system2) - .build(); - - expect(scene.systems.length).toBe(1); - }); - - test('流式调用应该工作正常', () => { - const entity = new Entity('TestEntity', 1); - const system = new TestSystem(); - - const scene = builder - .named('ComplexScene') - .withEntity(entity) - .withSystem(system) - .withEntityBuilder((builder) => { - return builder.named('BuilderEntity'); - }) - .build(); - - expect(scene.name).toBe('ComplexScene'); - expect(scene.entities.count).toBe(2); - expect(scene.systems.length).toBe(1); - }); - }); - - describe('ComponentBuilder - 组件构建器', () => { - test('应该能够创建组件构建器', () => { - const builder = new ComponentBuilder(TestComponent, 100); - expect(builder).toBeInstanceOf(ComponentBuilder); - }); - - test('应该能够设置组件属性', () => { - const component = new ComponentBuilder(TestComponent, 100) - .set('value', 200) - .build(); - - expect(component.value).toBe(200); - }); - - test('应该能够使用配置函数', () => { - const component = new ComponentBuilder(PositionComponent, 10, 20) - .configure((comp) => { - comp.x = 30; - comp.y = 40; - }) - .build(); - - expect(component.x).toBe(30); - expect(component.y).toBe(40); - }); - - test('应该能够条件性设置属性', () => { - const component = new ComponentBuilder(TestComponent, 100) - .setIf(true, 'value', 200) - .setIf(false, 'value', 300) - .build(); - - expect(component.value).toBe(200); - }); - - test('流式调用应该工作正常', () => { - const component = new ComponentBuilder(PositionComponent, 0, 0) - .set('x', 10) - .set('y', 20) - .setIf(true, 'x', 30) - .configure((comp) => { - comp.y = 40; - }) - .build(); - - expect(component.x).toBe(30); - expect(component.y).toBe(40); - }); - }); - - describe('ECSFluentAPI - 主API', () => { - let api: ECSFluentAPI; - - beforeEach(() => { - api = new ECSFluentAPI(scene, querySystem, eventSystem); - }); - - test('应该能够创建ECS API', () => { - expect(api).toBeInstanceOf(ECSFluentAPI); - }); - - test('应该能够创建实体构建器', () => { - const builder = api.createEntity(); - expect(builder).toBeInstanceOf(EntityBuilder); - }); - - test('应该能够创建场景构建器', () => { - const builder = api.createScene(); - expect(builder).toBeInstanceOf(SceneBuilder); - }); - - test('应该能够创建组件构建器', () => { - const builder = api.createComponent(TestComponent, 100); - expect(builder).toBeInstanceOf(ComponentBuilder); - }); - - test('应该能够创建查询构建器', () => { - const builder = api.query(); - expect(builder).toBeDefined(); - }); - - test('应该能够查找实体', () => { - const entity = scene.createEntity('TestEntity'); - entity.addComponent(new TestComponent(100)); - querySystem.setEntities([entity]); - - const results = api.find(TestComponent); - expect(results.length).toBe(1); - expect(results[0]).toBe(entity); - }); - - test('应该能够查找第一个匹配的实体', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - entity1.addComponent(new TestComponent(100)); - entity2.addComponent(new TestComponent(200)); - querySystem.setEntities([entity1, entity2]); - - const result = api.findFirst(TestComponent); - expect(result).not.toBeNull(); - expect([entity1, entity2]).toContain(result); - }); - - test('查找不存在的实体应该返回null', () => { - const result = api.findFirst(TestComponent); - expect(result).toBeNull(); - }); - - test('应该能够按名称查找实体', () => { - const entity = scene.createEntity('TestEntity'); - - const result = api.findByName('TestEntity'); - expect(result).toBe(entity); - }); - - test('应该能够按标签查找实体', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - entity1.tag = 42; - entity2.tag = 42; - - const results = api.findByTag(42); - expect(results.length).toBe(2); - expect(results).toContain(entity1); - expect(results).toContain(entity2); - }); - - test('应该能够触发同步事件', () => { - let eventReceived = false; - let eventData: any = null; - - api.on('test:event', (data) => { - eventReceived = true; - eventData = data; - }); - - api.emit('test:event', { message: 'hello' }); - - expect(eventReceived).toBe(true); - expect(eventData.message).toBe('hello'); - }); - - test('应该能够触发异步事件', async () => { - let eventReceived = false; - let eventData: any = null; - - api.on('test:event', (data) => { - eventReceived = true; - eventData = data; - }); - - await api.emitAsync('test:event', { message: 'hello' }); - - expect(eventReceived).toBe(true); - expect(eventData.message).toBe('hello'); - }); - - test('应该能够一次性监听事件', () => { - let callCount = 0; - - api.once('test:event', () => { - callCount++; - }); - - api.emit('test:event', {}); - api.emit('test:event', {}); - - expect(callCount).toBe(1); - }); - - test('应该能够移除事件监听器', () => { - let callCount = 0; - - const listenerId = api.on('test:event', () => { - callCount++; - }); - - api.emit('test:event', {}); - api.off('test:event', listenerId); - api.emit('test:event', {}); - - expect(callCount).toBe(1); - }); - - test('应该能够创建批量操作器', () => { - const entity1 = new Entity('Entity1', 1); - const entity2 = new Entity('Entity2', 2); - - const batch = api.batch([entity1, entity2]); - expect(batch).toBeInstanceOf(EntityBatchOperator); - }); - - test('应该能够获取统计信息', () => { - const stats = api.getStats(); - - expect(stats).toBeDefined(); - expect(stats.entityCount).toBeDefined(); - expect(stats.systemCount).toBeDefined(); - expect(stats.componentStats).toBeDefined(); - expect(stats.queryStats).toBeDefined(); - expect(stats.eventStats).toBeDefined(); - }); - }); - - describe('EntityBatchOperator - 批量操作器', () => { - let entity1: Entity; - let entity2: Entity; - let entity3: Entity; - let batchOp: EntityBatchOperator; - - beforeEach(() => { - entity1 = scene.createEntity('Entity1'); - entity2 = scene.createEntity('Entity2'); - entity3 = scene.createEntity('Entity3'); - batchOp = new EntityBatchOperator([entity1, entity2, entity3]); - }); - - test('应该能够创建批量操作器', () => { - expect(batchOp).toBeInstanceOf(EntityBatchOperator); - }); - - test('应该能够批量添加组件', () => { - const component = new TestComponent(100); - batchOp.addComponent(component); - - expect(entity1.hasComponent(TestComponent)).toBe(true); - expect(entity2.hasComponent(TestComponent)).toBe(true); - expect(entity3.hasComponent(TestComponent)).toBe(true); - }); - - test('应该能够批量移除组件', () => { - entity1.addComponent(new TestComponent(100)); - entity2.addComponent(new TestComponent(200)); - entity3.addComponent(new TestComponent(300)); - - batchOp.removeComponent(TestComponent); - - expect(entity1.hasComponent(TestComponent)).toBe(false); - expect(entity2.hasComponent(TestComponent)).toBe(false); - expect(entity3.hasComponent(TestComponent)).toBe(false); - }); - - test('应该能够批量设置活跃状态', () => { - batchOp.setActive(false); - - expect(entity1.active).toBe(false); - expect(entity2.active).toBe(false); - expect(entity3.active).toBe(false); - }); - - test('应该能够批量设置标签', () => { - batchOp.setTag(42); - - expect(entity1.tag).toBe(42); - expect(entity2.tag).toBe(42); - expect(entity3.tag).toBe(42); - }); - - test('应该能够批量执行操作', () => { - const names: string[] = []; - const indices: number[] = []; - - batchOp.forEach((entity, index) => { - names.push(entity.name); - indices.push(index); - }); - - expect(names).toEqual(['Entity1', 'Entity2', 'Entity3']); - expect(indices).toEqual([0, 1, 2]); - }); - - test('应该能够过滤实体', () => { - entity1.tag = 1; - entity2.tag = 2; - entity3.tag = 1; - - const filtered = batchOp.filter(entity => entity.tag === 1); - - expect(filtered.count()).toBe(2); - expect(filtered.toArray()).toContain(entity1); - expect(filtered.toArray()).toContain(entity3); - }); - - test('应该能够获取实体数组', () => { - const entities = batchOp.toArray(); - - expect(entities.length).toBe(3); - expect(entities).toContain(entity1); - expect(entities).toContain(entity2); - expect(entities).toContain(entity3); - }); - - test('应该能够获取实体数量', () => { - expect(batchOp.count()).toBe(3); - }); - - test('流式调用应该工作正常', () => { - const result = batchOp - .addComponent(new TestComponent(100)) - .setActive(false) - .setTag(42) - .forEach((entity) => { - entity.name = entity.name + '_Modified'; - }); - - expect(result).toBe(batchOp); - expect(entity1.hasComponent(TestComponent)).toBe(true); - expect(entity1.active).toBe(false); - expect(entity1.tag).toBe(42); - expect(entity1.name).toBe('Entity1_Modified'); - }); - }); - - describe('工厂函数和全局API', () => { - test('createECSAPI应该创建API实例', () => { - const api = createECSAPI(scene, querySystem, eventSystem); - expect(api).toBeInstanceOf(ECSFluentAPI); - }); - - test('initializeECS应该初始化全局ECS', () => { - initializeECS(scene, querySystem, eventSystem); - expect(ECS).toBeInstanceOf(ECSFluentAPI); - }); - - test('全局ECS应该可用', () => { - initializeECS(scene, querySystem, eventSystem); - - const builder = ECS.createEntity(); - expect(builder).toBeInstanceOf(EntityBuilder); - }); - }); -}); \ No newline at end of file diff --git a/packages/core/tests/ECS/Core/FluentAPI/EntityBuilder.test.ts b/packages/core/tests/ECS/Core/FluentAPI/EntityBuilder.test.ts new file mode 100644 index 00000000..4a3b9ba7 --- /dev/null +++ b/packages/core/tests/ECS/Core/FluentAPI/EntityBuilder.test.ts @@ -0,0 +1,321 @@ +import { EntityBuilder } from '../../../../src/ECS/Core/FluentAPI/EntityBuilder'; +import { Scene } from '../../../../src/ECS/Scene'; +import { Component } from '../../../../src/ECS/Component'; +import { HierarchySystem } from '../../../../src/ECS/Systems/HierarchySystem'; +import { ECSComponent } from '../../../../src/ECS/Decorators'; + +@ECSComponent('BuilderTestPosition') +class PositionComponent extends Component { + public x: number = 0; + public y: number = 0; + + constructor(x: number = 0, y: number = 0) { + super(); + this.x = x; + this.y = y; + } +} + +@ECSComponent('BuilderTestVelocity') +class VelocityComponent extends Component { + public vx: number = 0; + public vy: number = 0; +} + +@ECSComponent('BuilderTestHealth') +class HealthComponent extends Component { + public current: number = 100; + public max: number = 100; +} + +// Helper function to create EntityBuilder +function createBuilder(scene: Scene): EntityBuilder { + return new EntityBuilder(scene, scene.componentStorageManager); +} + +describe('EntityBuilder', () => { + let scene: Scene; + let hierarchySystem: HierarchySystem; + + beforeEach(() => { + scene = new Scene({ name: 'BuilderTestScene' }); + hierarchySystem = new HierarchySystem(); + scene.addSystem(hierarchySystem); + }); + + afterEach(() => { + scene.end(); + }); + + describe('basic building', () => { + test('should create entity with name', () => { + const builder = createBuilder(scene); + const entity = builder.named('TestEntity').build(); + + expect(entity.name).toBe('TestEntity'); + }); + + test('should create entity with tag', () => { + const builder = createBuilder(scene); + const entity = builder.tagged(0x100).build(); + + expect(entity.tag).toBe(0x100); + }); + + test('should support chaining name and tag', () => { + const entity = createBuilder(scene) + .named('ChainedEntity') + .tagged(0x200) + .build(); + + expect(entity.name).toBe('ChainedEntity'); + expect(entity.tag).toBe(0x200); + }); + }); + + describe('component management', () => { + test('should add single component with .with()', () => { + const entity = createBuilder(scene) + .with(new PositionComponent(10, 20)) + .build(); + + const pos = entity.getComponent(PositionComponent); + expect(pos).not.toBeNull(); + expect(pos!.x).toBe(10); + expect(pos!.y).toBe(20); + }); + + test('should add multiple components with .withComponents()', () => { + const entity = createBuilder(scene) + .withComponents( + new PositionComponent(5, 10), + new VelocityComponent(), + new HealthComponent() + ) + .build(); + + expect(entity.hasComponent(PositionComponent)).toBe(true); + expect(entity.hasComponent(VelocityComponent)).toBe(true); + expect(entity.hasComponent(HealthComponent)).toBe(true); + }); + + test('should conditionally add component with .withIf()', () => { + const shouldAdd = true; + const shouldNotAdd = false; + + const entity = createBuilder(scene) + .withIf(shouldAdd, new PositionComponent()) + .withIf(shouldNotAdd, new VelocityComponent()) + .build(); + + expect(entity.hasComponent(PositionComponent)).toBe(true); + expect(entity.hasComponent(VelocityComponent)).toBe(false); + }); + + test('should add component using factory with .withFactory()', () => { + const entity = createBuilder(scene) + .withFactory(() => new PositionComponent(100, 200)) + .build(); + + const pos = entity.getComponent(PositionComponent); + expect(pos).not.toBeNull(); + expect(pos!.x).toBe(100); + expect(pos!.y).toBe(200); + }); + + test('should configure existing component with .configure()', () => { + const entity = createBuilder(scene) + .with(new PositionComponent(0, 0)) + .configure(PositionComponent, (pos: PositionComponent) => { + pos.x = 999; + pos.y = 888; + }) + .build(); + + const pos = entity.getComponent(PositionComponent); + expect(pos!.x).toBe(999); + expect(pos!.y).toBe(888); + }); + + test('.configure() should do nothing if component does not exist', () => { + const entity = createBuilder(scene) + .configure(PositionComponent, (pos: PositionComponent) => { + pos.x = 100; + }) + .build(); + + expect(entity.hasComponent(PositionComponent)).toBe(false); + }); + }); + + describe('entity state', () => { + test('should set enabled state', () => { + const disabledEntity = createBuilder(scene) + .enabled(false) + .build(); + + const enabledEntity = createBuilder(scene) + .enabled(true) + .build(); + + expect(disabledEntity.enabled).toBe(false); + expect(enabledEntity.enabled).toBe(true); + }); + + test('should set active state', () => { + const inactiveEntity = createBuilder(scene) + .active(false) + .build(); + + const activeEntity = createBuilder(scene) + .active(true) + .build(); + + expect(inactiveEntity.active).toBe(false); + expect(activeEntity.active).toBe(true); + }); + }); + + describe('hierarchy building', () => { + test('should call withChild method', () => { + const childBuilder = createBuilder(scene).named('Child'); + const builder = createBuilder(scene) + .named('Parent') + .withChild(childBuilder); + + // withChild returns the builder for chaining + expect(builder).toBeInstanceOf(EntityBuilder); + }); + + test('should call withChildren method', () => { + const child1Builder = createBuilder(scene).named('Child1'); + const child2Builder = createBuilder(scene).named('Child2'); + + const builder = createBuilder(scene) + .named('Parent') + .withChildren(child1Builder, child2Builder); + + // withChildren returns the builder for chaining + expect(builder).toBeInstanceOf(EntityBuilder); + }); + + test('should call withChildFactory method', () => { + const builder = createBuilder(scene) + .named('Parent') + .with(new PositionComponent(100, 100)) + .withChildFactory((parentEntity) => { + return createBuilder(scene) + .named('ChildFromFactory') + .with(new PositionComponent(10, 20)); + }); + + // withChildFactory returns the builder for chaining + expect(builder).toBeInstanceOf(EntityBuilder); + }); + + test('should call withChildIf method', () => { + const shouldAdd = true; + const shouldNotAdd = false; + + const child1Builder = createBuilder(scene).named('Child1'); + const builder1 = createBuilder(scene) + .named('Parent') + .withChildIf(shouldAdd, child1Builder); + + expect(builder1).toBeInstanceOf(EntityBuilder); + + const child2Builder = createBuilder(scene).named('Child2'); + const builder2 = createBuilder(scene) + .named('Parent2') + .withChildIf(shouldNotAdd, child2Builder); + + expect(builder2).toBeInstanceOf(EntityBuilder); + }); + }); + + describe('spawning and cloning', () => { + test('should spawn entity to scene with .spawn()', () => { + const initialCount = scene.entities.count; + + const entity = createBuilder(scene) + .named('SpawnedEntity') + .with(new PositionComponent()) + .spawn(); + + expect(scene.entities.count).toBe(initialCount + 1); + expect(scene.findEntityById(entity.id)).toBe(entity); + }); + + test('.build() should not add to scene automatically', () => { + const initialCount = scene.entities.count; + + createBuilder(scene) + .named('BuiltEntity') + .build(); + + expect(scene.entities.count).toBe(initialCount); + }); + + test('should clone builder', () => { + const builder = createBuilder(scene) + .named('OriginalEntity') + .tagged(0x50); + + const clonedBuilder = builder.clone(); + + expect(clonedBuilder).not.toBe(builder); + expect(clonedBuilder).toBeInstanceOf(EntityBuilder); + }); + }); + + describe('complex building scenarios', () => { + test('should build complete entity with all options', () => { + const entity = createBuilder(scene) + .named('CompleteEntity') + .tagged(0x100) + .with(new PositionComponent(50, 75)) + .with(new VelocityComponent()) + .withFactory(() => new HealthComponent()) + .configure(HealthComponent, (h: HealthComponent) => { + h.current = 80; + h.max = 100; + }) + .enabled(true) + .active(true) + .build(); + + expect(entity.name).toBe('CompleteEntity'); + expect(entity.tag).toBe(0x100); + expect(entity.enabled).toBe(true); + expect(entity.active).toBe(true); + + expect(entity.hasComponent(PositionComponent)).toBe(true); + expect(entity.hasComponent(VelocityComponent)).toBe(true); + expect(entity.hasComponent(HealthComponent)).toBe(true); + + const health = entity.getComponent(HealthComponent); + expect(health!.current).toBe(80); + expect(health!.max).toBe(100); + }); + + test('should support complex chaining', () => { + const builder = createBuilder(scene) + .named('Root') + .with(new PositionComponent(1, 1)); + + // Add child builder chain + const childBuilder = createBuilder(scene) + .named('Child') + .with(new PositionComponent(2, 2)); + + // Chain withChild + builder.withChild(childBuilder); + + // Build and spawn + const root = builder.spawn(); + + expect(root.name).toBe('Root'); + expect(root.hasComponent(PositionComponent)).toBe(true); + }); + }); +}); diff --git a/packages/core/tests/ECS/EntityRefIntegration.test.ts b/packages/core/tests/ECS/EntityRefIntegration.test.ts deleted file mode 100644 index 3d14eac1..00000000 --- a/packages/core/tests/ECS/EntityRefIntegration.test.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { describe, test, expect, beforeEach } from '@jest/globals'; -import { Scene } from '../../src/ECS/Scene'; -import { Component } from '../../src/ECS/Component'; -import { Entity } from '../../src/ECS/Entity'; -import { EntityRef, ECSComponent } from '../../src/ECS/Decorators'; - -@ECSComponent('ParentRef') -class ParentComponent extends Component { - @EntityRef() parent: Entity | null = null; -} - -@ECSComponent('TargetRef') -class TargetComponent extends Component { - @EntityRef() target: Entity | null = null; - @EntityRef() ally: Entity | null = null; -} - -describe('EntityRef Integration Tests', () => { - let scene: Scene; - - beforeEach(() => { - scene = new Scene({ name: 'TestScene' }); - }); - - describe('基础功能', () => { - test('应该支持EntityRef装饰器', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const comp = entity1.addComponent(new ParentComponent()); - - comp.parent = entity2; - - expect(comp.parent).toBe(entity2); - }); - - test('Entity销毁时应该自动清理所有引用', () => { - const parent = scene.createEntity('Parent'); - const child1 = scene.createEntity('Child1'); - const child2 = scene.createEntity('Child2'); - - const comp1 = child1.addComponent(new ParentComponent()); - const comp2 = child2.addComponent(new ParentComponent()); - - comp1.parent = parent; - comp2.parent = parent; - - expect(comp1.parent).toBe(parent); - expect(comp2.parent).toBe(parent); - - parent.destroy(); - - expect(comp1.parent).toBeNull(); - expect(comp2.parent).toBeNull(); - }); - - test('修改引用应该更新ReferenceTracker', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const entity3 = scene.createEntity('Entity3'); - const comp = entity1.addComponent(new ParentComponent()); - - comp.parent = entity2; - expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(1); - - comp.parent = entity3; - expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(0); - expect(scene.referenceTracker.getReferencesTo(entity3.id)).toHaveLength(1); - }); - - test('设置为null应该注销引用', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const comp = entity1.addComponent(new ParentComponent()); - - comp.parent = entity2; - expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(1); - - comp.parent = null; - expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(0); - }); - }); - - describe('Component生命周期', () => { - test('移除Component应该清理其所有引用注册', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const comp = entity1.addComponent(new ParentComponent()); - - comp.parent = entity2; - expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(1); - - entity1.removeComponent(comp); - expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(0); - }); - - test('移除Component应该清除entityId引用', () => { - const entity1 = scene.createEntity('Entity1'); - const comp = entity1.addComponent(new ParentComponent()); - - expect(comp.entityId).toBe(entity1.id); - - entity1.removeComponent(comp); - expect(comp.entityId).toBeNull(); - }); - }); - - describe('多属性引用', () => { - test('应该支持同一Component的多个EntityRef属性', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const entity3 = scene.createEntity('Entity3'); - const comp = entity1.addComponent(new TargetComponent()); - - comp.target = entity2; - comp.ally = entity3; - - expect(comp.target).toBe(entity2); - expect(comp.ally).toBe(entity3); - - entity2.destroy(); - - expect(comp.target).toBeNull(); - expect(comp.ally).toBe(entity3); - - entity3.destroy(); - - expect(comp.ally).toBeNull(); - }); - }); - - describe('边界情况', () => { - test('跨Scene引用应该失败', () => { - const scene2 = new Scene({ name: 'TestScene2' }); - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene2.createEntity('Entity2'); - const comp = entity1.addComponent(new ParentComponent()); - - comp.parent = entity2; - - expect(comp.parent).toBeNull(); - }); - - test('引用已销毁的Entity应该失败', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const comp = entity1.addComponent(new ParentComponent()); - - entity2.destroy(); - comp.parent = entity2; - - expect(comp.parent).toBeNull(); - }); - - test('重复设置相同值不应重复注册', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const comp = entity1.addComponent(new ParentComponent()); - - comp.parent = entity2; - comp.parent = entity2; - comp.parent = entity2; - - expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(1); - }); - - test('循环引用应该正常工作', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const comp1 = entity1.addComponent(new ParentComponent()); - const comp2 = entity2.addComponent(new ParentComponent()); - - comp1.parent = entity2; - comp2.parent = entity1; - - expect(comp1.parent).toBe(entity2); - expect(comp2.parent).toBe(entity1); - - entity1.destroy(); - - expect(comp2.parent).toBeNull(); - expect(entity2.isDestroyed).toBe(false); - }); - }); - - describe('复杂场景', () => { - test('父子实体销毁应该正确清理引用', () => { - const parent = scene.createEntity('Parent'); - const child1 = scene.createEntity('Child1'); - const child2 = scene.createEntity('Child2'); - const observer = scene.createEntity('Observer'); - - parent.addChild(child1); - parent.addChild(child2); - - const observerComp = observer.addComponent(new TargetComponent()); - observerComp.target = parent; - observerComp.ally = child1; - - expect(observerComp.target).toBe(parent); - expect(observerComp.ally).toBe(child1); - - parent.destroy(); - - expect(observerComp.target).toBeNull(); - expect(observerComp.ally).toBeNull(); - expect(child1.isDestroyed).toBe(true); - expect(child2.isDestroyed).toBe(true); - }); - - test('大量引用场景', () => { - const target = scene.createEntity('Target'); - const entities: Entity[] = []; - const components: ParentComponent[] = []; - - for (let i = 0; i < 100; i++) { - const entity = scene.createEntity(`Entity${i}`); - const comp = entity.addComponent(new ParentComponent()); - comp.parent = target; - entities.push(entity); - components.push(comp); - } - - expect(scene.referenceTracker.getReferencesTo(target.id)).toHaveLength(100); - - target.destroy(); - - for (const comp of components) { - expect(comp.parent).toBeNull(); - } - - expect(scene.referenceTracker.getReferencesTo(target.id)).toHaveLength(0); - }); - - test('批量销毁后引用应全部清理', () => { - const entities: Entity[] = []; - const components: TargetComponent[] = []; - - for (let i = 0; i < 50; i++) { - entities.push(scene.createEntity(`Entity${i}`)); - } - - for (let i = 0; i < 50; i++) { - const comp = entities[i].addComponent(new TargetComponent()); - if (i > 0) { - comp.target = entities[i - 1]; - } - if (i < 49) { - comp.ally = entities[i + 1]; - } - components.push(comp); - } - - scene.destroyAllEntities(); - - for (const comp of components) { - expect(comp.target).toBeNull(); - expect(comp.ally).toBeNull(); - } - }); - }); - - describe('调试功能', () => { - test('getDebugInfo应该返回引用信息', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const comp = entity1.addComponent(new ParentComponent()); - - comp.parent = entity2; - - const debugInfo = scene.referenceTracker.getDebugInfo(); - expect(debugInfo).toHaveProperty(`entity_${entity2.id}`); - }); - }); -}); diff --git a/packages/core/tests/ECS/EntityTags.test.ts b/packages/core/tests/ECS/EntityTags.test.ts new file mode 100644 index 00000000..7d34deb3 --- /dev/null +++ b/packages/core/tests/ECS/EntityTags.test.ts @@ -0,0 +1,237 @@ +import { + EntityTags, + hasEntityTag, + addEntityTag, + removeEntityTag, + isFolder, + isHidden, + isLocked +} from '../../src/ECS/EntityTags'; + +describe('EntityTags', () => { + describe('tag constants', () => { + test('should have correct NONE value', () => { + expect(EntityTags.NONE).toBe(0x0000); + }); + + test('should have correct FOLDER value', () => { + expect(EntityTags.FOLDER).toBe(0x1000); + }); + + test('should have correct HIDDEN value', () => { + expect(EntityTags.HIDDEN).toBe(0x2000); + }); + + test('should have correct LOCKED value', () => { + expect(EntityTags.LOCKED).toBe(0x4000); + }); + + test('should have correct EDITOR_ONLY value', () => { + expect(EntityTags.EDITOR_ONLY).toBe(0x8000); + }); + + test('should have correct PREFAB_INSTANCE value', () => { + expect(EntityTags.PREFAB_INSTANCE).toBe(0x0100); + }); + + test('should have correct PREFAB_ROOT value', () => { + expect(EntityTags.PREFAB_ROOT).toBe(0x0200); + }); + + test('all tags should have unique values', () => { + const values = Object.values(EntityTags).filter((v) => typeof v === 'number'); + const uniqueValues = new Set(values); + expect(uniqueValues.size).toBe(values.length); + }); + }); + + describe('hasEntityTag', () => { + test('should return true when tag is present', () => { + const tag = EntityTags.FOLDER; + expect(hasEntityTag(tag, EntityTags.FOLDER)).toBe(true); + }); + + test('should return false when tag is not present', () => { + const tag = EntityTags.FOLDER; + expect(hasEntityTag(tag, EntityTags.HIDDEN)).toBe(false); + }); + + test('should work with combined tags', () => { + const combined = EntityTags.FOLDER | EntityTags.HIDDEN | EntityTags.LOCKED; + + expect(hasEntityTag(combined, EntityTags.FOLDER)).toBe(true); + expect(hasEntityTag(combined, EntityTags.HIDDEN)).toBe(true); + expect(hasEntityTag(combined, EntityTags.LOCKED)).toBe(true); + expect(hasEntityTag(combined, EntityTags.EDITOR_ONLY)).toBe(false); + }); + + test('should return false for NONE tag', () => { + const tag = EntityTags.NONE; + expect(hasEntityTag(tag, EntityTags.FOLDER)).toBe(false); + }); + }); + + describe('addEntityTag', () => { + test('should add tag to empty tags', () => { + const result = addEntityTag(EntityTags.NONE, EntityTags.FOLDER); + expect(result).toBe(EntityTags.FOLDER); + }); + + test('should add tag to existing tags', () => { + const existing = EntityTags.FOLDER as number; + const result = addEntityTag(existing, EntityTags.HIDDEN); + + expect(hasEntityTag(result, EntityTags.FOLDER)).toBe(true); + expect(hasEntityTag(result, EntityTags.HIDDEN)).toBe(true); + }); + + test('should not change value when adding same tag', () => { + const existing = EntityTags.FOLDER as number; + const result = addEntityTag(existing, EntityTags.FOLDER); + + expect(result).toBe(EntityTags.FOLDER); + }); + + test('should handle multiple tag additions', () => { + let tag: number = EntityTags.NONE; + tag = addEntityTag(tag, EntityTags.FOLDER); + tag = addEntityTag(tag, EntityTags.HIDDEN); + tag = addEntityTag(tag, EntityTags.LOCKED); + + expect(hasEntityTag(tag, EntityTags.FOLDER)).toBe(true); + expect(hasEntityTag(tag, EntityTags.HIDDEN)).toBe(true); + expect(hasEntityTag(tag, EntityTags.LOCKED)).toBe(true); + }); + }); + + describe('removeEntityTag', () => { + test('should remove tag from combined tags', () => { + const combined = (EntityTags.FOLDER | EntityTags.HIDDEN) as number; + const result = removeEntityTag(combined, EntityTags.HIDDEN); + + expect(hasEntityTag(result, EntityTags.FOLDER)).toBe(true); + expect(hasEntityTag(result, EntityTags.HIDDEN)).toBe(false); + }); + + test('should return same value when removing non-existent tag', () => { + const existing = EntityTags.FOLDER as number; + const result = removeEntityTag(existing, EntityTags.HIDDEN); + + expect(result).toBe(EntityTags.FOLDER); + }); + + test('should return NONE when removing last tag', () => { + const result = removeEntityTag(EntityTags.FOLDER, EntityTags.FOLDER); + expect(result).toBe(EntityTags.NONE); + }); + + test('should handle multiple tag removals', () => { + let tag: number = EntityTags.FOLDER | EntityTags.HIDDEN | EntityTags.LOCKED; + tag = removeEntityTag(tag, EntityTags.FOLDER); + tag = removeEntityTag(tag, EntityTags.LOCKED); + + expect(hasEntityTag(tag, EntityTags.FOLDER)).toBe(false); + expect(hasEntityTag(tag, EntityTags.HIDDEN)).toBe(true); + expect(hasEntityTag(tag, EntityTags.LOCKED)).toBe(false); + }); + }); + + describe('isFolder', () => { + test('should return true for folder tag', () => { + expect(isFolder(EntityTags.FOLDER)).toBe(true); + }); + + test('should return true for combined tags including folder', () => { + const combined = EntityTags.FOLDER | EntityTags.HIDDEN; + expect(isFolder(combined)).toBe(true); + }); + + test('should return false for non-folder tag', () => { + expect(isFolder(EntityTags.HIDDEN)).toBe(false); + expect(isFolder(EntityTags.NONE)).toBe(false); + }); + }); + + describe('isHidden', () => { + test('should return true for hidden tag', () => { + expect(isHidden(EntityTags.HIDDEN)).toBe(true); + }); + + test('should return true for combined tags including hidden', () => { + const combined = EntityTags.FOLDER | EntityTags.HIDDEN; + expect(isHidden(combined)).toBe(true); + }); + + test('should return false for non-hidden tag', () => { + expect(isHidden(EntityTags.FOLDER)).toBe(false); + expect(isHidden(EntityTags.NONE)).toBe(false); + }); + }); + + describe('isLocked', () => { + test('should return true for locked tag', () => { + expect(isLocked(EntityTags.LOCKED)).toBe(true); + }); + + test('should return true for combined tags including locked', () => { + const combined = EntityTags.FOLDER | EntityTags.LOCKED; + expect(isLocked(combined)).toBe(true); + }); + + test('should return false for non-locked tag', () => { + expect(isLocked(EntityTags.FOLDER)).toBe(false); + expect(isLocked(EntityTags.NONE)).toBe(false); + }); + }); + + describe('tag combinations', () => { + test('should correctly combine and identify multiple tags', () => { + const tag = (EntityTags.FOLDER | EntityTags.HIDDEN | EntityTags.PREFAB_ROOT) as number; + + expect(isFolder(tag)).toBe(true); + expect(isHidden(tag)).toBe(true); + expect(hasEntityTag(tag, EntityTags.PREFAB_ROOT)).toBe(true); + expect(isLocked(tag)).toBe(false); + }); + + test('should support complex add/remove operations', () => { + let tag: number = EntityTags.NONE; + + // Add tags + tag = addEntityTag(tag, EntityTags.FOLDER); + tag = addEntityTag(tag, EntityTags.HIDDEN); + tag = addEntityTag(tag, EntityTags.LOCKED); + tag = addEntityTag(tag, EntityTags.EDITOR_ONLY); + + expect(isFolder(tag)).toBe(true); + expect(isHidden(tag)).toBe(true); + expect(isLocked(tag)).toBe(true); + expect(hasEntityTag(tag, EntityTags.EDITOR_ONLY)).toBe(true); + + // Remove some tags + tag = removeEntityTag(tag, EntityTags.HIDDEN); + tag = removeEntityTag(tag, EntityTags.LOCKED); + + expect(isFolder(tag)).toBe(true); + expect(isHidden(tag)).toBe(false); + expect(isLocked(tag)).toBe(false); + expect(hasEntityTag(tag, EntityTags.EDITOR_ONLY)).toBe(true); + }); + + test('should work correctly with prefab tags', () => { + const prefabInstanceTag = EntityTags.PREFAB_INSTANCE as number; + const prefabRootTag = EntityTags.PREFAB_ROOT as number; + + expect(hasEntityTag(prefabInstanceTag, EntityTags.PREFAB_INSTANCE)).toBe(true); + expect(hasEntityTag(prefabInstanceTag, EntityTags.PREFAB_ROOT)).toBe(false); + + expect(hasEntityTag(prefabRootTag, EntityTags.PREFAB_ROOT)).toBe(true); + expect(hasEntityTag(prefabRootTag, EntityTags.PREFAB_INSTANCE)).toBe(false); + + // Combine both + const combined = (prefabInstanceTag | prefabRootTag) as number; + expect(hasEntityTag(combined, EntityTags.PREFAB_INSTANCE)).toBe(true); + expect(hasEntityTag(combined, EntityTags.PREFAB_ROOT)).toBe(true); + }); + }); +}); diff --git a/packages/core/tests/ECS/Hierarchy.test.ts b/packages/core/tests/ECS/Hierarchy.test.ts new file mode 100644 index 00000000..39206875 --- /dev/null +++ b/packages/core/tests/ECS/Hierarchy.test.ts @@ -0,0 +1,648 @@ +import { Scene, Entity, HierarchyComponent, HierarchySystem } from '../../src'; + +describe('HierarchySystem', () => { + let scene: Scene; + let hierarchySystem: HierarchySystem; + + beforeEach(() => { + scene = new Scene(); + scene.initialize(); + hierarchySystem = new HierarchySystem(); + scene.addSystem(hierarchySystem); + }); + + afterEach(() => { + scene.end(); + }); + + describe('setParent', () => { + it('should set parent-child relationship', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + + expect(hierarchySystem.getParent(child)).toBe(parent); + expect(hierarchySystem.getChildren(parent)).toContain(child); + expect(hierarchySystem.getChildCount(parent)).toBe(1); + }); + + it('should auto-add HierarchyComponent if not present', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + expect(parent.getComponent(HierarchyComponent)).toBeNull(); + expect(child.getComponent(HierarchyComponent)).toBeNull(); + + hierarchySystem.setParent(child, parent); + + expect(parent.getComponent(HierarchyComponent)).not.toBeNull(); + expect(child.getComponent(HierarchyComponent)).not.toBeNull(); + }); + + it('should move child to root when parent is null', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + expect(hierarchySystem.getParent(child)).toBe(parent); + + hierarchySystem.setParent(child, null); + expect(hierarchySystem.getParent(child)).toBeNull(); + expect(hierarchySystem.getChildren(parent)).not.toContain(child); + }); + + it('should transfer child to new parent', () => { + const parent1 = scene.createEntity('Parent1'); + const parent2 = scene.createEntity('Parent2'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent1); + expect(hierarchySystem.getParent(child)).toBe(parent1); + expect(hierarchySystem.getChildCount(parent1)).toBe(1); + + hierarchySystem.setParent(child, parent2); + expect(hierarchySystem.getParent(child)).toBe(parent2); + expect(hierarchySystem.getChildCount(parent1)).toBe(0); + expect(hierarchySystem.getChildCount(parent2)).toBe(1); + }); + + it('should throw error on circular reference', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + hierarchySystem.setParent(child, parent); + hierarchySystem.setParent(grandchild, child); + + expect(() => { + hierarchySystem.setParent(parent, grandchild); + }).toThrow('Cannot set parent: would create circular reference'); + }); + + it('should not change if setting same parent', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + const hierarchy = child.getComponent(HierarchyComponent)!; + hierarchy.bCacheDirty = false; + + hierarchySystem.setParent(child, parent); + // Should not mark dirty since parent didn't change + expect(hierarchy.bCacheDirty).toBe(false); + }); + }); + + describe('insertChildAt', () => { + it('should insert child at specific position', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + const child3 = scene.createEntity('Child3'); + + hierarchySystem.setParent(child1, parent); + hierarchySystem.setParent(child3, parent); + hierarchySystem.insertChildAt(parent, child2, 1); + + const children = hierarchySystem.getChildren(parent); + expect(children[0]).toBe(child1); + expect(children[1]).toBe(child2); + expect(children[2]).toBe(child3); + }); + + it('should append child when index is -1', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + + hierarchySystem.setParent(child1, parent); + hierarchySystem.insertChildAt(parent, child2, -1); + + const children = hierarchySystem.getChildren(parent); + expect(children[children.length - 1]).toBe(child2); + }); + }); + + describe('removeChild', () => { + it('should remove child from parent', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + expect(hierarchySystem.getChildCount(parent)).toBe(1); + + const result = hierarchySystem.removeChild(parent, child); + expect(result).toBe(true); + expect(hierarchySystem.getChildCount(parent)).toBe(0); + expect(hierarchySystem.getParent(child)).toBeNull(); + }); + + it('should return false if child is not a child of parent', () => { + const parent1 = scene.createEntity('Parent1'); + const parent2 = scene.createEntity('Parent2'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent1); + + const result = hierarchySystem.removeChild(parent2, child); + expect(result).toBe(false); + }); + }); + + describe('removeAllChildren', () => { + it('should remove all children from parent', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + const child3 = scene.createEntity('Child3'); + + hierarchySystem.setParent(child1, parent); + hierarchySystem.setParent(child2, parent); + hierarchySystem.setParent(child3, parent); + expect(hierarchySystem.getChildCount(parent)).toBe(3); + + hierarchySystem.removeAllChildren(parent); + expect(hierarchySystem.getChildCount(parent)).toBe(0); + expect(hierarchySystem.getParent(child1)).toBeNull(); + expect(hierarchySystem.getParent(child2)).toBeNull(); + expect(hierarchySystem.getParent(child3)).toBeNull(); + }); + }); + + describe('hierarchy queries', () => { + it('should check if entity has children', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + expect(hierarchySystem.hasChildren(parent)).toBe(false); + + hierarchySystem.setParent(child, parent); + expect(hierarchySystem.hasChildren(parent)).toBe(true); + }); + + it('should check isAncestorOf', () => { + const grandparent = scene.createEntity('Grandparent'); + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(parent, grandparent); + hierarchySystem.setParent(child, parent); + + expect(hierarchySystem.isAncestorOf(grandparent, child)).toBe(true); + expect(hierarchySystem.isAncestorOf(parent, child)).toBe(true); + expect(hierarchySystem.isAncestorOf(child, grandparent)).toBe(false); + }); + + it('should check isDescendantOf', () => { + const grandparent = scene.createEntity('Grandparent'); + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(parent, grandparent); + hierarchySystem.setParent(child, parent); + + expect(hierarchySystem.isDescendantOf(child, grandparent)).toBe(true); + expect(hierarchySystem.isDescendantOf(child, parent)).toBe(true); + expect(hierarchySystem.isDescendantOf(grandparent, child)).toBe(false); + }); + + it('should get root entity', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + hierarchySystem.setParent(child, root); + hierarchySystem.setParent(grandchild, child); + + expect(hierarchySystem.getRoot(grandchild)).toBe(root); + expect(hierarchySystem.getRoot(child)).toBe(root); + expect(hierarchySystem.getRoot(root)).toBe(root); + }); + + it('should get depth correctly', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + root.addComponent(new HierarchyComponent()); + hierarchySystem.setParent(child, root); + hierarchySystem.setParent(grandchild, child); + + expect(hierarchySystem.getDepth(root)).toBe(0); + expect(hierarchySystem.getDepth(child)).toBe(1); + expect(hierarchySystem.getDepth(grandchild)).toBe(2); + }); + }); + + describe('findChild', () => { + it('should find child by name', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Target'); + + hierarchySystem.setParent(child1, parent); + hierarchySystem.setParent(child2, parent); + + const found = hierarchySystem.findChild(parent, 'Target'); + expect(found).toBe(child2); + }); + + it('should find child recursively', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Target'); + + hierarchySystem.setParent(child, root); + hierarchySystem.setParent(grandchild, child); + + const found = hierarchySystem.findChild(root, 'Target', true); + expect(found).toBe(grandchild); + + const notFound = hierarchySystem.findChild(root, 'Target', false); + expect(notFound).toBeNull(); + }); + }); + + describe('forEachChild', () => { + it('should iterate over children', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + + hierarchySystem.setParent(child1, parent); + hierarchySystem.setParent(child2, parent); + + const visited: Entity[] = []; + hierarchySystem.forEachChild(parent, (child) => { + visited.push(child); + }); + + expect(visited).toContain(child1); + expect(visited).toContain(child2); + expect(visited.length).toBe(2); + }); + + it('should iterate recursively', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + hierarchySystem.setParent(child, root); + hierarchySystem.setParent(grandchild, child); + + const visited: Entity[] = []; + hierarchySystem.forEachChild(root, (entity) => { + visited.push(entity); + }, true); + + expect(visited).toContain(child); + expect(visited).toContain(grandchild); + expect(visited.length).toBe(2); + }); + }); + + describe('getRootEntities', () => { + it('should return all root entities', () => { + const root1 = scene.createEntity('Root1'); + const root2 = scene.createEntity('Root2'); + const child = scene.createEntity('Child'); + + root1.addComponent(new HierarchyComponent()); + root2.addComponent(new HierarchyComponent()); + hierarchySystem.setParent(child, root1); + + const roots = hierarchySystem.getRootEntities(); + expect(roots).toContain(root1); + expect(roots).toContain(root2); + expect(roots).not.toContain(child); + }); + }); + + describe('activeInHierarchy', () => { + it('should be inactive if parent is inactive', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + + expect(hierarchySystem.isActiveInHierarchy(child)).toBe(true); + + parent.active = false; + // Mark cache dirty to recalculate + const childHierarchy = child.getComponent(HierarchyComponent)!; + childHierarchy.bCacheDirty = true; + + expect(hierarchySystem.isActiveInHierarchy(child)).toBe(false); + }); + + it('should be inactive if self is inactive', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + child.active = false; + + expect(hierarchySystem.isActiveInHierarchy(child)).toBe(false); + }); + }); +}); + +describe('HierarchyComponent', () => { + it('should have correct default values', () => { + const component = new HierarchyComponent(); + + expect(component.parentId).toBeNull(); + expect(component.childIds).toEqual([]); + expect(component.depth).toBe(0); + expect(component.bActiveInHierarchy).toBe(true); + expect(component.bCacheDirty).toBe(true); + }); +}); + +describe('HierarchySystem - Extended Tests', () => { + let scene: Scene; + let hierarchySystem: HierarchySystem; + + beforeEach(() => { + scene = new Scene(); + scene.initialize(); + hierarchySystem = new HierarchySystem(); + scene.addSystem(hierarchySystem); + }); + + afterEach(() => { + scene.end(); + }); + + describe('findChildrenByTag', () => { + it('should find children by tag', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + const child3 = scene.createEntity('Child3'); + + child1.tag = 0x01; + child2.tag = 0x02; + child3.tag = 0x01; + + hierarchySystem.setParent(child1, parent); + hierarchySystem.setParent(child2, parent); + hierarchySystem.setParent(child3, parent); + + const found = hierarchySystem.findChildrenByTag(parent, 0x01); + expect(found.length).toBe(2); + expect(found).toContain(child1); + expect(found).toContain(child3); + }); + + it('should find children by tag recursively', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + child.tag = 0x01; + grandchild.tag = 0x01; + + hierarchySystem.setParent(child, root); + hierarchySystem.setParent(grandchild, child); + + const foundNonRecursive = hierarchySystem.findChildrenByTag(root, 0x01, false); + expect(foundNonRecursive.length).toBe(1); + expect(foundNonRecursive[0]).toBe(child); + + const foundRecursive = hierarchySystem.findChildrenByTag(root, 0x01, true); + expect(foundRecursive.length).toBe(2); + expect(foundRecursive).toContain(child); + expect(foundRecursive).toContain(grandchild); + }); + + it('should return empty array when no children match tag', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + child.tag = 0x01; + + hierarchySystem.setParent(child, parent); + + const found = hierarchySystem.findChildrenByTag(parent, 0x02); + expect(found).toEqual([]); + }); + }); + + describe('flattenHierarchy', () => { + it('should flatten hierarchy with expanded nodes', () => { + const root = scene.createEntity('Root'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + const grandchild = scene.createEntity('Grandchild'); + + root.addComponent(new HierarchyComponent()); + hierarchySystem.setParent(child1, root); + hierarchySystem.setParent(child2, root); + hierarchySystem.setParent(grandchild, child1); + + const expandedIds = new Set([root.id, child1.id]); + const flattened = hierarchySystem.flattenHierarchy(expandedIds); + + expect(flattened.length).toBe(4); + expect(flattened[0].entity).toBe(root); + expect(flattened[0].depth).toBe(0); + expect(flattened[0].bHasChildren).toBe(true); + expect(flattened[0].bIsExpanded).toBe(true); + }); + + it('should not include children of collapsed nodes', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + root.addComponent(new HierarchyComponent()); + hierarchySystem.setParent(child, root); + hierarchySystem.setParent(grandchild, child); + + // Root is expanded, but child is collapsed + const expandedIds = new Set([root.id]); + const flattened = hierarchySystem.flattenHierarchy(expandedIds); + + expect(flattened.length).toBe(2); + expect(flattened[0].entity).toBe(root); + expect(flattened[1].entity).toBe(child); + expect(flattened[1].bHasChildren).toBe(true); + expect(flattened[1].bIsExpanded).toBe(false); + }); + + it('should return empty array when no root entities', () => { + const flattened = hierarchySystem.flattenHierarchy(new Set()); + expect(flattened).toEqual([]); + }); + }); + + describe('updateOrder', () => { + it('should have negative update order for early processing', () => { + expect(hierarchySystem.updateOrder).toBe(-1000); + }); + }); + + describe('process - cache update', () => { + it('should update dirty caches during process', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + + // Cache should be dirty after setParent + const childHierarchy = child.getComponent(HierarchyComponent)!; + expect(childHierarchy.bCacheDirty).toBe(true); + + // Update scene to process + scene.update(); + + // Cache should be clean after process + expect(childHierarchy.bCacheDirty).toBe(false); + }); + }); + + describe('insertChildAt edge cases', () => { + it('should handle circular reference prevention', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + hierarchySystem.setParent(child, parent); + hierarchySystem.setParent(grandchild, child); + + expect(() => { + hierarchySystem.insertChildAt(grandchild, parent, 0); + }).toThrow('Cannot set parent: would create circular reference'); + }); + + it('should move child within same parent to different position', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + const child3 = scene.createEntity('Child3'); + + hierarchySystem.setParent(child1, parent); + hierarchySystem.setParent(child2, parent); + hierarchySystem.setParent(child3, parent); + + // Move child3 to position 0 + hierarchySystem.insertChildAt(parent, child3, 0); + + const children = hierarchySystem.getChildren(parent); + expect(children[0]).toBe(child3); + expect(children[1]).toBe(child1); + expect(children[2]).toBe(child2); + }); + }); + + describe('removeChild edge cases', () => { + it('should return false when parent has no HierarchyComponent', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + const result = hierarchySystem.removeChild(parent, child); + expect(result).toBe(false); + }); + + it('should return false when child has no HierarchyComponent', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + parent.addComponent(new HierarchyComponent()); + + const result = hierarchySystem.removeChild(parent, child); + expect(result).toBe(false); + }); + }); + + describe('removeAllChildren edge cases', () => { + it('should handle entity with no HierarchyComponent', () => { + const parent = scene.createEntity('Parent'); + + expect(() => { + hierarchySystem.removeAllChildren(parent); + }).not.toThrow(); + }); + }); + + describe('getChildren edge cases', () => { + it('should return empty array when entity has no HierarchyComponent', () => { + const entity = scene.createEntity('Entity'); + const children = hierarchySystem.getChildren(entity); + expect(children).toEqual([]); + }); + }); + + describe('getChildCount edge cases', () => { + it('should return 0 when entity has no HierarchyComponent', () => { + const entity = scene.createEntity('Entity'); + expect(hierarchySystem.getChildCount(entity)).toBe(0); + }); + }); + + describe('getDepth edge cases', () => { + it('should return 0 when entity has no HierarchyComponent', () => { + const entity = scene.createEntity('Entity'); + expect(hierarchySystem.getDepth(entity)).toBe(0); + }); + + it('should use cached depth when cache is valid', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + parent.addComponent(new HierarchyComponent()); + hierarchySystem.setParent(child, parent); + + // First call computes depth + const depth1 = hierarchySystem.getDepth(child); + expect(depth1).toBe(1); + + // Mark cache as valid + const childHierarchy = child.getComponent(HierarchyComponent)!; + childHierarchy.bCacheDirty = false; + + // Second call should use cache + const depth2 = hierarchySystem.getDepth(child); + expect(depth2).toBe(1); + }); + }); + + describe('isActiveInHierarchy edge cases', () => { + it('should return entity.active when entity has no HierarchyComponent', () => { + const entity = scene.createEntity('Entity'); + entity.active = true; + expect(hierarchySystem.isActiveInHierarchy(entity)).toBe(true); + + entity.active = false; + expect(hierarchySystem.isActiveInHierarchy(entity)).toBe(false); + }); + + it('should use cached value when cache is valid', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + + // First call computes activeInHierarchy + const active1 = hierarchySystem.isActiveInHierarchy(child); + expect(active1).toBe(true); + + // Mark cache as valid + const childHierarchy = child.getComponent(HierarchyComponent)!; + childHierarchy.bCacheDirty = false; + + // Second call should use cache + const active2 = hierarchySystem.isActiveInHierarchy(child); + expect(active2).toBe(true); + }); + }); + + describe('dispose', () => { + it('should not throw when disposing', () => { + expect(() => { + hierarchySystem.dispose(); + }).not.toThrow(); + }); + }); +}); diff --git a/packages/core/tests/ECS/Scene.test.ts b/packages/core/tests/ECS/Scene.test.ts index 9a515052..1e82d94d 100644 --- a/packages/core/tests/ECS/Scene.test.ts +++ b/packages/core/tests/ECS/Scene.test.ts @@ -629,4 +629,201 @@ describe('Scene - 场景管理系统测试', () => { scene2.end(); }); }); + + describe('扩展测试 - 补齐覆盖率', () => { + describe('实体标签查找', () => { + test('findEntitiesByTag 应该返回具有指定标签的实体', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.tag = 0x01; + + const entity2 = scene.createEntity('Entity2'); + entity2.tag = 0x02; + + const entity3 = scene.createEntity('Entity3'); + entity3.tag = 0x01; + + const found = scene.findEntitiesByTag(0x01); + + expect(found.length).toBe(2); + expect(found).toContain(entity1); + expect(found).toContain(entity3); + }); + + test('findEntitiesByTag 应该在没有匹配时返回空数组', () => { + scene.createEntity('Entity1'); + + const found = scene.findEntitiesByTag(0xFF); + + expect(found).toEqual([]); + }); + }); + + describe('批量实体操作', () => { + test('destroyEntities 应该批量销毁实体', () => { + const entities = scene.createEntities(5, 'Entity'); + expect(scene.entities.count).toBe(5); + + const toDestroy = entities.slice(0, 3); + scene.destroyEntities(toDestroy); + + expect(scene.entities.count).toBe(2); + }); + + test('destroyEntities 应该处理空数组', () => { + scene.createEntities(3, 'Entity'); + + expect(() => { + scene.destroyEntities([]); + }).not.toThrow(); + + expect(scene.entities.count).toBe(3); + }); + }); + + describe('查询方法', () => { + test('queryAny 应该返回具有任意一个组件的实体', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent()); + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new VelocityComponent()); + + const entity3 = scene.createEntity('Entity3'); + entity3.addComponent(new HealthComponent()); + + const result = scene.queryAny(PositionComponent, VelocityComponent); + + expect(result.entities.length).toBe(2); + }); + + test('queryNone 应该返回不包含指定组件的实体', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent()); + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new VelocityComponent()); + + const entity3 = scene.createEntity('Entity3'); + + const result = scene.queryNone(PositionComponent); + + expect(result.entities.length).toBe(2); + expect(result.entities).toContain(entity2); + expect(result.entities).toContain(entity3); + }); + + test('query 应该创建类型安全的查询构建器', () => { + const builder = scene.query(); + expect(builder).toBeDefined(); + + const matcher = builder.withAll(PositionComponent).buildMatcher(); + expect(matcher).toBeDefined(); + }); + }); + + describe('服务容器', () => { + test('scene.services 应该返回服务容器', () => { + expect(scene.services).toBeDefined(); + }); + }); + + describe('系统错误处理', () => { + test('频繁出错的系统应该被自动禁用', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + class ErrorProneSystem extends EntitySystem { + constructor() { + super(Matcher.empty().all(PositionComponent)); + } + + protected override process(): void { + throw new Error('Intentional error'); + } + } + + const system = new ErrorProneSystem(); + scene.addEntityProcessor(system); + + const entity = scene.createEntity('TestEntity'); + entity.addComponent(new PositionComponent(0, 0)); + + // 多次更新以触发错误阈值 + for (let i = 0; i < 15; i++) { + scene.update(); + } + + // 系统应该被禁用 + expect(system.enabled).toBe(false); + + consoleSpy.mockRestore(); + }); + }); + + describe('已废弃方法', () => { + test('getEntityByName 应该作为 findEntity 的别名工作', () => { + const entity = scene.createEntity('TestEntity'); + + const found = scene.getEntityByName('TestEntity'); + + expect(found).toBe(entity); + }); + + test('getEntitiesByTag 应该作为 findEntitiesByTag 的别名工作', () => { + const entity = scene.createEntity('Entity'); + entity.tag = 0x10; + + const found = scene.getEntitiesByTag(0x10); + + expect(found.length).toBe(1); + expect(found[0]).toBe(entity); + }); + }); + + describe('系统管理扩展', () => { + test('getSystem 应该返回指定类型的系统', () => { + const movementSystem = new MovementSystem(); + scene.addEntityProcessor(movementSystem); + + const found = scene.getSystem(MovementSystem); + + expect(found).toBe(movementSystem); + }); + + test('getSystem 应该在系统不存在时返回 null', () => { + const found = scene.getSystem(MovementSystem); + + expect(found).toBeNull(); + }); + + test('markSystemsOrderDirty 应该标记系统顺序为脏', () => { + const system1 = new MovementSystem(); + const system2 = new RenderSystem(); + + scene.addEntityProcessor(system1); + scene.addEntityProcessor(system2); + + // 访问 systems 以清除脏标记 + const _ = scene.systems; + + // 标记为脏 + scene.markSystemsOrderDirty(); + + // 再次访问应该重新构建缓存 + const systems = scene.systems; + expect(systems).toBeDefined(); + }); + }); + + describe('延迟缓存清理', () => { + test('addEntity 应该支持延迟缓存清理', () => { + scene.createEntity('Entity1'); + const entity2 = new Entity('Entity2', scene.identifierPool.checkOut()); + + // 延迟缓存清理 + scene.addEntity(entity2, true); + + expect(scene.entities.count).toBe(2); + }); + }); + }); }); \ No newline at end of file diff --git a/packages/core/tests/ECS/Serialization/EntitySerializer.test.ts b/packages/core/tests/ECS/Serialization/EntitySerializer.test.ts new file mode 100644 index 00000000..36809ab7 --- /dev/null +++ b/packages/core/tests/ECS/Serialization/EntitySerializer.test.ts @@ -0,0 +1,495 @@ +import { EntitySerializer, SerializedEntity } from '../../../src/ECS/Serialization/EntitySerializer'; +import { Scene } from '../../../src/ECS/Scene'; +import { Entity } from '../../../src/ECS/Entity'; +import { Component } from '../../../src/ECS/Component'; +import { HierarchySystem } from '../../../src/ECS/Systems/HierarchySystem'; +import { HierarchyComponent } from '../../../src/ECS/Components/HierarchyComponent'; +import { ECSComponent } from '../../../src/ECS/Decorators'; +import { ComponentRegistry, ComponentType } from '../../../src/ECS/Core/ComponentStorage'; +import { Serializable, Serialize } from '../../../src/ECS/Serialization'; + +@ECSComponent('EntitySerTest_Position') +@Serializable({ version: 1 }) +class PositionComponent extends Component { + @Serialize() + public x: number = 0; + + @Serialize() + public y: number = 0; + + constructor(x: number = 0, y: number = 0) { + super(); + this.x = x; + this.y = y; + } +} + +@ECSComponent('EntitySerTest_Velocity') +@Serializable({ version: 1 }) +class VelocityComponent extends Component { + @Serialize() + public vx: number = 0; + + @Serialize() + public vy: number = 0; +} + +describe('EntitySerializer', () => { + let scene: Scene; + let hierarchySystem: HierarchySystem; + let componentRegistry: Map; + + beforeEach(() => { + ComponentRegistry.reset(); + ComponentRegistry.register(PositionComponent); + ComponentRegistry.register(VelocityComponent); + ComponentRegistry.register(HierarchyComponent); + + scene = new Scene({ name: 'EntitySerializerTestScene' }); + hierarchySystem = new HierarchySystem(); + scene.addSystem(hierarchySystem); + + componentRegistry = ComponentRegistry.getAllComponentNames() as Map; + }); + + afterEach(() => { + scene.end(); + }); + + describe('serialize', () => { + test('should serialize basic entity properties', () => { + const entity = scene.createEntity('TestEntity'); + entity.tag = 42; + entity.active = false; + entity.enabled = false; + entity.updateOrder = 10; + + const serialized = EntitySerializer.serialize(entity, false); + + expect(serialized.id).toBe(entity.id); + expect(serialized.name).toBe('TestEntity'); + expect(serialized.tag).toBe(42); + expect(serialized.active).toBe(false); + expect(serialized.enabled).toBe(false); + expect(serialized.updateOrder).toBe(10); + }); + + test('should serialize entity with components', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent(100, 200)); + entity.addComponent(new VelocityComponent()); + + const serialized = EntitySerializer.serialize(entity, false); + + expect(serialized.components.length).toBe(2); + }); + + test('should serialize entity without children when includeChildren is false', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + hierarchySystem.setParent(child, parent); + + const serialized = EntitySerializer.serialize(parent, false, hierarchySystem); + + expect(serialized.children).toEqual([]); + }); + + test('should serialize entity with children when includeChildren is true', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + + hierarchySystem.setParent(child1, parent); + hierarchySystem.setParent(child2, parent); + + const serialized = EntitySerializer.serialize(parent, true, hierarchySystem); + + expect(serialized.children.length).toBe(2); + expect(serialized.children.some(c => c.name === 'Child1')).toBe(true); + expect(serialized.children.some(c => c.name === 'Child2')).toBe(true); + }); + + test('should serialize nested hierarchy', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + hierarchySystem.setParent(child, root); + hierarchySystem.setParent(grandchild, child); + + const serialized = EntitySerializer.serialize(root, true, hierarchySystem); + + expect(serialized.children.length).toBe(1); + expect(serialized.children[0].name).toBe('Child'); + expect(serialized.children[0].children.length).toBe(1); + expect(serialized.children[0].children[0].name).toBe('Grandchild'); + }); + + test('should include parentId in serialized data', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + hierarchySystem.setParent(child, parent); + + const serializedChild = EntitySerializer.serialize(child, false, hierarchySystem); + + expect(serializedChild.parentId).toBe(parent.id); + }); + }); + + describe('deserialize', () => { + test('should deserialize basic entity properties', () => { + const serialized: SerializedEntity = { + id: 999, + name: 'DeserializedEntity', + tag: 77, + active: false, + enabled: false, + updateOrder: 5, + components: [], + children: [] + }; + + let nextId = 1; + const entity = EntitySerializer.deserialize( + serialized, + componentRegistry, + () => nextId++, + false + ); + + expect(entity.name).toBe('DeserializedEntity'); + expect(entity.tag).toBe(77); + expect(entity.active).toBe(false); + expect(entity.enabled).toBe(false); + expect(entity.updateOrder).toBe(5); + }); + + test('should preserve IDs when preserveIds is true', () => { + const serialized: SerializedEntity = { + id: 999, + name: 'Entity', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [] + }; + + const entity = EntitySerializer.deserialize( + serialized, + componentRegistry, + () => 1, + true + ); + + expect(entity.id).toBe(999); + }); + + test('should generate new IDs when preserveIds is false', () => { + const serialized: SerializedEntity = { + id: 999, + name: 'Entity', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [] + }; + + let nextId = 100; + const entity = EntitySerializer.deserialize( + serialized, + componentRegistry, + () => nextId++, + false + ); + + expect(entity.id).toBe(100); + }); + + test('should deserialize components', () => { + const serialized: SerializedEntity = { + id: 1, + name: 'Entity', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [ + { type: 'EntitySerTest_Position', version: 1, data: { x: 100, y: 200 } } + ], + children: [] + }; + + const entity = EntitySerializer.deserialize( + serialized, + componentRegistry, + () => 1, + true, + scene + ); + + expect(entity.hasComponent(PositionComponent)).toBe(true); + const pos = entity.getComponent(PositionComponent)!; + expect(pos.x).toBe(100); + expect(pos.y).toBe(200); + }); + + test('should deserialize children with hierarchy relationships', () => { + const serialized: SerializedEntity = { + id: 1, + name: 'Parent', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [ + { + id: 2, + name: 'Child', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [] + } + ] + }; + + let nextId = 10; + const allEntities = new Map(); + const entity = EntitySerializer.deserialize( + serialized, + componentRegistry, + () => nextId++, + false, + scene, + hierarchySystem, + allEntities + ); + + expect(allEntities.size).toBe(2); + + // Add deserialized entities to scene so hierarchySystem can find them + for (const [, e] of allEntities) { + scene.addEntity(e); + } + + const children = hierarchySystem.getChildren(entity); + expect(children.length).toBe(1); + expect(children[0].name).toBe('Child'); + }); + }); + + describe('serializeEntities', () => { + test('should serialize multiple entities', () => { + const entity1 = scene.createEntity('Entity1'); + const entity2 = scene.createEntity('Entity2'); + + const serialized = EntitySerializer.serializeEntities([entity1, entity2], false); + + expect(serialized.length).toBe(2); + }); + + test('should only serialize root entities when includeChildren is true', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + hierarchySystem.setParent(child, root); + + const serialized = EntitySerializer.serializeEntities( + [root, child], + true, + hierarchySystem + ); + + // Should only have root (child is serialized inside root) + expect(serialized.length).toBe(1); + expect(serialized[0].name).toBe('Root'); + expect(serialized[0].children.length).toBe(1); + }); + }); + + describe('deserializeEntities', () => { + test('should deserialize multiple entities', () => { + const serializedEntities: SerializedEntity[] = [ + { + id: 1, + name: 'Entity1', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [] + }, + { + id: 2, + name: 'Entity2', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [] + } + ]; + + let nextId = 100; + const { rootEntities, allEntities } = EntitySerializer.deserializeEntities( + serializedEntities, + componentRegistry, + () => nextId++, + false, + scene + ); + + expect(rootEntities.length).toBe(2); + expect(allEntities.size).toBe(2); + }); + + test('should deserialize entities with nested hierarchy', () => { + const serializedEntities: SerializedEntity[] = [ + { + id: 1, + name: 'Root', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [ + { + id: 2, + name: 'Child', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [ + { + id: 3, + name: 'Grandchild', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [] + } + ] + } + ] + } + ]; + + let nextId = 10; + const { rootEntities, allEntities } = EntitySerializer.deserializeEntities( + serializedEntities, + componentRegistry, + () => nextId++, + false, + scene, + hierarchySystem + ); + + expect(rootEntities.length).toBe(1); + expect(allEntities.size).toBe(3); + }); + }); + + describe('clone', () => { + test('should clone entity with new ID', () => { + const original = scene.createEntity('Original'); + original.tag = 99; + original.addComponent(new PositionComponent(50, 100)); + + // Use serialize + deserialize with scene for proper cloning + const serialized = EntitySerializer.serialize(original, false); + let nextId = 1000; + const cloned = EntitySerializer.deserialize( + serialized, + componentRegistry, + () => nextId++, + false, + scene // Pass scene so components can be added + ); + + expect(cloned.id).not.toBe(original.id); + expect(cloned.name).toBe('Original'); + expect(cloned.tag).toBe(99); + expect(cloned.hasComponent(PositionComponent)).toBe(true); + + const clonedPos = cloned.getComponent(PositionComponent)!; + expect(clonedPos.x).toBe(50); + expect(clonedPos.y).toBe(100); + }); + + test('should clone entity basic properties without components', () => { + // Test the clone method directly with entity that has no components + const original = scene.createEntity('Original'); + original.tag = 99; + original.active = false; + original.enabled = false; + original.updateOrder = 5; + + let nextId = 1000; + const cloned = EntitySerializer.clone( + original, + componentRegistry, + () => nextId++ + ); + + expect(cloned.id).not.toBe(original.id); + expect(cloned.name).toBe('Original'); + expect(cloned.tag).toBe(99); + expect(cloned.active).toBe(false); + expect(cloned.enabled).toBe(false); + expect(cloned.updateOrder).toBe(5); + }); + + test('should clone entity with children hierarchy data', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + hierarchySystem.setParent(child, parent); + + // Serialize with children + const serialized = EntitySerializer.serialize(parent, true, hierarchySystem); + + // Verify the serialized data contains children + expect(serialized.children.length).toBe(1); + expect(serialized.children[0].name).toBe('Child'); + }); + }); + + describe('edge cases', () => { + test('should handle entity with no components', () => { + const entity = scene.createEntity('Empty'); + const serialized = EntitySerializer.serialize(entity, false); + + expect(serialized.components).toEqual([]); + }); + + test('should handle entity with no hierarchy component', () => { + const entity = new Entity('Standalone', 999); + const serialized = EntitySerializer.serialize(entity, true); + + expect(serialized.children).toEqual([]); + expect(serialized.parentId).toBeUndefined(); + }); + + test('should handle default values in serialization', () => { + const entity = scene.createEntity('Default'); + + const serialized = EntitySerializer.serialize(entity, false); + + expect(serialized.active).toBe(true); + expect(serialized.enabled).toBe(true); + expect(serialized.updateOrder).toBe(0); + }); + }); +}); diff --git a/packages/core/tests/ECS/Serialization/ParentChildSerialization.test.ts b/packages/core/tests/ECS/Serialization/ParentChildSerialization.test.ts deleted file mode 100644 index 69c9c809..00000000 --- a/packages/core/tests/ECS/Serialization/ParentChildSerialization.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * 父子实体序列化和反序列化测试 - * - * 测试场景序列化和反序列化时父子实体关系的正确性 - */ - -import { Scene } from '../../../src'; - -describe('父子实体序列化测试', () => { - let scene: Scene; - - beforeEach(() => { - scene = new Scene({ name: 'TestScene' }); - }); - - afterEach(() => { - scene.end(); - }); - - test('应该正确反序列化父子实体层次结构', () => { - // 创建父实体 - const parent = scene.createEntity('parent'); - parent.tag = 100; - - // 创建2个子实体 - const child1 = scene.createEntity('child1'); - child1.tag = 200; - parent.addChild(child1); - - const child2 = scene.createEntity('child2'); - child2.tag = 200; - parent.addChild(child2); - - // 创建1个顶层实体(对照组) - const topLevel = scene.createEntity('topLevel'); - topLevel.tag = 200; - - // 验证序列化前的状态 - expect(scene.querySystem.queryAll().entities.length).toBe(4); - expect(scene.findEntitiesByTag(100).length).toBe(1); - expect(scene.findEntitiesByTag(200).length).toBe(3); - - // 序列化 - const serialized = scene.serialize({ format: 'json' }); - - // 创建新场景并反序列化 - const scene2 = new Scene({ name: 'LoadTestScene' }); - scene2.deserialize(serialized as string, { - strategy: 'replace', - preserveIds: true, - }); - - // 验证所有实体都被正确恢复 - const allEntities = scene2.querySystem.queryAll().entities; - expect(allEntities.length).toBe(4); - expect(scene2.findEntitiesByTag(100).length).toBe(1); - expect(scene2.findEntitiesByTag(200).length).toBe(3); - - // 验证父子关系正确恢复 - const restoredParent = scene2.findEntity('parent'); - expect(restoredParent).not.toBeNull(); - expect(restoredParent!.children.length).toBe(2); - - const restoredChild1 = scene2.findEntity('child1'); - const restoredChild2 = scene2.findEntity('child2'); - expect(restoredChild1).not.toBeNull(); - expect(restoredChild2).not.toBeNull(); - expect(restoredChild1!.parent).toBe(restoredParent); - expect(restoredChild2!.parent).toBe(restoredParent); - - scene2.end(); - }); - - test('应该正确反序列化多层级实体层次结构', () => { - // 创建多层级实体结构:grandparent -> parent -> child - const grandparent = scene.createEntity('grandparent'); - grandparent.tag = 1; - - const parent = scene.createEntity('parent'); - parent.tag = 2; - grandparent.addChild(parent); - - const child = scene.createEntity('child'); - child.tag = 3; - parent.addChild(child); - - expect(scene.querySystem.queryAll().entities.length).toBe(3); - - // 序列化 - const serialized = scene.serialize({ format: 'json' }); - - // 创建新场景并反序列化 - const scene2 = new Scene({ name: 'LoadTestScene' }); - scene2.deserialize(serialized as string, { - strategy: 'replace', - preserveIds: true, - }); - - // 验证多层级结构正确恢复 - expect(scene2.querySystem.queryAll().entities.length).toBe(3); - - const restoredGrandparent = scene2.findEntity('grandparent'); - const restoredParent = scene2.findEntity('parent'); - const restoredChild = scene2.findEntity('child'); - - expect(restoredGrandparent).not.toBeNull(); - expect(restoredParent).not.toBeNull(); - expect(restoredChild).not.toBeNull(); - - expect(restoredParent!.parent).toBe(restoredGrandparent); - expect(restoredChild!.parent).toBe(restoredParent); - expect(restoredGrandparent!.children.length).toBe(1); - expect(restoredParent!.children.length).toBe(1); - - scene2.end(); - }); -}); diff --git a/packages/core/tests/ECS/Serialization/SceneSerializer.test.ts b/packages/core/tests/ECS/Serialization/SceneSerializer.test.ts new file mode 100644 index 00000000..1ed03a46 --- /dev/null +++ b/packages/core/tests/ECS/Serialization/SceneSerializer.test.ts @@ -0,0 +1,439 @@ +import { SceneSerializer } from '../../../src/ECS/Serialization/SceneSerializer'; +import { Scene } from '../../../src/ECS/Scene'; +import { Component } from '../../../src/ECS/Component'; +import { HierarchySystem } from '../../../src/ECS/Systems/HierarchySystem'; +import { ECSComponent } from '../../../src/ECS/Decorators'; +import { ComponentRegistry, ComponentType } from '../../../src/ECS/Core/ComponentStorage'; +import { Serializable, Serialize } from '../../../src/ECS/Serialization'; + +@ECSComponent('SceneSerTest_Position') +@Serializable({ version: 1 }) +class PositionComponent extends Component { + @Serialize() + public x: number = 0; + + @Serialize() + public y: number = 0; + + constructor(x: number = 0, y: number = 0) { + super(); + this.x = x; + this.y = y; + } +} + +@ECSComponent('SceneSerTest_Velocity') +@Serializable({ version: 1 }) +class VelocityComponent extends Component { + @Serialize() + public vx: number = 0; + + @Serialize() + public vy: number = 0; +} + +describe('SceneSerializer', () => { + let scene: Scene; + let componentRegistry: Map; + + beforeEach(() => { + ComponentRegistry.reset(); + ComponentRegistry.register(PositionComponent); + ComponentRegistry.register(VelocityComponent); + + scene = new Scene({ name: 'SceneSerializerTestScene' }); + + componentRegistry = ComponentRegistry.getAllComponentNames() as Map; + }); + + afterEach(() => { + scene.end(); + }); + + describe('serialize', () => { + test('should serialize scene to JSON string', () => { + scene.createEntity('Entity1').addComponent(new PositionComponent(10, 20)); + scene.createEntity('Entity2').addComponent(new VelocityComponent()); + + const result = SceneSerializer.serialize(scene); + + expect(typeof result).toBe('string'); + const parsed = JSON.parse(result as string); + expect(parsed.name).toBe('SceneSerializerTestScene'); + expect(parsed.entities.length).toBe(2); + }); + + test('should serialize scene to binary format', () => { + scene.createEntity('Entity'); + + const result = SceneSerializer.serialize(scene, { format: 'binary' }); + + expect(result).toBeInstanceOf(Uint8Array); + }); + + test('should include metadata when requested', () => { + scene.createEntity('Entity'); + + const result = SceneSerializer.serialize(scene, { includeMetadata: true }); + const parsed = JSON.parse(result as string); + + expect(parsed.metadata).toBeDefined(); + expect(parsed.metadata.entityCount).toBe(1); + expect(parsed.timestamp).toBeDefined(); + }); + + test('should pretty print JSON when requested', () => { + scene.createEntity('Entity'); + + const result = SceneSerializer.serialize(scene, { pretty: true }); + + expect(typeof result).toBe('string'); + expect((result as string).includes('\n')).toBe(true); + expect((result as string).includes(' ')).toBe(true); + }); + + test('should serialize scene data', () => { + scene.sceneData.set('level', 5); + scene.sceneData.set('config', { difficulty: 'hard' }); + + const result = SceneSerializer.serialize(scene); + const parsed = JSON.parse(result as string); + + expect(parsed.sceneData).toBeDefined(); + expect(parsed.sceneData.level).toBe(5); + expect(parsed.sceneData.config.difficulty).toBe('hard'); + }); + + test('should serialize with component filter', () => { + scene.createEntity('Entity1').addComponent(new PositionComponent()); + scene.createEntity('Entity2').addComponent(new VelocityComponent()); + + const result = SceneSerializer.serialize(scene, { + components: [PositionComponent] + }); + const parsed = JSON.parse(result as string); + + // Only entities with PositionComponent should be included + expect(parsed.entities.length).toBe(1); + }); + }); + + describe('deserialize', () => { + test('should deserialize scene from JSON string', () => { + scene.createEntity('Entity1').addComponent(new PositionComponent(100, 200)); + + const serialized = SceneSerializer.serialize(scene); + + const newScene = new Scene({ name: 'NewScene' }); + SceneSerializer.deserialize(newScene, serialized, { componentRegistry }); + + expect(newScene.entities.count).toBe(1); + const entity = newScene.findEntity('Entity1'); + expect(entity).not.toBeNull(); + expect(entity!.hasComponent(PositionComponent)).toBe(true); + + const pos = entity!.getComponent(PositionComponent)!; + expect(pos.x).toBe(100); + expect(pos.y).toBe(200); + + newScene.end(); + }); + + test('should deserialize scene from binary format', () => { + scene.createEntity('BinaryEntity').addComponent(new PositionComponent(50, 75)); + + const serialized = SceneSerializer.serialize(scene, { format: 'binary' }); + + const newScene = new Scene({ name: 'NewScene' }); + SceneSerializer.deserialize(newScene, serialized, { componentRegistry }); + + expect(newScene.entities.count).toBe(1); + const entity = newScene.findEntity('BinaryEntity'); + expect(entity).not.toBeNull(); + + newScene.end(); + }); + + test('should replace existing entities with strategy replace', () => { + scene.createEntity('Original'); + const serialized = SceneSerializer.serialize(scene); + + const targetScene = new Scene({ name: 'Target' }); + targetScene.createEntity('Existing1'); + targetScene.createEntity('Existing2'); + expect(targetScene.entities.count).toBe(2); + + SceneSerializer.deserialize(targetScene, serialized, { + strategy: 'replace', + componentRegistry + }); + + expect(targetScene.entities.count).toBe(1); + expect(targetScene.findEntity('Original')).not.toBeNull(); + expect(targetScene.findEntity('Existing1')).toBeNull(); + + targetScene.end(); + }); + + test('should merge with existing entities with strategy merge', () => { + scene.createEntity('FromSave'); + const serialized = SceneSerializer.serialize(scene); + + const targetScene = new Scene({ name: 'Target' }); + targetScene.createEntity('Existing'); + expect(targetScene.entities.count).toBe(1); + + SceneSerializer.deserialize(targetScene, serialized, { + strategy: 'merge', + componentRegistry + }); + + expect(targetScene.entities.count).toBe(2); + expect(targetScene.findEntity('Existing')).not.toBeNull(); + expect(targetScene.findEntity('FromSave')).not.toBeNull(); + + targetScene.end(); + }); + + test('should restore scene data', () => { + scene.sceneData.set('weather', 'sunny'); + scene.sceneData.set('time', 12.5); + + const serialized = SceneSerializer.serialize(scene); + + const newScene = new Scene({ name: 'NewScene' }); + SceneSerializer.deserialize(newScene, serialized, { componentRegistry }); + + expect(newScene.sceneData.get('weather')).toBe('sunny'); + expect(newScene.sceneData.get('time')).toBe(12.5); + + newScene.end(); + }); + + test('should call migration function when versions differ', () => { + scene.createEntity('Entity'); + const serialized = SceneSerializer.serialize(scene); + + // Manually modify version + const parsed = JSON.parse(serialized as string); + parsed.version = 0; + const modifiedSerialized = JSON.stringify(parsed); + + const migrationFn = jest.fn((oldVersion, newVersion, data) => { + expect(oldVersion).toBe(0); + return data; + }); + + const newScene = new Scene({ name: 'NewScene' }); + SceneSerializer.deserialize(newScene, modifiedSerialized, { + componentRegistry, + migration: migrationFn + }); + + expect(migrationFn).toHaveBeenCalled(); + + newScene.end(); + }); + + test('should throw on invalid JSON', () => { + const newScene = new Scene({ name: 'NewScene' }); + + expect(() => { + SceneSerializer.deserialize(newScene, 'invalid json{{{', { componentRegistry }); + }).toThrow(); + + newScene.end(); + }); + }); + + describe('validate', () => { + test('should validate correct save data', () => { + scene.createEntity('Entity'); + const serialized = SceneSerializer.serialize(scene); + + const result = SceneSerializer.validate(serialized as string); + + expect(result.valid).toBe(true); + expect(result.version).toBe(1); + }); + + test('should return errors for missing version', () => { + const invalid = JSON.stringify({ entities: [], componentTypeRegistry: [] }); + + const result = SceneSerializer.validate(invalid); + + expect(result.valid).toBe(false); + expect(result.errors).toContain('Missing version field'); + }); + + test('should return errors for missing entities', () => { + const invalid = JSON.stringify({ version: 1, componentTypeRegistry: [] }); + + const result = SceneSerializer.validate(invalid); + + expect(result.valid).toBe(false); + expect(result.errors).toContain('Missing or invalid entities field'); + }); + + test('should return errors for missing componentTypeRegistry', () => { + const invalid = JSON.stringify({ version: 1, entities: [] }); + + const result = SceneSerializer.validate(invalid); + + expect(result.valid).toBe(false); + expect(result.errors).toContain('Missing or invalid componentTypeRegistry field'); + }); + + test('should handle JSON parse errors', () => { + const result = SceneSerializer.validate('not valid json'); + + expect(result.valid).toBe(false); + expect(result.errors).toBeDefined(); + expect(result.errors![0]).toContain('JSON parse error'); + }); + }); + + describe('getInfo', () => { + test('should return info from save data', () => { + scene.name = 'InfoTestScene'; + scene.createEntity('Entity1'); + scene.createEntity('Entity2'); + scene.createEntity('Entity3'); + + const serialized = SceneSerializer.serialize(scene); + const info = SceneSerializer.getInfo(serialized as string); + + expect(info).not.toBeNull(); + expect(info!.name).toBe('InfoTestScene'); + expect(info!.entityCount).toBe(3); + expect(info!.version).toBe(1); + }); + + test('should return null for invalid data', () => { + const info = SceneSerializer.getInfo('invalid'); + + expect(info).toBeNull(); + }); + + test('should include timestamp when present', () => { + scene.createEntity('Entity'); + const serialized = SceneSerializer.serialize(scene, { includeMetadata: true }); + const info = SceneSerializer.getInfo(serialized as string); + + expect(info!.timestamp).toBeDefined(); + }); + }); + + describe('scene data serialization', () => { + test('should serialize Date objects', () => { + const date = new Date('2024-01-01T00:00:00Z'); + scene.sceneData.set('createdAt', date); + + const serialized = SceneSerializer.serialize(scene); + const parsed = JSON.parse(serialized as string); + + expect(parsed.sceneData.createdAt.__type).toBe('Date'); + }); + + test('should deserialize Date objects', () => { + const date = new Date('2024-01-01T00:00:00Z'); + scene.sceneData.set('createdAt', date); + + const serialized = SceneSerializer.serialize(scene); + + const newScene = new Scene({ name: 'NewScene' }); + SceneSerializer.deserialize(newScene, serialized, { componentRegistry }); + + const restoredDate = newScene.sceneData.get('createdAt'); + expect(restoredDate).toBeInstanceOf(Date); + expect(restoredDate.getTime()).toBe(date.getTime()); + + newScene.end(); + }); + + test('should serialize Map objects', () => { + const map = new Map([['key1', 'value1'], ['key2', 'value2']]); + scene.sceneData.set('mapping', map); + + const serialized = SceneSerializer.serialize(scene); + const parsed = JSON.parse(serialized as string); + + expect(parsed.sceneData.mapping.__type).toBe('Map'); + }); + + test('should deserialize Map objects', () => { + const map = new Map([['key1', 'value1'], ['key2', 'value2']]); + scene.sceneData.set('mapping', map); + + const serialized = SceneSerializer.serialize(scene); + + const newScene = new Scene({ name: 'NewScene' }); + SceneSerializer.deserialize(newScene, serialized, { componentRegistry }); + + const restoredMap = newScene.sceneData.get('mapping'); + expect(restoredMap).toBeInstanceOf(Map); + expect(restoredMap.get('key1')).toBe('value1'); + expect(restoredMap.get('key2')).toBe('value2'); + + newScene.end(); + }); + + test('should serialize Set objects', () => { + const set = new Set([1, 2, 3]); + scene.sceneData.set('numbers', set); + + const serialized = SceneSerializer.serialize(scene); + const parsed = JSON.parse(serialized as string); + + expect(parsed.sceneData.numbers.__type).toBe('Set'); + }); + + test('should deserialize Set objects', () => { + const set = new Set([1, 2, 3]); + scene.sceneData.set('numbers', set); + + const serialized = SceneSerializer.serialize(scene); + + const newScene = new Scene({ name: 'NewScene' }); + SceneSerializer.deserialize(newScene, serialized, { componentRegistry }); + + const restoredSet = newScene.sceneData.get('numbers'); + expect(restoredSet).toBeInstanceOf(Set); + expect(restoredSet.has(1)).toBe(true); + expect(restoredSet.has(2)).toBe(true); + expect(restoredSet.has(3)).toBe(true); + + newScene.end(); + }); + }); + + describe('hierarchy serialization', () => { + test('should serialize and deserialize entity hierarchy', () => { + const hierarchySystem = new HierarchySystem(); + scene.addSystem(hierarchySystem); + + const root = scene.createEntity('Root'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + + hierarchySystem.setParent(child1, root); + hierarchySystem.setParent(child2, root); + + const serialized = SceneSerializer.serialize(scene); + + const newScene = new Scene({ name: 'NewScene' }); + const newHierarchySystem = new HierarchySystem(); + newScene.addSystem(newHierarchySystem); + + SceneSerializer.deserialize(newScene, serialized, { componentRegistry }); + + const newRoot = newScene.findEntity('Root'); + expect(newRoot).not.toBeNull(); + + const children = newHierarchySystem.getChildren(newRoot!); + expect(children.length).toBe(2); + + newScene.end(); + }); + }); +}); diff --git a/packages/core/tests/ECS/Serialization/Serialization.test.ts b/packages/core/tests/ECS/Serialization/Serialization.test.ts deleted file mode 100644 index acedd0ac..00000000 --- a/packages/core/tests/ECS/Serialization/Serialization.test.ts +++ /dev/null @@ -1,629 +0,0 @@ -/** - * 序列化系统测试 - */ - -import { Component } from '../../../src/ECS/Component'; -import { Scene } from '../../../src/ECS/Scene'; -import { Entity } from '../../../src/ECS/Entity'; -import { - Serializable, - Serialize, - SerializeAsMap, - SerializeAsSet, - IgnoreSerialization, - ComponentSerializer, - EntitySerializer, - SceneSerializer, - VersionMigrationManager, - MigrationBuilder -} from '../../../src/ECS/Serialization'; -import { ECSComponent } from '../../../src/ECS/Decorators'; -import { ComponentRegistry } from '../../../src/ECS/Core/ComponentStorage'; - -// 测试组件定义 -@ECSComponent('Position') -@Serializable({ version: 1 }) -class PositionComponent extends Component { - @Serialize() - public x: number = 0; - - @Serialize() - public y: number = 0; - - constructor(x: number = 0, y: number = 0) { - super(); - this.x = x; - this.y = y; - } -} - -@ECSComponent('Velocity') -@Serializable({ version: 1 }) -class VelocityComponent extends Component { - @Serialize() - public dx: number = 0; - - @Serialize() - public dy: number = 0; -} - -@ECSComponent('Player') -@Serializable({ version: 1 }) -class PlayerComponent extends Component { - @Serialize() - public name: string = ''; - - @Serialize() - public level: number = 1; - - @SerializeAsMap() - public inventory: Map = new Map(); - - @SerializeAsSet() - public tags: Set = new Set(); - - @IgnoreSerialization() - public tempCache: any = null; -} - -@ECSComponent('Health') -@Serializable({ version: 1 }) -class HealthComponent extends Component { - @Serialize() - public current: number = 100; - - @Serialize() - public max: number = 100; -} - -// 非可序列化组件 -class NonSerializableComponent extends Component { - public data: any = null; -} - -describe('ECS Serialization System', () => { - let scene: Scene; - - beforeEach(() => { - // 清空测试环境 - ComponentRegistry.reset(); - - // 重新注册测试组件(因为reset会清空所有注册) - ComponentRegistry.register(PositionComponent); - ComponentRegistry.register(VelocityComponent); - ComponentRegistry.register(PlayerComponent); - ComponentRegistry.register(HealthComponent); - - // 创建测试场景 - scene = new Scene(); - }); - - describe('Component Serialization', () => { - it('should serialize a simple component', () => { - const position = new PositionComponent(100, 200); - const serialized = ComponentSerializer.serialize(position); - - expect(serialized).not.toBeNull(); - expect(serialized!.type).toBe('Position'); - expect(serialized!.version).toBe(1); - expect(serialized!.data.x).toBe(100); - expect(serialized!.data.y).toBe(200); - }); - - it('should deserialize a simple component', () => { - const serializedData = { - type: 'Position', - version: 1, - data: { x: 150, y: 250 } - }; - - const registry = ComponentRegistry.getAllComponentNames() as Map; - const component = ComponentSerializer.deserialize(serializedData, registry); - - expect(component).not.toBeNull(); - expect(component).toBeInstanceOf(PositionComponent); - expect((component as PositionComponent).x).toBe(150); - expect((component as PositionComponent).y).toBe(250); - }); - - it('should serialize Map fields', () => { - const player = new PlayerComponent(); - player.name = 'Hero'; - player.level = 5; - player.inventory.set('sword', 1); - player.inventory.set('potion', 10); - - const serialized = ComponentSerializer.serialize(player); - - expect(serialized).not.toBeNull(); - expect(serialized!.data.inventory).toEqual([ - ['sword', 1], - ['potion', 10] - ]); - }); - - it('should deserialize Map fields', () => { - const serializedData = { - type: 'Player', - version: 1, - data: { - name: 'Hero', - level: 5, - inventory: [ - ['sword', 1], - ['potion', 10] - ], - tags: ['warrior', 'hero'] - } - }; - - const registry = ComponentRegistry.getAllComponentNames() as Map; - const component = ComponentSerializer.deserialize( - serializedData, - registry - ) as PlayerComponent; - - expect(component).not.toBeNull(); - expect(component.inventory.get('sword')).toBe(1); - expect(component.inventory.get('potion')).toBe(10); - expect(component.tags.has('warrior')).toBe(true); - expect(component.tags.has('hero')).toBe(true); - }); - - it('should ignore fields marked with @IgnoreSerialization', () => { - const player = new PlayerComponent(); - player.tempCache = { foo: 'bar' }; - - const serialized = ComponentSerializer.serialize(player); - - expect(serialized).not.toBeNull(); - expect(serialized!.data.tempCache).toBeUndefined(); - }); - - it('should return null for non-serializable components', () => { - const nonSerializable = new NonSerializableComponent(); - const serialized = ComponentSerializer.serialize(nonSerializable); - - expect(serialized).toBeNull(); - }); - }); - - describe('Entity Serialization', () => { - it('should serialize an entity with components', () => { - const entity = scene.createEntity('Player'); - entity.addComponent(new PositionComponent(50, 100)); - entity.addComponent(new VelocityComponent()); - entity.tag = 10; - - const serialized = EntitySerializer.serialize(entity); - - expect(serialized.id).toBe(entity.id); - expect(serialized.name).toBe('Player'); - expect(serialized.tag).toBe(10); - expect(serialized.components.length).toBe(2); - }); - - it('should serialize entity hierarchy', () => { - const parent = scene.createEntity('Parent'); - const child = scene.createEntity('Child'); - - parent.addComponent(new PositionComponent(0, 0)); - child.addComponent(new PositionComponent(10, 10)); - parent.addChild(child); - - const serialized = EntitySerializer.serialize(parent); - - expect(serialized.children.length).toBe(1); - expect(serialized.children[0].id).toBe(child.id); - expect(serialized.children[0].name).toBe('Child'); - }); - - it('should deserialize an entity', () => { - const serializedEntity = { - id: 1, - name: 'TestEntity', - tag: 5, - active: true, - enabled: true, - updateOrder: 0, - components: [ - { - type: 'Position', - version: 1, - data: { x: 100, y: 200 } - } - ], - children: [] - }; - - const registry = ComponentRegistry.getAllComponentNames() as Map; - let idCounter = 10; - const entity = EntitySerializer.deserialize( - serializedEntity, - registry, - () => idCounter++, - false, - scene - ); - - expect(entity.name).toBe('TestEntity'); - expect(entity.tag).toBe(5); - expect(entity.components.length).toBe(1); - }); - }); - - describe('Scene Serialization', () => { - let scene: Scene; - - beforeEach(() => { - scene = new Scene({ name: 'TestScene' }); - }); - - afterEach(() => { - scene.end(); - }); - - it('should serialize a scene', () => { - const entity1 = scene.createEntity('Entity1'); - entity1.addComponent(new PositionComponent(10, 20)); - - const entity2 = scene.createEntity('Entity2'); - entity2.addComponent(new PlayerComponent()); - - const saveData = scene.serialize({ format: 'json', pretty: true }); - - expect(saveData).toBeTruthy(); - expect(typeof saveData).toBe('string'); - - const parsed = JSON.parse(saveData as string); - expect(parsed.name).toBe('TestScene'); - expect(parsed.version).toBe(1); - expect(parsed.entities.length).toBe(2); - }); - - it('should deserialize a scene with replace strategy', () => { - // 创建初始实体 - const entity1 = scene.createEntity('Initial'); - entity1.addComponent(new PositionComponent(0, 0)); - - // 序列化 - const entity2 = scene.createEntity('ToSave'); - entity2.addComponent(new PositionComponent(100, 100)); - const saveData = scene.serialize(); - - // 清空并重新加载 - scene.deserialize(saveData, { - strategy: 'replace', - // componentRegistry会自动从ComponentRegistry获取 - }); - - expect(scene.entities.count).toBeGreaterThan(0); - }); - - it('should filter components during serialization', () => { - const entity = scene.createEntity('Mixed'); - entity.addComponent(new PositionComponent(1, 2)); - entity.addComponent(new PlayerComponent()); - entity.addComponent(new HealthComponent()); - - const saveData = scene.serialize({ - components: [PositionComponent, PlayerComponent], - format: 'json' - }); - - const parsed = JSON.parse(saveData as string); - expect(parsed.entities.length).toBeGreaterThan(0); - }); - - it('should preserve entity hierarchy', () => { - const parent = scene.createEntity('Parent'); - const child = scene.createEntity('Child'); - parent.addChild(child); - - parent.addComponent(new PositionComponent(0, 0)); - child.addComponent(new PositionComponent(10, 10)); - - const saveData = scene.serialize({ format: 'json' }); - const parsed = JSON.parse(saveData as string); - - // 只有父实体在顶层 - expect(parsed.entities.length).toBe(1); - expect(parsed.entities[0].children.length).toBe(1); - }); - - it('should validate save data', () => { - const entity = scene.createEntity('Test'); - entity.addComponent(new PositionComponent(5, 5)); - - const saveData = scene.serialize({ format: 'json' }); - const validation = SceneSerializer.validate(saveData as string); - - expect(validation.valid).toBe(true); - expect(validation.version).toBe(1); - }); - - it('should get save data info', () => { - const entity = scene.createEntity('InfoTest'); - entity.addComponent(new PositionComponent(1, 1)); - - const saveData = scene.serialize({ format: 'json' }); - const info = SceneSerializer.getInfo(saveData as string); - - expect(info).not.toBeNull(); - expect(info!.name).toBe('TestScene'); - expect(info!.version).toBe(1); - }); - }); - - describe('Version Migration', () => { - @ECSComponent('OldPlayer') - @Serializable({ version: 1 }) - class OldPlayerV1 extends Component { - @Serialize() - public name: string = ''; - - @Serialize() - public hp: number = 100; - } - - @ECSComponent('OldPlayer') - @Serializable({ version: 2 }) - class OldPlayerV2 extends Component { - @Serialize() - public name: string = ''; - - @Serialize() - public health: number = 100; // 重命名了字段 - - @Serialize() - public maxHealth: number = 100; // 新增字段 - } - - beforeEach(() => { - VersionMigrationManager.clearMigrations(); - }); - - it('should migrate component from v1 to v2', () => { - // 注册迁移 - VersionMigrationManager.registerComponentMigration( - 'OldPlayer', - 1, - 2, - (data) => { - return { - name: data.name, - health: data.hp, - maxHealth: data.hp - }; - } - ); - - const v1Data = { - type: 'OldPlayer', - version: 1, - data: { name: 'Hero', hp: 80 } - }; - - const migrated = VersionMigrationManager.migrateComponent(v1Data, 2); - - expect(migrated.version).toBe(2); - expect(migrated.data.health).toBe(80); - expect(migrated.data.maxHealth).toBe(80); - expect(migrated.data.hp).toBeUndefined(); - }); - - it('should use MigrationBuilder for component migration', () => { - new MigrationBuilder() - .forComponent('Player') - .fromVersionToVersion(1, 2) - .migrate((data: any) => { - data.experience = 0; - return data; - }); - - expect(VersionMigrationManager.canMigrateComponent('Player', 1, 2)).toBe(true); - }); - - it('should check migration path availability', () => { - VersionMigrationManager.registerComponentMigration('Test', 1, 2, (d) => d); - VersionMigrationManager.registerComponentMigration('Test', 2, 3, (d) => d); - - expect(VersionMigrationManager.canMigrateComponent('Test', 1, 3)).toBe(true); - expect(VersionMigrationManager.canMigrateComponent('Test', 1, 4)).toBe(false); - }); - - it('should get migration path', () => { - VersionMigrationManager.registerComponentMigration('PathTest', 1, 2, (d) => d); - VersionMigrationManager.registerComponentMigration('PathTest', 2, 3, (d) => d); - - const path = VersionMigrationManager.getComponentMigrationPath('PathTest'); - - expect(path).toEqual([1, 2]); - }); - }); - - // ComponentTypeRegistry已被移除,现在使用ComponentRegistry自动管理组件类型 - - describe('Integration Tests', () => { - it('should perform full save/load cycle', () => { - const scene1 = new Scene({ name: 'SaveTest' }); - - // 创建复杂实体 - const player = scene1.createEntity('Player'); - const playerComp = new PlayerComponent(); - playerComp.name = 'TestHero'; - playerComp.level = 10; - playerComp.inventory.set('sword', 1); - playerComp.inventory.set('shield', 1); - playerComp.tags.add('warrior'); - - player.addComponent(playerComp); - player.addComponent(new PositionComponent(100, 200)); - player.addComponent(new HealthComponent()); - - // 创建子实体 - const weapon = scene1.createEntity('Weapon'); - weapon.addComponent(new PositionComponent(5, 0)); - player.addChild(weapon); - - // 序列化 - const saveData = scene1.serialize(); - - // 新场景 - const scene2 = new Scene({ name: 'LoadTest' }); - - // 反序列化 - scene2.deserialize(saveData, { - strategy: 'replace', - // componentRegistry会自动从ComponentRegistry获取 - }); - - // 验证 - const loadedPlayer = scene2.findEntity('Player'); - expect(loadedPlayer).not.toBeNull(); - - const loadedPlayerComp = loadedPlayer!.getComponent(PlayerComponent as any) as PlayerComponent; - expect(loadedPlayerComp).not.toBeNull(); - expect(loadedPlayerComp.name).toBe('TestHero'); - expect(loadedPlayerComp.level).toBe(10); - expect(loadedPlayerComp.inventory.get('sword')).toBe(1); - expect(loadedPlayerComp.tags.has('warrior')).toBe(true); - - // 验证层级结构 - expect(loadedPlayer!.childCount).toBe(1); - - scene1.end(); - scene2.end(); - }); - - it('should serialize and deserialize scene custom data', () => { - const scene1 = new Scene({ name: 'SceneDataTest' }); - - // 设置场景自定义数据 - scene1.sceneData.set('weather', 'rainy'); - scene1.sceneData.set('timeOfDay', 14.5); - scene1.sceneData.set('difficulty', 'hard'); - scene1.sceneData.set('checkpoint', { x: 100, y: 200 }); - scene1.sceneData.set('tags', new Set(['action', 'adventure'])); - scene1.sceneData.set('metadata', new Map([['author', 'test'], ['version', '1.0']])); - - // 序列化 - const saveData = scene1.serialize(); - - // 新场景 - const scene2 = new Scene({ name: 'LoadTest' }); - - // 反序列化 - scene2.deserialize(saveData, { - strategy: 'replace', - // componentRegistry会自动从ComponentRegistry获取 - }); - - // 验证场景数据 - expect(scene2.sceneData.get('weather')).toBe('rainy'); - expect(scene2.sceneData.get('timeOfDay')).toBe(14.5); - expect(scene2.sceneData.get('difficulty')).toBe('hard'); - expect(scene2.sceneData.get('checkpoint')).toEqual({ x: 100, y: 200 }); - - const tags = scene2.sceneData.get('tags'); - expect(tags).toBeInstanceOf(Set); - expect(tags.has('action')).toBe(true); - expect(tags.has('adventure')).toBe(true); - - const metadata = scene2.sceneData.get('metadata'); - expect(metadata).toBeInstanceOf(Map); - expect(metadata.get('author')).toBe('test'); - expect(metadata.get('version')).toBe('1.0'); - - scene1.end(); - scene2.end(); - }); - - it('should serialize and deserialize using binary format', () => { - const scene1 = new Scene({ name: 'BinaryTest' }); - - // 创建测试数据 - const player = scene1.createEntity('Player'); - const playerComp = new PlayerComponent(); - playerComp.name = 'BinaryHero'; - playerComp.level = 5; - playerComp.inventory.set('sword', 1); - player.addComponent(playerComp); - player.addComponent(new PositionComponent(100, 200)); - - scene1.sceneData.set('weather', 'sunny'); - scene1.sceneData.set('score', 9999); - - // 二进制序列化 - const binaryData = scene1.serialize({ format: 'binary' }); - - // 验证是Uint8Array类型 - expect(binaryData instanceof Uint8Array).toBe(true); - expect((binaryData as Uint8Array).length).toBeGreaterThan(0); - - // 新场景反序列化二进制数据 - const scene2 = new Scene({ name: 'LoadTest' }); - scene2.deserialize(binaryData, { - strategy: 'replace', - // componentRegistry会自动从ComponentRegistry获取 - }); - - // 验证数据完整性 - const loadedPlayer = scene2.findEntity('Player'); - expect(loadedPlayer).not.toBeNull(); - - const loadedPlayerComp = loadedPlayer!.getComponent(PlayerComponent as any) as PlayerComponent; - expect(loadedPlayerComp.name).toBe('BinaryHero'); - expect(loadedPlayerComp.level).toBe(5); - expect(loadedPlayerComp.inventory.get('sword')).toBe(1); - - const loadedPos = loadedPlayer!.getComponent(PositionComponent as any) as PositionComponent; - expect(loadedPos.x).toBe(100); - expect(loadedPos.y).toBe(200); - - expect(scene2.sceneData.get('weather')).toBe('sunny'); - expect(scene2.sceneData.get('score')).toBe(9999); - - scene1.end(); - scene2.end(); - }); - - it('should handle complex nested data in binary format', () => { - const scene1 = new Scene({ name: 'NestedBinaryTest' }); - - // 复杂嵌套数据 - scene1.sceneData.set('config', { - graphics: { - quality: 'high', - resolution: { width: 1920, height: 1080 } - }, - audio: { - masterVolume: 0.8, - effects: new Map([['music', 0.7], ['sfx', 0.9]]) - }, - tags: new Set(['multiplayer', 'ranked']), - timestamp: new Date('2024-01-01') - }); - - // 二进制序列化 - const binaryData = scene1.serialize({ format: 'binary' }); - - // 反序列化 - const scene2 = new Scene({ name: 'LoadTest' }); - scene2.deserialize(binaryData, { - // componentRegistry会自动从ComponentRegistry获取 - }); - - const config = scene2.sceneData.get('config'); - expect(config.graphics.quality).toBe('high'); - expect(config.graphics.resolution.width).toBe(1920); - expect(config.audio.masterVolume).toBe(0.8); - expect(config.audio.effects.get('music')).toBe(0.7); - expect(config.tags.has('multiplayer')).toBe(true); - expect(config.timestamp).toBeInstanceOf(Date); - - scene1.end(); - scene2.end(); - }); - }); -}); diff --git a/packages/core/tests/SceneQuery.test.ts b/packages/core/tests/SceneQuery.test.ts deleted file mode 100644 index 5f2ff952..00000000 --- a/packages/core/tests/SceneQuery.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Scene查询方法测试 - */ - -import { Component } from '../src/ECS/Component'; -import { Entity } from '../src/ECS/Entity'; -import { Scene } from '../src/ECS/Scene'; -import { Core } from '../src/Core'; -import { ECSComponent } from '../src/ECS/Decorators'; -import { EntitySystem } from '../src/ECS/Systems/EntitySystem'; - -@ECSComponent('Position') -class Position extends Component { - constructor(public x: number = 0, public y: number = 0) { - super(); - } -} - -@ECSComponent('Velocity') -class Velocity extends Component { - constructor(public dx: number = 0, public dy: number = 0) { - super(); - } -} - -@ECSComponent('Disabled') -class Disabled extends Component {} - -describe('Scene查询方法', () => { - let scene: Scene; - - beforeEach(() => { - Core.create({ debug: false, enableEntitySystems: true }); - scene = new Scene(); - scene.initialize(); - }); - - afterEach(() => { - scene.end(); - }); - - describe('基础查询方法', () => { - test('queryAll 查询拥有所有组件的实体', () => { - const e1 = scene.createEntity('E1'); - e1.addComponent(new Position(10, 20)); - e1.addComponent(new Velocity(1, 2)); - - const e2 = scene.createEntity('E2'); - e2.addComponent(new Position(30, 40)); - - const result = scene.queryAll(Position, Velocity); - - expect(result.entities).toHaveLength(1); - expect(result.entities[0]).toBe(e1); - }); - - test('queryAny 查询拥有任意组件的实体', () => { - const e1 = scene.createEntity('E1'); - e1.addComponent(new Position(10, 20)); - - const e2 = scene.createEntity('E2'); - e2.addComponent(new Velocity(1, 2)); - - const e3 = scene.createEntity('E3'); - e3.addComponent(new Disabled()); - - const result = scene.queryAny(Position, Velocity); - - expect(result.entities).toHaveLength(2); - }); - - test('queryNone 查询不包含指定组件的实体', () => { - const e1 = scene.createEntity('E1'); - e1.addComponent(new Position(10, 20)); - - const e2 = scene.createEntity('E2'); - e2.addComponent(new Position(30, 40)); - e2.addComponent(new Disabled()); - - const result = scene.queryNone(Disabled); - - expect(result.entities).toHaveLength(1); - expect(result.entities[0]).toBe(e1); - }); - }); - - describe('TypedQueryBuilder', () => { - test('scene.query() 创建类型安全的查询构建器', () => { - const e1 = scene.createEntity('E1'); - e1.addComponent(new Position(10, 20)); - e1.addComponent(new Velocity(1, 2)); - - const e2 = scene.createEntity('E2'); - e2.addComponent(new Position(30, 40)); - e2.addComponent(new Velocity(3, 4)); - e2.addComponent(new Disabled()); - - // 构建查询 - const query = scene.query() - .withAll(Position, Velocity) - .withNone(Disabled); - - const matcher = query.buildMatcher(); - - // 创建System使用这个matcher - class TestSystem extends EntitySystem { - public processedCount = 0; - - constructor() { - super(matcher); - } - - protected override process(entities: readonly Entity[]): void { - this.processedCount = entities.length; - } - } - - const system = new TestSystem(); - scene.addSystem(system); - scene.update(); - - // 应该只处理e1(e2被Disabled排除) - expect(system.processedCount).toBe(1); - }); - - test('TypedQueryBuilder 支持复杂查询', () => { - const e1 = scene.createEntity('E1'); - e1.addComponent(new Position(10, 20)); - e1.tag = 100; - - const e2 = scene.createEntity('E2'); - e2.addComponent(new Position(30, 40)); - e2.tag = 200; - - const query = scene.query() - .withAll(Position) - .withTag(100); - - const condition = query.getCondition(); - - expect(condition.all).toContain(Position as any); - expect(condition.tag).toBe(100); - }); - }); -}); diff --git a/packages/core/tests/TypeInference.test.ts b/packages/core/tests/TypeInference.test.ts deleted file mode 100644 index 7682d682..00000000 --- a/packages/core/tests/TypeInference.test.ts +++ /dev/null @@ -1,205 +0,0 @@ -/** - * TypeScript类型推断测试 - * - * 验证组件类型自动推断功能 - */ - -import { Component } from '../src/ECS/Component'; -import { Entity } from '../src/ECS/Entity'; -import { Scene } from '../src/ECS/Scene'; -import { Core } from '../src/Core'; -import { ECSComponent } from '../src/ECS/Decorators'; -import { requireComponent, tryGetComponent, getComponents } from '../src/ECS/TypedEntity'; - -// 测试组件 -@ECSComponent('Position') -class Position extends Component { - constructor(public x: number = 0, public y: number = 0) { - super(); - } -} - -@ECSComponent('Velocity') -class Velocity extends Component { - constructor(public dx: number = 0, public dy: number = 0) { - super(); - } -} - -@ECSComponent('Health') -class Health extends Component { - constructor(public value: number = 100, public maxValue: number = 100) { - super(); - } -} - -describe('TypeScript类型推断', () => { - let scene: Scene; - let entity: Entity; - - beforeEach(() => { - Core.create({ debug: false, enableEntitySystems: true }); - scene = new Scene(); - scene.initialize(); - entity = scene.createEntity('TestEntity'); - }); - - afterEach(() => { - scene.end(); - }); - - describe('Entity.getComponent 类型推断', () => { - test('getComponent 应该自动推断正确的返回类型', () => { - entity.addComponent(new Position(100, 200)); - - // 类型推断为 Position | null - const position = entity.getComponent(Position); - - // TypeScript应该知道position可能为null - expect(position).not.toBeNull(); - - // 在null检查后,TypeScript应该知道position是Position类型 - if (position) { - expect(position.x).toBe(100); - expect(position.y).toBe(200); - - // 这些操作应该有完整的类型提示 - position.x += 10; - position.y += 20; - - expect(position.x).toBe(110); - expect(position.y).toBe(220); - } - }); - - test('getComponent 返回null时类型安全', () => { - // 实体没有Velocity组件 - const velocity = entity.getComponent(Velocity); - - // 应该返回null - expect(velocity).toBeNull(); - }); - - test('多个不同类型组件的类型推断', () => { - entity.addComponent(new Position(10, 20)); - entity.addComponent(new Velocity(1, 2)); - entity.addComponent(new Health(100)); - - const pos = entity.getComponent(Position); - const vel = entity.getComponent(Velocity); - const health = entity.getComponent(Health); - - // 所有组件都应该被正确推断 - if (pos && vel && health) { - // Position类型的字段 - pos.x = 50; - pos.y = 60; - - // Velocity类型的字段 - vel.dx = 5; - vel.dy = 10; - - // Health类型的字段 - health.value = 80; - health.maxValue = 150; - - expect(pos.x).toBe(50); - expect(vel.dx).toBe(5); - expect(health.value).toBe(80); - } - }); - }); - - describe('Entity.createComponent 类型推断', () => { - test('createComponent 应该自动推断返回类型', () => { - // 应该推断为Position类型(非null) - const position = entity.createComponent(Position, 100, 200); - - expect(position).toBeInstanceOf(Position); - expect(position.x).toBe(100); - expect(position.y).toBe(200); - - // 应该有完整的类型提示 - position.x = 300; - expect(position.x).toBe(300); - }); - }); - - describe('Entity.hasComponent 类型守卫', () => { - test('hasComponent 可以用作类型守卫', () => { - entity.addComponent(new Position(10, 20)); - - if (entity.hasComponent(Position)) { - // 在这个作用域内,我们知道组件存在 - const pos = entity.getComponent(Position)!; - pos.x = 100; - expect(pos.x).toBe(100); - } - }); - }); - - describe('Entity.getOrCreateComponent 类型推断', () => { - test('getOrCreateComponent 应该自动推断返回类型', () => { - // 第一次调用:创建新组件 - const position1 = entity.getOrCreateComponent(Position, 50, 60); - expect(position1.x).toBe(50); - expect(position1.y).toBe(60); - - // 第二次调用:返回已存在的组件 - const position2 = entity.getOrCreateComponent(Position, 100, 200); - - // 应该是同一个组件 - expect(position2).toBe(position1); - expect(position2.x).toBe(50); // 值未改变 - }); - }); - - describe('TypedEntity工具函数类型推断', () => { - test('requireComponent 返回非空类型', () => { - entity.addComponent(new Position(100, 200)); - - // requireComponent 返回非null类型 - const position = requireComponent(entity, Position); - - // 不需要null检查 - expect(position.x).toBe(100); - position.x = 300; - expect(position.x).toBe(300); - }); - - test('tryGetComponent 返回可选类型', () => { - entity.addComponent(new Position(50, 50)); - - const position = tryGetComponent(entity, Position); - - // 应该返回组件 - expect(position).toBeDefined(); - if (position) { - expect(position.x).toBe(50); - } - - // 不存在的组件返回undefined - const velocity = tryGetComponent(entity, Velocity); - expect(velocity).toBeUndefined(); - }); - - test('getComponents 批量获取组件', () => { - entity.addComponent(new Position(10, 20)); - entity.addComponent(new Velocity(1, 2)); - entity.addComponent(new Health(100)); - - const [pos, vel, health] = getComponents(entity, Position, Velocity, Health); - - // 应该推断为数组类型 - expect(pos).not.toBeNull(); - expect(vel).not.toBeNull(); - expect(health).not.toBeNull(); - - if (pos && vel && health) { - expect(pos.x).toBe(10); - expect(vel.dx).toBe(1); - expect(health.value).toBe(100); - } - }); - }); -}); diff --git a/packages/core/tests/Utils/Debug/EntityDataCollector.test.ts b/packages/core/tests/Utils/Debug/EntityDataCollector.test.ts new file mode 100644 index 00000000..32f63661 --- /dev/null +++ b/packages/core/tests/Utils/Debug/EntityDataCollector.test.ts @@ -0,0 +1,410 @@ +import { EntityDataCollector } from '../../../src/Utils/Debug/EntityDataCollector'; +import { Scene } from '../../../src/ECS/Scene'; +import { Component } from '../../../src/ECS/Component'; +import { HierarchySystem } from '../../../src/ECS/Systems/HierarchySystem'; +import { HierarchyComponent } from '../../../src/ECS/Components/HierarchyComponent'; +import { ECSComponent } from '../../../src/ECS/Decorators'; + +@ECSComponent('TestPosition') +class PositionComponent extends Component { + public x: number = 0; + public y: number = 0; + + constructor(x: number = 0, y: number = 0) { + super(); + this.x = x; + this.y = y; + } +} + +@ECSComponent('TestVelocity') +class VelocityComponent extends Component { + public vx: number = 0; + public vy: number = 0; +} + +describe('EntityDataCollector', () => { + let collector: EntityDataCollector; + let scene: Scene; + + beforeEach(() => { + collector = new EntityDataCollector(); + scene = new Scene({ name: 'TestScene' }); + }); + + afterEach(() => { + scene.end(); + }); + + describe('collectEntityData', () => { + test('should return empty data when scene is null', () => { + const data = collector.collectEntityData(null); + + expect(data.totalEntities).toBe(0); + expect(data.activeEntities).toBe(0); + expect(data.pendingAdd).toBe(0); + expect(data.pendingRemove).toBe(0); + expect(data.entitiesPerArchetype).toEqual([]); + expect(data.topEntitiesByComponents).toEqual([]); + }); + + test('should return empty data when scene is undefined', () => { + const data = collector.collectEntityData(undefined); + + expect(data.totalEntities).toBe(0); + expect(data.activeEntities).toBe(0); + }); + + test('should collect entity data from scene', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new VelocityComponent()); + + const data = collector.collectEntityData(scene); + + expect(data.totalEntities).toBe(2); + expect(data.activeEntities).toBeGreaterThanOrEqual(0); + }); + + test('should collect archetype distribution', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent()); + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new PositionComponent()); + + const entity3 = scene.createEntity('Entity3'); + entity3.addComponent(new VelocityComponent()); + + const data = collector.collectEntityData(scene); + + expect(data.entitiesPerArchetype.length).toBeGreaterThan(0); + }); + }); + + describe('getRawEntityList', () => { + test('should return empty array when scene is null', () => { + const list = collector.getRawEntityList(null); + expect(list).toEqual([]); + }); + + test('should return raw entity list from scene', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + entity1.tag = 0x01; + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new VelocityComponent()); + + const list = collector.getRawEntityList(scene); + + expect(list.length).toBe(2); + expect(list[0].name).toBe('Entity1'); + expect(list[0].componentCount).toBe(1); + expect(list[0].tag).toBe(0x01); + }); + + test('should include hierarchy information', () => { + const hierarchySystem = new HierarchySystem(); + scene.addSystem(hierarchySystem); + + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + + const list = collector.getRawEntityList(scene); + const childInfo = list.find(e => e.name === 'Child'); + + expect(childInfo).toBeDefined(); + expect(childInfo!.parentId).toBe(parent.id); + expect(childInfo!.depth).toBe(1); + }); + }); + + describe('getEntityDetails', () => { + test('should return null when scene is null', () => { + const details = collector.getEntityDetails(1, null); + expect(details).toBeNull(); + }); + + test('should return null when entity not found', () => { + const details = collector.getEntityDetails(9999, scene); + expect(details).toBeNull(); + }); + + test('should return entity details', () => { + const entity = scene.createEntity('TestEntity'); + entity.addComponent(new PositionComponent(100, 200)); + entity.tag = 42; + + const details = collector.getEntityDetails(entity.id, scene); + + expect(details).not.toBeNull(); + expect(details.componentCount).toBe(1); + expect(details.scene).toBeDefined(); + }); + + test('should handle errors gracefully', () => { + const details = collector.getEntityDetails(-1, scene); + expect(details).toBeNull(); + }); + }); + + describe('collectEntityDataWithMemory', () => { + test('should return empty data when scene is null', () => { + const data = collector.collectEntityDataWithMemory(null); + + expect(data.totalEntities).toBe(0); + expect(data.entityHierarchy).toEqual([]); + expect(data.entityDetailsMap).toEqual({}); + }); + + test('should collect entity data with memory information', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent(10, 20)); + + const data = collector.collectEntityDataWithMemory(scene); + + expect(data.totalEntities).toBe(1); + expect(data.topEntitiesByComponents.length).toBeGreaterThan(0); + }); + + test('should include entity details map', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent()); + + const data = collector.collectEntityDataWithMemory(scene); + + expect(data.entityDetailsMap).toBeDefined(); + expect(data.entityDetailsMap![entity.id]).toBeDefined(); + }); + + test('should build entity hierarchy tree', () => { + const hierarchySystem = new HierarchySystem(); + scene.addSystem(hierarchySystem); + + const root = scene.createEntity('Root'); + root.addComponent(new HierarchyComponent()); + + const child = scene.createEntity('Child'); + hierarchySystem.setParent(child, root); + + const data = collector.collectEntityDataWithMemory(scene); + + expect(data.entityHierarchy).toBeDefined(); + expect(data.entityHierarchy!.length).toBe(1); + expect(data.entityHierarchy![0].name).toBe('Root'); + }); + }); + + describe('estimateEntityMemoryUsage', () => { + test('should estimate memory for entity', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent(10, 20)); + + const memory = collector.estimateEntityMemoryUsage(entity); + + expect(memory).toBeGreaterThanOrEqual(0); + expect(typeof memory).toBe('number'); + }); + + test('should return 0 for invalid entity', () => { + const memory = collector.estimateEntityMemoryUsage(null); + expect(memory).toBe(0); + }); + + test('should handle errors and return 0', () => { + const badEntity = { components: null }; + const memory = collector.estimateEntityMemoryUsage(badEntity); + expect(memory).toBeGreaterThanOrEqual(0); + }); + }); + + describe('calculateObjectSize', () => { + test('should return 0 for null/undefined', () => { + expect(collector.calculateObjectSize(null)).toBe(0); + expect(collector.calculateObjectSize(undefined)).toBe(0); + }); + + test('should calculate size for simple object', () => { + const obj = { x: 10, y: 20, name: 'test' }; + const size = collector.calculateObjectSize(obj); + + expect(size).toBeGreaterThan(0); + }); + + test('should respect exclude keys', () => { + const obj = { x: 10, excluded: 'large string'.repeat(100) }; + const sizeWithExclude = collector.calculateObjectSize(obj, ['excluded']); + const sizeWithoutExclude = collector.calculateObjectSize(obj); + + expect(sizeWithExclude).toBeLessThan(sizeWithoutExclude); + }); + + test('should handle nested objects with limited depth', () => { + const obj = { + level1: { + level2: { + level3: { + value: 42 + } + } + } + }; + + const size = collector.calculateObjectSize(obj); + expect(size).toBeGreaterThan(0); + }); + }); + + describe('extractComponentDetails', () => { + test('should extract component details', () => { + const component = new PositionComponent(100, 200); + const details = collector.extractComponentDetails([component]); + + expect(details.length).toBe(1); + expect(details[0].typeName).toBe('TestPosition'); + expect(details[0].properties.x).toBe(100); + expect(details[0].properties.y).toBe(200); + }); + + test('should handle empty components array', () => { + const details = collector.extractComponentDetails([]); + expect(details).toEqual([]); + }); + + test('should skip private properties', () => { + class ComponentWithPrivate extends Component { + public publicValue: number = 1; + private _privateValue: number = 2; + } + + const component = new ComponentWithPrivate(); + const details = collector.extractComponentDetails([component]); + + expect(details[0].properties.publicValue).toBe(1); + expect(details[0].properties._privateValue).toBeUndefined(); + }); + }); + + describe('getComponentProperties', () => { + test('should return empty object when scene is null', () => { + const props = collector.getComponentProperties(1, 0, null); + expect(props).toEqual({}); + }); + + test('should return empty object when entity not found', () => { + const props = collector.getComponentProperties(9999, 0, scene); + expect(props).toEqual({}); + }); + + test('should return empty object when component index is out of bounds', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent()); + + const props = collector.getComponentProperties(entity.id, 99, scene); + expect(props).toEqual({}); + }); + + test('should return component properties', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent(50, 75)); + + const props = collector.getComponentProperties(entity.id, 0, scene); + + expect(props.x).toBe(50); + expect(props.y).toBe(75); + }); + }); + + describe('expandLazyObject', () => { + test('should return null when scene is null', () => { + const result = collector.expandLazyObject(1, 0, 'path', null); + expect(result).toBeNull(); + }); + + test('should return null when entity not found', () => { + const result = collector.expandLazyObject(9999, 0, 'path', scene); + expect(result).toBeNull(); + }); + + test('should return null when component index is out of bounds', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent()); + + const result = collector.expandLazyObject(entity.id, 99, '', scene); + expect(result).toBeNull(); + }); + + test('should expand object at path', () => { + class ComponentWithNested extends Component { + public nested = { value: 42 }; + } + + const entity = scene.createEntity('Entity'); + entity.addComponent(new ComponentWithNested()); + + const result = collector.expandLazyObject(entity.id, 0, 'nested', scene); + + expect(result).toBeDefined(); + expect(result.value).toBe(42); + }); + + test('should handle array index in path', () => { + class ComponentWithArray extends Component { + public items = [{ id: 1 }, { id: 2 }]; + } + + const entity = scene.createEntity('Entity'); + entity.addComponent(new ComponentWithArray()); + + const result = collector.expandLazyObject(entity.id, 0, 'items[1]', scene); + + expect(result).toBeDefined(); + expect(result.id).toBe(2); + }); + }); + + describe('edge cases', () => { + test('should handle scene without entities buffer', () => { + const mockScene = { + entities: null, + getSystem: () => null + }; + + const data = collector.collectEntityData(mockScene as any); + expect(data.totalEntities).toBe(0); + }); + + test('should handle entity with long string properties', () => { + class ComponentWithLongString extends Component { + public longText = 'x'.repeat(300); + } + + const entity = scene.createEntity('Entity'); + entity.addComponent(new ComponentWithLongString()); + + const details = collector.extractComponentDetails(entity.components); + + expect(details[0].properties.longText).toContain('[长字符串:'); + }); + + test('should handle entity with large arrays', () => { + class ComponentWithLargeArray extends Component { + public items = Array.from({ length: 20 }, (_, i) => i); + } + + const entity = scene.createEntity('Entity'); + entity.addComponent(new ComponentWithLargeArray()); + + const details = collector.extractComponentDetails(entity.components); + + expect(details[0].properties.items).toBeDefined(); + expect(details[0].properties.items._isLazyArray).toBe(true); + expect(details[0].properties.items._arrayLength).toBe(20); + }); + }); +}); diff --git a/packages/core/tests/Utils/Profiler/AutoProfiler.test.ts b/packages/core/tests/Utils/Profiler/AutoProfiler.test.ts new file mode 100644 index 00000000..a159be10 --- /dev/null +++ b/packages/core/tests/Utils/Profiler/AutoProfiler.test.ts @@ -0,0 +1,417 @@ +import { AutoProfiler, Profile, ProfileClass } from '../../../src/Utils/Profiler/AutoProfiler'; +import { ProfilerSDK } from '../../../src/Utils/Profiler/ProfilerSDK'; +import { ProfileCategory } from '../../../src/Utils/Profiler/ProfilerTypes'; + +describe('AutoProfiler', () => { + beforeEach(() => { + AutoProfiler.resetInstance(); + ProfilerSDK.reset(); + ProfilerSDK.setEnabled(true); + }); + + afterEach(() => { + AutoProfiler.resetInstance(); + ProfilerSDK.reset(); + }); + + describe('getInstance', () => { + test('should return singleton instance', () => { + const instance1 = AutoProfiler.getInstance(); + const instance2 = AutoProfiler.getInstance(); + expect(instance1).toBe(instance2); + }); + + test('should accept custom config', () => { + const instance = AutoProfiler.getInstance({ minDuration: 1.0 }); + expect(instance).toBeDefined(); + }); + }); + + describe('resetInstance', () => { + test('should reset the singleton instance', () => { + const instance1 = AutoProfiler.getInstance(); + AutoProfiler.resetInstance(); + const instance2 = AutoProfiler.getInstance(); + expect(instance1).not.toBe(instance2); + }); + }); + + describe('setEnabled', () => { + test('should enable/disable auto profiling', () => { + AutoProfiler.setEnabled(false); + const instance = AutoProfiler.getInstance(); + expect(instance).toBeDefined(); + + AutoProfiler.setEnabled(true); + expect(instance).toBeDefined(); + }); + }); + + describe('wrapFunction', () => { + test('should wrap a synchronous function', () => { + ProfilerSDK.beginFrame(); + + const originalFn = (a: number, b: number) => a + b; + const wrappedFn = AutoProfiler.wrapFunction(originalFn, 'add', ProfileCategory.Custom); + + const result = wrappedFn(2, 3); + expect(result).toBe(5); + + ProfilerSDK.endFrame(); + }); + + test('should preserve function behavior', () => { + const originalFn = (x: number) => x * 2; + const wrappedFn = AutoProfiler.wrapFunction(originalFn, 'double', ProfileCategory.Script); + + ProfilerSDK.beginFrame(); + expect(wrappedFn(5)).toBe(10); + expect(wrappedFn(0)).toBe(0); + expect(wrappedFn(-3)).toBe(-6); + ProfilerSDK.endFrame(); + }); + + test('should handle async functions', async () => { + const asyncFn = async (x: number) => { + await new Promise((resolve) => setTimeout(resolve, 1)); + return x * 2; + }; + + const wrappedFn = AutoProfiler.wrapFunction(asyncFn, 'asyncDouble', ProfileCategory.Script); + + ProfilerSDK.beginFrame(); + const result = await wrappedFn(5); + expect(result).toBe(10); + ProfilerSDK.endFrame(); + }); + + test('should handle function that throws error', () => { + const errorFn = () => { + throw new Error('Test error'); + }; + + const wrappedFn = AutoProfiler.wrapFunction(errorFn, 'errorFn', ProfileCategory.Script); + + ProfilerSDK.beginFrame(); + expect(() => wrappedFn()).toThrow('Test error'); + ProfilerSDK.endFrame(); + }); + + test('should return original function when disabled', () => { + AutoProfiler.setEnabled(false); + + const originalFn = (x: number) => x + 1; + const wrappedFn = AutoProfiler.wrapFunction(originalFn, 'increment', ProfileCategory.Script); + + expect(wrappedFn(5)).toBe(6); + }); + }); + + describe('wrapInstance', () => { + test('should wrap all methods of an object', () => { + class Calculator { + add(a: number, b: number): number { + return a + b; + } + subtract(a: number, b: number): number { + return a - b; + } + } + + const calc = new Calculator(); + AutoProfiler.wrapInstance(calc, 'Calculator', ProfileCategory.Script); + + ProfilerSDK.beginFrame(); + expect(calc.add(5, 3)).toBe(8); + expect(calc.subtract(5, 3)).toBe(2); + ProfilerSDK.endFrame(); + }); + + test('should not wrap already wrapped objects', () => { + class MyClass { + getValue(): number { + return 42; + } + } + + const obj = new MyClass(); + AutoProfiler.wrapInstance(obj, 'MyClass', ProfileCategory.Custom); + AutoProfiler.wrapInstance(obj, 'MyClass', ProfileCategory.Custom); + + ProfilerSDK.beginFrame(); + expect(obj.getValue()).toBe(42); + ProfilerSDK.endFrame(); + }); + + test('should return object unchanged when disabled', () => { + AutoProfiler.setEnabled(false); + + class MyClass { + getValue(): number { + return 42; + } + } + + const obj = new MyClass(); + const wrapped = AutoProfiler.wrapInstance(obj, 'MyClass', ProfileCategory.Custom); + + expect(wrapped).toBe(obj); + }); + + test('should exclude methods matching exclude patterns', () => { + class MyClass { + getValue(): number { + return 42; + } + _privateMethod(): number { + return 1; + } + getName(): string { + return 'test'; + } + isValid(): boolean { + return true; + } + hasData(): boolean { + return true; + } + } + + const obj = new MyClass(); + AutoProfiler.wrapInstance(obj, 'MyClass', ProfileCategory.Custom); + + ProfilerSDK.beginFrame(); + expect(obj.getValue()).toBe(42); + expect(obj._privateMethod()).toBe(1); + expect(obj.getName()).toBe('test'); + expect(obj.isValid()).toBe(true); + expect(obj.hasData()).toBe(true); + ProfilerSDK.endFrame(); + }); + }); + + describe('registerClass', () => { + test('should register a class for auto profiling', () => { + class MySystem { + update(): void { + // Do something + } + } + + const RegisteredClass = AutoProfiler.registerClass(MySystem, ProfileCategory.ECS); + + ProfilerSDK.beginFrame(); + const instance = new RegisteredClass(); + instance.update(); + ProfilerSDK.endFrame(); + + expect(instance).toBeInstanceOf(MySystem); + }); + + test('should accept custom class name', () => { + class MySystem { + process(): number { + return 1; + } + } + + const RegisteredClass = AutoProfiler.registerClass(MySystem, ProfileCategory.ECS, 'CustomSystem'); + + ProfilerSDK.beginFrame(); + const instance = new RegisteredClass(); + expect(instance.process()).toBe(1); + ProfilerSDK.endFrame(); + }); + }); + + describe('sampling profiler', () => { + test('should start and stop sampling', () => { + AutoProfiler.startSampling(); + const samples = AutoProfiler.stopSampling(); + + expect(Array.isArray(samples)).toBe(true); + }); + + test('should return empty array when sampling was never started', () => { + const samples = AutoProfiler.stopSampling(); + expect(samples).toEqual([]); + }); + + test('should collect samples during execution', async () => { + AutoProfiler.startSampling(); + + // Do some work + for (let i = 0; i < 100; i++) { + Math.sqrt(i); + } + + // Wait a bit for samples to accumulate + await new Promise((resolve) => setTimeout(resolve, 50)); + + const samples = AutoProfiler.stopSampling(); + expect(Array.isArray(samples)).toBe(true); + }); + }); + + describe('dispose', () => { + test('should clean up resources', () => { + const instance = AutoProfiler.getInstance(); + instance.startSampling(); + instance.dispose(); + + // After dispose, stopping sampling should return empty array + const samples = instance.stopSampling(); + expect(samples).toEqual([]); + }); + }); + + describe('minDuration filtering', () => { + test('should respect minDuration setting', () => { + AutoProfiler.resetInstance(); + const instance = AutoProfiler.getInstance({ minDuration: 1000 }); + + const quickFn = () => 1; + const wrappedFn = instance.wrapFunction(quickFn, 'quickFn', ProfileCategory.Script); + + ProfilerSDK.beginFrame(); + expect(wrappedFn()).toBe(1); + ProfilerSDK.endFrame(); + }); + }); +}); + +describe('@Profile decorator', () => { + beforeEach(() => { + ProfilerSDK.reset(); + ProfilerSDK.setEnabled(true); + }); + + afterEach(() => { + ProfilerSDK.reset(); + }); + + test('should profile decorated methods', () => { + class TestClass { + @Profile() + calculate(): number { + return 42; + } + } + + const instance = new TestClass(); + + ProfilerSDK.beginFrame(); + const result = instance.calculate(); + ProfilerSDK.endFrame(); + + expect(result).toBe(42); + }); + + test('should use custom name when provided', () => { + class TestClass { + @Profile('CustomMethodName', ProfileCategory.Physics) + compute(): number { + return 100; + } + } + + const instance = new TestClass(); + + ProfilerSDK.beginFrame(); + expect(instance.compute()).toBe(100); + ProfilerSDK.endFrame(); + }); + + test('should handle async methods', async () => { + class TestClass { + @Profile() + async asyncMethod(): Promise { + await new Promise((resolve) => setTimeout(resolve, 1)); + return 99; + } + } + + const instance = new TestClass(); + + ProfilerSDK.beginFrame(); + const result = await instance.asyncMethod(); + ProfilerSDK.endFrame(); + + expect(result).toBe(99); + }); + + test('should handle method that throws error', () => { + class TestClass { + @Profile() + throwingMethod(): void { + throw new Error('Decorated error'); + } + } + + const instance = new TestClass(); + + ProfilerSDK.beginFrame(); + expect(() => instance.throwingMethod()).toThrow('Decorated error'); + ProfilerSDK.endFrame(); + }); + + test('should skip profiling when ProfilerSDK is disabled', () => { + ProfilerSDK.setEnabled(false); + + class TestClass { + @Profile() + simpleMethod(): number { + return 1; + } + } + + const instance = new TestClass(); + expect(instance.simpleMethod()).toBe(1); + }); +}); + +describe('@ProfileClass decorator', () => { + beforeEach(() => { + AutoProfiler.resetInstance(); + ProfilerSDK.reset(); + ProfilerSDK.setEnabled(true); + }); + + afterEach(() => { + AutoProfiler.resetInstance(); + ProfilerSDK.reset(); + }); + + test('should profile all methods of decorated class', () => { + @ProfileClass(ProfileCategory.ECS) + class GameSystem { + update(): void { + // Update logic + } + + render(): number { + return 1; + } + } + + ProfilerSDK.beginFrame(); + const system = new GameSystem(); + system.update(); + expect(system.render()).toBe(1); + ProfilerSDK.endFrame(); + }); + + test('should use default category when not specified', () => { + @ProfileClass() + class DefaultSystem { + process(): boolean { + return true; + } + } + + ProfilerSDK.beginFrame(); + const system = new DefaultSystem(); + expect(system.process()).toBe(true); + ProfilerSDK.endFrame(); + }); +}); diff --git a/packages/ecs-engine-bindgen/package.json b/packages/ecs-engine-bindgen/package.json index 42ca2939..5f1699d3 100644 --- a/packages/ecs-engine-bindgen/package.json +++ b/packages/ecs-engine-bindgen/package.json @@ -2,25 +2,23 @@ "name": "@esengine/ecs-engine-bindgen", "version": "0.1.0", "description": "Bridge layer between ECS Framework and Rust Engine | ECS框架与Rust引擎之间的桥接层", - "main": "bin/index.js", - "module": "bin/index.js", - "types": "bin/index.d.ts", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", "exports": { ".": { - "types": "./bin/index.d.ts", - "import": "./bin/index.js", - "require": "./bin/index.js", - "default": "./bin/index.js" + "types": "./dist/index.d.ts", + "import": "./dist/index.js" } }, "files": [ - "bin", "dist" ], "scripts": { - "build": "tsc", - "build:watch": "tsc --watch", - "clean": "rimraf bin dist" + "build": "tsup", + "build:watch": "tsup --watch", + "clean": "rimraf dist" }, "repository": { "type": "git", @@ -36,15 +34,16 @@ ], "author": "ESEngine Team", "license": "MIT", - "peerDependencies": { - "@esengine/ecs-framework": "workspace:*", - "@esengine/ecs-components": "workspace:*", - "@esengine/asset-system": "workspace:*" - }, "optionalDependencies": { "es-engine": "file:../engine/pkg" }, "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/sprite": "workspace:*", + "@esengine/camera": "workspace:*", + "@esengine/asset-system": "workspace:*", + "tsup": "^8.5.1", "typescript": "^5.8.0", "rimraf": "^5.0.0" } diff --git a/packages/ecs-engine-bindgen/src/core/SpriteRenderHelper.ts b/packages/ecs-engine-bindgen/src/core/SpriteRenderHelper.ts index 73214220..d00b6ac9 100644 --- a/packages/ecs-engine-bindgen/src/core/SpriteRenderHelper.ts +++ b/packages/ecs-engine-bindgen/src/core/SpriteRenderHelper.ts @@ -6,7 +6,7 @@ import { Entity, Component } from '@esengine/ecs-framework'; import type { EngineBridge } from './EngineBridge'; import { RenderBatcher } from './RenderBatcher'; -import { SpriteComponent } from '@esengine/ecs-components'; +import { SpriteComponent } from '@esengine/sprite'; import type { SpriteRenderData } from '../types'; /** @@ -20,6 +20,12 @@ export interface ITransformComponent { position: { x: number; y: number; z?: number }; rotation: number | { x: number; y: number; z: number }; scale: { x: number; y: number; z?: number }; + /** 世界位置(由 TransformSystem 计算,考虑父级变换) */ + worldPosition?: { x: number; y: number; z?: number }; + /** 世界旋转(由 TransformSystem 计算,考虑父级变换) */ + worldRotation?: { x: number; y: number; z: number }; + /** 世界缩放(由 TransformSystem 计算,考虑父级变换) */ + worldScale?: { x: number; y: number; z?: number }; } /** diff --git a/packages/ecs-engine-bindgen/src/index.ts b/packages/ecs-engine-bindgen/src/index.ts index af231e0f..51abf0d1 100644 --- a/packages/ecs-engine-bindgen/src/index.ts +++ b/packages/ecs-engine-bindgen/src/index.ts @@ -5,9 +5,11 @@ * @packageDocumentation */ -export { EngineBridge, EngineBridgeConfig } from './core/EngineBridge'; +export { EngineBridge } from './core/EngineBridge'; +export type { EngineBridgeConfig } from './core/EngineBridge'; export { RenderBatcher } from './core/RenderBatcher'; -export { SpriteRenderHelper, ITransformComponent } from './core/SpriteRenderHelper'; +export { SpriteRenderHelper } from './core/SpriteRenderHelper'; +export type { ITransformComponent } from './core/SpriteRenderHelper'; export { EngineRenderSystem, type TransformComponentType, type IRenderDataProvider, type IUIRenderDataProvider, type GizmoDataProviderFn, type HasGizmoProviderFn, type ProviderRenderData } from './systems/EngineRenderSystem'; export { CameraSystem } from './systems/CameraSystem'; export * from './types'; diff --git a/packages/ecs-engine-bindgen/src/systems/CameraSystem.ts b/packages/ecs-engine-bindgen/src/systems/CameraSystem.ts index 6281ce3e..a2663505 100644 --- a/packages/ecs-engine-bindgen/src/systems/CameraSystem.ts +++ b/packages/ecs-engine-bindgen/src/systems/CameraSystem.ts @@ -4,7 +4,7 @@ */ import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework'; -import { CameraComponent } from '@esengine/ecs-components'; +import { CameraComponent } from '@esengine/camera'; import type { EngineBridge } from '../core/EngineBridge'; @ECSSystem('Camera', { updateOrder: -100 }) diff --git a/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts b/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts index cd0fd251..fef29376 100644 --- a/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts +++ b/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts @@ -4,7 +4,9 @@ */ import { EntitySystem, Matcher, Entity, ComponentType, ECSSystem, Component, Core } from '@esengine/ecs-framework'; -import { SpriteComponent, CameraComponent, TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent } from '@esengine/sprite'; +import { CameraComponent } from '@esengine/camera'; import type { EngineBridge } from '../core/EngineBridge'; import { RenderBatcher } from '../core/RenderBatcher'; import type { SpriteRenderData } from '../types'; @@ -269,10 +271,12 @@ export class EngineRenderSystem extends EntitySystem { } } - // Handle rotation as number or Vector3 (use z for 2D) - const rotation = typeof transform.rotation === 'number' - ? transform.rotation - : transform.rotation.z; + // 使用世界变换(由 TransformSystem 计算,考虑父级变换),回退到本地变换 + const pos = transform.worldPosition ?? transform.position; + const scl = transform.worldScale ?? transform.scale; + const rot = transform.worldRotation + ? transform.worldRotation.z + : (typeof transform.rotation === 'number' ? transform.rotation : transform.rotation.z); // Convert hex color string to packed RGBA | 将十六进制颜色字符串转换为打包的RGBA const color = this.hexToPackedColor(sprite.color, sprite.alpha); @@ -286,14 +290,14 @@ export class EngineRenderSystem extends EntitySystem { textureId = this.bridge.getOrLoadTextureByPath(sprite.texture); } - // Pass actual display dimensions (sprite size * transform scale) - // 传递实际显示尺寸(sprite尺寸 * 变换缩放) + // Pass actual display dimensions (sprite size * world transform scale) + // 传递实际显示尺寸(sprite尺寸 * 世界变换缩放) const renderData: SpriteRenderData = { - x: transform.position.x, - y: transform.position.y, - rotation, - scaleX: sprite.width * transform.scale.x, - scaleY: sprite.height * transform.scale.y, + x: pos.x, + y: pos.y, + rotation: rot, + scaleX: sprite.width * scl.x, + scaleY: sprite.height * scl.y, originX: sprite.anchorX, originY: sprite.anchorY, textureId, diff --git a/packages/ecs-engine-bindgen/tsconfig.build.json b/packages/ecs-engine-bindgen/tsconfig.build.json new file mode 100644 index 00000000..0aa96c5a --- /dev/null +++ b/packages/ecs-engine-bindgen/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist" + }, + "exclude": ["node_modules", "dist", "bin", "**/*.test.ts"] +} diff --git a/packages/ecs-engine-bindgen/tsconfig.json b/packages/ecs-engine-bindgen/tsconfig.json index fb4d04e9..aeab6ae6 100644 --- a/packages/ecs-engine-bindgen/tsconfig.json +++ b/packages/ecs-engine-bindgen/tsconfig.json @@ -1,22 +1,9 @@ { + "extends": "../../tsconfig.base.json", "compilerOptions": { - "target": "ES2020", - "module": "ES2020", - "lib": ["ES2020", "DOM"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "./bin", - "rootDir": "./src", - "strict": true, "composite": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true + "outDir": "./dist", + "rootDir": "./src" }, "include": ["src/**/*"], "exclude": ["node_modules", "bin", "dist"] diff --git a/packages/ecs-engine-bindgen/tsup.config.ts b/packages/ecs-engine-bindgen/tsup.config.ts new file mode 100644 index 00000000..eb7dbc8c --- /dev/null +++ b/packages/ecs-engine-bindgen/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset({ + external: ['es-engine'] + }), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/editor-app/package.json b/packages/editor-app/package.json index 92fe15d2..0d478ece 100644 --- a/packages/editor-app/package.json +++ b/packages/editor-app/package.json @@ -17,16 +17,25 @@ "dependencies": { "@esengine/asset-system": "workspace:*", "@esengine/behavior-tree": "workspace:*", + "@esengine/behavior-tree-editor": "workspace:*", "@esengine/blueprint": "workspace:*", + "@esengine/blueprint-editor": "workspace:*", "@esengine/editor-runtime": "workspace:*", - "@esengine/ecs-components": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/sprite": "workspace:*", + "@esengine/camera": "workspace:*", + "@esengine/audio": "workspace:*", "@esengine/physics-rapier2d": "workspace:*", + "@esengine/physics-rapier2d-editor": "workspace:*", "@esengine/tilemap": "workspace:*", + "@esengine/tilemap-editor": "workspace:*", "@esengine/ui": "workspace:*", + "@esengine/ui-editor": "workspace:*", "@esengine/ecs-engine-bindgen": "workspace:*", "@esengine/ecs-framework": "workspace:*", "@esengine/editor-core": "workspace:*", "@esengine/engine": "workspace:*", + "@esengine/runtime-core": "workspace:*", "@monaco-editor/react": "^4.7.0", "@tauri-apps/api": "^2.2.0", "@tauri-apps/plugin-cli": "^2.4.1", diff --git a/packages/editor-app/src-tauri/src/profiler_ws.rs b/packages/editor-app/src-tauri/src/profiler_ws.rs index f9110c7c..889272d2 100644 --- a/packages/editor-app/src-tauri/src/profiler_ws.rs +++ b/packages/editor-app/src-tauri/src/profiler_ws.rs @@ -43,7 +43,6 @@ impl ProfilerServer { result = listener.accept() => { match result { Ok((stream, peer_addr)) => { - println!("[ProfilerServer] New connection from: {}", peer_addr); let tx = tx.clone(); tokio::spawn(handle_connection(stream, peer_addr, tx)); } @@ -98,7 +97,12 @@ async fn handle_connection( let ws_stream = match accept_async(stream).await { Ok(ws) => ws, Err(e) => { - eprintln!("[ProfilerServer] WebSocket error: {}", e); + // 忽略非 WebSocket 连接的错误(如普通 HTTP 请求、健康检查等) + // 这些是正常现象,不需要输出错误日志 + let error_str = e.to_string(); + if !error_str.contains("Connection: upgrade") && !error_str.contains("protocol error") { + eprintln!("[ProfilerServer] WebSocket error: {}", e); + } return; } }; diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index 224ffa29..01536f42 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -34,9 +34,7 @@ import { ProjectCreationWizard } from './components/ProjectCreationWizard'; import { SceneHierarchy } from './components/SceneHierarchy'; import { Inspector } from './components/inspectors/Inspector'; import { AssetBrowser } from './components/AssetBrowser'; -import { ConsolePanel } from './components/ConsolePanel'; import { Viewport } from './components/Viewport'; -import { ProfilerWindow } from './components/ProfilerWindow'; import { AdvancedProfilerWindow } from './components/AdvancedProfilerWindow'; import { PortManager } from './components/PortManager'; import { SettingsWindow } from './components/SettingsWindow'; @@ -110,7 +108,6 @@ function App() { const [panels, setPanels] = useState([]); const [pluginUpdateTrigger, setPluginUpdateTrigger] = useState(0); const [isRemoteConnected, setIsRemoteConnected] = useState(false); - const [isProfilerMode, setIsProfilerMode] = useState(false); const [showProjectWizard, setShowProjectWizard] = useState(false); const { @@ -372,6 +369,9 @@ function App() { await projectService.openProject(projectPath); + // 注意:插件配置会在引擎初始化后加载和激活 + // Note: Plugin config will be loaded and activated after engine initialization + // 设置 Tauri project:// 协议的基础路径(用于加载插件等项目文件) await TauriAPI.setProjectBasePath(projectPath); @@ -392,6 +392,19 @@ function App() { throw new Error(locale === 'zh' ? '引擎初始化超时' : 'Engine initialization timeout'); } + // 加载项目插件配置并激活插件(在引擎初始化后、模块系统初始化前) + // Load project plugin config and activate plugins (after engine init, before module system init) + if (pluginManager) { + const pluginSettings = projectService.getPluginSettings(); + console.log('[App] Plugin settings from project:', pluginSettings); + if (pluginSettings && pluginSettings.enabledPlugins.length > 0) { + console.log('[App] Loading plugin config:', pluginSettings.enabledPlugins); + await pluginManager.loadConfig({ enabledPlugins: pluginSettings.enabledPlugins }); + } else { + console.log('[App] No plugin settings found in project config'); + } + } + // 初始化模块系统(所有插件的 runtimeModule 会在 PluginManager 安装时自动注册) await engineService.initializeModuleSystems(); @@ -520,13 +533,6 @@ function App() { } }; - const handleProfilerMode = async () => { - setIsProfilerMode(true); - setIsRemoteConnected(true); - setProjectLoaded(true); - setStatus(t('header.status.profilerMode') || 'Profiler Mode - Waiting for connection...'); - }; - const handleNewScene = async () => { if (!sceneManager) { console.error('SceneManagerService not available'); @@ -631,7 +637,6 @@ function App() { setProjectLoaded(false); setCurrentProjectPath(null); - setIsProfilerMode(false); setStatus(t('header.status.ready')); }; @@ -705,45 +710,26 @@ function App() { useEffect(() => { if (projectLoaded && entityStore && messageHub && logService && uiRegistry && pluginManager) { - let corePanels: FlexDockPanel[]; - - if (isProfilerMode) { - corePanels = [ - { - id: 'scene-hierarchy', - title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy', - content: , - closable: false - }, - { - id: 'inspector', - title: locale === 'zh' ? '检视器' : 'Inspector', - content: , - closable: false - } - ]; - } else { - corePanels = [ - { - id: 'scene-hierarchy', - title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy', - content: , - closable: false - }, - { - id: 'viewport', - title: locale === 'zh' ? '视口' : 'Viewport', - content: , - closable: false - }, - { - id: 'inspector', - title: locale === 'zh' ? '检视器' : 'Inspector', - content: , - closable: false - } - ]; - } + const corePanels: FlexDockPanel[] = [ + { + id: 'scene-hierarchy', + title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy', + content: , + closable: false + }, + { + id: 'viewport', + title: locale === 'zh' ? '视口' : 'Viewport', + content: , + closable: false + }, + { + id: 'inspector', + title: locale === 'zh' ? '检视器' : 'Inspector', + content: , + closable: false + } + ]; // 获取启用的插件面板 const pluginPanels: FlexDockPanel[] = uiRegistry.getAllPanels() @@ -797,7 +783,7 @@ function App() { setPanels([...corePanels, ...pluginPanels, ...dynamicPanels]); } - }, [projectLoaded, entityStore, messageHub, logService, uiRegistry, pluginManager, locale, currentProjectPath, t, pluginUpdateTrigger, isProfilerMode, handleOpenSceneByPath, activeDynamicPanels, dynamicPanelTitles]); + }, [projectLoaded, entityStore, messageHub, logService, uiRegistry, pluginManager, locale, currentProjectPath, t, pluginUpdateTrigger, handleOpenSceneByPath, activeDynamicPanels, dynamicPanelTitles]); if (!initialized) { @@ -819,7 +805,6 @@ function App() { onOpenProject={handleOpenProject} onCreateProject={handleCreateProject} onOpenRecentProject={handleOpenRecentProject} - onProfilerMode={handleProfilerMode} onLocaleChange={handleLocaleChange} recentProjects={recentProjects} locale={locale} @@ -947,12 +932,11 @@ function App() { /> - {showProfiler && ( - setShowProfiler(false)} /> - )} - - {showAdvancedProfiler && ( - setShowAdvancedProfiler(false)} /> + {(showProfiler || showAdvancedProfiler) && ( + { + setShowProfiler(false); + setShowAdvancedProfiler(false); + }} /> )} {showPortManager && ( diff --git a/packages/editor-app/src/app/managers/PluginInstaller.ts b/packages/editor-app/src/app/managers/PluginInstaller.ts index df8ff967..37108a12 100644 --- a/packages/editor-app/src/app/managers/PluginInstaller.ts +++ b/packages/editor-app/src/app/managers/PluginInstaller.ts @@ -1,6 +1,9 @@ /** * 插件安装器 * Plugin Installer + * + * 现在所有插件都使用统一的 IPlugin 接口,无需适配器。 + * Now all plugins use the unified IPlugin interface, no adapter needed. */ import type { PluginManager } from '@esengine/editor-core'; @@ -13,12 +16,12 @@ import { EditorAppearancePlugin } from '../../plugins/builtin/EditorAppearancePl import { PluginConfigPlugin } from '../../plugins/builtin/PluginConfigPlugin'; import { ProjectSettingsPlugin } from '../../plugins/builtin/ProjectSettingsPlugin'; -// 统一模块插件(CSS 已内联到 JS 中,导入时自动注入) -import { TilemapPlugin } from '@esengine/tilemap'; -import { UIPlugin } from '@esengine/ui'; -import { BehaviorTreePlugin } from '@esengine/behavior-tree'; -import { Physics2DPlugin } from '@esengine/physics-rapier2d'; -import { BlueprintPlugin } from '@esengine/blueprint/editor'; +// 统一模块插件(从编辑器包导入完整插件,包含 runtime + editor) +import { BehaviorTreePlugin } from '@esengine/behavior-tree-editor'; +import { Physics2DPlugin } from '@esengine/physics-rapier2d-editor'; +import { TilemapPlugin } from '@esengine/tilemap-editor'; +import { UIPlugin } from '@esengine/ui-editor'; +import { BlueprintPlugin } from '@esengine/blueprint-editor'; export class PluginInstaller { /** @@ -61,13 +64,13 @@ export class PluginInstaller { console.error(`[PluginInstaller] ${name} is invalid: missing descriptor`, plugin); continue; } + // 详细日志,检查 editorModule 是否存在 + console.log(`[PluginInstaller] ${name}: descriptor.id=${plugin.descriptor.id}, hasRuntimeModule=${!!plugin.runtimeModule}, hasEditorModule=${!!plugin.editorModule}`); try { pluginManager.register(plugin); } catch (error) { console.error(`[PluginInstaller] Failed to register ${name}:`, error); } } - - // All builtin plugins registered } } diff --git a/packages/editor-app/src/app/managers/ServiceRegistry.ts b/packages/editor-app/src/app/managers/ServiceRegistry.ts index 19240107..d25f4e4e 100644 --- a/packages/editor-app/src/app/managers/ServiceRegistry.ts +++ b/packages/editor-app/src/app/managers/ServiceRegistry.ts @@ -14,6 +14,7 @@ import { SceneManagerService, SceneTemplateRegistry, FileActionRegistry, + IFileActionRegistry, EntityCreationRegistry, PluginManager, IPluginManager, @@ -28,14 +29,11 @@ import { CompilerRegistry, ICompilerRegistry } from '@esengine/editor-core'; -import { - TransformComponent, - SpriteComponent, - SpriteAnimatorComponent, - TextComponent, - CameraComponent, - AudioSourceComponent -} from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent, SpriteAnimatorComponent } from '@esengine/sprite'; +import { CameraComponent } from '@esengine/camera'; +import { AudioSourceComponent } from '@esengine/audio'; +import { UITextComponent } from '@esengine/ui'; import { BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree'; import { TauriFileAPI } from '../../adapters/TauriFileAPI'; import { DIContainer } from '../../core/di/DIContainer'; @@ -110,7 +108,7 @@ export class ServiceRegistry { { name: 'TransformComponent', type: TransformComponent, editorName: 'Transform', category: 'components.category.core', description: 'components.transform.description', icon: 'Move3d' }, { name: 'SpriteComponent', type: SpriteComponent, editorName: 'Sprite', category: 'components.category.rendering', description: 'components.sprite.description', icon: 'Image' }, { name: 'SpriteAnimatorComponent', type: SpriteAnimatorComponent, editorName: 'SpriteAnimator', category: 'components.category.rendering', description: 'components.spriteAnimator.description', icon: 'Film' }, - { name: 'TextComponent', type: TextComponent, editorName: 'Text', category: 'components.category.rendering', description: 'components.text.description', icon: 'Type' }, + { name: 'UITextComponent', type: UITextComponent, editorName: 'UIText', category: 'components.category.ui', description: 'components.text.description', icon: 'Type' }, { name: 'CameraComponent', type: CameraComponent, editorName: 'Camera', category: 'components.category.rendering', description: 'components.camera.description', icon: 'Camera' }, { name: 'AudioSourceComponent', type: AudioSourceComponent, editorName: 'AudioSource', category: 'components.category.audio', description: 'components.audioSource.description', icon: 'Volume2' }, { name: 'BehaviorTreeRuntimeComponent', type: BehaviorTreeRuntimeComponent, editorName: 'BehaviorTreeRuntime', category: 'components.category.ai', description: 'components.behaviorTreeRuntime.description', icon: 'GitBranch' } @@ -154,6 +152,7 @@ export class ServiceRegistry { Core.services.registerInstance(SettingsRegistry, settingsRegistry); Core.services.registerInstance(SceneManagerService, sceneManager); Core.services.registerInstance(FileActionRegistry, fileActionRegistry); + Core.services.registerInstance(IFileActionRegistry, fileActionRegistry); // Symbol 注册用于跨包插件访问 Core.services.registerInstance(EntityCreationRegistry, entityCreationRegistry); Core.services.registerInstance(ComponentActionRegistry, componentActionRegistry); Core.services.registerInstance(ComponentInspectorRegistry, componentInspectorRegistry); diff --git a/packages/editor-app/src/application/commands/entity/CreateAnimatedSpriteEntityCommand.ts b/packages/editor-app/src/application/commands/entity/CreateAnimatedSpriteEntityCommand.ts index e782b526..1790f724 100644 --- a/packages/editor-app/src/application/commands/entity/CreateAnimatedSpriteEntityCommand.ts +++ b/packages/editor-app/src/application/commands/entity/CreateAnimatedSpriteEntityCommand.ts @@ -1,6 +1,7 @@ -import { Core, Entity } from '@esengine/ecs-framework'; +import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub } from '@esengine/editor-core'; -import { TransformComponent, SpriteComponent, SpriteAnimatorComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent, SpriteAnimatorComponent } from '@esengine/sprite'; import { BaseCommand } from '../BaseCommand'; /** @@ -28,13 +29,15 @@ export class CreateAnimatedSpriteEntityCommand extends BaseCommand { this.entity = scene.createEntity(this.entityName); this.entityId = this.entity.id; - // 添加Transform、Sprite和Animator组件 + // 添加 Transform、Sprite、Animator 和 Hierarchy 组件 this.entity.addComponent(new TransformComponent()); this.entity.addComponent(new SpriteComponent()); this.entity.addComponent(new SpriteAnimatorComponent()); + this.entity.addComponent(new HierarchyComponent()); if (this.parentEntity) { - this.parentEntity.addChild(this.entity); + const hierarchySystem = scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(this.entity, this.parentEntity); } this.entityStore.addEntity(this.entity, this.parentEntity); diff --git a/packages/editor-app/src/application/commands/entity/CreateCameraEntityCommand.ts b/packages/editor-app/src/application/commands/entity/CreateCameraEntityCommand.ts index 2fbdb722..5e04d8cf 100644 --- a/packages/editor-app/src/application/commands/entity/CreateCameraEntityCommand.ts +++ b/packages/editor-app/src/application/commands/entity/CreateCameraEntityCommand.ts @@ -1,6 +1,7 @@ -import { Core, Entity } from '@esengine/ecs-framework'; +import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub } from '@esengine/editor-core'; -import { TransformComponent, CameraComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { CameraComponent } from '@esengine/camera'; import { BaseCommand } from '../BaseCommand'; /** @@ -28,12 +29,14 @@ export class CreateCameraEntityCommand extends BaseCommand { this.entity = scene.createEntity(this.entityName); this.entityId = this.entity.id; - // 添加Transform和Camera组件 + // 添加 Transform、Camera 和 Hierarchy 组件 this.entity.addComponent(new TransformComponent()); this.entity.addComponent(new CameraComponent()); + this.entity.addComponent(new HierarchyComponent()); if (this.parentEntity) { - this.parentEntity.addChild(this.entity); + const hierarchySystem = scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(this.entity, this.parentEntity); } this.entityStore.addEntity(this.entity, this.parentEntity); diff --git a/packages/editor-app/src/application/commands/entity/CreateEntityCommand.ts b/packages/editor-app/src/application/commands/entity/CreateEntityCommand.ts index 88bae7ea..467799c0 100644 --- a/packages/editor-app/src/application/commands/entity/CreateEntityCommand.ts +++ b/packages/editor-app/src/application/commands/entity/CreateEntityCommand.ts @@ -1,6 +1,6 @@ -import { Core, Entity } from '@esengine/ecs-framework'; +import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub } from '@esengine/editor-core'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; import { BaseCommand } from '../BaseCommand'; /** @@ -28,11 +28,15 @@ export class CreateEntityCommand extends BaseCommand { this.entity = scene.createEntity(this.entityName); this.entityId = this.entity.id; - // 自动添加Transform组件 + // 自动添加 Transform 组件 this.entity.addComponent(new TransformComponent()); + // 添加 HierarchyComponent 支持层级结构 + this.entity.addComponent(new HierarchyComponent()); + if (this.parentEntity) { - this.parentEntity.addChild(this.entity); + const hierarchySystem = scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(this.entity, this.parentEntity); } this.entityStore.addEntity(this.entity, this.parentEntity); diff --git a/packages/editor-app/src/application/commands/entity/CreateSpriteEntityCommand.ts b/packages/editor-app/src/application/commands/entity/CreateSpriteEntityCommand.ts index 25cfffcb..c5054c38 100644 --- a/packages/editor-app/src/application/commands/entity/CreateSpriteEntityCommand.ts +++ b/packages/editor-app/src/application/commands/entity/CreateSpriteEntityCommand.ts @@ -1,6 +1,7 @@ -import { Core, Entity } from '@esengine/ecs-framework'; +import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub } from '@esengine/editor-core'; -import { TransformComponent, SpriteComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent } from '@esengine/sprite'; import { BaseCommand } from '../BaseCommand'; /** @@ -28,12 +29,14 @@ export class CreateSpriteEntityCommand extends BaseCommand { this.entity = scene.createEntity(this.entityName); this.entityId = this.entity.id; - // 添加Transform和Sprite组件 + // 添加 Transform、Sprite 和 Hierarchy 组件 this.entity.addComponent(new TransformComponent()); this.entity.addComponent(new SpriteComponent()); + this.entity.addComponent(new HierarchyComponent()); if (this.parentEntity) { - this.parentEntity.addChild(this.entity); + const hierarchySystem = scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(this.entity, this.parentEntity); } this.entityStore.addEntity(this.entity, this.parentEntity); diff --git a/packages/editor-app/src/application/commands/entity/CreateTilemapEntityCommand.ts b/packages/editor-app/src/application/commands/entity/CreateTilemapEntityCommand.ts index 79d1a5b7..74ca0356 100644 --- a/packages/editor-app/src/application/commands/entity/CreateTilemapEntityCommand.ts +++ b/packages/editor-app/src/application/commands/entity/CreateTilemapEntityCommand.ts @@ -1,6 +1,6 @@ -import { Core, Entity } from '@esengine/ecs-framework'; +import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub } from '@esengine/editor-core'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; import { TilemapComponent } from '@esengine/tilemap'; import { BaseCommand } from '../BaseCommand'; @@ -48,8 +48,9 @@ export class CreateTilemapEntityCommand extends BaseCommand { this.entity = scene.createEntity(this.entityName); this.entityId = this.entity.id; - // 添加Transform组件 + // 添加 Transform 和 Hierarchy 组件 this.entity.addComponent(new TransformComponent()); + this.entity.addComponent(new HierarchyComponent()); // 创建并配置Tilemap组件 const tilemapComponent = new TilemapComponent(); @@ -79,7 +80,8 @@ export class CreateTilemapEntityCommand extends BaseCommand { this.entity.addComponent(tilemapComponent); if (this.parentEntity) { - this.parentEntity.addChild(this.entity); + const hierarchySystem = scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(this.entity, this.parentEntity); } this.entityStore.addEntity(this.entity, this.parentEntity); diff --git a/packages/editor-app/src/application/commands/entity/DeleteEntityCommand.ts b/packages/editor-app/src/application/commands/entity/DeleteEntityCommand.ts index fb904754..be216093 100644 --- a/packages/editor-app/src/application/commands/entity/DeleteEntityCommand.ts +++ b/packages/editor-app/src/application/commands/entity/DeleteEntityCommand.ts @@ -1,4 +1,4 @@ -import { Core, Entity, Component } from '@esengine/ecs-framework'; +import { Core, Entity, Component, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub } from '@esengine/editor-core'; import { BaseCommand } from '../BaseCommand'; @@ -8,9 +8,9 @@ import { BaseCommand } from '../BaseCommand'; export class DeleteEntityCommand extends BaseCommand { private entityId: number; private entityName: string; - private parentEntity: Entity | null; + private parentEntityId: number | null; private components: Component[] = []; - private childEntities: Entity[] = []; + private childEntityIds: number[] = []; constructor( private entityStore: EntityStoreService, @@ -20,18 +20,28 @@ export class DeleteEntityCommand extends BaseCommand { super(); this.entityId = entity.id; this.entityName = entity.name; - this.parentEntity = entity.parent; + + // 通过 HierarchyComponent 获取父实体 ID + const hierarchy = entity.getComponent(HierarchyComponent); + this.parentEntityId = hierarchy?.parentId ?? null; // 保存组件状态用于撤销 this.components = [...entity.components]; - // 保存子实体 - this.childEntities = [...entity.children]; + + // 保存子实体 ID + this.childEntityIds = hierarchy?.childIds ? [...hierarchy.childIds] : []; } execute(): void { + const scene = Core.scene; + if (!scene) return; + // 先移除子实体 - for (const child of this.childEntities) { - this.entityStore.removeEntity(child); + for (const childId of this.childEntityIds) { + const child = scene.findEntityById(childId); + if (child) { + this.entityStore.removeEntity(child); + } } this.entityStore.removeEntity(this.entity); @@ -46,12 +56,17 @@ export class DeleteEntityCommand extends BaseCommand { throw new Error('场景未初始化'); } + const hierarchySystem = scene.getSystem(HierarchySystem); + // 重新创建实体 const newEntity = scene.createEntity(this.entityName); // 设置父实体 - if (this.parentEntity) { - this.parentEntity.addChild(newEntity); + if (this.parentEntityId !== null && hierarchySystem) { + const parentEntity = scene.findEntityById(this.parentEntityId); + if (parentEntity) { + hierarchySystem.setParent(newEntity, parentEntity); + } } // 恢复组件 @@ -71,12 +86,20 @@ export class DeleteEntityCommand extends BaseCommand { } // 恢复子实体 - for (const child of this.childEntities) { - newEntity.addChild(child); - this.entityStore.addEntity(child, newEntity); + for (const childId of this.childEntityIds) { + const child = scene.findEntityById(childId); + if (child && hierarchySystem) { + hierarchySystem.setParent(child, newEntity); + this.entityStore.addEntity(child, newEntity); + } } - this.entityStore.addEntity(newEntity, this.parentEntity ?? undefined); + // 获取父实体 + const parentEntity = this.parentEntityId !== null + ? scene.findEntityById(this.parentEntityId) ?? undefined + : undefined; + + this.entityStore.addEntity(newEntity, parentEntity); this.entityStore.selectEntity(newEntity); // 更新引用 diff --git a/packages/editor-app/src/application/commands/entity/ReparentEntityCommand.ts b/packages/editor-app/src/application/commands/entity/ReparentEntityCommand.ts new file mode 100644 index 00000000..b2cc3942 --- /dev/null +++ b/packages/editor-app/src/application/commands/entity/ReparentEntityCommand.ts @@ -0,0 +1,194 @@ +import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; +import { EntityStoreService, MessageHub } from '@esengine/editor-core'; +import { BaseCommand } from '../BaseCommand'; + +/** + * 拖放位置类型 + */ +export enum DropPosition { + /** 在目标之前 */ + BEFORE = 'before', + /** 在目标内部(作为子级) */ + INSIDE = 'inside', + /** 在目标之后 */ + AFTER = 'after' +} + +/** + * 重新设置实体父级命令 + * + * 支持拖拽重排功能,可以将实体移动到: + * - 另一个实体之前 (BEFORE) + * - 另一个实体内部作为子级 (INSIDE) + * - 另一个实体之后 (AFTER) + */ +export class ReparentEntityCommand extends BaseCommand { + private oldParentId: number | null; + private oldSiblingIndex: number; + + constructor( + private entityStore: EntityStoreService, + private messageHub: MessageHub, + private entity: Entity, + private targetEntity: Entity, + private dropPosition: DropPosition + ) { + super(); + + // 保存原始状态用于撤销 + const hierarchy = entity.getComponent(HierarchyComponent); + this.oldParentId = hierarchy?.parentId ?? null; + + // 获取在兄弟列表中的原始索引 + this.oldSiblingIndex = this.getSiblingIndex(entity); + } + + execute(): void { + const scene = Core.scene; + if (!scene) { + console.warn('[ReparentEntityCommand] No scene available'); + return; + } + + const hierarchySystem = scene.getSystem(HierarchySystem); + if (!hierarchySystem) { + console.warn('[ReparentEntityCommand] No HierarchySystem found'); + return; + } + + // 确保目标实体有 HierarchyComponent + if (!this.targetEntity.getComponent(HierarchyComponent)) { + this.targetEntity.addComponent(new HierarchyComponent()); + } + + console.log(`[ReparentEntityCommand] Moving ${this.entity.name} to ${this.targetEntity.name} (${this.dropPosition})`); + + switch (this.dropPosition) { + case DropPosition.INSIDE: + // 移动到目标实体内部作为最后一个子级 + hierarchySystem.setParent(this.entity, this.targetEntity); + break; + + case DropPosition.BEFORE: + case DropPosition.AFTER: + // 移动到目标实体的同级 + this.moveToSibling(hierarchySystem); + break; + } + + this.entityStore.syncFromScene(); + this.messageHub.publish('entity:reparented', { + entityId: this.entity.id, + targetId: this.targetEntity.id, + position: this.dropPosition + }); + } + + undo(): void { + const scene = Core.scene; + if (!scene) return; + + const hierarchySystem = scene.getSystem(HierarchySystem); + if (!hierarchySystem) return; + + // 恢复到原始父级 + const oldParent = this.oldParentId !== null + ? scene.findEntityById(this.oldParentId) + : null; + + if (oldParent) { + // 恢复到原始父级的指定位置 + hierarchySystem.insertChildAt(oldParent, this.entity, this.oldSiblingIndex); + } else { + // 恢复到根级 + hierarchySystem.setParent(this.entity, null); + } + + this.entityStore.syncFromScene(); + this.messageHub.publish('entity:reparented', { + entityId: this.entity.id, + targetId: null, + position: 'undo' + }); + } + + getDescription(): string { + const positionText = this.dropPosition === DropPosition.INSIDE + ? '移入' + : this.dropPosition === DropPosition.BEFORE ? '移动到前面' : '移动到后面'; + return `${positionText}: ${this.entity.name} -> ${this.targetEntity.name}`; + } + + /** + * 移动到目标的同级位置 + */ + private moveToSibling(hierarchySystem: HierarchySystem): void { + const targetHierarchy = this.targetEntity.getComponent(HierarchyComponent); + const targetParentId = targetHierarchy?.parentId ?? null; + + const scene = Core.scene; + if (!scene) return; + + // 获取目标的父实体 + const targetParent = targetParentId !== null + ? scene.findEntityById(targetParentId) + : null; + + // 获取目标在兄弟列表中的索引 + let targetIndex = this.getSiblingIndex(this.targetEntity); + + // 根据放置位置调整索引 + if (this.dropPosition === DropPosition.AFTER) { + targetIndex++; + } + + // 如果移动到同一父级下,需要考虑原位置对索引的影响 + const entityHierarchy = this.entity.getComponent(HierarchyComponent); + const entityParentId = entityHierarchy?.parentId ?? null; + + const bSameParent = entityParentId === targetParentId; + if (bSameParent) { + const currentIndex = this.getSiblingIndex(this.entity); + if (currentIndex < targetIndex) { + targetIndex--; + } + } + + console.log(`[ReparentEntityCommand] moveToSibling: targetParent=${targetParent?.name ?? 'ROOT'}, targetIndex=${targetIndex}`); + + if (targetParent) { + // 有父级,插入到父级的指定位置 + hierarchySystem.insertChildAt(targetParent, this.entity, targetIndex); + } else { + // 目标在根级 + // 先确保实体移动到根级 + if (entityParentId !== null) { + hierarchySystem.setParent(this.entity, null); + } + // 然后调整根级顺序 + this.entityStore.reorderEntity(this.entity.id, targetIndex); + } + } + + /** + * 获取实体在兄弟列表中的索引 + */ + private getSiblingIndex(entity: Entity): number { + const scene = Core.scene; + if (!scene) return 0; + + const hierarchy = entity.getComponent(HierarchyComponent); + const parentId = hierarchy?.parentId; + + if (parentId === null || parentId === undefined) { + // 根级实体,从 EntityStoreService 获取 + return this.entityStore.getRootEntityIds().indexOf(entity.id); + } + + const parent = scene.findEntityById(parentId); + if (!parent) return 0; + + const parentHierarchy = parent.getComponent(HierarchyComponent); + return parentHierarchy?.childIds.indexOf(entity.id) ?? 0; + } +} diff --git a/packages/editor-app/src/application/commands/entity/index.ts b/packages/editor-app/src/application/commands/entity/index.ts index 61eebbfa..ac52ea8c 100644 --- a/packages/editor-app/src/application/commands/entity/index.ts +++ b/packages/editor-app/src/application/commands/entity/index.ts @@ -4,4 +4,5 @@ export { CreateAnimatedSpriteEntityCommand } from './CreateAnimatedSpriteEntityC export { CreateCameraEntityCommand } from './CreateCameraEntityCommand'; export { CreateTilemapEntityCommand } from './CreateTilemapEntityCommand'; export { DeleteEntityCommand } from './DeleteEntityCommand'; +export { ReparentEntityCommand, DropPosition } from './ReparentEntityCommand'; diff --git a/packages/editor-app/src/components/AdvancedProfiler.tsx b/packages/editor-app/src/components/AdvancedProfiler.tsx index 34bfc0cf..0b959992 100644 --- a/packages/editor-app/src/components/AdvancedProfiler.tsx +++ b/packages/editor-app/src/components/AdvancedProfiler.tsx @@ -8,6 +8,19 @@ import '../styles/AdvancedProfiler.css'; /** * 高级性能数据接口(与 Core 的 IAdvancedProfilerData 对应) */ +interface HotspotItem { + name: string; + category: string; + inclusiveTime: number; + inclusiveTimePercent: number; + exclusiveTime: number; + exclusiveTimePercent: number; + callCount: number; + avgCallTime: number; + depth: number; + children?: HotspotItem[]; +} + interface AdvancedProfilerData { currentFrame: { frameNumber: number; @@ -41,16 +54,7 @@ interface AdvancedProfilerData { percentOfFrame: number; }>; }>; - hotspots: Array<{ - name: string; - category: string; - inclusiveTime: number; - inclusiveTimePercent: number; - exclusiveTime: number; - exclusiveTimePercent: number; - callCount: number; - avgCallTime: number; - }>; + hotspots: HotspotItem[]; callGraph: { currentFunction: string | null; callers: Array<{ @@ -120,18 +124,72 @@ const CATEGORY_COLORS: Record = { 'Custom': '#64748b' }; +type DataMode = 'oneframe' | 'average' | 'maximum'; + export function AdvancedProfiler({ profilerService }: AdvancedProfilerProps) { const [data, setData] = useState(null); const [isPaused, setIsPaused] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [selectedFunction, setSelectedFunction] = useState(null); const [expandedCategories, setExpandedCategories] = useState>(new Set(['ECS'])); + const [expandedHotspots, setExpandedHotspots] = useState>(new Set()); const [sortColumn, setSortColumn] = useState('incTime'); const [sortDirection, setSortDirection] = useState('desc'); const [viewMode, setViewMode] = useState<'hierarchical' | 'flat'>('hierarchical'); + const [dataMode, setDataMode] = useState('average'); const canvasRef = useRef(null); const frameHistoryRef = useRef>([]); const lastDataRef = useRef(null); + // 用于计算平均值和最大值的历史数据 + const hotspotHistoryRef = useRef>(new Map()); + + // 更新历史数据 + const updateHotspotHistory = useCallback((hotspots: HotspotItem[]) => { + const updateItem = (item: HotspotItem) => { + const history = hotspotHistoryRef.current.get(item.name) || { times: [], maxTime: 0 }; + history.times.push(item.inclusiveTime); + // 保留最近 60 帧的数据 + if (history.times.length > 60) { + history.times.shift(); + } + history.maxTime = Math.max(history.maxTime, item.inclusiveTime); + hotspotHistoryRef.current.set(item.name, history); + + if (item.children) { + item.children.forEach(updateItem); + } + }; + hotspots.forEach(updateItem); + }, []); + + // 根据数据模式处理 hotspots + const processHotspotsWithDataMode = useCallback((hotspots: HotspotItem[], mode: DataMode): HotspotItem[] => { + if (mode === 'oneframe') { + return hotspots; + } + + const processItem = (item: HotspotItem): HotspotItem => { + const history = hotspotHistoryRef.current.get(item.name); + let processedTime = item.inclusiveTime; + + if (history && history.times.length > 0) { + if (mode === 'average') { + processedTime = history.times.reduce((a, b) => a + b, 0) / history.times.length; + } else if (mode === 'maximum') { + processedTime = history.maxTime; + } + } + + return { + ...item, + inclusiveTime: processedTime, + avgCallTime: item.callCount > 0 ? processedTime / item.callCount : 0, + children: item.children ? item.children.map(processItem) : undefined + }; + }; + + return hotspots.map(processItem); + }, []); // 订阅数据更新 useEffect(() => { @@ -142,18 +200,21 @@ export function AdvancedProfiler({ profilerService }: AdvancedProfilerProps) { // 解析高级性能数据 if (rawData.advancedProfiler) { + // 更新历史数据 + updateHotspotHistory(rawData.advancedProfiler.hotspots); setData(rawData.advancedProfiler); lastDataRef.current = rawData.advancedProfiler; } else if (rawData.performance) { // 从传统数据构建 const advancedData = buildFromLegacyData(rawData); + updateHotspotHistory(advancedData.hotspots); setData(advancedData); lastDataRef.current = advancedData; } }); return unsubscribe; - }, [profilerService, isPaused]); + }, [profilerService, isPaused, updateHotspotHistory]); // 当选中函数变化时,通知服务端 useEffect(() => { @@ -317,44 +378,90 @@ export function AdvancedProfiler({ profilerService }: AdvancedProfilerProps) { return percent.toFixed(1) + '%'; }; + // 展平层级数据用于显示 + const flattenHotspots = (items: HotspotItem[], result: HotspotItem[] = []): HotspotItem[] => { + for (const item of items) { + // 搜索过滤 + const matchesSearch = searchTerm === '' || item.name.toLowerCase().includes(searchTerm.toLowerCase()); + + if (viewMode === 'flat') { + // 扁平模式:显示所有层级的项目 + if (matchesSearch) { + result.push({ ...item, depth: 0 }); // 扁平模式下深度都是0 + } + if (item.children) { + flattenHotspots(item.children, result); + } + } else { + // 层级模式:根据展开状态显示 + if (matchesSearch || (item.children && item.children.some(c => c.name.toLowerCase().includes(searchTerm.toLowerCase())))) { + result.push(item); + } + if (item.children && expandedHotspots.has(item.name)) { + flattenHotspots(item.children, result); + } + } + } + return result; + }; + + // 切换展开状态 + const toggleHotspotExpand = (name: string) => { + setExpandedHotspots(prev => { + const next = new Set(prev); + if (next.has(name)) { + next.delete(name); + } else { + next.add(name); + } + return next; + }); + }; + // 排序数据 - const getSortedHotspots = () => { + const getSortedHotspots = (): HotspotItem[] => { if (!data) return []; - const filtered = data.hotspots.filter(h => - searchTerm === '' || h.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); + // 先根据数据模式处理 hotspots + const processedHotspots = processHotspotsWithDataMode(data.hotspots, dataMode); + const flattened = flattenHotspots(processedHotspots); - return [...filtered].sort((a, b) => { - let comparison = 0; - switch (sortColumn) { - case 'name': - comparison = a.name.localeCompare(b.name); - break; - case 'incTime': - comparison = a.inclusiveTime - b.inclusiveTime; - break; - case 'incPercent': - comparison = a.inclusiveTimePercent - b.inclusiveTimePercent; - break; - case 'excTime': - comparison = a.exclusiveTime - b.exclusiveTime; - break; - case 'excPercent': - comparison = a.exclusiveTimePercent - b.exclusiveTimePercent; - break; - case 'calls': - comparison = a.callCount - b.callCount; - break; - case 'avgTime': - comparison = a.avgCallTime - b.avgCallTime; - break; - case 'framePercent': - comparison = a.inclusiveTimePercent - b.inclusiveTimePercent; - break; - } - return sortDirection === 'asc' ? comparison : -comparison; - }); + // 扁平模式下排序 + if (viewMode === 'flat') { + return [...flattened].sort((a, b) => { + let comparison = 0; + switch (sortColumn) { + case 'name': + comparison = a.name.localeCompare(b.name); + break; + case 'incTime': + comparison = a.inclusiveTime - b.inclusiveTime; + break; + case 'incPercent': + comparison = a.inclusiveTimePercent - b.inclusiveTimePercent; + break; + case 'excTime': + comparison = a.exclusiveTime - b.exclusiveTime; + break; + case 'excPercent': + comparison = a.exclusiveTimePercent - b.exclusiveTimePercent; + break; + case 'calls': + comparison = a.callCount - b.callCount; + break; + case 'avgTime': + comparison = a.avgCallTime - b.avgCallTime; + break; + case 'framePercent': + comparison = a.inclusiveTimePercent - b.inclusiveTimePercent; + break; + } + return sortDirection === 'asc' ? comparison : -comparison; + }); + } + + // 层级模式下保持原有层级顺序 + return flattened; }; const renderSortIcon = (column: SortColumn) => { @@ -512,7 +619,11 @@ export function AdvancedProfiler({ profilerService }: AdvancedProfilerProps) { Call Graph
- setDataMode(e.target.value as DataMode)} + > @@ -654,49 +765,67 @@ export function AdvancedProfiler({ profilerService }: AdvancedProfilerProps) {
- {getSortedHotspots().map((item, index) => ( -
setSelectedFunction(item.name)} - > -
- - - {item.name} -
-
- {formatTime(item.inclusiveTime)} -
-
-
-
50 ? 'critical' : item.inclusiveTimePercent > 25 ? 'warning' : ''}`} - style={{ width: `${Math.min(item.inclusiveTimePercent, 100)}%` }} + {getSortedHotspots().map((item, index) => { + const hasChildren = item.children && item.children.length > 0; + const isExpanded = expandedHotspots.has(item.name); + const indentPadding = viewMode === 'hierarchical' ? item.depth * 16 : 0; + + return ( +
setSelectedFunction(item.name)} + > +
+ {hasChildren && viewMode === 'hierarchical' ? ( + { + e.stopPropagation(); + toggleHotspotExpand(item.name); + }} + > + {isExpanded ? : } + + ) : ( + + )} + - {formatPercent(item.inclusiveTimePercent)} + {item.name} +
+
+ {formatTime(item.inclusiveTime)} +
+
+
+
50 ? 'critical' : item.inclusiveTimePercent > 25 ? 'warning' : ''}`} + style={{ width: `${Math.min(item.inclusiveTimePercent, 100)}%` }} + /> + {formatPercent(item.inclusiveTimePercent)} +
+
+
+ {formatTime(item.exclusiveTime)} +
+
+ {formatPercent(item.exclusiveTimePercent)} +
+
+ {item.callCount} +
+
+ {formatTime(item.avgCallTime)} +
+
+ {formatPercent(item.inclusiveTimePercent)}
-
- {formatTime(item.exclusiveTime)} -
-
- {formatPercent(item.exclusiveTimePercent)} -
-
- {item.callCount} -
-
- {formatTime(item.avgCallTime)} -
-
- {formatPercent(item.inclusiveTimePercent)} -
-
- ))} + ); + })}
@@ -716,7 +845,7 @@ function buildFromLegacyData(rawData: any): AdvancedProfilerData { const fps = frameTime > 0 ? Math.round(1000 / frameTime) : 0; // 构建 hotspots - const hotspots = systems.map((sys: any) => ({ + const hotspots: HotspotItem[] = systems.map((sys: any) => ({ name: sys.name || sys.type || 'Unknown', category: 'ECS', inclusiveTime: sys.executionTime || 0, @@ -724,7 +853,8 @@ function buildFromLegacyData(rawData: any): AdvancedProfilerData { exclusiveTime: sys.executionTime || 0, exclusiveTimePercent: frameTime > 0 ? (sys.executionTime / frameTime) * 100 : 0, callCount: 1, - avgCallTime: sys.executionTime || 0 + avgCallTime: sys.executionTime || 0, + depth: 0 })); // 构建 categoryStats diff --git a/packages/editor-app/src/components/AdvancedProfilerWindow.tsx b/packages/editor-app/src/components/AdvancedProfilerWindow.tsx index 6b9fca30..e25d8f74 100644 --- a/packages/editor-app/src/components/AdvancedProfilerWindow.tsx +++ b/packages/editor-app/src/components/AdvancedProfilerWindow.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react'; -import { X, BarChart3 } from 'lucide-react'; +import { useState, useEffect, useCallback } from 'react'; +import { X, BarChart3, Maximize2, Minimize2 } from 'lucide-react'; import { ProfilerService } from '../services/ProfilerService'; import { AdvancedProfiler } from './AdvancedProfiler'; import '../styles/ProfilerWindow.css'; @@ -15,6 +15,7 @@ interface WindowWithProfiler extends Window { export function AdvancedProfilerWindow({ onClose }: AdvancedProfilerWindowProps) { const [profilerService, setProfilerService] = useState(null); const [isConnected, setIsConnected] = useState(false); + const [isFullscreen, setIsFullscreen] = useState(false); useEffect(() => { const service = (window as WindowWithProfiler).__PROFILER_SERVICE__; @@ -36,12 +37,35 @@ export function AdvancedProfilerWindow({ onClose }: AdvancedProfilerWindowProps) return () => clearInterval(interval); }, [profilerService]); + // 处理 ESC 键退出全屏 + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isFullscreen) { + setIsFullscreen(false); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isFullscreen]); + + const toggleFullscreen = useCallback(() => { + setIsFullscreen(prev => !prev); + }, []); + + const windowStyle = isFullscreen + ? { width: '100vw', height: '100vh', maxWidth: 'none', borderRadius: 0 } + : { width: '90vw', height: '85vh', maxWidth: '1600px' }; + return ( -
+
e.stopPropagation()} - style={{ width: '90vw', height: '85vh', maxWidth: '1600px' }} + style={windowStyle} >
@@ -53,9 +77,18 @@ export function AdvancedProfilerWindow({ onClose }: AdvancedProfilerWindowProps) )}
- +
+ + +
diff --git a/packages/editor-app/src/components/ConsolePanel.tsx b/packages/editor-app/src/components/ConsolePanel.tsx deleted file mode 100644 index 59223a0b..00000000 --- a/packages/editor-app/src/components/ConsolePanel.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import { useState, useEffect, useRef, useMemo, memo } from 'react'; -import { LogService, LogEntry } from '@esengine/editor-core'; -import { LogLevel } from '@esengine/ecs-framework'; -import { Trash2, AlertCircle, Info, AlertTriangle, XCircle, Bug, Search, Wifi } from 'lucide-react'; -import { JsonViewer } from './JsonViewer'; -import '../styles/ConsolePanel.css'; - -interface ConsolePanelProps { - logService: LogService; -} - -const MAX_LOGS = 1000; - -// 提取JSON检测和格式化逻辑 -function tryParseJSON(message: string): { isJSON: boolean; parsed?: unknown } { - try { - const parsed: unknown = JSON.parse(message); - return { isJSON: true, parsed }; - } catch { - return { isJSON: false }; - } -} - -// 格式化时间 -function formatTime(date: Date): string { - const hours = date.getHours().toString().padStart(2, '0'); - const minutes = date.getMinutes().toString().padStart(2, '0'); - const seconds = date.getSeconds().toString().padStart(2, '0'); - const ms = date.getMilliseconds().toString().padStart(3, '0'); - return `${hours}:${minutes}:${seconds}.${ms}`; -} - -// 日志等级图标 -function getLevelIcon(level: LogLevel) { - switch (level) { - case LogLevel.Debug: - return ; - case LogLevel.Info: - return ; - case LogLevel.Warn: - return ; - case LogLevel.Error: - case LogLevel.Fatal: - return ; - default: - return ; - } -} - -// 日志等级样式类 -function getLevelClass(level: LogLevel): string { - switch (level) { - case LogLevel.Debug: - return 'log-entry-debug'; - case LogLevel.Info: - return 'log-entry-info'; - case LogLevel.Warn: - return 'log-entry-warn'; - case LogLevel.Error: - case LogLevel.Fatal: - return 'log-entry-error'; - default: - return ''; - } -} - -// 单个日志条目组件 -const LogEntryItem = memo(({ log, onOpenJsonViewer }: { - log: LogEntry; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onOpenJsonViewer: (data: any) => void; -}) => { - const { isJSON, parsed } = useMemo(() => tryParseJSON(log.message), [log.message]); - const shouldTruncate = log.message.length > 200; - const [isExpanded, setIsExpanded] = useState(false); - - return ( -
-
- {getLevelIcon(log.level)} -
-
- {formatTime(log.timestamp)} -
-
- [{log.source === 'remote' ? '🌐 Remote' : log.source}] -
- {log.clientId && ( -
- {log.clientId} -
- )} -
-
-
- {shouldTruncate && !isExpanded ? ( - <> - - {log.message.substring(0, 200)}... - - - - ) : ( - <> - {log.message} - {shouldTruncate && ( - - )} - - )} -
- {isJSON && parsed !== undefined && ( - - )} -
-
-
- ); -}); - -LogEntryItem.displayName = 'LogEntryItem'; - -export function ConsolePanel({ logService }: ConsolePanelProps) { - // 状态管理 - const [logs, setLogs] = useState(() => logService.getLogs().slice(-MAX_LOGS)); - const [filter, setFilter] = useState(''); - const [levelFilter, setLevelFilter] = useState>(new Set([ - LogLevel.Debug, - LogLevel.Info, - LogLevel.Warn, - LogLevel.Error, - LogLevel.Fatal - ])); - const [showRemoteOnly, setShowRemoteOnly] = useState(false); - const [autoScroll, setAutoScroll] = useState(true); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [jsonViewerData, setJsonViewerData] = useState(null); - const logContainerRef = useRef(null); - - // 订阅日志更新 - useEffect(() => { - const unsubscribe = logService.subscribe((entry) => { - setLogs((prev) => { - const newLogs = [...prev, entry]; - return newLogs.length > MAX_LOGS ? newLogs.slice(-MAX_LOGS) : newLogs; - }); - }); - - return unsubscribe; - }, [logService]); - - // 自动滚动 - useEffect(() => { - if (autoScroll && logContainerRef.current) { - logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; - } - }, [logs, autoScroll]); - - // 处理滚动 - const handleScroll = () => { - if (logContainerRef.current) { - const { scrollTop, scrollHeight, clientHeight } = logContainerRef.current; - const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; - setAutoScroll(isAtBottom); - } - }; - - // 清空日志 - const handleClear = () => { - logService.clear(); - setLogs([]); - }; - - // 切换等级过滤 - const toggleLevelFilter = (level: LogLevel) => { - const newFilter = new Set(levelFilter); - if (newFilter.has(level)) { - newFilter.delete(level); - } else { - newFilter.add(level); - } - setLevelFilter(newFilter); - }; - - // 过滤日志 - const filteredLogs = useMemo(() => { - return logs.filter((log) => { - if (!levelFilter.has(log.level)) return false; - if (showRemoteOnly && log.source !== 'remote') return false; - if (filter && !log.message.toLowerCase().includes(filter.toLowerCase())) { - return false; - } - return true; - }); - }, [logs, levelFilter, showRemoteOnly, filter]); - - // 统计各等级日志数量 - const levelCounts = useMemo(() => ({ - [LogLevel.Debug]: logs.filter((l) => l.level === LogLevel.Debug).length, - [LogLevel.Info]: logs.filter((l) => l.level === LogLevel.Info).length, - [LogLevel.Warn]: logs.filter((l) => l.level === LogLevel.Warn).length, - [LogLevel.Error]: logs.filter((l) => l.level === LogLevel.Error || l.level === LogLevel.Fatal).length - }), [logs]); - - const remoteLogCount = useMemo(() => - logs.filter((l) => l.source === 'remote').length - , [logs]); - - return ( -
-
-
- -
- - setFilter(e.target.value)} - /> -
-
-
- - - - - -
-
-
- {filteredLogs.length === 0 ? ( -
- -

No logs to display

-
- ) : ( - filteredLogs.map((log, index) => ( - - )) - )} -
- {jsonViewerData && ( - setJsonViewerData(null)} - /> - )} - {!autoScroll && ( - - )} -
- ); -} diff --git a/packages/editor-app/src/components/ContentBrowser.tsx b/packages/editor-app/src/components/ContentBrowser.tsx index 512d48b8..476b90e3 100644 --- a/packages/editor-app/src/components/ContentBrowser.tsx +++ b/packages/editor-app/src/components/ContentBrowser.tsx @@ -26,7 +26,15 @@ import { Trash2, Edit3, ExternalLink, - PanelRightClose + PanelRightClose, + Tag, + Link, + FileSearch, + Globe, + Package, + Clipboard, + RefreshCw, + Settings } from 'lucide-react'; import { Core } from '@esengine/ecs-framework'; import { MessageHub, FileActionRegistry, type FileCreationTemplate } from '@esengine/editor-core'; @@ -564,9 +572,191 @@ export function ContentBrowser({ icon: , onClick: () => handleAssetDoubleClick(asset) }); + items.push({ label: '', separator: true, onClick: () => {} }); + + // 保存 + items.push({ + label: locale === 'zh' ? '保存' : 'Save', + icon: , + shortcut: 'Ctrl+S', + onClick: () => { + console.log('Save file:', asset.path); + } + }); } + // 重命名 + items.push({ + label: locale === 'zh' ? '重命名' : 'Rename', + icon: , + shortcut: 'F2', + onClick: () => { + setRenameDialog({ asset, newName: asset.name }); + setContextMenu(null); + } + }); + + // 批量重命名 + items.push({ + label: locale === 'zh' ? '批量重命名' : 'Batch Rename', + icon: , + shortcut: 'Shift+F2', + disabled: true, + onClick: () => { + console.log('Batch rename'); + } + }); + + // 复制 + items.push({ + label: locale === 'zh' ? '复制' : 'Duplicate', + icon: , + shortcut: 'Ctrl+D', + onClick: () => { + console.log('Duplicate:', asset.path); + } + }); + + // 删除 + items.push({ + label: locale === 'zh' ? '删除' : 'Delete', + icon: , + shortcut: 'Delete', + onClick: () => { + setDeleteConfirmDialog(asset); + setContextMenu(null); + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 资产操作子菜单 + items.push({ + label: locale === 'zh' ? '资产操作' : 'Asset Actions', + icon: , + onClick: () => {}, + children: [ + { + label: locale === 'zh' ? '重新导入' : 'Reimport', + icon: , + onClick: () => { + console.log('Reimport asset:', asset.path); + } + }, + { + label: locale === 'zh' ? '导出...' : 'Export...', + icon: , + onClick: () => { + console.log('Export asset:', asset.path); + } + }, + { label: '', separator: true, onClick: () => {} }, + { + label: locale === 'zh' ? '迁移资产' : 'Migrate Asset', + icon: , + onClick: () => { + console.log('Migrate asset:', asset.path); + } + } + ] + }); + + // 资产本地化子菜单 + items.push({ + label: locale === 'zh' ? '资产本地化' : 'Asset Localization', + icon: , + onClick: () => {}, + children: [ + { + label: locale === 'zh' ? '创建本地化资产' : 'Create Localized Asset', + onClick: () => { + console.log('Create localized asset:', asset.path); + } + }, + { + label: locale === 'zh' ? '导入翻译' : 'Import Translation', + onClick: () => { + console.log('Import translation:', asset.path); + } + }, + { + label: locale === 'zh' ? '导出翻译' : 'Export Translation', + onClick: () => { + console.log('Export translation:', asset.path); + } + } + ] + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 标签管理 + items.push({ + label: locale === 'zh' ? '管理标签' : 'Manage Tags', + icon: , + shortcut: 'Ctrl+T', + onClick: () => { + console.log('Manage tags:', asset.path); + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 路径复制选项 + items.push({ + label: locale === 'zh' ? '复制引用' : 'Copy Reference', + icon: , + shortcut: 'Ctrl+C', + onClick: () => { + navigator.clipboard.writeText(asset.path); + } + }); + + items.push({ + label: locale === 'zh' ? '拷贝Object路径' : 'Copy Object Path', + icon: , + shortcut: 'Ctrl+Shift+C', + onClick: () => { + const objectPath = asset.path.replace(/\\/g, '/'); + navigator.clipboard.writeText(objectPath); + } + }); + + items.push({ + label: locale === 'zh' ? '拷贝包路径' : 'Copy Package Path', + icon: , + shortcut: 'Ctrl+Alt+C', + onClick: () => { + const packagePath = '/' + asset.path.replace(/\\/g, '/').split('/').slice(-2).join('/'); + navigator.clipboard.writeText(packagePath); + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 引用查看器 + items.push({ + label: locale === 'zh' ? '引用查看器' : 'Reference Viewer', + icon: , + shortcut: 'Alt+Shift+R', + onClick: () => { + console.log('Open reference viewer:', asset.path); + } + }); + + items.push({ + label: locale === 'zh' ? '尺寸信息图' : 'Size Map', + icon: , + shortcut: 'Alt+Shift+D', + onClick: () => { + console.log('Show size map:', asset.path); + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 在文件管理器中显示 items.push({ label: locale === 'zh' ? '在文件管理器中显示' : 'Show in Explorer', icon: , @@ -579,34 +769,8 @@ export function ContentBrowser({ } }); - items.push({ - label: locale === 'zh' ? '复制路径' : 'Copy Path', - icon: , - onClick: () => navigator.clipboard.writeText(asset.path) - }); - - items.push({ label: '', separator: true, onClick: () => {} }); - - items.push({ - label: locale === 'zh' ? '重命名' : 'Rename', - icon: , - onClick: () => { - setRenameDialog({ asset, newName: asset.name }); - setContextMenu(null); - } - }); - - items.push({ - label: locale === 'zh' ? '删除' : 'Delete', - icon: , - onClick: () => { - setDeleteConfirmDialog(asset); - setContextMenu(null); - } - }); - return items; - }, [currentPath, fileActionRegistry, handleAssetDoubleClick, loadAssets, locale, t.newFolder]); + }, [currentPath, fileActionRegistry, handleAssetDoubleClick, loadAssets, locale, t.newFolder, setRenameDialog, setDeleteConfirmDialog, setContextMenu, setCreateFileDialog]); // Render folder tree node const renderFolderNode = useCallback((node: FolderNode, depth: number = 0) => { @@ -818,7 +982,10 @@ export function ContentBrowser({ className={`cb-asset-item ${selectedPaths.has(asset.path) ? 'selected' : ''}`} onClick={(e) => handleAssetClick(asset, e)} onDoubleClick={() => handleAssetDoubleClick(asset)} - onContextMenu={(e) => handleContextMenu(e, asset)} + onContextMenu={(e) => { + e.stopPropagation(); + handleContextMenu(e, asset); + }} draggable={asset.type === 'file'} onDragStart={(e) => { if (asset.type === 'file') { diff --git a/packages/editor-app/src/components/ContextMenu.tsx b/packages/editor-app/src/components/ContextMenu.tsx index 1ac4c72b..31ee18b7 100644 --- a/packages/editor-app/src/components/ContextMenu.tsx +++ b/packages/editor-app/src/components/ContextMenu.tsx @@ -1,4 +1,5 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, useCallback } from 'react'; +import { ChevronRight } from 'lucide-react'; import '../styles/ContextMenu.css'; export interface ContextMenuItem { @@ -7,6 +8,10 @@ export interface ContextMenuItem { onClick: () => void; disabled?: boolean; separator?: boolean; + /** 快捷键提示文本 */ + shortcut?: string; + /** 子菜单项 */ + children?: ContextMenuItem[]; } interface ContextMenuProps { @@ -15,9 +20,113 @@ interface ContextMenuProps { onClose: () => void; } +interface SubMenuProps { + items: ContextMenuItem[]; + parentRect: DOMRect; + onClose: () => void; +} + +/** + * 子菜单组件 + */ +function SubMenu({ items, parentRect, onClose }: SubMenuProps) { + const menuRef = useRef(null); + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [activeSubmenuIndex, setActiveSubmenuIndex] = useState(null); + const [submenuRect, setSubmenuRect] = useState(null); + + useEffect(() => { + if (menuRef.current) { + const menu = menuRef.current; + const rect = menu.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // 默认在父菜单右侧显示 + let x = parentRect.right; + let y = parentRect.top; + + // 如果右侧空间不足,显示在左侧 + if (x + rect.width > viewportWidth) { + x = parentRect.left - rect.width; + } + + // 如果底部空间不足,向上调整 + if (y + rect.height > viewportHeight) { + y = Math.max(0, viewportHeight - rect.height - 10); + } + + setPosition({ x, y }); + } + }, [parentRect]); + + const handleItemMouseEnter = useCallback((index: number, item: ContextMenuItem, e: React.MouseEvent) => { + if (item.children && item.children.length > 0) { + setActiveSubmenuIndex(index); + const itemRect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + setSubmenuRect(itemRect); + } else { + setActiveSubmenuIndex(null); + setSubmenuRect(null); + } + }, []); + + return ( +
+ {items.map((item, index) => { + if (item.separator) { + return
; + } + + const hasChildren = item.children && item.children.length > 0; + + return ( +
{ + if (!item.disabled && !hasChildren) { + item.onClick(); + onClose(); + } + }} + onMouseEnter={(e) => handleItemMouseEnter(index, item, e)} + onMouseLeave={() => { + if (!item.children) { + setActiveSubmenuIndex(null); + } + }} + > + {item.icon && {item.icon}} + {item.label} + {item.shortcut && {item.shortcut}} + {hasChildren && } + {activeSubmenuIndex === index && submenuRect && item.children && ( + + )} +
+ ); + })} +
+ ); +} + export function ContextMenu({ items, position, onClose }: ContextMenuProps) { const menuRef = useRef(null); const [adjustedPosition, setAdjustedPosition] = useState(position); + const [activeSubmenuIndex, setActiveSubmenuIndex] = useState(null); + const [submenuRect, setSubmenuRect] = useState(null); useEffect(() => { if (menuRef.current) { @@ -65,6 +174,17 @@ export function ContextMenu({ items, position, onClose }: ContextMenuProps) { }; }, [onClose]); + const handleItemMouseEnter = useCallback((index: number, item: ContextMenuItem, e: React.MouseEvent) => { + if (item.children && item.children.length > 0) { + setActiveSubmenuIndex(index); + const itemRect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + setSubmenuRect(itemRect); + } else { + setActiveSubmenuIndex(null); + setSubmenuRect(null); + } + }, []); + return (
; } + const hasChildren = item.children && item.children.length > 0; + return (
{ - if (!item.disabled) { + if (!item.disabled && !hasChildren) { item.onClick(); onClose(); } }} + onMouseEnter={(e) => handleItemMouseEnter(index, item, e)} + onMouseLeave={() => { + if (!item.children) { + setActiveSubmenuIndex(null); + } + }} > {item.icon && {item.icon}} {item.label} + {item.shortcut && {item.shortcut}} + {hasChildren && } + {activeSubmenuIndex === index && submenuRect && item.children && ( + + )}
); })} diff --git a/packages/editor-app/src/components/FileTree.tsx b/packages/editor-app/src/components/FileTree.tsx index 03395fee..74c589fd 100644 --- a/packages/editor-app/src/components/FileTree.tsx +++ b/packages/editor-app/src/components/FileTree.tsx @@ -1,6 +1,9 @@ import { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; import * as LucideIcons from 'lucide-react'; -import { Folder, ChevronRight, ChevronDown, File, Edit3, Trash2, FolderOpen, Copy, FileText, FolderPlus, Plus } from 'lucide-react'; +import { + Folder, ChevronRight, ChevronDown, File, Edit3, Trash2, FolderOpen, Copy, FileText, FolderPlus, Plus, + Save, Tag, Link, FileSearch, Globe, Package, Clipboard, RefreshCw, Settings +} from 'lucide-react'; import { TauriAPI, DirectoryEntry } from '../api/tauri'; import { MessageHub, FileActionRegistry } from '@esengine/editor-core'; import { Core } from '@esengine/ecs-framework'; @@ -614,25 +617,187 @@ export const FileTree = forwardRef(({ rootPath, o } } } + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 文件操作菜单项 + items.push({ + label: '保存', + icon: , + shortcut: 'Ctrl+S', + onClick: () => { + // TODO: 实现保存功能 + console.log('Save file:', node.path); + } + }); } items.push({ label: '重命名', icon: , + shortcut: 'F2', onClick: () => { setRenamingNode(node.path); setNewName(node.name); } }); + items.push({ + label: '批量重命名', + icon: , + shortcut: 'Shift+F2', + disabled: true, // TODO: 实现批量重命名 + onClick: () => { + console.log('Batch rename'); + } + }); + + items.push({ + label: '复制', + icon: , + shortcut: 'Ctrl+D', + onClick: () => { + // TODO: 实现复制功能 + console.log('Duplicate:', node.path); + } + }); + items.push({ label: '删除', icon: , + shortcut: 'Delete', onClick: () => handleDeleteClick(node) }); items.push({ label: '', separator: true, onClick: () => {} }); + // 资产操作子菜单 + items.push({ + label: '资产操作', + icon: , + onClick: () => {}, + children: [ + { + label: '重新导入', + icon: , + onClick: () => { + console.log('Reimport asset:', node.path); + } + }, + { + label: '导出...', + icon: , + onClick: () => { + console.log('Export asset:', node.path); + } + }, + { label: '', separator: true, onClick: () => {} }, + { + label: '迁移资产', + icon: , + onClick: () => { + console.log('Migrate asset:', node.path); + } + } + ] + }); + + // 资产本地化子菜单 + items.push({ + label: '资产本地化', + icon: , + onClick: () => {}, + children: [ + { + label: '创建本地化资产', + onClick: () => { + console.log('Create localized asset:', node.path); + } + }, + { + label: '导入翻译', + onClick: () => { + console.log('Import translation:', node.path); + } + }, + { + label: '导出翻译', + onClick: () => { + console.log('Export translation:', node.path); + } + } + ] + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 标签和引用 + items.push({ + label: '管理标签', + icon: , + shortcut: 'Ctrl+T', + onClick: () => { + console.log('Manage tags:', node.path); + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 路径复制选项 + items.push({ + label: '复制引用', + icon: , + shortcut: 'Ctrl+C', + onClick: () => { + navigator.clipboard.writeText(node.path); + } + }); + + items.push({ + label: '拷贝Object路径', + icon: , + shortcut: 'Ctrl+Shift+C', + onClick: () => { + // 生成对象路径格式 + const objectPath = node.path.replace(/\\/g, '/'); + navigator.clipboard.writeText(objectPath); + } + }); + + items.push({ + label: '拷贝包路径', + icon: , + shortcut: 'Ctrl+Alt+C', + onClick: () => { + // 生成包路径格式 + const packagePath = '/' + node.path.replace(/\\/g, '/').split('/').slice(-2).join('/'); + navigator.clipboard.writeText(packagePath); + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 引用查看器 + items.push({ + label: '引用查看器', + icon: , + shortcut: 'Alt+Shift+R', + onClick: () => { + console.log('Open reference viewer:', node.path); + } + }); + + items.push({ + label: '尺寸信息图', + icon: , + shortcut: 'Alt+Shift+D', + onClick: () => { + console.log('Show size map:', node.path); + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + if (node.type === 'folder') { items.push({ label: '新建文件', @@ -675,14 +840,6 @@ export const FileTree = forwardRef(({ rootPath, o } }); - items.push({ - label: '复制路径', - icon: , - onClick: () => { - navigator.clipboard.writeText(node.path); - } - }); - return items; }; diff --git a/packages/editor-app/src/components/OutputLogPanel.tsx b/packages/editor-app/src/components/OutputLogPanel.tsx index 71bfc910..a3b22e74 100644 --- a/packages/editor-app/src/components/OutputLogPanel.tsx +++ b/packages/editor-app/src/components/OutputLogPanel.tsx @@ -3,9 +3,8 @@ import { LogService, LogEntry } from '@esengine/editor-core'; import { LogLevel } from '@esengine/ecs-framework'; import { Search, Filter, Settings, X, Trash2, ChevronDown, - Bug, Info, AlertTriangle, XCircle, AlertCircle, Wifi, Pause, Play + Bug, Info, AlertTriangle, XCircle, AlertCircle, Wifi, Pause, Play, Copy } from 'lucide-react'; -import { JsonViewer } from './JsonViewer'; import '../styles/OutputLogPanel.css'; interface OutputLogPanelProps { @@ -16,15 +15,6 @@ interface OutputLogPanelProps { const MAX_LOGS = 1000; -function tryParseJSON(message: string): { isJSON: boolean; parsed?: unknown } { - try { - const parsed: unknown = JSON.parse(message); - return { isJSON: true, parsed }; - } catch { - return { isJSON: false }; - } -} - function formatTime(date: Date): string { const hours = date.getHours().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, '0'); @@ -33,103 +23,121 @@ function formatTime(date: Date): string { return `${hours}:${minutes}:${seconds}.${ms}`; } -function getLevelIcon(level: LogLevel) { +function getLevelIcon(level: LogLevel, size: number = 14) { switch (level) { case LogLevel.Debug: - return ; + return ; case LogLevel.Info: - return ; + return ; case LogLevel.Warn: - return ; + return ; case LogLevel.Error: case LogLevel.Fatal: - return ; + return ; default: - return ; + return ; } } function getLevelClass(level: LogLevel): string { switch (level) { case LogLevel.Debug: - return 'log-entry-debug'; + return 'output-log-entry-debug'; case LogLevel.Info: - return 'log-entry-info'; + return 'output-log-entry-info'; case LogLevel.Warn: - return 'log-entry-warn'; + return 'output-log-entry-warn'; case LogLevel.Error: case LogLevel.Fatal: - return 'log-entry-error'; + return 'output-log-entry-error'; default: return ''; } } -const LogEntryItem = memo(({ log, onOpenJsonViewer }: { +/** + * 尝试从消息中提取堆栈信息 + */ +function extractStackTrace(message: string): { message: string; stack: string | null } { + const stackPattern = /\n\s*at\s+/; + if (stackPattern.test(message)) { + const lines = message.split('\n'); + const messageLines: string[] = []; + const stackLines: string[] = []; + let inStack = false; + + for (const line of lines) { + if (line.trim().startsWith('at ') || inStack) { + inStack = true; + stackLines.push(line); + } else { + messageLines.push(line); + } + } + + return { + message: messageLines.join('\n').trim(), + stack: stackLines.length > 0 ? stackLines.join('\n') : null + }; + } + + return { message, stack: null }; +} + +const LogEntryItem = memo(({ log, isExpanded, onToggle, onCopy }: { log: LogEntry; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onOpenJsonViewer: (data: any) => void; + isExpanded: boolean; + onToggle: () => void; + onCopy: () => void; }) => { - const { isJSON, parsed } = useMemo(() => tryParseJSON(log.message), [log.message]); - const shouldTruncate = log.message.length > 200; - const [isExpanded, setIsExpanded] = useState(false); + // 优先使用 log.stack,否则尝试从 message 中提取 + const { message, stack } = useMemo(() => { + if (log.stack) { + return { message: log.message, stack: log.stack }; + } + return extractStackTrace(log.message); + }, [log.message, log.stack]); + + const hasStack = !!stack; return ( -
-
- {getLevelIcon(log.level)} +
+
+
+ {getLevelIcon(log.level)} +
+
+ {formatTime(log.timestamp)} +
+
+ [{log.source === 'remote' ? 'Remote' : log.source}] +
+
+ {message} +
+
-
- {formatTime(log.timestamp)} -
-
- [{log.source === 'remote' ? '🌐 Remote' : log.source}] -
- {log.clientId && ( -
- {log.clientId} + {isExpanded && stack && ( +
+
调用堆栈:
+ {stack.split('\n').filter(line => line.trim()).map((line, index) => ( +
+ {line} +
+ ))}
)} -
-
-
- {shouldTruncate && !isExpanded ? ( - <> - - {log.message.substring(0, 200)}... - - - - ) : ( - <> - {log.message} - {shouldTruncate && ( - - )} - - )} -
- {isJSON && parsed !== undefined && ( - - )} -
-
); }); @@ -150,10 +158,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog const [autoScroll, setAutoScroll] = useState(true); const [showFilterMenu, setShowFilterMenu] = useState(false); const [showSettingsMenu, setShowSettingsMenu] = useState(false); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [jsonViewerData, setJsonViewerData] = useState(null); - const [showTimestamp, setShowTimestamp] = useState(true); - const [showSource, setShowSource] = useState(true); + const [expandedLogIds, setExpandedLogIds] = useState>(new Set()); const logContainerRef = useRef(null); const filterMenuRef = useRef(null); const settingsMenuRef = useRef(null); @@ -174,7 +179,6 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog } }, [logs, autoScroll]); - // Close menus on outside click useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (filterMenuRef.current && !filterMenuRef.current.contains(e.target as Node)) { @@ -199,6 +203,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog const handleClear = useCallback(() => { logService.clear(); setLogs([]); + setExpandedLogIds(new Set()); }, [logService]); const toggleLevelFilter = useCallback((level: LogLevel) => { @@ -213,6 +218,22 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog }); }, []); + const toggleLogExpanded = useCallback((logId: string) => { + setExpandedLogIds(prev => { + const newSet = new Set(prev); + if (newSet.has(logId)) { + newSet.delete(logId); + } else { + newSet.add(logId); + } + return newSet; + }); + }, []); + + const handleCopyLog = useCallback((log: LogEntry) => { + navigator.clipboard.writeText(log.message); + }, []); + const filteredLogs = useMemo(() => { return logs.filter((log) => { if (!levelFilter.has(log.level)) return false; @@ -376,26 +397,6 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog {showSettingsMenu && (
-
- {locale === 'zh' ? '显示选项' : 'Display Options'} -
- - -
)}
- - {/* JSON Viewer Modal */} - {jsonViewerData && ( - setJsonViewerData(null)} - /> - )}
); } diff --git a/packages/editor-app/src/components/PluginListSetting.tsx b/packages/editor-app/src/components/PluginListSetting.tsx index f4a2ec2c..5b8d3ccc 100644 --- a/packages/editor-app/src/components/PluginListSetting.tsx +++ b/packages/editor-app/src/components/PluginListSetting.tsx @@ -11,8 +11,8 @@ import { useState, useEffect } from 'react'; import { Core } from '@esengine/ecs-framework'; -import { PluginManager, type RegisteredPlugin, type PluginCategory } from '@esengine/editor-core'; -import { Check, Lock, RefreshCw, Package } from 'lucide-react'; +import { PluginManager, type RegisteredPlugin, type PluginCategory, ProjectService } from '@esengine/editor-core'; +import { Check, Lock, Package } from 'lucide-react'; import { NotificationService } from '../services/NotificationService'; import '../styles/PluginListSetting.css'; @@ -30,14 +30,14 @@ const categoryLabels: Record = { networking: { zh: '网络', en: 'Networking' }, tools: { zh: '工具', en: 'Tools' }, scripting: { zh: '脚本', en: 'Scripting' }, - content: { zh: '内容', en: 'Content' } + content: { zh: '内容', en: 'Content' }, + tilemap: { zh: '瓦片地图', en: 'Tilemap' } }; -const categoryOrder: PluginCategory[] = ['core', 'rendering', 'ui', 'ai', 'scripting', 'physics', 'audio', 'networking', 'tools', 'content']; +const categoryOrder: PluginCategory[] = ['core', 'rendering', 'ui', 'ai', 'scripting', 'physics', 'audio', 'networking', 'tilemap', 'tools', 'content']; export function PluginListSetting({ pluginManager }: PluginListSettingProps) { const [plugins, setPlugins] = useState([]); - const [pendingChanges, setPendingChanges] = useState>(new Map()); useEffect(() => { loadPlugins(); @@ -55,11 +55,11 @@ export function PluginListSetting({ pluginManager }: PluginListSettingProps) { } }; - const handleToggle = (pluginId: string) => { - const plugin = plugins.find(p => p.loader.descriptor.id === pluginId); + const handleToggle = async (pluginId: string) => { + const plugin = plugins.find(p => p.plugin.descriptor.id === pluginId); if (!plugin) return; - const descriptor = plugin.loader.descriptor; + const descriptor = plugin.plugin.descriptor; // 核心插件不可禁用 if (descriptor.isCore) { @@ -69,11 +69,11 @@ export function PluginListSetting({ pluginManager }: PluginListSettingProps) { const newEnabled = !plugin.enabled; - // 检查依赖 + // 检查依赖(启用时) if (newEnabled) { const deps = descriptor.dependencies || []; const missingDeps = deps.filter(dep => { - const depPlugin = plugins.find(p => p.loader.descriptor.id === dep.id); + const depPlugin = plugins.find(p => p.plugin.descriptor.id === dep.id); return depPlugin && !depPlugin.enabled; }); @@ -81,44 +81,76 @@ export function PluginListSetting({ pluginManager }: PluginListSettingProps) { showWarning(`需要先启用依赖插件: ${missingDeps.map(d => d.id).join(', ')}`); return; } - } else { - // 检查是否有其他插件依赖此插件 - const dependents = plugins.filter(p => { - if (!p.enabled || p.loader.descriptor.id === pluginId) return false; - const deps = p.loader.descriptor.dependencies || []; - return deps.some(d => d.id === pluginId); - }); - - if (dependents.length > 0) { - showWarning(`以下插件依赖此插件: ${dependents.map(p => p.loader.descriptor.name).join(', ')}`); - return; - } } - // 记录待处理的更改 - const newPendingChanges = new Map(pendingChanges); - newPendingChanges.set(pluginId, newEnabled); - setPendingChanges(newPendingChanges); + // 调用 PluginManager 的动态启用/禁用方法 + console.log(`[PluginListSetting] ${newEnabled ? 'Enabling' : 'Disabling'} plugin: ${pluginId}`); + let success: boolean; + if (newEnabled) { + success = await pluginManager.enable(pluginId); + } else { + success = await pluginManager.disable(pluginId); + } + console.log(`[PluginListSetting] ${newEnabled ? 'Enable' : 'Disable'} result: ${success}`); + + if (!success) { + showWarning(newEnabled ? '启用插件失败' : '禁用插件失败'); + return; + } // 更新本地状态 setPlugins(plugins.map(p => { - if (p.loader.descriptor.id === pluginId) { + if (p.plugin.descriptor.id === pluginId) { return { ...p, enabled: newEnabled }; } return p; })); - // 调用 PluginManager 的启用/禁用方法 - if (newEnabled) { - pluginManager.enable(pluginId); - } else { - pluginManager.disable(pluginId); + // 保存到项目配置 + savePluginConfigToProject(); + + // 通知用户(如果有编辑器模块变更) + const hasEditorModule = !!plugin.plugin.editorModule; + if (hasEditorModule) { + const notificationService = Core.services.tryResolve(NotificationService) as NotificationService | null; + if (notificationService) { + notificationService.show( + newEnabled ? `已启用插件: ${descriptor.name}` : `已禁用插件: ${descriptor.name}`, + 'success', + 2000 + ); + } + } + }; + + /** + * 保存插件配置到项目文件 + */ + const savePluginConfigToProject = async () => { + const projectService = Core.services.tryResolve(ProjectService); + if (!projectService || !projectService.isProjectOpen()) { + console.warn('[PluginListSetting] Cannot save: project not open'); + return; + } + + // 获取当前启用的插件列表(排除核心插件) + const enabledPlugins = pluginManager.getEnabledPlugins() + .filter(p => !p.plugin.descriptor.isCore) + .map(p => p.plugin.descriptor.id); + + console.log('[PluginListSetting] Saving enabled plugins:', enabledPlugins); + + try { + await projectService.setEnabledPlugins(enabledPlugins); + console.log('[PluginListSetting] Plugin config saved successfully'); + } catch (error) { + console.error('[PluginListSetting] Failed to save plugin config:', error); } }; // 按类别分组并排序 const groupedPlugins = plugins.reduce((acc, plugin) => { - const category = plugin.loader.descriptor.category; + const category = plugin.plugin.descriptor.category; if (!acc[category]) { acc[category] = []; } @@ -131,13 +163,6 @@ export function PluginListSetting({ pluginManager }: PluginListSettingProps) { return (
- {pendingChanges.size > 0 && ( -
- - 部分更改需要重启编辑器后生效 -
- )} - {sortedCategories.map(category => (
@@ -145,9 +170,9 @@ export function PluginListSetting({ pluginManager }: PluginListSettingProps) {
{groupedPlugins[category].map(plugin => { - const descriptor = plugin.loader.descriptor; - const hasRuntime = !!plugin.loader.runtimeModule; - const hasEditor = !!plugin.loader.editorModule; + const descriptor = plugin.plugin.descriptor; + const hasRuntime = !!plugin.plugin.runtimeModule; + const hasEditor = !!plugin.plugin.editorModule; return (
{ const settings = SettingsService.getInstance(); - setServerPort(settings.get('profiler.port', '8080')); + const savedPort = settings.get('profiler.port', 8080); + console.log('[PortManager] Initial port from settings:', savedPort); + setServerPort(String(savedPort)); const handleSettingsChange = ((event: CustomEvent) => { + console.log('[PortManager] settings:changed event received:', event.detail); const newPort = event.detail['profiler.port']; - if (newPort) { - setServerPort(newPort); + if (newPort !== undefined) { + console.log('[PortManager] Updating port to:', newPort); + setServerPort(String(newPort)); } }) as EventListener; diff --git a/packages/editor-app/src/components/SceneHierarchy.tsx b/packages/editor-app/src/components/SceneHierarchy.tsx index 10bc5c28..706acd00 100644 --- a/packages/editor-app/src/components/SceneHierarchy.tsx +++ b/packages/editor-app/src/components/SceneHierarchy.tsx @@ -1,16 +1,16 @@ -import { useState, useEffect, useRef } from 'react'; -import { Entity, Core } from '@esengine/ecs-framework'; +import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; +import { Entity, Core, HierarchySystem, HierarchyComponent, EntityTags, isFolder } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub, SceneManagerService, CommandManager, EntityCreationRegistry, EntityCreationTemplate } from '@esengine/editor-core'; import { useLocale } from '../hooks/useLocale'; import * as LucideIcons from 'lucide-react'; import { Box, Wifi, Search, Plus, Trash2, Monitor, Globe, ChevronRight, ChevronDown, Eye, Star, Lock, Settings, Filter, Folder, Sun, Cloud, Mountain, Flag, - SquareStack + SquareStack, FolderPlus } from 'lucide-react'; import { ProfilerService, RemoteEntity } from '../services/ProfilerService'; import { confirm } from '@tauri-apps/plugin-dialog'; -import { CreateEntityCommand, DeleteEntityCommand } from '../application/commands/entity'; +import { CreateEntityCommand, DeleteEntityCommand, ReparentEntityCommand, DropPosition } from '../application/commands/entity'; import '../styles/SceneHierarchy.css'; function getIconComponent(iconName: string | undefined, size: number = 14): React.ReactNode { @@ -61,8 +61,19 @@ interface SceneHierarchyProps { interface EntityNode { entity: Entity; children: EntityNode[]; - isExpanded: boolean; depth: number; + bIsFolder: boolean; + hasChildren: boolean; +} + +/** + * 拖放指示器位置 + */ +enum DropIndicator { + NONE = 'none', + BEFORE = 'before', + INSIDE = 'inside', + AFTER = 'after' } export function SceneHierarchy({ entityStore, messageHub, commandManager, isProfilerMode = false }: SceneHierarchyProps) { @@ -78,9 +89,9 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf const [isSceneModified, setIsSceneModified] = useState(false); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; entityId: number | null } | null>(null); const [draggedEntityId, setDraggedEntityId] = useState(null); - const [dropTargetIndex, setDropTargetIndex] = useState(null); + const [dropTarget, setDropTarget] = useState<{ entityId: number; indicator: DropIndicator } | null>(null); const [pluginTemplates, setPluginTemplates] = useState([]); - const [expandedFolders, setExpandedFolders] = useState>(new Set()); + const [expandedIds, setExpandedIds] = useState>(new Set([-1])); // -1 is scene root const [sortColumn, setSortColumn] = useState('name'); const [sortDirection, setSortDirection] = useState('asc'); const [showFilterMenu, setShowFilterMenu] = useState(false); @@ -89,6 +100,68 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf const isShowingRemote = viewMode === 'remote' && isRemoteConnected; const selectedId = selectedIds.size > 0 ? Array.from(selectedIds)[0] : null; + /** + * 构建层级树结构 + */ + const buildEntityTree = useCallback((rootEntities: Entity[]): EntityNode[] => { + const scene = Core.scene; + if (!scene) return []; + + const buildNode = (entity: Entity, depth: number): EntityNode => { + const hierarchy = entity.getComponent(HierarchyComponent); + const childIds = hierarchy?.childIds ?? []; + const bIsEntityFolder = isFolder(entity.tag); + + const children: EntityNode[] = []; + for (const childId of childIds) { + const childEntity = scene.findEntityById(childId); + if (childEntity) { + children.push(buildNode(childEntity, depth + 1)); + } + } + + return { + entity, + children, + depth, + bIsFolder: bIsEntityFolder, + hasChildren: children.length > 0 + }; + }; + + return rootEntities.map((entity) => buildNode(entity, 1)); + }, []); + + /** + * 扁平化树为带深度信息的列表(用于渲染) + */ + const flattenTree = useCallback((nodes: EntityNode[], expandedSet: Set): EntityNode[] => { + const result: EntityNode[] = []; + + const traverse = (nodeList: EntityNode[]) => { + for (const node of nodeList) { + result.push(node); + + const bIsExpanded = expandedSet.has(node.entity.id); + if (bIsExpanded && node.children.length > 0) { + traverse(node.children); + } + } + }; + + traverse(nodes); + return result; + }, []); + + /** + * 层级树和扁平化列表 + */ + const entityTree = useMemo(() => buildEntityTree(entities), [entities, buildEntityTree]); + const flattenedEntities = useMemo( + () => expandedIds.has(-1) ? flattenTree(entityTree, expandedIds) : [], + [entityTree, expandedIds, flattenTree] + ); + // Get entity creation templates from plugins useEffect(() => { const updateTemplates = () => { @@ -171,7 +244,9 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf const unsubSelect = messageHub.subscribe('entity:selected', handleSelection); const unsubSceneLoaded = messageHub.subscribe('scene:loaded', updateEntities); const unsubSceneNew = messageHub.subscribe('scene:new', updateEntities); + const unsubSceneRestored = messageHub.subscribe('scene:restored', updateEntities); const unsubReordered = messageHub.subscribe('entity:reordered', updateEntities); + const unsubReparented = messageHub.subscribe('entity:reparented', updateEntities); return () => { unsubAdd(); @@ -180,7 +255,9 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf unsubSelect(); unsubSceneLoaded(); unsubSceneNew(); + unsubSceneRestored(); unsubReordered(); + unsubReparented(); }; }, [entityStore, messageHub]); @@ -258,35 +335,110 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf } }; - const handleDragStart = (e: React.DragEvent, entityId: number) => { + const handleDragStart = useCallback((e: React.DragEvent, entityId: number) => { setDraggedEntityId(entityId); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', entityId.toString()); - }; + }, []); - const handleDragOver = (e: React.DragEvent, index: number) => { + /** + * 根据鼠标位置计算拖放指示器位置 + * 上20%区域 = BEFORE, 中间60% = INSIDE, 下20% = AFTER + * 所有实体都支持作为父节点接收子节点 + */ + const calculateDropIndicator = useCallback((e: React.DragEvent, _targetNode: EntityNode): DropIndicator => { + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + const y = e.clientY - rect.top; + const height = rect.height; + + if (y < height * 0.2) { + return DropIndicator.BEFORE; + } else if (y > height * 0.8) { + return DropIndicator.AFTER; + } else { + return DropIndicator.INSIDE; + } + }, []); + + const handleDragOver = useCallback((e: React.DragEvent, targetNode: EntityNode) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; - setDropTargetIndex(index); - }; - const handleDragLeave = () => { - setDropTargetIndex(null); - }; - - const handleDrop = (e: React.DragEvent, targetIndex: number) => { - e.preventDefault(); - if (draggedEntityId !== null) { - entityStore.reorderEntity(draggedEntityId, targetIndex); + // 不能拖放到自己 + if (draggedEntityId === targetNode.entity.id) { + setDropTarget(null); + return; } - setDraggedEntityId(null); - setDropTargetIndex(null); - }; - const handleDragEnd = () => { + // 检查是否拖到自己的子节点 + const scene = Core.scene; + if (scene && draggedEntityId !== null) { + const hierarchySystem = scene.getSystem(HierarchySystem); + const draggedEntity = scene.findEntityById(draggedEntityId); + if (draggedEntity && hierarchySystem?.isAncestorOf(draggedEntity, targetNode.entity)) { + setDropTarget(null); + return; + } + } + + const indicator = calculateDropIndicator(e, targetNode); + setDropTarget({ entityId: targetNode.entity.id, indicator }); + }, [draggedEntityId, calculateDropIndicator]); + + const handleDragLeave = useCallback(() => { + setDropTarget(null); + }, []); + + const handleDrop = useCallback((e: React.DragEvent, targetNode: EntityNode) => { + e.preventDefault(); + + if (draggedEntityId === null || !dropTarget) { + setDraggedEntityId(null); + setDropTarget(null); + return; + } + + const scene = Core.scene; + if (!scene) return; + + const draggedEntity = scene.findEntityById(draggedEntityId); + if (!draggedEntity) return; + + // 转换 DropIndicator 到 DropPosition + let dropPosition: DropPosition; + switch (dropTarget.indicator) { + case DropIndicator.BEFORE: + dropPosition = DropPosition.BEFORE; + break; + case DropIndicator.INSIDE: + dropPosition = DropPosition.INSIDE; + // 自动展开目标节点 + setExpandedIds(prev => new Set([...prev, targetNode.entity.id])); + break; + case DropIndicator.AFTER: + dropPosition = DropPosition.AFTER; + break; + default: + dropPosition = DropPosition.AFTER; + } + + const command = new ReparentEntityCommand( + entityStore, + messageHub, + draggedEntity, + targetNode.entity, + dropPosition + ); + commandManager.execute(command); + setDraggedEntityId(null); - setDropTargetIndex(null); - }; + setDropTarget(null); + }, [draggedEntityId, dropTarget, entityStore, messageHub, commandManager]); + + const handleDragEnd = useCallback(() => { + setDraggedEntityId(null); + setDropTarget(null); + }, []); const handleRemoteEntityClick = (entity: RemoteEntity) => { setSelectedIds(new Set([entity.id])); @@ -373,8 +525,8 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf return () => window.removeEventListener('keydown', handleKeyDown); }, [selectedId, isShowingRemote]); - const toggleFolderExpand = (entityId: number) => { - setExpandedFolders(prev => { + const toggleExpand = useCallback((entityId: number) => { + setExpandedIds(prev => { const next = new Set(prev); if (next.has(entityId)) { next.delete(entityId); @@ -383,7 +535,29 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf } return next; }); - }; + }, []); + + /** + * 创建文件夹实体 + */ + const handleCreateFolder = useCallback(() => { + const entityCount = entityStore.getAllEntities().length; + const folderName = locale === 'zh' ? `文件夹 ${entityCount + 1}` : `Folder ${entityCount + 1}`; + + const scene = Core.scene; + if (!scene) return; + + const entity = scene.createEntity(folderName); + entity.tag = EntityTags.FOLDER; + + // 添加 HierarchyComponent 支持层级结构 + entity.addComponent(new HierarchyComponent()); + + entityStore.addEntity(entity); + entityStore.selectEntity(entity); + messageHub.publish('entity:added', { entity }); + messageHub.publish('scene:modified', {}); + }, [entityStore, messageHub, locale]); const handleSortClick = (column: SortColumn) => { if (sortColumn === column) { @@ -394,20 +568,33 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf } }; - // Get entity type for display - const getEntityType = (entity: Entity): string => { + /** + * 获取实体类型显示名称 + */ + const getEntityType = useCallback((entity: Entity): string => { + if (isFolder(entity.tag)) { + return 'Folder'; + } + const components = entity.components || []; if (components.length > 0) { const firstComponent = components[0]; return firstComponent?.constructor?.name || 'Entity'; } return 'Entity'; - }; + }, []); - // Get icon for entity type - const getEntityIcon = (entityType: string): React.ReactNode => { + /** + * 获取实体类型图标 + */ + const getEntityIcon = useCallback((entity: Entity): React.ReactNode => { + if (isFolder(entity.tag)) { + return ; + } + + const entityType = getEntityType(entity); return entityTypeIcons[entityType] || ; - }; + }, [getEntityType]); // Filter entities based on search query const filterRemoteEntities = (entityList: RemoteEntity[]): RemoteEntity[] => { @@ -443,13 +630,10 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf }); }; - const displayEntities = isShowingRemote - ? filterRemoteEntities(remoteEntities) - : filterLocalEntities(entities); const showRemoteIndicator = isShowingRemote && remoteEntities.length > 0; const displaySceneName = isShowingRemote && remoteSceneName ? remoteSceneName : sceneName; - const totalCount = displayEntities.length; + const totalCount = isShowingRemote ? remoteEntities.length : entityStore.getAllEntities().length; const selectedCount = selectedIds.size; return ( @@ -479,13 +663,22 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
{!isShowingRemote && ( - + <> + + + )} + + {sortedCategories.length > 0 &&
} {sortedCategories.map(([category, templates]) => ( diff --git a/packages/editor-app/src/components/SettingsWindow.tsx b/packages/editor-app/src/components/SettingsWindow.tsx index d31320dc..3e5a5739 100644 --- a/packages/editor-app/src/components/SettingsWindow.tsx +++ b/packages/editor-app/src/components/SettingsWindow.tsx @@ -148,9 +148,14 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }: } else { const value = settings.get(key, descriptor.defaultValue); initialValues.set(key, value); + if (key.startsWith('profiler.')) { + console.log(`[SettingsWindow] Loading ${key}: stored=${settings.get(key, undefined)}, default=${descriptor.defaultValue}, using=${value}`); + } } } + console.log('[SettingsWindow] Initial values for profiler:', + Array.from(initialValues.entries()).filter(([k]) => k.startsWith('profiler.'))); setValues(initialValues); }, [settingsRegistry, initialCategoryId]); @@ -162,10 +167,24 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }: const newErrors = new Map(errors); if (!settingsRegistry.validateSetting(descriptor, value)) { newErrors.set(key, descriptor.validator?.errorMessage || '无效值'); + setErrors(newErrors); + return; // 验证失败,不保存 } else { newErrors.delete(key); } setErrors(newErrors); + + // 实时保存设置 + const settings = SettingsService.getInstance(); + if (!key.startsWith('project.')) { + settings.set(key, value); + console.log(`[SettingsWindow] Saved ${key}:`, value); + + // 触发设置变更事件 + window.dispatchEvent(new CustomEvent('settings:changed', { + detail: { [key]: value } + })); + } }; const handleSave = async () => { @@ -208,6 +227,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }: await projectService.setUIDesignResolution({ width: newWidth, height: newHeight }); } + console.log('[SettingsWindow] Saving settings, changedSettings:', changedSettings); window.dispatchEvent(new CustomEvent('settings:changed', { detail: changedSettings })); diff --git a/packages/editor-app/src/components/StartupPage.tsx b/packages/editor-app/src/components/StartupPage.tsx index 4c3077ee..f39e1541 100644 --- a/packages/editor-app/src/components/StartupPage.tsx +++ b/packages/editor-app/src/components/StartupPage.tsx @@ -11,7 +11,6 @@ interface StartupPageProps { onOpenProject: () => void; onCreateProject: () => void; onOpenRecentProject?: (projectPath: string) => void; - onProfilerMode?: () => void; onLocaleChange?: (locale: Locale) => void; recentProjects?: string[]; locale: string; @@ -22,7 +21,7 @@ const LANGUAGES = [ { code: 'zh', name: '中文' } ]; -export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onProfilerMode, onLocaleChange, recentProjects = [], locale }: StartupPageProps) { +export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) { const [showLogo, setShowLogo] = useState(true); const [hoveredProject, setHoveredProject] = useState(null); const [appVersion, setAppVersion] = useState(''); @@ -62,10 +61,8 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec subtitle: 'Professional Game Development Tool', openProject: 'Open Project', createProject: 'Create Project', - profilerMode: 'Profiler Mode', recentProjects: 'Recent Projects', noRecentProjects: 'No recent projects', - comingSoon: 'Coming Soon', updateAvailable: 'New version available', updateNow: 'Update Now', installing: 'Installing...', @@ -76,10 +73,8 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec subtitle: '专业游戏开发工具', openProject: '打开项目', createProject: '创建新项目', - profilerMode: '性能分析模式', recentProjects: '最近的项目', noRecentProjects: '没有最近的项目', - comingSoon: '即将推出', updateAvailable: '发现新版本', updateNow: '立即更新', installing: '正在安装...', @@ -126,13 +121,6 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec {t.createProject} - -
diff --git a/packages/editor-app/src/components/Viewport.tsx b/packages/editor-app/src/components/Viewport.tsx index 18b31c6c..32c04d95 100644 --- a/packages/editor-app/src/components/Viewport.tsx +++ b/packages/editor-app/src/components/Viewport.tsx @@ -8,8 +8,9 @@ import '../styles/Viewport.css'; import { useEngine } from '../hooks/useEngine'; import { EngineService } from '../services/EngineService'; import { Core, Entity, SceneSerializer } from '@esengine/ecs-framework'; -import { MessageHub } from '@esengine/editor-core'; -import { TransformComponent, CameraComponent } from '@esengine/ecs-components'; +import { MessageHub, ProjectService, AssetRegistryService } from '@esengine/editor-core'; +import { TransformComponent } from '@esengine/engine-core'; +import { CameraComponent } from '@esengine/camera'; import { UITransformComponent } from '@esengine/ui'; import { TauriAPI } from '../api/tauri'; import { open } from '@tauri-apps/plugin-shell'; @@ -59,7 +60,8 @@ function generateRuntimeHtml(): string { const runtime = ECSRuntime.create({ canvasId: 'runtime-canvas', width: window.innerWidth, - height: window.innerHeight + height: window.innerHeight, + projectConfigUrl: '/ecs-editor.config.json' }); await runtime.initialize(esEngine); @@ -354,11 +356,13 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) { if (messageHubRef.current) { const propertyName = mode === 'move' ? 'position' : mode === 'rotate' ? 'rotation' : 'scale'; + const value = propertyName === 'position' ? transform.position : + propertyName === 'rotation' ? transform.rotation : transform.scale; messageHubRef.current.publish('component:property:changed', { entity, component: transform, propertyName, - value: transform[propertyName] + value }); } } @@ -373,16 +377,29 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) { const rotationSpeed = 0.01; uiTransform.rotation += deltaX * rotationSpeed; } else if (mode === 'scale') { - const width = uiTransform.width * uiTransform.scaleX; - const height = uiTransform.height * uiTransform.scaleY; - const centerX = uiTransform.x + width * uiTransform.pivotX; - const centerY = uiTransform.y + height * uiTransform.pivotY; - const startDist = Math.sqrt((worldStart.x - centerX) ** 2 + (worldStart.y - centerY) ** 2); - const endDist = Math.sqrt((worldEnd.x - centerX) ** 2 + (worldEnd.y - centerY) ** 2); + const oldWidth = uiTransform.width * uiTransform.scaleX; + const oldHeight = uiTransform.height * uiTransform.scaleY; + + // pivot点的世界坐标(缩放前) + const pivotWorldX = uiTransform.x + oldWidth * uiTransform.pivotX; + const pivotWorldY = uiTransform.y + oldHeight * uiTransform.pivotY; + + const startDist = Math.sqrt((worldStart.x - pivotWorldX) ** 2 + (worldStart.y - pivotWorldY) ** 2); + const endDist = Math.sqrt((worldEnd.x - pivotWorldX) ** 2 + (worldEnd.y - pivotWorldY) ** 2); + if (startDist > 0) { const scaleFactor = endDist / startDist; - uiTransform.scaleX *= scaleFactor; - uiTransform.scaleY *= scaleFactor; + const newScaleX = uiTransform.scaleX * scaleFactor; + const newScaleY = uiTransform.scaleY * scaleFactor; + + const newWidth = uiTransform.width * newScaleX; + const newHeight = uiTransform.height * newScaleY; + + // 调整位置使pivot点保持不动 + uiTransform.x = pivotWorldX - newWidth * uiTransform.pivotX; + uiTransform.y = pivotWorldY - newHeight * uiTransform.pivotY; + uiTransform.scaleX = newScaleX; + uiTransform.scaleY = newScaleY; } } @@ -689,46 +706,118 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) { // Write scene data and HTML (always update) await TauriAPI.writeFileContent(`${runtimeDir}/scene.json`, sceneData); - // Copy texture assets referenced in the scene - // 复制场景中引用的纹理资产 - const sceneObj = JSON.parse(sceneData); - const texturePathSet = new Set(); - - // Find all texture paths in sprite components - if (sceneObj.entities) { - for (const entity of sceneObj.entities) { - if (entity.components) { - for (const comp of entity.components) { - if (comp.type === 'Sprite' && comp.data?.texture) { - texturePathSet.add(comp.data.texture); - } - } - } + // Copy project config file (for plugin settings) + // 复制项目配置文件(用于插件设置) + const projectService = Core.services.tryResolve(ProjectService); + const projectPath = projectService?.getCurrentProject()?.path; + if (projectPath) { + const configPath = `${projectPath}\\ecs-editor.config.json`; + const configExists = await TauriAPI.pathExists(configPath); + if (configExists) { + await TauriAPI.copyFile(configPath, `${runtimeDir}\\ecs-editor.config.json`); + console.log('[Viewport] Copied project config to runtime dir'); } } - // Create assets directory and copy textures + // Create assets directory + // 创建资产目录 const assetsDir = `${runtimeDir}\\assets`; const assetsDirExists = await TauriAPI.pathExists(assetsDir); if (!assetsDirExists) { await TauriAPI.createDirectory(assetsDir); } - for (const texturePath of texturePathSet) { - if (texturePath && (texturePath.includes(':\\') || texturePath.startsWith('/'))) { - try { - const filename = texturePath.split(/[/\\]/).pop() || ''; - const destPath = `${assetsDir}\\${filename}`; - const exists = await TauriAPI.pathExists(texturePath); - if (exists) { - await TauriAPI.copyFile(texturePath, destPath); + // Collect all asset paths from scene + // 从场景中收集所有资产路径 + const sceneObj = JSON.parse(sceneData); + const assetPaths = new Set(); + + // Scan all components for asset references + if (sceneObj.entities) { + for (const entity of sceneObj.entities) { + if (entity.components) { + for (const comp of entity.components) { + // Sprite textures + if (comp.type === 'Sprite' && comp.data?.texture) { + assetPaths.add(comp.data.texture); + } + // Behavior tree assets + if (comp.type === 'BehaviorTreeRuntime' && comp.data?.treeAssetId) { + assetPaths.add(comp.data.treeAssetId); + } + // Tilemap assets + if (comp.type === 'Tilemap' && comp.data?.tmxPath) { + assetPaths.add(comp.data.tmxPath); + } + // Audio assets + if (comp.type === 'AudioSource' && comp.data?.clip) { + assetPaths.add(comp.data.clip); + } } - } catch (error) { - console.error(`Failed to copy texture ${texturePath}:`, error); } } } + // Build asset catalog and copy files + // 构建资产目录并复制文件 + const catalogEntries: Record = {}; + + for (const assetPath of assetPaths) { + if (!assetPath || (!assetPath.includes(':\\') && !assetPath.startsWith('/'))) continue; + + try { + const exists = await TauriAPI.pathExists(assetPath); + if (!exists) { + console.warn(`[Viewport] Asset not found: ${assetPath}`); + continue; + } + + // Get filename and determine relative path + const filename = assetPath.split(/[/\\]/).pop() || ''; + const destPath = `${assetsDir}\\${filename}`; + const relativePath = `assets/${filename}`; + + // Copy file + await TauriAPI.copyFile(assetPath, destPath); + + // Determine asset type from extension + const ext = filename.substring(filename.lastIndexOf('.')).toLowerCase(); + const typeMap: Record = { + '.png': 'texture', '.jpg': 'texture', '.jpeg': 'texture', '.webp': 'texture', + '.btree': 'btree', + '.tmx': 'tilemap', '.tsx': 'tileset', + '.mp3': 'audio', '.ogg': 'audio', '.wav': 'audio', + '.json': 'json' + }; + const assetType = typeMap[ext] || 'binary'; + + // Generate simple GUID based on path + const guid = assetPath.replace(/[^a-zA-Z0-9]/g, '-').substring(0, 36); + + catalogEntries[guid] = { + guid, + path: relativePath, + type: assetType, + size: 0, + hash: '' + }; + + console.log(`[Viewport] Copied asset: ${filename}`); + } catch (error) { + console.error(`[Viewport] Failed to copy asset ${assetPath}:`, error); + } + } + + // Write asset catalog + // 写入资产目录 + const assetCatalog = { + version: '1.0.0', + createdAt: Date.now(), + entries: catalogEntries + }; + await TauriAPI.writeFileContent(`${runtimeDir}/asset-catalog.json`, JSON.stringify(assetCatalog, null, 2)); + console.log(`[Viewport] Asset catalog created with ${Object.keys(catalogEntries).length} entries`); + const runtimeHtml = generateRuntimeHtml(); await TauriAPI.writeFileContent(`${runtimeDir}/index.html`, runtimeHtml); @@ -781,6 +870,19 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) { await runtimeResolver.initialize(); await runtimeResolver.prepareRuntimeFiles(runtimeDir); + // Copy project config file (for plugin settings) + const projectService = Core.services.tryResolve(ProjectService); + if (projectService) { + const currentProject = projectService.getCurrentProject(); + if (currentProject?.path) { + const configPath = `${currentProject.path}\\ecs-editor.config.json`; + const configExists = await TauriAPI.pathExists(configPath); + if (configExists) { + await TauriAPI.copyFile(configPath, `${runtimeDir}\\ecs-editor.config.json`); + } + } + } + // Write scene data and HTML const sceneDataStr = typeof sceneData === 'string' ? sceneData : new TextDecoder().decode(sceneData); await TauriAPI.writeFileContent(`${runtimeDir}/scene.json`, sceneDataStr); diff --git a/packages/editor-app/src/components/inspectors/Inspector.tsx b/packages/editor-app/src/components/inspectors/Inspector.tsx index 1cae1c2d..75a3480e 100644 --- a/packages/editor-app/src/components/inspectors/Inspector.tsx +++ b/packages/editor-app/src/components/inspectors/Inspector.tsx @@ -124,7 +124,15 @@ export function Inspector({ entityStore: _entityStore, messageHub, inspectorRegi setComponentVersion((prev) => prev + 1); }; + const handleSceneRestored = () => { + // 场景恢复后,清除当前选中的实体(因为旧引用已无效) + // 用户需要重新选择实体 + setTarget(null); + setComponentVersion(0); + }; + const unsubEntitySelect = messageHub.subscribe('entity:selected', handleEntitySelection); + const unsubSceneRestored = messageHub.subscribe('scene:restored', handleSceneRestored); const unsubRemoteSelect = messageHub.subscribe('remote-entity:selected', handleRemoteEntitySelection); const unsubNodeSelect = messageHub.subscribe('behavior-tree:node-selected', handleExtensionSelection); const unsubAssetFileSelect = messageHub.subscribe('asset-file:selected', handleAssetFileSelection); @@ -136,6 +144,7 @@ export function Inspector({ entityStore: _entityStore, messageHub, inspectorRegi return () => { unsubEntitySelect(); + unsubSceneRestored(); unsubRemoteSelect(); unsubNodeSelect(); unsubAssetFileSelect(); diff --git a/packages/editor-app/src/components/inspectors/component-inspectors/TransformComponentInspector.tsx b/packages/editor-app/src/components/inspectors/component-inspectors/TransformComponentInspector.tsx index e93dedeb..68b84e11 100644 --- a/packages/editor-app/src/components/inspectors/component-inspectors/TransformComponentInspector.tsx +++ b/packages/editor-app/src/components/inspectors/component-inspectors/TransformComponentInspector.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { Component } from '@esengine/ecs-framework'; import { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; import { ChevronDown, Lock, Unlock } from 'lucide-react'; import '../../../styles/TransformInspector.css'; diff --git a/packages/editor-app/src/gizmos/SpriteGizmo.ts b/packages/editor-app/src/gizmos/SpriteGizmo.ts index a70e4959..aa906b6d 100644 --- a/packages/editor-app/src/gizmos/SpriteGizmo.ts +++ b/packages/editor-app/src/gizmos/SpriteGizmo.ts @@ -11,7 +11,8 @@ import type { Entity } from '@esengine/ecs-framework'; import type { IGizmoRenderData, IRectGizmoData, GizmoColor } from '@esengine/editor-core'; import { GizmoColors, GizmoRegistry } from '@esengine/editor-core'; -import { SpriteComponent, TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent } from '@esengine/sprite'; /** * Gizmo provider function for SpriteComponent. diff --git a/packages/editor-app/src/infrastructure/field-editors/AnimationClipsFieldEditor.tsx b/packages/editor-app/src/infrastructure/field-editors/AnimationClipsFieldEditor.tsx index e4948b96..26460c4f 100644 --- a/packages/editor-app/src/infrastructure/field-editors/AnimationClipsFieldEditor.tsx +++ b/packages/editor-app/src/infrastructure/field-editors/AnimationClipsFieldEditor.tsx @@ -2,7 +2,7 @@ import React, { useState, useCallback, useEffect } from 'react'; import { IFieldEditor, FieldEditorProps, MessageHub } from '@esengine/editor-core'; import { Core } from '@esengine/ecs-framework'; import { Plus, Trash2, ChevronDown, ChevronRight, Film, Upload, Star, Play, Square } from 'lucide-react'; -import type { AnimationClip, AnimationFrame, SpriteAnimatorComponent } from '@esengine/ecs-components'; +import type { AnimationClip, AnimationFrame, SpriteAnimatorComponent } from '@esengine/sprite'; import { AssetField } from '../../components/inspectors/fields/AssetField'; import { EngineService } from '../../services/EngineService'; diff --git a/packages/editor-app/src/locales/en.ts b/packages/editor-app/src/locales/en.ts index d4e0366e..e4d57e8f 100644 --- a/packages/editor-app/src/locales/en.ts +++ b/packages/editor-app/src/locales/en.ts @@ -15,8 +15,7 @@ export const en: Translations = { ready: 'Editor Ready', failed: 'Initialization Failed', projectOpened: 'Project Opened', - remoteConnected: 'Remote Game Connected', - profilerMode: 'Profiler Mode - Waiting for connection...' + remoteConnected: 'Remote Game Connected' } }, hierarchy: { diff --git a/packages/editor-app/src/locales/zh.ts b/packages/editor-app/src/locales/zh.ts index db117b63..8151a083 100644 --- a/packages/editor-app/src/locales/zh.ts +++ b/packages/editor-app/src/locales/zh.ts @@ -15,8 +15,7 @@ export const zh: Translations = { ready: '编辑器就绪', failed: '初始化失败', projectOpened: '项目已打开', - remoteConnected: '远程游戏已连接', - profilerMode: '性能分析模式 - 等待连接...' + remoteConnected: '远程游戏已连接' } }, hierarchy: { diff --git a/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx b/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx index 91561f83..da4ed07b 100644 --- a/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx +++ b/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx @@ -5,7 +5,7 @@ import type { ServiceContainer } from '@esengine/ecs-framework'; import { createLogger } from '@esengine/ecs-framework'; -import type { IPluginLoader, IEditorModuleLoader, PluginDescriptor } from '@esengine/editor-core'; +import type { IPlugin, IEditorModuleLoader, PluginDescriptor } from '@esengine/editor-core'; import { SettingsRegistry } from '@esengine/editor-core'; import { SettingsService } from '../../services/SettingsService'; @@ -122,7 +122,7 @@ const descriptor: PluginDescriptor = { ] }; -export const EditorAppearancePlugin: IPluginLoader = { +export const EditorAppearancePlugin: IPlugin = { descriptor, editorModule: new EditorAppearanceEditorModule() }; diff --git a/packages/editor-app/src/plugins/builtin/GizmoPlugin.ts b/packages/editor-app/src/plugins/builtin/GizmoPlugin.ts index df44343b..13184883 100644 --- a/packages/editor-app/src/plugins/builtin/GizmoPlugin.ts +++ b/packages/editor-app/src/plugins/builtin/GizmoPlugin.ts @@ -4,7 +4,7 @@ */ import type { ServiceContainer } from '@esengine/ecs-framework'; -import type { IPluginLoader, IEditorModuleLoader, PluginDescriptor, GizmoProviderRegistration } from '@esengine/editor-core'; +import type { IPlugin, IEditorModuleLoader, PluginDescriptor, GizmoProviderRegistration } from '@esengine/editor-core'; import { registerSpriteGizmo } from '../../gizmos'; /** @@ -44,7 +44,7 @@ const descriptor: PluginDescriptor = { ] }; -export const GizmoPlugin: IPluginLoader = { +export const GizmoPlugin: IPlugin = { descriptor, editorModule: new GizmoEditorModule() }; diff --git a/packages/editor-app/src/plugins/builtin/PluginConfigPlugin.tsx b/packages/editor-app/src/plugins/builtin/PluginConfigPlugin.tsx index f7701891..26e3c54c 100644 --- a/packages/editor-app/src/plugins/builtin/PluginConfigPlugin.tsx +++ b/packages/editor-app/src/plugins/builtin/PluginConfigPlugin.tsx @@ -5,7 +5,7 @@ import type { ServiceContainer } from '@esengine/ecs-framework'; import { createLogger } from '@esengine/ecs-framework'; -import type { IPluginLoader, IEditorModuleLoader, PluginDescriptor } from '@esengine/editor-core'; +import type { IPlugin, IEditorModuleLoader, PluginDescriptor } from '@esengine/editor-core'; import { SettingsRegistry } from '@esengine/editor-core'; const logger = createLogger('PluginConfigPlugin'); @@ -71,7 +71,7 @@ const descriptor: PluginDescriptor = { ] }; -export const PluginConfigPlugin: IPluginLoader = { +export const PluginConfigPlugin: IPlugin = { descriptor, editorModule: new PluginConfigEditorModule() }; diff --git a/packages/editor-app/src/plugins/builtin/ProfilerPlugin.tsx b/packages/editor-app/src/plugins/builtin/ProfilerPlugin.tsx index c48e3c14..7bdbcc62 100644 --- a/packages/editor-app/src/plugins/builtin/ProfilerPlugin.tsx +++ b/packages/editor-app/src/plugins/builtin/ProfilerPlugin.tsx @@ -5,14 +5,12 @@ import type { ServiceContainer } from '@esengine/ecs-framework'; import type { - IPluginLoader, + IPlugin, IEditorModuleLoader, PluginDescriptor, - PanelDescriptor, MenuItemDescriptor } from '@esengine/editor-core'; -import { MessageHub, SettingsRegistry, PanelPosition } from '@esengine/editor-core'; -import { ProfilerDockPanel } from '../../components/ProfilerDockPanel'; +import { MessageHub, SettingsRegistry } from '@esengine/editor-core'; import { ProfilerService } from '../../services/ProfilerService'; /** @@ -100,19 +98,6 @@ class ProfilerEditorModule implements IEditorModuleLoader { delete (window as any).__PROFILER_SERVICE__; } - getPanels(): PanelDescriptor[] { - return [ - { - id: 'profiler-monitor', - title: 'Performance Monitor', - position: PanelPosition.Center, - closable: false, - component: ProfilerDockPanel, - order: 200 - } - ]; - } - getMenuItems(): MenuItemDescriptor[] { return [ { @@ -122,14 +107,6 @@ class ProfilerEditorModule implements IEditorModuleLoader { execute: () => { this.messageHub?.publish('ui:openWindow', { windowId: 'profiler' }); } - }, - { - id: 'window.advancedProfiler', - label: 'Advanced Profiler', - parentId: 'window', - execute: () => { - this.messageHub?.publish('ui:openWindow', { windowId: 'advancedProfiler' }); - } } ]; } @@ -151,13 +128,12 @@ const descriptor: PluginDescriptor = { { name: 'ProfilerEditor', type: 'editor', - loadingPhase: 'postDefault', - panels: ['profiler-monitor'] + loadingPhase: 'postDefault' } ] }; -export const ProfilerPlugin: IPluginLoader = { +export const ProfilerPlugin: IPlugin = { descriptor, editorModule: new ProfilerEditorModule() }; diff --git a/packages/editor-app/src/plugins/builtin/ProjectSettingsPlugin.tsx b/packages/editor-app/src/plugins/builtin/ProjectSettingsPlugin.tsx index 67defe7d..424dc35c 100644 --- a/packages/editor-app/src/plugins/builtin/ProjectSettingsPlugin.tsx +++ b/packages/editor-app/src/plugins/builtin/ProjectSettingsPlugin.tsx @@ -8,7 +8,7 @@ import type { ServiceContainer } from '@esengine/ecs-framework'; import { createLogger, Core } from '@esengine/ecs-framework'; -import type { IPluginLoader, IEditorModuleLoader, PluginDescriptor } from '@esengine/editor-core'; +import type { IPlugin, IEditorModuleLoader, PluginDescriptor } from '@esengine/editor-core'; import { SettingsRegistry, ProjectService } from '@esengine/editor-core'; import EngineService from '../../services/EngineService'; @@ -167,7 +167,7 @@ const descriptor: PluginDescriptor = { ] }; -export const ProjectSettingsPlugin: IPluginLoader = { +export const ProjectSettingsPlugin: IPlugin = { descriptor, editorModule: new ProjectSettingsEditorModule() }; diff --git a/packages/editor-app/src/plugins/builtin/SceneInspectorPlugin.ts b/packages/editor-app/src/plugins/builtin/SceneInspectorPlugin.ts index d1ac645b..47fa3266 100644 --- a/packages/editor-app/src/plugins/builtin/SceneInspectorPlugin.ts +++ b/packages/editor-app/src/plugins/builtin/SceneInspectorPlugin.ts @@ -6,7 +6,7 @@ import { Core, Entity } from '@esengine/ecs-framework'; import type { ServiceContainer } from '@esengine/ecs-framework'; import type { - IPluginLoader, + IPlugin, IEditorModuleLoader, PluginDescriptor, PanelDescriptor, @@ -15,7 +15,9 @@ import type { EntityCreationTemplate } from '@esengine/editor-core'; import { PanelPosition, EntityStoreService, MessageHub } from '@esengine/editor-core'; -import { TransformComponent, SpriteComponent, SpriteAnimatorComponent, CameraComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent, SpriteAnimatorComponent } from '@esengine/sprite'; +import { CameraComponent } from '@esengine/camera'; /** * Scene Inspector 编辑器模块 @@ -186,14 +188,12 @@ const descriptor: PluginDescriptor = { { name: 'SceneInspectorEditor', type: 'editor', - loadingPhase: 'default', - panels: ['panel-scene-hierarchy', 'panel-entity-inspector'], - inspectors: ['EntityInspector'] + loadingPhase: 'default' } ] }; -export const SceneInspectorPlugin: IPluginLoader = { +export const SceneInspectorPlugin: IPlugin = { descriptor, editorModule: new SceneInspectorEditorModule() }; diff --git a/packages/editor-app/src/plugins/builtin/index.ts b/packages/editor-app/src/plugins/builtin/index.ts index 31c82848..08b5b57a 100644 --- a/packages/editor-app/src/plugins/builtin/index.ts +++ b/packages/editor-app/src/plugins/builtin/index.ts @@ -9,4 +9,5 @@ export { ProfilerPlugin } from './ProfilerPlugin'; export { EditorAppearancePlugin } from './EditorAppearancePlugin'; export { PluginConfigPlugin } from './PluginConfigPlugin'; export { ProjectSettingsPlugin } from './ProjectSettingsPlugin'; -export { BlueprintPlugin } from '@esengine/blueprint/editor'; +// TODO: Re-enable when blueprint-editor package is fixed +// export { BlueprintPlugin } from '@esengine/blueprint-editor'; diff --git a/packages/editor-app/src/services/EditorEngineSync.ts b/packages/editor-app/src/services/EditorEngineSync.ts index 394e9eb7..0735a0cd 100644 --- a/packages/editor-app/src/services/EditorEngineSync.ts +++ b/packages/editor-app/src/services/EditorEngineSync.ts @@ -8,7 +8,8 @@ import { Entity, Component } from '@esengine/ecs-framework'; import { MessageHub, EntityStoreService } from '@esengine/editor-core'; -import { TransformComponent, SpriteComponent, SpriteAnimatorComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent, SpriteAnimatorComponent } from '@esengine/sprite'; import { EngineService } from './EngineService'; export class EditorEngineSync { diff --git a/packages/editor-app/src/services/EngineService.ts b/packages/editor-app/src/services/EngineService.ts index e40d9915..72253a57 100644 --- a/packages/editor-app/src/services/EngineService.ts +++ b/packages/editor-app/src/services/EngineService.ts @@ -1,16 +1,17 @@ /** * Engine service for managing Rust engine lifecycle. * 管理Rust引擎生命周期的服务。 + * + * 使用统一的 GameRuntime 架构 + * Uses the unified GameRuntime architecture */ -import { EngineBridge, EngineRenderSystem, GizmoDataProviderFn, HasGizmoProviderFn, CameraConfig, CameraSystem } from '@esengine/ecs-engine-bindgen'; import { GizmoRegistry, EntityStoreService, MessageHub, SceneManagerService, ProjectService, PluginManager, IPluginManager, type SystemContext } from '@esengine/editor-core'; -import { Core, Scene, Entity, SceneSerializer } from '@esengine/ecs-framework'; -import { TransformComponent, SpriteComponent, SpriteAnimatorComponent, SpriteAnimatorSystem } from '@esengine/ecs-components'; -import { TilemapComponent, TilemapRenderingSystem } from '@esengine/tilemap'; -import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree'; -import { UIRenderDataProvider, invalidateUIRenderCaches, UIInputSystem } from '@esengine/ui'; -import { Physics2DSystem } from '@esengine/physics-rapier2d'; +import { Core, Scene, Entity, SceneSerializer, ProfilerSDK } from '@esengine/ecs-framework'; +import { CameraConfig } from '@esengine/ecs-engine-bindgen'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent, SpriteAnimatorComponent } from '@esengine/sprite'; +import { invalidateUIRenderCaches } from '@esengine/ui'; import * as esEngine from '@esengine/engine'; import { AssetManager, @@ -20,39 +21,41 @@ import { globalPathResolver, SceneResourceManager } from '@esengine/asset-system'; +import { + GameRuntime, + createGameRuntime, + EditorPlatformAdapter, + type GameRuntimeConfig +} from '@esengine/runtime-core'; import { convertFileSrc } from '@tauri-apps/api/core'; import { IdGenerator } from '../utils/idGenerator'; /** * Engine service singleton for editor integration. * 用于编辑器集成的引擎服务单例。 + * + * 内部使用 GameRuntime,对外保持原有 API 兼容 + * Internally uses GameRuntime, maintains original API compatibility externally */ export class EngineService { private static instance: EngineService | null = null; - private bridge: EngineBridge | null = null; - private scene: Scene | null = null; - private renderSystem: EngineRenderSystem | null = null; - private cameraSystem: CameraSystem | null = null; - private animatorSystem: SpriteAnimatorSystem | null = null; - private tilemapSystem: TilemapRenderingSystem | null = null; - private behaviorTreeSystem: BehaviorTreeExecutionSystem | null = null; - private physicsSystem: Physics2DSystem | null = null; - private uiRenderProvider: UIRenderDataProvider | null = null; - private uiInputSystem: UIInputSystem | null = null; - private initialized = false; - private modulesInitialized = false; - private running = false; - private animationFrameId: number | null = null; - private lastTime = 0; - private sceneSnapshot: string | null = null; - private assetManager: AssetManager | null = null; - private engineIntegration: EngineIntegration | null = null; - private sceneResourceManager: SceneResourceManager | null = null; - private assetPathResolver: AssetPathResolver | null = null; - private assetSystemInitialized = false; - private initializationError: Error | null = null; - private canvasId: string | null = null; + private _runtime: GameRuntime | null = null; + private _initialized = false; + private _modulesInitialized = false; + private _running = false; + private _canvasId: string | null = null; + + // 资产系统相关 + private _assetManager: AssetManager | null = null; + private _engineIntegration: EngineIntegration | null = null; + private _sceneResourceManager: SceneResourceManager | null = null; + private _assetPathResolver: AssetPathResolver | null = null; + private _assetSystemInitialized = false; + private _initializationError: Error | null = null; + + // 编辑器相机状态(用于恢复) + private _editorCameraState = { x: 0, y: 0, zoom: 1 }; private constructor() {} @@ -69,151 +72,110 @@ export class EngineService { /** * 等待引擎初始化完成 - * @param timeout 超时时间(毫秒),默认 10 秒 */ async waitForInitialization(timeout = 10000): Promise { - if (this.initialized) { + if (this._initialized) { return true; } const startTime = Date.now(); - while (!this.initialized && Date.now() - startTime < timeout) { + while (!this._initialized && Date.now() - startTime < timeout) { await new Promise(resolve => setTimeout(resolve, 100)); } - return this.initialized; + return this._initialized; } /** * Initialize the engine with canvas. * 使用canvas初始化引擎。 - * - * 注意:此方法只初始化引擎基础设施(Core、渲染系统等), - * 模块的初始化需要在项目打开后调用 initializeModuleSystems() */ async initialize(canvasId: string): Promise { - if (this.initialized) { + if (this._initialized) { return; } - this.canvasId = canvasId; + this._canvasId = canvasId; try { - // Create engine bridge | 创建引擎桥接 - this.bridge = new EngineBridge({ - canvasId - }); - - // Initialize WASM with pre-imported module | 使用预导入模块初始化WASM - await this.bridge.initializeWithModule(esEngine); - - // Set path resolver for Tauri asset URLs | 设置Tauri资产URL的路径解析器 - this.bridge.setPathResolver((path: string) => { - // If already a URL, return as-is + // 创建路径转换函数 + const pathTransformer = (path: string) => { if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('data:') || path.startsWith('asset://')) { return path; } - // Convert file path to Tauri asset URL return convertFileSrc(path); + }; + + // 创建编辑器平台适配器 + const platform = new EditorPlatformAdapter({ + wasmModule: esEngine, + pathTransformer, + gizmoDataProvider: (component, entity, isSelected) => + GizmoRegistry.getGizmoData(component, entity, isSelected), + hasGizmoProvider: (component) => + GizmoRegistry.hasProvider(component.constructor as any) }); - // Initialize Core if not already | 初始化Core(如果尚未初始化) - if (!Core.scene) { - Core.create({ debug: false }); - } + // 创建统一运行时 + // 编辑器模式下跳过内部插件加载,由 editor-core 的 PluginManager 管理 + this._runtime = createGameRuntime({ + platform, + canvasId, + autoStartRenderLoop: true, + uiCanvasSize: { width: 1920, height: 1080 }, + skipPluginLoading: true // 编辑器自己管理插件 + }); - // 使用现有 Core 场景或创建新的 - if (Core.scene) { - this.scene = Core.scene as Scene; - } else { - this.scene = new Scene({ name: 'EditorScene' }); - Core.setScene(this.scene); - } + await this._runtime.initialize(); - // Add camera system (基础系统,始终需要) - this.cameraSystem = new CameraSystem(this.bridge); - this.scene.addSystem(this.cameraSystem); + // 启用性能分析器(编辑器模式默认启用) + ProfilerSDK.setEnabled(true); - // Add render system to the scene (基础系统,始终需要) - this.renderSystem = new EngineRenderSystem(this.bridge, TransformComponent); - this.scene.addSystem(this.renderSystem); - - // Inject GizmoRegistry into render system - this.renderSystem.setGizmoRegistry( - ((component, entity, isSelected) => - GizmoRegistry.getGizmoData(component, entity, isSelected)) as GizmoDataProviderFn, - ((component) => - GizmoRegistry.hasProvider(component.constructor as any)) as HasGizmoProviderFn + // 设置 Gizmo 注册表 + this._runtime.setGizmoRegistry( + (component, entity, isSelected) => + GizmoRegistry.getGizmoData(component, entity, isSelected), + (component) => + GizmoRegistry.hasProvider(component.constructor as any) ); - // Set initial UI canvas size (will be updated from ProjectService when project opens) - // 设置初始 UI 画布尺寸(项目打开后会从 ProjectService 更新为项目配置的分辨率) - this.renderSystem.setUICanvasSize(1920, 1080); + // 初始化资产系统 + await this._initializeAssetSystem(); - // Initialize asset system | 初始化资产系统 - await this.initializeAssetSystem(); - - // Start the default world to enable system updates - // 启动默认world以启用系统更新 - const defaultWorld = Core.worldManager.getWorld('__default__'); - if (defaultWorld && !defaultWorld.isActive) { - defaultWorld.start(); - } - - this.initialized = true; - - // Sync viewport size immediately after initialization - // 初始化后立即同步视口尺寸 + // 同步视口尺寸 const canvas = document.getElementById(canvasId) as HTMLCanvasElement; if (canvas && canvas.parentElement) { - // Get container size in CSS pixels - // 获取容器尺寸(CSS像素) const rect = canvas.parentElement.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; - // Canvas internal size uses DPR for sharpness - // Canvas内部尺寸使用DPR以保持清晰 canvas.width = Math.floor(rect.width * dpr); canvas.height = Math.floor(rect.height * dpr); canvas.style.width = `${rect.width}px`; canvas.style.height = `${rect.height}px`; - // Camera uses actual canvas pixels for correct rendering - // 相机使用实际canvas像素以保证正确渲染 - this.bridge.resize(canvas.width, canvas.height); + this._runtime.resize(canvas.width, canvas.height); } - // Auto-start render loop for editor preview | 自动启动渲染循环用于编辑器预览 - this.startRenderLoop(); + this._initialized = true; } catch (error) { - console.error('Failed to initialize engine | 引擎初始化失败:', error); + console.error('Failed to initialize engine:', error); throw error; } } /** * 初始化模块系统 - * Initialize module systems for all enabled plugins - * - * 通过 PluginManager 初始化所有插件的运行时模块 - * Initialize all plugin runtime modules via PluginManager */ async initializeModuleSystems(): Promise { - if (!this.initialized) { + if (!this._initialized || !this._runtime) { console.error('Engine not initialized. Call initialize() first.'); return; } - if (!this.scene || !this.renderSystem || !this.bridge) { - console.error('Scene or render system not available.'); - return; - } - - // 如果之前已经初始化过模块,先清理 - if (this.modulesInitialized) { + if (this._modulesInitialized) { this.clearModuleSystems(); } @@ -224,201 +186,123 @@ export class EngineService { return; } - // 初始化所有插件的运行时模块(注册组件和服务) - // Initialize all plugin runtime modules (register components and services) + // 初始化所有插件的运行时模块 await pluginManager.initializeRuntime(Core.services); // 创建系统上下文 - // Create system context const context: SystemContext = { core: Core, - engineBridge: this.bridge, - renderSystem: this.renderSystem, - assetManager: this.assetManager, + engineBridge: this._runtime.bridge, + renderSystem: this._runtime.renderSystem, + assetManager: this._assetManager, isEditor: true }; // 让插件为场景创建系统 - // Let plugins create systems for scene - pluginManager.createSystemsForScene(this.scene, context); + pluginManager.createSystemsForScene(this._runtime.scene!, context); - // 保存插件创建的系统引用 - // Save system references created by plugins - this.animatorSystem = context.animatorSystem as SpriteAnimatorSystem | undefined ?? null; - this.tilemapSystem = context.tilemapSystem as TilemapRenderingSystem | undefined ?? null; - this.behaviorTreeSystem = context.behaviorTreeSystem as BehaviorTreeExecutionSystem | undefined ?? null; - this.physicsSystem = context.physicsSystem as Physics2DSystem | undefined ?? null; - this.uiRenderProvider = context.uiRenderProvider as UIRenderDataProvider | undefined ?? null; - this.uiInputSystem = context.uiInputSystem as UIInputSystem | undefined ?? null; + // 同步系统引用到 GameRuntime 的 systemContext(用于 start/stop 时启用/禁用系统) + this._runtime.updateSystemContext({ + animatorSystem: context.animatorSystem, + behaviorTreeSystem: context.behaviorTreeSystem, + physicsSystem: context.physicsSystem, + uiInputSystem: context.uiInputSystem, + uiRenderProvider: context.uiRenderProvider + }); - // 设置 UI 渲染数据提供者到 EngineRenderSystem - // Set UI render data provider to EngineRenderSystem - if (this.uiRenderProvider && this.renderSystem) { - this.renderSystem.setUIRenderDataProvider(this.uiRenderProvider); + // 设置 UI 渲染数据提供者 + if (context.uiRenderProvider && this._runtime.renderSystem) { + this._runtime.renderSystem.setUIRenderDataProvider(context.uiRenderProvider); } - // 在编辑器模式下,动画、行为树和物理系统默认禁用 - // In editor mode, animation, behavior tree and physics systems are disabled by default - if (this.animatorSystem) { - this.animatorSystem.enabled = false; + // 在编辑器模式下,禁用游戏逻辑系统 + if (context.animatorSystem) { + context.animatorSystem.enabled = false; } - if (this.behaviorTreeSystem) { - this.behaviorTreeSystem.enabled = false; + if (context.behaviorTreeSystem) { + context.behaviorTreeSystem.enabled = false; } - if (this.physicsSystem) { - this.physicsSystem.enabled = false; + if (context.physicsSystem) { + context.physicsSystem.enabled = false; } - this.modulesInitialized = true; + this._modulesInitialized = true; } /** * 清理模块系统 - * 用于项目关闭或切换时 - * Clear module systems, used when project closes or switches */ clearModuleSystems(): void { - // 通过 PluginManager 清理场景系统 - // Clear scene systems via PluginManager const pluginManager = Core.services.tryResolve(IPluginManager); if (pluginManager) { pluginManager.clearSceneSystems(); } - // Unbind UI input system before clearing - // 清理前解绑 UI 输入系统 - if (this.uiInputSystem) { - this.uiInputSystem.unbind(); + const ctx = this._runtime?.systemContext; + if (ctx?.uiInputSystem) { + ctx.uiInputSystem.unbind?.(); } - // 清空本地引用(系统的实际清理由场景管理) - // Clear local references (actual system cleanup is managed by scene) - this.animatorSystem = null; - this.tilemapSystem = null; - this.behaviorTreeSystem = null; - this.physicsSystem = null; - this.uiRenderProvider = null; - this.uiInputSystem = null; - this.modulesInitialized = false; + this._modulesInitialized = false; } /** * 检查模块系统是否已初始化 */ isModulesInitialized(): boolean { - return this.modulesInitialized; + return this._modulesInitialized; } - /** - * Start render loop (editor preview mode). - * 启动渲染循环(编辑器预览模式)。 - */ - private startRenderLoop(): void { - if (this.animationFrameId !== null) { - return; - } - this.lastTime = performance.now(); - this.renderLoop(); - } - - private frameCount = 0; - - /** - * Render loop for editor preview (always runs). - * 编辑器预览的渲染循环(始终运行)。 - */ - private renderLoop = (): void => { - const currentTime = performance.now(); - const deltaTime = (currentTime - this.lastTime) / 1000; - this.lastTime = currentTime; - - this.frameCount++; - - // Update via Core (handles deltaTime internally) | 通过Core更新 - Core.update(deltaTime); - - // Note: Rendering is handled by EngineRenderSystem.process() - // Texture loading is handled automatically via Rust engine's path-based loading - // 注意:渲染由 EngineRenderSystem.process() 处理 - // 纹理加载由Rust引擎的路径加载自动处理 - - this.animationFrameId = requestAnimationFrame(this.renderLoop); - }; - /** * Check if engine is initialized. - * 检查引擎是否已初始化。 */ isInitialized(): boolean { - return this.initialized; + return this._initialized; } /** * Check if engine is running. - * 检查引擎是否正在运行。 */ isRunning(): boolean { - return this.running; + return this._running; } /** - * Start the game loop. - * 启动游戏循环。 + * Start the game loop (preview mode). */ start(): void { - if (!this.initialized || this.running) { + if (!this._initialized || !this._runtime || this._running) { return; } - this.running = true; - this.lastTime = performance.now(); + this._running = true; + this._runtime.start(); - // Enable preview mode for UI rendering (screen space overlay) - // 启用预览模式用于 UI 渲染(屏幕空间叠加) - if (this.renderSystem) { - this.renderSystem.setPreviewMode(true); - } + // 启动自动播放动画 + this._startAutoPlayAnimations(); + } - // Bind UI input system to canvas for event handling - // 绑定 UI 输入系统到 canvas 以处理事件 - if (this.uiInputSystem && this.canvasId) { - const canvas = document.getElementById(this.canvasId) as HTMLCanvasElement; - if (canvas) { - this.uiInputSystem.bindToCanvas(canvas); - } - } + /** + * Stop the game loop. + */ + stop(): void { + if (!this._runtime) return; - // Enable animator system and start auto-play animations - // 启用动画系统并启动自动播放的动画 - if (this.animatorSystem) { - this.animatorSystem.enabled = true; - } - // Enable behavior tree system for preview - // 启用行为树系统用于预览 - if (this.behaviorTreeSystem) { - this.behaviorTreeSystem.enabled = true; - // 启动所有自动启动的行为树(因为在编辑器模式下 onAdded 不会处理) - // Start all auto-start behavior trees (since onAdded doesn't handle them in editor mode) - this.behaviorTreeSystem.startAllAutoStartTrees(); - } - // Enable physics system for preview - // 启用物理系统用于预览 - if (this.physicsSystem) { - this.physicsSystem.enabled = true; - } - this.startAutoPlayAnimations(); + this._running = false; + this._runtime.stop(); - this.gameLoop(); + // 停止所有动画 + this._stopAllAnimations(); } /** * Start all auto-play animations. - * 启动所有自动播放的动画。 */ - private startAutoPlayAnimations(): void { - if (!this.scene) return; + private _startAutoPlayAnimations(): void { + const scene = this._runtime?.scene; + if (!scene) return; - const entities = this.scene.entities.findEntitiesWithComponent(SpriteAnimatorComponent); + const entities = scene.entities.findEntitiesWithComponent(SpriteAnimatorComponent); for (const entity of entities) { const animator = entity.getComponent(SpriteAnimatorComponent); if (animator && animator.autoPlay && animator.defaultAnimation) { @@ -428,20 +312,18 @@ export class EngineService { } /** - * Stop all animations and reset to first frame. - * 停止所有动画并重置到第一帧。 + * Stop all animations. */ - private stopAllAnimations(): void { - if (!this.scene) return; + private _stopAllAnimations(): void { + const scene = this._runtime?.scene; + if (!scene) return; - const entities = this.scene.entities.findEntitiesWithComponent(SpriteAnimatorComponent); + const entities = scene.entities.findEntitiesWithComponent(SpriteAnimatorComponent); for (const entity of entities) { const animator = entity.getComponent(SpriteAnimatorComponent); if (animator) { animator.stop(); - // Reset sprite texture to first frame - // 重置精灵纹理到第一帧 const sprite = entity.getComponent(SpriteComponent); if (sprite && animator.clips && animator.clips.length > 0) { const firstClip = animator.clips[0]; @@ -457,74 +339,65 @@ export class EngineService { } /** - * Stop the game loop. - * 停止游戏循环。 + * Initialize asset system */ - stop(): void { - this.running = false; + private async _initializeAssetSystem(): Promise { + try { + this._assetManager = new AssetManager(); - // Disable preview mode for UI rendering (back to world space) - // 禁用预览模式用于 UI 渲染(返回世界空间) - if (this.renderSystem) { - this.renderSystem.setPreviewMode(false); - } + const pathTransformerFn = (path: string) => { + if (!path.startsWith('http://') && !path.startsWith('https://') && + !path.startsWith('data:') && !path.startsWith('asset://')) { + if (!path.startsWith('/') && !path.match(/^[a-zA-Z]:/)) { + const projectService = Core.services.tryResolve(ProjectService); + if (projectService && projectService.isProjectOpen()) { + const projectInfo = projectService.getCurrentProject(); + if (projectInfo) { + const projectPath = projectInfo.path; + const separator = projectPath.includes('\\') ? '\\' : '/'; + path = `${projectPath}${separator}${path.replace(/\//g, separator)}`; + } + } + } + return convertFileSrc(path); + } + return path; + }; - // Unbind UI input system from canvas - // 从 canvas 解绑 UI 输入系统 - if (this.uiInputSystem) { - this.uiInputSystem.unbind(); - } + this._assetPathResolver = new AssetPathResolver({ + platform: AssetPlatform.Editor, + pathTransformer: pathTransformerFn + }); - // Disable animator system and stop all animations - // 禁用动画系统并停止所有动画 - if (this.animatorSystem) { - this.animatorSystem.enabled = false; - } - // Disable behavior tree system - // 禁用行为树系统 - if (this.behaviorTreeSystem) { - this.behaviorTreeSystem.enabled = false; - } - // Disable and reset physics system - // 禁用并重置物理系统 - if (this.physicsSystem) { - this.physicsSystem.enabled = false; - // Reset physics world state to prepare for next preview - // 重置物理世界状态,为下次预览做准备 - this.physicsSystem.reset(); - } - this.stopAllAnimations(); + globalPathResolver.updateConfig({ + platform: AssetPlatform.Editor, + pathTransformer: pathTransformerFn + }); - // Note: Don't cancel animationFrameId here, as renderLoop should keep running - // for editor preview. The renderLoop will continue but gameLoop will stop - // because this.running is false. - // 注意:这里不要取消 animationFrameId,因为 renderLoop 应该继续运行 - // 用于编辑器预览。renderLoop 会继续运行,但 gameLoop 会停止 - // 因为 this.running 是 false。 + if (this._runtime?.bridge) { + this._engineIntegration = new EngineIntegration(this._assetManager, this._runtime.bridge); + + this._sceneResourceManager = new SceneResourceManager(); + this._sceneResourceManager.setResourceLoader(this._engineIntegration); + + const sceneManagerService = Core.services.tryResolve(SceneManagerService); + if (sceneManagerService) { + sceneManagerService.setSceneResourceManager(this._sceneResourceManager); + } + } + + this._assetSystemInitialized = true; + this._initializationError = null; + } catch (error) { + this._assetSystemInitialized = false; + this._initializationError = error instanceof Error ? error : new Error(String(error)); + console.error('Failed to initialize asset system:', error); + throw this._initializationError; + } } - /** - * Main game loop. - * 主游戏循环。 - */ - private gameLoop = (): void => { - if (!this.running) { - return; - } - - const currentTime = performance.now(); - const deltaTime = (currentTime - this.lastTime) / 1000; - this.lastTime = currentTime; - - // Update via Core | 通过Core更新 - Core.update(deltaTime); - - this.animationFrameId = requestAnimationFrame(this.gameLoop); - }; - /** * Create entity with sprite and transform. - * 创建带精灵和变换的实体。 */ createSpriteEntity(name: string, options?: { x?: number; @@ -533,13 +406,11 @@ export class EngineService { width?: number; height?: number; }): Entity | null { - if (!this.scene) { - return null; - } + const scene = this._runtime?.scene; + if (!scene) return null; - const entity = this.scene.createEntity(name); + const entity = scene.createEntity(name); - // Add transform | 添加变换组件 const transform = new TransformComponent(); if (options) { transform.position.x = options.x ?? 0; @@ -547,7 +418,6 @@ export class EngineService { } entity.addComponent(transform); - // Add sprite | 添加精灵组件 const sprite = new SpriteComponent(); if (options) { sprite.textureId = options.textureId ?? 0; @@ -559,409 +429,221 @@ export class EngineService { return entity; } - /** - * Initialize asset system - * 初始化资产系统 - */ - private async initializeAssetSystem(): Promise { - try { - // 创建资产管理器 / Create asset manager - this.assetManager = new AssetManager(); - - // 创建路径解析器 / Create path resolver - const pathTransformerFn = (path: string) => { - // 编辑器平台使用Tauri的convertFileSrc - // Use Tauri's convertFileSrc for editor platform - if (!path.startsWith('http://') && !path.startsWith('https://') && !path.startsWith('data:') && !path.startsWith('asset://')) { - // 如果是相对路径,需要先转换为绝对路径 - // If it's a relative path, convert to absolute path first - if (!path.startsWith('/') && !path.match(/^[a-zA-Z]:/)) { - const projectService = Core.services.tryResolve(ProjectService); - if (projectService && projectService.isProjectOpen()) { - const projectInfo = projectService.getCurrentProject(); - if (projectInfo) { - const projectPath = projectInfo.path; - // 规范化路径分隔符 / Normalize path separators - const separator = projectPath.includes('\\') ? '\\' : '/'; - path = `${projectPath}${separator}${path.replace(/\//g, separator)}`; - } - } - } - return convertFileSrc(path); - } - return path; - }; - - this.assetPathResolver = new AssetPathResolver({ - platform: AssetPlatform.Editor, - pathTransformer: pathTransformerFn - }); - - // 配置全局路径解析器,供组件使用 - // Configure global path resolver for components to use - globalPathResolver.updateConfig({ - platform: AssetPlatform.Editor, - pathTransformer: pathTransformerFn - }); - - // 创建引擎集成 / Create engine integration - if (this.bridge) { - this.engineIntegration = new EngineIntegration(this.assetManager, this.bridge); - - // 创建场景资源管理器 / Create scene resource manager - this.sceneResourceManager = new SceneResourceManager(); - this.sceneResourceManager.setResourceLoader(this.engineIntegration); - - // 将 SceneResourceManager 设置到 SceneManagerService - // Set SceneResourceManager to SceneManagerService - const sceneManagerService = Core.services.tryResolve(SceneManagerService); - if (sceneManagerService) { - sceneManagerService.setSceneResourceManager(this.sceneResourceManager); - } - } - - this.assetSystemInitialized = true; - this.initializationError = null; - } catch (error) { - this.assetSystemInitialized = false; - this.initializationError = error instanceof Error ? error : new Error(String(error)); - console.error('Failed to initialize asset system:', error); - - // Notify user of failure - const messageHub = Core.services.tryResolve(MessageHub); - if (messageHub) { - messageHub.publish('notification:error', { - title: 'Asset System Error', - message: 'Failed to initialize asset system. Some features may not work properly.' - }); - } - - throw this.initializationError; - } - } - /** * Load texture. - * 加载纹理。 */ loadTexture(id: number, url: string): void { - if (this.renderSystem) { - this.renderSystem.loadTexture(id, url); - } + this._runtime?.renderSystem?.loadTexture(id, url); } /** * Load texture through asset system - * 通过资产系统加载纹理 */ async loadTextureAsset(path: string): Promise { - // Check if asset system is properly initialized - if (!this.assetSystemInitialized || this.initializationError) { + if (!this._assetSystemInitialized || this._initializationError) { console.warn('Asset system not initialized, using fallback texture loading'); const textureId = IdGenerator.nextId('texture-fallback'); this.loadTexture(textureId, path); return textureId; } - if (!this.engineIntegration) { - // 回退到直接加载 / Fallback to direct loading + if (!this._engineIntegration) { const textureId = IdGenerator.nextId('texture'); this.loadTexture(textureId, path); return textureId; } try { - return await this.engineIntegration.loadTextureForComponent(path); + return await this._engineIntegration.loadTextureForComponent(path); } catch (error) { console.error('Failed to load texture asset:', error); - // Return a valid fallback ID instead of 0 const fallbackId = IdGenerator.nextId('texture-fallback'); - - // Notify about texture loading failure - const messageHub = Core.services.tryResolve(MessageHub); - if (messageHub) { - messageHub.publish('notification:warning', { - title: 'Texture Loading Failed', - message: `Could not load texture: ${path}` - }); - } - return fallbackId; } } /** * Get asset manager - * 获取资产管理器 */ getAssetManager(): AssetManager | null { - return this.assetManager; + return this._assetManager; } /** * Get engine integration - * 获取引擎集成 */ getEngineIntegration(): EngineIntegration | null { - return this.engineIntegration; + return this._engineIntegration; } /** * Get asset path resolver - * 获取资产路径解析器 */ getAssetPathResolver(): AssetPathResolver | null { - return this.assetPathResolver; + return this._assetPathResolver; } /** * Get engine statistics. - * 获取引擎统计信息。 */ getStats(): { fps: number; drawCalls: number; spriteCount: number } { - if (!this.renderSystem) { - return { fps: 0, drawCalls: 0, spriteCount: 0 }; - } - - const engineStats = this.renderSystem.getStats(); - return { - fps: engineStats?.fps ?? 0, - drawCalls: engineStats?.drawCalls ?? 0, - spriteCount: this.renderSystem.spriteCount - }; + return this._runtime?.getStats() ?? { fps: 0, drawCalls: 0, spriteCount: 0 }; } /** * Get the ECS scene. - * 获取ECS场景。 */ getScene(): Scene | null { - return this.scene; + return this._runtime?.scene ?? null; } /** * Enable animation preview in editor mode. - * 在编辑器模式下启用动画预览。 */ enableAnimationPreview(): void { - if (this.animatorSystem && !this.running) { - // Clear entity cache to force re-query when enabled - // 清除实体缓存以便启用时强制重新查询 - this.animatorSystem.clearEntityCache(); - this.animatorSystem.enabled = true; + const ctx = this._runtime?.systemContext; + if (ctx?.animatorSystem && !this._running) { + ctx.animatorSystem.clearEntityCache?.(); + ctx.animatorSystem.enabled = true; } } /** * Disable animation preview in editor mode. - * 在编辑器模式下禁用动画预览。 */ disableAnimationPreview(): void { - if (this.animatorSystem && !this.running) { - this.animatorSystem.enabled = false; + const ctx = this._runtime?.systemContext; + if (ctx?.animatorSystem && !this._running) { + ctx.animatorSystem.enabled = false; } } /** * Check if animation preview is enabled. - * 检查动画预览是否启用。 */ isAnimationPreviewEnabled(): boolean { - return this.animatorSystem?.enabled ?? false; + return this._runtime?.systemContext?.animatorSystem?.enabled ?? false; } /** * Get the engine bridge. - * 获取引擎桥接。 */ - getBridge(): EngineBridge | null { - return this.bridge; + getBridge() { + return this._runtime?.bridge ?? null; } /** * Resize the engine viewport. - * 调整引擎视口大小。 */ resize(width: number, height: number): void { - if (this.bridge) { - this.bridge.resize(width, height); - } + this._runtime?.resize(width, height); } /** * Set camera position, zoom, and rotation. - * 设置相机位置、缩放和旋转。 */ setCamera(config: CameraConfig): void { - if (this.bridge) { - this.bridge.setCamera(config); - } + this._runtime?.setCamera(config); } /** * Get camera state. - * 获取相机状态。 */ getCamera(): CameraConfig { - if (this.bridge) { - return this.bridge.getCamera(); - } - return { x: 0, y: 0, zoom: 1, rotation: 0 }; + return this._runtime?.getCamera() ?? { x: 0, y: 0, zoom: 1, rotation: 0 }; } /** * Set grid visibility. - * 设置网格可见性。 */ setShowGrid(show: boolean): void { - if (this.bridge) { - this.bridge.setShowGrid(show); - } + this._runtime?.setShowGrid(show); } /** * Set clear color (background color). - * 设置清除颜色(背景颜色)。 */ setClearColor(r: number, g: number, b: number, a: number = 1.0): void { - if (this.bridge) { - this.bridge.setClearColor(r, g, b, a); - } + this._runtime?.setClearColor(r, g, b, a); } /** * Set gizmo visibility. - * 设置Gizmo可见性。 */ setShowGizmos(show: boolean): void { - if (this.renderSystem) { - this.renderSystem.setShowGizmos(show); - } + this._runtime?.setShowGizmos(show); } /** * Get gizmo visibility. - * 获取Gizmo可见性。 */ getShowGizmos(): boolean { - return this.renderSystem?.getShowGizmos() ?? true; + return this._runtime?.renderSystem?.getShowGizmos() ?? true; } /** * Set UI canvas size for boundary display. - * 设置 UI 画布尺寸以显示边界。 */ setUICanvasSize(width: number, height: number): void { - if (this.renderSystem) { - this.renderSystem.setUICanvasSize(width, height); - } + this._runtime?.setUICanvasSize(width, height); } /** * Get UI canvas size. - * 获取 UI 画布尺寸。 */ getUICanvasSize(): { width: number; height: number } { - return this.renderSystem?.getUICanvasSize() ?? { width: 0, height: 0 }; + return this._runtime?.getUICanvasSize() ?? { width: 0, height: 0 }; } /** * Set UI canvas boundary visibility. - * 设置 UI 画布边界可见性。 */ setShowUICanvasBoundary(show: boolean): void { - if (this.renderSystem) { - this.renderSystem.setShowUICanvasBoundary(show); - } + this._runtime?.setShowUICanvasBoundary(show); } /** * Get UI canvas boundary visibility. - * 获取 UI 画布边界可见性。 */ getShowUICanvasBoundary(): boolean { - return this.renderSystem?.getShowUICanvasBoundary() ?? true; + return this._runtime?.getShowUICanvasBoundary() ?? true; } // ===== Scene Snapshot API ===== - // ===== 场景快照 API ===== /** * Save a snapshot of the current scene state. - * 保存当前场景状态的快照。 */ saveSceneSnapshot(): boolean { - if (!this.scene) { - console.warn('Cannot save snapshot: no scene available'); - return false; - } - - try { - // Use SceneSerializer from core library - this.sceneSnapshot = SceneSerializer.serialize(this.scene, { - format: 'json', - pretty: false, - includeMetadata: false - }) as string; - - return true; - } catch (error) { - console.error('Failed to save scene snapshot:', error); - return false; - } + return this._runtime?.saveSceneSnapshot() ?? false; } /** * Restore scene state from saved snapshot. - * 从保存的快照恢复场景状态。 */ async restoreSceneSnapshot(): Promise { - if (!this.scene || !this.sceneSnapshot) { - console.warn('Cannot restore snapshot: no scene or snapshot available'); - return false; - } + if (!this._runtime) return false; - try { - // Clear tilemap rendering cache before restoring - // 恢复前清除瓦片地图渲染缓存 - if (this.tilemapSystem) { - this.tilemapSystem.clearCache(); - } + const success = await this._runtime.restoreSceneSnapshot(); - // Clear UI render caches before restoring - // 恢复前清除 UI 渲染缓存 + if (success) { + // 清除 UI 渲染缓存 invalidateUIRenderCaches(); - // Use SceneSerializer from core library - SceneSerializer.deserialize(this.scene, this.sceneSnapshot, { - strategy: 'replace', - preserveIds: true - }); - - // 加载场景资源 / Load scene resources - if (this.sceneResourceManager) { - await this.sceneResourceManager.loadSceneResources(this.scene); - } else { - console.warn('[EngineService] SceneResourceManager not available, skipping resource loading'); + // 加载场景资源 + if (this._sceneResourceManager && this._runtime.scene) { + await this._sceneResourceManager.loadSceneResources(this._runtime.scene); } - // Sync EntityStore with restored scene entities + // 同步 EntityStore const entityStore = Core.services.tryResolve(EntityStoreService); const messageHub = Core.services.tryResolve(MessageHub); if (entityStore && messageHub) { - // Remember selected entity ID before clearing const selectedEntity = entityStore.getSelectedEntity(); const selectedId = selectedEntity?.id; - // Clear old entities from store - entityStore.clear(); + entityStore.syncFromScene(); - // Add restored entities to store - for (const entity of this.scene.entities.buffer) { - entityStore.addEntity(entity); - } - - // Re-select the same entity (now with new reference) if (selectedId !== undefined) { const newEntity = entityStore.getEntity(selectedId); if (newEntity) { @@ -969,181 +651,133 @@ export class EngineService { } } - // Notify UI to refresh messageHub.publish('scene:restored', {}); } - - this.sceneSnapshot = null; - return true; - } catch (error) { - console.error('Failed to restore scene snapshot:', error); - return false; } + + return success; } /** * Check if a snapshot exists. - * 检查是否存在快照。 */ hasSnapshot(): boolean { - return this.sceneSnapshot !== null; + return this._runtime?.hasSnapshot() ?? false; } /** * Set selected entity IDs for gizmo display. - * 设置选中的实体ID用于Gizmo显示。 */ setSelectedEntityIds(ids: number[]): void { - if (this.renderSystem) { - this.renderSystem.setSelectedEntityIds(ids); - } + this._runtime?.setSelectedEntityIds(ids); } /** * Set transform tool mode. - * 设置变换工具模式。 */ setTransformMode(mode: 'select' | 'move' | 'rotate' | 'scale'): void { - if (this.renderSystem) { - this.renderSystem.setTransformMode(mode); - } + this._runtime?.setTransformMode(mode); } /** * Get transform tool mode. - * 获取变换工具模式。 */ getTransformMode(): 'select' | 'move' | 'rotate' | 'scale' { - return this.renderSystem?.getTransformMode() ?? 'select'; + return this._runtime?.getTransformMode() ?? 'select'; } // ===== Multi-viewport API ===== - // ===== 多视口 API ===== /** * Register a new viewport. - * 注册新视口。 */ registerViewport(id: string, canvasId: string): void { - if (this.bridge) { - this.bridge.registerViewport(id, canvasId); - } + this._runtime?.registerViewport(id, canvasId); } /** * Unregister a viewport. - * 注销视口。 */ unregisterViewport(id: string): void { - if (this.bridge) { - this.bridge.unregisterViewport(id); - } + this._runtime?.unregisterViewport(id); } /** * Set the active viewport. - * 设置活动视口。 */ setActiveViewport(id: string): boolean { - if (this.bridge) { - return this.bridge.setActiveViewport(id); - } - return false; + return this._runtime?.setActiveViewport(id) ?? false; } /** * Set camera for a specific viewport. - * 为特定视口设置相机。 */ setViewportCamera(viewportId: string, config: CameraConfig): void { - if (this.bridge) { - this.bridge.setViewportCamera(viewportId, config); - } + this._runtime?.bridge?.setViewportCamera(viewportId, config); } /** * Get camera for a specific viewport. - * 获取特定视口的相机。 */ getViewportCamera(viewportId: string): CameraConfig | null { - if (this.bridge) { - return this.bridge.getViewportCamera(viewportId); - } - return null; + return this._runtime?.bridge?.getViewportCamera(viewportId) ?? null; } /** * Set viewport configuration. - * 设置视口配置。 */ setViewportConfig(viewportId: string, showGrid: boolean, showGizmos: boolean): void { - if (this.bridge) { - this.bridge.setViewportConfig(viewportId, showGrid, showGizmos); - } + this._runtime?.bridge?.setViewportConfig(viewportId, showGrid, showGizmos); } /** * Resize a specific viewport. - * 调整特定视口大小。 */ resizeViewport(viewportId: string, width: number, height: number): void { - if (this.bridge) { - this.bridge.resizeViewport(viewportId, width, height); - } + this._runtime?.bridge?.resizeViewport(viewportId, width, height); } /** * Render to a specific viewport. - * 渲染到特定视口。 */ renderToViewport(viewportId: string): void { - if (this.bridge) { - this.bridge.renderToViewport(viewportId); - } + this._runtime?.bridge?.renderToViewport(viewportId); } /** * Get all registered viewport IDs. - * 获取所有已注册的视口ID。 */ getViewportIds(): string[] { - if (this.bridge) { - return this.bridge.getViewportIds(); - } - return []; + return this._runtime?.bridge?.getViewportIds() ?? []; + } + + /** + * Get the underlying GameRuntime instance. + * 获取底层 GameRuntime 实例。 + */ + getRuntime(): GameRuntime | null { + return this._runtime; } /** * Dispose engine resources. - * 释放引擎资源。 */ dispose(): void { this.stop(); - // Stop render loop | 停止渲染循环 - if (this.animationFrameId !== null) { - cancelAnimationFrame(this.animationFrameId); - this.animationFrameId = null; + if (this._assetManager) { + this._assetManager.dispose(); + this._assetManager = null; } - // Dispose asset system | 释放资产系统 - if (this.assetManager) { - this.assetManager.dispose(); - this.assetManager = null; - } - this.engineIntegration = null; + this._engineIntegration = null; - // Scene doesn't have a destroy method, just clear reference - // 场景没有destroy方法,只需清除引用 - this.scene = null; - - if (this.bridge) { - this.bridge.dispose(); - this.bridge = null; + if (this._runtime) { + this._runtime.dispose(); + this._runtime = null; } - this.renderSystem = null; - this.initialized = false; + this._initialized = false; } } diff --git a/packages/editor-app/src/services/PluginSDKRegistry.ts b/packages/editor-app/src/services/PluginSDKRegistry.ts index 79c600be..f92393b3 100644 --- a/packages/editor-app/src/services/PluginSDKRegistry.ts +++ b/packages/editor-app/src/services/PluginSDKRegistry.ts @@ -18,7 +18,10 @@ import { EntityStoreService, MessageHub } from '@esengine/editor-core'; import * as editorRuntime from '@esengine/editor-runtime'; import * as ecsFramework from '@esengine/ecs-framework'; import * as behaviorTree from '@esengine/behavior-tree'; -import * as ecsComponents from '@esengine/ecs-components'; +import * as engineCore from '@esengine/engine-core'; +import * as sprite from '@esengine/sprite'; +import * as camera from '@esengine/camera'; +import * as audio from '@esengine/audio'; // 存储服务实例引用(在初始化时设置) let entityStoreInstance: EntityStoreService | null = null; @@ -29,7 +32,10 @@ const SDK_MODULES = { '@esengine/editor-runtime': editorRuntime, '@esengine/ecs-framework': ecsFramework, '@esengine/behavior-tree': behaviorTree, - '@esengine/ecs-components': ecsComponents, + '@esengine/engine-core': engineCore, + '@esengine/sprite': sprite, + '@esengine/camera': camera, + '@esengine/audio': audio, } as const; // 全局变量名称映射(用于插件构建配置) @@ -37,7 +43,10 @@ export const SDK_GLOBALS = { '@esengine/editor-runtime': '__ESENGINE__.editorRuntime', '@esengine/ecs-framework': '__ESENGINE__.ecsFramework', '@esengine/behavior-tree': '__ESENGINE__.behaviorTree', - '@esengine/ecs-components': '__ESENGINE__.ecsComponents', + '@esengine/engine-core': '__ESENGINE__.engineCore', + '@esengine/sprite': '__ESENGINE__.sprite', + '@esengine/camera': '__ESENGINE__.camera', + '@esengine/audio': '__ESENGINE__.audio', } as const; /** @@ -62,7 +71,10 @@ interface ESEngineGlobal { editorRuntime: typeof editorRuntime; ecsFramework: typeof ecsFramework; behaviorTree: typeof behaviorTree; - ecsComponents: typeof ecsComponents; + engineCore: typeof engineCore; + sprite: typeof sprite; + camera: typeof camera; + audio: typeof audio; require: (moduleName: string) => any; api: IPluginAPI; } @@ -117,7 +129,10 @@ export class PluginSDKRegistry { editorRuntime, ecsFramework, behaviorTree, - ecsComponents, + engineCore, + sprite, + camera, + audio, require: this.requireModule.bind(this), api: pluginAPI, }; diff --git a/packages/editor-app/src/services/ProfilerService.ts b/packages/editor-app/src/services/ProfilerService.ts index 0a4519fc..0d47d73f 100644 --- a/packages/editor-app/src/services/ProfilerService.ts +++ b/packages/editor-app/src/services/ProfilerService.ts @@ -70,7 +70,7 @@ type AdvancedProfilerDataListener = (data: AdvancedProfilerDataPayload) => void; export class ProfilerService { private ws: WebSocket | null = null; private isServerRunning = false; - private wsPort: string; + private wsPort: number; private listeners: Set = new Set(); private advancedListeners: Set = new Set(); private currentData: ProfilerData | null = null; @@ -82,7 +82,7 @@ export class ProfilerService { constructor() { const settings = SettingsService.getInstance(); - this.wsPort = settings.get('profiler.port', '8080'); + this.wsPort = settings.get('profiler.port', 8080); this.autoStart = settings.get('profiler.autoStart', true); this.startServerCheck(); @@ -97,8 +97,9 @@ export class ProfilerService { private listenToSettingsChanges(): void { window.addEventListener('settings:changed', ((event: CustomEvent) => { const newPort = event.detail['profiler.port']; - if (newPort && newPort !== this.wsPort) { - this.wsPort = newPort; + if (newPort !== undefined && Number(newPort) !== this.wsPort) { + console.log(`[ProfilerService] Port changed from ${this.wsPort} to ${newPort}`); + this.wsPort = Number(newPort); this.reconnectWithNewPort(); } }) as EventListener); @@ -247,8 +248,8 @@ export class ProfilerService { private async startServer(): Promise { try { - const port = parseInt(this.wsPort); - await invoke('start_profiler_server', { port }); + console.log(`[ProfilerService] Starting server on port ${this.wsPort}`); + await invoke('start_profiler_server', { port: this.wsPort }); this.isServerRunning = true; } catch (error) { // Ignore "already running" error - it's expected in some cases @@ -300,7 +301,7 @@ export class ProfilerService { try { const message = JSON.parse(event.data); if (message.type === 'debug_data' && message.data) { - this.handleDebugData(message.data); + this.handleDebugData(message.data, message.advancedProfiler); } else if (message.type === 'get_raw_entity_list_response' && message.data) { this.handleRawEntityListResponse(message.data); } else if (message.type === 'get_entity_details_response' && message.data) { @@ -338,7 +339,7 @@ export class ProfilerService { } } - private handleDebugData(debugData: any): void { + private handleDebugData(debugData: any, advancedProfiler?: any): void { const performance = debugData.performance; if (!performance) return; @@ -380,18 +381,25 @@ export class ProfilerService { this.notifyListeners(this.currentData); - // 通知高级监听器原始数据 - this.lastRawData = { - performance: debugData.performance, - systems: { - systemsInfo: systems.map(sys => ({ - name: sys.name, - executionTime: sys.executionTime, - entityCount: sys.entityCount, - averageTime: sys.averageTime - })) - } - }; + // 如果有高级性能数据,优先使用它 + if (advancedProfiler) { + this.lastRawData = { + advancedProfiler + }; + } else { + // 否则使用传统数据 + this.lastRawData = { + performance: debugData.performance, + systems: { + systemsInfo: systems.map(sys => ({ + name: sys.name, + executionTime: sys.executionTime, + entityCount: sys.entityCount, + averageTime: sys.averageTime + })) + } + }; + } this.notifyAdvancedListeners(this.lastRawData); // 请求完整的实体列表 diff --git a/packages/editor-app/src/styles/AdvancedProfiler.css b/packages/editor-app/src/styles/AdvancedProfiler.css index 6bdb6046..47d74eb8 100644 --- a/packages/editor-app/src/styles/AdvancedProfiler.css +++ b/packages/editor-app/src/styles/AdvancedProfiler.css @@ -586,9 +586,44 @@ .profiler-table-cell.name .expand-icon { color: #666; flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; +} + +.profiler-table-cell.name .expand-icon.clickable { cursor: pointer; } +.profiler-table-cell.name .expand-icon.clickable:hover { + color: #fff; + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; +} + +.profiler-table-cell.name .expand-icon.placeholder { + visibility: hidden; +} + +/* 层级行的背景色变化 */ +.profiler-table-row.depth-1 { + background: rgba(255, 255, 255, 0.02); +} + +.profiler-table-row.depth-2 { + background: rgba(255, 255, 255, 0.04); +} + +.profiler-table-row.depth-3 { + background: rgba(255, 255, 255, 0.06); +} + +.profiler-table-row.depth-4 { + background: rgba(255, 255, 255, 0.08); +} + .profiler-table-cell.name .category-dot { width: 6px; height: 6px; diff --git a/packages/editor-app/src/styles/ConsolePanel.css b/packages/editor-app/src/styles/ConsolePanel.css deleted file mode 100644 index 463a111b..00000000 --- a/packages/editor-app/src/styles/ConsolePanel.css +++ /dev/null @@ -1,445 +0,0 @@ -.console-panel { - display: flex; - flex-direction: column; - height: 100%; - background: var(--color-bg-base); - position: relative; -} - -.console-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 8px; - background: var(--color-bg-elevated); - border-bottom: 1px solid var(--color-border-default); - flex-shrink: 0; - gap: 8px; - height: 26px; -} - -.console-toolbar-left { - display: flex; - align-items: center; - gap: 8px; - flex: 1; -} - -.console-toolbar-right { - display: flex; - align-items: center; - gap: 4px; -} - -.console-btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 6px; - background: transparent; - border: none; - border-radius: var(--radius-sm); - color: var(--color-text-secondary); - cursor: pointer; - transition: all var(--transition-fast); -} - -.console-btn:hover { - background: var(--color-bg-hover); - color: var(--color-text-primary); -} - -.console-search { - display: flex; - align-items: center; - gap: 6px; - flex: 1; - max-width: 300px; - background: var(--color-bg-inset); - border: 1px solid var(--color-border-default); - border-radius: var(--radius-sm); - padding: 4px 8px; - color: var(--color-text-tertiary); -} - -.console-search:focus-within { - border-color: var(--color-primary); - color: var(--color-text-primary); -} - -.console-search input { - flex: 1; - background: transparent; - border: none; - outline: none; - color: var(--color-text-primary); - font-size: var(--font-size-xs); - font-family: var(--font-family-mono); -} - -.console-search input::placeholder { - color: var(--color-text-tertiary); -} - -.console-filter-btn { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 8px; - background: transparent; - border: 1px solid transparent; - border-radius: var(--radius-sm); - cursor: pointer; - font-size: var(--font-size-xs); - font-weight: 500; - transition: all var(--transition-fast); - opacity: 0.5; -} - -.console-filter-btn:hover { - opacity: 1; - background: var(--color-bg-hover); -} - -.console-filter-btn.active { - opacity: 1; - border-color: currentColor; - background: rgba(255, 255, 255, 0.05); -} - -.console-filter-btn:nth-child(1) { - color: #10b981; -} - -.console-filter-btn:nth-child(1).active { - color: #34d399; - border-color: #10b981; -} - -.console-filter-btn:nth-child(2) { - color: #858585; -} - -.console-filter-btn:nth-child(2).active { - color: #a0a0a0; - border-color: #858585; -} - -.console-filter-btn:nth-child(3) { - color: #4a9eff; -} - -.console-filter-btn:nth-child(3).active { - color: #6eb3ff; - border-color: #4a9eff; -} - -.console-filter-btn:nth-child(4) { - color: #ffc107; -} - -.console-filter-btn:nth-child(4).active { - color: #ffd54f; - border-color: #ffc107; -} - -.console-filter-btn:nth-child(5) { - color: #f44336; -} - -.console-filter-btn:nth-child(5).active { - color: #ef5350; - border-color: #f44336; -} - -.console-filter-btn span { - font-size: var(--font-size-xs); - font-family: var(--font-family-mono); -} - -.console-content { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - font-family: var(--font-family-mono); - font-size: var(--font-size-xs); - line-height: 1.4; -} - -.console-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - color: var(--color-text-tertiary); - gap: 8px; -} - -.console-empty p { - margin: 0; - font-size: var(--font-size-sm); -} - -.log-entry { - display: flex; - align-items: flex-start; - gap: 8px; - padding: 4px 12px; - border-bottom: 1px solid var(--color-border-subtle); - transition: background-color var(--transition-fast); -} - -.log-entry:hover { - background: rgba(255, 255, 255, 0.02); -} - -.log-entry-icon { - display: flex; - align-items: center; - padding-top: 2px; - flex-shrink: 0; -} - -.log-entry-time { - color: var(--color-text-tertiary); - font-size: var(--font-size-xs); - white-space: nowrap; - padding-top: 2px; - flex-shrink: 0; - font-variant-numeric: tabular-nums; -} - -.log-entry-source { - color: var(--color-text-secondary); - font-size: var(--font-size-xs); - white-space: nowrap; - padding-top: 2px; - flex-shrink: 0; - opacity: 0.7; -} - -.log-entry-source.source-remote { - color: #4a9eff; - opacity: 1; - font-weight: 600; -} - -.log-entry-client { - color: #10b981; - font-size: calc(var(--font-size-xs) - 2px); - white-space: nowrap; - padding: 1px 6px; - flex-shrink: 0; - background: rgba(16, 185, 129, 0.15); - border: 1px solid rgba(16, 185, 129, 0.4); - border-radius: var(--radius-sm); - font-weight: 600; - font-family: var(--font-family-mono); -} - -.log-entry-remote { - border-left: 2px solid #4a9eff; - background: rgba(74, 158, 255, 0.05); -} - -.log-entry-expander { - display: flex; - align-items: center; - padding-top: 2px; - cursor: pointer; - color: var(--color-text-secondary); - flex-shrink: 0; - transition: color var(--transition-fast); -} - -.log-entry-expander:hover { - color: var(--color-text-primary); -} - -.log-entry-expanded { - flex-direction: column; - align-items: flex-start; -} - -.log-entry-expanded .log-entry-message { - padding-left: 22px; - width: 100%; -} - -.log-entry-message { - flex: 1; - color: var(--color-text-primary); - word-break: break-word; - padding-top: 2px; -} - -.log-message-container { - display: flex; - align-items: flex-start; - gap: 8px; - width: 100%; -} - -.log-message-preview { - opacity: 0.9; - flex: 1; -} - -.log-open-json-btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 4px; - background: var(--color-primary); - border: none; - border-radius: var(--radius-sm); - color: white; - cursor: pointer; - opacity: 0.7; - transition: all var(--transition-fast); - flex-shrink: 0; -} - -.log-open-json-btn:hover { - opacity: 1; - transform: scale(1.1); -} - -.log-expand-btn { - display: inline-flex; - align-items: center; - justify-content: center; - margin-left: 8px; - padding: 2px 6px; - background: var(--color-bg-elevated); - border: 1px solid var(--color-border-default); - border-radius: var(--radius-sm); - color: var(--color-primary); - cursor: pointer; - font-size: var(--font-size-xs); - font-family: var(--font-family-mono); - transition: all var(--transition-fast); -} - -.log-expand-btn:hover { - background: var(--color-bg-hover); - border-color: var(--color-primary); -} - -.log-message-json { - margin: 4px 0 0 0; - padding: 8px; - background: rgba(0, 0, 0, 0.3); - border-radius: var(--radius-sm); - border: 1px solid var(--color-border-default); - font-family: var(--font-family-mono); - font-size: var(--font-size-xs); - line-height: 1.5; - overflow: auto; - white-space: pre; - color: #a0e7a0; - max-height: 400px; -} - -.log-message-json::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -.log-message-json::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.2); - border-radius: 4px; -} - -.log-message-json::-webkit-scrollbar-thumb { - background: rgba(160, 231, 160, 0.3); - border-radius: 4px; -} - -.log-message-json::-webkit-scrollbar-thumb:hover { - background: rgba(160, 231, 160, 0.5); -} - -.log-entry-debug { - color: var(--color-text-tertiary); -} - -.log-entry-debug .log-entry-icon { - color: #858585; -} - -.log-entry-info .log-entry-icon { - color: #4a9eff; -} - -.log-entry-warn { - background: rgba(255, 193, 7, 0.05); -} - -.log-entry-warn .log-entry-icon { - color: #ffc107; -} - -.log-entry-error { - background: rgba(244, 67, 54, 0.05); -} - -.log-entry-error .log-entry-icon { - color: #f44336; -} - -.console-scroll-to-bottom { - position: absolute; - bottom: 12px; - left: 50%; - transform: translateX(-50%); - padding: 6px 12px; - background: var(--color-primary); - color: var(--color-text-inverse); - border: none; - border-radius: var(--radius-sm); - font-size: var(--font-size-xs); - font-weight: 500; - cursor: pointer; - box-shadow: var(--shadow-md); - transition: all var(--transition-fast); - z-index: var(--z-index-above); -} - -.console-scroll-to-bottom:hover { - background: var(--color-primary-hover); - transform: translateX(-50%) translateY(-2px); - box-shadow: var(--shadow-lg); -} - -.console-scroll-to-bottom:active { - transform: translateX(-50%) translateY(0); -} - -/* Scrollbar */ -.console-content::-webkit-scrollbar { - width: 10px; -} - -.console-content::-webkit-scrollbar-track { - background: var(--color-bg-elevated); -} - -.console-content::-webkit-scrollbar-thumb { - background: var(--color-border-default); - border-radius: 5px; -} - -.console-content::-webkit-scrollbar-thumb:hover { - background: var(--color-text-secondary); -} - -@media (prefers-reduced-motion: reduce) { - .console-btn, - .console-filter-btn, - .log-entry, - .console-scroll-to-bottom { - transition: none; - } -} diff --git a/packages/editor-app/src/styles/ContextMenu.css b/packages/editor-app/src/styles/ContextMenu.css index 2e933bc5..3ec496aa 100644 --- a/packages/editor-app/src/styles/ContextMenu.css +++ b/packages/editor-app/src/styles/ContextMenu.css @@ -5,24 +5,30 @@ border-radius: 4px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); padding: 4px 0; - min-width: 180px; + min-width: 200px; z-index: var(--z-index-popover); font-size: 13px; } +.context-menu.submenu { + position: fixed; +} + .context-menu-item { display: flex; align-items: center; gap: 8px; padding: 6px 12px; + padding-right: 24px; color: #cccccc; cursor: pointer; transition: background-color 0.1s ease; user-select: none; + position: relative; } .context-menu-item:hover:not(.disabled) { - background-color: #383838; + background-color: #094771; color: #ffffff; } @@ -32,6 +38,10 @@ opacity: 0.5; } +.context-menu-item.has-submenu { + padding-right: 28px; +} + .context-menu-icon { display: flex; align-items: center; @@ -51,8 +61,41 @@ white-space: nowrap; } +.context-menu-shortcut { + color: #888888; + font-size: 11px; + margin-left: 24px; + white-space: nowrap; +} + +.context-menu-item:hover:not(.disabled) .context-menu-shortcut { + color: #aaaaaa; +} + +.context-menu-arrow { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + color: #888888; +} + +.context-menu-item:hover:not(.disabled) .context-menu-arrow { + color: #ffffff; +} + .context-menu-separator { height: 1px; background-color: #3e3e42; margin: 4px 0; } + +/* Section header in submenu */ +.context-menu-section-header { + padding: 4px 12px; + color: #888888; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: default; +} diff --git a/packages/editor-app/src/styles/OutputLogPanel.css b/packages/editor-app/src/styles/OutputLogPanel.css index 2bc12939..ead429e1 100644 --- a/packages/editor-app/src/styles/OutputLogPanel.css +++ b/packages/editor-app/src/styles/OutputLogPanel.css @@ -318,15 +318,6 @@ background: #5a5a5a; } -/* Hide timestamp/source based on settings */ -.output-log-content.hide-timestamp .output-log-entry-time { - display: none; -} - -.output-log-content.hide-source .output-log-entry-source { - display: none; -} - /* Empty State */ .output-log-empty { display: flex; @@ -345,12 +336,7 @@ /* ==================== Log Entry ==================== */ .output-log-entry { - display: flex; - align-items: flex-start; - gap: 8px; - padding: 4px 12px; border-bottom: 1px solid #2a2a2a; - line-height: 1.4; transition: background 0.1s ease; } @@ -358,6 +344,19 @@ background: rgba(255, 255, 255, 0.02); } +.output-log-entry.expanded { + background: rgba(255, 255, 255, 0.03); +} + +.output-log-entry-main { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 6px 12px; + line-height: 1.4; + cursor: pointer; +} + .output-log-entry-icon { display: flex; align-items: center; @@ -389,78 +388,69 @@ font-weight: 600; } -.output-log-entry-client { - color: #10b981; - font-size: 10px; - white-space: nowrap; - padding: 1px 6px; - flex-shrink: 0; - background: rgba(16, 185, 129, 0.15); - border: 1px solid rgba(16, 185, 129, 0.4); - border-radius: 3px; - font-weight: 600; -} - .output-log-entry-message { flex: 1; color: #e0e0e0; word-break: break-word; padding-top: 2px; + white-space: pre-wrap; } -.output-log-message-container { - display: flex; - align-items: flex-start; - gap: 8px; -} - -.output-log-message-text { - flex: 1; -} - -.output-log-message-preview { - opacity: 0.9; -} - -.output-log-expand-btn { - display: inline; - margin-left: 8px; - padding: 2px 6px; - background: #3c3c3c; - border: 1px solid #4a4a4a; - border-radius: 3px; - color: #3b82f6; - font-size: 10px; - cursor: pointer; - transition: all 0.1s ease; -} - -.output-log-expand-btn:hover { - background: #4a4a4a; - border-color: #3b82f6; -} - -.output-log-json-btn { +.output-log-entry-copy { display: flex; align-items: center; justify-content: center; - padding: 2px 6px; - background: #3b82f6; - border: none; - border-radius: 3px; - color: #fff; - font-size: 10px; - font-weight: 600; + width: 22px; + height: 22px; + padding: 0; + background: transparent; + border: 1px solid transparent; + border-radius: 4px; + color: #666; cursor: pointer; - opacity: 0.8; - transition: all 0.1s ease; + opacity: 0; + transition: all 0.15s ease; flex-shrink: 0; } -.output-log-json-btn:hover { +.output-log-entry:hover .output-log-entry-copy { opacity: 1; } +.output-log-entry-copy:hover { + background: #3c3c3c; + border-color: #5a5a5a; + color: #e0e0e0; +} + +/* Stack Trace (expanded) */ +.output-log-entry-stack { + padding: 8px 12px 12px 42px; + background: rgba(0, 0, 0, 0.2); + border-top: 1px solid #2a2a2a; +} + +.output-log-stack-header { + color: #888; + font-size: 11px; + margin-bottom: 6px; + font-weight: 600; +} + +.output-log-stack-line { + color: #888; + font-size: 11px; + line-height: 1.6; + white-space: pre; + padding-left: 12px; +} + +.output-log-stack-line:hover { + color: #4a9eff; + background: rgba(74, 158, 255, 0.1); + cursor: pointer; +} + /* Log Level Styles */ .output-log-entry-debug { color: #858585; @@ -470,10 +460,18 @@ color: #858585; } +.output-log-entry-debug .output-log-entry-message { + color: #858585; +} + .output-log-entry-info .output-log-entry-icon { color: #4a9eff; } +.output-log-entry-info .output-log-entry-message { + color: #e0e0e0; +} + .output-log-entry-warn { background: rgba(255, 193, 7, 0.05); } @@ -482,6 +480,10 @@ color: #ffc107; } +.output-log-entry-warn .output-log-entry-message { + color: #ffc107; +} + .output-log-entry-error { background: rgba(244, 67, 54, 0.08); } @@ -490,6 +492,10 @@ color: #f44336; } +.output-log-entry-error .output-log-entry-message { + color: #f44336; +} + .log-entry-remote { border-left: 2px solid #4a9eff; background: rgba(74, 158, 255, 0.03); diff --git a/packages/editor-app/src/styles/ProfilerWindow.css b/packages/editor-app/src/styles/ProfilerWindow.css index 98641c1f..a9535b7f 100644 --- a/packages/editor-app/src/styles/ProfilerWindow.css +++ b/packages/editor-app/src/styles/ProfilerWindow.css @@ -88,6 +88,30 @@ } } +.profiler-window-controls { + display: flex; + align-items: center; + gap: 4px; +} + +.profiler-window-btn { + padding: 6px; + background: transparent; + border: none; + border-radius: var(--radius-sm); + color: var(--color-text-secondary); + cursor: pointer; + transition: all var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; +} + +.profiler-window-btn:hover { + background: var(--color-bg-hover); + color: var(--color-text-primary); +} + .profiler-window-close { padding: 6px; background: transparent; @@ -106,6 +130,24 @@ color: var(--color-text-primary); } +/* Fullscreen styles */ +.profiler-window-overlay.fullscreen { + background: rgba(0, 0, 0, 0.95); +} + +.profiler-window.fullscreen { + border: none; + border-radius: 0; + max-height: none; + height: 100vh; + width: 100vw; + max-width: none; +} + +.profiler-window.fullscreen .profiler-window-header { + border-radius: 0; +} + .profiler-window-toolbar { display: flex; align-items: center; diff --git a/packages/editor-app/src/styles/SceneHierarchy.css b/packages/editor-app/src/styles/SceneHierarchy.css index 9fcaa2bb..a5ceee5c 100644 --- a/packages/editor-app/src/styles/SceneHierarchy.css +++ b/packages/editor-app/src/styles/SceneHierarchy.css @@ -243,10 +243,20 @@ opacity: 0.5; } -.outliner-item.drop-target { +/* Drop Indicators */ +.outliner-item.drop-before { border-top: 2px solid #4a9eff; } +.outliner-item.drop-after { + border-bottom: 2px solid #4a9eff; +} + +.outliner-item.drop-inside { + background: rgba(74, 158, 255, 0.2); + box-shadow: inset 0 0 0 1px #4a9eff; +} + .outliner-item.disabled { opacity: 0.5; } @@ -291,6 +301,17 @@ flex-shrink: 0; } +.outliner-item-expand.clickable { + cursor: pointer; + border-radius: 3px; + transition: background 0.15s ease, color 0.15s ease; +} + +.outliner-item-expand.clickable:hover { + color: #fff; + background: rgba(255, 255, 255, 0.1); +} + .outliner-item-expand:hover { color: #ccc; } diff --git a/packages/editor-core/package.json b/packages/editor-core/package.json index 19bbd3f2..8e7a0a61 100644 --- a/packages/editor-core/package.json +++ b/packages/editor-core/package.json @@ -33,15 +33,15 @@ "build:watch": "tsc --watch", "rebuild": "npm run clean && npm run build", "build:npm": "npm run build && node build-rollup.cjs", - "test": "jest --config jest.config.cjs", - "test:watch": "jest --watch --config jest.config.cjs", - "test:coverage": "jest --coverage --config jest.config.cjs", "lint": "eslint \"src/**/*.{ts,tsx}\"", "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix" }, "author": "yhh", "license": "MIT", "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/asset-system": "workspace:*", + "@esengine/engine-core": "workspace:*", "@babel/core": "^7.28.3", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", @@ -61,11 +61,7 @@ "rollup": "^4.42.0", "rollup-plugin-dts": "^6.2.1", "ts-jest": "^29.4.0", - "typescript": "^5.8.3" - }, - "peerDependencies": { - "@esengine/asset-system": "*", - "@esengine/ecs-framework": "^2.2.8", + "typescript": "^5.8.3", "react": "^18.2.0", "rxjs": "^7.8.0", "tsyringe": "^4.8.0" diff --git a/packages/editor-core/rollup.config.cjs b/packages/editor-core/rollup.config.cjs index 84ad1602..bc68fad0 100644 --- a/packages/editor-core/rollup.config.cjs +++ b/packages/editor-core/rollup.config.cjs @@ -15,7 +15,13 @@ const banner = `/** * @license ${pkg.license} */`; -const external = ['@esengine/ecs-framework']; +const external = [ + '@esengine/ecs-framework', + 'react', + 'react-dom', + 'react/jsx-runtime', + /^@types\// +]; const commonPlugins = [ resolve({ diff --git a/packages/editor-core/src/Plugin/IPluginLoader.ts b/packages/editor-core/src/Plugin/IPluginLoader.ts index 590d37b0..ea386662 100644 --- a/packages/editor-core/src/Plugin/IPluginLoader.ts +++ b/packages/editor-core/src/Plugin/IPluginLoader.ts @@ -1,71 +1,29 @@ /** - * 插件加载器接口 - * Plugin loader interfaces + * 编辑器模块接口 + * Editor module interfaces + * + * 定义编辑器专用的模块接口和 UI 描述符类型。 + * Define editor-specific module interfaces and UI descriptor types. */ -import type { IScene, ServiceContainer, ComponentRegistry } from '@esengine/ecs-framework'; -import type { PluginDescriptor } from './PluginDescriptor'; +import type { ServiceContainer } from '@esengine/ecs-framework'; -/** - * 系统创建上下文 - * System creation context - */ -export interface SystemContext { - /** 是否为编辑器模式 | Is editor mode */ - isEditor: boolean; +// 从 PluginDescriptor 重新导出(来源于 engine-core) +export type { + PluginCategory, + LoadingPhase, + ModuleType, + ModuleDescriptor, + PluginDependency, + PluginDescriptor, + SystemContext, + IRuntimeModule, + IPlugin +} from './PluginDescriptor'; - /** 引擎桥接(如有) | Engine bridge (if available) */ - engineBridge?: any; - - /** 渲染系统(如有) | Render system (if available) */ - renderSystem?: any; - - /** 其他已创建的系统引用 | Other created system references */ - [key: string]: any; -} - -/** - * 运行时模块加载器 - * Runtime module loader - */ -export interface IRuntimeModuleLoader { - /** - * 注册组件到 ComponentRegistry - * Register components to ComponentRegistry - */ - registerComponents(registry: typeof ComponentRegistry): void; - - /** - * 注册服务到 ServiceContainer - * Register services to ServiceContainer - */ - registerServices?(services: ServiceContainer): void; - - /** - * 为场景创建系统 - * Create systems for scene - */ - createSystems?(scene: IScene, context: SystemContext): void; - - /** - * 所有系统创建完成后调用 - * 用于处理跨插件的系统依赖关系 - * Called after all systems are created, used for cross-plugin system dependencies - */ - onSystemsCreated?(scene: IScene, context: SystemContext): void; - - /** - * 模块初始化完成回调 - * Module initialization complete callback - */ - onInitialize?(): Promise; - - /** - * 模块销毁回调 - * Module destroy callback - */ - onDestroy?(): void; -} +// ============================================================================ +// UI 描述符类型 | UI Descriptor Types +// ============================================================================ /** * 面板位置 @@ -146,8 +104,8 @@ export interface ToolbarItemDescriptor { } /** - * 组件检视器提供者(简化版) - * Component inspector provider (simplified) + * 组件检视器提供者 + * Component inspector provider */ export interface ComponentInspectorProviderDef { /** 组件类型名 | Component type name */ @@ -235,6 +193,33 @@ export interface ISerializer { deserialize(data: Uint8Array): T; } +/** + * 文件创建模板 + * File creation template + */ +export interface FileCreationTemplate { + /** 模板ID | Template ID */ + id: string; + /** 标签 | Label */ + label: string; + /** 扩展名 | Extension */ + extension: string; + /** 图标 | Icon */ + icon?: string; + /** 分类 | Category */ + category?: string; + /** + * 获取文件内容 | Get file content + * @param fileName 文件名(不含路径,含扩展名) + * @returns 文件内容字符串 + */ + getContent: (fileName: string) => string | Promise; +} + +// ============================================================================ +// 编辑器模块接口 | Editor Module Interface +// ============================================================================ + /** * 编辑器模块加载器 * Editor module loader @@ -327,43 +312,22 @@ export interface IEditorModuleLoader { setLocale?(locale: string): void; } -/** - * 统一插件加载器 - * Unified plugin loader - */ -export interface IPluginLoader { - /** 插件描述符 | Plugin descriptor */ - readonly descriptor: PluginDescriptor; - - /** 运行时模块(可选) | Runtime module (optional) */ - readonly runtimeModule?: IRuntimeModuleLoader; - - /** 编辑器模块(可选) | Editor module (optional) */ - readonly editorModule?: IEditorModuleLoader; -} +// ============================================================================ +// 类型别名(向后兼容)| Type Aliases (backward compatibility) +// ============================================================================ /** - * 文件创建模板 - * File creation template + * IPluginLoader 类型别名 * - * 插件通过 getContent 提供文件内容,编辑器负责写入文件。 - * 这样可以避免插件直接访问文件系统带来的权限问题。 + * @deprecated 使用 IPlugin 代替。IPluginLoader 只是 IPlugin 的别名。 + * @deprecated Use IPlugin instead. IPluginLoader is just an alias for IPlugin. */ -export interface FileCreationTemplate { - /** 模板ID | Template ID */ - id: string; - /** 标签 | Label */ - label: string; - /** 扩展名 | Extension */ - extension: string; - /** 图标 | Icon */ - icon?: string; - /** 分类 | Category */ - category?: string; - /** - * 获取文件内容 | Get file content - * @param fileName 文件名(不含路径,含扩展名) - * @returns 文件内容字符串 - */ - getContent: (fileName: string) => string | Promise; -} +export type { IPlugin as IPluginLoader } from './PluginDescriptor'; + +/** + * IRuntimeModuleLoader 类型别名 + * + * @deprecated 使用 IRuntimeModule 代替。 + * @deprecated Use IRuntimeModule instead. + */ +export type { IRuntimeModule as IRuntimeModuleLoader } from './PluginDescriptor'; diff --git a/packages/editor-core/src/Plugin/PluginDescriptor.ts b/packages/editor-core/src/Plugin/PluginDescriptor.ts index e5e5b90d..37a2308d 100644 --- a/packages/editor-core/src/Plugin/PluginDescriptor.ts +++ b/packages/editor-core/src/Plugin/PluginDescriptor.ts @@ -1,155 +1,23 @@ /** - * 插件系统类型定义 - * Plugin system type definitions + * 插件描述符类型 + * Plugin descriptor types + * + * 从 @esengine/engine-core 重新导出基础类型,并添加编辑器专用类型。 + * Re-export base types from @esengine/engine-core, and add editor-specific types. */ -/** - * 插件类别 - * Plugin category - */ -export type PluginCategory = - | 'core' // 核心功能 | Core functionality - | 'rendering' // 渲染相关 | Rendering - | 'ui' // UI 系统 | UI System - | 'ai' // AI/行为树 | AI/Behavior - | 'physics' // 物理引擎 | Physics - | 'audio' // 音频系统 | Audio - | 'networking' // 网络功能 | Networking - | 'tools' // 工具/编辑器扩展 | Tools/Editor extensions - | 'scripting' // 脚本/蓝图 | Scripting/Blueprint - | 'content'; // 内容/资源 | Content/Assets - -/** - * 加载阶段 - 控制插件模块的加载顺序 - * Loading phase - controls the loading order of plugin modules - */ -export type LoadingPhase = - | 'earliest' // 最早加载(核心模块) | Earliest (core modules) - | 'preDefault' // 默认之前 | Before default - | 'default' // 默认阶段 | Default phase - | 'postDefault' // 默认之后 | After default - | 'postEngine'; // 引擎初始化后 | After engine init - -/** - * 模块类型 - * Module type - */ -export type ModuleType = 'runtime' | 'editor'; - -/** - * 模块描述符 - 描述插件内的一个模块 - * Module descriptor - describes a module within a plugin - */ -export interface ModuleDescriptor { - /** 模块名称 | Module name */ - name: string; - - /** 模块类型 | Module type */ - type: ModuleType; - - /** 加载阶段 | Loading phase */ - loadingPhase?: LoadingPhase; - - /** 模块入口文件(相对路径) | Module entry file (relative path) */ - entry?: string; - - // ===== 运行时模块配置 | Runtime module config ===== - - /** 导出的组件类名列表 | Exported component class names */ - components?: string[]; - - /** 导出的系统类名列表 | Exported system class names */ - systems?: string[]; - - /** 导出的服务类名列表 | Exported service class names */ - services?: string[]; - - // ===== 编辑器模块配置 | Editor module config ===== - - /** 注册的面板ID列表 | Registered panel IDs */ - panels?: string[]; - - /** 注册的检视器类型列表 | Registered inspector types */ - inspectors?: string[]; - - /** 注册的 Gizmo 提供者列表 | Registered Gizmo providers */ - gizmoProviders?: string[]; - - /** 注册的编译器列表 | Registered compilers */ - compilers?: string[]; - - /** 注册的文件处理器扩展名 | Registered file handler extensions */ - fileHandlers?: string[]; -} - -/** - * 插件依赖 - * Plugin dependency - */ -export interface PluginDependency { - /** 依赖的插件ID | Dependent plugin ID */ - id: string; - - /** 版本要求(semver) | Version requirement (semver) */ - version?: string; - - /** 是否可选 | Optional */ - optional?: boolean; -} - -/** - * 插件描述符 - 对应 plugin.json 文件 - * Plugin descriptor - corresponds to plugin.json - */ -export interface PluginDescriptor { - /** 插件唯一标识符,如 "@esengine/tilemap" | Unique plugin ID */ - id: string; - - /** 显示名称 | Display name */ - name: string; - - /** 版本号 | Version */ - version: string; - - /** 描述 | Description */ - description?: string; - - /** 作者 | Author */ - author?: string; - - /** 许可证 | License */ - license?: string; - - /** 插件类别 | Plugin category */ - category: PluginCategory; - - /** 标签(用于搜索) | Tags (for search) */ - tags?: string[]; - - /** 图标(Lucide 图标名) | Icon (Lucide icon name) */ - icon?: string; - - /** 是否默认启用 | Enabled by default */ - enabledByDefault: boolean; - - /** 是否可以包含内容资产 | Can contain content assets */ - canContainContent: boolean; - - /** 是否为引擎内置插件 | Is engine built-in plugin */ - isEnginePlugin: boolean; - - /** 是否为核心插件(不可禁用) | Is core plugin (cannot be disabled) */ - isCore?: boolean; - - /** 模块列表 | Module list */ - modules: ModuleDescriptor[]; - - /** 依赖列表 | Dependency list */ - dependencies?: PluginDependency[]; - - /** 平台要求 | Platform requirements */ - platforms?: ('web' | 'desktop' | 'mobile')[]; -} +// 从 engine-core 重新导出所有插件相关类型 +export type { + PluginCategory, + LoadingPhase, + ModuleType, + ModuleDescriptor, + PluginDependency, + PluginDescriptor, + SystemContext, + IRuntimeModule, + IPlugin +} from '@esengine/engine-core'; /** * 插件状态 diff --git a/packages/editor-core/src/Plugin/PluginManager.ts b/packages/editor-core/src/Plugin/PluginManager.ts index b809498c..0b2e05cc 100644 --- a/packages/editor-core/src/Plugin/PluginManager.ts +++ b/packages/editor-core/src/Plugin/PluginManager.ts @@ -9,16 +9,18 @@ import type { PluginDescriptor, PluginState, PluginCategory, - LoadingPhase + LoadingPhase, + IPlugin } from './PluginDescriptor'; import type { - IPluginLoader, - SystemContext + SystemContext, + IEditorModuleLoader } from './IPluginLoader'; import { EntityCreationRegistry } from '../Services/EntityCreationRegistry'; import { ComponentActionRegistry } from '../Services/ComponentActionRegistry'; import { FileActionRegistry } from '../Services/FileActionRegistry'; import { UIRegistry } from '../Services/UIRegistry'; +import { MessageHub } from '../Services/MessageHub'; const logger = createLogger('PluginManager'); @@ -28,13 +30,65 @@ const logger = createLogger('PluginManager'); */ export const IPluginManager = Symbol.for('IPluginManager'); +/** + * 标准化后的插件描述符(所有字段都有值) + * Normalized plugin descriptor (all fields have values) + */ +export interface NormalizedPluginDescriptor { + id: string; + name: string; + version: string; + description: string; + category: PluginCategory; + tags: string[]; + icon?: string; + enabledByDefault: boolean; + canContainContent: boolean; + isEnginePlugin: boolean; + isCore: boolean; + modules: Array<{ name: string; type: 'runtime' | 'editor'; loadingPhase: LoadingPhase }>; + dependencies: Array<{ id: string; version?: string; optional?: boolean }>; + platforms: ('web' | 'desktop' | 'mobile')[]; +} + +/** + * 标准化后的插件(内部使用) + * Normalized plugin (internal use) + */ +export interface NormalizedPlugin { + descriptor: NormalizedPluginDescriptor; + runtimeModule?: IPlugin['runtimeModule']; + editorModule?: IEditorModuleLoader; +} + +/** + * 插件注册的资源(用于卸载时清理) + * Resources registered by plugin (for cleanup on unload) + */ +export interface PluginRegisteredResources { + /** 注册的面板ID | Registered panel IDs */ + panelIds: string[]; + /** 注册的菜单ID | Registered menu IDs */ + menuIds: string[]; + /** 注册的工具栏ID | Registered toolbar IDs */ + toolbarIds: string[]; + /** 注册的实体模板ID | Registered entity template IDs */ + entityTemplateIds: string[]; + /** 注册的组件操作 | Registered component actions */ + componentActions: Array<{ componentName: string; actionId: string }>; + /** 注册的文件处理器 | Registered file handlers */ + fileHandlers: any[]; + /** 注册的文件模板 | Registered file templates */ + fileTemplates: any[]; +} + /** * 已注册的插件信息 * Registered plugin info */ export interface RegisteredPlugin { - /** 插件加载器 | Plugin loader */ - loader: IPluginLoader; + /** 标准化后的插件 | Normalized plugin */ + plugin: NormalizedPlugin; /** 插件状态 | Plugin state */ state: PluginState; /** 错误信息 | Error info */ @@ -45,6 +99,8 @@ export interface RegisteredPlugin { loadedAt?: number; /** 激活时间 | Activation time */ activatedAt?: number; + /** 插件注册的资源 | Resources registered by plugin */ + registeredResources?: PluginRegisteredResources; } /** @@ -80,9 +136,29 @@ export class PluginManager implements IService { private plugins: Map = new Map(); private initialized = false; private editorInitialized = false; + private services: ServiceContainer | null = null; + private currentScene: IScene | null = null; + private currentContext: SystemContext | null = null; constructor() {} + /** + * 设置服务容器(用于动态启用插件) + * Set service container (for dynamic plugin enabling) + */ + setServiceContainer(services: ServiceContainer): void { + this.services = services; + } + + /** + * 设置当前场景和上下文(用于动态创建系统) + * Set current scene and context (for dynamic system creation) + */ + setSceneContext(scene: IScene, context: SystemContext): void { + this.currentScene = scene; + this.currentContext = context; + } + /** * 释放资源 * Dispose resources @@ -91,25 +167,60 @@ export class PluginManager implements IService { this.reset(); } + /** + * 标准化插件描述符,填充默认值 + * Normalize plugin descriptor, fill in defaults + */ + private normalizePlugin(input: IPlugin): NormalizedPlugin { + const d = input.descriptor; + return { + descriptor: { + id: d.id, + name: d.name, + version: d.version, + description: d.description ?? '', + category: d.category ?? 'tools', + tags: d.tags ?? [], + icon: d.icon, + enabledByDefault: d.enabledByDefault ?? false, + canContainContent: d.canContainContent ?? false, + isEnginePlugin: d.isEnginePlugin ?? true, + isCore: d.isCore ?? false, + modules: (d.modules ?? [{ name: 'Runtime', type: 'runtime' as const, loadingPhase: 'default' as const }]).map((m: { name: string; type: 'runtime' | 'editor'; loadingPhase?: LoadingPhase }) => ({ + name: m.name, + type: m.type, + loadingPhase: m.loadingPhase ?? 'default' as LoadingPhase + })), + dependencies: d.dependencies ?? [], + platforms: d.platforms ?? ['web', 'desktop'] + }, + runtimeModule: input.runtimeModule, + editorModule: input.editorModule as IEditorModuleLoader | undefined + }; + } + /** * 注册插件 * Register plugin + * + * 接受任何符合 IPlugin 接口的插件,内部会标准化所有字段。 + * Accepts any plugin conforming to IPlugin interface, normalizes all fields internally. */ - register(loader: IPluginLoader): void { - if (!loader) { - logger.error('Cannot register plugin: loader is null or undefined'); + register(plugin: IPlugin): void { + if (!plugin) { + logger.error('Cannot register plugin: plugin is null or undefined'); return; } - if (!loader.descriptor) { - logger.error('Cannot register plugin: descriptor is null or undefined', loader); + if (!plugin.descriptor) { + logger.error('Cannot register plugin: descriptor is null or undefined', plugin); return; } - const { id } = loader.descriptor; + const { id } = plugin.descriptor; if (!id) { - logger.error('Cannot register plugin: descriptor.id is null or undefined', loader.descriptor); + logger.error('Cannot register plugin: descriptor.id is null or undefined', plugin.descriptor); return; } @@ -118,36 +229,42 @@ export class PluginManager implements IService { return; } - const enabled = loader.descriptor.isCore || loader.descriptor.enabledByDefault; + const normalized = this.normalizePlugin(plugin); + const enabled = normalized.descriptor.isCore || normalized.descriptor.enabledByDefault; this.plugins.set(id, { - loader, + plugin: normalized, state: 'loaded', enabled, loadedAt: Date.now() }); - logger.info(`Plugin registered: ${id} (${loader.descriptor.name})`); + logger.info(`Plugin registered: ${id} (${normalized.descriptor.name})`); } /** - * 启用插件 - * Enable plugin + * 启用插件(动态加载编辑器模块和运行时系统) + * Enable plugin (dynamically load editor module and runtime systems) */ - enable(pluginId: string): boolean { + async enable(pluginId: string): Promise { const plugin = this.plugins.get(pluginId); if (!plugin) { logger.error(`Plugin ${pluginId} not found`); return false; } - if (plugin.loader.descriptor.isCore) { + if (plugin.plugin.descriptor.isCore) { logger.warn(`Core plugin ${pluginId} cannot be disabled/enabled`); return false; } + if (plugin.enabled) { + logger.warn(`Plugin ${pluginId} is already enabled`); + return true; + } + // 检查依赖 - const deps = plugin.loader.descriptor.dependencies || []; + const deps = plugin.plugin.descriptor.dependencies; for (const dep of deps) { if (dep.optional) continue; const depPlugin = this.plugins.get(dep.id); @@ -158,31 +275,57 @@ export class PluginManager implements IService { } plugin.enabled = true; - plugin.state = 'loaded'; - logger.info(`Plugin enabled: ${pluginId}`); - return true; + plugin.state = 'loading'; + + try { + // 动态加载编辑器模块 + if (this.services && this.editorInitialized) { + await this.activatePluginEditor(pluginId); + } + + // 动态加载运行时模块 + if (this.currentScene && this.currentContext && this.initialized) { + await this.activatePluginRuntime(pluginId); + } + + plugin.state = 'active'; + plugin.activatedAt = Date.now(); + logger.info(`Plugin enabled and activated: ${pluginId}`); + return true; + } catch (e) { + logger.error(`Failed to activate plugin ${pluginId}:`, e); + plugin.state = 'error'; + plugin.error = e as Error; + plugin.enabled = false; + return false; + } } /** - * 禁用插件 - * Disable plugin + * 禁用插件(动态卸载编辑器模块和运行时系统) + * Disable plugin (dynamically unload editor module and runtime systems) */ - disable(pluginId: string): boolean { + async disable(pluginId: string): Promise { const plugin = this.plugins.get(pluginId); if (!plugin) { logger.error(`Plugin ${pluginId} not found`); return false; } - if (plugin.loader.descriptor.isCore) { + if (plugin.plugin.descriptor.isCore) { logger.warn(`Core plugin ${pluginId} cannot be disabled`); return false; } + if (!plugin.enabled) { + logger.warn(`Plugin ${pluginId} is already disabled`); + return true; + } + // 检查是否有其他插件依赖此插件 for (const [id, p] of this.plugins) { if (!p.enabled || id === pluginId) continue; - const deps = p.loader.descriptor.dependencies || []; + const deps = p.plugin.descriptor.dependencies; const hasDep = deps.some(d => d.id === pluginId && !d.optional); if (hasDep) { logger.error(`Cannot disable ${pluginId}: plugin ${id} depends on it`); @@ -190,10 +333,318 @@ export class PluginManager implements IService { } } - plugin.enabled = false; - plugin.state = 'disabled'; - logger.info(`Plugin disabled: ${pluginId}`); - return true; + try { + // 卸载编辑器模块 + if (this.services) { + await this.deactivatePluginEditor(pluginId); + } + + // 卸载运行时模块(清理系统) + if (this.currentScene) { + this.deactivatePluginRuntime(pluginId); + } + + plugin.enabled = false; + plugin.state = 'disabled'; + plugin.registeredResources = undefined; + logger.info(`Plugin disabled: ${pluginId}`); + return true; + } catch (e) { + logger.error(`Failed to deactivate plugin ${pluginId}:`, e); + return false; + } + } + + /** + * 动态激活插件的编辑器模块 + * Dynamically activate plugin's editor module + */ + private async activatePluginEditor(pluginId: string): Promise { + const plugin = this.plugins.get(pluginId); + if (!plugin || !this.services) { + logger.warn(`activatePluginEditor: skipping ${pluginId} (plugin=${!!plugin}, services=${!!this.services})`); + return; + } + + const editorModule = plugin.plugin.editorModule; + if (!editorModule) { + logger.debug(`activatePluginEditor: ${pluginId} has no editorModule`); + return; + } + + logger.info(`activatePluginEditor: activating ${pluginId}`); + + // 初始化资源跟踪 + const resources: PluginRegisteredResources = { + panelIds: [], + menuIds: [], + toolbarIds: [], + entityTemplateIds: [], + componentActions: [], + fileHandlers: [], + fileTemplates: [] + }; + + // 获取注册表服务 + const entityCreationRegistry = this.services.tryResolve(EntityCreationRegistry); + const componentActionRegistry = this.services.tryResolve(ComponentActionRegistry); + const fileActionRegistry = this.services.tryResolve(FileActionRegistry); + const uiRegistry = this.services.tryResolve(UIRegistry); + + // 安装编辑器模块 + await editorModule.install(this.services); + logger.debug(`Editor module installed: ${pluginId}`); + + // 注册实体创建模板 + if (entityCreationRegistry && editorModule.getEntityCreationTemplates) { + const templates = editorModule.getEntityCreationTemplates(); + logger.info(`[${pluginId}] getEntityCreationTemplates returned ${templates?.length ?? 0} templates`); + if (templates && templates.length > 0) { + entityCreationRegistry.registerMany(templates); + resources.entityTemplateIds = templates.map(t => t.id); + logger.info(`Registered ${templates.length} entity templates from: ${pluginId}`, templates.map(t => t.id)); + } + } else { + logger.debug(`[${pluginId}] entityCreationRegistry=${!!entityCreationRegistry}, hasGetEntityCreationTemplates=${!!editorModule.getEntityCreationTemplates}`); + } + + // 注册组件操作 + if (componentActionRegistry && editorModule.getComponentActions) { + const actions = editorModule.getComponentActions(); + if (actions && actions.length > 0) { + for (const action of actions) { + componentActionRegistry.register(action); + resources.componentActions.push({ + componentName: action.componentName, + actionId: action.id + }); + } + logger.debug(`Registered ${actions.length} component actions from: ${pluginId}`); + } + } + + // 注册文件操作处理器 + if (fileActionRegistry && editorModule.getFileActionHandlers) { + const handlers = editorModule.getFileActionHandlers(); + if (handlers && handlers.length > 0) { + for (const handler of handlers) { + fileActionRegistry.registerActionHandler(handler); + resources.fileHandlers.push(handler); + } + logger.debug(`Registered ${handlers.length} file action handlers from: ${pluginId}`); + } + } + + // 注册文件创建模板 + if (fileActionRegistry && editorModule.getFileCreationTemplates) { + const templates = editorModule.getFileCreationTemplates(); + if (templates && templates.length > 0) { + for (const template of templates) { + fileActionRegistry.registerCreationTemplate(template); + resources.fileTemplates.push(template); + } + logger.debug(`Registered ${templates.length} file creation templates from: ${pluginId}`); + } + } + + // 注册面板 + if (uiRegistry && editorModule.getPanels) { + const panels = editorModule.getPanels(); + if (panels && panels.length > 0) { + uiRegistry.registerPanels(panels); + resources.panelIds = panels.map(p => p.id); + logger.debug(`Registered ${panels.length} panels from: ${pluginId}`); + } + } + + // 注册菜单 + if (uiRegistry && editorModule.getMenuItems) { + const menuItems = editorModule.getMenuItems(); + if (menuItems && menuItems.length > 0) { + for (const item of menuItems) { + // 转换 MenuItemDescriptor 到 MenuItem(execute -> onClick) + const menuItem = { + ...item, + onClick: item.execute + }; + uiRegistry.registerMenu(menuItem as any); + resources.menuIds.push(item.id); + } + logger.debug(`Registered ${menuItems.length} menu items from: ${pluginId}`); + } + } + + // 注册工具栏 + if (uiRegistry && editorModule.getToolbarItems) { + const toolbarItems = editorModule.getToolbarItems(); + if (toolbarItems && toolbarItems.length > 0) { + for (const item of toolbarItems) { + uiRegistry.registerToolbarItem(item as any); + resources.toolbarIds.push(item.id); + } + logger.debug(`Registered ${toolbarItems.length} toolbar items from: ${pluginId}`); + } + } + + // 保存注册的资源 + plugin.registeredResources = resources; + + // 调用 onEditorReady + if (editorModule.onEditorReady) { + await editorModule.onEditorReady(); + } + + // 发布插件安装事件,通知 UI 刷新 + const messageHub = this.services.tryResolve(MessageHub); + if (messageHub) { + messageHub.publish('plugin:installed', { pluginId }); + } + } + + /** + * 动态卸载插件的编辑器模块 + * Dynamically deactivate plugin's editor module + */ + private async deactivatePluginEditor(pluginId: string): Promise { + const plugin = this.plugins.get(pluginId); + if (!plugin || !this.services) return; + + const editorModule = plugin.plugin.editorModule; + const resources = plugin.registeredResources; + + // 获取注册表服务 + const entityCreationRegistry = this.services.tryResolve(EntityCreationRegistry); + const componentActionRegistry = this.services.tryResolve(ComponentActionRegistry); + const fileActionRegistry = this.services.tryResolve(FileActionRegistry); + const uiRegistry = this.services.tryResolve(UIRegistry); + + if (resources) { + // 注销面板 + if (uiRegistry) { + for (const panelId of resources.panelIds) { + uiRegistry.unregisterPanel(panelId); + } + } + + // 注销菜单 + if (uiRegistry) { + for (const menuId of resources.menuIds) { + uiRegistry.unregisterMenu(menuId); + } + } + + // 注销工具栏 + if (uiRegistry) { + for (const toolbarId of resources.toolbarIds) { + uiRegistry.unregisterToolbarItem(toolbarId); + } + } + + // 注销实体模板 + if (entityCreationRegistry) { + for (const templateId of resources.entityTemplateIds) { + entityCreationRegistry.unregister(templateId); + } + } + + // 注销组件操作 + if (componentActionRegistry) { + for (const action of resources.componentActions) { + componentActionRegistry.unregister(action.componentName, action.actionId); + } + } + + // 注销文件处理器 + if (fileActionRegistry) { + for (const handler of resources.fileHandlers) { + fileActionRegistry.unregisterActionHandler(handler); + } + } + + // 注销文件模板 + if (fileActionRegistry) { + for (const template of resources.fileTemplates) { + fileActionRegistry.unregisterCreationTemplate(template); + } + } + + logger.debug(`Unregistered resources for: ${pluginId}`); + } + + // 调用 uninstall + if (editorModule?.uninstall) { + await editorModule.uninstall(); + logger.debug(`Editor module uninstalled: ${pluginId}`); + } + + // 发布插件卸载事件,通知 UI 刷新 + const messageHub = this.services.tryResolve(MessageHub); + if (messageHub) { + messageHub.publish('plugin:uninstalled', { pluginId }); + } + } + + /** + * 动态激活插件的运行时模块 + * Dynamically activate plugin's runtime module + */ + private async activatePluginRuntime(pluginId: string): Promise { + const plugin = this.plugins.get(pluginId); + if (!plugin || !this.currentScene || !this.currentContext || !this.services) return; + + const runtimeModule = plugin.plugin.runtimeModule; + if (!runtimeModule) return; + + // 注册组件 + if (runtimeModule.registerComponents) { + runtimeModule.registerComponents(ComponentRegistry); + logger.debug(`Components registered for: ${pluginId}`); + } + + // 注册服务 + if (runtimeModule.registerServices) { + runtimeModule.registerServices(this.services); + logger.debug(`Services registered for: ${pluginId}`); + } + + // 创建系统 + if (runtimeModule.createSystems) { + runtimeModule.createSystems(this.currentScene, this.currentContext); + logger.debug(`Systems created for: ${pluginId}`); + } + + // 调用系统创建后回调 + if (runtimeModule.onSystemsCreated) { + runtimeModule.onSystemsCreated(this.currentScene, this.currentContext); + logger.debug(`Systems wired for: ${pluginId}`); + } + + // 调用初始化 + if (runtimeModule.onInitialize) { + await runtimeModule.onInitialize(); + logger.debug(`Runtime initialized for: ${pluginId}`); + } + } + + /** + * 动态卸载插件的运行时模块 + * Dynamically deactivate plugin's runtime module + */ + private deactivatePluginRuntime(pluginId: string): void { + const plugin = this.plugins.get(pluginId); + if (!plugin) return; + + const runtimeModule = plugin.plugin.runtimeModule; + if (!runtimeModule) return; + + // 调用销毁回调 + if (runtimeModule.onDestroy) { + runtimeModule.onDestroy(); + logger.debug(`Runtime destroyed for: ${pluginId}`); + } + + // 注意:组件和服务无法动态注销,这是设计限制 + // 系统的移除需要场景支持,暂时只调用 onDestroy } /** @@ -235,7 +686,7 @@ export class PluginManager implements IService { */ getPluginsByCategory(category: PluginCategory): RegisteredPlugin[] { return this.getAllPlugins().filter( - p => p.loader.descriptor.category === category + p => p.plugin.descriptor.category === category ); } @@ -257,6 +708,9 @@ export class PluginManager implements IService { return; } + // 保存服务容器引用 + this.services = services; + logger.info('Initializing runtime modules...'); const sortedPlugins = this.sortByLoadingPhase('runtime'); @@ -266,8 +720,8 @@ export class PluginManager implements IService { const plugin = this.plugins.get(pluginId); if (!plugin?.enabled) continue; - const runtimeModule = plugin.loader.runtimeModule; - if (runtimeModule) { + const runtimeModule = plugin.plugin.runtimeModule; + if (runtimeModule?.registerComponents) { try { runtimeModule.registerComponents(ComponentRegistry); logger.debug(`Components registered for: ${pluginId}`); @@ -284,7 +738,7 @@ export class PluginManager implements IService { const plugin = this.plugins.get(pluginId); if (!plugin?.enabled || plugin.state === 'error') continue; - const runtimeModule = plugin.loader.runtimeModule; + const runtimeModule = plugin.plugin.runtimeModule; if (runtimeModule?.registerServices) { try { runtimeModule.registerServices(services); @@ -302,7 +756,7 @@ export class PluginManager implements IService { const plugin = this.plugins.get(pluginId); if (!plugin?.enabled || plugin.state === 'error') continue; - const runtimeModule = plugin.loader.runtimeModule; + const runtimeModule = plugin.plugin.runtimeModule; if (runtimeModule?.onInitialize) { try { await runtimeModule.onInitialize(); @@ -330,6 +784,10 @@ export class PluginManager implements IService { * Create systems for scene */ createSystemsForScene(scene: IScene, context: SystemContext): void { + // 保存场景和上下文引用(用于动态启用插件) + this.currentScene = scene; + this.currentContext = context; + logger.info('Creating systems for scene...'); console.log('[PluginManager] createSystemsForScene called, context.assetManager:', context.assetManager ? 'exists' : 'null'); @@ -340,10 +798,10 @@ export class PluginManager implements IService { // Phase 1: Create all systems for (const pluginId of sortedPlugins) { const plugin = this.plugins.get(pluginId); - console.log(`[PluginManager] Plugin ${pluginId}: enabled=${plugin?.enabled}, state=${plugin?.state}, hasRuntimeModule=${!!plugin?.loader.runtimeModule}`); + console.log(`[PluginManager] Plugin ${pluginId}: enabled=${plugin?.enabled}, state=${plugin?.state}, hasRuntimeModule=${!!plugin?.plugin.runtimeModule}`); if (!plugin?.enabled || plugin.state === 'error') continue; - const runtimeModule = plugin.loader.runtimeModule; + const runtimeModule = plugin.plugin.runtimeModule; if (runtimeModule?.createSystems) { try { console.log(`[PluginManager] Calling createSystems for: ${pluginId}`); @@ -361,7 +819,7 @@ export class PluginManager implements IService { const plugin = this.plugins.get(pluginId); if (!plugin?.enabled || plugin.state === 'error') continue; - const runtimeModule = plugin.loader.runtimeModule; + const runtimeModule = plugin.plugin.runtimeModule; if (runtimeModule?.onSystemsCreated) { try { runtimeModule.onSystemsCreated(scene, context); @@ -385,82 +843,26 @@ export class PluginManager implements IService { return; } + // 保存服务容器引用 + this.services = services; + logger.info('Initializing editor modules...'); const sortedPlugins = this.sortByLoadingPhase('editor'); - - // 获取注册表服务 | Get registry services - const entityCreationRegistry = services.tryResolve(EntityCreationRegistry); - const componentActionRegistry = services.tryResolve(ComponentActionRegistry); - const fileActionRegistry = services.tryResolve(FileActionRegistry); - const uiRegistry = services.tryResolve(UIRegistry); + logger.info(`Sorted plugins for editor initialization: ${sortedPlugins.join(', ')}`); for (const pluginId of sortedPlugins) { const plugin = this.plugins.get(pluginId); + logger.debug(`Processing plugin ${pluginId}: enabled=${plugin?.enabled}, hasEditorModule=${!!plugin?.plugin.editorModule}`); if (!plugin?.enabled) continue; - const editorModule = plugin.loader.editorModule; - if (editorModule) { - try { - // 安装编辑器模块 | Install editor module - await editorModule.install(services); - logger.debug(`Editor module installed: ${pluginId}`); - - // 注册实体创建模板 | Register entity creation templates - if (entityCreationRegistry && editorModule.getEntityCreationTemplates) { - const templates = editorModule.getEntityCreationTemplates(); - if (templates && templates.length > 0) { - entityCreationRegistry.registerMany(templates); - logger.debug(`Registered ${templates.length} entity templates from: ${pluginId}`); - } - } - - // 注册组件操作 | Register component actions - if (componentActionRegistry && editorModule.getComponentActions) { - const actions = editorModule.getComponentActions(); - if (actions && actions.length > 0) { - for (const action of actions) { - componentActionRegistry.register(action); - } - logger.debug(`Registered ${actions.length} component actions from: ${pluginId}`); - } - } - - // 注册文件操作处理器 | Register file action handlers - if (fileActionRegistry && editorModule.getFileActionHandlers) { - const handlers = editorModule.getFileActionHandlers(); - if (handlers && handlers.length > 0) { - for (const handler of handlers) { - fileActionRegistry.registerActionHandler(handler); - } - logger.debug(`Registered ${handlers.length} file action handlers from: ${pluginId}`); - } - } - - // 注册文件创建模板 | Register file creation templates - if (fileActionRegistry && editorModule.getFileCreationTemplates) { - const templates = editorModule.getFileCreationTemplates(); - if (templates && templates.length > 0) { - for (const template of templates) { - fileActionRegistry.registerCreationTemplate(template); - } - logger.debug(`Registered ${templates.length} file creation templates from: ${pluginId}`); - } - } - - // 注册面板 | Register panels - if (uiRegistry && editorModule.getPanels) { - const panels = editorModule.getPanels(); - if (panels && panels.length > 0) { - uiRegistry.registerPanels(panels); - logger.debug(`Registered ${panels.length} panels from: ${pluginId}`); - } - } - } catch (e) { - logger.error(`Failed to install editor module for ${pluginId}:`, e); - plugin.state = 'error'; - plugin.error = e as Error; - } + try { + // 使用统一的激活方法,自动跟踪注册的资源 + await this.activatePluginEditor(pluginId); + } catch (e) { + logger.error(`Failed to install editor module for ${pluginId}:`, e); + plugin.state = 'error'; + plugin.error = e as Error; } } @@ -479,77 +881,14 @@ export class PluginManager implements IService { return; } - const editorModule = plugin.loader.editorModule; - if (!editorModule) { - return; + // 确保服务容器已设置 + if (!this.services) { + this.services = services; } - // 获取注册表服务 | Get registry services - const entityCreationRegistry = services.tryResolve(EntityCreationRegistry); - const componentActionRegistry = services.tryResolve(ComponentActionRegistry); - const fileActionRegistry = services.tryResolve(FileActionRegistry); - const uiRegistry = services.tryResolve(UIRegistry); - try { - // 安装编辑器模块 | Install editor module - await editorModule.install(services); - logger.debug(`Editor module installed: ${pluginId}`); - - // 注册实体创建模板 | Register entity creation templates - if (entityCreationRegistry && editorModule.getEntityCreationTemplates) { - const templates = editorModule.getEntityCreationTemplates(); - if (templates && templates.length > 0) { - entityCreationRegistry.registerMany(templates); - logger.debug(`Registered ${templates.length} entity templates from: ${pluginId}`); - } - } - - // 注册组件操作 | Register component actions - if (componentActionRegistry && editorModule.getComponentActions) { - const actions = editorModule.getComponentActions(); - if (actions && actions.length > 0) { - for (const action of actions) { - componentActionRegistry.register(action); - } - logger.debug(`Registered ${actions.length} component actions from: ${pluginId}`); - } - } - - // 注册文件操作处理器 | Register file action handlers - if (fileActionRegistry && editorModule.getFileActionHandlers) { - const handlers = editorModule.getFileActionHandlers(); - if (handlers && handlers.length > 0) { - for (const handler of handlers) { - fileActionRegistry.registerActionHandler(handler); - } - logger.debug(`Registered ${handlers.length} file action handlers from: ${pluginId}`); - } - } - - // 注册文件创建模板 | Register file creation templates - if (fileActionRegistry && editorModule.getFileCreationTemplates) { - const templates = editorModule.getFileCreationTemplates(); - if (templates && templates.length > 0) { - for (const template of templates) { - fileActionRegistry.registerCreationTemplate(template); - } - logger.debug(`Registered ${templates.length} file creation templates from: ${pluginId}`); - } - } - - // 注册面板 | Register panels - if (uiRegistry && editorModule.getPanels) { - const panels = editorModule.getPanels(); - if (panels && panels.length > 0) { - uiRegistry.registerPanels(panels); - logger.debug(`Registered ${panels.length} panels from: ${pluginId}`); - } - } - - // 调用 onEditorReady(如果编辑器已就绪) - if (this.editorInitialized && editorModule.onEditorReady) { - await editorModule.onEditorReady(); - } + // 使用统一的激活方法 + await this.activatePluginEditor(pluginId); } catch (e) { logger.error(`Failed to install editor module for ${pluginId}:`, e); plugin.state = 'error'; @@ -564,7 +903,7 @@ export class PluginManager implements IService { async notifyEditorReady(): Promise { for (const [pluginId, plugin] of this.plugins) { if (!plugin.enabled) continue; - const editorModule = plugin.loader.editorModule; + const editorModule = plugin.plugin.editorModule; if (editorModule?.onEditorReady) { try { await editorModule.onEditorReady(); @@ -582,7 +921,7 @@ export class PluginManager implements IService { async notifyProjectOpen(projectPath: string): Promise { for (const [pluginId, plugin] of this.plugins) { if (!plugin.enabled) continue; - const editorModule = plugin.loader.editorModule; + const editorModule = plugin.plugin.editorModule; if (editorModule?.onProjectOpen) { try { await editorModule.onProjectOpen(projectPath); @@ -600,7 +939,7 @@ export class PluginManager implements IService { async notifyProjectClose(): Promise { for (const [pluginId, plugin] of this.plugins) { if (!plugin.enabled) continue; - const editorModule = plugin.loader.editorModule; + const editorModule = plugin.plugin.editorModule; if (editorModule?.onProjectClose) { try { await editorModule.onProjectClose(); @@ -618,7 +957,7 @@ export class PluginManager implements IService { notifySceneLoaded(scenePath: string): void { for (const [pluginId, plugin] of this.plugins) { if (!plugin.enabled) continue; - const editorModule = plugin.loader.editorModule; + const editorModule = plugin.plugin.editorModule; if (editorModule?.onSceneLoaded) { try { editorModule.onSceneLoaded(scenePath); @@ -636,7 +975,7 @@ export class PluginManager implements IService { notifySceneSaving(scenePath: string): boolean { for (const [pluginId, plugin] of this.plugins) { if (!plugin.enabled) continue; - const editorModule = plugin.loader.editorModule; + const editorModule = plugin.plugin.editorModule; if (editorModule?.onSceneSaving) { try { const result = editorModule.onSceneSaving(scenePath); @@ -658,7 +997,7 @@ export class PluginManager implements IService { setLocale(locale: string): void { for (const [pluginId, plugin] of this.plugins) { if (!plugin.enabled) continue; - const editorModule = plugin.loader.editorModule; + const editorModule = plugin.plugin.editorModule; if (editorModule?.setLocale) { try { editorModule.setLocale(locale); @@ -676,7 +1015,7 @@ export class PluginManager implements IService { exportConfig(): PluginConfig { const enabledPlugins: string[] = []; for (const [id, plugin] of this.plugins) { - if (plugin.enabled && !plugin.loader.descriptor.isCore) { + if (plugin.enabled && !plugin.plugin.descriptor.isCore) { enabledPlugins.push(id); } } @@ -684,22 +1023,48 @@ export class PluginManager implements IService { } /** - * 加载配置 - * Load configuration + * 加载配置并激活插件 + * Load configuration and activate plugins + * + * 此方法会: + * 1. 根据配置启用/禁用插件 + * 2. 激活新启用插件的编辑器模块 + * 3. 卸载新禁用插件的编辑器模块 */ - loadConfig(config: PluginConfig): void { + async loadConfig(config: PluginConfig): Promise { const { enabledPlugins } = config; + logger.info(`loadConfig called with: ${enabledPlugins.join(', ')}`); + + // 收集状态变化的插件 + const toEnable: string[] = []; + const toDisable: string[] = []; for (const [id, plugin] of this.plugins) { - if (plugin.loader.descriptor.isCore) { - plugin.enabled = true; - } else { - plugin.enabled = enabledPlugins.includes(id); - plugin.state = plugin.enabled ? 'loaded' : 'disabled'; + if (plugin.plugin.descriptor.isCore) { + continue; // 核心插件始终启用 + } + + const shouldBeEnabled = enabledPlugins.includes(id); + const wasEnabled = plugin.enabled; + + if (shouldBeEnabled && !wasEnabled) { + toEnable.push(id); + } else if (!shouldBeEnabled && wasEnabled) { + toDisable.push(id); } } - logger.info(`Loaded config: ${enabledPlugins.length} plugins enabled`); + // 禁用不再需要的插件 + for (const pluginId of toDisable) { + await this.disable(pluginId); + } + + // 启用新插件 + for (const pluginId of toEnable) { + await this.enable(pluginId); + } + + logger.info(`Config loaded and applied: ${toEnable.length} enabled, ${toDisable.length} disabled`); } /** @@ -718,12 +1083,12 @@ export class PluginManager implements IService { const pluginB = this.plugins.get(b); const moduleA = moduleType === 'runtime' - ? pluginA?.loader.descriptor.modules.find(m => m.type === 'runtime') - : pluginA?.loader.descriptor.modules.find(m => m.type === 'editor'); + ? pluginA?.plugin.descriptor.modules.find(m => m.type === 'runtime') + : pluginA?.plugin.descriptor.modules.find(m => m.type === 'editor'); const moduleB = moduleType === 'runtime' - ? pluginB?.loader.descriptor.modules.find(m => m.type === 'runtime') - : pluginB?.loader.descriptor.modules.find(m => m.type === 'editor'); + ? pluginB?.plugin.descriptor.modules.find(m => m.type === 'runtime') + : pluginB?.plugin.descriptor.modules.find(m => m.type === 'editor'); const phaseA = moduleA?.loadingPhase || 'default'; const phaseB = moduleB?.loadingPhase || 'default'; @@ -748,7 +1113,7 @@ export class PluginManager implements IService { const plugin = this.plugins.get(id); if (plugin) { - const deps = plugin.loader.descriptor.dependencies || []; + const deps = plugin.plugin.descriptor.dependencies || []; for (const dep of deps) { if (pluginIds.includes(dep.id)) { visit(dep.id); @@ -773,7 +1138,7 @@ export class PluginManager implements IService { clearSceneSystems(): void { for (const [pluginId, plugin] of this.plugins) { if (!plugin.enabled) continue; - const runtimeModule = plugin.loader.runtimeModule; + const runtimeModule = plugin.plugin.runtimeModule; if (runtimeModule?.onDestroy) { try { runtimeModule.onDestroy(); diff --git a/packages/editor-core/src/Services/AssetRegistryService.ts b/packages/editor-core/src/Services/AssetRegistryService.ts new file mode 100644 index 00000000..a9509ad8 --- /dev/null +++ b/packages/editor-core/src/Services/AssetRegistryService.ts @@ -0,0 +1,650 @@ +/** + * Asset Registry Service + * 资产注册表服务 + * + * 负责扫描项目资产目录,为每个资产生成唯一GUID, + * 并维护 GUID ↔ 路径 的映射关系。 + * + * Responsible for scanning project asset directories, + * generating unique GUIDs for each asset, and maintaining + * GUID ↔ path mappings. + */ + +import { Core } from '@esengine/ecs-framework'; +import { MessageHub } from './MessageHub'; + +// Simple logger for AssetRegistry +const logger = { + info: (msg: string, ...args: unknown[]) => console.log(`[AssetRegistry] ${msg}`, ...args), + warn: (msg: string, ...args: unknown[]) => console.warn(`[AssetRegistry] ${msg}`, ...args), + error: (msg: string, ...args: unknown[]) => console.error(`[AssetRegistry] ${msg}`, ...args), + debug: (msg: string, ...args: unknown[]) => console.debug(`[AssetRegistry] ${msg}`, ...args), +}; + +/** + * Asset GUID type (simplified, no dependency on asset-system) + */ +export type AssetGUID = string; + +/** + * Asset type for registry (using different name to avoid conflict) + */ +export type AssetRegistryType = string; + +/** + * Asset metadata (simplified) + */ +export interface IAssetRegistryMetadata { + guid: AssetGUID; + path: string; + type: AssetRegistryType; + name: string; + size: number; + hash: string; + lastModified: number; +} + +/** + * Asset catalog entry for export + */ +export interface IAssetRegistryCatalogEntry { + guid: AssetGUID; + path: string; + type: AssetRegistryType; + size: number; + hash: string; +} + +/** + * Asset file info from filesystem scan + */ +export interface AssetFileInfo { + /** Absolute path to the file */ + absolutePath: string; + /** Path relative to project root */ + relativePath: string; + /** File name without extension */ + name: string; + /** File extension (e.g., '.png', '.btree') */ + extension: string; + /** File size in bytes */ + size: number; + /** Last modified timestamp */ + lastModified: number; +} + +/** + * Asset registry manifest stored in project + * 存储在项目中的资产注册表清单 + */ +export interface AssetManifest { + version: string; + createdAt: number; + updatedAt: number; + assets: Record; +} + +/** + * Single asset entry in manifest + */ +export interface AssetManifestEntry { + guid: AssetGUID; + relativePath: string; + type: AssetRegistryType; + hash?: string; +} + +/** + * Extension to asset type mapping + */ +const EXTENSION_TYPE_MAP: Record = { + // Textures + '.png': 'texture', + '.jpg': 'texture', + '.jpeg': 'texture', + '.webp': 'texture', + '.gif': 'texture', + // Audio + '.mp3': 'audio', + '.ogg': 'audio', + '.wav': 'audio', + // Data + '.json': 'json', + '.txt': 'text', + // Custom types + '.btree': 'btree', + '.ecs': 'scene', + '.prefab': 'prefab', + '.tmx': 'tilemap', + '.tsx': 'tileset', +}; + +/** + * File system interface for asset scanning + */ +interface IFileSystem { + readDir(path: string): Promise; + readFile(path: string): Promise; + writeFile(path: string, content: string): Promise; + exists(path: string): Promise; + stat(path: string): Promise<{ size: number; mtime: number; isDirectory: boolean }>; + isDirectory(path: string): Promise; +} + +/** + * Simple in-memory asset database + */ +class SimpleAssetDatabase { + private readonly _metadata = new Map(); + private readonly _pathToGuid = new Map(); + private readonly _typeToGuids = new Map>(); + + addAsset(metadata: IAssetRegistryMetadata): void { + const { guid, path, type } = metadata; + this._metadata.set(guid, metadata); + this._pathToGuid.set(path, guid); + + if (!this._typeToGuids.has(type)) { + this._typeToGuids.set(type, new Set()); + } + this._typeToGuids.get(type)!.add(guid); + } + + removeAsset(guid: AssetGUID): void { + const metadata = this._metadata.get(guid); + if (!metadata) return; + + this._metadata.delete(guid); + this._pathToGuid.delete(metadata.path); + + const typeSet = this._typeToGuids.get(metadata.type); + if (typeSet) { + typeSet.delete(guid); + } + } + + getMetadata(guid: AssetGUID): IAssetRegistryMetadata | undefined { + return this._metadata.get(guid); + } + + getMetadataByPath(path: string): IAssetRegistryMetadata | undefined { + const guid = this._pathToGuid.get(path); + return guid ? this._metadata.get(guid) : undefined; + } + + findAssetsByType(type: AssetRegistryType): AssetGUID[] { + const guids = this._typeToGuids.get(type); + return guids ? Array.from(guids) : []; + } + + exportToCatalog(): IAssetRegistryCatalogEntry[] { + const entries: IAssetRegistryCatalogEntry[] = []; + this._metadata.forEach((metadata) => { + entries.push({ + guid: metadata.guid, + path: metadata.path, + type: metadata.type, + size: metadata.size, + hash: metadata.hash + }); + }); + return entries; + } + + getStatistics(): { totalAssets: number } { + return { totalAssets: this._metadata.size }; + } + + clear(): void { + this._metadata.clear(); + this._pathToGuid.clear(); + this._typeToGuids.clear(); + } +} + +/** + * Asset Registry Service + */ +export class AssetRegistryService { + private _database: SimpleAssetDatabase; + private _projectPath: string | null = null; + private _manifest: AssetManifest | null = null; + private _fileSystem: IFileSystem | null = null; + private _messageHub: MessageHub | null = null; + private _initialized = false; + + /** Manifest file name */ + static readonly MANIFEST_FILE = 'asset-manifest.json'; + /** Current manifest version */ + static readonly MANIFEST_VERSION = '1.0.0'; + + constructor() { + this._database = new SimpleAssetDatabase(); + } + + /** + * Initialize the service + */ + async initialize(): Promise { + if (this._initialized) return; + + // Get file system service + const IFileSystemServiceKey = Symbol.for('IFileSystemService'); + this._fileSystem = Core.services.tryResolve(IFileSystemServiceKey) as IFileSystem | null; + + // Get message hub + this._messageHub = Core.services.tryResolve(MessageHub) as MessageHub | null; + + // Subscribe to project events + if (this._messageHub) { + this._messageHub.subscribe('project:opened', this._onProjectOpened.bind(this)); + this._messageHub.subscribe('project:closed', this._onProjectClosed.bind(this)); + } + + this._initialized = true; + logger.info('AssetRegistryService initialized'); + } + + /** + * Handle project opened event + */ + private async _onProjectOpened(data: { path: string }): Promise { + await this.loadProject(data.path); + } + + /** + * Handle project closed event + */ + private _onProjectClosed(): void { + this.unloadProject(); + } + + /** + * Load project and scan assets + */ + async loadProject(projectPath: string): Promise { + if (!this._fileSystem) { + logger.warn('FileSystem service not available, skipping asset registry'); + return; + } + + this._projectPath = projectPath; + this._database.clear(); + + // Try to load existing manifest + await this._loadManifest(); + + // Scan assets directory + await this._scanAssetsDirectory(); + + // Save updated manifest + await this._saveManifest(); + + logger.info(`Project assets loaded: ${this._database.getStatistics().totalAssets} assets`); + + // Publish event + this._messageHub?.publish('assets:registry:loaded', { + projectPath, + assetCount: this._database.getStatistics().totalAssets + }); + } + + /** + * Unload current project + */ + unloadProject(): void { + this._projectPath = null; + this._manifest = null; + this._database.clear(); + logger.info('Project assets unloaded'); + } + + /** + * Load manifest from project + */ + private async _loadManifest(): Promise { + if (!this._fileSystem || !this._projectPath) return; + + const manifestPath = this._getManifestPath(); + + try { + const exists = await this._fileSystem.exists(manifestPath); + if (exists) { + const content = await this._fileSystem.readFile(manifestPath); + this._manifest = JSON.parse(content); + logger.debug('Loaded existing asset manifest'); + } else { + this._manifest = this._createEmptyManifest(); + logger.debug('Created new asset manifest'); + } + } catch (error) { + logger.warn('Failed to load manifest, creating new one:', error); + this._manifest = this._createEmptyManifest(); + } + } + + /** + * Save manifest to project + */ + private async _saveManifest(): Promise { + if (!this._fileSystem || !this._projectPath || !this._manifest) return; + + const manifestPath = this._getManifestPath(); + this._manifest.updatedAt = Date.now(); + + try { + const content = JSON.stringify(this._manifest, null, 2); + await this._fileSystem.writeFile(manifestPath, content); + logger.debug('Saved asset manifest'); + } catch (error) { + logger.error('Failed to save manifest:', error); + } + } + + /** + * Get manifest file path + */ + private _getManifestPath(): string { + const sep = this._projectPath!.includes('\\') ? '\\' : '/'; + return `${this._projectPath}${sep}${AssetRegistryService.MANIFEST_FILE}`; + } + + /** + * Create empty manifest + */ + private _createEmptyManifest(): AssetManifest { + return { + version: AssetRegistryService.MANIFEST_VERSION, + createdAt: Date.now(), + updatedAt: Date.now(), + assets: {} + }; + } + + /** + * Scan assets directory and register all assets + */ + private async _scanAssetsDirectory(): Promise { + if (!this._fileSystem || !this._projectPath) return; + + const sep = this._projectPath.includes('\\') ? '\\' : '/'; + const assetsPath = `${this._projectPath}${sep}assets`; + + try { + const exists = await this._fileSystem.exists(assetsPath); + if (!exists) { + logger.info('No assets directory found'); + return; + } + + await this._scanDirectory(assetsPath, 'assets'); + } catch (error) { + logger.error('Failed to scan assets directory:', error); + } + } + + /** + * Recursively scan a directory + */ + private async _scanDirectory(absolutePath: string, relativePath: string): Promise { + if (!this._fileSystem) return; + + try { + const entries = await this._fileSystem.readDir(absolutePath); + const sep = absolutePath.includes('\\') ? '\\' : '/'; + + for (const entry of entries) { + const entryAbsPath = `${absolutePath}${sep}${entry}`; + const entryRelPath = `${relativePath}/${entry}`; + + try { + const isDir = await this._fileSystem.isDirectory(entryAbsPath); + + if (isDir) { + // Recursively scan subdirectory + await this._scanDirectory(entryAbsPath, entryRelPath); + } else { + // Register file as asset + await this._registerAssetFile(entryAbsPath, entryRelPath); + } + } catch (error) { + logger.warn(`Failed to process entry ${entry}:`, error); + } + } + } catch (error) { + logger.warn(`Failed to read directory ${absolutePath}:`, error); + } + } + + /** + * Register a single asset file + */ + private async _registerAssetFile(absolutePath: string, relativePath: string): Promise { + if (!this._fileSystem || !this._manifest) return; + + // Get file extension + const lastDot = relativePath.lastIndexOf('.'); + if (lastDot === -1) return; // Skip files without extension + + const extension = relativePath.substring(lastDot).toLowerCase(); + const assetType = EXTENSION_TYPE_MAP[extension]; + + // Skip unknown file types + if (!assetType) return; + + // Get file info + let stat: { size: number; mtime: number }; + try { + stat = await this._fileSystem.stat(absolutePath); + } catch { + return; + } + + // Check if already in manifest + let guid: AssetGUID; + const existingEntry = this._manifest.assets[relativePath]; + + if (existingEntry) { + guid = existingEntry.guid; + } else { + // Generate new GUID + guid = this._generateGUID(); + this._manifest.assets[relativePath] = { + guid, + relativePath, + type: assetType + }; + } + + // Get file name + const lastSlash = relativePath.lastIndexOf('/'); + const fileName = lastSlash >= 0 ? relativePath.substring(lastSlash + 1) : relativePath; + const name = fileName.substring(0, fileName.lastIndexOf('.')); + + // Create metadata + const metadata: IAssetRegistryMetadata = { + guid, + path: relativePath, + type: assetType, + name, + size: stat.size, + hash: '', // Could compute hash if needed + lastModified: stat.mtime + }; + + // Register in database + this._database.addAsset(metadata); + } + + /** + * Generate a unique GUID + */ + private _generateGUID(): AssetGUID { + // Simple UUID v4 generation + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + } + + // ==================== Public API ==================== + + /** + * Get asset metadata by GUID + */ + getAsset(guid: AssetGUID): IAssetRegistryMetadata | undefined { + return this._database.getMetadata(guid); + } + + /** + * Get asset metadata by relative path + */ + getAssetByPath(relativePath: string): IAssetRegistryMetadata | undefined { + return this._database.getMetadataByPath(relativePath); + } + + /** + * Get GUID for a relative path + */ + getGuidByPath(relativePath: string): AssetGUID | undefined { + const metadata = this._database.getMetadataByPath(relativePath); + return metadata?.guid; + } + + /** + * Get relative path for a GUID + */ + getPathByGuid(guid: AssetGUID): string | undefined { + const metadata = this._database.getMetadata(guid); + return metadata?.path; + } + + /** + * Convert absolute path to relative path + */ + absoluteToRelative(absolutePath: string): string | null { + if (!this._projectPath) return null; + + const normalizedAbs = absolutePath.replace(/\\/g, '/'); + const normalizedProject = this._projectPath.replace(/\\/g, '/'); + + if (normalizedAbs.startsWith(normalizedProject)) { + return normalizedAbs.substring(normalizedProject.length + 1); + } + + return null; + } + + /** + * Convert relative path to absolute path + */ + relativeToAbsolute(relativePath: string): string | null { + if (!this._projectPath) return null; + + const sep = this._projectPath.includes('\\') ? '\\' : '/'; + return `${this._projectPath}${sep}${relativePath.replace(/\//g, sep)}`; + } + + /** + * Find assets by type + */ + findAssetsByType(type: AssetRegistryType): IAssetRegistryMetadata[] { + const guids = this._database.findAssetsByType(type); + return guids + .map(guid => this._database.getMetadata(guid)) + .filter((m): m is IAssetRegistryMetadata => m !== undefined); + } + + /** + * Get all registered assets + */ + getAllAssets(): IAssetRegistryMetadata[] { + const entries = this._database.exportToCatalog(); + return entries.map(entry => this._database.getMetadata(entry.guid)) + .filter((m): m is IAssetRegistryMetadata => m !== undefined); + } + + /** + * Export catalog for runtime use + * 导出运行时使用的资产目录 + */ + exportCatalog(): IAssetRegistryCatalogEntry[] { + return this._database.exportToCatalog(); + } + + /** + * Export catalog as JSON string + */ + exportCatalogJSON(): string { + const entries = this._database.exportToCatalog(); + const catalog = { + version: '1.0.0', + createdAt: Date.now(), + entries: Object.fromEntries(entries.map(e => [e.guid, e])) + }; + return JSON.stringify(catalog, null, 2); + } + + /** + * Register a new asset (e.g., when a file is created) + */ + async registerAsset(absolutePath: string): Promise { + const relativePath = this.absoluteToRelative(absolutePath); + if (!relativePath) return null; + + await this._registerAssetFile(absolutePath, relativePath); + await this._saveManifest(); + + const metadata = this._database.getMetadataByPath(relativePath); + return metadata?.guid ?? null; + } + + /** + * Unregister an asset (e.g., when a file is deleted) + */ + async unregisterAsset(absolutePath: string): Promise { + const relativePath = this.absoluteToRelative(absolutePath); + if (!relativePath || !this._manifest) return; + + const metadata = this._database.getMetadataByPath(relativePath); + if (metadata) { + this._database.removeAsset(metadata.guid); + delete this._manifest.assets[relativePath]; + await this._saveManifest(); + } + } + + /** + * Refresh a single asset (e.g., when file is modified) + */ + async refreshAsset(absolutePath: string): Promise { + const relativePath = this.absoluteToRelative(absolutePath); + if (!relativePath) return; + + // Re-register the asset + await this._registerAssetFile(absolutePath, relativePath); + await this._saveManifest(); + } + + /** + * Get database statistics + */ + getStatistics() { + return this._database.getStatistics(); + } + + /** + * Check if service is ready + */ + get isReady(): boolean { + return this._initialized && this._projectPath !== null; + } + + /** + * Get current project path + */ + get projectPath(): string | null { + return this._projectPath; + } +} diff --git a/packages/editor-core/src/Services/EntityStoreService.ts b/packages/editor-core/src/Services/EntityStoreService.ts index 1ac7898a..2e0dc2ea 100644 --- a/packages/editor-core/src/Services/EntityStoreService.ts +++ b/packages/editor-core/src/Services/EntityStoreService.ts @@ -1,4 +1,4 @@ -import { Injectable, IService, Entity, Core } from '@esengine/ecs-framework'; +import { Injectable, IService, Entity, Core, HierarchyComponent } from '@esengine/ecs-framework'; import { MessageHub } from './MessageHub'; export interface EntityTreeNode { @@ -68,6 +68,10 @@ export class EntityStoreService implements IService { .filter((e): e is Entity => e !== undefined); } + public getRootEntityIds(): number[] { + return [...this.rootEntityIds]; + } + public getEntity(id: number): Entity | undefined { return this.entities.get(id); } @@ -88,7 +92,9 @@ export class EntityStoreService implements IService { scene.entities.forEach((entity) => { this.entities.set(entity.id, entity); - if (!entity.parent) { + const hierarchy = entity.getComponent(HierarchyComponent); + const bHasNoParent = hierarchy?.parentId === null || hierarchy?.parentId === undefined; + if (bHasNoParent) { this.rootEntityIds.push(entity.id); } }); diff --git a/packages/editor-core/src/Services/FileActionRegistry.ts b/packages/editor-core/src/Services/FileActionRegistry.ts index 73ff697f..9eede3ec 100644 --- a/packages/editor-core/src/Services/FileActionRegistry.ts +++ b/packages/editor-core/src/Services/FileActionRegistry.ts @@ -19,6 +19,12 @@ export interface AssetCreationMapping { canCreate?: boolean; } +/** + * FileActionRegistry 服务标识符 + * FileActionRegistry service identifier + */ +export const IFileActionRegistry = Symbol.for('IFileActionRegistry'); + /** * 文件操作注册表服务 * diff --git a/packages/editor-core/src/Services/LogService.ts b/packages/editor-core/src/Services/LogService.ts index 56c54368..ddd64041 100644 --- a/packages/editor-core/src/Services/LogService.ts +++ b/packages/editor-core/src/Services/LogService.ts @@ -8,6 +8,7 @@ export interface LogEntry { source: string; message: string; args: unknown[]; + stack?: string; // 调用堆栈 clientId?: string; // 远程客户端ID } @@ -59,12 +60,12 @@ export class LogService implements IService { }; console.warn = (...args: unknown[]) => { - this.addLog(LogLevel.Warn, 'console', this.formatMessage(args), args); + this.addLog(LogLevel.Warn, 'console', this.formatMessage(args), args, true); this.originalConsole.warn(...args); }; console.error = (...args: unknown[]) => { - this.addLog(LogLevel.Error, 'console', this.formatMessage(args), args); + this.addLog(LogLevel.Error, 'console', this.formatMessage(args), args, true); this.originalConsole.error(...args); }; @@ -93,7 +94,10 @@ export class LogService implements IService { private formatMessage(args: unknown[]): string { return args.map((arg) => { if (typeof arg === 'string') return arg; - if (arg instanceof Error) return arg.message; + if (arg instanceof Error) { + // 包含错误消息和堆栈 + return arg.stack || arg.message; + } try { return JSON.stringify(arg); } catch { @@ -102,17 +106,30 @@ export class LogService implements IService { }).join(' '); } + /** + * 捕获当前调用堆栈 + */ + private captureStack(): string { + const stack = new Error().stack; + if (!stack) return ''; + + // 移除前几行(Error、captureStack、addLog、console.xxx) + const lines = stack.split('\n'); + return lines.slice(4).join('\n'); + } + /** * 添加日志 */ - private addLog(level: LogLevel, source: string, message: string, args: unknown[]): void { + private addLog(level: LogLevel, source: string, message: string, args: unknown[], includeStack = false): void { const entry: LogEntry = { id: this.nextId++, timestamp: new Date(), level, source, message, - args + args, + stack: includeStack ? this.captureStack() : undefined }; this.logs.push(entry); diff --git a/packages/editor-core/src/Services/ProjectService.ts b/packages/editor-core/src/Services/ProjectService.ts index 57212724..e2b124ff 100644 --- a/packages/editor-core/src/Services/ProjectService.ts +++ b/packages/editor-core/src/Services/ProjectService.ts @@ -26,6 +26,15 @@ export interface UIDesignResolution { height: number; } +/** + * 插件配置 + * Plugin Configuration + */ +export interface PluginSettings { + /** 启用的插件 ID 列表 / Enabled plugin IDs */ + enabledPlugins: string[]; +} + export interface ProjectConfig { projectType?: ProjectType; componentsPath?: string; @@ -35,6 +44,8 @@ export interface ProjectConfig { defaultScene?: string; /** UI 设计分辨率 / UI design resolution */ uiDesignResolution?: UIDesignResolution; + /** 插件配置 / Plugin settings */ + plugins?: PluginSettings; } @Injectable() @@ -200,16 +211,21 @@ export class ProjectService implements IService { private async loadConfig(configPath: string): Promise { try { const content = await this.fileAPI.readFileContent(configPath); + logger.debug('Raw config content:', content); const config = JSON.parse(content) as ProjectConfig; - return { + logger.debug('Parsed config plugins:', config.plugins); + const result = { projectType: config.projectType || 'esengine', componentsPath: config.componentsPath || '', componentPattern: config.componentPattern || '**/*.ts', buildOutput: config.buildOutput || 'temp/editor-components', scenesPath: config.scenesPath || 'scenes', defaultScene: config.defaultScene || 'main.ecs', - uiDesignResolution: config.uiDesignResolution + uiDesignResolution: config.uiDesignResolution, + plugins: config.plugins }; + logger.debug('Loaded config result:', result); + return result; } catch (error) { logger.warn('Failed to load config, using defaults', error); return { @@ -280,6 +296,60 @@ export class ProjectService implements IService { await this.updateConfig({ uiDesignResolution: resolution }); } + /** + * 获取启用的插件列表 + * Get enabled plugins list + */ + public getEnabledPlugins(): string[] { + return this.projectConfig?.plugins?.enabledPlugins || []; + } + + /** + * 获取插件配置 + * Get plugin settings + */ + public getPluginSettings(): PluginSettings | null { + logger.debug('getPluginSettings called, projectConfig:', this.projectConfig); + logger.debug('getPluginSettings plugins:', this.projectConfig?.plugins); + return this.projectConfig?.plugins || null; + } + + /** + * 设置启用的插件列表 + * Set enabled plugins list + * + * @param enabledPlugins - Array of enabled plugin IDs + */ + public async setEnabledPlugins(enabledPlugins: string[]): Promise { + await this.updateConfig({ + plugins: { + enabledPlugins + } + }); + await this.messageHub.publish('project:pluginsChanged', { enabledPlugins }); + logger.info('Plugin settings saved', { count: enabledPlugins.length }); + } + + /** + * 启用插件 + * Enable a plugin + */ + public async enablePlugin(pluginId: string): Promise { + const current = this.getEnabledPlugins(); + if (!current.includes(pluginId)) { + await this.setEnabledPlugins([...current, pluginId]); + } + } + + /** + * 禁用插件 + * Disable a plugin + */ + public async disablePlugin(pluginId: string): Promise { + const current = this.getEnabledPlugins(); + await this.setEnabledPlugins(current.filter(id => id !== pluginId)); + } + public dispose(): void { this.currentProject = null; this.projectConfig = null; diff --git a/packages/editor-core/src/Services/PropertyMetadata.ts b/packages/editor-core/src/Services/PropertyMetadata.ts index 17e3ac07..c9ff87af 100644 --- a/packages/editor-core/src/Services/PropertyMetadata.ts +++ b/packages/editor-core/src/Services/PropertyMetadata.ts @@ -4,6 +4,14 @@ import { createLogger } from '@esengine/ecs-framework'; const logger = createLogger('PropertyMetadata'); +/** + * 不需要在 Inspector 中显示的内部组件类型 + * 这些组件不使用 @Property 装饰器,因为它们的属性不应该被手动编辑 + */ +const INTERNAL_COMPONENTS = new Set([ + 'HierarchyComponent' +]); + export type { PropertyOptions, PropertyAction, PropertyControl, AssetType, EnumOption }; export type PropertyMetadata = PropertyOptions; export type PropertyType = 'number' | 'integer' | 'string' | 'boolean' | 'color' | 'vector2' | 'vector3' | 'enum' | 'asset' | 'animationClips'; @@ -53,7 +61,10 @@ export class PropertyMetadataService implements IService { } // 没有元数据时返回空对象 - logger.warn(`No property metadata found for component: ${component.constructor.name}`); + // 内部组件(如 HierarchyComponent)不需要警告 + if (!INTERNAL_COMPONENTS.has(component.constructor.name)) { + logger.warn(`No property metadata found for component: ${component.constructor.name}`); + } return {}; } diff --git a/packages/editor-core/src/Services/SettingsRegistry.ts b/packages/editor-core/src/Services/SettingsRegistry.ts index ccdd063b..9a5b1fef 100644 --- a/packages/editor-core/src/Services/SettingsRegistry.ts +++ b/packages/editor-core/src/Services/SettingsRegistry.ts @@ -55,6 +55,7 @@ export class SettingsRegistry implements IService { if (this.categories.has(category.id)) { console.warn(`[SettingsRegistry] Category ${category.id} already registered, overwriting`); } + console.log(`[SettingsRegistry] Registering category: ${category.id} (${category.title}), sections: ${category.sections.map(s => s.id).join(', ')}`); this.categories.set(category.id, category); } diff --git a/packages/editor-core/src/index.ts b/packages/editor-core/src/index.ts index f431c4d8..4765d2e0 100644 --- a/packages/editor-core/src/index.ts +++ b/packages/editor-core/src/index.ts @@ -39,6 +39,7 @@ export * from './Services/IFieldEditor'; export * from './Services/FieldEditorRegistry'; export * from './Services/ComponentInspectorRegistry'; export * from './Services/ComponentActionRegistry'; +export * from './Services/AssetRegistryService'; export * from './Gizmos'; diff --git a/packages/editor-runtime/package.json b/packages/editor-runtime/package.json index b9f70fd0..99f671ba 100644 --- a/packages/editor-runtime/package.json +++ b/packages/editor-runtime/package.json @@ -26,18 +26,16 @@ "tsyringe": "^4.10.0", "reflect-metadata": "^0.2.2" }, - "peerDependencies": { + "devDependencies": { "@esengine/ecs-framework": "workspace:*", "@esengine/editor-core": "workspace:*", "@esengine/ui": "workspace:*", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", "react": "^18.3.1", "react-dom": "^18.3.1", "zustand": "^5.0.8", - "lucide-react": "^0.545.0" - }, - "devDependencies": { - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", + "lucide-react": "^0.545.0", "rimraf": "^5.0.0", "typescript": "^5.8.3", "vite": "^6.0.7", diff --git a/packages/editor-runtime/src/index.ts b/packages/editor-runtime/src/index.ts index 1473af6e..5b0a49ee 100644 --- a/packages/editor-runtime/src/index.ts +++ b/packages/editor-runtime/src/index.ts @@ -135,6 +135,7 @@ export { IMessageHub, ICompilerRegistry, IInspectorRegistry, + IFileActionRegistry, } from '@esengine/editor-core'; // Type-only exports from editor-core diff --git a/packages/engine-core/package.json b/packages/engine-core/package.json new file mode 100644 index 00000000..ea1a87e3 --- /dev/null +++ b/packages/engine-core/package.json @@ -0,0 +1,44 @@ +{ + "name": "@esengine/engine-core", + "version": "1.0.0", + "description": "Engine core components - Transform, etc.", + "esengine": { + "plugin": true, + "pluginExport": "EnginePlugin", + "category": "core", + "isEnginePlugin": true + }, + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/build-config": "workspace:*", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "engine", + "transform" + ], + "author": "yhh", + "license": "MIT" +} diff --git a/packages/engine-core/src/EnginePlugin.ts b/packages/engine-core/src/EnginePlugin.ts new file mode 100644 index 00000000..72aee990 --- /dev/null +++ b/packages/engine-core/src/EnginePlugin.ts @@ -0,0 +1,227 @@ +/** + * 插件系统核心类型定义 + * Plugin system core type definitions + * + * 这是插件类型的唯一定义源,editor-core 重新导出并扩展这些类型。 + * This is the single source of truth for plugin types. editor-core re-exports and extends them. + */ + +import type { ComponentRegistry as ComponentRegistryType, IScene, ServiceContainer } from '@esengine/ecs-framework'; +import { TransformComponent } from './TransformComponent'; + +// ============================================================================ +// 基础类型 | Basic Types +// ============================================================================ + +/** + * 插件类别 + * Plugin category + */ +export type PluginCategory = + | 'core' // 核心功能 | Core functionality + | 'rendering' // 渲染相关 | Rendering + | 'ui' // UI 系统 | UI System + | 'ai' // AI/行为树 | AI/Behavior + | 'physics' // 物理引擎 | Physics + | 'audio' // 音频系统 | Audio + | 'networking' // 网络功能 | Networking + | 'tools' // 工具/编辑器扩展 | Tools/Editor extensions + | 'scripting' // 脚本/蓝图 | Scripting/Blueprint + | 'tilemap' // 瓦片地图 | Tilemap + | 'content'; // 内容/资源 | Content/Assets + +/** + * 加载阶段 - 控制插件模块的加载顺序 + * Loading phase - controls the loading order of plugin modules + */ +export type LoadingPhase = + | 'earliest' // 最早加载(核心模块) | Earliest (core modules) + | 'preDefault' // 默认之前 | Before default + | 'default' // 默认阶段 | Default phase + | 'postDefault' // 默认之后 | After default + | 'postEngine'; // 引擎初始化后 | After engine init + +/** + * 模块类型 + * Module type + */ +export type ModuleType = 'runtime' | 'editor'; + +/** + * 模块描述符 + * Module descriptor + */ +export interface ModuleDescriptor { + /** 模块名称 | Module name */ + name: string; + /** 模块类型 | Module type */ + type: ModuleType; + /** 加载阶段 | Loading phase */ + loadingPhase?: LoadingPhase; +} + +/** + * 插件依赖 + * Plugin dependency + */ +export interface PluginDependency { + /** 依赖的插件ID | Dependent plugin ID */ + id: string; + /** 版本要求 | Version requirement */ + version?: string; + /** 是否可选 | Optional */ + optional?: boolean; +} + +// ============================================================================ +// 插件描述符 | Plugin Descriptor +// ============================================================================ + +/** + * 插件描述符 + * Plugin descriptor + * + * 所有字段都是可选的,PluginManager 会填充默认值。 + * All fields are optional, PluginManager will fill in defaults. + */ +export interface PluginDescriptor { + /** 插件唯一标识符 | Unique plugin ID */ + id: string; + /** 显示名称 | Display name */ + name: string; + /** 版本号 | Version */ + version: string; + /** 描述 | Description */ + description?: string; + /** 插件类别 | Plugin category */ + category?: PluginCategory; + /** 标签(用于搜索) | Tags (for search) */ + tags?: string[]; + /** 图标(Lucide 图标名) | Icon (Lucide icon name) */ + icon?: string; + /** 是否默认启用 | Enabled by default */ + enabledByDefault?: boolean; + /** 是否可以包含内容资产 | Can contain content assets */ + canContainContent?: boolean; + /** 是否为引擎内置插件 | Is engine built-in plugin */ + isEnginePlugin?: boolean; + /** 是否为核心插件(不可禁用) | Is core plugin (cannot be disabled) */ + isCore?: boolean; + /** 模块列表 | Module list */ + modules?: ModuleDescriptor[]; + /** 依赖列表 | Dependency list */ + dependencies?: PluginDependency[]; + /** 平台要求 | Platform requirements */ + platforms?: ('web' | 'desktop' | 'mobile')[]; +} + +// ============================================================================ +// 系统上下文 | System Context +// ============================================================================ + +/** + * 系统创建上下文 + * System creation context + */ +export interface SystemContext { + /** 是否为编辑器模式 | Is editor mode */ + isEditor: boolean; + /** 引擎桥接(如有) | Engine bridge (if available) */ + engineBridge?: any; + /** 渲染系统(如有) | Render system (if available) */ + renderSystem?: any; + /** 其他已创建的系统引用 | Other created system references */ + [key: string]: any; +} + +// ============================================================================ +// 运行时模块 | Runtime Module +// ============================================================================ + +/** + * 运行时模块接口 + * Runtime module interface + */ +export interface IRuntimeModule { + /** + * 注册组件到 ComponentRegistry + * Register components to ComponentRegistry + */ + registerComponents?(registry: typeof ComponentRegistryType): void; + + /** + * 注册服务到 ServiceContainer + * Register services to ServiceContainer + */ + registerServices?(services: ServiceContainer): void; + + /** + * 为场景创建系统 + * Create systems for scene + */ + createSystems?(scene: IScene, context: SystemContext): void; + + /** + * 所有系统创建完成后调用 + * Called after all systems are created + */ + onSystemsCreated?(scene: IScene, context: SystemContext): void; + + /** + * 模块初始化完成回调 + * Module initialization complete callback + */ + onInitialize?(): Promise; + + /** + * 模块销毁回调 + * Module destroy callback + */ + onDestroy?(): void; +} + +// ============================================================================ +// 插件接口 | Plugin Interface +// ============================================================================ + +/** + * 插件接口 + * Plugin interface + * + * 这是所有插件包导出的统一类型。 + * This is the unified type that all plugin packages export. + */ +export interface IPlugin { + /** 插件描述符 | Plugin descriptor */ + readonly descriptor: PluginDescriptor; + /** 运行时模块(可选) | Runtime module (optional) */ + readonly runtimeModule?: IRuntimeModule; + /** 编辑器模块(可选,类型为 any 以避免循环依赖) | Editor module (optional, typed as any to avoid circular deps) */ + readonly editorModule?: any; +} + +// ============================================================================ +// Engine Core 插件 | Engine Core Plugin +// ============================================================================ + +class EngineRuntimeModule implements IRuntimeModule { + registerComponents(registry: typeof ComponentRegistryType): void { + registry.register(TransformComponent); + } +} + +const descriptor: PluginDescriptor = { + id: '@esengine/engine-core', + name: 'Engine Core', + version: '1.0.0', + description: 'Transform 等核心组件', + category: 'core', + enabledByDefault: true, + isEnginePlugin: true, + isCore: true +}; + +export const EnginePlugin: IPlugin = { + descriptor, + runtimeModule: new EngineRuntimeModule() +}; diff --git a/packages/engine-core/src/HierarchyComponent.ts b/packages/engine-core/src/HierarchyComponent.ts new file mode 100644 index 00000000..0b37f509 --- /dev/null +++ b/packages/engine-core/src/HierarchyComponent.ts @@ -0,0 +1,2 @@ +// Re-export from ecs-framework +export { HierarchyComponent } from '@esengine/ecs-framework'; diff --git a/packages/engine-core/src/HierarchySystem.ts b/packages/engine-core/src/HierarchySystem.ts new file mode 100644 index 00000000..e16f983a --- /dev/null +++ b/packages/engine-core/src/HierarchySystem.ts @@ -0,0 +1,2 @@ +// Re-export from ecs-framework +export { HierarchySystem } from '@esengine/ecs-framework'; diff --git a/packages/engine-core/src/TransformComponent.ts b/packages/engine-core/src/TransformComponent.ts new file mode 100644 index 00000000..bdc8e7a4 --- /dev/null +++ b/packages/engine-core/src/TransformComponent.ts @@ -0,0 +1,110 @@ +import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework'; + +export interface Vector3 { + x: number; + y: number; + z: number; +} + +/** + * 3x3 矩阵(用于 2D 变换:旋转 + 缩放) + * 实际存储为 [a, b, c, d, tx, ty] 形式的仿射变换 + */ +export interface Matrix2D { + a: number; // scaleX * cos(rotation) + b: number; // scaleX * sin(rotation) + c: number; // scaleY * -sin(rotation) + d: number; // scaleY * cos(rotation) + tx: number; // translateX + ty: number; // translateY +} + +@ECSComponent('Transform') +@Serializable({ version: 1, typeId: 'Transform' }) +export class TransformComponent extends Component { + @Serialize() + @Property({ type: 'vector3', label: 'Position' }) + position: Vector3 = { x: 0, y: 0, z: 0 }; + + /** 欧拉角,单位:度 */ + @Serialize() + @Property({ type: 'vector3', label: 'Rotation' }) + rotation: Vector3 = { x: 0, y: 0, z: 0 }; + + @Serialize() + @Property({ type: 'vector3', label: 'Scale' }) + scale: Vector3 = { x: 1, y: 1, z: 1 }; + + // ===== 世界变换(由 TransformSystem 计算)===== + + /** 世界位置(只读,由 TransformSystem 计算) */ + worldPosition: Vector3 = { x: 0, y: 0, z: 0 }; + + /** 世界旋转(只读,由 TransformSystem 计算) */ + worldRotation: Vector3 = { x: 0, y: 0, z: 0 }; + + /** 世界缩放(只读,由 TransformSystem 计算) */ + worldScale: Vector3 = { x: 1, y: 1, z: 1 }; + + /** 本地到世界的 2D 变换矩阵(只读,由 TransformSystem 计算) */ + localToWorldMatrix: Matrix2D = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }; + + /** 变换是否需要更新 */ + bDirty: boolean = true; + + constructor(x: number = 0, y: number = 0, z: number = 0) { + super(); + this.position = { x, y, z }; + // 初始化世界变换为本地变换值(在 TransformSystem 更新前使用) + this.worldPosition = { x, y, z }; + } + + setPosition(x: number, y: number, z: number = 0): this { + this.position = { x, y, z }; + this.bDirty = true; + return this; + } + + setRotation(x: number, y: number, z: number): this { + this.rotation = { x, y, z }; + this.bDirty = true; + return this; + } + + setScale(x: number, y: number, z: number = 1): this { + this.scale = { x, y, z }; + this.bDirty = true; + return this; + } + + /** + * 将本地坐标转换为世界坐标 + */ + localToWorld(localX: number, localY: number): { x: number; y: number } { + const m = this.localToWorldMatrix; + return { + x: m.a * localX + m.c * localY + m.tx, + y: m.b * localX + m.d * localY + m.ty + }; + } + + /** + * 将世界坐标转换为本地坐标 + */ + worldToLocal(worldX: number, worldY: number): { x: number; y: number } { + const m = this.localToWorldMatrix; + const det = m.a * m.d - m.b * m.c; + if (Math.abs(det) < 1e-10) { + return { x: 0, y: 0 }; + } + + const invDet = 1 / det; + const dx = worldX - m.tx; + const dy = worldY - m.ty; + + return { + x: (m.d * dx - m.c * dy) * invDet, + y: (-m.b * dx + m.a * dy) * invDet + }; + } +} diff --git a/packages/engine-core/src/TransformSystem.ts b/packages/engine-core/src/TransformSystem.ts new file mode 100644 index 00000000..fde4f06e --- /dev/null +++ b/packages/engine-core/src/TransformSystem.ts @@ -0,0 +1,145 @@ +import { EntitySystem, Matcher, Entity, ECSSystem, HierarchyComponent } from '@esengine/ecs-framework'; +import { TransformComponent, Matrix2D } from './TransformComponent'; + +const DEG_TO_RAD = Math.PI / 180; + +/** + * 变换系统 + * Transform System - Calculates world transforms based on hierarchy + * + * 根据实体层级关系计算世界变换矩阵。 + * 子实体的世界变换 = 父实体世界变换 * 子实体本地变换 + */ +@ECSSystem('Transform', { updateOrder: -100 }) +export class TransformSystem extends EntitySystem { + constructor() { + super(Matcher.empty().all(TransformComponent)); + } + + protected override process(entities: readonly Entity[]): void { + // 获取所有根实体(没有父级或父级没有 TransformComponent) + const rootEntities = entities.filter(e => { + const hierarchy = e.getComponent(HierarchyComponent); + if (!hierarchy || hierarchy.parentId === null) { + return true; + } + const parent = this.scene?.findEntityById(hierarchy.parentId); + return !parent || !parent.hasComponent(TransformComponent); + }); + + // 从根实体开始递归计算世界变换 + for (const entity of rootEntities) { + this.updateWorldTransform(entity, null); + } + } + + /** + * 递归更新实体及其子实体的世界变换 + */ + private updateWorldTransform(entity: Entity, parentMatrix: Matrix2D | null): void { + const transform = entity.getComponent(TransformComponent); + if (!transform) return; + + // 计算本地变换矩阵 + const localMatrix = this.calculateLocalMatrix(transform); + + // 计算世界变换矩阵 + if (parentMatrix) { + // 世界矩阵 = 父矩阵 * 本地矩阵 + transform.localToWorldMatrix = this.multiplyMatrices(parentMatrix, localMatrix); + } else { + // 没有父级,本地矩阵就是世界矩阵 + transform.localToWorldMatrix = localMatrix; + } + + // 从世界矩阵提取世界位置、旋转、缩放 + this.decomposeMatrix(transform); + + transform.bDirty = false; + + // 递归处理子实体 + const hierarchy = entity.getComponent(HierarchyComponent); + if (hierarchy && hierarchy.childIds.length > 0) { + for (const childId of hierarchy.childIds) { + const child = this.scene?.findEntityById(childId); + if (child) { + this.updateWorldTransform(child, transform.localToWorldMatrix); + } + } + } + } + + /** + * 计算本地变换矩阵 + */ + private calculateLocalMatrix(transform: TransformComponent): Matrix2D { + const { position, rotation, scale } = transform; + + // 只使用 z 轴旋转(2D) + const rad = rotation.z * DEG_TO_RAD; + const cos = Math.cos(rad); + const sin = Math.sin(rad); + + // 构建仿射变换矩阵: Scale -> Rotate -> Translate + // [a c tx] [sx 0 0] [cos -sin 0] [1 0 tx] + // [b d ty] = [0 sy 0] * [sin cos 0] * [0 1 ty] + // [0 0 1] [0 0 1] [0 0 1] [0 0 1] + + return { + a: scale.x * cos, + b: scale.x * sin, + c: scale.y * -sin, + d: scale.y * cos, + tx: position.x, + ty: position.y + }; + } + + /** + * 矩阵乘法: result = a * b + */ + private multiplyMatrices(a: Matrix2D, b: Matrix2D): Matrix2D { + return { + a: a.a * b.a + a.c * b.b, + b: a.b * b.a + a.d * b.b, + c: a.a * b.c + a.c * b.d, + d: a.b * b.c + a.d * b.d, + tx: a.a * b.tx + a.c * b.ty + a.tx, + ty: a.b * b.tx + a.d * b.ty + a.ty + }; + } + + /** + * 从世界矩阵分解出位置、旋转、缩放 + */ + private decomposeMatrix(transform: TransformComponent): void { + const m = transform.localToWorldMatrix; + + // 位置直接从矩阵获取 + transform.worldPosition.x = m.tx; + transform.worldPosition.y = m.ty; + transform.worldPosition.z = transform.position.z; + + // 计算缩放 + const scaleX = Math.sqrt(m.a * m.a + m.b * m.b); + const scaleY = Math.sqrt(m.c * m.c + m.d * m.d); + + // 检测负缩放(通过行列式符号) + const det = m.a * m.d - m.b * m.c; + const sign = det < 0 ? -1 : 1; + + transform.worldScale.x = scaleX; + transform.worldScale.y = scaleY * sign; + transform.worldScale.z = transform.scale.z; + + // 计算旋转(从归一化的矩阵) + if (scaleX > 1e-10) { + const rotation = Math.atan2(m.b / scaleX, m.a / scaleX); + transform.worldRotation.z = rotation / DEG_TO_RAD; + } else { + transform.worldRotation.z = 0; + } + transform.worldRotation.x = transform.rotation.x; + transform.worldRotation.y = transform.rotation.y; + } +} diff --git a/packages/engine-core/src/index.ts b/packages/engine-core/src/index.ts new file mode 100644 index 00000000..2ca10a7a --- /dev/null +++ b/packages/engine-core/src/index.ts @@ -0,0 +1,17 @@ +export { TransformComponent, type Vector3, type Matrix2D } from './TransformComponent'; +export { TransformSystem } from './TransformSystem'; +export { HierarchyComponent } from './HierarchyComponent'; +export { HierarchySystem } from './HierarchySystem'; +export { + EnginePlugin, + // 类型导出 + type PluginCategory, + type LoadingPhase, + type ModuleType, + type ModuleDescriptor, + type PluginDependency, + type PluginDescriptor, + type SystemContext, + type IRuntimeModule, + type IPlugin +} from './EnginePlugin'; diff --git a/packages/engine-core/tsconfig.build.json b/packages/engine-core/tsconfig.build.json new file mode 100644 index 00000000..f39a0594 --- /dev/null +++ b/packages/engine-core/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/engine-core/tsconfig.json b/packages/engine-core/tsconfig.json new file mode 100644 index 00000000..02f5f187 --- /dev/null +++ b/packages/engine-core/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../core" } + ] +} diff --git a/packages/engine-core/tsup.config.ts b/packages/engine-core/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/engine-core/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/node-editor/package.json b/packages/node-editor/package.json index 8c3afebc..e3812889 100644 --- a/packages/node-editor/package.json +++ b/packages/node-editor/package.json @@ -35,19 +35,9 @@ ], "author": "yhh", "license": "MIT", - "peerDependencies": { - "react": "^18.3.1", - "zustand": "^4.5.2" - }, - "peerDependenciesMeta": { - "react": { - "optional": false - }, - "zustand": { - "optional": false - } - }, "devDependencies": { + "react": "^18.3.1", + "zustand": "^5.0.8", "@types/node": "^20.19.17", "@types/react": "^18.3.12", "@vitejs/plugin-react": "^4.7.0", diff --git a/packages/physics-rapier2d-editor/package.json b/packages/physics-rapier2d-editor/package.json new file mode 100644 index 00000000..2bb3b3a1 --- /dev/null +++ b/packages/physics-rapier2d-editor/package.json @@ -0,0 +1,47 @@ +{ + "name": "@esengine/physics-rapier2d-editor", + "version": "1.0.0", + "description": "Editor support for @esengine/physics-rapier2d - inspectors and gizmos", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "dependencies": { + "@esengine/physics-rapier2d": "workspace:*" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "lucide-react": "^0.545.0", + "react": "^18.3.1", + "@types/react": "^18.3.12", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "physics", + "rapier2d", + "editor" + ], + "author": "", + "license": "MIT" +} diff --git a/packages/physics-rapier2d/src/editor/index.ts b/packages/physics-rapier2d-editor/src/Physics2DEditorModule.ts similarity index 94% rename from packages/physics-rapier2d/src/editor/index.ts rename to packages/physics-rapier2d-editor/src/Physics2DEditorModule.ts index 89c90564..9d49c398 100644 --- a/packages/physics-rapier2d/src/editor/index.ts +++ b/packages/physics-rapier2d-editor/src/Physics2DEditorModule.ts @@ -1,6 +1,6 @@ /** - * Physics 2D Editor Module Entry - * 2D 物理编辑器模块入口 + * Physics 2D Editor Module + * 2D 物理编辑器模块 */ import type { ServiceContainer, Entity } from '@esengine/ecs-framework'; @@ -16,17 +16,21 @@ import { ComponentRegistry, SettingsRegistry } from '@esengine/editor-core'; -import { DEFAULT_PHYSICS_CONFIG } from '../types/Physics2DTypes'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; -// Local imports -import { Rigidbody2DComponent } from '../components/Rigidbody2DComponent'; -import { BoxCollider2DComponent } from '../components/BoxCollider2DComponent'; -import { CircleCollider2DComponent } from '../components/CircleCollider2DComponent'; -import { CapsuleCollider2DComponent } from '../components/CapsuleCollider2DComponent'; -import { PolygonCollider2DComponent } from '../components/PolygonCollider2DComponent'; +// Import from @esengine/physics-rapier2d package +import { + DEFAULT_PHYSICS_CONFIG, + Rigidbody2DComponent, + BoxCollider2DComponent, + CircleCollider2DComponent, + CapsuleCollider2DComponent, + PolygonCollider2DComponent +} from '@esengine/physics-rapier2d'; +import { Physics2DSystem } from '@esengine/physics-rapier2d/runtime'; + +// Local editor imports import { registerPhysics2DGizmos } from './gizmos/Physics2DGizmo'; -import { Physics2DSystem } from '../systems/Physics2DSystem'; import { CollisionMatrixEditor } from './components/CollisionMatrixEditor'; /** @@ -418,7 +422,3 @@ export class Physics2DEditorModule implements IEditorModuleLoader { } export const physics2DEditorModule = new Physics2DEditorModule(); - -// Plugin exports -export { Physics2DPlugin } from './Physics2DPlugin'; -export default physics2DEditorModule; diff --git a/packages/physics-rapier2d-editor/src/Physics2DPlugin.ts b/packages/physics-rapier2d-editor/src/Physics2DPlugin.ts new file mode 100644 index 00000000..2f73ef07 --- /dev/null +++ b/packages/physics-rapier2d-editor/src/Physics2DPlugin.ts @@ -0,0 +1,37 @@ +/** + * Physics 2D Plugin (Complete) + * 完整的 2D 物理插件(运行时 + 编辑器) + */ + +import type { IPlugin, PluginDescriptor } from '@esengine/editor-core'; +import { PhysicsRuntimeModule } from '@esengine/physics-rapier2d/runtime'; +import { physics2DEditorModule } from './Physics2DEditorModule'; + +/** + * Physics 2D 插件描述符 + * Physics 2D Plugin Descriptor + */ +const descriptor: PluginDescriptor = { + id: '@esengine/physics-rapier2d', + name: 'Physics 2D', + version: '1.0.0', + description: 'Deterministic 2D physics with Rapier2D', + category: 'physics', + enabledByDefault: true, + isEnginePlugin: true, + canContainContent: false, + modules: [ + { name: 'Runtime', type: 'runtime', loadingPhase: 'default' }, + { name: 'Editor', type: 'editor', loadingPhase: 'postDefault' } + ] +}; + +/** + * 完整的 Physics 2D 插件(运行时 + 编辑器) + * Complete Physics 2D Plugin (runtime + editor) + */ +export const Physics2DPlugin: IPlugin = { + descriptor, + runtimeModule: new PhysicsRuntimeModule(), + editorModule: physics2DEditorModule +}; diff --git a/packages/physics-rapier2d/src/editor/components/CollisionLayerSelector.tsx b/packages/physics-rapier2d-editor/src/components/CollisionLayerSelector.tsx similarity index 99% rename from packages/physics-rapier2d/src/editor/components/CollisionLayerSelector.tsx rename to packages/physics-rapier2d-editor/src/components/CollisionLayerSelector.tsx index 4e8ebf38..c7a91837 100644 --- a/packages/physics-rapier2d/src/editor/components/CollisionLayerSelector.tsx +++ b/packages/physics-rapier2d-editor/src/components/CollisionLayerSelector.tsx @@ -4,7 +4,7 @@ */ import React, { useState, useEffect, useRef } from 'react'; -import { CollisionLayerConfig } from '../../services/CollisionLayerConfig'; +import { CollisionLayerConfig } from '@esengine/physics-rapier2d'; interface CollisionLayerSelectorProps { value: number; diff --git a/packages/physics-rapier2d/src/editor/components/CollisionMatrixEditor.tsx b/packages/physics-rapier2d-editor/src/components/CollisionMatrixEditor.tsx similarity index 99% rename from packages/physics-rapier2d/src/editor/components/CollisionMatrixEditor.tsx rename to packages/physics-rapier2d-editor/src/components/CollisionMatrixEditor.tsx index 86a4ee70..295796fe 100644 --- a/packages/physics-rapier2d/src/editor/components/CollisionMatrixEditor.tsx +++ b/packages/physics-rapier2d-editor/src/components/CollisionMatrixEditor.tsx @@ -7,7 +7,7 @@ */ import React, { useState, useEffect, useCallback, useRef } from 'react'; -import { CollisionLayerConfig } from '../../services/CollisionLayerConfig'; +import { CollisionLayerConfig } from '@esengine/physics-rapier2d'; interface CollisionMatrixEditorProps { onClose?: () => void; diff --git a/packages/physics-rapier2d/src/editor/gizmos/Physics2DGizmo.ts b/packages/physics-rapier2d-editor/src/gizmos/Physics2DGizmo.ts similarity index 95% rename from packages/physics-rapier2d/src/editor/gizmos/Physics2DGizmo.ts rename to packages/physics-rapier2d-editor/src/gizmos/Physics2DGizmo.ts index d56b0a76..370d00fc 100644 --- a/packages/physics-rapier2d/src/editor/gizmos/Physics2DGizmo.ts +++ b/packages/physics-rapier2d-editor/src/gizmos/Physics2DGizmo.ts @@ -16,14 +16,16 @@ import type { GizmoColor } from '@esengine/editor-core'; import { GizmoRegistry } from '@esengine/editor-core'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; -import { BoxCollider2DComponent } from '../../components/BoxCollider2DComponent'; -import { CircleCollider2DComponent } from '../../components/CircleCollider2DComponent'; -import { CapsuleCollider2DComponent } from '../../components/CapsuleCollider2DComponent'; -import { PolygonCollider2DComponent } from '../../components/PolygonCollider2DComponent'; -import { Rigidbody2DComponent } from '../../components/Rigidbody2DComponent'; -import { RigidbodyType2D } from '../../types/Physics2DTypes'; +import { + BoxCollider2DComponent, + CircleCollider2DComponent, + CapsuleCollider2DComponent, + PolygonCollider2DComponent, + Rigidbody2DComponent, + RigidbodyType2D +} from '@esengine/physics-rapier2d'; /** * 物理 Gizmo 颜色配置 diff --git a/packages/physics-rapier2d-editor/src/index.ts b/packages/physics-rapier2d-editor/src/index.ts new file mode 100644 index 00000000..1a6ae785 --- /dev/null +++ b/packages/physics-rapier2d-editor/src/index.ts @@ -0,0 +1,11 @@ +/** + * Physics 2D Editor Module Entry + * 2D 物理编辑器模块入口 + */ + +// Re-export editor module +export { Physics2DEditorModule, physics2DEditorModule } from './Physics2DEditorModule'; +export { physics2DEditorModule as default } from './Physics2DEditorModule'; + +// Re-export plugin +export { Physics2DPlugin } from './Physics2DPlugin'; diff --git a/packages/physics-rapier2d/src/editor/inspectors/BoxCollider2DInspector.tsx b/packages/physics-rapier2d-editor/src/inspectors/BoxCollider2DInspector.tsx similarity index 97% rename from packages/physics-rapier2d/src/editor/inspectors/BoxCollider2DInspector.tsx rename to packages/physics-rapier2d-editor/src/inspectors/BoxCollider2DInspector.tsx index be335f4a..0e27eadc 100644 --- a/packages/physics-rapier2d/src/editor/inspectors/BoxCollider2DInspector.tsx +++ b/packages/physics-rapier2d-editor/src/inspectors/BoxCollider2DInspector.tsx @@ -6,8 +6,7 @@ import React from 'react'; import { Component } from '@esengine/ecs-framework'; import type { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core'; -import { BoxCollider2DComponent } from '../../components/BoxCollider2DComponent'; -import { CollisionLayer2D } from '../../types/Physics2DTypes'; +import { BoxCollider2DComponent, CollisionLayer2D } from '@esengine/physics-rapier2d'; export class BoxCollider2DInspectorProvider implements IComponentInspector { readonly id = 'boxcollider2d-inspector'; diff --git a/packages/physics-rapier2d/src/editor/inspectors/CircleCollider2DInspector.tsx b/packages/physics-rapier2d-editor/src/inspectors/CircleCollider2DInspector.tsx similarity index 97% rename from packages/physics-rapier2d/src/editor/inspectors/CircleCollider2DInspector.tsx rename to packages/physics-rapier2d-editor/src/inspectors/CircleCollider2DInspector.tsx index d16d6542..14d72488 100644 --- a/packages/physics-rapier2d/src/editor/inspectors/CircleCollider2DInspector.tsx +++ b/packages/physics-rapier2d-editor/src/inspectors/CircleCollider2DInspector.tsx @@ -6,8 +6,7 @@ import React from 'react'; import { Component } from '@esengine/ecs-framework'; import type { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core'; -import { CircleCollider2DComponent } from '../../components/CircleCollider2DComponent'; -import { CollisionLayer2D } from '../../types/Physics2DTypes'; +import { CircleCollider2DComponent, CollisionLayer2D } from '@esengine/physics-rapier2d'; export class CircleCollider2DInspectorProvider implements IComponentInspector { readonly id = 'circlecollider2d-inspector'; diff --git a/packages/physics-rapier2d/src/editor/inspectors/Rigidbody2DInspector.tsx b/packages/physics-rapier2d-editor/src/inspectors/Rigidbody2DInspector.tsx similarity index 98% rename from packages/physics-rapier2d/src/editor/inspectors/Rigidbody2DInspector.tsx rename to packages/physics-rapier2d-editor/src/inspectors/Rigidbody2DInspector.tsx index 77a7c77a..b38c4969 100644 --- a/packages/physics-rapier2d/src/editor/inspectors/Rigidbody2DInspector.tsx +++ b/packages/physics-rapier2d-editor/src/inspectors/Rigidbody2DInspector.tsx @@ -6,8 +6,7 @@ import React from 'react'; import { Component } from '@esengine/ecs-framework'; import type { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core'; -import { Rigidbody2DComponent } from '../../components/Rigidbody2DComponent'; -import { RigidbodyType2D, CollisionDetectionMode2D } from '../../types/Physics2DTypes'; +import { Rigidbody2DComponent, RigidbodyType2D, CollisionDetectionMode2D } from '@esengine/physics-rapier2d'; export class Rigidbody2DInspectorProvider implements IComponentInspector { readonly id = 'rigidbody2d-inspector'; diff --git a/packages/physics-rapier2d-editor/tsconfig.build.json b/packages/physics-rapier2d-editor/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/physics-rapier2d-editor/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/physics-rapier2d-editor/tsconfig.json b/packages/physics-rapier2d-editor/tsconfig.json new file mode 100644 index 00000000..d659ba24 --- /dev/null +++ b/packages/physics-rapier2d-editor/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "jsx": "react-jsx", + "moduleResolution": "bundler" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/physics-rapier2d-editor/tsup.config.ts b/packages/physics-rapier2d-editor/tsup.config.ts new file mode 100644 index 00000000..b4f49f5d --- /dev/null +++ b/packages/physics-rapier2d-editor/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { editorOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...editorOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/physics-rapier2d/package.json b/packages/physics-rapier2d/package.json index a64ca634..cb5686ad 100644 --- a/packages/physics-rapier2d/package.json +++ b/packages/physics-rapier2d/package.json @@ -2,6 +2,11 @@ "name": "@esengine/physics-rapier2d", "version": "1.0.0", "description": "Deterministic 2D physics engine based on Rapier2D with enhanced-determinism", + "esengine": { + "plugin": true, + "pluginExport": "PhysicsPlugin", + "category": "physics" + }, "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", @@ -14,21 +19,15 @@ "./runtime": { "types": "./dist/runtime.d.ts", "import": "./dist/runtime.js" - }, - "./editor": { - "types": "./dist/editor/index.d.ts", - "import": "./dist/editor/index.js" - }, - "./plugin.json": "./plugin.json" + } }, "files": [ - "dist", - "plugin.json" + "dist" ], "scripts": { "clean": "rimraf dist tsconfig.tsbuildinfo", - "build": "vite build", - "build:watch": "vite build --watch", + "build": "tsup", + "build:watch": "tsup --watch", "type-check": "tsc --noEmit" }, "keywords": [ @@ -41,30 +40,17 @@ ], "author": "yhh", "license": "MIT", - "peerDependencies": { - "@esengine/ecs-framework": ">=2.0.0", - "@esengine/ecs-components": "workspace:*", - "@esengine/editor-core": "workspace:*", - "react": "^18.3.1" - }, - "peerDependenciesMeta": { - "@esengine/editor-core": { - "optional": true - }, - "react": { - "optional": true - } - }, "dependencies": { "@dimforge/rapier2d-compat": "^0.14.0" }, "devDependencies": { - "@types/react": "^18.3.12", - "@vitejs/plugin-react": "^4.7.0", + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/build-config": "workspace:*", "rimraf": "^5.0.0", - "typescript": "^5.8.3", - "vite": "^6.0.7", - "vite-plugin-dts": "^4.5.0" + "tsup": "^8.0.0", + "typescript": "^5.8.3" }, "publishConfig": { "access": "public", diff --git a/packages/physics-rapier2d/src/Physics2DComponentsModule.ts b/packages/physics-rapier2d/src/Physics2DComponentsModule.ts index dd2ac7e8..5650c9b6 100644 --- a/packages/physics-rapier2d/src/Physics2DComponentsModule.ts +++ b/packages/physics-rapier2d/src/Physics2DComponentsModule.ts @@ -7,7 +7,7 @@ */ import { ComponentRegistry } from '@esengine/ecs-framework'; -import type { IRuntimeModuleLoader } from '@esengine/ecs-components'; +import type { IRuntimeModule } from '@esengine/engine-core'; // Components (no WASM dependency) import { Rigidbody2DComponent } from './components/Rigidbody2DComponent'; @@ -23,7 +23,7 @@ import { PolygonCollider2DComponent } from './components/PolygonCollider2DCompon * 仅实现组件注册,不包含系统创建和 WASM 初始化 * 用于编辑器场景序列化 */ -export class Physics2DComponentsModule implements IRuntimeModuleLoader { +export class Physics2DComponentsModule implements IRuntimeModule { /** * 注册组件到 ComponentRegistry */ diff --git a/packages/physics-rapier2d/src/PhysicsEditorPlugin.ts b/packages/physics-rapier2d/src/PhysicsEditorPlugin.ts new file mode 100644 index 00000000..f9e4afab --- /dev/null +++ b/packages/physics-rapier2d/src/PhysicsEditorPlugin.ts @@ -0,0 +1,40 @@ +/** + * Physics Editor Plugin + * + * 编辑器版本的物理插件,不包含 WASM 依赖。 + * Editor version of physics plugin, without WASM dependencies. + * + * 用于编辑器中注册插件描述符,但不创建运行时模块。 + * 运行时使用 PhysicsPlugin from '@esengine/physics-rapier2d/runtime' + */ + +import type { IPlugin, PluginDescriptor } from '@esengine/engine-core'; + +const descriptor: PluginDescriptor = { + id: '@esengine/physics-rapier2d', + name: 'Physics 2D', + version: '1.0.0', + description: 'Deterministic 2D physics with Rapier2D', + category: 'physics', + enabledByDefault: false, + canContainContent: false, + isEnginePlugin: true, + modules: [ + { + name: 'PhysicsRuntime', + type: 'runtime', + loadingPhase: 'default' + } + ] +}; + +/** + * 编辑器物理插件(无运行时模块) + * Editor physics plugin (no runtime module) + * + * 编辑器使用此版本注册插件,运行时使用带 WASM 的完整版本。 + */ +export const Physics2DPlugin: IPlugin = { + descriptor + // No runtime module - editor doesn't need physics simulation +}; diff --git a/packages/physics-rapier2d/src/PhysicsRuntimeModule.ts b/packages/physics-rapier2d/src/PhysicsRuntimeModule.ts index fc5a4cbf..18fabfc1 100644 --- a/packages/physics-rapier2d/src/PhysicsRuntimeModule.ts +++ b/packages/physics-rapier2d/src/PhysicsRuntimeModule.ts @@ -1,54 +1,31 @@ -/** - * Physics 2D Runtime Module - * 2D 物理运行时模块 - * - * 提供确定性 2D 物理模拟功能 - */ - import type { IScene, ServiceContainer } from '@esengine/ecs-framework'; import { ComponentRegistry } from '@esengine/ecs-framework'; -import type { IRuntimeModuleLoader, SystemContext } from '@esengine/ecs-components'; +import type { IRuntimeModule, IPlugin, PluginDescriptor, SystemContext } from '@esengine/engine-core'; import * as RAPIER from '@dimforge/rapier2d-compat'; -// Components import { Rigidbody2DComponent } from './components/Rigidbody2DComponent'; import { BoxCollider2DComponent } from './components/BoxCollider2DComponent'; import { CircleCollider2DComponent } from './components/CircleCollider2DComponent'; import { CapsuleCollider2DComponent } from './components/CapsuleCollider2DComponent'; import { PolygonCollider2DComponent } from './components/PolygonCollider2DComponent'; - -// Systems import { Physics2DSystem } from './systems/Physics2DSystem'; - -// Services import { Physics2DService } from './services/Physics2DService'; -/** - * Physics 2D Runtime Module - * 2D 物理运行时模块 - * - * 实现 IRuntimeModuleLoader 接口,提供: - * - 物理组件注册 - * - 物理系统创建 - * - Rapier2D 初始化 - */ -export class PhysicsRuntimeModule implements IRuntimeModuleLoader { +export interface PhysicsSystemContext extends SystemContext { + physicsSystem?: Physics2DSystem; + physics2DWorld?: any; + physicsConfig?: any; +} + +class PhysicsRuntimeModule implements IRuntimeModule { private _rapierModule: typeof RAPIER | null = null; private _physicsSystem: Physics2DSystem | null = null; - /** - * 初始化模块 - * 异步初始化 Rapier2D WASM 模块 - */ async onInitialize(): Promise { - // 初始化 Rapier2D WASM await RAPIER.init(); this._rapierModule = RAPIER; } - /** - * 注册组件 - */ registerComponents(registry: typeof ComponentRegistry): void { registry.register(Rigidbody2DComponent); registry.register(BoxCollider2DComponent); @@ -57,61 +34,56 @@ export class PhysicsRuntimeModule implements IRuntimeModuleLoader { registry.register(PolygonCollider2DComponent); } - /** - * 注册服务 - */ - registerServices?(services: ServiceContainer): void { - // 注册物理服务 + registerServices(services: ServiceContainer): void { services.registerSingleton(Physics2DService); } - /** - * 创建系统 - */ createSystems(scene: IScene, context: SystemContext): void { - // 创建物理系统 + const physicsContext = context as PhysicsSystemContext; + const physicsSystem = new Physics2DSystem({ - physics: context.physicsConfig, - updateOrder: -1000 // 在其他系统之前运行 + physics: physicsContext.physicsConfig, + updateOrder: -1000 }); scene.addSystem(physicsSystem); this._physicsSystem = physicsSystem; - // 如果 Rapier 已加载,初始化物理系统 if (this._rapierModule) { physicsSystem.initializeWithRapier(this._rapierModule); } - // 导出到上下文供其他系统使用 - context.physicsSystem = physicsSystem; - context.physics2DWorld = physicsSystem.world; + physicsContext.physicsSystem = physicsSystem; + physicsContext.physics2DWorld = physicsSystem.world; } - /** - * 销毁模块 - */ onDestroy(): void { this._physicsSystem = null; this._rapierModule = null; } - /** - * 获取 Rapier 模块 - */ getRapierModule(): typeof RAPIER | null { return this._rapierModule; } - /** - * 获取物理系统 - */ getPhysicsSystem(): Physics2DSystem | null { return this._physicsSystem; } } -/** - * 默认导出模块实例 - */ -export default new PhysicsRuntimeModule(); +const descriptor: PluginDescriptor = { + id: '@esengine/physics-rapier2d', + name: 'Physics 2D', + version: '1.0.0', + description: 'Deterministic 2D physics with Rapier2D', + category: 'physics', + enabledByDefault: true, + isEnginePlugin: true +}; + +export const PhysicsPlugin: IPlugin = { + descriptor, + runtimeModule: new PhysicsRuntimeModule() +}; + +export { PhysicsRuntimeModule }; diff --git a/packages/physics-rapier2d/src/components/Collider2DBase.ts b/packages/physics-rapier2d/src/components/Collider2DBase.ts index 076ddf0a..b45ff803 100644 --- a/packages/physics-rapier2d/src/components/Collider2DBase.ts +++ b/packages/physics-rapier2d/src/components/Collider2DBase.ts @@ -4,7 +4,8 @@ */ import { Component, Property, Serialize } from '@esengine/ecs-framework'; -import { Vector2, CollisionLayer2D } from '../types/Physics2DTypes'; +import { CollisionLayer2D } from '../types/Physics2DTypes'; +import type { Vector2 } from '../types/Physics2DTypes'; /** * 2D 碰撞体基类 diff --git a/packages/physics-rapier2d/src/components/Rigidbody2DComponent.ts b/packages/physics-rapier2d/src/components/Rigidbody2DComponent.ts index 17e259e6..f4a019fe 100644 --- a/packages/physics-rapier2d/src/components/Rigidbody2DComponent.ts +++ b/packages/physics-rapier2d/src/components/Rigidbody2DComponent.ts @@ -4,7 +4,8 @@ */ import { Component, Property, Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework'; -import { RigidbodyType2D, CollisionDetectionMode2D, Vector2 } from '../types/Physics2DTypes'; +import { RigidbodyType2D, CollisionDetectionMode2D } from '../types/Physics2DTypes'; +import type { Vector2 } from '../types/Physics2DTypes'; /** * 刚体约束配置 diff --git a/packages/physics-rapier2d/src/editor/Physics2DPlugin.ts b/packages/physics-rapier2d/src/editor/Physics2DPlugin.ts deleted file mode 100644 index d3e2af53..00000000 --- a/packages/physics-rapier2d/src/editor/Physics2DPlugin.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Physics 2D Unified Plugin - * 2D 物理统一插件 - * - * 编辑器专用插件入口 - * 使用完整运行时模块以支持编辑器预览 - */ - -import type { IPluginLoader, PluginDescriptor } from '@esengine/editor-core'; -import { Physics2DEditorModule } from './index'; -import { PhysicsRuntimeModule } from '../PhysicsRuntimeModule'; -import { CollisionLayerConfig } from '../services/CollisionLayerConfig'; - -// 暴露 CollisionLayerConfig 到全局,供 CollisionLayerField 访问 -(window as any).__PHYSICS_RAPIER2D__ = { - CollisionLayerConfig -}; - -/** - * 插件描述符 - */ -const descriptor: PluginDescriptor = { - id: '@esengine/physics-rapier2d', - name: 'Rapier 2D Physics', - version: '1.0.0', - description: '基于 Rapier2D 的确定性 2D 物理引擎', - category: 'physics', - enabledByDefault: true, - canContainContent: false, - isEnginePlugin: true, - modules: [ - { - name: 'PhysicsRuntime', - type: 'runtime', - loadingPhase: 'default', - entry: './src/runtime.ts' - }, - { - name: 'PhysicsEditor', - type: 'editor', - loadingPhase: 'default', - entry: './src/editor/index.ts' - } - ], - dependencies: [ - { id: '@esengine/ecs-framework', version: '^2.0.0' }, - { id: '@esengine/ecs-components', version: '^1.0.0' } - ], - icon: 'Atom' -}; - -/** - * Physics 2D Plugin Loader - * 2D 物理插件加载器 - * - * - runtimeModule: 完整运行时模块(含 WASM 物理系统),支持编辑器预览和游戏运行 - * - editorModule: 编辑器功能模块(检视器、Gizmo、实体模板等) - */ -export const Physics2DPlugin: IPluginLoader = { - descriptor, - editorModule: new Physics2DEditorModule(), - runtimeModule: new PhysicsRuntimeModule(), -}; - -export default Physics2DPlugin; diff --git a/packages/physics-rapier2d/src/index.ts b/packages/physics-rapier2d/src/index.ts index 49760ef0..52d4bb4d 100644 --- a/packages/physics-rapier2d/src/index.ts +++ b/packages/physics-rapier2d/src/index.ts @@ -22,5 +22,5 @@ export * from './services'; // Systems (type only for editor usage) export type { Physics2DSystem } from './systems/Physics2DSystem'; -// Plugin (for editor, no WASM dependency) -export { Physics2DPlugin } from './editor/Physics2DPlugin'; +// Editor plugin (no WASM dependency) +export { Physics2DPlugin } from './PhysicsEditorPlugin'; diff --git a/packages/physics-rapier2d/src/runtime.ts b/packages/physics-rapier2d/src/runtime.ts index 9973d19b..1c226001 100644 --- a/packages/physics-rapier2d/src/runtime.ts +++ b/packages/physics-rapier2d/src/runtime.ts @@ -52,6 +52,5 @@ export { Physics2DSystem, type Physics2DSystemConfig } from './systems/Physics2D // Services export { Physics2DService } from './services/Physics2DService'; -// Runtime Module -export { PhysicsRuntimeModule } from './PhysicsRuntimeModule'; -export { default as physicsRuntimeModule } from './PhysicsRuntimeModule'; +// Runtime module and plugin +export { PhysicsRuntimeModule, PhysicsPlugin, type PhysicsSystemContext } from './PhysicsRuntimeModule'; diff --git a/packages/physics-rapier2d/src/runtime/index.ts b/packages/physics-rapier2d/src/runtime/index.ts index f5896dce..057c0f95 100644 --- a/packages/physics-rapier2d/src/runtime/index.ts +++ b/packages/physics-rapier2d/src/runtime/index.ts @@ -6,8 +6,8 @@ * Contains WASM dependencies, for actual runtime environment */ -// Re-export runtime module with WASM -export { PhysicsRuntimeModule, default as physicsRuntimeModule } from '../PhysicsRuntimeModule'; +// Re-export runtime module and plugin with WASM +export { PhysicsRuntimeModule, PhysicsPlugin, type PhysicsSystemContext } from '../PhysicsRuntimeModule'; // Re-export world and system (they have WASM type dependencies) export { Physics2DWorld } from '../world/Physics2DWorld'; diff --git a/packages/physics-rapier2d/src/systems/Physics2DSystem.ts b/packages/physics-rapier2d/src/systems/Physics2DSystem.ts index 7cce60a9..151c12cb 100644 --- a/packages/physics-rapier2d/src/systems/Physics2DSystem.ts +++ b/packages/physics-rapier2d/src/systems/Physics2DSystem.ts @@ -6,7 +6,7 @@ */ import { EntitySystem, Matcher, type Entity } from '@esengine/ecs-framework'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; import { Physics2DWorld } from '../world/Physics2DWorld'; import { Rigidbody2DComponent } from '../components/Rigidbody2DComponent'; import { Collider2DBase } from '../components/Collider2DBase'; @@ -66,9 +66,6 @@ export class Physics2DSystem extends EntitySystem { // 待处理的新实体队列 private _pendingEntities: Entity[] = []; - // Transform 组件类型(用于检查) - private _transformType = TransformComponent; - constructor(config?: Physics2DSystemConfig) { // 匹配所有拥有 Rigidbody2DComponent 的实体 super(Matcher.empty().all(Rigidbody2DComponent)); @@ -290,10 +287,13 @@ export class Physics2DSystem extends EntitySystem { */ private _createPhysicsBody(entity: Entity): void { const rigidbody = entity.getComponent(Rigidbody2DComponent); - const transform = entity.getComponent(this._transformType); + const transform = entity.getComponent(TransformComponent); if (!rigidbody || !transform) { - this.logger.warn(`Entity ${entity.name} missing required components for physics`); + const missing: string[] = []; + if (!rigidbody) missing.push('Rigidbody2DComponent'); + if (!transform) missing.push('TransformComponent'); + this.logger.warn(`Entity ${entity.name} missing required components: ${missing.join(', ')}`); return; } @@ -361,7 +361,7 @@ export class Physics2DSystem extends EntitySystem { private _syncTransformsToPhysics(entities: readonly Entity[]): void { for (const entity of entities) { const rigidbody = entity.getComponent(Rigidbody2DComponent); - const transform = entity.getComponent(this._transformType); + const transform = entity.getComponent(TransformComponent); const mapping = this._entityBodies.get(entity.id); if (!rigidbody || !transform || !mapping) continue; @@ -465,7 +465,7 @@ export class Physics2DSystem extends EntitySystem { private _syncPhysicsToTransforms(entities: readonly Entity[]): void { for (const entity of entities) { const rigidbody = entity.getComponent(Rigidbody2DComponent); - const transform = entity.getComponent(this._transformType); + const transform = entity.getComponent(TransformComponent); const mapping = this._entityBodies.get(entity.id); if (!rigidbody || !transform || !mapping) continue; diff --git a/packages/physics-rapier2d/tsconfig.build.json b/packages/physics-rapier2d/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/physics-rapier2d/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/physics-rapier2d/tsconfig.json b/packages/physics-rapier2d/tsconfig.json index 2d7897fe..a449a378 100644 --- a/packages/physics-rapier2d/tsconfig.json +++ b/packages/physics-rapier2d/tsconfig.json @@ -25,6 +25,6 @@ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"], "references": [ { "path": "../core" }, - { "path": "../components" } + { "path": "../engine-core" } ] } diff --git a/packages/physics-rapier2d/tsup.config.ts b/packages/physics-rapier2d/tsup.config.ts new file mode 100644 index 00000000..aca96ccf --- /dev/null +++ b/packages/physics-rapier2d/tsup.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'tsup'; +import { STANDARD_EXTERNALS } from '../build-config/src/types'; + +// Physics-rapier2d keeps runtime entry for WASM loading +export default defineConfig({ + entry: { + index: 'src/index.ts', + runtime: 'src/runtime.ts' + }, + format: ['esm'], + dts: true, + sourcemap: true, + clean: true, + tsconfig: 'tsconfig.build.json', + // 使用标准外部依赖列表,确保所有 @esengine/* 包都被外部化 + // 这避免了类被重复打包导致 instanceof 检查失败的问题 + external: [ + ...STANDARD_EXTERNALS, + ], + esbuildOptions(options) { + options.jsx = 'automatic'; + } +}); diff --git a/packages/physics-rapier2d/vite.config.ts b/packages/physics-rapier2d/vite.config.ts deleted file mode 100644 index 9a0f8c08..00000000 --- a/packages/physics-rapier2d/vite.config.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; -import dts from 'vite-plugin-dts'; -import react from '@vitejs/plugin-react'; - -export default defineConfig({ - plugins: [ - react(), - dts({ - include: ['src'], - outDir: 'dist', - rollupTypes: false, - tsconfigPath: './tsconfig.json' - }) - ], - esbuild: { - jsx: 'automatic', - }, - build: { - lib: { - entry: { - index: resolve(__dirname, 'src/index.ts'), - runtime: resolve(__dirname, 'src/runtime.ts'), - 'editor/index': resolve(__dirname, 'src/editor/index.ts') - }, - formats: ['es'], - fileName: (format, entryName) => `${entryName}.js` - }, - rollupOptions: { - external: [ - '@esengine/ecs-framework', - '@esengine/ecs-components', - '@esengine/editor-core', - 'react', - 'react/jsx-runtime', - /^@esengine\// - ], - output: { - exports: 'named', - preserveModules: false - } - }, - target: 'es2020', - minify: false, - sourcemap: true - } -}); diff --git a/packages/platform-common/tsconfig.json b/packages/platform-common/tsconfig.json index ddda78d4..4416d783 100644 --- a/packages/platform-common/tsconfig.json +++ b/packages/platform-common/tsconfig.json @@ -17,7 +17,10 @@ "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, - "noEmit": false + "noEmit": false, + "composite": true, + "incremental": true, + "tsBuildInfoFile": "./dist/.tsbuildinfo" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/packages/platform-web/package.json b/packages/platform-web/package.json index b3134068..f799ecd7 100644 --- a/packages/platform-web/package.json +++ b/packages/platform-web/package.json @@ -37,18 +37,20 @@ ], "author": "yhh", "license": "MIT", - "peerDependencies": { - "@esengine/asset-system": "workspace:*", - "@esengine/behavior-tree": "workspace:*", - "@esengine/ecs-components": "workspace:*", - "@esengine/ecs-engine-bindgen": "workspace:*", - "@esengine/ecs-framework": "workspace:*", - "@esengine/physics-rapier2d": "workspace:*", - "@esengine/platform-common": "workspace:*", - "@esengine/tilemap": "workspace:*", - "@esengine/ui": "workspace:*" - }, "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/asset-system": "workspace:*", + "@esengine/platform-common": "workspace:*", + "@esengine/audio": "workspace:*", + "@esengine/behavior-tree": "workspace:*", + "@esengine/camera": "workspace:*", + "@esengine/ecs-engine-bindgen": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/physics-rapier2d": "workspace:*", + "@esengine/runtime-core": "workspace:*", + "@esengine/sprite": "workspace:*", + "@esengine/tilemap": "workspace:*", + "@esengine/ui": "workspace:*", "@rollup/plugin-alias": "^6.0.0", "@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-node-resolve": "^16.0.1", diff --git a/packages/platform-web/rollup.runtime.config.js b/packages/platform-web/rollup.runtime.config.js index b5b3ab39..0188dfdd 100644 --- a/packages/platform-web/rollup.runtime.config.js +++ b/packages/platform-web/rollup.runtime.config.js @@ -17,7 +17,13 @@ export default { external: [ 'react', 'react-dom', - '@esengine/editor-core' + '@esengine/editor-core', + // Editor packages should never be imported in runtime + '@esengine/ui-editor', + '@esengine/tilemap-editor', + '@esengine/behavior-tree-editor', + '@esengine/blueprint-editor', + '@esengine/physics-rapier2d-editor' ], plugins: [ // Replace process.env.NODE_ENV for browser @@ -28,10 +34,7 @@ export default { resolve({ browser: true, preferBuiltins: false, - // Only resolve main/module fields, not source - mainFields: ['module', 'main'], - // Support package.json exports field for subpath imports - exportConditions: ['import', 'module', 'default'] + exportConditions: ['import', 'default'] }), commonjs(), typescript({ diff --git a/packages/platform-web/src/RuntimeSystems.ts b/packages/platform-web/src/RuntimeSystems.ts deleted file mode 100644 index 8ebf1452..00000000 --- a/packages/platform-web/src/RuntimeSystems.ts +++ /dev/null @@ -1,410 +0,0 @@ -/** - * Runtime Systems Configuration - * 运行时系统配置 - */ - -import { Core, ComponentRegistry, ServiceContainer } from '@esengine/ecs-framework'; -import type { IScene } from '@esengine/ecs-framework'; -import { EngineBridge, EngineRenderSystem, CameraSystem } from '@esengine/ecs-engine-bindgen'; -import { TransformComponent, SpriteAnimatorSystem, CoreRuntimeModule } from '@esengine/ecs-components'; -import type { SystemContext, IPluginLoader, IRuntimeModuleLoader, PluginDescriptor } from '@esengine/ecs-components'; -// Import runtime modules -// 注意:这些模块需要从各自包的 runtime 入口点导入,以避免编辑器依赖(React 等) -import { UIRuntimeModule, UIRenderDataProvider, UIInputSystem } from '@esengine/ui/runtime'; -import { TilemapRuntimeModule, TilemapRenderingSystem } from '@esengine/tilemap/runtime'; -import { BehaviorTreeRuntimeModule, BehaviorTreeExecutionSystem } from '@esengine/behavior-tree/runtime'; -import { PhysicsRuntimeModule, Physics2DSystem } from '@esengine/physics-rapier2d/runtime'; - -/** - * 运行时系统集合 - */ -export interface RuntimeSystems { - cameraSystem: CameraSystem; - animatorSystem?: SpriteAnimatorSystem; - tilemapSystem?: TilemapRenderingSystem; - behaviorTreeSystem?: BehaviorTreeExecutionSystem; - physicsSystem?: Physics2DSystem; - renderSystem: EngineRenderSystem; - uiRenderProvider?: UIRenderDataProvider; -} - -/** - * 运行时配置 - */ -export interface RuntimeModuleConfig { - /** 启用的插件 ID 列表,不指定则启用所有已注册插件 */ - enabledPlugins?: string[]; - /** 是否为编辑器模式 */ - isEditor?: boolean; - /** Canvas ID 用于 UI 输入绑定 */ - canvasId?: string; -} - -/** - * 运行时插件管理器(简化版,用于独立运行时) - * Runtime Plugin Manager (simplified, for standalone runtime) - */ -class RuntimePluginManager { - private plugins: Map = new Map(); - private enabledPlugins: Set = new Set(); - private initialized = false; - - /** - * 注册插件 - */ - register(plugin: IPluginLoader): void { - const id = plugin.descriptor.id; - if (this.plugins.has(id)) { - return; - } - this.plugins.set(id, plugin); - // 默认启用 - if (plugin.descriptor.enabledByDefault !== false) { - this.enabledPlugins.add(id); - } - } - - /** - * 启用插件 - */ - enable(pluginId: string): void { - this.enabledPlugins.add(pluginId); - } - - /** - * 禁用插件 - */ - disable(pluginId: string): void { - this.enabledPlugins.delete(pluginId); - } - - /** - * 加载配置 - */ - loadConfig(config: { enabledPlugins: string[] }): void { - this.enabledPlugins.clear(); - for (const id of config.enabledPlugins) { - this.enabledPlugins.add(id); - } - // 始终启用引擎插件 - for (const [id, plugin] of this.plugins) { - if (plugin.descriptor.isEnginePlugin) { - this.enabledPlugins.add(id); - } - } - } - - /** - * 初始化运行时(注册组件和服务) - */ - async initializeRuntime(services: ServiceContainer): Promise { - if (this.initialized) { - return; - } - - // 注册组件 - for (const [id, plugin] of this.plugins) { - if (!this.enabledPlugins.has(id)) { - continue; - } - const runtimeModule = plugin.runtimeModule; - if (runtimeModule) { - try { - runtimeModule.registerComponents(ComponentRegistry); - } catch (e) { - console.error(`Failed to register components for ${id}:`, e); - } - } - } - - // 注册服务 - for (const [id, plugin] of this.plugins) { - if (!this.enabledPlugins.has(id)) continue; - const runtimeModule = plugin.runtimeModule; - if (runtimeModule?.registerServices) { - try { - runtimeModule.registerServices(services); - } catch (e) { - console.error(`Failed to register services for ${id}:`, e); - } - } - } - - // 调用初始化回调 - for (const [id, plugin] of this.plugins) { - if (!this.enabledPlugins.has(id)) continue; - const runtimeModule = plugin.runtimeModule; - if (runtimeModule?.onInitialize) { - try { - await runtimeModule.onInitialize(); - } catch (e) { - console.error(`Failed to initialize ${id}:`, e); - } - } - } - - this.initialized = true; - } - - /** - * 为场景创建系统 - */ - createSystemsForScene(scene: IScene, context: SystemContext): void { - // Phase 1: 创建所有系统 - // Phase 1: Create all systems - for (const [id, plugin] of this.plugins) { - if (!this.enabledPlugins.has(id)) continue; - const runtimeModule = plugin.runtimeModule; - if (runtimeModule?.createSystems) { - try { - runtimeModule.createSystems(scene, context); - } catch (e) { - console.error(`Failed to create systems for ${id}:`, e); - } - } - } - - // Phase 2: 连接跨插件依赖 - // Phase 2: Wire cross-plugin dependencies - for (const [id, plugin] of this.plugins) { - if (!this.enabledPlugins.has(id)) continue; - const runtimeModule = plugin.runtimeModule; - if (runtimeModule?.onSystemsCreated) { - try { - runtimeModule.onSystemsCreated(scene, context); - } catch (e) { - console.error(`Failed to wire dependencies for ${id}:`, e); - } - } - } - } - - /** - * 获取所有已注册的插件 - */ - getPlugins(): IPluginLoader[] { - return Array.from(this.plugins.values()); - } - - /** - * 检查插件是否启用 - */ - isEnabled(pluginId: string): boolean { - return this.enabledPlugins.has(pluginId); - } - - /** - * 重置 - */ - reset(): void { - this.plugins.clear(); - this.enabledPlugins.clear(); - this.initialized = false; - } -} - -// 单例运行时插件管理器 -const runtimePluginManager = new RuntimePluginManager(); - -/** - * 创建运行时专用的插件加载器 - * Create runtime-only plugin loaders (without editor modules to avoid code splitting issues) - */ -function createRuntimeOnlyPlugin( - descriptor: PluginDescriptor, - runtimeModule: IRuntimeModuleLoader -): IPluginLoader { - return { - descriptor, - runtimeModule, - // No editor module for runtime builds - }; -} - -// 运行时专用插件描述符 | Runtime-only plugin descriptors -const coreDescriptor: PluginDescriptor = { - id: '@esengine/ecs-components', - name: 'Core Components', - version: '1.0.0', - category: 'core', - enabledByDefault: true, - isEnginePlugin: true, - modules: [{ name: 'CoreRuntime', type: 'runtime', entry: './src/index.ts' }] -}; - -const uiDescriptor: PluginDescriptor = { - id: '@esengine/ui', - name: 'UI System', - version: '1.0.0', - category: 'ui', - enabledByDefault: true, - isEnginePlugin: true, - modules: [{ name: 'UIRuntime', type: 'runtime', entry: './src/index.ts' }] -}; - -const tilemapDescriptor: PluginDescriptor = { - id: '@esengine/tilemap', - name: 'Tilemap System', - version: '1.0.0', - category: 'rendering', - enabledByDefault: true, - isEnginePlugin: true, - modules: [{ name: 'TilemapRuntime', type: 'runtime', entry: './src/index.ts' }] -}; - -const behaviorTreeDescriptor: PluginDescriptor = { - id: '@esengine/behavior-tree', - name: 'Behavior Tree', - version: '1.0.0', - category: 'ai', - enabledByDefault: true, - isEnginePlugin: true, - modules: [{ name: 'BehaviorTreeRuntime', type: 'runtime', entry: './src/index.ts' }] -}; - -const physicsDescriptor: PluginDescriptor = { - id: '@esengine/physics-rapier2d', - name: 'Rapier 2D Physics', - version: '1.0.0', - category: 'physics', - enabledByDefault: true, - isEnginePlugin: true, - modules: [{ name: 'PhysicsRuntime', type: 'runtime', entry: './src/runtime.ts' }] -}; - -/** - * 注册所有可用插件 - * 仅注册插件描述信息,不初始化组件和服务 - */ -export function registerAvailablePlugins(): void { - try { - runtimePluginManager.register(createRuntimeOnlyPlugin(coreDescriptor, new CoreRuntimeModule())); - } catch (e) { - console.error('[RuntimeSystems] Failed to register CoreRuntimeModule:', e); - } - - try { - runtimePluginManager.register(createRuntimeOnlyPlugin(uiDescriptor, new UIRuntimeModule())); - } catch (e) { - console.error('[RuntimeSystems] Failed to register UIRuntimeModule:', e); - } - - try { - runtimePluginManager.register(createRuntimeOnlyPlugin(tilemapDescriptor, new TilemapRuntimeModule())); - } catch (e) { - console.error('[RuntimeSystems] Failed to register TilemapRuntimeModule:', e); - } - - try { - runtimePluginManager.register(createRuntimeOnlyPlugin(behaviorTreeDescriptor, new BehaviorTreeRuntimeModule())); - } catch (e) { - console.error('[RuntimeSystems] Failed to register BehaviorTreeRuntimeModule:', e); - } - - try { - runtimePluginManager.register(createRuntimeOnlyPlugin(physicsDescriptor, new PhysicsRuntimeModule())); - } catch (e) { - console.error('[RuntimeSystems] Failed to register PhysicsRuntimeModule:', e); - } -} - -/** - * 初始化运行时(完整流程) - * 用于独立游戏运行时,一次性完成所有初始化 - */ -export async function initializeRuntime( - coreInstance: typeof Core, - config?: RuntimeModuleConfig -): Promise { - registerAvailablePlugins(); - - if (config?.enabledPlugins) { - runtimePluginManager.loadConfig({ enabledPlugins: config.enabledPlugins }); - } else { - // 默认启用所有插件 - for (const plugin of runtimePluginManager.getPlugins()) { - runtimePluginManager.enable(plugin.descriptor.id); - } - } - - await runtimePluginManager.initializeRuntime(coreInstance.services); -} - -/** - * 初始化插件(编辑器用) - * 根据项目配置初始化已启用的插件 - * - * @param coreInstance Core 实例 - * @param enabledPlugins 启用的插件 ID 列表 - */ -export async function initializePluginsForProject( - coreInstance: typeof Core, - enabledPlugins: string[] -): Promise { - // 确保插件已注册 - registerAvailablePlugins(); - - // 加载项目的插件配置 - runtimePluginManager.loadConfig({ enabledPlugins }); - - // 初始化插件(注册组件和服务) - await runtimePluginManager.initializeRuntime(coreInstance.services); -} - -/** - * 创建运行时系统 - */ -export function createRuntimeSystems( - scene: IScene, - bridge: EngineBridge, - config?: RuntimeModuleConfig -): RuntimeSystems { - const isEditor = config?.isEditor ?? false; - - const cameraSystem = new CameraSystem(bridge); - scene.addSystem(cameraSystem); - - const renderSystem = new EngineRenderSystem(bridge, TransformComponent); - - const context: SystemContext = { - core: Core, - engineBridge: bridge, - renderSystem, - isEditor - }; - - runtimePluginManager.createSystemsForScene(scene, context); - - // 注册 UI 渲染提供者到渲染系统 - // Register UI render provider to render system - if (context.uiRenderProvider) { - renderSystem.setUIRenderDataProvider(context.uiRenderProvider); - } - - // 独立运行时始终使用预览模式(屏幕空间 UI) - // Standalone runtime always uses preview mode (screen space UI) - if (!isEditor) { - renderSystem.setPreviewMode(true); - } - - scene.addSystem(renderSystem); - - // 绑定 UIInputSystem 到 canvas(用于 UI 交互) - // Bind UIInputSystem to canvas (for UI interaction) - if (config?.canvasId && context.uiInputSystem) { - const canvas = document.getElementById(config.canvasId) as HTMLCanvasElement; - if (canvas) { - (context.uiInputSystem as UIInputSystem).bindToCanvas(canvas); - } - } - - return { - cameraSystem, - animatorSystem: context.animatorSystem as SpriteAnimatorSystem | undefined, - tilemapSystem: context.tilemapSystem as TilemapRenderingSystem | undefined, - behaviorTreeSystem: context.behaviorTreeSystem as BehaviorTreeExecutionSystem | undefined, - renderSystem, - uiRenderProvider: context.uiRenderProvider as UIRenderDataProvider | undefined - }; -} - diff --git a/packages/platform-web/src/index.ts b/packages/platform-web/src/index.ts index 274c2ea6..83f8739f 100644 --- a/packages/platform-web/src/index.ts +++ b/packages/platform-web/src/index.ts @@ -1,28 +1,32 @@ /** - * Web/H5 平台适配器包 + * @esengine/platform-web + * + * Web/H5 平台适配器 - 仅包含平台差异代码 + * 通用运行时逻辑在 @esengine/runtime-core + * * @packageDocumentation */ -// 引擎桥接 -export { EngineBridge } from './EngineBridge'; -export type { EngineBridgeConfig } from './EngineBridge'; - -// 子系统 +// Web 平台子系统 export { WebCanvasSubsystem } from './subsystems/WebCanvasSubsystem'; export { WebInputSubsystem } from './subsystems/WebInputSubsystem'; export { WebStorageSubsystem } from './subsystems/WebStorageSubsystem'; export { WebWASMSubsystem } from './subsystems/WebWASMSubsystem'; -// 运行时系统配置 -export { - registerAvailablePlugins, - initializeRuntime, - initializePluginsForProject, - createRuntimeSystems -} from './RuntimeSystems'; -export type { RuntimeSystems, RuntimeModuleConfig } from './RuntimeSystems'; +// Web 特定系统 +export { Canvas2DRenderSystem } from './systems/Canvas2DRenderSystem'; -// 工具 export function isWebPlatform(): boolean { return typeof window !== 'undefined' && typeof document !== 'undefined'; } + +export function getWebCanvas(canvasId: string): HTMLCanvasElement | null { + return document.getElementById(canvasId) as HTMLCanvasElement | null; +} + +export function createWebCanvas(width: number, height: number): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; +} diff --git a/packages/platform-web/src/runtime.ts b/packages/platform-web/src/runtime.ts index 811d65b6..b3d162bc 100644 --- a/packages/platform-web/src/runtime.ts +++ b/packages/platform-web/src/runtime.ts @@ -1,123 +1,196 @@ /** * Browser Runtime Entry Point * 浏览器运行时入口 + * + * 使用统一的 GameRuntime 架构,静态导入所有插件 + * Uses the unified GameRuntime architecture with static plugin imports */ -import { Core, Scene, SceneSerializer } from '@esengine/ecs-framework'; -import { EngineBridge } from '@esengine/ecs-engine-bindgen'; -import { TransformComponent, SpriteComponent, SpriteAnimatorComponent, CameraComponent } from '@esengine/ecs-components'; -import { AssetManager, EngineIntegration } from '@esengine/asset-system'; -import { initializeRuntime, createRuntimeSystems, type RuntimeSystems } from './RuntimeSystems'; +import { Core } from '@esengine/ecs-framework'; +import { + GameRuntime, + createGameRuntime, + BrowserPlatformAdapter, + runtimePluginManager, + BrowserFileSystemService +} from '@esengine/runtime-core'; -interface RuntimeConfig { +// 静态导入所有运行时插件(与编辑器保持一致) +// Static import all runtime plugins (consistent with editor) +import { EnginePlugin } from '@esengine/engine-core'; +import { CameraPlugin } from '@esengine/camera'; +import { SpritePlugin } from '@esengine/sprite'; +import { AudioPlugin } from '@esengine/audio'; +import { UIPlugin } from '@esengine/ui'; +import { TilemapPlugin } from '@esengine/tilemap'; +import { BehaviorTreePlugin } from '@esengine/behavior-tree'; +// 使用 runtime 子路径导入,包含 WASM 依赖 +import { PhysicsPlugin } from '@esengine/physics-rapier2d/runtime'; + +// 预注册所有插件(在 GameRuntime 初始化前) +// Pre-register all plugins (before GameRuntime initialization) +const ALL_PLUGINS = [ + EnginePlugin, + CameraPlugin, + SpritePlugin, + AudioPlugin, + UIPlugin, + TilemapPlugin, + BehaviorTreePlugin, + PhysicsPlugin, +]; + +// 注册并启用所有插件(浏览器运行时默认启用所有功能) +for (const plugin of ALL_PLUGINS) { + if (plugin) { + runtimePluginManager.register(plugin); + // 确保所有插件都启用(覆盖 enabledByDefault: false) + runtimePluginManager.enable(plugin.descriptor.id); + } +} + +export interface RuntimeConfig { canvasId: string; width?: number; height?: number; + /** 项目配置文件 URL / Project config file URL */ + projectConfigUrl?: string; + /** 资产目录文件 URL / Asset catalog file URL */ + assetCatalogUrl?: string; + /** 资产基础 URL / Asset base URL */ + assetBaseUrl?: string; } +/** + * 编辑器项目配置文件格式 + * Editor project config file format (ecs-editor.config.json) + */ +interface EditorProjectConfig { + projectType?: string; + plugins?: { + enabledPlugins: string[]; + }; + [key: string]: any; +} + +/** + * Browser Runtime Wrapper + * 浏览器运行时包装器 + */ class BrowserRuntime { - private bridge: EngineBridge; - private systems: RuntimeSystems | null = null; - private animationId: number | null = null; - private assetManager: AssetManager; - private engineIntegration: EngineIntegration; - private canvasId: string; + private _runtime: GameRuntime | null = null; + private _canvasId: string; + private _configUrl?: string; + private _assetCatalogUrl?: string; + private _assetBaseUrl?: string; + private _fileSystem: BrowserFileSystemService | null = null; constructor(config: RuntimeConfig) { - this.canvasId = config.canvasId; - if (!Core.Instance) { - Core.create(); + this._canvasId = config.canvasId; + this._configUrl = config.projectConfigUrl; + this._assetCatalogUrl = config.assetCatalogUrl ?? '/asset-catalog.json'; + this._assetBaseUrl = config.assetBaseUrl ?? '/assets'; + } + + /** + * 从配置文件 URL 加载插件配置 + */ + private async _loadConfigFromUrl(): Promise { + if (!this._configUrl) return; + + try { + const response = await fetch(this._configUrl); + if (!response.ok) { + console.warn(`[BrowserRuntime] Failed to load config from ${this._configUrl}: ${response.status}`); + return; + } + + const editorConfig: EditorProjectConfig = await response.json(); + + // 如果有插件配置,应用到 runtimePluginManager + if (editorConfig.plugins?.enabledPlugins) { + runtimePluginManager.loadConfig({ enabledPlugins: editorConfig.plugins.enabledPlugins }); + console.log('[BrowserRuntime] Loaded plugin config:', editorConfig.plugins.enabledPlugins); + } + } catch (error) { + console.warn('[BrowserRuntime] Error loading config file:', error); } - - if (!Core.scene) { - const runtimeScene = new Scene({ name: 'Runtime Scene' }); - Core.setScene(runtimeScene); - } - - this.bridge = new EngineBridge({ - canvasId: config.canvasId, - width: config.width || window.innerWidth, - height: config.height || window.innerHeight - }); - - this.assetManager = new AssetManager(); - this.engineIntegration = new EngineIntegration(this.assetManager, this.bridge); } async initialize(wasmModule: any): Promise { - await this.bridge.initializeWithModule(wasmModule); + // 从配置文件加载插件配置(如果指定了 URL) + await this._loadConfigFromUrl(); - this.bridge.setPathResolver((path: string) => { - if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('/asset?')) { - return path; - } - return `/asset?path=${encodeURIComponent(path)}`; + // 初始化浏览器文件系统服务(用于资产加载) + // Initialize browser file system service (for asset loading) + this._fileSystem = new BrowserFileSystemService({ + baseUrl: this._assetBaseUrl, + catalogUrl: this._assetCatalogUrl, + enableCache: true + }); + await this._fileSystem.initialize(); + + // 创建浏览器平台适配器 + const platform = new BrowserPlatformAdapter({ + wasmModule: wasmModule }); - this.bridge.setShowGrid(false); - this.bridge.setShowGizmos(false); - - // 初始化模块系统 - await initializeRuntime(Core); - - // 创建运行时系统(传入 canvasId 用于 UI 输入绑定) - this.systems = createRuntimeSystems(Core.scene!, this.bridge, { - canvasId: this.canvasId + // 创建统一运行时 + // 插件已经预注册了,GameRuntime 会检测到并跳过动态加载 + this._runtime = createGameRuntime({ + platform, + canvasId: this._canvasId, + width: window.innerWidth, + height: window.innerHeight, + autoStartRenderLoop: false }); + + await this._runtime.initialize(); + + // 注册文件系统服务到 Core.services(必须在 GameRuntime.initialize 之后,因为 Core 在那时才创建) + // Register file system service to Core.services (must be after GameRuntime.initialize as Core is created there) + const IFileSystemServiceKey = Symbol.for('IFileSystemService'); + if (!Core.services.isRegistered(IFileSystemServiceKey)) { + Core.services.registerInstance(IFileSystemServiceKey, this._fileSystem); + } + + // 设置浏览器特定配置 + this._runtime.setShowGrid(false); + this._runtime.setShowGizmos(false); } async loadScene(sceneUrl: string): Promise { - const response = await fetch(sceneUrl); - const sceneJson = await response.text(); - - if (!Core.scene) { - throw new Error('Core.scene not initialized'); + if (!this._runtime) { + throw new Error('Runtime not initialized'); } - - SceneSerializer.deserialize(Core.scene, sceneJson, { - strategy: 'replace', - preserveIds: true - }); + await this._runtime.loadSceneFromUrl(sceneUrl); } start(): void { - if (this.animationId !== null) return; - - let lastTime = performance.now(); - const loop = () => { - const currentTime = performance.now(); - const deltaTime = (currentTime - lastTime) / 1000; - lastTime = currentTime; - - Core.update(deltaTime); - - this.animationId = requestAnimationFrame(loop); - }; - - loop(); + if (!this._runtime) return; + this._runtime.start(); } stop(): void { - if (this.animationId !== null) { - cancelAnimationFrame(this.animationId); - this.animationId = null; - } + if (!this._runtime) return; + this._runtime.stop(); } handleResize(width: number, height: number): void { - this.bridge.resize(width, height); + if (!this._runtime) return; + this._runtime.resize(width, height); } - getAssetManager(): AssetManager { - return this.assetManager; + get assetManager() { + return this._runtime?.assetManager ?? null; } - getEngineIntegration(): EngineIntegration { - return this.engineIntegration; + get engineIntegration() { + return this._runtime?.engineIntegration ?? null; } - getSystems(): RuntimeSystems | null { - return this.systems; + get gameRuntime(): GameRuntime | null { + return this._runtime; } } @@ -125,8 +198,7 @@ export default { create: (config: RuntimeConfig) => new BrowserRuntime(config), BrowserRuntime, Core, - TransformComponent, - SpriteComponent, - SpriteAnimatorComponent, - CameraComponent + GameRuntime, + createGameRuntime, + BrowserPlatformAdapter }; diff --git a/packages/platform-web/src/systems/Canvas2DRenderSystem.ts b/packages/platform-web/src/systems/Canvas2DRenderSystem.ts index 41689238..e3def8db 100644 --- a/packages/platform-web/src/systems/Canvas2DRenderSystem.ts +++ b/packages/platform-web/src/systems/Canvas2DRenderSystem.ts @@ -4,7 +4,8 @@ */ import { EntitySystem, Matcher, ECSSystem, Core } from '@esengine/ecs-framework'; -import { TransformComponent, SpriteComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent } from '@esengine/sprite'; @ECSSystem('Canvas2DRender', { updateOrder: 1000 }) export class Canvas2DRenderSystem extends EntitySystem { @@ -54,11 +55,12 @@ export class Canvas2DRenderSystem extends EntitySystem { this.ctx.save(); - const x = (transform.position.x || 0) + this.canvas.width / 2; - const y = this.canvas.height / 2 - (transform.position.y || 0); - const width = (sprite.width || 64) * (transform.scale.x || 1); - const height = (sprite.height || 64) * (transform.scale.y || 1); - const rotation = -(transform.rotation.z || 0) * Math.PI / 180; + // 使用世界变换(由 TransformSystem 计算,考虑父级变换) + const x = (transform.worldPosition.x || 0) + this.canvas.width / 2; + const y = this.canvas.height / 2 - (transform.worldPosition.y || 0); + const width = (sprite.width || 64) * (transform.worldScale.x || 1); + const height = (sprite.height || 64) * (transform.worldScale.y || 1); + const rotation = -(transform.worldRotation.z || 0) * Math.PI / 180; this.ctx.translate(x, y); this.ctx.rotate(rotation); diff --git a/packages/platform-web/tsconfig.json b/packages/platform-web/tsconfig.json index b90bddc2..1f5f66b7 100644 --- a/packages/platform-web/tsconfig.json +++ b/packages/platform-web/tsconfig.json @@ -20,5 +20,8 @@ "noEmit": false }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../platform-common" } + ] } diff --git a/packages/platform-wechat/package.json b/packages/platform-wechat/package.json index 3c358d5c..1dbf8289 100644 --- a/packages/platform-wechat/package.json +++ b/packages/platform-wechat/package.json @@ -31,7 +31,7 @@ ], "author": "yhh", "license": "MIT", - "peerDependencies": { + "dependencies": { "@esengine/ecs-framework": "workspace:*", "@esengine/platform-common": "workspace:*" }, diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json new file mode 100644 index 00000000..cc43ea9d --- /dev/null +++ b/packages/runtime-core/package.json @@ -0,0 +1,41 @@ +{ + "name": "@esengine/runtime-core", + "version": "1.0.0", + "description": "Runtime core - plugin management and system initialization", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/ecs-engine-bindgen": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/asset-system": "workspace:*", + "@esengine/build-config": "workspace:*", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "runtime", + "plugin" + ], + "author": "yhh", + "license": "MIT" +} diff --git a/packages/runtime-core/src/GameRuntime.ts b/packages/runtime-core/src/GameRuntime.ts new file mode 100644 index 00000000..ce1b5e9a --- /dev/null +++ b/packages/runtime-core/src/GameRuntime.ts @@ -0,0 +1,824 @@ +/** + * Unified Game Runtime + * 统一游戏运行时 + * + * 这是编辑器预览和独立运行的统一入口点 + * This is the unified entry point for editor preview and standalone runtime + */ + +import { Core, Scene, SceneSerializer, HierarchySystem } from '@esengine/ecs-framework'; +import { EngineBridge, EngineRenderSystem, CameraSystem } from '@esengine/ecs-engine-bindgen'; +import { TransformComponent, TransformSystem } from '@esengine/engine-core'; +import { AssetManager, EngineIntegration } from '@esengine/asset-system'; +import { + runtimePluginManager, + type SystemContext, + type IRuntimeModule +} from './PluginManager'; +import { + loadEnabledPlugins, + type PluginPackageInfo, + type ProjectPluginConfig +} from './PluginLoader'; +import { + BUILTIN_PLUGIN_PACKAGES, + mergeProjectConfig, + type ProjectConfig +} from './ProjectConfig'; +import type { IPlatformAdapter, PlatformAdapterConfig } from './IPlatformAdapter'; + +/** + * 运行时配置 + * Runtime configuration + */ +export interface GameRuntimeConfig { + /** 平台适配器 */ + platform: IPlatformAdapter; + /** 项目配置 */ + projectConfig?: Partial; + /** Canvas ID */ + canvasId: string; + /** 初始宽度 */ + width?: number; + /** 初始高度 */ + height?: number; + /** 是否自动启动渲染循环 */ + autoStartRenderLoop?: boolean; + /** UI 画布尺寸 */ + uiCanvasSize?: { width: number; height: number }; + /** + * 跳过内部插件加载 + * 编辑器模式下,插件由 editor-core 的 PluginManager 管理 + * Skip internal plugin loading - editor mode uses editor-core's PluginManager + */ + skipPluginLoading?: boolean; +} + +/** + * 运行时状态 + * Runtime state + */ +export interface RuntimeState { + initialized: boolean; + running: boolean; + paused: boolean; +} + +/** + * 统一游戏运行时 + * Unified Game Runtime + * + * 提供编辑器预览和独立运行的统一实现 + * Provides unified implementation for editor preview and standalone runtime + */ +export class GameRuntime { + private _platform: IPlatformAdapter; + private _bridge: EngineBridge | null = null; + private _scene: Scene | null = null; + private _renderSystem: EngineRenderSystem | null = null; + private _cameraSystem: CameraSystem | null = null; + private _assetManager: AssetManager | null = null; + private _engineIntegration: EngineIntegration | null = null; + private _projectConfig: ProjectConfig; + private _config: GameRuntimeConfig; + + private _state: RuntimeState = { + initialized: false, + running: false, + paused: false + }; + + private _animationFrameId: number | null = null; + private _lastTime = 0; + + // 系统上下文,供插件使用 + private _systemContext: SystemContext | null = null; + + // 场景快照(用于编辑器预览后恢复) + private _sceneSnapshot: string | null = null; + + // Gizmo 注册表注入函数 + private _gizmoDataProvider?: (component: any, entity: any, isSelected: boolean) => any; + private _hasGizmoProvider?: (component: any) => boolean; + + constructor(config: GameRuntimeConfig) { + this._config = config; + this._platform = config.platform; + this._projectConfig = mergeProjectConfig(config.projectConfig || {}); + } + + /** + * 获取运行时状态 + */ + get state(): RuntimeState { + return { ...this._state }; + } + + /** + * 获取场景 + */ + get scene(): Scene | null { + return this._scene; + } + + /** + * 获取引擎桥接 + */ + get bridge(): EngineBridge | null { + return this._bridge; + } + + /** + * 获取渲染系统 + */ + get renderSystem(): EngineRenderSystem | null { + return this._renderSystem; + } + + /** + * 获取资产管理器 + */ + get assetManager(): AssetManager | null { + return this._assetManager; + } + + /** + * 获取引擎集成 + */ + get engineIntegration(): EngineIntegration | null { + return this._engineIntegration; + } + + /** + * 获取系统上下文 + */ + get systemContext(): SystemContext | null { + return this._systemContext; + } + + /** + * 更新系统上下文(用于编辑器模式下同步外部创建的系统引用) + * Update system context (for syncing externally created system references in editor mode) + */ + updateSystemContext(updates: Partial): void { + if (this._systemContext) { + Object.assign(this._systemContext, updates); + } + } + + /** + * 获取平台适配器 + */ + get platform(): IPlatformAdapter { + return this._platform; + } + + /** + * 初始化运行时 + * Initialize runtime + */ + async initialize(): Promise { + if (this._state.initialized) { + return; + } + + try { + // 1. 初始化平台 + await this._platform.initialize({ + canvasId: this._config.canvasId, + width: this._config.width, + height: this._config.height, + isEditor: this._platform.isEditorMode() + }); + + // 2. 获取 WASM 模块并创建引擎桥接 + const wasmModule = await this._platform.getWasmModule(); + this._bridge = new EngineBridge({ + canvasId: this._config.canvasId, + width: this._config.width, + height: this._config.height + }); + await this._bridge.initializeWithModule(wasmModule); + + // 3. 设置路径解析器 + this._bridge.setPathResolver((path: string) => { + return this._platform.pathResolver.resolve(path); + }); + + // 4. 初始化 ECS Core + if (!Core.Instance) { + Core.create({ debug: false }); + } + + // 5. 创建或获取场景 + if (Core.scene) { + this._scene = Core.scene as Scene; + } else { + this._scene = new Scene({ name: 'GameScene' }); + Core.setScene(this._scene); + } + + // 6. 添加基础系统 + this._scene.addSystem(new HierarchySystem()); + this._scene.addSystem(new TransformSystem()); + + this._cameraSystem = new CameraSystem(this._bridge); + this._scene.addSystem(this._cameraSystem); + + this._renderSystem = new EngineRenderSystem(this._bridge, TransformComponent); + + // 7. 设置 UI 画布尺寸 + if (this._config.uiCanvasSize) { + this._renderSystem.setUICanvasSize( + this._config.uiCanvasSize.width, + this._config.uiCanvasSize.height + ); + } else { + this._renderSystem.setUICanvasSize(1920, 1080); + } + + // 8. 创建资产系统 + this._assetManager = new AssetManager(); + this._engineIntegration = new EngineIntegration(this._assetManager, this._bridge); + + // 9. 加载并初始化插件(编辑器模式下跳过,由 editor-core 的 PluginManager 处理) + if (!this._config.skipPluginLoading) { + await this._initializePlugins(); + } + + // 10. 创建系统上下文 + this._systemContext = { + isEditor: this._platform.isEditorMode(), + engineBridge: this._bridge, + renderSystem: this._renderSystem, + assetManager: this._assetManager + }; + + // 11. 让插件创建系统(编辑器模式下跳过,由 EngineService.initializeModuleSystems 处理) + if (!this._config.skipPluginLoading) { + runtimePluginManager.createSystemsForScene(this._scene, this._systemContext); + } + + // 11. 设置 UI 渲染数据提供者(如果有) + if (this._systemContext.uiRenderProvider) { + this._renderSystem.setUIRenderDataProvider(this._systemContext.uiRenderProvider); + } + + // 12. 添加渲染系统(在所有其他系统之后) + this._scene.addSystem(this._renderSystem); + + // 13. 启动默认 world + const defaultWorld = Core.worldManager.getWorld('__default__'); + if (defaultWorld && !defaultWorld.isActive) { + defaultWorld.start(); + } + + // 14. 编辑器模式下的特殊处理 + if (this._platform.isEditorMode()) { + // 禁用游戏逻辑系统 + this._disableGameLogicSystems(); + } + + this._state.initialized = true; + + // 15. 自动启动渲染循环 + if (this._config.autoStartRenderLoop !== false) { + this._startRenderLoop(); + } + } catch (error) { + console.error('[GameRuntime] Initialization failed:', error); + throw error; + } + } + + /** + * 加载并初始化插件 + */ + private async _initializePlugins(): Promise { + // 检查是否已有插件注册(静态导入场景) + // Check if plugins are already registered (static import scenario) + const hasPlugins = runtimePluginManager.getPlugins().length > 0; + + if (!hasPlugins) { + // 没有预注册的插件,尝试动态加载 + // No pre-registered plugins, try dynamic loading + await loadEnabledPlugins( + { plugins: this._projectConfig.plugins }, + BUILTIN_PLUGIN_PACKAGES + ); + } + + // 初始化插件(注册组件和服务) + await runtimePluginManager.initializeRuntime(Core.services); + } + + /** + * 禁用游戏逻辑系统(编辑器模式) + */ + private _disableGameLogicSystems(): void { + const ctx = this._systemContext; + if (!ctx) return; + + // 这些系统由插件创建,通过 context 传递引用 + if (ctx.animatorSystem) { + ctx.animatorSystem.enabled = false; + } + if (ctx.behaviorTreeSystem) { + ctx.behaviorTreeSystem.enabled = false; + } + if (ctx.physicsSystem) { + ctx.physicsSystem.enabled = false; + } + } + + /** + * 启用游戏逻辑系统(预览/运行模式) + */ + private _enableGameLogicSystems(): void { + const ctx = this._systemContext; + if (!ctx) return; + + if (ctx.animatorSystem) { + ctx.animatorSystem.enabled = true; + } + if (ctx.behaviorTreeSystem) { + ctx.behaviorTreeSystem.enabled = true; + ctx.behaviorTreeSystem.startAllAutoStartTrees?.(); + } + if (ctx.physicsSystem) { + ctx.physicsSystem.enabled = true; + } + } + + /** + * 启动渲染循环 + */ + private _startRenderLoop(): void { + if (this._animationFrameId !== null) { + return; + } + this._lastTime = performance.now(); + this._renderLoop(); + } + + /** + * 渲染循环 + */ + private _renderLoop = (): void => { + const currentTime = performance.now(); + const deltaTime = (currentTime - this._lastTime) / 1000; + this._lastTime = currentTime; + + // 更新 ECS + Core.update(deltaTime); + + this._animationFrameId = requestAnimationFrame(this._renderLoop); + }; + + /** + * 停止渲染循环 + */ + private _stopRenderLoop(): void { + if (this._animationFrameId !== null) { + cancelAnimationFrame(this._animationFrameId); + this._animationFrameId = null; + } + } + + /** + * 开始运行(启用游戏逻辑) + * Start running (enable game logic) + */ + start(): void { + if (!this._state.initialized || this._state.running) { + return; + } + + this._state.running = true; + this._state.paused = false; + + // 启用预览模式 + if (this._renderSystem) { + this._renderSystem.setPreviewMode(true); + } + + // 启用游戏逻辑系统 + this._enableGameLogicSystems(); + + // 绑定 UI 输入 + const ctx = this._systemContext; + if (ctx?.uiInputSystem && this._config.canvasId) { + const canvas = document.getElementById(this._config.canvasId) as HTMLCanvasElement; + if (canvas) { + ctx.uiInputSystem.bindToCanvas(canvas); + } + } + + // 确保渲染循环在运行 + this._startRenderLoop(); + } + + /** + * 暂停运行 + * Pause running + */ + pause(): void { + if (!this._state.running || this._state.paused) { + return; + } + this._state.paused = true; + } + + /** + * 恢复运行 + * Resume running + */ + resume(): void { + if (!this._state.running || !this._state.paused) { + return; + } + this._state.paused = false; + } + + /** + * 停止运行(禁用游戏逻辑) + * Stop running (disable game logic) + */ + stop(): void { + if (!this._state.running) { + return; + } + + this._state.running = false; + this._state.paused = false; + + // 禁用预览模式 + if (this._renderSystem) { + this._renderSystem.setPreviewMode(false); + } + + // 解绑 UI 输入 + const ctx = this._systemContext; + if (ctx?.uiInputSystem) { + ctx.uiInputSystem.unbind?.(); + } + + // 禁用游戏逻辑系统 + this._disableGameLogicSystems(); + + // 重置物理系统 + if (ctx?.physicsSystem) { + ctx.physicsSystem.reset?.(); + } + } + + /** + * 单步执行 + * Step forward one frame + */ + step(): void { + if (!this._state.initialized) { + return; + } + + // 启用系统执行一帧 + this._enableGameLogicSystems(); + Core.update(1 / 60); + this._disableGameLogicSystems(); + } + + /** + * 加载场景数据 + * Load scene data + */ + async loadScene(sceneData: string | object): Promise { + if (!this._scene) { + throw new Error('Scene not initialized'); + } + + const jsonStr = typeof sceneData === 'string' + ? sceneData + : JSON.stringify(sceneData); + + SceneSerializer.deserialize(this._scene, jsonStr, { + strategy: 'replace', + preserveIds: true + }); + } + + /** + * 从 URL 加载场景 + * Load scene from URL + */ + async loadSceneFromUrl(url: string): Promise { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to load scene from ${url}: ${response.status}`); + } + const sceneJson = await response.text(); + await this.loadScene(sceneJson); + } + + /** + * 调整视口大小 + * Resize viewport + */ + resize(width: number, height: number): void { + if (this._bridge) { + this._bridge.resize(width, height); + } + this._platform.resize(width, height); + } + + /** + * 设置相机 + * Set camera + */ + setCamera(config: { x: number; y: number; zoom: number; rotation?: number }): void { + if (this._bridge) { + this._bridge.setCamera({ + x: config.x, + y: config.y, + zoom: config.zoom, + rotation: config.rotation ?? 0 + }); + } + } + + /** + * 获取相机状态 + * Get camera state + */ + getCamera(): { x: number; y: number; zoom: number; rotation: number } { + if (this._bridge) { + return this._bridge.getCamera(); + } + return { x: 0, y: 0, zoom: 1, rotation: 0 }; + } + + /** + * 设置网格显示 + * Set grid visibility + */ + setShowGrid(show: boolean): void { + if (this._bridge) { + this._bridge.setShowGrid(show); + } + } + + /** + * 设置 Gizmo 显示 + * Set gizmo visibility + */ + setShowGizmos(show: boolean): void { + if (this._renderSystem) { + this._renderSystem.setShowGizmos(show); + } + } + + /** + * 设置清除颜色 + * Set clear color + */ + setClearColor(r: number, g: number, b: number, a: number = 1.0): void { + if (this._bridge) { + this._bridge.setClearColor(r, g, b, a); + } + } + + /** + * 获取统计信息 + * Get stats + */ + getStats(): { fps: number; drawCalls: number; spriteCount: number } { + if (!this._renderSystem) { + return { fps: 0, drawCalls: 0, spriteCount: 0 }; + } + + const engineStats = this._renderSystem.getStats(); + return { + fps: engineStats?.fps ?? 0, + drawCalls: engineStats?.drawCalls ?? 0, + spriteCount: this._renderSystem.spriteCount + }; + } + + // ===== 编辑器特有功能 ===== + // ===== Editor-specific features ===== + + /** + * 设置 Gizmo 注册表(编辑器模式) + * Set gizmo registry (editor mode) + */ + setGizmoRegistry( + gizmoDataProvider: (component: any, entity: any, isSelected: boolean) => any, + hasGizmoProvider: (component: any) => boolean + ): void { + this._gizmoDataProvider = gizmoDataProvider; + this._hasGizmoProvider = hasGizmoProvider; + + if (this._renderSystem) { + this._renderSystem.setGizmoRegistry(gizmoDataProvider, hasGizmoProvider); + } + } + + /** + * 设置选中的实体 ID(编辑器模式) + * Set selected entity IDs (editor mode) + */ + setSelectedEntityIds(ids: number[]): void { + if (this._renderSystem) { + this._renderSystem.setSelectedEntityIds(ids); + } + } + + /** + * 设置变换工具模式(编辑器模式) + * Set transform tool mode (editor mode) + */ + setTransformMode(mode: 'select' | 'move' | 'rotate' | 'scale'): void { + if (this._renderSystem) { + this._renderSystem.setTransformMode(mode); + } + } + + /** + * 获取变换工具模式 + * Get transform tool mode + */ + getTransformMode(): 'select' | 'move' | 'rotate' | 'scale' { + return this._renderSystem?.getTransformMode() ?? 'select'; + } + + /** + * 设置 UI 画布尺寸 + * Set UI canvas size + */ + setUICanvasSize(width: number, height: number): void { + if (this._renderSystem) { + this._renderSystem.setUICanvasSize(width, height); + } + } + + /** + * 获取 UI 画布尺寸 + * Get UI canvas size + */ + getUICanvasSize(): { width: number; height: number } { + return this._renderSystem?.getUICanvasSize() ?? { width: 0, height: 0 }; + } + + /** + * 设置 UI 画布边界显示 + * Set UI canvas boundary visibility + */ + setShowUICanvasBoundary(show: boolean): void { + if (this._renderSystem) { + this._renderSystem.setShowUICanvasBoundary(show); + } + } + + /** + * 获取 UI 画布边界显示状态 + * Get UI canvas boundary visibility + */ + getShowUICanvasBoundary(): boolean { + return this._renderSystem?.getShowUICanvasBoundary() ?? true; + } + + // ===== 场景快照 API ===== + // ===== Scene Snapshot API ===== + + /** + * 保存场景快照 + * Save scene snapshot + */ + saveSceneSnapshot(): boolean { + if (!this._scene) { + console.warn('[GameRuntime] Cannot save snapshot: no scene'); + return false; + } + + try { + this._sceneSnapshot = SceneSerializer.serialize(this._scene, { + format: 'json', + pretty: false, + includeMetadata: false + }) as string; + return true; + } catch (error) { + console.error('[GameRuntime] Failed to save snapshot:', error); + return false; + } + } + + /** + * 恢复场景快照 + * Restore scene snapshot + */ + async restoreSceneSnapshot(): Promise { + if (!this._scene || !this._sceneSnapshot) { + console.warn('[GameRuntime] Cannot restore: no scene or snapshot'); + return false; + } + + try { + // 清除缓存 + const ctx = this._systemContext; + if (ctx?.tilemapSystem) { + ctx.tilemapSystem.clearCache?.(); + } + + // 反序列化场景 + SceneSerializer.deserialize(this._scene, this._sceneSnapshot, { + strategy: 'replace', + preserveIds: true + }); + + this._sceneSnapshot = null; + return true; + } catch (error) { + console.error('[GameRuntime] Failed to restore snapshot:', error); + return false; + } + } + + /** + * 检查是否有快照 + * Check if snapshot exists + */ + hasSnapshot(): boolean { + return this._sceneSnapshot !== null; + } + + // ===== 多视口 API ===== + // ===== Multi-viewport API ===== + + /** + * 注册视口 + * Register viewport + */ + registerViewport(id: string, canvasId: string): void { + if (this._bridge) { + this._bridge.registerViewport(id, canvasId); + } + } + + /** + * 注销视口 + * Unregister viewport + */ + unregisterViewport(id: string): void { + if (this._bridge) { + this._bridge.unregisterViewport(id); + } + } + + /** + * 设置活动视口 + * Set active viewport + */ + setActiveViewport(id: string): boolean { + if (this._bridge) { + return this._bridge.setActiveViewport(id); + } + return false; + } + + /** + * 释放资源 + * Dispose resources + */ + dispose(): void { + this.stop(); + this._stopRenderLoop(); + + if (this._assetManager) { + this._assetManager.dispose(); + this._assetManager = null; + } + + this._engineIntegration = null; + this._scene = null; + + if (this._bridge) { + this._bridge.dispose(); + this._bridge = null; + } + + this._renderSystem = null; + this._cameraSystem = null; + this._systemContext = null; + this._platform.dispose(); + + this._state.initialized = false; + } +} + +/** + * 创建游戏运行时实例 + * Create game runtime instance + */ +export function createGameRuntime(config: GameRuntimeConfig): GameRuntime { + return new GameRuntime(config); +} diff --git a/packages/runtime-core/src/IPlatformAdapter.ts b/packages/runtime-core/src/IPlatformAdapter.ts new file mode 100644 index 00000000..739a2231 --- /dev/null +++ b/packages/runtime-core/src/IPlatformAdapter.ts @@ -0,0 +1,152 @@ +/** + * Platform Adapter Interface + * 平台适配器接口 + * + * 定义不同平台(编辑器、浏览器、原生)需要实现的适配器接口 + * Defines the adapter interface that different platforms need to implement + */ + +/** + * 资源路径解析器 + * Asset path resolver + */ +export interface IPathResolver { + /** + * 解析资源路径为可加载的 URL + * Resolve asset path to a loadable URL + */ + resolve(path: string): string; +} + +/** + * 平台能力标识 + * Platform capability flags + */ +export interface PlatformCapabilities { + /** 是否支持文件系统访问 / Supports file system access */ + fileSystem: boolean; + /** 是否支持热重载 / Supports hot reload */ + hotReload: boolean; + /** 是否支持 Gizmo 显示 / Supports gizmo display */ + gizmos: boolean; + /** 是否支持网格显示 / Supports grid display */ + grid: boolean; + /** 是否支持场景编辑 / Supports scene editing */ + sceneEditing: boolean; +} + +/** + * 平台适配器配置 + * Platform adapter configuration + */ +export interface PlatformAdapterConfig { + /** Canvas 元素 ID */ + canvasId: string; + /** 初始宽度 */ + width?: number; + /** 初始高度 */ + height?: number; + /** 是否为编辑器模式 */ + isEditor?: boolean; +} + +/** + * 平台适配器接口 + * Platform adapter interface + * + * 不同平台通过实现此接口来提供平台特定的功能 + * Different platforms implement this interface to provide platform-specific functionality + */ +export interface IPlatformAdapter { + /** + * 平台名称 + * Platform name + */ + readonly name: string; + + /** + * 平台能力 + * Platform capabilities + */ + readonly capabilities: PlatformCapabilities; + + /** + * 路径解析器 + * Path resolver + */ + readonly pathResolver: IPathResolver; + + /** + * 初始化平台 + * Initialize platform + */ + initialize(config: PlatformAdapterConfig): Promise; + + /** + * 获取 WASM 模块 + * Get WASM module + * + * 不同平台可能以不同方式加载 WASM + * Different platforms may load WASM in different ways + */ + getWasmModule(): Promise; + + /** + * 获取 Canvas 元素 + * Get canvas element + */ + getCanvas(): HTMLCanvasElement | null; + + /** + * 调整视口大小 + * Resize viewport + */ + resize(width: number, height: number): void; + + /** + * 获取当前视口尺寸 + * Get current viewport size + */ + getViewportSize(): { width: number; height: number }; + + /** + * 是否为编辑器模式 + * Whether in editor mode + */ + isEditorMode(): boolean; + + /** + * 设置是否显示网格(仅编辑器模式有效) + * Set whether to show grid (only effective in editor mode) + */ + setShowGrid?(show: boolean): void; + + /** + * 设置是否显示 Gizmos(仅编辑器模式有效) + * Set whether to show gizmos (only effective in editor mode) + */ + setShowGizmos?(show: boolean): void; + + /** + * 释放资源 + * Dispose resources + */ + dispose(): void; +} + +/** + * 默认路径解析器(直接返回路径) + * Default path resolver (returns path as-is) + */ +export class DefaultPathResolver implements IPathResolver { + resolve(path: string): string { + // 如果已经是 URL,直接返回 + if (path.startsWith('http://') || + path.startsWith('https://') || + path.startsWith('data:') || + path.startsWith('blob:')) { + return path; + } + return path; + } +} diff --git a/packages/runtime-core/src/PluginLoader.ts b/packages/runtime-core/src/PluginLoader.ts new file mode 100644 index 00000000..2b7eb2ba --- /dev/null +++ b/packages/runtime-core/src/PluginLoader.ts @@ -0,0 +1,118 @@ +import { runtimePluginManager, type IPlugin } from './PluginManager'; + +export interface PluginPackageInfo { + plugin: boolean; + pluginExport: string; + category?: string; + isEnginePlugin?: boolean; +} + +export interface PluginConfig { + enabled: boolean; + options?: Record; +} + +export interface ProjectPluginConfig { + plugins: Record; +} + +interface LoadedPluginInfo { + id: string; + plugin: IPlugin; + packageInfo: PluginPackageInfo; +} + +const loadedPlugins = new Map(); + +/** + * 从模块动态加载插件 + * @param packageId 包 ID,如 '@esengine/sprite' + * @param packageInfo 包的 esengine 配置 + */ +export async function loadPlugin( + packageId: string, + packageInfo: PluginPackageInfo +): Promise { + if (loadedPlugins.has(packageId)) { + return loadedPlugins.get(packageId)!.plugin; + } + + try { + const module = await import(/* @vite-ignore */ packageId); + const exportName = packageInfo.pluginExport || 'default'; + const plugin = module[exportName] as IPlugin; + + if (!plugin || !plugin.descriptor) { + console.warn(`[PluginLoader] Invalid plugin export from ${packageId}`); + return null; + } + + loadedPlugins.set(packageId, { + id: packageId, + plugin, + packageInfo + }); + + return plugin; + } catch (error) { + console.error(`[PluginLoader] Failed to load plugin ${packageId}:`, error); + return null; + } +} + +/** + * 根据项目配置加载所有启用的插件 + */ +export async function loadEnabledPlugins( + config: ProjectPluginConfig, + packageInfoMap: Record +): Promise { + const sortedPlugins: Array<{ id: string; info: PluginPackageInfo }> = []; + + for (const [id, pluginConfig] of Object.entries(config.plugins)) { + if (!pluginConfig.enabled) continue; + + const packageInfo = packageInfoMap[id]; + if (!packageInfo) { + console.warn(`[PluginLoader] No package info for ${id}, skipping`); + continue; + } + + sortedPlugins.push({ id, info: packageInfo }); + } + + // 引擎核心插件优先加载 + sortedPlugins.sort((a, b) => { + if (a.info.isEnginePlugin && !b.info.isEnginePlugin) return -1; + if (!a.info.isEnginePlugin && b.info.isEnginePlugin) return 1; + return 0; + }); + + for (const { id, info } of sortedPlugins) { + const plugin = await loadPlugin(id, info); + if (plugin) { + runtimePluginManager.register(plugin); + } + } +} + +/** + * 注册预加载的插件(用于已静态导入的插件) + */ +export function registerStaticPlugin(plugin: IPlugin): void { + runtimePluginManager.register(plugin); +} + +/** + * 获取已加载的插件列表 + */ +export function getLoadedPlugins(): IPlugin[] { + return Array.from(loadedPlugins.values()).map(info => info.plugin); +} + +/** + * 重置插件加载器状态 + */ +export function resetPluginLoader(): void { + loadedPlugins.clear(); +} diff --git a/packages/runtime-core/src/PluginManager.ts b/packages/runtime-core/src/PluginManager.ts new file mode 100644 index 00000000..c3e10e63 --- /dev/null +++ b/packages/runtime-core/src/PluginManager.ts @@ -0,0 +1,166 @@ +/** + * Runtime Plugin Manager + * 运行时插件管理器 + */ + +import { ComponentRegistry, ServiceContainer } from '@esengine/ecs-framework'; +import type { IScene } from '@esengine/ecs-framework'; + +export interface SystemContext { + isEditor: boolean; + [key: string]: any; +} + +export interface PluginDescriptor { + id: string; + name: string; + version: string; + description?: string; + category?: string; + enabledByDefault?: boolean; + isEnginePlugin?: boolean; +} + +export interface IRuntimeModule { + registerComponents?(registry: typeof ComponentRegistry): void; + registerServices?(services: ServiceContainer): void; + createSystems?(scene: IScene, context: SystemContext): void; + onSystemsCreated?(scene: IScene, context: SystemContext): void; + onInitialize?(): Promise; + onDestroy?(): void; +} + +export interface IPlugin { + readonly descriptor: PluginDescriptor; + readonly runtimeModule?: IRuntimeModule; +} + +export class RuntimePluginManager { + private _plugins = new Map(); + private _enabledPlugins = new Set(); + private _bInitialized = false; + + register(plugin: IPlugin): void { + const id = plugin.descriptor.id; + if (this._plugins.has(id)) { + return; + } + this._plugins.set(id, plugin); + if (plugin.descriptor.enabledByDefault !== false) { + this._enabledPlugins.add(id); + } + } + + enable(pluginId: string): void { + this._enabledPlugins.add(pluginId); + } + + disable(pluginId: string): void { + this._enabledPlugins.delete(pluginId); + } + + isEnabled(pluginId: string): boolean { + return this._enabledPlugins.has(pluginId); + } + + loadConfig(config: { enabledPlugins: string[] }): void { + this._enabledPlugins.clear(); + for (const id of config.enabledPlugins) { + this._enabledPlugins.add(id); + } + // 始终启用引擎核心插件 + for (const [id, plugin] of this._plugins) { + if (plugin.descriptor.isEnginePlugin) { + this._enabledPlugins.add(id); + } + } + } + + async initializeRuntime(services: ServiceContainer): Promise { + if (this._bInitialized) { + return; + } + + for (const [id, plugin] of this._plugins) { + if (!this._enabledPlugins.has(id)) continue; + const mod = plugin.runtimeModule; + if (mod?.registerComponents) { + try { + mod.registerComponents(ComponentRegistry); + } catch (e) { + console.error(`[PluginManager] Failed to register components for ${id}:`, e); + } + } + } + + for (const [id, plugin] of this._plugins) { + if (!this._enabledPlugins.has(id)) continue; + const mod = plugin.runtimeModule; + if (mod?.registerServices) { + try { + mod.registerServices(services); + } catch (e) { + console.error(`[PluginManager] Failed to register services for ${id}:`, e); + } + } + } + + for (const [id, plugin] of this._plugins) { + if (!this._enabledPlugins.has(id)) continue; + const mod = plugin.runtimeModule; + if (mod?.onInitialize) { + try { + await mod.onInitialize(); + } catch (e) { + console.error(`[PluginManager] Failed to initialize ${id}:`, e); + } + } + } + + this._bInitialized = true; + } + + createSystemsForScene(scene: IScene, context: SystemContext): void { + // Phase 1: 创建系统 + for (const [id, plugin] of this._plugins) { + if (!this._enabledPlugins.has(id)) continue; + const mod = plugin.runtimeModule; + if (mod?.createSystems) { + try { + mod.createSystems(scene, context); + } catch (e) { + console.error(`[PluginManager] Failed to create systems for ${id}:`, e); + } + } + } + + // Phase 2: 连接跨插件依赖 + for (const [id, plugin] of this._plugins) { + if (!this._enabledPlugins.has(id)) continue; + const mod = plugin.runtimeModule; + if (mod?.onSystemsCreated) { + try { + mod.onSystemsCreated(scene, context); + } catch (e) { + console.error(`[PluginManager] Failed to wire dependencies for ${id}:`, e); + } + } + } + } + + getPlugins(): IPlugin[] { + return Array.from(this._plugins.values()); + } + + getPlugin(id: string): IPlugin | undefined { + return this._plugins.get(id); + } + + reset(): void { + this._plugins.clear(); + this._enabledPlugins.clear(); + this._bInitialized = false; + } +} + +export const runtimePluginManager = new RuntimePluginManager(); diff --git a/packages/runtime-core/src/ProjectConfig.ts b/packages/runtime-core/src/ProjectConfig.ts new file mode 100644 index 00000000..9297438b --- /dev/null +++ b/packages/runtime-core/src/ProjectConfig.ts @@ -0,0 +1,128 @@ +import type { PluginPackageInfo, PluginConfig } from './PluginLoader'; + +export interface ProjectConfig { + name: string; + version: string; + plugins: Record; +} + +/** + * 内置引擎插件的包信息 + * 这些信息在构建时从各包的 package.json 中提取 + */ +export const BUILTIN_PLUGIN_PACKAGES: Record = { + '@esengine/engine-core': { + plugin: true, + pluginExport: 'EnginePlugin', + category: 'core', + isEnginePlugin: true + }, + '@esengine/camera': { + plugin: true, + pluginExport: 'CameraPlugin', + category: 'core', + isEnginePlugin: true + }, + '@esengine/sprite': { + plugin: true, + pluginExport: 'SpritePlugin', + category: 'rendering', + isEnginePlugin: true + }, + '@esengine/audio': { + plugin: true, + pluginExport: 'AudioPlugin', + category: 'audio', + isEnginePlugin: true + }, + '@esengine/ui': { + plugin: true, + pluginExport: 'UIPlugin', + category: 'ui' + }, + '@esengine/tilemap': { + plugin: true, + pluginExport: 'TilemapPlugin', + category: 'tilemap' + }, + '@esengine/behavior-tree': { + plugin: true, + pluginExport: 'BehaviorTreePlugin', + category: 'ai' + }, + '@esengine/physics-rapier2d': { + plugin: true, + pluginExport: 'PhysicsPlugin', + category: 'physics' + } +}; + +/** + * 创建默认项目配置 + */ +export function createDefaultProjectConfig(): ProjectConfig { + return { + name: 'New Project', + version: '1.0.0', + plugins: { + '@esengine/engine-core': { enabled: true }, + '@esengine/camera': { enabled: true }, + '@esengine/sprite': { enabled: true }, + '@esengine/audio': { enabled: true }, + '@esengine/ui': { enabled: true }, + '@esengine/tilemap': { enabled: false }, + '@esengine/behavior-tree': { enabled: false }, + '@esengine/physics-rapier2d': { enabled: false } + } + }; +} + +/** + * 合并用户配置与默认配置 + */ +export function mergeProjectConfig( + userConfig: Partial +): ProjectConfig { + const defaultConfig = createDefaultProjectConfig(); + + return { + name: userConfig.name || defaultConfig.name, + version: userConfig.version || defaultConfig.version, + plugins: { + ...defaultConfig.plugins, + ...userConfig.plugins + } + }; +} + +/** + * 从编辑器的 enabledPlugins 列表创建项目配置 + * Create project config from editor's enabledPlugins list + * + * @param enabledPlugins - 启用的插件 ID 列表 / List of enabled plugin IDs + */ +export function createProjectConfigFromEnabledList( + enabledPlugins: string[] +): ProjectConfig { + const defaultConfig = createDefaultProjectConfig(); + + // 先禁用所有非核心插件 + // First disable all non-core plugins + const plugins: Record = {}; + + for (const id of Object.keys(defaultConfig.plugins)) { + const packageInfo = BUILTIN_PLUGIN_PACKAGES[id]; + // 核心插件始终启用 + // Core plugins are always enabled + if (packageInfo?.isEnginePlugin) { + plugins[id] = { enabled: true }; + } else { + plugins[id] = { enabled: enabledPlugins.includes(id) }; + } + } + + return { + ...defaultConfig, + plugins + }; +} diff --git a/packages/runtime-core/src/RuntimeBootstrap.ts b/packages/runtime-core/src/RuntimeBootstrap.ts new file mode 100644 index 00000000..93933e5c --- /dev/null +++ b/packages/runtime-core/src/RuntimeBootstrap.ts @@ -0,0 +1,66 @@ +/** + * Runtime Bootstrap + * 运行时启动器 - 提供通用的初始化流程 + */ + +import { Core } from '@esengine/ecs-framework'; +import type { IScene } from '@esengine/ecs-framework'; +import { + runtimePluginManager, + type IPlugin, + type IRuntimeModule, + type PluginDescriptor, + type SystemContext +} from './PluginManager'; + +export interface RuntimeConfig { + enabledPlugins?: string[]; + isEditor?: boolean; +} + +/** + * 创建插件(简化工厂) + */ +export function createPlugin( + descriptor: PluginDescriptor, + runtimeModule: IRuntimeModule +): IPlugin { + return { descriptor, runtimeModule }; +} + +/** + * 注册插件到运行时 + */ +export function registerPlugin(plugin: IPlugin): void { + runtimePluginManager.register(plugin); +} + +/** + * 初始化运行时 + * @param config 运行时配置 + */ +export async function initializeRuntime(config?: RuntimeConfig): Promise { + if (config?.enabledPlugins) { + runtimePluginManager.loadConfig({ enabledPlugins: config.enabledPlugins }); + } else { + for (const plugin of runtimePluginManager.getPlugins()) { + runtimePluginManager.enable(plugin.descriptor.id); + } + } + + await runtimePluginManager.initializeRuntime(Core.services); +} + +/** + * 为场景创建系统 + */ +export function createSystemsForScene(scene: IScene, context: SystemContext): void { + runtimePluginManager.createSystemsForScene(scene, context); +} + +/** + * 重置运行时(用于热重载等场景) + */ +export function resetRuntime(): void { + runtimePluginManager.reset(); +} diff --git a/packages/runtime-core/src/adapters/BrowserPlatformAdapter.ts b/packages/runtime-core/src/adapters/BrowserPlatformAdapter.ts new file mode 100644 index 00000000..804ffbda --- /dev/null +++ b/packages/runtime-core/src/adapters/BrowserPlatformAdapter.ts @@ -0,0 +1,143 @@ +/** + * Browser Platform Adapter + * 浏览器平台适配器 + * + * 用于独立浏览器运行时的平台适配器 + * Platform adapter for standalone browser runtime + */ + +import type { + IPlatformAdapter, + IPathResolver, + PlatformCapabilities, + PlatformAdapterConfig +} from '../IPlatformAdapter'; + +/** + * 浏览器路径解析器 + * Browser path resolver + */ +export class BrowserPathResolver implements IPathResolver { + private _baseUrl: string; + + constructor(baseUrl: string = '') { + this._baseUrl = baseUrl; + } + + resolve(path: string): string { + // 如果已经是完整 URL,直接返回 + if (path.startsWith('http://') || + path.startsWith('https://') || + path.startsWith('data:') || + path.startsWith('blob:') || + path.startsWith('/asset?')) { + return path; + } + + // 相对路径,添加资产请求前缀 + return `/asset?path=${encodeURIComponent(path)}`; + } + + /** + * 更新基础 URL + */ + setBaseUrl(baseUrl: string): void { + this._baseUrl = baseUrl; + } +} + +/** + * 浏览器平台适配器配置 + */ +export interface BrowserPlatformConfig { + /** WASM 模块(预加载的)*/ + wasmModule?: any; + /** WASM 模块加载器(异步加载)*/ + wasmModuleLoader?: () => Promise; + /** 资产基础 URL */ + assetBaseUrl?: string; +} + +/** + * 浏览器平台适配器 + * Browser platform adapter + */ +export class BrowserPlatformAdapter implements IPlatformAdapter { + readonly name = 'browser'; + + readonly capabilities: PlatformCapabilities = { + fileSystem: false, + hotReload: false, + gizmos: false, + grid: false, + sceneEditing: false + }; + + private _pathResolver: BrowserPathResolver; + private _canvas: HTMLCanvasElement | null = null; + private _config: BrowserPlatformConfig; + private _viewportSize = { width: 0, height: 0 }; + + constructor(config: BrowserPlatformConfig = {}) { + this._config = config; + this._pathResolver = new BrowserPathResolver(config.assetBaseUrl || ''); + } + + get pathResolver(): IPathResolver { + return this._pathResolver; + } + + async initialize(config: PlatformAdapterConfig): Promise { + // 获取 Canvas + this._canvas = document.getElementById(config.canvasId) as HTMLCanvasElement; + if (!this._canvas) { + throw new Error(`Canvas not found: ${config.canvasId}`); + } + + // 设置尺寸 + const width = config.width || window.innerWidth; + const height = config.height || window.innerHeight; + this._canvas.width = width; + this._canvas.height = height; + this._viewportSize = { width, height }; + } + + async getWasmModule(): Promise { + // 如果已提供模块,直接返回 + if (this._config.wasmModule) { + return this._config.wasmModule; + } + + // 如果提供了加载器,使用加载器 + if (this._config.wasmModuleLoader) { + return this._config.wasmModuleLoader(); + } + + // 默认:尝试动态导入 + throw new Error('No WASM module or loader provided'); + } + + getCanvas(): HTMLCanvasElement | null { + return this._canvas; + } + + resize(width: number, height: number): void { + if (this._canvas) { + this._canvas.width = width; + this._canvas.height = height; + } + this._viewportSize = { width, height }; + } + + getViewportSize(): { width: number; height: number } { + return { ...this._viewportSize }; + } + + isEditorMode(): boolean { + return false; + } + + dispose(): void { + this._canvas = null; + } +} diff --git a/packages/runtime-core/src/adapters/EditorPlatformAdapter.ts b/packages/runtime-core/src/adapters/EditorPlatformAdapter.ts new file mode 100644 index 00000000..356d003f --- /dev/null +++ b/packages/runtime-core/src/adapters/EditorPlatformAdapter.ts @@ -0,0 +1,185 @@ +/** + * Editor Platform Adapter + * 编辑器平台适配器 + * + * 用于 Tauri 编辑器内嵌预览的平台适配器 + * Platform adapter for Tauri editor embedded preview + */ + +import type { + IPlatformAdapter, + IPathResolver, + PlatformCapabilities, + PlatformAdapterConfig +} from '../IPlatformAdapter'; + +/** + * 编辑器路径解析器 + * Editor path resolver + * + * 使用 Tauri 的 convertFileSrc 转换本地文件路径 + * Uses Tauri's convertFileSrc to convert local file paths + */ +export class EditorPathResolver implements IPathResolver { + private _pathTransformer: (path: string) => string; + + constructor(pathTransformer: (path: string) => string) { + this._pathTransformer = pathTransformer; + } + + resolve(path: string): string { + // 如果已经是 URL,直接返回 + if (path.startsWith('http://') || + path.startsWith('https://') || + path.startsWith('data:') || + path.startsWith('blob:') || + path.startsWith('asset://')) { + return path; + } + + // 使用 Tauri 路径转换器 + return this._pathTransformer(path); + } + + /** + * 更新路径转换器 + */ + setPathTransformer(transformer: (path: string) => string): void { + this._pathTransformer = transformer; + } +} + +/** + * 编辑器平台适配器配置 + */ +export interface EditorPlatformConfig { + /** WASM 模块(预加载的)*/ + wasmModule: any; + /** 路径转换函数(使用 Tauri 的 convertFileSrc)*/ + pathTransformer: (path: string) => string; + /** Gizmo 数据提供者 */ + gizmoDataProvider?: (component: any, entity: any, isSelected: boolean) => any; + /** Gizmo 检查函数 */ + hasGizmoProvider?: (component: any) => boolean; +} + +/** + * 编辑器平台适配器 + * Editor platform adapter + */ +export class EditorPlatformAdapter implements IPlatformAdapter { + readonly name = 'editor'; + + readonly capabilities: PlatformCapabilities = { + fileSystem: true, + hotReload: true, + gizmos: true, + grid: true, + sceneEditing: true + }; + + private _pathResolver: EditorPathResolver; + private _canvas: HTMLCanvasElement | null = null; + private _config: EditorPlatformConfig; + private _viewportSize = { width: 0, height: 0 }; + private _showGrid = true; + private _showGizmos = true; + + constructor(config: EditorPlatformConfig) { + this._config = config; + this._pathResolver = new EditorPathResolver(config.pathTransformer); + } + + get pathResolver(): IPathResolver { + return this._pathResolver; + } + + /** + * 获取 Gizmo 数据提供者 + */ + get gizmoDataProvider() { + return this._config.gizmoDataProvider; + } + + /** + * 获取 Gizmo 检查函数 + */ + get hasGizmoProvider() { + return this._config.hasGizmoProvider; + } + + async initialize(config: PlatformAdapterConfig): Promise { + // 获取 Canvas + this._canvas = document.getElementById(config.canvasId) as HTMLCanvasElement; + if (!this._canvas) { + throw new Error(`Canvas not found: ${config.canvasId}`); + } + + // 处理 DPR 缩放 + const dpr = window.devicePixelRatio || 1; + const container = this._canvas.parentElement; + + if (container) { + const rect = container.getBoundingClientRect(); + const width = config.width || Math.floor(rect.width * dpr); + const height = config.height || Math.floor(rect.height * dpr); + + this._canvas.width = width; + this._canvas.height = height; + this._canvas.style.width = `${rect.width}px`; + this._canvas.style.height = `${rect.height}px`; + + this._viewportSize = { width, height }; + } else { + const width = config.width || window.innerWidth; + const height = config.height || window.innerHeight; + this._canvas.width = width; + this._canvas.height = height; + this._viewportSize = { width, height }; + } + } + + async getWasmModule(): Promise { + return this._config.wasmModule; + } + + getCanvas(): HTMLCanvasElement | null { + return this._canvas; + } + + resize(width: number, height: number): void { + if (this._canvas) { + this._canvas.width = width; + this._canvas.height = height; + } + this._viewportSize = { width, height }; + } + + getViewportSize(): { width: number; height: number } { + return { ...this._viewportSize }; + } + + isEditorMode(): boolean { + return true; + } + + setShowGrid(show: boolean): void { + this._showGrid = show; + } + + getShowGrid(): boolean { + return this._showGrid; + } + + setShowGizmos(show: boolean): void { + this._showGizmos = show; + } + + getShowGizmos(): boolean { + return this._showGizmos; + } + + dispose(): void { + this._canvas = null; + } +} diff --git a/packages/runtime-core/src/adapters/index.ts b/packages/runtime-core/src/adapters/index.ts new file mode 100644 index 00000000..3688ab60 --- /dev/null +++ b/packages/runtime-core/src/adapters/index.ts @@ -0,0 +1,7 @@ +/** + * Platform Adapters + * 平台适配器 + */ + +export { BrowserPlatformAdapter, BrowserPathResolver, type BrowserPlatformConfig } from './BrowserPlatformAdapter'; +export { EditorPlatformAdapter, EditorPathResolver, type EditorPlatformConfig } from './EditorPlatformAdapter'; diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts new file mode 100644 index 00000000..24fdf1c8 --- /dev/null +++ b/packages/runtime-core/src/index.ts @@ -0,0 +1,72 @@ +export { + RuntimePluginManager, + runtimePluginManager, + type SystemContext, + type PluginDescriptor, + type IRuntimeModule, + type IPlugin +} from './PluginManager'; + +export { + createPlugin, + registerPlugin, + initializeRuntime, + createSystemsForScene, + resetRuntime, + type RuntimeConfig +} from './RuntimeBootstrap'; + +export { + loadPlugin, + loadEnabledPlugins, + registerStaticPlugin, + getLoadedPlugins, + resetPluginLoader, + type PluginPackageInfo, + type PluginConfig, + type ProjectPluginConfig +} from './PluginLoader'; + +export { + BUILTIN_PLUGIN_PACKAGES, + createDefaultProjectConfig, + mergeProjectConfig, + createProjectConfigFromEnabledList, + type ProjectConfig +} from './ProjectConfig'; + +// Platform Adapter +export { + DefaultPathResolver, + type IPlatformAdapter, + type IPathResolver, + type PlatformCapabilities, + type PlatformAdapterConfig +} from './IPlatformAdapter'; + +// Game Runtime +export { + GameRuntime, + createGameRuntime, + type GameRuntimeConfig, + type RuntimeState +} from './GameRuntime'; + +// Platform Adapters +export { + BrowserPlatformAdapter, + BrowserPathResolver, + type BrowserPlatformConfig, + EditorPlatformAdapter, + EditorPathResolver, + type EditorPlatformConfig +} from './adapters'; + +// Browser File System Service +export { + BrowserFileSystemService, + createBrowserFileSystem, + type AssetCatalog, + type AssetCatalogEntry, + type BrowserFileSystemOptions +} from './services/BrowserFileSystemService'; diff --git a/packages/runtime-core/src/services/BrowserFileSystemService.ts b/packages/runtime-core/src/services/BrowserFileSystemService.ts new file mode 100644 index 00000000..bccd56ad --- /dev/null +++ b/packages/runtime-core/src/services/BrowserFileSystemService.ts @@ -0,0 +1,306 @@ +/** + * Browser File System Service + * 浏览器文件系统服务 + * + * 在浏览器运行时环境中,通过 HTTP fetch 加载资产文件。 + * 使用资产目录(asset-catalog.json)来解析 GUID 到实际 URL。 + * + * In browser runtime environment, loads asset files via HTTP fetch. + * Uses asset catalog to resolve GUIDs to actual URLs. + */ + +/** + * Asset catalog entry + */ +export interface AssetCatalogEntry { + guid: string; + path: string; + type: string; + size: number; + hash: string; +} + +/** + * Asset catalog loaded from JSON + */ +export interface AssetCatalog { + version: string; + createdAt: number; + entries: Record; +} + +/** + * Browser file system service options + */ +export interface BrowserFileSystemOptions { + /** Base URL for assets (e.g., '/assets' or 'https://cdn.example.com/assets') */ + baseUrl?: string; + /** Asset catalog URL */ + catalogUrl?: string; + /** Enable caching */ + enableCache?: boolean; +} + +/** + * Browser File System Service + * + * Provides file system-like API for browser environments + * by fetching files over HTTP. + */ +export class BrowserFileSystemService { + private _baseUrl: string; + private _catalogUrl: string; + private _catalog: AssetCatalog | null = null; + private _cache = new Map(); + private _enableCache: boolean; + private _initialized = false; + + constructor(options: BrowserFileSystemOptions = {}) { + this._baseUrl = options.baseUrl ?? '/assets'; + this._catalogUrl = options.catalogUrl ?? '/asset-catalog.json'; + this._enableCache = options.enableCache ?? true; + } + + /** + * Initialize service and load catalog + */ + async initialize(): Promise { + if (this._initialized) return; + + try { + await this._loadCatalog(); + this._initialized = true; + console.log('[BrowserFileSystem] Initialized with', + Object.keys(this._catalog?.entries ?? {}).length, 'assets'); + } catch (error) { + console.warn('[BrowserFileSystem] Failed to load catalog:', error); + // Continue without catalog - will use path-based loading + this._initialized = true; + } + } + + /** + * Load asset catalog + */ + private async _loadCatalog(): Promise { + const response = await fetch(this._catalogUrl); + if (!response.ok) { + throw new Error(`Failed to fetch catalog: ${response.status}`); + } + this._catalog = await response.json(); + } + + /** + * Read file content as string + * @param path - Can be GUID, relative path, or absolute path + */ + async readFile(path: string): Promise { + const url = this._resolveUrl(path); + + // Check cache + if (this._enableCache && this._cache.has(url)) { + return this._cache.get(url)!; + } + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch file: ${url} (${response.status})`); + } + + const content = await response.text(); + + // Cache result + if (this._enableCache) { + this._cache.set(url, content); + } + + return content; + } + + /** + * Read file as binary (ArrayBuffer) + */ + async readBinary(path: string): Promise { + const url = this._resolveUrl(path); + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch binary: ${url} (${response.status})`); + } + + return response.arrayBuffer(); + } + + /** + * Read file as Blob + */ + async readBlob(path: string): Promise { + const url = this._resolveUrl(path); + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch blob: ${url} (${response.status})`); + } + + return response.blob(); + } + + /** + * Check if file exists (via HEAD request) + */ + async exists(path: string): Promise { + const url = this._resolveUrl(path); + + try { + const response = await fetch(url, { method: 'HEAD' }); + return response.ok; + } catch { + return false; + } + } + + /** + * Resolve path to URL + * Handles GUID, relative path, and absolute path + */ + private _resolveUrl(path: string): string { + // Check if it's a GUID and we have a catalog + if (this._catalog && this._isGuid(path)) { + const entry = this._catalog.entries[path]; + if (entry) { + return this._pathToUrl(entry.path); + } + } + + // Check if it's an absolute Windows path (e.g., F:\...) + if (/^[A-Za-z]:[\\/]/.test(path)) { + // Try to extract relative path from absolute path + const relativePath = this._extractRelativePath(path); + if (relativePath) { + return this._pathToUrl(relativePath); + } + // Fallback: use just the filename + const filename = path.split(/[\\/]/).pop(); + return `${this._baseUrl}/${filename}`; + } + + // Check if it's already a URL + if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('/')) { + return path; + } + + // Treat as relative path + return this._pathToUrl(path); + } + + /** + * Convert relative path to URL + */ + private _pathToUrl(relativePath: string): string { + // Normalize path separators + const normalized = relativePath.replace(/\\/g, '/'); + + // Remove leading 'assets/' if baseUrl already includes it + let cleanPath = normalized; + if (cleanPath.startsWith('assets/') && this._baseUrl.endsWith('/assets')) { + cleanPath = cleanPath.substring(7); + } + + // Ensure no double slashes + const base = this._baseUrl.endsWith('/') ? this._baseUrl.slice(0, -1) : this._baseUrl; + const path = cleanPath.startsWith('/') ? cleanPath.slice(1) : cleanPath; + + return `${base}/${path}`; + } + + /** + * Extract relative path from absolute path + */ + private _extractRelativePath(absolutePath: string): string | null { + const normalized = absolutePath.replace(/\\/g, '/'); + + // Look for 'assets/' in the path + const assetsIndex = normalized.toLowerCase().indexOf('/assets/'); + if (assetsIndex >= 0) { + return normalized.substring(assetsIndex + 1); // Include 'assets/' + } + + return null; + } + + /** + * Check if string looks like a GUID + */ + private _isGuid(str: string): boolean { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str); + } + + /** + * Get asset metadata from catalog + */ + getAssetMetadata(guidOrPath: string): AssetCatalogEntry | null { + if (!this._catalog) return null; + + // Try as GUID + if (this._catalog.entries[guidOrPath]) { + return this._catalog.entries[guidOrPath]; + } + + // Try as path + for (const entry of Object.values(this._catalog.entries)) { + if (entry.path === guidOrPath) { + return entry; + } + } + + return null; + } + + /** + * Get all assets of a specific type + */ + getAssetsByType(type: string): AssetCatalogEntry[] { + if (!this._catalog) return []; + + return Object.values(this._catalog.entries) + .filter(entry => entry.type === type); + } + + /** + * Clear cache + */ + clearCache(): void { + this._cache.clear(); + } + + /** + * Get catalog + */ + get catalog(): AssetCatalog | null { + return this._catalog; + } + + /** + * Check if initialized + */ + get isInitialized(): boolean { + return this._initialized; + } + + /** + * Dispose service and clear resources + * Required by IService interface + */ + dispose(): void { + this._cache.clear(); + this._catalog = null; + this._initialized = false; + } +} + +/** + * Create and register browser file system service + */ +export function createBrowserFileSystem(options?: BrowserFileSystemOptions): BrowserFileSystemService { + return new BrowserFileSystemService(options); +} diff --git a/packages/runtime-core/tsconfig.build.json b/packages/runtime-core/tsconfig.build.json new file mode 100644 index 00000000..f39a0594 --- /dev/null +++ b/packages/runtime-core/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/runtime-core/tsconfig.json b/packages/runtime-core/tsconfig.json new file mode 100644 index 00000000..be81e3e6 --- /dev/null +++ b/packages/runtime-core/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../core" }, + { "path": "../ecs-engine-bindgen" }, + { "path": "../engine-core" }, + { "path": "../asset-system" } + ] +} diff --git a/packages/runtime-core/tsup.config.ts b/packages/runtime-core/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/runtime-core/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/sprite-editor/package.json b/packages/sprite-editor/package.json new file mode 100644 index 00000000..234a67b0 --- /dev/null +++ b/packages/sprite-editor/package.json @@ -0,0 +1,44 @@ +{ + "name": "@esengine/sprite-editor", + "version": "1.0.0", + "description": "Editor components for sprite system", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/sprite": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "react": "^18.3.1", + "@types/react": "^18.2.0", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "sprite", + "editor", + "animation" + ], + "author": "yhh", + "license": "MIT" +} diff --git a/packages/sprite-editor/src/index.ts b/packages/sprite-editor/src/index.ts new file mode 100644 index 00000000..7ca9e4fc --- /dev/null +++ b/packages/sprite-editor/src/index.ts @@ -0,0 +1,147 @@ +/** + * @esengine/sprite-editor + * + * Editor support for @esengine/sprite - inspectors, field editors, and entity templates + * 精灵编辑器支持 - 检视器、字段编辑器和实体模板 + */ + +import type { Entity, ServiceContainer } from '@esengine/ecs-framework'; +import { Core } from '@esengine/ecs-framework'; +import type { + IEditorModuleLoader, + EntityCreationTemplate +} from '@esengine/editor-core'; +import { + EntityStoreService, + MessageHub, + ComponentRegistry +} from '@esengine/editor-core'; +import { TransformComponent } from '@esengine/engine-core'; + +// Runtime imports from @esengine/sprite +import { + SpriteComponent, + SpriteAnimatorComponent +} from '@esengine/sprite'; + +/** + * 精灵编辑器模块 + * Sprite Editor Module + */ +export class SpriteEditorModule implements IEditorModuleLoader { + async install(services: ServiceContainer): Promise { + // 注册 Sprite 组件到编辑器组件注册表 | Register Sprite components to editor component registry + const componentRegistry = services.resolve(ComponentRegistry); + if (componentRegistry) { + const spriteComponents = [ + { + name: 'Sprite', + type: SpriteComponent, + category: 'components.category.rendering', + description: '2D sprite rendering component', + icon: 'Image' + }, + { + name: 'SpriteAnimator', + type: SpriteAnimatorComponent, + category: 'components.category.rendering', + description: 'Sprite frame animation component', + icon: 'Film' + }, + ]; + + for (const comp of spriteComponents) { + componentRegistry.register({ + name: comp.name, + type: comp.type, + category: comp.category, + description: comp.description, + icon: comp.icon + }); + } + } + } + + async uninstall(): Promise { + // Nothing to cleanup + } + + getEntityCreationTemplates(): EntityCreationTemplate[] { + return [ + // Sprite Entity + { + id: 'create-sprite', + label: 'Sprite', + icon: 'Image', + category: 'rendering', + order: 100, + create: (): number => { + return this.createSpriteEntity('Sprite'); + } + }, + + // Animated Sprite Entity + { + id: 'create-animated-sprite', + label: 'Animated Sprite', + icon: 'Film', + category: 'rendering', + order: 101, + create: (): number => { + return this.createSpriteEntity('AnimatedSprite', (entity) => { + const animator = new SpriteAnimatorComponent(); + animator.autoPlay = true; + entity.addComponent(animator); + }); + } + }, + ]; + } + + /** + * 创建 Sprite 实体的辅助方法 + * Helper method to create Sprite entity + */ + private createSpriteEntity(baseName: string, configure?: (entity: Entity) => void): number { + const scene = Core.scene; + if (!scene) { + throw new Error('Scene not available'); + } + + const entityStore = Core.services.resolve(EntityStoreService); + const messageHub = Core.services.resolve(MessageHub); + + if (!entityStore || !messageHub) { + throw new Error('EntityStoreService or MessageHub not available'); + } + + const existingCount = entityStore.getAllEntities() + .filter((e: Entity) => e.name.startsWith(baseName)).length; + const entityName = existingCount > 0 ? `${baseName} ${existingCount + 1}` : baseName; + + const entity = scene.createEntity(entityName); + + // Add Transform component + const transform = new TransformComponent(); + entity.addComponent(transform); + + // Add Sprite component + const sprite = new SpriteComponent(); + entity.addComponent(sprite); + + if (configure) { + configure(entity); + } + + entityStore.addEntity(entity); + messageHub.publish('entity:added', { entity }); + messageHub.publish('scene:modified', {}); + entityStore.selectEntity(entity); + + return entity.id; + } +} + +export const spriteEditorModule = new SpriteEditorModule(); + +export default spriteEditorModule; diff --git a/packages/sprite-editor/tsconfig.build.json b/packages/sprite-editor/tsconfig.build.json new file mode 100644 index 00000000..29e209e8 --- /dev/null +++ b/packages/sprite-editor/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/sprite-editor/tsconfig.json b/packages/sprite-editor/tsconfig.json new file mode 100644 index 00000000..a1614d97 --- /dev/null +++ b/packages/sprite-editor/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src", + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../core" }, + { "path": "../sprite" }, + { "path": "../editor-core" } + ] +} diff --git a/packages/sprite-editor/tsup.config.ts b/packages/sprite-editor/tsup.config.ts new file mode 100644 index 00000000..b4f49f5d --- /dev/null +++ b/packages/sprite-editor/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { editorOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...editorOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/sprite/package.json b/packages/sprite/package.json new file mode 100644 index 00000000..9375d9c9 --- /dev/null +++ b/packages/sprite/package.json @@ -0,0 +1,48 @@ +{ + "name": "@esengine/sprite", + "version": "1.0.0", + "description": "ECS-based 2D sprite rendering and animation system", + "esengine": { + "plugin": true, + "pluginExport": "SpritePlugin", + "category": "rendering", + "isEnginePlugin": true + }, + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/asset-system": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "sprite", + "animation", + "2d", + "webgl" + ], + "author": "yhh", + "license": "MIT" +} diff --git a/packages/components/src/SpriteAnimatorComponent.ts b/packages/sprite/src/SpriteAnimatorComponent.ts similarity index 100% rename from packages/components/src/SpriteAnimatorComponent.ts rename to packages/sprite/src/SpriteAnimatorComponent.ts diff --git a/packages/components/src/SpriteComponent.ts b/packages/sprite/src/SpriteComponent.ts similarity index 100% rename from packages/components/src/SpriteComponent.ts rename to packages/sprite/src/SpriteComponent.ts diff --git a/packages/sprite/src/SpriteRuntimeModule.ts b/packages/sprite/src/SpriteRuntimeModule.ts new file mode 100644 index 00000000..5b93ccab --- /dev/null +++ b/packages/sprite/src/SpriteRuntimeModule.ts @@ -0,0 +1,42 @@ +import type { ComponentRegistry as ComponentRegistryType, IScene } from '@esengine/ecs-framework'; +import type { IRuntimeModule, IPlugin, PluginDescriptor, SystemContext } from '@esengine/engine-core'; +import { SpriteComponent } from './SpriteComponent'; +import { SpriteAnimatorComponent } from './SpriteAnimatorComponent'; +import { SpriteAnimatorSystem } from './systems/SpriteAnimatorSystem'; + +export type { SystemContext, PluginDescriptor, IRuntimeModule as IRuntimeModuleLoader, IPlugin as IPluginLoader }; + +class SpriteRuntimeModule implements IRuntimeModule { + registerComponents(registry: typeof ComponentRegistryType): void { + registry.register(SpriteComponent); + registry.register(SpriteAnimatorComponent); + } + + createSystems(scene: IScene, context: SystemContext): void { + const animatorSystem = new SpriteAnimatorSystem(); + + if (context.isEditor) { + animatorSystem.enabled = false; + } + + scene.addSystem(animatorSystem); + (context as any).animatorSystem = animatorSystem; + } +} + +const descriptor: PluginDescriptor = { + id: '@esengine/sprite', + name: 'Sprite Components', + version: '1.0.0', + description: 'Sprite and SpriteAnimator components for 2D rendering', + category: 'rendering', + enabledByDefault: true, + isEnginePlugin: true +}; + +export const SpritePlugin: IPlugin = { + descriptor, + runtimeModule: new SpriteRuntimeModule() +}; + +export { SpriteRuntimeModule }; diff --git a/packages/sprite/src/index.ts b/packages/sprite/src/index.ts new file mode 100644 index 00000000..074306aa --- /dev/null +++ b/packages/sprite/src/index.ts @@ -0,0 +1,5 @@ +export { SpriteComponent } from './SpriteComponent'; +export { SpriteAnimatorComponent } from './SpriteAnimatorComponent'; +export type { AnimationFrame, AnimationClip } from './SpriteAnimatorComponent'; +export { SpriteAnimatorSystem } from './systems/SpriteAnimatorSystem'; +export { SpriteRuntimeModule, SpritePlugin } from './SpriteRuntimeModule'; diff --git a/packages/components/src/systems/SpriteAnimatorSystem.ts b/packages/sprite/src/systems/SpriteAnimatorSystem.ts similarity index 100% rename from packages/components/src/systems/SpriteAnimatorSystem.ts rename to packages/sprite/src/systems/SpriteAnimatorSystem.ts diff --git a/packages/sprite/tsconfig.build.json b/packages/sprite/tsconfig.build.json new file mode 100644 index 00000000..f39a0594 --- /dev/null +++ b/packages/sprite/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/sprite/tsconfig.json b/packages/sprite/tsconfig.json new file mode 100644 index 00000000..8b270592 --- /dev/null +++ b/packages/sprite/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../core" }, + { "path": "../asset-system" } + ] +} diff --git a/packages/sprite/tsup.config.ts b/packages/sprite/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/sprite/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/tilemap-editor/package.json b/packages/tilemap-editor/package.json new file mode 100644 index 00000000..ade22014 --- /dev/null +++ b/packages/tilemap-editor/package.json @@ -0,0 +1,47 @@ +{ + "name": "@esengine/tilemap-editor", + "version": "1.0.0", + "description": "Editor support for @esengine/tilemap - tilemap editor, tools, and panels", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "dependencies": { + "@esengine/tilemap": "workspace:*" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "lucide-react": "^0.545.0", + "react": "^18.3.1", + "zustand": "^5.0.8", + "@types/react": "^18.3.12", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "tilemap", + "editor" + ], + "author": "", + "license": "MIT" +} diff --git a/packages/tilemap/src/editor/components/TilemapCanvas.tsx b/packages/tilemap-editor/src/components/TilemapCanvas.tsx similarity index 99% rename from packages/tilemap/src/editor/components/TilemapCanvas.tsx rename to packages/tilemap-editor/src/components/TilemapCanvas.tsx index 3161f734..1ae45435 100644 --- a/packages/tilemap/src/editor/components/TilemapCanvas.tsx +++ b/packages/tilemap-editor/src/components/TilemapCanvas.tsx @@ -3,7 +3,7 @@ */ import React, { useRef, useEffect, useState, useCallback } from 'react'; -import type { TilemapComponent } from '../../TilemapComponent'; +import type { TilemapComponent } from '@esengine/tilemap'; import { useTilemapEditorStore } from '../stores/TilemapEditorStore'; import type { ITilemapTool, ToolContext } from '../tools/ITilemapTool'; import { BrushTool } from '../tools/BrushTool'; diff --git a/packages/tilemap/src/editor/components/TilesetPreview.tsx b/packages/tilemap-editor/src/components/TilesetPreview.tsx similarity index 100% rename from packages/tilemap/src/editor/components/TilesetPreview.tsx rename to packages/tilemap-editor/src/components/TilesetPreview.tsx diff --git a/packages/tilemap/src/editor/components/panels/LayerPanel.tsx b/packages/tilemap-editor/src/components/panels/LayerPanel.tsx similarity index 99% rename from packages/tilemap/src/editor/components/panels/LayerPanel.tsx rename to packages/tilemap-editor/src/components/panels/LayerPanel.tsx index d6bea3ec..90bde5ac 100644 --- a/packages/tilemap/src/editor/components/panels/LayerPanel.tsx +++ b/packages/tilemap-editor/src/components/panels/LayerPanel.tsx @@ -6,7 +6,7 @@ import React, { useState, useCallback } from 'react'; import { Eye, EyeOff, Lock, Unlock, Plus, Trash2, ChevronUp, ChevronDown, Paintbrush, Shield, Grid3X3 } from 'lucide-react'; import { useTilemapEditorStore, type LayerState } from '../../stores/TilemapEditorStore'; -import type { TilemapComponent } from '../../../TilemapComponent'; +import type { TilemapComponent } from '@esengine/tilemap'; interface LayerPanelProps { tilemap: TilemapComponent | null; diff --git a/packages/tilemap/src/editor/components/panels/TileSetSelectorPanel.tsx b/packages/tilemap-editor/src/components/panels/TileSetSelectorPanel.tsx similarity index 100% rename from packages/tilemap/src/editor/components/panels/TileSetSelectorPanel.tsx rename to packages/tilemap-editor/src/components/panels/TileSetSelectorPanel.tsx diff --git a/packages/tilemap/src/editor/components/panels/TilemapDetailsPanel.tsx b/packages/tilemap-editor/src/components/panels/TilemapDetailsPanel.tsx similarity index 99% rename from packages/tilemap/src/editor/components/panels/TilemapDetailsPanel.tsx rename to packages/tilemap-editor/src/components/panels/TilemapDetailsPanel.tsx index fed14a16..9f9fc177 100644 --- a/packages/tilemap/src/editor/components/panels/TilemapDetailsPanel.tsx +++ b/packages/tilemap-editor/src/components/panels/TilemapDetailsPanel.tsx @@ -18,7 +18,7 @@ import { EyeOff } from 'lucide-react'; import { useTilemapEditorStore, type LayerState } from '../../stores/TilemapEditorStore'; -import type { TilemapComponent } from '../../../TilemapComponent'; +import type { TilemapComponent } from '@esengine/tilemap'; import '../../styles/TilemapDetailsPanel.css'; interface TilemapDetailsPanelProps { diff --git a/packages/tilemap/src/editor/components/panels/TilemapEditorPanel.tsx b/packages/tilemap-editor/src/components/panels/TilemapEditorPanel.tsx similarity index 99% rename from packages/tilemap/src/editor/components/panels/TilemapEditorPanel.tsx rename to packages/tilemap-editor/src/components/panels/TilemapEditorPanel.tsx index f690efd5..6898b761 100644 --- a/packages/tilemap/src/editor/components/panels/TilemapEditorPanel.tsx +++ b/packages/tilemap-editor/src/components/panels/TilemapEditorPanel.tsx @@ -36,7 +36,7 @@ import { } from 'lucide-react'; import { Core, Entity } from '@esengine/ecs-framework'; import { MessageHub, ProjectService, IFileSystemService, type IFileSystem, IDialogService, type IDialog } from '@esengine/editor-core'; -import { TilemapComponent, type ITilesetData, type ResizeAnchor } from '../../../TilemapComponent'; +import { TilemapComponent, type ITilesetData, type ResizeAnchor } from '@esengine/tilemap'; import { useTilemapEditorStore, type TilemapToolType, type LayerState } from '../../stores/TilemapEditorStore'; import { TilemapCanvas } from '../TilemapCanvas'; import { TileSetSelectorPanel } from './TileSetSelectorPanel'; diff --git a/packages/tilemap/src/editor/components/panels/TilesetPanel.tsx b/packages/tilemap-editor/src/components/panels/TilesetPanel.tsx similarity index 98% rename from packages/tilemap/src/editor/components/panels/TilesetPanel.tsx rename to packages/tilemap-editor/src/components/panels/TilesetPanel.tsx index db69e5a8..b515868f 100644 --- a/packages/tilemap/src/editor/components/panels/TilesetPanel.tsx +++ b/packages/tilemap-editor/src/components/panels/TilesetPanel.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useCallback } from 'react'; import { Core } from '@esengine/ecs-framework'; import { MessageHub } from '@esengine/editor-core'; -import { TilemapComponent, type ITilesetData } from '../../../TilemapComponent'; +import { TilemapComponent, type ITilesetData } from '@esengine/tilemap'; import { useTilemapEditorStore } from '../../stores/TilemapEditorStore'; import { TilesetPreview } from '../TilesetPreview'; import '../../styles/TilemapEditor.css'; diff --git a/packages/tilemap/src/editor/gizmos/TilemapGizmo.ts b/packages/tilemap-editor/src/gizmos/TilemapGizmo.ts similarity index 98% rename from packages/tilemap/src/editor/gizmos/TilemapGizmo.ts rename to packages/tilemap-editor/src/gizmos/TilemapGizmo.ts index 2038578e..8f009aa8 100644 --- a/packages/tilemap/src/editor/gizmos/TilemapGizmo.ts +++ b/packages/tilemap-editor/src/gizmos/TilemapGizmo.ts @@ -11,9 +11,9 @@ import type { Entity } from '@esengine/ecs-framework'; import type { IGizmoRenderData, IRectGizmoData, IGridGizmoData, GizmoColor } from '@esengine/editor-core'; import { GizmoColors, GizmoRegistry } from '@esengine/editor-core'; -import { TransformComponent } from '@esengine/ecs-components'; -import { TilemapComponent } from '../../TilemapComponent'; -import { TilemapCollider2DComponent, TilemapColliderMode } from '../../physics/TilemapCollider2DComponent'; +import { TransformComponent } from '@esengine/engine-core'; +import { TilemapComponent } from '@esengine/tilemap'; +import { TilemapCollider2DComponent, TilemapColliderMode } from '@esengine/tilemap'; /** * Tilemap Collider Gizmo 颜色配置 diff --git a/packages/tilemap/src/editor/index.ts b/packages/tilemap-editor/src/index.ts similarity index 92% rename from packages/tilemap/src/editor/index.ts rename to packages/tilemap-editor/src/index.ts index aade1be3..c227dd47 100644 --- a/packages/tilemap/src/editor/index.ts +++ b/packages/tilemap-editor/src/index.ts @@ -28,11 +28,11 @@ import { FileActionRegistry } from '@esengine/editor-core'; import type { IDialog, IFileSystem } from '@esengine/editor-core'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; -// Local imports -import { TilemapComponent } from '../TilemapComponent'; -import { TilemapCollider2DComponent } from '../physics/TilemapCollider2DComponent'; +// Runtime imports from @esengine/tilemap +import { TilemapComponent, TilemapCollider2DComponent, TilemapRuntimeModule } from '@esengine/tilemap'; +import type { IPlugin, PluginDescriptor } from '@esengine/editor-core'; import { TilemapEditorPanel } from './components/panels/TilemapEditorPanel'; import { TilemapInspectorProvider } from './providers/TilemapInspectorProvider'; import { registerTilemapGizmo } from './gizmos/TilemapGizmo'; @@ -354,6 +354,33 @@ export class TilemapEditorModule implements IEditorModuleLoader { export const tilemapEditorModule = new TilemapEditorModule(); -// Plugin exports -export { TilemapPlugin } from './TilemapPlugin'; +/** + * Tilemap 插件描述符 + * Tilemap Plugin Descriptor + */ +const descriptor: PluginDescriptor = { + id: '@esengine/tilemap', + name: 'Tilemap', + version: '1.0.0', + description: 'Tilemap system with Tiled editor support', + category: 'tilemap', + enabledByDefault: false, + isEnginePlugin: true, + canContainContent: true, + modules: [ + { name: 'Runtime', type: 'runtime', loadingPhase: 'default' }, + { name: 'Editor', type: 'editor', loadingPhase: 'postDefault' } + ] +}; + +/** + * 完整的 Tilemap 插件(运行时 + 编辑器) + * Complete Tilemap Plugin (runtime + editor) + */ +export const TilemapPlugin: IPlugin = { + descriptor, + runtimeModule: new TilemapRuntimeModule(), + editorModule: tilemapEditorModule +}; + export default tilemapEditorModule; diff --git a/packages/tilemap/src/editor/providers/TilemapInspectorProvider.tsx b/packages/tilemap-editor/src/providers/TilemapInspectorProvider.tsx similarity index 98% rename from packages/tilemap/src/editor/providers/TilemapInspectorProvider.tsx rename to packages/tilemap-editor/src/providers/TilemapInspectorProvider.tsx index d1ff993c..2ea16c76 100644 --- a/packages/tilemap/src/editor/providers/TilemapInspectorProvider.tsx +++ b/packages/tilemap-editor/src/providers/TilemapInspectorProvider.tsx @@ -7,7 +7,7 @@ import { Edit3 } from 'lucide-react'; import type { IInspectorProvider, InspectorContext } from '@esengine/editor-core'; import { MessageHub } from '@esengine/editor-core'; import { Core } from '@esengine/ecs-framework'; -import type { TilemapComponent } from '../../TilemapComponent'; +import type { TilemapComponent } from '@esengine/tilemap'; interface TilemapInspectorData { entityId: string; diff --git a/packages/tilemap/src/editor/stores/TilemapEditorStore.ts b/packages/tilemap-editor/src/stores/TilemapEditorStore.ts similarity index 100% rename from packages/tilemap/src/editor/stores/TilemapEditorStore.ts rename to packages/tilemap-editor/src/stores/TilemapEditorStore.ts diff --git a/packages/tilemap/src/editor/styles/TileSetSelectorPanel.css b/packages/tilemap-editor/src/styles/TileSetSelectorPanel.css similarity index 100% rename from packages/tilemap/src/editor/styles/TileSetSelectorPanel.css rename to packages/tilemap-editor/src/styles/TileSetSelectorPanel.css diff --git a/packages/tilemap/src/editor/styles/TilemapDetailsPanel.css b/packages/tilemap-editor/src/styles/TilemapDetailsPanel.css similarity index 100% rename from packages/tilemap/src/editor/styles/TilemapDetailsPanel.css rename to packages/tilemap-editor/src/styles/TilemapDetailsPanel.css diff --git a/packages/tilemap/src/editor/styles/TilemapEditor.css b/packages/tilemap-editor/src/styles/TilemapEditor.css similarity index 100% rename from packages/tilemap/src/editor/styles/TilemapEditor.css rename to packages/tilemap-editor/src/styles/TilemapEditor.css diff --git a/packages/tilemap/src/editor/tools/BrushTool.ts b/packages/tilemap-editor/src/tools/BrushTool.ts similarity index 100% rename from packages/tilemap/src/editor/tools/BrushTool.ts rename to packages/tilemap-editor/src/tools/BrushTool.ts diff --git a/packages/tilemap/src/editor/tools/EraserTool.ts b/packages/tilemap-editor/src/tools/EraserTool.ts similarity index 100% rename from packages/tilemap/src/editor/tools/EraserTool.ts rename to packages/tilemap-editor/src/tools/EraserTool.ts diff --git a/packages/tilemap/src/editor/tools/FillTool.ts b/packages/tilemap-editor/src/tools/FillTool.ts similarity index 100% rename from packages/tilemap/src/editor/tools/FillTool.ts rename to packages/tilemap-editor/src/tools/FillTool.ts diff --git a/packages/tilemap/src/editor/tools/ITilemapTool.ts b/packages/tilemap-editor/src/tools/ITilemapTool.ts similarity index 93% rename from packages/tilemap/src/editor/tools/ITilemapTool.ts rename to packages/tilemap-editor/src/tools/ITilemapTool.ts index f628e8fe..2127aa04 100644 --- a/packages/tilemap/src/editor/tools/ITilemapTool.ts +++ b/packages/tilemap-editor/src/tools/ITilemapTool.ts @@ -2,7 +2,7 @@ * Tilemap Tool Interface */ -import type { TilemapComponent } from '../../TilemapComponent'; +import type { TilemapComponent } from '@esengine/tilemap'; import type { TileSelection } from '../stores/TilemapEditorStore'; export interface ToolContext { diff --git a/packages/tilemap-editor/tsconfig.build.json b/packages/tilemap-editor/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/tilemap-editor/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/tilemap-editor/tsconfig.json b/packages/tilemap-editor/tsconfig.json new file mode 100644 index 00000000..d099ddd8 --- /dev/null +++ b/packages/tilemap-editor/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/tilemap-editor/tsup.config.ts b/packages/tilemap-editor/tsup.config.ts new file mode 100644 index 00000000..b4f49f5d --- /dev/null +++ b/packages/tilemap-editor/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { editorOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...editorOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/tilemap/package.json b/packages/tilemap/package.json index 3d17cafa..c574382b 100644 --- a/packages/tilemap/package.json +++ b/packages/tilemap/package.json @@ -1,85 +1,58 @@ { - "name": "@esengine/tilemap", - "version": "1.0.0", - "description": "Tilemap system for ECS Framework - supports Tiled editor import", - "main": "dist/index.js", - "module": "dist/index.js", - "types": "dist/index.d.ts", - "type": "module", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "name": "@esengine/tilemap", + "version": "1.0.0", + "description": "Tilemap system for ECS Framework - supports Tiled editor import", + "esengine": { + "plugin": true, + "pluginExport": "TilemapPlugin", + "category": "tilemap" }, - "./runtime": { - "types": "./dist/runtime.d.ts", - "import": "./dist/runtime.js" + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } }, - "./editor": { - "types": "./dist/editor/index.d.ts", - "import": "./dist/editor/index.js" + "files": [ + "dist" + ], + "keywords": [ + "ecs", + "tilemap", + "tiled", + "game-engine", + "2d", + "typescript" + ], + "scripts": { + "clean": "rimraf dist tsconfig.tsbuildinfo", + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "rebuild": "npm run clean && npm run build" }, - "./plugin.json": "./plugin.json" - }, - "files": [ - "dist", - "plugin.json" - ], - "keywords": [ - "ecs", - "tilemap", - "tiled", - "game-engine", - "2d", - "typescript" - ], - "scripts": { - "clean": "rimraf dist tsconfig.tsbuildinfo", - "build": "vite build", - "build:watch": "vite build --watch", - "type-check": "tsc --noEmit", - "rebuild": "npm run clean && npm run build" - }, - "author": "yhh", - "license": "MIT", - "devDependencies": { - "@types/react": "^18.3.12", - "@vitejs/plugin-react": "^4.7.0", - "rimraf": "^5.0.0", - "typescript": "^5.8.3", - "vite": "^6.0.7", - "vite-plugin-dts": "^3.7.0" - }, - "peerDependencies": { - "@esengine/ecs-framework": "^2.2.8", - "@esengine/asset-system": "workspace:*", - "@esengine/ecs-components": "workspace:*", - "@esengine/ecs-engine-bindgen": "workspace:*", - "@esengine/editor-core": "workspace:*", - "react": "^18.3.1", - "zustand": "^5.0.8", - "lucide-react": "^0.545.0" - }, - "peerDependenciesMeta": { - "@esengine/editor-core": { - "optional": true + "author": "yhh", + "license": "MIT", + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/asset-system": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/ecs-engine-bindgen": "workspace:*", + "@esengine/build-config": "workspace:*", + "rimraf": "^5.0.0", + "tsup": "^8.0.0", + "typescript": "^5.8.3" }, - "react": { - "optional": true + "dependencies": { + "tslib": "^2.8.1" }, - "zustand": { - "optional": true - }, - "lucide-react": { - "optional": true + "repository": { + "type": "git", + "url": "https://github.com/esengine/ecs-framework.git", + "directory": "packages/tilemap" } - }, - "dependencies": { - "tslib": "^2.8.1" - }, - "repository": { - "type": "git", - "url": "https://github.com/esengine/ecs-framework.git", - "directory": "packages/tilemap" - } } diff --git a/packages/tilemap/src/TilemapRuntimeModule.ts b/packages/tilemap/src/TilemapRuntimeModule.ts index 4f6ff8bd..19790e3d 100644 --- a/packages/tilemap/src/TilemapRuntimeModule.ts +++ b/packages/tilemap/src/TilemapRuntimeModule.ts @@ -1,11 +1,6 @@ -/** - * Tilemap Runtime Module (Pure runtime, no editor dependencies) - * Tilemap 运行时模块(纯运行时,无编辑器依赖) - */ - import type { IScene } from '@esengine/ecs-framework'; import { ComponentRegistry } from '@esengine/ecs-framework'; -import type { IRuntimeModuleLoader, SystemContext } from '@esengine/ecs-components'; +import type { IRuntimeModule, IPlugin, PluginDescriptor, SystemContext } from '@esengine/engine-core'; import type { AssetManager } from '@esengine/asset-system'; import { TilemapComponent } from './TilemapComponent'; @@ -13,13 +8,17 @@ import { TilemapRenderingSystem } from './systems/TilemapRenderingSystem'; import { TilemapCollider2DComponent } from './physics/TilemapCollider2DComponent'; import { TilemapPhysicsSystem, type IPhysicsWorld } from './physics/TilemapPhysicsSystem'; import { TilemapLoader } from './loaders/TilemapLoader'; -import { TilemapAssetType } from './index'; +import { TilemapAssetType } from './constants'; -/** - * Tilemap Runtime Module - * Tilemap 运行时模块 - */ -export class TilemapRuntimeModule implements IRuntimeModuleLoader { +export interface TilemapSystemContext extends SystemContext { + tilemapSystem?: TilemapRenderingSystem; + tilemapPhysicsSystem?: TilemapPhysicsSystem; + physics2DWorld?: IPhysicsWorld; + assetManager?: AssetManager; + renderSystem?: any; +} + +class TilemapRuntimeModule implements IRuntimeModule { private _tilemapPhysicsSystem: TilemapPhysicsSystem | null = null; private _loaderRegistered = false; @@ -29,47 +28,54 @@ export class TilemapRuntimeModule implements IRuntimeModuleLoader { } createSystems(scene: IScene, context: SystemContext): void { - // 注册 Tilemap 加载器到 AssetManager - // Register tilemap loader to AssetManager - const assetManager = context.assetManager as AssetManager | undefined; - if (!this._loaderRegistered && assetManager) { - assetManager.registerLoader(TilemapAssetType, new TilemapLoader()); + const tilemapContext = context as TilemapSystemContext; + + if (!this._loaderRegistered && tilemapContext.assetManager) { + tilemapContext.assetManager.registerLoader(TilemapAssetType, new TilemapLoader()); this._loaderRegistered = true; } - // Tilemap rendering system const tilemapSystem = new TilemapRenderingSystem(); scene.addSystem(tilemapSystem); - if (context.renderSystem) { - context.renderSystem.addRenderDataProvider(tilemapSystem); + if (tilemapContext.renderSystem) { + tilemapContext.renderSystem.addRenderDataProvider(tilemapSystem); } - context.tilemapSystem = tilemapSystem; + tilemapContext.tilemapSystem = tilemapSystem; - // Tilemap physics system this._tilemapPhysicsSystem = new TilemapPhysicsSystem(); scene.addSystem(this._tilemapPhysicsSystem); - context.tilemapPhysicsSystem = this._tilemapPhysicsSystem; + tilemapContext.tilemapPhysicsSystem = this._tilemapPhysicsSystem; } - /** - * 所有系统创建完成后,连接跨插件依赖 - * Wire cross-plugin dependencies after all systems are created - */ onSystemsCreated(_scene: IScene, context: SystemContext): void { - // 连接物理世界(如果物理插件已加载) - // Connect physics world (if physics plugin is loaded) - if (this._tilemapPhysicsSystem && context.physics2DWorld) { - this._tilemapPhysicsSystem.setPhysicsWorld(context.physics2DWorld as IPhysicsWorld); + const tilemapContext = context as TilemapSystemContext; + + if (this._tilemapPhysicsSystem && tilemapContext.physics2DWorld) { + this._tilemapPhysicsSystem.setPhysicsWorld(tilemapContext.physics2DWorld); } } - /** - * 获取 Tilemap 物理系统 - */ get tilemapPhysicsSystem(): TilemapPhysicsSystem | null { return this._tilemapPhysicsSystem; } } + +const descriptor: PluginDescriptor = { + id: '@esengine/tilemap', + name: 'Tilemap', + version: '1.0.0', + description: 'Tilemap system with Tiled editor support', + category: 'tilemap', + enabledByDefault: false, + isEnginePlugin: true +}; + +export const TilemapPlugin: IPlugin = { + descriptor, + runtimeModule: new TilemapRuntimeModule() +}; + +export { TilemapRuntimeModule }; diff --git a/packages/tilemap/src/constants.ts b/packages/tilemap/src/constants.ts new file mode 100644 index 00000000..9bfce3c9 --- /dev/null +++ b/packages/tilemap/src/constants.ts @@ -0,0 +1,9 @@ +/** + * Tilemap Constants + * 瓦片地图常量 + */ + +// Asset type constants for tilemap +// 瓦片地图资产类型常量 +export const TilemapAssetType = 'tilemap' as const; +export const TilesetAssetType = 'tileset' as const; diff --git a/packages/tilemap/src/editor/TilemapPlugin.ts b/packages/tilemap/src/editor/TilemapPlugin.ts deleted file mode 100644 index bf0c514e..00000000 --- a/packages/tilemap/src/editor/TilemapPlugin.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Tilemap 统一插件 - * Tilemap Unified Plugin - * - * 整合运行时模块和编辑器模块 - * Integrates runtime and editor modules - */ - -import type { IPluginLoader, PluginDescriptor } from '@esengine/editor-core'; - -// Runtime module -import { TilemapRuntimeModule } from '../TilemapRuntimeModule'; - -// Editor imports -import { TilemapEditorModule } from './index'; - -/** - * 插件描述符 - */ -const descriptor: PluginDescriptor = { - id: '@esengine/tilemap', - name: 'Tilemap System', - version: '1.0.0', - description: '瓦片地图系统,支持 Tiled 格式导入和高效渲染', - category: 'rendering', - enabledByDefault: true, - canContainContent: true, - isEnginePlugin: true, - modules: [ - { - name: 'TilemapRuntime', - type: 'runtime', - loadingPhase: 'default', - entry: './src/index.ts' - }, - { - name: 'TilemapEditor', - type: 'editor', - loadingPhase: 'default', - entry: './src/editor/index.ts' - } - ], - dependencies: [ - { id: '@esengine/core', version: '^1.0.0' }, - { id: '@esengine/physics-rapier2d', version: '^1.0.0', optional: true } - ], - icon: 'Grid3X3' -}; - -/** - * Tilemap 插件加载器 - * Tilemap plugin loader - */ -export const TilemapPlugin: IPluginLoader = { - descriptor, - runtimeModule: new TilemapRuntimeModule(), - editorModule: new TilemapEditorModule(), -}; - -export default TilemapPlugin; diff --git a/packages/tilemap/src/index.ts b/packages/tilemap/src/index.ts index 713a99d4..49eee765 100644 --- a/packages/tilemap/src/index.ts +++ b/packages/tilemap/src/index.ts @@ -3,10 +3,8 @@ * ECS框架的瓦片地图系统 */ -// Asset type constants for tilemap -// 瓦片地图资产类型常量 -export const TilemapAssetType = 'tilemap' as const; -export const TilesetAssetType = 'tileset' as const; +// Constants +export { TilemapAssetType, TilesetAssetType } from './constants'; // Component export { TilemapComponent } from './TilemapComponent'; @@ -33,8 +31,5 @@ export type { ITilesetAsset } from './loaders/TilesetLoader'; export { TiledConverter } from './loaders/TiledConverter'; export type { ITiledMap, ITiledConversionResult } from './loaders/TiledConverter'; -// Runtime module (no editor dependencies) -export { TilemapRuntimeModule } from './TilemapRuntimeModule'; - -// Plugin (for PluginManager - includes editor dependencies) -export { TilemapPlugin } from './editor/TilemapPlugin'; +// Runtime module and plugin +export { TilemapRuntimeModule, TilemapPlugin, type TilemapSystemContext } from './TilemapRuntimeModule'; diff --git a/packages/tilemap/src/loaders/TilemapLoader.ts b/packages/tilemap/src/loaders/TilemapLoader.ts index 8da3e314..44b9714b 100644 --- a/packages/tilemap/src/loaders/TilemapLoader.ts +++ b/packages/tilemap/src/loaders/TilemapLoader.ts @@ -10,7 +10,7 @@ import { AssetLoadError, IAssetLoader } from '@esengine/asset-system'; -import { TilemapAssetType } from '../index'; +import { TilemapAssetType } from '../constants'; /** * Tilemap data interface diff --git a/packages/tilemap/src/loaders/TilesetLoader.ts b/packages/tilemap/src/loaders/TilesetLoader.ts index 7f09d950..55e92c01 100644 --- a/packages/tilemap/src/loaders/TilesetLoader.ts +++ b/packages/tilemap/src/loaders/TilesetLoader.ts @@ -10,7 +10,7 @@ import { AssetLoadError, IAssetLoader } from '@esengine/asset-system'; -import { TilesetAssetType } from '../index'; +import { TilesetAssetType } from '../constants'; /** * Tileset data interface diff --git a/packages/tilemap/src/physics/TilemapPhysicsSystem.ts b/packages/tilemap/src/physics/TilemapPhysicsSystem.ts index 4b2a1404..ed502760 100644 --- a/packages/tilemap/src/physics/TilemapPhysicsSystem.ts +++ b/packages/tilemap/src/physics/TilemapPhysicsSystem.ts @@ -7,7 +7,7 @@ */ import { EntitySystem, Matcher, ECSSystem, type Entity, type Scene } from '@esengine/ecs-framework'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; import { TilemapComponent } from '../TilemapComponent'; import { TilemapCollider2DComponent, type CollisionRect } from './TilemapCollider2DComponent'; diff --git a/packages/tilemap/src/runtime.ts b/packages/tilemap/src/runtime.ts deleted file mode 100644 index 68adb124..00000000 --- a/packages/tilemap/src/runtime.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @esengine/tilemap Runtime Entry Point - * - * This entry point exports only runtime-related code without any editor dependencies. - * Use this for standalone game runtime builds. - * - * 此入口点仅导出运行时相关代码,不包含任何编辑器依赖。 - * 用于独立游戏运行时构建。 - */ - -// Components -export { TilemapComponent } from './TilemapComponent'; -export type { ITilemapData, ITilesetData } from './TilemapComponent'; -export type { ResizeAnchor } from './TilemapComponent'; - -export { TilemapCollider2DComponent, TilemapColliderMode } from './physics/TilemapCollider2DComponent'; -export type { CollisionRect } from './physics/TilemapCollider2DComponent'; - -// Systems -export { TilemapRenderingSystem } from './systems/TilemapRenderingSystem'; -export type { TilemapRenderData, ViewportBounds } from './systems/TilemapRenderingSystem'; - -export { TilemapPhysicsSystem } from './physics/TilemapPhysicsSystem'; -export type { IPhysicsWorld, IPhysics2DSystem } from './physics/TilemapPhysicsSystem'; - -// Loaders -export { TilemapLoader } from './loaders/TilemapLoader'; -export type { ITilemapAsset } from './loaders/TilemapLoader'; -export { TilesetLoader } from './loaders/TilesetLoader'; -export type { ITilesetAsset } from './loaders/TilesetLoader'; - -// Tiled converter -export { TiledConverter } from './loaders/TiledConverter'; -export type { ITiledMap, ITiledConversionResult } from './loaders/TiledConverter'; - -// Runtime module -export { TilemapRuntimeModule } from './TilemapRuntimeModule'; diff --git a/packages/tilemap/src/systems/TilemapRenderingSystem.ts b/packages/tilemap/src/systems/TilemapRenderingSystem.ts index 5caf4922..c2226c90 100644 --- a/packages/tilemap/src/systems/TilemapRenderingSystem.ts +++ b/packages/tilemap/src/systems/TilemapRenderingSystem.ts @@ -1,5 +1,5 @@ import { EntitySystem, Matcher, ECSSystem, Entity } from '@esengine/ecs-framework'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; import type { IRenderDataProvider } from '@esengine/ecs-engine-bindgen'; import { TilemapComponent } from '../TilemapComponent'; diff --git a/packages/tilemap/tsconfig.build.json b/packages/tilemap/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/tilemap/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/tilemap/tsconfig.json b/packages/tilemap/tsconfig.json index 98f43266..73b318fa 100644 --- a/packages/tilemap/tsconfig.json +++ b/packages/tilemap/tsconfig.json @@ -50,7 +50,7 @@ "path": "../core" }, { - "path": "../components" + "path": "../engine-core" }, { "path": "../ecs-engine-bindgen" diff --git a/packages/tilemap/tsup.config.ts b/packages/tilemap/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/tilemap/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/tilemap/vite.config.ts b/packages/tilemap/vite.config.ts deleted file mode 100644 index 71c14427..00000000 --- a/packages/tilemap/vite.config.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; -import dts from 'vite-plugin-dts'; -import react from '@vitejs/plugin-react'; - -/** - * 自定义插件:将 CSS 转换为自执行的样式注入代码 - * Custom plugin: Convert CSS to self-executing style injection code - * - * 当用户写 `import './styles.css'` 时,这个插件会: - * 1. 在构建时将 CSS 内容转换为 JS 代码 - * 2. JS 代码在模块导入时自动执行,将样式注入到 DOM - * 3. 使用唯一 ID 防止重复注入 - * - * When user writes `import './styles.css'`, this plugin will: - * 1. Convert CSS content to JS code during build - * 2. JS code auto-executes when module is imported, injecting styles to DOM - * 3. Uses unique ID to prevent duplicate injection - */ -function injectCSSPlugin(): any { - const cssIdMap = new Map(); - let cssCounter = 0; - - return { - name: 'inject-css-plugin', - enforce: 'post' as const, - generateBundle(_options: any, bundle: any) { - const bundleKeys = Object.keys(bundle); - - // 找到所有 CSS 文件 - const cssFiles = bundleKeys.filter(key => key.endsWith('.css')); - - for (const cssFile of cssFiles) { - const cssChunk = bundle[cssFile]; - if (!cssChunk || !cssChunk.source) continue; - - const cssContent = cssChunk.source; - const styleId = `esengine-tilemap-style-${cssCounter++}`; - cssIdMap.set(cssFile, styleId); - - // 生成样式注入代码 - const injectCode = `(function(){if(typeof document!=='undefined'){var s=document.createElement('style');s.id='${styleId}';if(!document.getElementById(s.id)){s.textContent=${JSON.stringify(cssContent)};document.head.appendChild(s);}}})();`; - - // 找到引用此 CSS 的 JS chunk 并注入代码 - for (const jsKey of bundleKeys) { - if (!jsKey.endsWith('.js')) continue; - const jsChunk = bundle[jsKey]; - if (!jsChunk || jsChunk.type !== 'chunk' || !jsChunk.code) continue; - - // 检查是否引用了这个 CSS(通过检查是否有相关的 import) - // 对于 vite 生成的代码,CSS 导入会被转换,所以我们直接注入到 editor/index.js - if (jsKey === 'editor/index.js' || jsKey.match(/^index-[^/]+\.js$/)) { - jsChunk.code = injectCode + '\n' + jsChunk.code; - } - } - - // 删除独立的 CSS 文件 - delete bundle[cssFile]; - } - } - }; -} - -export default defineConfig({ - plugins: [ - react(), - dts({ - include: ['src'], - outDir: 'dist', - rollupTypes: false - }), - injectCSSPlugin() - ], - esbuild: { - jsx: 'automatic', - }, - build: { - lib: { - entry: { - index: resolve(__dirname, 'src/index.ts'), - runtime: resolve(__dirname, 'src/runtime.ts'), - 'editor/index': resolve(__dirname, 'src/editor/index.ts') - }, - formats: ['es'], - fileName: (format, entryName) => `${entryName}.js` - }, - rollupOptions: { - external: [ - '@esengine/ecs-framework', - '@esengine/ecs-components', - '@esengine/ecs-engine-bindgen', - '@esengine/asset-system', - '@esengine/editor-core', - 'react', - 'react/jsx-runtime', - 'lucide-react', - 'zustand', - /^@esengine\//, - /^@tauri-apps\// - ], - output: { - exports: 'named', - preserveModules: false - } - }, - target: 'es2020', - minify: false, - sourcemap: true - } -}); diff --git a/packages/ui-editor/package.json b/packages/ui-editor/package.json new file mode 100644 index 00000000..d123d385 --- /dev/null +++ b/packages/ui-editor/package.json @@ -0,0 +1,47 @@ +{ + "name": "@esengine/ui-editor", + "version": "1.0.0", + "description": "Editor support for @esengine/ui - inspectors, gizmos, and entity templates", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./plugin.json": "./plugin.json" + }, + "files": [ + "dist", + "plugin.json" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "dependencies": { + "@esengine/ui": "workspace:*" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "lucide-react": "^0.545.0", + "react": "^18.3.1", + "@types/react": "^18.3.12", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "ui", + "editor" + ], + "author": "", + "license": "MIT" +} diff --git a/packages/ui/src/editor/gizmos/UITransformGizmo.ts b/packages/ui-editor/src/gizmos/UITransformGizmo.ts similarity index 59% rename from packages/ui/src/editor/gizmos/UITransformGizmo.ts rename to packages/ui-editor/src/gizmos/UITransformGizmo.ts index 20be2b2c..73a66643 100644 --- a/packages/ui/src/editor/gizmos/UITransformGizmo.ts +++ b/packages/ui-editor/src/gizmos/UITransformGizmo.ts @@ -1,7 +1,7 @@ import type { Entity } from '@esengine/ecs-framework'; import type { IGizmoRenderData, IRectGizmoData, GizmoColor } from '@esengine/editor-core'; import { GizmoRegistry } from '@esengine/editor-core'; -import { UITransformComponent } from '../../components'; +import { UITransformComponent } from '@esengine/ui'; const UI_GIZMO_COLOR: GizmoColor = { r: 0.2, g: 0.6, b: 1, a: 0.8 }; const UI_GIZMO_COLOR_UNSELECTED: GizmoColor = { r: 0.2, g: 0.6, b: 1, a: 0.3 }; @@ -21,24 +21,33 @@ function uiTransformGizmoProvider( // 否则回退到本地坐标 const x = transform.worldX ?? transform.x; const y = transform.worldY ?? transform.y; - const width = (transform.computedWidth ?? transform.width) * transform.scaleX; - const height = (transform.computedHeight ?? transform.height) * transform.scaleY; + // Use world scale for proper hierarchical transform inheritance + // 使用世界缩放以正确继承层级变换 + const scaleX = transform.worldScaleX ?? transform.scaleX; + const scaleY = transform.worldScaleY ?? transform.scaleY; + const width = (transform.computedWidth ?? transform.width) * scaleX; + const height = (transform.computedHeight ?? transform.height) * scaleY; + // Use world rotation for proper hierarchical transform inheritance + // 使用世界旋转以正确继承层级变换 + const rotation = transform.worldRotation ?? transform.rotation; + // 使用 transform 的 pivot 作为旋转/缩放中心 + const pivotX = transform.pivotX; + const pivotY = transform.pivotY; + // 渲染位置 = 左下角 + pivot 偏移 + const renderX = x + width * pivotX; + const renderY = y + height * pivotY; - // Use bottom-left position with origin at (0, 0) - // x, y is bottom-left corner in UITransform coordinate system (Y-up) - // This matches Gizmo origin=(0,0) which means reference point is at bottom-left - // 使用左下角位置,原点在 (0, 0) - // UITransform 坐标系中 x, y 是左下角(Y 向上) - // 这与 Gizmo origin=(0,0) 匹配,表示参考点在左下角 + // Use pivot position with transform's pivot values as origin + // 使用 transform 的 pivot 值作为 gizmo 的原点 const gizmo: IRectGizmoData = { type: 'rect', - x, - y, + x: renderX, + y: renderY, width, height, - rotation: transform.rotation, - originX: 0, - originY: 0, + rotation, + originX: pivotX, + originY: pivotY, color: isSelected ? UI_GIZMO_COLOR : UI_GIZMO_COLOR_UNSELECTED, showHandles: isSelected }; diff --git a/packages/ui/src/editor/gizmos/index.ts b/packages/ui-editor/src/gizmos/index.ts similarity index 100% rename from packages/ui/src/editor/gizmos/index.ts rename to packages/ui-editor/src/gizmos/index.ts diff --git a/packages/ui/src/editor/index.ts b/packages/ui-editor/src/index.ts similarity index 94% rename from packages/ui/src/editor/index.ts rename to packages/ui-editor/src/index.ts index c63faf57..d1869b85 100644 --- a/packages/ui/src/editor/index.ts +++ b/packages/ui-editor/src/index.ts @@ -1,18 +1,15 @@ /** - * UI 编辑器模块入口 - * UI Editor Module Entry + * @esengine/ui-editor + * + * Editor support for @esengine/ui - inspectors, gizmos, and entity templates + * UI 编辑器支持 - 检视器、Gizmo 和实体模板 */ -import React from 'react'; -import { LayoutGrid, Square, Type, MousePointer2, Sliders, BarChart3, ScrollText, PanelTop } from 'lucide-react'; import type { ServiceContainer, Entity } from '@esengine/ecs-framework'; import { Core } from '@esengine/ecs-framework'; import type { IEditorModuleLoader, - PanelDescriptor, - EntityCreationTemplate, - ComponentAction, - ComponentInspectorProviderDef + EntityCreationTemplate } from '@esengine/editor-core'; import { EntityStoreService, @@ -21,7 +18,7 @@ import { ComponentInspectorRegistry } from '@esengine/editor-core'; -// Local imports +// Runtime imports from @esengine/ui import { UITransformComponent, UIRenderComponent, @@ -35,7 +32,7 @@ import { UIProgressBarComponent, UISliderComponent, UIScrollViewComponent -} from '../components'; +} from '@esengine/ui'; import { UITransformInspector } from './inspectors'; import { registerUITransformGizmo, unregisterUITransformGizmo } from './gizmos'; @@ -405,6 +402,27 @@ export class UIEditorModule implements IEditorModuleLoader { export const uiEditorModule = new UIEditorModule(); -// Plugin exports -export { UIPlugin, UIRuntimeModule } from './UIPlugin'; +// 从 @esengine/ui 导入运行时模块 +import { UIRuntimeModule } from '@esengine/ui'; +import type { IPlugin, PluginDescriptor } from '@esengine/editor-core'; + +const descriptor: PluginDescriptor = { + id: '@esengine/ui', + name: 'UI', + version: '1.0.0', + description: 'ECS-based UI system with editor support', + category: 'ui', + enabledByDefault: true +}; + +/** + * 完整的 UI 插件(运行时 + 编辑器) + * Complete UI Plugin (runtime + editor) + */ +export const UIPlugin: IPlugin = { + descriptor, + runtimeModule: new UIRuntimeModule(), + editorModule: uiEditorModule +}; + export default uiEditorModule; diff --git a/packages/ui/src/editor/inspectors/UITransformInspector.tsx b/packages/ui-editor/src/inspectors/UITransformInspector.tsx similarity index 99% rename from packages/ui/src/editor/inspectors/UITransformInspector.tsx rename to packages/ui-editor/src/inspectors/UITransformInspector.tsx index ef20666f..6908a945 100644 --- a/packages/ui/src/editor/inspectors/UITransformInspector.tsx +++ b/packages/ui-editor/src/inspectors/UITransformInspector.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { Component } from '@esengine/ecs-framework'; import type { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core'; -import { UITransformComponent, AnchorPreset } from '../../components'; +import { UITransformComponent, AnchorPreset } from '@esengine/ui'; const DraggableNumberInput: React.FC<{ axis?: 'x' | 'y' | 'z' | 'w'; diff --git a/packages/ui/src/editor/inspectors/index.ts b/packages/ui-editor/src/inspectors/index.ts similarity index 100% rename from packages/ui/src/editor/inspectors/index.ts rename to packages/ui-editor/src/inspectors/index.ts diff --git a/packages/ui-editor/tsconfig.build.json b/packages/ui-editor/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/ui-editor/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/ui-editor/tsconfig.json b/packages/ui-editor/tsconfig.json new file mode 100644 index 00000000..d099ddd8 --- /dev/null +++ b/packages/ui-editor/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/ui-editor/tsup.config.ts b/packages/ui-editor/tsup.config.ts new file mode 100644 index 00000000..b4f49f5d --- /dev/null +++ b/packages/ui-editor/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { editorOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...editorOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/ui/package.json b/packages/ui/package.json index 31aa6f76..6437cf84 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -2,6 +2,11 @@ "name": "@esengine/ui", "version": "1.0.0", "description": "ECS-based UI system with WebGL rendering for games", + "esengine": { + "plugin": true, + "pluginExport": "UIPlugin", + "category": "ui" + }, "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", @@ -10,52 +15,24 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" - }, - "./runtime": { - "types": "./dist/runtime.d.ts", - "import": "./dist/runtime.js" - }, - "./editor": { - "types": "./dist/editor/index.d.ts", - "import": "./dist/editor/index.js" - }, - "./plugin.json": "./plugin.json" + } }, "files": [ - "dist", - "plugin.json" + "dist" ], "scripts": { - "build": "vite build", - "build:watch": "vite build --watch", + "build": "tsup", + "build:watch": "tsup --watch", "type-check": "tsc --noEmit", "clean": "rimraf dist" }, - "peerDependencies": { - "@esengine/ecs-framework": ">=2.0.0", - "@esengine/ecs-components": "workspace:*", - "@esengine/editor-core": "workspace:*", - "lucide-react": "^0.545.0", - "react": "^18.3.1" - }, - "peerDependenciesMeta": { - "@esengine/editor-core": { - "optional": true - }, - "react": { - "optional": true - }, - "lucide-react": { - "optional": true - } - }, "devDependencies": { - "@types/react": "^18.3.12", - "@vitejs/plugin-react": "^4.7.0", + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/build-config": "workspace:*", "rimraf": "^5.0.5", - "typescript": "^5.3.3", - "vite": "^5.0.0", - "vite-plugin-dts": "^3.7.0" + "tsup": "^8.0.0", + "typescript": "^5.3.3" }, "keywords": [ "ecs", diff --git a/packages/ui/src/UIBuilder.ts b/packages/ui/src/UIBuilder.ts index ed2b735a..a45caa45 100644 --- a/packages/ui/src/UIBuilder.ts +++ b/packages/ui/src/UIBuilder.ts @@ -1,4 +1,4 @@ -import { Entity, Scene } from '@esengine/ecs-framework'; +import { Entity, Scene, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; import { UITransformComponent, AnchorPreset } from './components/UITransformComponent'; import { UIRenderComponent, UIRenderType } from './components/UIRenderComponent'; import { UIInteractableComponent } from './components/UIInteractableComponent'; @@ -145,6 +145,9 @@ export class UIBuilder { private createBase(config: UIBaseConfig, defaultName: string): Entity { const entity = this.scene.createEntity(config.name ?? `${defaultName}_${this.idCounter++}`); + // 添加 HierarchyComponent 支持层级结构 + entity.addComponent(new HierarchyComponent()); + const transform = entity.addComponent(new UITransformComponent()); transform.x = config.x ?? 0; transform.y = config.y ?? 0; @@ -419,7 +422,10 @@ export class UIBuilder { * Add child to parent */ public addChild(parent: Entity, child: Entity): Entity { - parent.addChild(child); + const hierarchySystem = this.scene.getSystem(HierarchySystem); + if (hierarchySystem) { + hierarchySystem.setParent(child, parent); + } return child; } @@ -428,8 +434,11 @@ export class UIBuilder { * Add multiple children to parent */ public addChildren(parent: Entity, children: Entity[]): Entity[] { - for (const child of children) { - parent.addChild(child); + const hierarchySystem = this.scene.getSystem(HierarchySystem); + if (hierarchySystem) { + for (const child of children) { + hierarchySystem.setParent(child, parent); + } } return children; } diff --git a/packages/ui/src/UIRuntimeModule.ts b/packages/ui/src/UIRuntimeModule.ts index 2aa07771..2d8bfba5 100644 --- a/packages/ui/src/UIRuntimeModule.ts +++ b/packages/ui/src/UIRuntimeModule.ts @@ -1,11 +1,6 @@ -/** - * UI Runtime Module (Pure runtime, no editor dependencies) - * UI 运行时模块(纯运行时,无编辑器依赖) - */ - import type { IScene } from '@esengine/ecs-framework'; import { ComponentRegistry } from '@esengine/ecs-framework'; -import type { IRuntimeModuleLoader, SystemContext } from '@esengine/ecs-components'; +import type { IRuntimeModule, IPlugin, PluginDescriptor, SystemContext } from '@esengine/engine-core'; import { UITransformComponent, @@ -32,11 +27,14 @@ import { UIScrollViewRenderSystem } from './systems/render'; -/** - * UI Runtime Module - * UI 运行时模块 - */ -export class UIRuntimeModule implements IRuntimeModuleLoader { +export interface UISystemContext extends SystemContext { + uiLayoutSystem?: UILayoutSystem; + uiRenderProvider?: UIRenderDataProvider; + uiInputSystem?: UIInputSystem; + uiTextRenderSystem?: UITextRenderSystem; +} + +class UIRuntimeModule implements IRuntimeModule { registerComponents(registry: typeof ComponentRegistry): void { registry.register(UITransformComponent); registry.register(UIRenderComponent); @@ -50,6 +48,8 @@ export class UIRuntimeModule implements IRuntimeModuleLoader { } createSystems(scene: IScene, context: SystemContext): void { + const uiContext = context as UISystemContext; + const layoutSystem = new UILayoutSystem(); scene.addSystem(layoutSystem); @@ -77,9 +77,9 @@ export class UIRuntimeModule implements IRuntimeModuleLoader { const textRenderSystem = new UITextRenderSystem(); scene.addSystem(textRenderSystem); - if (context.engineBridge) { + if (uiContext.engineBridge) { textRenderSystem.setTextureCallback((id: number, dataUrl: string) => { - context.engineBridge.loadTexture(id, dataUrl); + uiContext.engineBridge.loadTexture(id, dataUrl); }); } @@ -88,9 +88,26 @@ export class UIRuntimeModule implements IRuntimeModuleLoader { inputSystem.setLayoutSystem(layoutSystem); scene.addSystem(inputSystem); - context.uiLayoutSystem = layoutSystem; - context.uiRenderProvider = uiRenderProvider; - context.uiInputSystem = inputSystem; - context.uiTextRenderSystem = textRenderSystem; + uiContext.uiLayoutSystem = layoutSystem; + uiContext.uiRenderProvider = uiRenderProvider; + uiContext.uiInputSystem = inputSystem; + uiContext.uiTextRenderSystem = textRenderSystem; } } + +const descriptor: PluginDescriptor = { + id: '@esengine/ui', + name: 'UI', + version: '1.0.0', + description: 'ECS-based UI system', + category: 'ui', + enabledByDefault: true, + isEnginePlugin: true +}; + +export const UIPlugin: IPlugin = { + descriptor, + runtimeModule: new UIRuntimeModule() +}; + +export { UIRuntimeModule }; diff --git a/packages/ui/src/components/UITransformComponent.ts b/packages/ui/src/components/UITransformComponent.ts index 04cfedd0..90c15a8b 100644 --- a/packages/ui/src/components/UITransformComponent.ts +++ b/packages/ui/src/components/UITransformComponent.ts @@ -232,6 +232,32 @@ export class UITransformComponent extends Component { */ public worldAlpha: number = 1; + /** + * 计算后的世界旋转(弧度,考虑父元素旋转) + * Computed world rotation in radians (considering parent rotation) + */ + public worldRotation: number = 0; + + /** + * 计算后的世界 X 缩放(考虑父元素缩放) + * Computed world X scale (considering parent scale) + */ + public worldScaleX: number = 1; + + /** + * 计算后的世界 Y 缩放(考虑父元素缩放) + * Computed world Y scale (considering parent scale) + */ + public worldScaleY: number = 1; + + /** + * 本地到世界的 2D 变换矩阵(只读,由 UILayoutSystem 计算) + * Local to world 2D transformation matrix (readonly, computed by UILayoutSystem) + */ + public localToWorldMatrix: { a: number; b: number; c: number; d: number; tx: number; ty: number } = { + a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 + }; + /** * 布局是否需要更新 * Flag indicating layout needs update diff --git a/packages/ui/src/editor/UIPlugin.ts b/packages/ui/src/editor/UIPlugin.ts deleted file mode 100644 index c98179e5..00000000 --- a/packages/ui/src/editor/UIPlugin.ts +++ /dev/null @@ -1,159 +0,0 @@ -/** - * UI 统一插件 - * UI Unified Plugin - */ - -import type { IScene, ServiceContainer } from '@esengine/ecs-framework'; -import { ComponentRegistry } from '@esengine/ecs-framework'; -import type { - IPluginLoader, - IRuntimeModuleLoader, - PluginDescriptor, - SystemContext -} from '@esengine/editor-core'; - -// Editor imports -import { UIEditorModule } from './index'; - -// Runtime imports -import { - UITransformComponent, - UIRenderComponent, - UIInteractableComponent, - UITextComponent, - UILayoutComponent, - UIButtonComponent, - UIProgressBarComponent, - UISliderComponent, - UIScrollViewComponent -} from '../components'; -import { UILayoutSystem } from '../systems/UILayoutSystem'; -import { UIInputSystem } from '../systems/UIInputSystem'; -import { UIRenderDataProvider } from '../systems/UIRenderDataProvider'; -// Render systems -import { - UIRenderBeginSystem, - UIRectRenderSystem, - UITextRenderSystem, - UIButtonRenderSystem, - UIProgressBarRenderSystem, - UISliderRenderSystem, - UIScrollViewRenderSystem -} from '../systems/render'; - -/** - * 插件描述符 - */ -const descriptor: PluginDescriptor = { - id: '@esengine/ui', - name: 'UI System', - version: '1.0.0', - description: '游戏 UI 系统,支持布局、交互、动画等', - category: 'ui', - enabledByDefault: true, - canContainContent: false, - isEnginePlugin: true, - modules: [ - { - name: 'UIRuntime', - type: 'runtime', - loadingPhase: 'default', - entry: './src/index.ts' - }, - { - name: 'UIEditor', - type: 'editor', - loadingPhase: 'default', - entry: './src/editor/index.ts' - } - ], - dependencies: [ - { id: '@esengine/core', version: '^1.0.0' } - ], - icon: 'LayoutGrid' -}; - -/** - * UI 运行时模块 - * UI runtime module - */ -export class UIRuntimeModule implements IRuntimeModuleLoader { - registerComponents(registry: typeof ComponentRegistry): void { - registry.register(UITransformComponent); - registry.register(UIRenderComponent); - registry.register(UIInteractableComponent); - registry.register(UITextComponent); - registry.register(UILayoutComponent); - registry.register(UIButtonComponent); - registry.register(UIProgressBarComponent); - registry.register(UISliderComponent); - registry.register(UIScrollViewComponent); - } - - createSystems(scene: IScene, context: SystemContext): void { - // UI Layout System (order: 50) - const layoutSystem = new UILayoutSystem(); - scene.addSystem(layoutSystem); - - // UI Render Begin System - clears collector at start of frame (order: 99) - const renderBeginSystem = new UIRenderBeginSystem(); - scene.addSystem(renderBeginSystem); - - // UI Render Systems - collect render data (order: 100-120) - const rectRenderSystem = new UIRectRenderSystem(); - scene.addSystem(rectRenderSystem); - - const progressBarRenderSystem = new UIProgressBarRenderSystem(); - scene.addSystem(progressBarRenderSystem); - - const sliderRenderSystem = new UISliderRenderSystem(); - scene.addSystem(sliderRenderSystem); - - const scrollViewRenderSystem = new UIScrollViewRenderSystem(); - scene.addSystem(scrollViewRenderSystem); - - const buttonRenderSystem = new UIButtonRenderSystem(); - scene.addSystem(buttonRenderSystem); - - const textRenderSystem = new UITextRenderSystem(); - scene.addSystem(textRenderSystem); - - // Set up text texture callback to register textures with engine - // 设置文本纹理回调以将纹理注册到引擎 - if (context.engineBridge) { - textRenderSystem.setTextureCallback((id: number, dataUrl: string) => { - // Load data URL as texture - context.engineBridge.loadTexture(id, dataUrl); - }); - } - - // UI Render Data Provider (not a system, just a provider) - // Note: Don't call addRenderDataProvider here - UI provider should be set via - // setUIRenderDataProvider for proper preview mode support - // 注意:不要在这里调用 addRenderDataProvider - UI 提供者应该通过 - // setUIRenderDataProvider 设置以支持预览模式 - const uiRenderProvider = new UIRenderDataProvider(); - - // UI Input System - const inputSystem = new UIInputSystem(); - scene.addSystem(inputSystem); - - // 保存引用 | Save references - context.uiLayoutSystem = layoutSystem; - context.uiRenderProvider = uiRenderProvider; - context.uiInputSystem = inputSystem; - context.uiTextRenderSystem = textRenderSystem; - } -} - -/** - * UI 插件加载器 - * UI plugin loader - */ -export const UIPlugin: IPluginLoader = { - descriptor, - runtimeModule: new UIRuntimeModule(), - editorModule: new UIEditorModule(), -}; - -export default UIPlugin; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index f568f87b..5cf2be07 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -161,8 +161,5 @@ export { type UIScrollViewConfig } from './UIBuilder'; -// Runtime module (no editor dependencies) -export { UIRuntimeModule } from './UIRuntimeModule'; - -// Plugin (for PluginManager - includes editor dependencies) -export { UIPlugin } from './editor/UIPlugin'; +// Runtime module and plugin +export { UIRuntimeModule, UIPlugin, type UISystemContext } from './UIRuntimeModule'; diff --git a/packages/ui/src/runtime.ts b/packages/ui/src/runtime.ts deleted file mode 100644 index 924846d5..00000000 --- a/packages/ui/src/runtime.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * @esengine/ui Runtime Entry Point - * - * This entry point exports only runtime-related code without any editor dependencies. - * Use this for standalone game runtime builds. - * - * 此入口点仅导出运行时相关代码,不包含任何编辑器依赖。 - * 用于独立游戏运行时构建。 - */ - -// Components - Core -export { - UITransformComponent, - AnchorPreset -} from './components/UITransformComponent'; - -export { - UIRenderComponent, - UIRenderType, - type UIBorderStyle, - type UIShadowStyle -} from './components/UIRenderComponent'; - -export { - UIInteractableComponent, - type UICursorType -} from './components/UIInteractableComponent'; - -export { - UITextComponent, - type UITextAlign, - type UITextVerticalAlign, - type UITextOverflow, - type UIFontWeight -} from './components/UITextComponent'; - -export { - UILayoutComponent, - UILayoutType, - UIJustifyContent, - UIAlignItems, - type UIPadding -} from './components/UILayoutComponent'; - -// Components - Widgets -export { - UIButtonComponent, - type UIButtonStyle, - type UIButtonDisplayMode -} from './components/widgets/UIButtonComponent'; - -export { - UIProgressBarComponent, - UIProgressDirection, - UIProgressFillMode -} from './components/widgets/UIProgressBarComponent'; - -export { - UISliderComponent, - UISliderOrientation -} from './components/widgets/UISliderComponent'; - -export { - UIScrollViewComponent, - UIScrollbarVisibility -} from './components/widgets/UIScrollViewComponent'; - -// Systems - Core -export { UILayoutSystem } from './systems/UILayoutSystem'; -export { UIInputSystem, MouseButton, type UIInputEvent } from './systems/UIInputSystem'; -export { UIAnimationSystem, Easing, type EasingFunction, type EasingName } from './systems/UIAnimationSystem'; -export { UIRenderDataProvider, type IRenderDataProvider, type IUIRenderDataProvider } from './systems/UIRenderDataProvider'; - -// Systems - Render (ECS-compliant render systems) -export { - // Collector - UIRenderCollector, - getUIRenderCollector, - resetUIRenderCollector, - invalidateUIRenderCaches, - type UIRenderPrimitive, - type ProviderRenderData, - // Render systems - UIRenderBeginSystem, - UIRectRenderSystem, - UITextRenderSystem, - UIButtonRenderSystem, - UIProgressBarRenderSystem, - UISliderRenderSystem, - UIScrollViewRenderSystem -} from './systems/render'; - -// Rendering -export { WebGLUIRenderer } from './rendering/WebGLUIRenderer'; -export { TextRenderer, type TextMeasurement, type TextRenderOptions } from './rendering/TextRenderer'; - -// Builder API -export { - UIBuilder, - type UIBaseConfig, - type UIButtonConfig, - type UITextConfig, - type UIImageConfig, - type UIProgressBarConfig, - type UISliderConfig, - type UIPanelConfig, - type UIScrollViewConfig -} from './UIBuilder'; - -// Runtime module -export { UIRuntimeModule } from './UIRuntimeModule'; diff --git a/packages/ui/src/systems/UILayoutSystem.ts b/packages/ui/src/systems/UILayoutSystem.ts index 4c3c5b3c..8d81c2bd 100644 --- a/packages/ui/src/systems/UILayoutSystem.ts +++ b/packages/ui/src/systems/UILayoutSystem.ts @@ -1,7 +1,20 @@ -import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework'; +import { EntitySystem, Matcher, Entity, ECSSystem, HierarchyComponent } from '@esengine/ecs-framework'; import { UITransformComponent } from '../components/UITransformComponent'; import { UILayoutComponent, UILayoutType, UIJustifyContent, UIAlignItems } from '../components/UILayoutComponent'; +/** + * 2D 变换矩阵类型 + * 2D transformation matrix type + */ +interface Matrix2D { + a: number; // scaleX * cos(rotation) + b: number; // scaleX * sin(rotation) + c: number; // scaleY * -sin(rotation) + d: number; // scaleY * cos(rotation) + tx: number; // translateX + ty: number; // translateY +} + /** * UI 布局系统 * UI Layout System - Computes layout for UI elements @@ -9,6 +22,9 @@ import { UILayoutComponent, UILayoutType, UIJustifyContent, UIAlignItems } from * 计算 UI 元素的世界坐标和尺寸 * Computes world coordinates and sizes for UI elements * + * 使用矩阵乘法计算世界变换:worldMatrix = parentMatrix * localMatrix + * Uses matrix multiplication for world transforms: worldMatrix = parentMatrix * localMatrix + * * 注意:canvasWidth/canvasHeight 是 UI 设计的参考尺寸,不是实际渲染视口大小 * Note: canvasWidth/canvasHeight is the UI design reference size, not the actual render viewport size */ @@ -60,7 +76,14 @@ export class UILayoutSystem extends EntitySystem { protected process(entities: readonly Entity[]): void { // 首先处理根元素(没有父元素的) - const rootEntities = entities.filter(e => !e.parent || !e.parent.hasComponent(UITransformComponent)); + const rootEntities = entities.filter(e => { + const hierarchy = e.getComponent(HierarchyComponent); + if (!hierarchy || hierarchy.parentId === null) { + return true; + } + const parent = this.scene?.findEntityById(hierarchy.parentId); + return !parent || !parent.hasComponent(UITransformComponent); + }); // 画布中心为原点,Y 轴向上为正 // Canvas center is origin, Y axis points up @@ -69,8 +92,11 @@ export class UILayoutSystem extends EntitySystem { const parentX = -this.canvasWidth / 2; const parentY = this.canvasHeight / 2; // Y 轴向上,所以顶部是正值 + // 根元素使用单位矩阵作为父矩阵 + const identityMatrix: Matrix2D = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }; + for (const entity of rootEntities) { - this.layoutEntity(entity, parentX, parentY, this.canvasWidth, this.canvasHeight, 1); + this.layoutEntity(entity, parentX, parentY, this.canvasWidth, this.canvasHeight, 1, identityMatrix); } } @@ -84,7 +110,8 @@ export class UILayoutSystem extends EntitySystem { parentY: number, parentWidth: number, parentHeight: number, - parentAlpha: number + parentAlpha: number, + parentMatrix: Matrix2D ): void { const transform = entity.getComponent(UITransformComponent); if (!transform) return; @@ -160,19 +187,23 @@ export class UILayoutSystem extends EntitySystem { worldY = anchorMaxY - transform.y; } - // 更新计算后的值 + // 更新布局计算的值 transform.worldX = worldX; transform.worldY = worldY; transform.computedWidth = width; transform.computedHeight = height; transform.worldAlpha = parentAlpha * transform.alpha; + + // 使用矩阵乘法计算世界变换 + this.updateWorldMatrix(transform, parentMatrix); + transform.layoutDirty = false; // 如果元素不可见,跳过子元素 if (!transform.visible) return; // 处理子元素布局 - const children = entity.children.filter(c => c.hasComponent(UITransformComponent)); + const children = this.getUIChildren(entity); if (children.length === 0) return; // 计算子元素的父容器边界 @@ -192,7 +223,8 @@ export class UILayoutSystem extends EntitySystem { childParentY, width, height, - transform.worldAlpha + transform.worldAlpha, + transform.localToWorldMatrix ); } } @@ -234,7 +266,8 @@ export class UILayoutSystem extends EntitySystem { parentTopY, parentTransform.computedWidth, parentTransform.computedHeight, - parentTransform.worldAlpha + parentTransform.worldAlpha, + parentTransform.localToWorldMatrix ); } } @@ -328,6 +361,8 @@ export class UILayoutSystem extends EntitySystem { childTransform.computedWidth = size.width; childTransform.computedHeight = childHeight; childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha; + // 使用矩阵乘法计算世界旋转和缩放 + this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix); childTransform.layoutDirty = false; // 递归处理子元素的子元素 @@ -424,6 +459,8 @@ export class UILayoutSystem extends EntitySystem { childTransform.computedWidth = childWidth; childTransform.computedHeight = size.height; childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha; + // 使用矩阵乘法计算世界旋转和缩放 + this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix); childTransform.layoutDirty = false; this.processChildrenRecursive(child, childTransform); @@ -478,18 +515,49 @@ export class UILayoutSystem extends EntitySystem { childTransform.computedWidth = cellWidth; childTransform.computedHeight = cellHeight; childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha; + // 使用矩阵乘法计算世界旋转和缩放 + this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix); childTransform.layoutDirty = false; this.processChildrenRecursive(child, childTransform); } } + /** + * 获取具有 UITransformComponent 的子实体 + * Get child entities that have UITransformComponent + * + * 优先使用 HierarchyComponent,如果没有则返回空数组 + */ + private getUIChildren(entity: Entity): Entity[] { + const hierarchy = entity.getComponent(HierarchyComponent); + + // 如果没有 HierarchyComponent,返回空数组 + // UI 实体应该通过 UIBuilder 创建,会自动添加 HierarchyComponent + if (!hierarchy) { + return []; + } + + if (hierarchy.childIds.length === 0) { + return []; + } + + const children: Entity[] = []; + for (const childId of hierarchy.childIds) { + const child = this.scene?.findEntityById(childId); + if (child && child.hasComponent(UITransformComponent)) { + children.push(child); + } + } + return children; + } + /** * 递归处理子元素 * Recursively process children */ private processChildrenRecursive(entity: Entity, parentTransform: UITransformComponent): void { - const children = entity.children.filter(c => c.hasComponent(UITransformComponent)); + const children = this.getUIChildren(entity); if (children.length === 0) return; // 计算子元素的父容器顶部 Y(worldY 是底部,顶部 = 底部 + 高度) @@ -506,9 +574,129 @@ export class UILayoutSystem extends EntitySystem { parentTopY, parentTransform.computedWidth, parentTransform.computedHeight, - parentTransform.worldAlpha + parentTransform.worldAlpha, + parentTransform.localToWorldMatrix ); } } } + + // ===== 矩阵计算方法 Matrix calculation methods ===== + + /** + * 计算本地变换矩阵 + * Calculate local transformation matrix + * + * @param pivotX - 轴心点 X (0-1) + * @param pivotY - 轴心点 Y (0-1) + * @param width - 元素宽度 + * @param height - 元素高度 + * @param rotation - 旋转角度(弧度) + * @param scaleX - X 缩放 + * @param scaleY - Y 缩放 + * @param x - 元素世界 X 位置 + * @param y - 元素世界 Y 位置 + */ + private calculateLocalMatrix( + pivotX: number, + pivotY: number, + width: number, + height: number, + rotation: number, + scaleX: number, + scaleY: number, + x: number, + y: number + ): Matrix2D { + const cos = Math.cos(rotation); + const sin = Math.sin(rotation); + + // 轴心点相对于元素左下角的偏移 + const px = width * pivotX; + const py = height * pivotY; + + // 构建变换矩阵: Translate(-pivot) -> Scale -> Rotate -> Translate(position + pivot) + // 最终矩阵将轴心点作为旋转/缩放中心 + return { + a: scaleX * cos, + b: scaleX * sin, + c: scaleY * -sin, + d: scaleY * cos, + tx: x + px - (scaleX * cos * px - scaleY * sin * py), + ty: y + py - (scaleX * sin * px + scaleY * cos * py) + }; + } + + /** + * 矩阵乘法: result = a * b + * Matrix multiplication: result = a * b + */ + private multiplyMatrices(a: Matrix2D, b: Matrix2D): Matrix2D { + return { + a: a.a * b.a + a.c * b.b, + b: a.b * b.a + a.d * b.b, + c: a.a * b.c + a.c * b.d, + d: a.b * b.c + a.d * b.d, + tx: a.a * b.tx + a.c * b.ty + a.tx, + ty: a.b * b.tx + a.d * b.ty + a.ty + }; + } + + /** + * 从世界矩阵分解出旋转和缩放 + * Decompose rotation and scale from world matrix + */ + private decomposeMatrix(m: Matrix2D): { rotation: number; scaleX: number; scaleY: number } { + // 计算缩放 + const scaleX = Math.sqrt(m.a * m.a + m.b * m.b); + const scaleY = Math.sqrt(m.c * m.c + m.d * m.d); + + // 检测负缩放(通过行列式符号) + const det = m.a * m.d - m.b * m.c; + const sign = det < 0 ? -1 : 1; + + // 计算旋转(从归一化的矩阵) + let rotation = 0; + if (scaleX > 1e-10) { + rotation = Math.atan2(m.b / scaleX, m.a / scaleX); + } + + return { + rotation, + scaleX, + scaleY: scaleY * sign + }; + } + + /** + * 更新元素的世界变换矩阵 + * Update element's world transformation matrix + */ + private updateWorldMatrix(transform: UITransformComponent, parentMatrix: Matrix2D | null): void { + // 计算本地矩阵 + const localMatrix = this.calculateLocalMatrix( + transform.pivotX, + transform.pivotY, + transform.computedWidth, + transform.computedHeight, + transform.rotation, + transform.scaleX, + transform.scaleY, + transform.worldX, + transform.worldY + ); + + // 计算世界矩阵 + if (parentMatrix) { + transform.localToWorldMatrix = this.multiplyMatrices(parentMatrix, localMatrix); + } else { + transform.localToWorldMatrix = localMatrix; + } + + // 从世界矩阵分解出世界旋转和缩放 + const decomposed = this.decomposeMatrix(transform.localToWorldMatrix); + transform.worldRotation = decomposed.rotation; + transform.worldScaleX = decomposed.scaleX; + transform.worldScaleY = decomposed.scaleY; + } } diff --git a/packages/ui/src/systems/render/UIButtonRenderSystem.ts b/packages/ui/src/systems/render/UIButtonRenderSystem.ts index 1291e1f5..25ec2850 100644 --- a/packages/ui/src/systems/render/UIButtonRenderSystem.ts +++ b/packages/ui/src/systems/render/UIButtonRenderSystem.ts @@ -48,10 +48,20 @@ export class UIButtonRenderSystem extends EntitySystem { const x = transform.worldX ?? transform.x; const y = transform.worldY ?? transform.y; - const width = (transform.computedWidth ?? transform.width) * transform.scaleX; - const height = (transform.computedHeight ?? transform.height) * transform.scaleY; + // 使用世界缩放和旋转 + const scaleX = transform.worldScaleX ?? transform.scaleX; + const scaleY = transform.worldScaleY ?? transform.scaleY; + const rotation = transform.worldRotation ?? transform.rotation; + const width = (transform.computedWidth ?? transform.width) * scaleX; + const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; const baseOrder = 100 + transform.zIndex; + // 使用 transform 的 pivot 作为旋转/缩放中心 + const pivotX = transform.pivotX; + const pivotY = transform.pivotY; + // 渲染位置 = 左下角 + pivot 偏移 + const renderX = x + width * pivotX; + const renderY = y + height * pivotY; // Render texture if in texture or both mode // 如果在纹理或两者模式下,渲染纹理 @@ -59,15 +69,15 @@ export class UIButtonRenderSystem extends EntitySystem { const texture = button.getStateTexture('normal'); if (texture) { collector.addRect( - x, y, + renderX, renderY, width, height, 0xFFFFFF, // White tint for texture alpha, baseOrder, { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0, + rotation, + pivotX, + pivotY, texturePath: texture } ); @@ -80,15 +90,15 @@ export class UIButtonRenderSystem extends EntitySystem { const bgAlpha = render?.backgroundAlpha ?? 1; if (bgAlpha > 0) { collector.addRect( - x, y, + renderX, renderY, width, height, button.currentColor, bgAlpha * alpha, baseOrder + (button.useTexture() ? 0.05 : 0), { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0 + rotation, + pivotX, + pivotY } ); } @@ -99,61 +109,72 @@ export class UIButtonRenderSystem extends EntitySystem { if (render && render.borderWidth > 0 && render.borderAlpha > 0) { this.renderBorder( collector, - x, y, width, height, + renderX, renderY, width, height, render.borderWidth, render.borderColor, render.borderAlpha * alpha, baseOrder + 0.1, - transform.rotation + rotation, + pivotX, + pivotY ); } } } /** - * Render border using top-left coordinates - * 使用左上角坐标渲染边框 + * Render border using pivot-based coordinates + * 使用基于 pivot 的坐标渲染边框 */ private renderBorder( collector: ReturnType, - x: number, y: number, + centerX: number, centerY: number, width: number, height: number, borderWidth: number, borderColor: number, alpha: number, sortOrder: number, - rotation: number + rotation: number, + pivotX: number, + pivotY: number ): void { + // 计算矩形的边界(相对于 pivot 中心) + const left = centerX - width * pivotX; + const bottom = centerY - height * pivotY; + const right = left + width; + const top = bottom + height; + // Top border collector.addRect( - x, y, + (left + right) / 2, top - borderWidth / 2, width, borderWidth, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Bottom border collector.addRect( - x, y + height - borderWidth, + (left + right) / 2, bottom + borderWidth / 2, width, borderWidth, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Left border (excluding corners) + const sideBorderHeight = height - borderWidth * 2; collector.addRect( - x, y + borderWidth, - borderWidth, height - borderWidth * 2, + left + borderWidth / 2, (top + bottom) / 2, + borderWidth, sideBorderHeight, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Right border (excluding corners) collector.addRect( - x + width - borderWidth, y + borderWidth, - borderWidth, height - borderWidth * 2, + right - borderWidth / 2, (top + bottom) / 2, + borderWidth, sideBorderHeight, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } } diff --git a/packages/ui/src/systems/render/UIProgressBarRenderSystem.ts b/packages/ui/src/systems/render/UIProgressBarRenderSystem.ts index ab06c752..a8c56905 100644 --- a/packages/ui/src/systems/render/UIProgressBarRenderSystem.ts +++ b/packages/ui/src/systems/render/UIProgressBarRenderSystem.ts @@ -45,23 +45,33 @@ export class UIProgressBarRenderSystem extends EntitySystem { const x = transform.worldX ?? transform.x; const y = transform.worldY ?? transform.y; - const width = (transform.computedWidth ?? transform.width) * transform.scaleX; - const height = (transform.computedHeight ?? transform.height) * transform.scaleY; + // 使用世界缩放和旋转 + const scaleX = transform.worldScaleX ?? transform.scaleX; + const scaleY = transform.worldScaleY ?? transform.scaleY; + const rotation = transform.worldRotation ?? transform.rotation; + const width = (transform.computedWidth ?? transform.width) * scaleX; + const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; const baseOrder = 100 + transform.zIndex; + // 使用 transform 的 pivot 作为旋转/缩放中心 + const pivotX = transform.pivotX; + const pivotY = transform.pivotY; + // 渲染位置 = 左下角 + pivot 偏移 + const renderX = x + width * pivotX; + const renderY = y + height * pivotY; - // Render background (x, y is top-left corner) - // 渲染背景(x, y 是左上角) + // Render background + // 渲染背景 if (progressBar.backgroundAlpha > 0) { collector.addRect( - x, y, width, height, + renderX, renderY, width, height, progressBar.backgroundColor, progressBar.backgroundAlpha * alpha, baseOrder, { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0 + rotation, + pivotX, + pivotY } ); } @@ -70,12 +80,14 @@ export class UIProgressBarRenderSystem extends EntitySystem { // 渲染边框 if (progressBar.borderWidth > 0) { this.renderBorder( - collector, x, y, width, height, + collector, renderX, renderY, width, height, progressBar.borderWidth, progressBar.borderColor, alpha, baseOrder + 0.2, - transform + transform, + pivotX, + pivotY ); } @@ -85,13 +97,15 @@ export class UIProgressBarRenderSystem extends EntitySystem { if (progress > 0 && progressBar.fillAlpha > 0) { if (progressBar.showSegments) { this.renderSegmentedFill( - collector, x, y, width, height, - progress, progressBar, alpha, baseOrder + 0.1, transform + collector, renderX, renderY, width, height, + progress, progressBar, alpha, baseOrder + 0.1, transform, + pivotX, pivotY ); } else { this.renderSolidFill( - collector, x, y, width, height, - progress, progressBar, alpha, baseOrder + 0.1, transform + collector, renderX, renderY, width, height, + progress, progressBar, alpha, baseOrder + 0.1, transform, + pivotX, pivotY ); } } @@ -102,57 +116,67 @@ export class UIProgressBarRenderSystem extends EntitySystem { * Render solid fill rectangle * 渲染实心填充矩形 * - * Note: x, y is the top-left corner of the progress bar - * 注意:x, y 是进度条的左上角 + * Note: centerX, centerY is the pivot position of the progress bar + * 注意:centerX, centerY 是进度条的 pivot 位置 */ private renderSolidFill( collector: ReturnType, - x: number, y: number, width: number, height: number, + centerX: number, centerY: number, width: number, height: number, progress: number, progressBar: UIProgressBarComponent, alpha: number, sortOrder: number, - transform: UITransformComponent + transform: UITransformComponent, + pivotX: number, + pivotY: number ): void { - let fillX = x; - let fillY = y; + const rotation = transform.worldRotation ?? transform.rotation; + + // 计算进度条的边界(相对于 pivot 中心) + const left = centerX - width * pivotX; + const bottom = centerY - height * pivotY; + + let fillX: number; + let fillY: number; let fillWidth = width; let fillHeight = height; // Calculate fill dimensions based on direction - // x, y is top-left corner, so calculations are simpler // 根据方向计算填充尺寸 - // x, y 是左上角,所以计算更简单 switch (progressBar.direction) { case UIProgressDirection.LeftToRight: fillWidth = width * progress; - // Fill starts from left (fillX = x, no change) + fillX = left + fillWidth / 2; + fillY = bottom + height / 2; break; case UIProgressDirection.RightToLeft: fillWidth = width * progress; - // Fill starts from right - fillX = x + width - fillWidth; + fillX = left + width - fillWidth / 2; + fillY = bottom + height / 2; break; case UIProgressDirection.BottomToTop: fillHeight = height * progress; - // Fill starts from bottom - fillY = y + height - fillHeight; + fillX = left + width / 2; + fillY = bottom + fillHeight / 2; break; case UIProgressDirection.TopToBottom: fillHeight = height * progress; - // Fill starts from top (fillY = y, no change) + fillX = left + width / 2; + fillY = bottom + height - fillHeight / 2; break; + + default: + fillX = left + fillWidth / 2; + fillY = bottom + height / 2; } // Determine fill color (gradient or solid) // 确定填充颜色(渐变或实心) let fillColor = progressBar.fillColor; if (progressBar.useGradient) { - // Simple linear interpolation between start and end colors - // 简单的起始和结束颜色线性插值 fillColor = this.lerpColor( progressBar.gradientStartColor, progressBar.gradientEndColor, @@ -166,9 +190,9 @@ export class UIProgressBarRenderSystem extends EntitySystem { progressBar.fillAlpha * alpha, sortOrder, { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0 + rotation, + pivotX: 0.5, + pivotY: 0.5 } ); } @@ -177,18 +201,21 @@ export class UIProgressBarRenderSystem extends EntitySystem { * Render segmented fill * 渲染分段填充 * - * Note: x, y is the top-left corner of the progress bar - * 注意:x, y 是进度条的左上角 + * Note: centerX, centerY is the pivot position of the progress bar + * 注意:centerX, centerY 是进度条的 pivot 位置 */ private renderSegmentedFill( collector: ReturnType, - x: number, y: number, width: number, height: number, + centerX: number, centerY: number, width: number, height: number, progress: number, progressBar: UIProgressBarComponent, alpha: number, sortOrder: number, - transform: UITransformComponent + transform: UITransformComponent, + pivotX: number, + pivotY: number ): void { + const rotation = transform.worldRotation ?? transform.rotation; const segments = progressBar.segments; const gap = progressBar.segmentGap; const filledSegments = Math.ceil(progress * segments); @@ -196,6 +223,10 @@ export class UIProgressBarRenderSystem extends EntitySystem { const isHorizontal = progressBar.direction === UIProgressDirection.LeftToRight || progressBar.direction === UIProgressDirection.RightToLeft; + // 计算进度条的边界(相对于 pivot 中心) + const left = centerX - width * pivotX; + const bottom = centerY - height * pivotY; + // Calculate segment dimensions // 计算段尺寸 let segmentWidth: number; @@ -209,41 +240,36 @@ export class UIProgressBarRenderSystem extends EntitySystem { segmentHeight = (height - gap * (segments - 1)) / segments; } - // x, y is already top-left corner - // x, y 已经是左上角 - const baseX = x; - const baseY = y; - for (let i = 0; i < filledSegments && i < segments; i++) { - let segX: number; - let segY: number; + let segCenterX: number; + let segCenterY: number; - // Calculate segment position based on direction (using top-left positions) - // 根据方向计算段位置(使用左上角位置) + // Calculate segment center position based on direction + // 根据方向计算段中心位置 switch (progressBar.direction) { case UIProgressDirection.LeftToRight: - segX = baseX + i * (segmentWidth + gap); - segY = baseY; + segCenterX = left + i * (segmentWidth + gap) + segmentWidth / 2; + segCenterY = bottom + height / 2; break; case UIProgressDirection.RightToLeft: - segX = baseX + width - (i + 1) * segmentWidth - i * gap; - segY = baseY; + segCenterX = left + width - i * (segmentWidth + gap) - segmentWidth / 2; + segCenterY = bottom + height / 2; break; case UIProgressDirection.TopToBottom: - segX = baseX; - segY = baseY + i * (segmentHeight + gap); + segCenterX = left + width / 2; + segCenterY = bottom + height - i * (segmentHeight + gap) - segmentHeight / 2; break; case UIProgressDirection.BottomToTop: - segX = baseX; - segY = baseY + height - (i + 1) * segmentHeight - i * gap; + segCenterX = left + width / 2; + segCenterY = bottom + i * (segmentHeight + gap) + segmentHeight / 2; break; default: - segX = baseX + i * (segmentWidth + gap); - segY = baseY; + segCenterX = left + i * (segmentWidth + gap) + segmentWidth / 2; + segCenterY = bottom + height / 2; } // Determine segment color @@ -258,19 +284,19 @@ export class UIProgressBarRenderSystem extends EntitySystem { ); } - // Use top-left position with pivot 0,0 - // 使用左上角位置,pivot 0,0 + // Use center position with pivot 0.5, 0.5 + // 使用中心位置,pivot 0.5, 0.5 collector.addRect( - segX, segY, + segCenterX, segCenterY, segmentWidth, segmentHeight, segmentColor, progressBar.fillAlpha * alpha, - sortOrder + i * 0.001, // Slight offset for each segment + sortOrder + i * 0.001, { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0 + rotation, + pivotX: 0.5, + pivotY: 0.5 } ); } @@ -280,51 +306,59 @@ export class UIProgressBarRenderSystem extends EntitySystem { * Render border * 渲染边框 * - * Note: x, y is the top-left corner of the progress bar - * 注意:x, y 是进度条的左上角 + * Note: centerX, centerY is the pivot position of the progress bar + * 注意:centerX, centerY 是进度条的 pivot 位置 */ private renderBorder( collector: ReturnType, - x: number, y: number, width: number, height: number, + centerX: number, centerY: number, width: number, height: number, borderWidth: number, borderColor: number, alpha: number, sortOrder: number, - _transform: UITransformComponent + transform: UITransformComponent, + pivotX: number, + pivotY: number ): void { - // x, y is already top-left corner - // x, y 已经是左上角 + const rotation = transform.worldRotation ?? transform.rotation; + + // 计算边界(相对于 pivot 中心) + const left = centerX - width * pivotX; + const bottom = centerY - height * pivotY; + const right = left + width; + const top = bottom + height; // Top border collector.addRect( - x, y, + (left + right) / 2, top - borderWidth / 2, width, borderWidth, borderColor, alpha, sortOrder, - { pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Bottom border collector.addRect( - x, y + height - borderWidth, + (left + right) / 2, bottom + borderWidth / 2, width, borderWidth, borderColor, alpha, sortOrder, - { pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Left border (excluding corners) + const sideBorderHeight = height - borderWidth * 2; collector.addRect( - x, y + borderWidth, - borderWidth, height - borderWidth * 2, + left + borderWidth / 2, (top + bottom) / 2, + borderWidth, sideBorderHeight, borderColor, alpha, sortOrder, - { pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Right border (excluding corners) collector.addRect( - x + width - borderWidth, y + borderWidth, - borderWidth, height - borderWidth * 2, + right - borderWidth / 2, (top + bottom) / 2, + borderWidth, sideBorderHeight, borderColor, alpha, sortOrder, - { pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } diff --git a/packages/ui/src/systems/render/UIRectRenderSystem.ts b/packages/ui/src/systems/render/UIRectRenderSystem.ts index 334c8941..63bc8cee 100644 --- a/packages/ui/src/systems/render/UIRectRenderSystem.ts +++ b/packages/ui/src/systems/render/UIRectRenderSystem.ts @@ -56,33 +56,40 @@ export class UIRectRenderSystem extends EntitySystem { const x = transform.worldX ?? transform.x; const y = transform.worldY ?? transform.y; - const width = (transform.computedWidth ?? transform.width) * transform.scaleX; - const height = (transform.computedHeight ?? transform.height) * transform.scaleY; + // 使用世界缩放(考虑父级缩放) + const scaleX = transform.worldScaleX ?? transform.scaleX; + const scaleY = transform.worldScaleY ?? transform.scaleY; + const width = (transform.computedWidth ?? transform.width) * scaleX; + const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; + // 使用世界旋转(考虑父级旋转) + const rotation = transform.worldRotation ?? transform.rotation; const baseOrder = 100 + transform.zIndex; + // 使用 transform 的 pivot 作为旋转/缩放中心 + const pivotX = transform.pivotX; + const pivotY = transform.pivotY; - // Use top-left position with origin at (0, 0) - // Like Sprite: x,y is anchor position, origin determines where anchor is on the rect - // For UI: x,y is top-left corner, so origin should be (0, 0) - // 使用左上角位置,原点在 (0, 0) - // 类似 Sprite:x,y 是锚点位置,origin 决定锚点在矩形上的位置 - // 对于 UI:x,y 是左上角,所以 origin 应该是 (0, 0) + // worldX/worldY 是元素左下角位置,需要转换为以 pivot 为中心的位置 + // pivot 相对于元素的偏移:(width * pivotX, height * pivotY) + // 渲染位置 = 左下角 + pivot 偏移 + const renderX = x + width * pivotX; + const renderY = y + height * pivotY; // Render shadow if enabled // 如果启用,渲染阴影 if (render.shadowEnabled && render.shadowAlpha > 0) { collector.addRect( - x + render.shadowOffsetX - render.shadowBlur, - y + render.shadowOffsetY - render.shadowBlur, + renderX + render.shadowOffsetX, + renderY + render.shadowOffsetY, width + render.shadowBlur * 2, height + render.shadowBlur * 2, render.shadowColor, render.shadowAlpha * alpha, baseOrder - 0.1, { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0 + rotation, + pivotX, + pivotY } ); } @@ -94,15 +101,15 @@ export class UIRectRenderSystem extends EntitySystem { const textureId = typeof render.texture === 'number' ? render.texture : undefined; collector.addRect( - x, y, + renderX, renderY, width, height, render.textureTint, alpha, baseOrder, { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0, + rotation, + pivotX, + pivotY, textureId, texturePath, uv: render.textureUV @@ -115,15 +122,15 @@ export class UIRectRenderSystem extends EntitySystem { // 如果启用填充,渲染背景颜色 else if (render.fillBackground && render.backgroundAlpha > 0) { collector.addRect( - x, y, + renderX, renderY, width, height, render.backgroundColor, render.backgroundAlpha * alpha, baseOrder, { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0 + rotation, + pivotX, + pivotY } ); } @@ -133,61 +140,78 @@ export class UIRectRenderSystem extends EntitySystem { if (render.borderWidth > 0 && render.borderAlpha > 0) { this.renderBorder( collector, - x, y, width, height, + renderX, renderY, width, height, render.borderWidth, render.borderColor, render.borderAlpha * alpha, baseOrder + 0.1, - transform.rotation + rotation, + pivotX, + pivotY ); } } } /** - * Render border using top-left coordinates - * 使用左上角坐标渲染边框 + * Render border using pivot-based coordinates + * 使用基于 pivot 的坐标渲染边框 */ private renderBorder( collector: ReturnType, - x: number, y: number, + centerX: number, centerY: number, width: number, height: number, borderWidth: number, borderColor: number, alpha: number, sortOrder: number, - rotation: number + rotation: number, + pivotX: number, + pivotY: number ): void { - // Top border (from top-left corner) + // 计算矩形的左下角位置(相对于 pivot 中心) + const left = centerX - width * pivotX; + const bottom = centerY - height * pivotY; + const right = left + width; + const top = bottom + height; + + // Top border + const topBorderCenterX = (left + right) / 2; + const topBorderCenterY = top - borderWidth / 2; collector.addRect( - x, y, + topBorderCenterX, topBorderCenterY, width, borderWidth, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Bottom border + const bottomBorderCenterY = bottom + borderWidth / 2; collector.addRect( - x, y + height - borderWidth, + topBorderCenterX, bottomBorderCenterY, width, borderWidth, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Left border (excluding corners) + const sideBorderHeight = height - borderWidth * 2; + const leftBorderCenterX = left + borderWidth / 2; + const sideBorderCenterY = (top + bottom) / 2; collector.addRect( - x, y + borderWidth, - borderWidth, height - borderWidth * 2, + leftBorderCenterX, sideBorderCenterY, + borderWidth, sideBorderHeight, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Right border (excluding corners) + const rightBorderCenterX = right - borderWidth / 2; collector.addRect( - x + width - borderWidth, y + borderWidth, - borderWidth, height - borderWidth * 2, + rightBorderCenterX, sideBorderCenterY, + borderWidth, sideBorderHeight, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } } diff --git a/packages/ui/src/systems/render/UIScrollViewRenderSystem.ts b/packages/ui/src/systems/render/UIScrollViewRenderSystem.ts index 077f3e44..1a37f282 100644 --- a/packages/ui/src/systems/render/UIScrollViewRenderSystem.ts +++ b/packages/ui/src/systems/render/UIScrollViewRenderSystem.ts @@ -46,15 +46,24 @@ export class UIScrollViewRenderSystem extends EntitySystem { const x = transform.worldX ?? transform.x; const y = transform.worldY ?? transform.y; - const width = (transform.computedWidth ?? transform.width) * transform.scaleX; - const height = (transform.computedHeight ?? transform.height) * transform.scaleY; + // 使用世界缩放 + const scaleX = transform.worldScaleX ?? transform.scaleX; + const scaleY = transform.worldScaleY ?? transform.scaleY; + const rotation = transform.worldRotation ?? transform.rotation; + const width = (transform.computedWidth ?? transform.width) * scaleX; + const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; const baseOrder = 100 + transform.zIndex; + // 使用 transform 的 pivot 计算位置 + const pivotX = transform.pivotX; + const pivotY = transform.pivotY; + // 渲染位置 = 左下角 + pivot 偏移 + const renderX = x + width * pivotX; + const renderY = y + height * pivotY; - // x, y is already top-left corner - // x, y 已经是左上角 - const baseX = x; - const baseY = y; + // 计算边界 + const baseX = renderX - width * pivotX; + const baseY = renderY - height * pivotY; // Render vertical scrollbar // 渲染垂直滚动条 @@ -62,7 +71,7 @@ export class UIScrollViewRenderSystem extends EntitySystem { this.renderVerticalScrollbar( collector, baseX, baseY, width, height, - scrollView, alpha, baseOrder + scrollView, alpha, baseOrder, rotation ); } @@ -72,7 +81,7 @@ export class UIScrollViewRenderSystem extends EntitySystem { this.renderHorizontalScrollbar( collector, baseX, baseY, width, height, - scrollView, alpha, baseOrder + scrollView, alpha, baseOrder, rotation ); } } @@ -88,7 +97,8 @@ export class UIScrollViewRenderSystem extends EntitySystem { viewWidth: number, viewHeight: number, scrollView: UIScrollViewComponent, alpha: number, - baseOrder: number + baseOrder: number, + rotation: number ): void { const scrollbarWidth = scrollView.scrollbarWidth; const hasHorizontal = scrollView.needsHorizontalScrollbar(viewWidth); @@ -108,7 +118,7 @@ export class UIScrollViewRenderSystem extends EntitySystem { scrollView.scrollbarTrackColor, scrollView.scrollbarTrackAlpha * alpha, baseOrder + 0.5, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } @@ -131,7 +141,7 @@ export class UIScrollViewRenderSystem extends EntitySystem { scrollView.scrollbarColor, handleAlpha * alpha, baseOrder + 0.6, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } @@ -145,7 +155,8 @@ export class UIScrollViewRenderSystem extends EntitySystem { viewWidth: number, viewHeight: number, scrollView: UIScrollViewComponent, alpha: number, - baseOrder: number + baseOrder: number, + rotation: number ): void { const scrollbarWidth = scrollView.scrollbarWidth; const hasVertical = scrollView.needsVerticalScrollbar(viewHeight); @@ -165,7 +176,7 @@ export class UIScrollViewRenderSystem extends EntitySystem { scrollView.scrollbarTrackColor, scrollView.scrollbarTrackAlpha * alpha, baseOrder + 0.5, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } @@ -188,7 +199,7 @@ export class UIScrollViewRenderSystem extends EntitySystem { scrollView.scrollbarColor, handleAlpha * alpha, baseOrder + 0.6, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } } diff --git a/packages/ui/src/systems/render/UISliderRenderSystem.ts b/packages/ui/src/systems/render/UISliderRenderSystem.ts index df0be72a..9f1927f0 100644 --- a/packages/ui/src/systems/render/UISliderRenderSystem.ts +++ b/packages/ui/src/systems/render/UISliderRenderSystem.ts @@ -45,10 +45,20 @@ export class UISliderRenderSystem extends EntitySystem { const x = transform.worldX ?? transform.x; const y = transform.worldY ?? transform.y; - const width = (transform.computedWidth ?? transform.width) * transform.scaleX; - const height = (transform.computedHeight ?? transform.height) * transform.scaleY; + // 使用世界缩放 + const scaleX = transform.worldScaleX ?? transform.scaleX; + const scaleY = transform.worldScaleY ?? transform.scaleY; + const rotation = transform.worldRotation ?? transform.rotation; + const width = (transform.computedWidth ?? transform.width) * scaleX; + const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; const baseOrder = 100 + transform.zIndex; + // 使用 transform 的 pivot 计算中心位置 + const pivotX = transform.pivotX; + const pivotY = transform.pivotY; + // 渲染位置 = 左下角 + pivot 偏移 + const renderX = x + width * pivotX; + const renderY = y + height * pivotY; const isHorizontal = slider.orientation === UISliderOrientation.Horizontal; const progress = slider.getProgress(); @@ -58,10 +68,10 @@ export class UISliderRenderSystem extends EntitySystem { const trackLength = isHorizontal ? width : height; const trackThickness = slider.trackThickness; - // Calculate center position (x, y is top-left corner) - // 计算中心位置(x, y 是左上角) - const centerX = x + width / 2; - const centerY = y + height / 2; + // Calculate center position based on pivot + // 基于 pivot 计算中心位置 + const centerX = renderX; + const centerY = renderY; // Render track (using center position with pivot 0.5) // 渲染轨道(使用中心位置,pivot 0.5) @@ -73,7 +83,7 @@ export class UISliderRenderSystem extends EntitySystem { slider.trackColor, slider.trackAlpha * alpha, baseOrder, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } else { collector.addRect( @@ -82,7 +92,7 @@ export class UISliderRenderSystem extends EntitySystem { slider.trackColor, slider.trackAlpha * alpha, baseOrder, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } } @@ -101,7 +111,7 @@ export class UISliderRenderSystem extends EntitySystem { slider.fillColor, slider.fillAlpha * alpha, baseOrder + 0.1, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } else { // Fill from bottom @@ -112,7 +122,7 @@ export class UISliderRenderSystem extends EntitySystem { slider.fillColor, slider.fillAlpha * alpha, baseOrder + 0.1, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } } @@ -124,7 +134,7 @@ export class UISliderRenderSystem extends EntitySystem { collector, centerX, centerY, trackLength, trackThickness, slider, alpha, baseOrder + 0.05, - isHorizontal + isHorizontal, rotation ); } @@ -147,7 +157,7 @@ export class UISliderRenderSystem extends EntitySystem { 0x000000, 0.3 * alpha, baseOrder + 0.15, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } @@ -159,7 +169,7 @@ export class UISliderRenderSystem extends EntitySystem { handleColor, alpha, baseOrder + 0.2, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Handle border (if any) @@ -172,7 +182,8 @@ export class UISliderRenderSystem extends EntitySystem { slider.handleBorderWidth, slider.handleBorderColor, alpha, - baseOrder + 0.25 + baseOrder + 0.25, + rotation ); } } @@ -189,7 +200,8 @@ export class UISliderRenderSystem extends EntitySystem { slider: UISliderComponent, alpha: number, sortOrder: number, - isHorizontal: boolean + isHorizontal: boolean, + rotation: number ): void { const tickCount = slider.tickCount + 2; // Include start and end ticks const tickSize = slider.tickSize; @@ -220,7 +232,7 @@ export class UISliderRenderSystem extends EntitySystem { slider.tickColor, alpha, sortOrder, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } } @@ -236,7 +248,8 @@ export class UISliderRenderSystem extends EntitySystem { borderWidth: number, borderColor: number, alpha: number, - sortOrder: number + sortOrder: number, + rotation: number ): void { const halfW = width / 2; const halfH = height / 2; @@ -247,7 +260,7 @@ export class UISliderRenderSystem extends EntitySystem { x, y - halfH + halfB, width, borderWidth, borderColor, alpha, sortOrder, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Bottom @@ -255,7 +268,7 @@ export class UISliderRenderSystem extends EntitySystem { x, y + halfH - halfB, width, borderWidth, borderColor, alpha, sortOrder, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Left @@ -263,7 +276,7 @@ export class UISliderRenderSystem extends EntitySystem { x - halfW + halfB, y, borderWidth, height - borderWidth * 2, borderColor, alpha, sortOrder, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Right @@ -271,7 +284,7 @@ export class UISliderRenderSystem extends EntitySystem { x + halfW - halfB, y, borderWidth, height - borderWidth * 2, borderColor, alpha, sortOrder, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } } diff --git a/packages/ui/src/systems/render/UITextRenderSystem.ts b/packages/ui/src/systems/render/UITextRenderSystem.ts index 91e4ec3a..5c118902 100644 --- a/packages/ui/src/systems/render/UITextRenderSystem.ts +++ b/packages/ui/src/systems/render/UITextRenderSystem.ts @@ -101,10 +101,20 @@ export class UITextRenderSystem extends EntitySystem { const x = transform.worldX ?? transform.x; const y = transform.worldY ?? transform.y; - const width = (transform.computedWidth ?? transform.width) * transform.scaleX; - const height = (transform.computedHeight ?? transform.height) * transform.scaleY; + // 使用世界缩放和旋转 + const scaleX = transform.worldScaleX ?? transform.scaleX; + const scaleY = transform.worldScaleY ?? transform.scaleY; + const rotation = transform.worldRotation ?? transform.rotation; + const width = (transform.computedWidth ?? transform.width) * scaleX; + const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; const baseOrder = 100 + transform.zIndex; + // 使用 transform 的 pivot 作为旋转/缩放中心 + const pivotX = transform.pivotX; + const pivotY = transform.pivotY; + // 渲染位置 = 左下角 + pivot 偏移 + const renderX = x + width * pivotX; + const renderY = y + height * pivotY; // Generate or retrieve cached texture // 生成或获取缓存的纹理 @@ -114,18 +124,18 @@ export class UITextRenderSystem extends EntitySystem { if (textureId === null) continue; - // Use top-left position with origin at (0, 0) - // 使用左上角位置,原点在 (0, 0) + // Use pivot position with transform's pivot values + // 使用 transform 的 pivot 值作为旋转中心 collector.addRect( - x, y, + renderX, renderY, width, height, 0xFFFFFF, // White tint (color is baked into texture) alpha, baseOrder + 1, // Text renders above background { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0, + rotation, + pivotX, + pivotY, textureId } ); diff --git a/packages/ui/tsconfig.build.json b/packages/ui/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/ui/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index a2c2555e..e3f78665 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -25,7 +25,6 @@ "exclude": ["node_modules", "dist"], "references": [ { "path": "../core" }, - { "path": "../components" }, { "path": "../editor-core" } ] } diff --git a/packages/ui/tsup.config.ts b/packages/ui/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/ui/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts deleted file mode 100644 index 8fc3c196..00000000 --- a/packages/ui/vite.config.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; -import dts from 'vite-plugin-dts'; -import react from '@vitejs/plugin-react'; - -export default defineConfig({ - plugins: [ - react(), - dts({ - include: ['src'], - outDir: 'dist', - rollupTypes: false - }) - ], - esbuild: { - jsx: 'automatic', - }, - build: { - lib: { - entry: { - index: resolve(__dirname, 'src/index.ts'), - runtime: resolve(__dirname, 'src/runtime.ts'), - 'editor/index': resolve(__dirname, 'src/editor/index.ts') - }, - formats: ['es'], - fileName: (format, entryName) => `${entryName}.js` - }, - rollupOptions: { - external: [ - '@esengine/ecs-framework', - '@esengine/editor-core', - 'react', - 'react/jsx-runtime', - 'lucide-react', - /^@esengine\// - ], - output: { - exports: 'named', - preserveModules: false - } - }, - target: 'es2020', - minify: false, - sourcemap: true - } -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9088347c..bf0a7e10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,6 +126,9 @@ importers: ts-jest: specifier: ^29.4.0 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3) + turbo: + specifier: ^2.6.1 + version: 2.6.1 typedoc: specifier: ^0.28.13 version: 0.28.14(typescript@5.9.3) @@ -146,75 +149,68 @@ importers: version: 1.6.4(@algolia/client-search@5.44.0)(@types/node@20.19.25)(@types/react@18.3.27)(axios@1.13.2)(postcss@8.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(terser@5.44.1)(typescript@5.9.3) packages/asset-system: - dependencies: - '@esengine/ecs-framework': - specifier: ^2.0.0 - version: link:../core devDependencies: - '@rollup/plugin-commonjs': - specifier: ^28.0.3 - version: 28.0.9(rollup@4.53.3) - '@rollup/plugin-node-resolve': - specifier: ^16.0.1 - version: 16.0.3(rollup@4.53.3) - '@rollup/plugin-typescript': - specifier: ^11.1.6 - version: 11.1.6(rollup@4.53.3)(tslib@2.8.1)(typescript@5.9.3) + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core rimraf: specifier: ^5.0.0 version: 5.0.10 - rollup: - specifier: ^4.42.0 - version: 4.53.3 - rollup-plugin-dts: - specifier: ^6.2.1 - version: 6.2.3(rollup@4.53.3)(typescript@5.9.3) + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.8.3 version: 5.9.3 + packages/audio: + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + packages/behavior-tree: dependencies: - '@esengine/asset-system': - specifier: workspace:* - version: link:../asset-system - '@esengine/ecs-components': - specifier: workspace:* - version: link:../components - '@esengine/ecs-framework': - specifier: '>=2.0.0' - version: link:../core - '@esengine/editor-runtime': - specifier: workspace:* - version: link:../editor-runtime - lucide-react: - specifier: ^0.545.0 - version: 0.545.0(react@18.3.1) - react: - specifier: ^18.3.1 - version: 18.3.1 tslib: specifier: ^2.8.1 version: 2.8.1 - zustand: - specifier: ^4.5.2 - version: 4.5.7(@types/react@18.3.27)(react@18.3.1) devDependencies: - '@tauri-apps/plugin-fs': - specifier: ^2.4.2 - version: 2.4.4 + '@esengine/asset-system': + specifier: workspace:* + version: link:../asset-system + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core '@types/jest': specifier: ^29.5.14 version: 29.5.14 '@types/node': specifier: ^20.19.17 version: 20.19.25 - '@types/react': - specifier: ^18.3.12 - version: 18.3.27 - '@vitejs/plugin-react': - specifier: ^4.7.0 - version: 4.7.0(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.19.25) @@ -224,76 +220,197 @@ importers: ts-jest: specifier: ^29.4.0 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3) + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.8.3 version: 5.9.3 - vite: - specifier: ^6.0.7 - version: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) - vite-plugin-dts: - specifier: ^3.7.0 - version: 3.9.1(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) - packages/blueprint: + packages/behavior-tree-editor: dependencies: + '@esengine/behavior-tree': + specifier: workspace:* + version: link:../behavior-tree + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config '@esengine/ecs-framework': - specifier: '>=2.0.0' + specifier: workspace:* version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core '@esengine/editor-runtime': specifier: workspace:* version: link:../editor-runtime + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core '@esengine/node-editor': specifier: workspace:* version: link:../node-editor - react: - specifier: ^18.3.1 - version: 18.3.1 - tslib: - specifier: ^2.8.1 - version: 2.8.1 - zustand: - specifier: ^4.5.2 - version: 4.5.7(@types/react@18.3.27)(react@18.3.1) - devDependencies: - '@types/node': - specifier: ^20.19.17 - version: 20.19.25 '@types/react': specifier: ^18.3.12 version: 18.3.27 - '@vitejs/plugin-react': - specifier: ^4.7.0 - version: 4.7.0(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + lucide-react: + specifier: ^0.545.0 + version: 0.545.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + zustand: + specifier: ^5.0.8 + version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) + + packages/blueprint: + dependencies: + tslib: + specifier: ^2.8.1 + version: 2.8.1 + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@types/node': + specifier: ^20.19.17 + version: 20.19.25 rimraf: specifier: ^5.0.0 version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + + packages/blueprint-editor: + dependencies: + '@esengine/blueprint': + specifier: workspace:* + version: link:../blueprint + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + '@esengine/node-editor': + specifier: workspace:* + version: link:../node-editor + '@types/react': + specifier: ^18.3.12 + version: 18.3.27 + lucide-react: + specifier: ^0.545.0 + version: 0.545.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + zustand: + specifier: ^5.0.8 + version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) + + packages/build-config: + devDependencies: + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.8.3 version: 5.9.3 vite: - specifier: ^6.0.7 + specifier: ^6.3.5 version: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) vite-plugin-dts: - specifier: ^3.7.0 - version: 3.9.1(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + specifier: ^4.5.4 + version: 4.5.4(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) - packages/components: - dependencies: - '@esengine/asset-system': - specifier: workspace:* - version: link:../asset-system - '@esengine/ecs-framework': - specifier: ^2.2.8 - version: link:../core - tslib: - specifier: ^2.8.1 - version: 2.8.1 + packages/camera: devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core rimraf: - specifier: ^5.0.0 + specifier: ^5.0.5 version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: - specifier: ^5.8.3 + specifier: ^5.3.3 + version: 5.9.3 + + packages/camera-editor: + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/camera': + specifier: workspace:* + version: link:../camera + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + '@types/react': + specifier: ^18.2.0 + version: 18.3.27 + react: + specifier: ^18.3.1 + version: 18.3.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 version: 5.9.3 packages/core: @@ -367,20 +484,28 @@ importers: version: 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) packages/ecs-engine-bindgen: - dependencies: + devDependencies: '@esengine/asset-system': specifier: workspace:* version: link:../asset-system - '@esengine/ecs-components': + '@esengine/camera': specifier: workspace:* - version: link:../components + version: link:../camera '@esengine/ecs-framework': specifier: workspace:* version: link:../core - devDependencies: + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + '@esengine/sprite': + specifier: workspace:* + version: link:../sprite rimraf: specifier: ^5.0.0 version: 5.0.10 + tsup: + specifier: ^8.5.1 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.8.0 version: 5.9.3 @@ -394,15 +519,24 @@ importers: '@esengine/asset-system': specifier: workspace:* version: link:../asset-system + '@esengine/audio': + specifier: workspace:* + version: link:../audio '@esengine/behavior-tree': specifier: workspace:* version: link:../behavior-tree + '@esengine/behavior-tree-editor': + specifier: workspace:* + version: link:../behavior-tree-editor '@esengine/blueprint': specifier: workspace:* version: link:../blueprint - '@esengine/ecs-components': + '@esengine/blueprint-editor': specifier: workspace:* - version: link:../components + version: link:../blueprint-editor + '@esengine/camera': + specifier: workspace:* + version: link:../camera '@esengine/ecs-engine-bindgen': specifier: workspace:* version: link:../ecs-engine-bindgen @@ -418,15 +552,33 @@ importers: '@esengine/engine': specifier: workspace:* version: link:../engine + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core '@esengine/physics-rapier2d': specifier: workspace:* version: link:../physics-rapier2d + '@esengine/physics-rapier2d-editor': + specifier: workspace:* + version: link:../physics-rapier2d-editor + '@esengine/runtime-core': + specifier: workspace:* + version: link:../runtime-core + '@esengine/sprite': + specifier: workspace:* + version: link:../sprite '@esengine/tilemap': specifier: workspace:* version: link:../tilemap + '@esengine/tilemap-editor': + specifier: workspace:* + version: link:../tilemap-editor '@esengine/ui': specifier: workspace:* version: link:../ui + '@esengine/ui-editor': + specifier: workspace:* + version: link:../ui-editor '@monaco-editor/react': specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -536,24 +688,9 @@ importers: packages/editor-core: dependencies: - '@esengine/asset-system': - specifier: '*' - version: link:../asset-system - '@esengine/ecs-framework': - specifier: ^2.2.8 - version: link:../core - react: - specifier: ^18.2.0 - version: 18.3.1 - rxjs: - specifier: ^7.8.0 - version: 7.8.2 tslib: specifier: ^2.8.1 version: 2.8.1 - tsyringe: - specifier: ^4.8.0 - version: 4.10.0 devDependencies: '@babel/core': specifier: ^7.28.3 @@ -567,6 +704,15 @@ importers: '@babel/preset-env': specifier: ^7.28.3 version: 7.28.5(@babel/core@7.28.5) + '@esengine/asset-system': + specifier: workspace:* + version: link:../asset-system + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core '@eslint/js': specifier: ^9.37.0 version: 9.39.1 @@ -597,6 +743,9 @@ importers: jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.19.25) + react: + specifier: ^18.2.0 + version: 18.3.1 rimraf: specifier: ^5.0.0 version: 5.0.10 @@ -606,9 +755,15 @@ importers: rollup-plugin-dts: specifier: ^6.2.1 version: 6.2.3(rollup@4.53.3)(typescript@5.9.3) + rxjs: + specifier: ^7.8.0 + version: 7.8.2 ts-jest: specifier: ^29.4.0 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3) + tsyringe: + specifier: ^4.8.0 + version: 4.10.0 typescript: specifier: ^5.8.3 version: 5.9.3 @@ -618,15 +773,6 @@ importers: packages/editor-runtime: dependencies: - '@esengine/ecs-framework': - specifier: workspace:* - version: link:../core - '@esengine/editor-core': - specifier: workspace:* - version: link:../editor-core - '@esengine/ui': - specifier: workspace:* - version: link:../ui '@tauri-apps/api': specifier: ^2.2.0 version: 2.9.0 @@ -636,6 +782,28 @@ importers: '@tauri-apps/plugin-fs': specifier: ^2.4.2 version: 2.4.4 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + tsyringe: + specifier: ^4.10.0 + version: 4.10.0 + devDependencies: + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core + '@esengine/ui': + specifier: workspace:* + version: link:../ui + '@types/react': + specifier: ^18.3.12 + version: 18.3.27 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.7(@types/react@18.3.27) lucide-react: specifier: ^0.545.0 version: 0.545.0(react@18.3.1) @@ -645,22 +813,6 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) - reflect-metadata: - specifier: ^0.2.2 - version: 0.2.2 - tsyringe: - specifier: ^4.10.0 - version: 4.10.0 - zustand: - specifier: ^5.0.8 - version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) - devDependencies: - '@types/react': - specifier: ^18.3.12 - version: 18.3.27 - '@types/react-dom': - specifier: ^18.3.1 - version: 18.3.7(@types/react@18.3.27) rimraf: specifier: ^5.0.0 version: 5.0.10 @@ -673,6 +825,9 @@ importers: vite-plugin-dts: specifier: ^4.5.0 version: 4.5.4(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + zustand: + specifier: ^5.0.8 + version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) packages/engine: devDependencies: @@ -680,6 +835,24 @@ importers: specifier: ^5.0.0 version: 5.0.10 + packages/engine-core: + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + packages/math: devDependencies: '@rollup/plugin-commonjs': @@ -721,15 +894,9 @@ importers: packages/node-editor: dependencies: - react: - specifier: ^18.3.1 - version: 18.3.1 tslib: specifier: ^2.8.1 version: 2.8.1 - zustand: - specifier: ^4.5.2 - version: 4.5.7(@types/react@18.3.27)(react@18.3.1) devDependencies: '@types/node': specifier: ^20.19.17 @@ -740,6 +907,9 @@ importers: '@vitejs/plugin-react': specifier: ^4.7.0 version: 4.7.0(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + react: + specifier: ^18.3.1 + version: 18.3.1 rimraf: specifier: ^5.0.0 version: 5.0.10 @@ -752,43 +922,74 @@ importers: vite-plugin-dts: specifier: ^3.7.0 version: 3.9.1(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + zustand: + specifier: ^5.0.8 + version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) packages/physics-rapier2d: dependencies: '@dimforge/rapier2d-compat': specifier: ^0.14.0 version: 0.14.0 - '@esengine/ecs-components': + devDependencies: + '@esengine/build-config': specifier: workspace:* - version: link:../components + version: link:../build-config '@esengine/ecs-framework': - specifier: '>=2.0.0' + specifier: workspace:* version: link:../core '@esengine/editor-core': specifier: workspace:* version: link:../editor-core - react: - specifier: ^18.3.1 - version: 18.3.1 - devDependencies: - '@types/react': - specifier: ^18.3.12 - version: 18.3.27 - '@vitejs/plugin-react': - specifier: ^4.7.0 - version: 4.7.0(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core rimraf: specifier: ^5.0.0 version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.8.3 version: 5.9.3 - vite: - specifier: ^6.0.7 - version: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) - vite-plugin-dts: - specifier: ^4.5.0 - version: 4.5.4(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + + packages/physics-rapier2d-editor: + dependencies: + '@esengine/physics-rapier2d': + specifier: workspace:* + version: link:../physics-rapier2d + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + '@types/react': + specifier: ^18.3.12 + version: 18.3.27 + lucide-react: + specifier: ^0.545.0 + version: 0.545.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 packages/platform-common: devDependencies: @@ -815,35 +1016,46 @@ importers: version: 5.9.3 packages/platform-web: - dependencies: + devDependencies: '@esengine/asset-system': specifier: workspace:* version: link:../asset-system + '@esengine/audio': + specifier: workspace:* + version: link:../audio '@esengine/behavior-tree': specifier: workspace:* version: link:../behavior-tree - '@esengine/ecs-components': + '@esengine/camera': specifier: workspace:* - version: link:../components + version: link:../camera '@esengine/ecs-engine-bindgen': specifier: workspace:* version: link:../ecs-engine-bindgen '@esengine/ecs-framework': specifier: workspace:* version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core '@esengine/physics-rapier2d': specifier: workspace:* version: link:../physics-rapier2d '@esengine/platform-common': specifier: workspace:* version: link:../platform-common + '@esengine/runtime-core': + specifier: workspace:* + version: link:../runtime-core + '@esengine/sprite': + specifier: workspace:* + version: link:../sprite '@esengine/tilemap': specifier: workspace:* version: link:../tilemap '@esengine/ui': specifier: workspace:* version: link:../ui - devDependencies: '@rollup/plugin-alias': specifier: ^6.0.0 version: 6.0.0(rollup@4.53.3) @@ -906,91 +1118,215 @@ importers: specifier: ^5.8.3 version: 5.9.3 - packages/tilemap: - dependencies: + packages/runtime-core: + devDependencies: '@esengine/asset-system': specifier: workspace:* version: link:../asset-system - '@esengine/ecs-components': + '@esengine/build-config': specifier: workspace:* - version: link:../components + version: link:../build-config '@esengine/ecs-engine-bindgen': specifier: workspace:* version: link:../ecs-engine-bindgen '@esengine/ecs-framework': - specifier: ^2.2.8 + specifier: workspace:* version: link:../core - '@esengine/editor-core': + '@esengine/engine-core': specifier: workspace:* - version: link:../editor-core - lucide-react: - specifier: ^0.545.0 - version: 0.545.0(react@18.3.1) - react: - specifier: ^18.3.1 - version: 18.3.1 - tslib: - specifier: ^2.8.1 - version: 2.8.1 - zustand: - specifier: ^5.0.8 - version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) - devDependencies: - '@types/react': - specifier: ^18.3.12 - version: 18.3.27 - '@vitejs/plugin-react': - specifier: ^4.7.0 - version: 4.7.0(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) - rimraf: - specifier: ^5.0.0 - version: 5.0.10 - typescript: - specifier: ^5.8.3 - version: 5.9.3 - vite: - specifier: ^6.0.7 - version: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) - vite-plugin-dts: - specifier: ^3.7.0 - version: 3.9.1(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) - - packages/ui: - dependencies: - '@esengine/ecs-components': - specifier: workspace:* - version: link:../components - '@esengine/ecs-framework': - specifier: '>=2.0.0' - version: link:../core - '@esengine/editor-core': - specifier: workspace:* - version: link:../editor-core - lucide-react: - specifier: ^0.545.0 - version: 0.545.0(react@18.3.1) - react: - specifier: ^18.3.1 - version: 18.3.1 - devDependencies: - '@types/react': - specifier: ^18.3.12 - version: 18.3.27 - '@vitejs/plugin-react': - specifier: ^4.7.0 - version: 4.7.0(vite@5.4.21(@types/node@20.19.25)(terser@5.44.1)) + version: link:../engine-core rimraf: specifier: ^5.0.5 version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.3.3 version: 5.9.3 - vite: + + packages/sprite: + devDependencies: + '@esengine/asset-system': + specifier: workspace:* + version: link:../asset-system + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + packages/sprite-editor: + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + '@esengine/sprite': + specifier: workspace:* + version: link:../sprite + '@types/react': + specifier: ^18.2.0 + version: 18.3.27 + react: + specifier: ^18.3.1 + version: 18.3.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + packages/tilemap: + dependencies: + tslib: + specifier: ^2.8.1 + version: 2.8.1 + devDependencies: + '@esengine/asset-system': + specifier: workspace:* + version: link:../asset-system + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-engine-bindgen': + specifier: workspace:* + version: link:../ecs-engine-bindgen + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + rimraf: specifier: ^5.0.0 - version: 5.4.21(@types/node@20.19.25)(terser@5.44.1) - vite-plugin-dts: - specifier: ^3.7.0 - version: 3.9.1(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@5.4.21(@types/node@20.19.25)(terser@5.44.1)) + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + + packages/tilemap-editor: + dependencies: + '@esengine/tilemap': + specifier: workspace:* + version: link:../tilemap + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + '@types/react': + specifier: ^18.3.12 + version: 18.3.27 + lucide-react: + specifier: ^0.545.0 + version: 0.545.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + zustand: + specifier: ^5.0.8 + version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) + + packages/ui: + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + packages/ui-editor: + dependencies: + '@esengine/ui': + specifier: workspace:* + version: link:../ui + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core + '@types/react': + specifier: ^18.3.12 + version: 18.3.27 + lucide-react: + specifier: ^0.545.0 + version: 0.545.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 packages: @@ -1791,6 +2127,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.0': + resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} @@ -1803,6 +2145,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.0': + resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} @@ -1815,6 +2163,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.0': + resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} @@ -1827,6 +2181,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.0': + resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} @@ -1839,6 +2199,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.0': + resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} @@ -1851,6 +2217,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.0': + resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} @@ -1863,6 +2235,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.0': + resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} @@ -1875,6 +2253,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.0': + resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} @@ -1887,6 +2271,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.0': + resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} @@ -1899,6 +2289,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.0': + resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} @@ -1911,6 +2307,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.0': + resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} @@ -1923,6 +2325,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.0': + resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} @@ -1935,6 +2343,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.0': + resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} @@ -1947,6 +2361,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.0': + resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} @@ -1959,6 +2379,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.0': + resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} @@ -1971,6 +2397,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.0': + resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} @@ -1983,12 +2415,24 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.0': + resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.0': + resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} @@ -2001,12 +2445,24 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.0': + resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.0': + resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} @@ -2019,12 +2475,24 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.0': + resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.0': + resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} @@ -2037,6 +2505,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.0': + resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} @@ -2049,6 +2523,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.0': + resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} @@ -2061,6 +2541,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.0': + resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -2073,6 +2559,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.0': + resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4002,6 +4494,12 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + byte-size@8.1.1: resolution: {integrity: sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==} engines: {node: '>=12.17'} @@ -4010,6 +4508,10 @@ packages: resolution: {integrity: sha512-fey6+4jDK7TFtFg/klGSvNKJctyU7n2aQdnM+CO0ruLPbqqMOM8Tio0Pc+deqUeVKX1tL5DQep1zQ7+37aTAsA==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + cacache@18.0.4: resolution: {integrity: sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==} engines: {node: ^16.14.0 || >=18.0.0} @@ -4193,6 +4695,10 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + commander@9.5.0: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} @@ -4228,6 +4734,10 @@ packages: config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} @@ -4598,6 +5108,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.0: + resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -4801,6 +5316,9 @@ packages: resolution: {integrity: sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==} engines: {node: '>=18'} + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -5597,6 +6115,10 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -5745,6 +6267,10 @@ packages: resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==} engines: {node: '>=8'} + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + local-pkg@1.1.2: resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} engines: {node: '>=14'} @@ -6735,6 +7261,24 @@ packages: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + postcss-selector-parser@6.1.2: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} @@ -7262,6 +7806,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -7385,6 +7933,11 @@ packages: style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + super-regex@1.1.0: resolution: {integrity: sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==} engines: {node: '>=18'} @@ -7484,6 +8037,9 @@ packages: resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==} engines: {node: '>=12'} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -7526,6 +8082,10 @@ packages: resolution: {integrity: sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==} engines: {node: '>= 0.4'} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + treeverse@3.0.0: resolution: {integrity: sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -7546,6 +8106,9 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-jest@29.4.5: resolution: {integrity: sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -7583,6 +8146,25 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + tsyringe@4.10.0: resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} engines: {node: '>= 6.0.0'} @@ -7591,6 +8173,40 @@ packages: resolution: {integrity: sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==} engines: {node: ^16.14.0 || >=18.0.0} + turbo-darwin-64@2.6.1: + resolution: {integrity: sha512-Dm0HwhyZF4J0uLqkhUyCVJvKM9Rw7M03v3J9A7drHDQW0qAbIGBrUijQ8g4Q9Cciw/BXRRd8Uzkc3oue+qn+ZQ==} + cpu: [x64] + os: [darwin] + + turbo-darwin-arm64@2.6.1: + resolution: {integrity: sha512-U0PIPTPyxdLsrC3jN7jaJUwgzX5sVUBsKLO7+6AL+OASaa1NbT1pPdiZoTkblBAALLP76FM0LlnsVQOnmjYhyw==} + cpu: [arm64] + os: [darwin] + + turbo-linux-64@2.6.1: + resolution: {integrity: sha512-eM1uLWgzv89bxlK29qwQEr9xYWBhmO/EGiH22UGfq+uXr+QW1OvNKKMogSN65Ry8lElMH4LZh0aX2DEc7eC0Mw==} + cpu: [x64] + os: [linux] + + turbo-linux-arm64@2.6.1: + resolution: {integrity: sha512-MFFh7AxAQAycXKuZDrbeutfWM5Ep0CEZ9u7zs4Hn2FvOViTCzIfEhmuJou3/a5+q5VX1zTxQrKGy+4Lf5cdpsA==} + cpu: [arm64] + os: [linux] + + turbo-windows-64@2.6.1: + resolution: {integrity: sha512-buq7/VAN7KOjMYi4tSZT5m+jpqyhbRU2EUTTvp6V0Ii8dAkY2tAAjQN1q5q2ByflYWKecbQNTqxmVploE0LVwQ==} + cpu: [x64] + os: [win32] + + turbo-windows-arm64@2.6.1: + resolution: {integrity: sha512-7w+AD5vJp3R+FB0YOj1YJcNcOOvBior7bcHTodqp90S3x3bLgpr7tE6xOea1e8JkP7GK6ciKVUpQvV7psiwU5Q==} + cpu: [arm64] + os: [win32] + + turbo@2.6.1: + resolution: {integrity: sha512-qBwXXuDT3rA53kbNafGbT5r++BrhRgx3sAo0cHoDAeG9g1ItTmUMgltz3Hy7Hazy1ODqNpR+C7QwqL6DYB52yA==} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -8171,21 +8787,6 @@ packages: engines: {node: '>=8.0.0'} hasBin: true - zustand@4.5.7: - resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} - engines: {node: '>=12.7.0'} - peerDependencies: - '@types/react': '>=16.8' - immer: '>=9.0.6' - react: '>=16.8' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - zustand@5.0.8: resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} engines: {node: '>=12.20.0'} @@ -9238,147 +9839,225 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true + '@esbuild/aix-ppc64@0.27.0': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true '@esbuild/android-arm64@0.25.12': optional: true + '@esbuild/android-arm64@0.27.0': + optional: true + '@esbuild/android-arm@0.21.5': optional: true '@esbuild/android-arm@0.25.12': optional: true + '@esbuild/android-arm@0.27.0': + optional: true + '@esbuild/android-x64@0.21.5': optional: true '@esbuild/android-x64@0.25.12': optional: true + '@esbuild/android-x64@0.27.0': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true '@esbuild/darwin-arm64@0.25.12': optional: true + '@esbuild/darwin-arm64@0.27.0': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true '@esbuild/darwin-x64@0.25.12': optional: true + '@esbuild/darwin-x64@0.27.0': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true '@esbuild/freebsd-arm64@0.25.12': optional: true + '@esbuild/freebsd-arm64@0.27.0': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true '@esbuild/freebsd-x64@0.25.12': optional: true + '@esbuild/freebsd-x64@0.27.0': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true '@esbuild/linux-arm64@0.25.12': optional: true + '@esbuild/linux-arm64@0.27.0': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true '@esbuild/linux-arm@0.25.12': optional: true + '@esbuild/linux-arm@0.27.0': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true '@esbuild/linux-ia32@0.25.12': optional: true + '@esbuild/linux-ia32@0.27.0': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true '@esbuild/linux-loong64@0.25.12': optional: true + '@esbuild/linux-loong64@0.27.0': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true '@esbuild/linux-mips64el@0.25.12': optional: true + '@esbuild/linux-mips64el@0.27.0': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true '@esbuild/linux-ppc64@0.25.12': optional: true + '@esbuild/linux-ppc64@0.27.0': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true '@esbuild/linux-riscv64@0.25.12': optional: true + '@esbuild/linux-riscv64@0.27.0': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true '@esbuild/linux-s390x@0.25.12': optional: true + '@esbuild/linux-s390x@0.27.0': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true '@esbuild/linux-x64@0.25.12': optional: true + '@esbuild/linux-x64@0.27.0': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true + '@esbuild/netbsd-arm64@0.27.0': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true '@esbuild/netbsd-x64@0.25.12': optional: true + '@esbuild/netbsd-x64@0.27.0': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true + '@esbuild/openbsd-arm64@0.27.0': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true '@esbuild/openbsd-x64@0.25.12': optional: true + '@esbuild/openbsd-x64@0.27.0': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true + '@esbuild/openharmony-arm64@0.27.0': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true '@esbuild/sunos-x64@0.25.12': optional: true + '@esbuild/sunos-x64@0.27.0': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true '@esbuild/win32-arm64@0.25.12': optional: true + '@esbuild/win32-arm64@0.27.0': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true '@esbuild/win32-ia32@0.25.12': optional: true + '@esbuild/win32-ia32@0.27.0': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true '@esbuild/win32-x64@0.25.12': optional: true + '@esbuild/win32-x64@0.27.0': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))': dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -11243,18 +11922,6 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@20.19.25)(terser@5.44.1))': - dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) - '@rolldown/pluginutils': 1.0.0-beta.27 - '@types/babel__core': 7.20.5 - react-refresh: 0.17.0 - vite: 5.4.21(@types/node@20.19.25)(terser@5.44.1) - transitivePeerDependencies: - - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.5 @@ -11765,10 +12432,17 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bundle-require@5.1.0(esbuild@0.27.0): + dependencies: + esbuild: 0.27.0 + load-tsconfig: 0.2.5 + byte-size@8.1.1: {} bytes-iec@3.1.1: {} + cac@6.7.14: {} + cacache@18.0.4: dependencies: '@npmcli/fs': 3.1.1 @@ -11941,6 +12615,8 @@ snapshots: commander@2.20.3: {} + commander@4.1.1: {} + commander@9.5.0: optional: true @@ -11975,6 +12651,8 @@ snapshots: ini: 1.3.8 proto-list: 1.2.4 + consola@3.4.2: {} + console-control-strings@1.1.0: {} conventional-changelog-angular@7.0.0: @@ -12364,6 +13042,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.0 + '@esbuild/android-arm': 0.27.0 + '@esbuild/android-arm64': 0.27.0 + '@esbuild/android-x64': 0.27.0 + '@esbuild/darwin-arm64': 0.27.0 + '@esbuild/darwin-x64': 0.27.0 + '@esbuild/freebsd-arm64': 0.27.0 + '@esbuild/freebsd-x64': 0.27.0 + '@esbuild/linux-arm': 0.27.0 + '@esbuild/linux-arm64': 0.27.0 + '@esbuild/linux-ia32': 0.27.0 + '@esbuild/linux-loong64': 0.27.0 + '@esbuild/linux-mips64el': 0.27.0 + '@esbuild/linux-ppc64': 0.27.0 + '@esbuild/linux-riscv64': 0.27.0 + '@esbuild/linux-s390x': 0.27.0 + '@esbuild/linux-x64': 0.27.0 + '@esbuild/netbsd-arm64': 0.27.0 + '@esbuild/netbsd-x64': 0.27.0 + '@esbuild/openbsd-arm64': 0.27.0 + '@esbuild/openbsd-x64': 0.27.0 + '@esbuild/openharmony-arm64': 0.27.0 + '@esbuild/sunos-x64': 0.27.0 + '@esbuild/win32-arm64': 0.27.0 + '@esbuild/win32-ia32': 0.27.0 + '@esbuild/win32-x64': 0.27.0 + escalade@3.2.0: {} escape-string-regexp@1.0.5: {} @@ -12612,6 +13319,12 @@ snapshots: semver-regex: 4.0.5 super-regex: 1.1.0 + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.0 + rollup: 4.53.3 + flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -13664,6 +14377,8 @@ snapshots: jju@1.4.0: {} + joycon@3.1.1: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -13921,6 +14636,8 @@ snapshots: strip-bom: 4.0.0 type-fest: 0.6.0 + load-tsconfig@0.2.5: {} + local-pkg@1.1.2: dependencies: mlly: 1.8.0 @@ -15091,6 +15808,14 @@ snapshots: dependencies: find-up: 3.0.0 + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(yaml@2.8.1): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 2.6.1 + postcss: 8.5.6 + yaml: 2.8.1 + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 @@ -15737,6 +16462,8 @@ snapshots: source-map@0.6.1: {} + source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} spawn-error-forwarder@1.0.0: {} @@ -15854,6 +16581,16 @@ snapshots: dependencies: inline-style-parser: 0.2.7 + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + super-regex@1.1.0: dependencies: function-timeout: 1.0.2 @@ -15965,6 +16702,8 @@ snapshots: dependencies: convert-hrtime: 5.0.0 + tinyexec@0.3.2: {} + tinyexec@1.0.2: {} tinyglobby@0.2.12: @@ -16004,6 +16743,8 @@ snapshots: traverse@0.6.8: {} + tree-kill@1.2.2: {} + treeverse@3.0.0: {} trim-lines@3.0.1: {} @@ -16016,6 +16757,8 @@ snapshots: dependencies: typescript: 5.9.3 + ts-interface-checker@0.1.13: {} + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 @@ -16046,6 +16789,36 @@ snapshots: tslib@2.8.1: {} + tsup@8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.0) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.0 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(yaml@2.8.1) + resolve-from: 5.0.0 + rollup: 4.53.3 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + '@microsoft/api-extractor': 7.55.1(@types/node@20.19.25) + '@swc/core': 1.15.3 + postcss: 8.5.6 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsyringe@4.10.0: dependencies: tslib: 1.14.1 @@ -16058,6 +16831,33 @@ snapshots: transitivePeerDependencies: - supports-color + turbo-darwin-64@2.6.1: + optional: true + + turbo-darwin-arm64@2.6.1: + optional: true + + turbo-linux-64@2.6.1: + optional: true + + turbo-linux-arm64@2.6.1: + optional: true + + turbo-windows-64@2.6.1: + optional: true + + turbo-windows-arm64@2.6.1: + optional: true + + turbo@2.6.1: + optionalDependencies: + turbo-darwin-64: 2.6.1 + turbo-darwin-arm64: 2.6.1 + turbo-linux-64: 2.6.1 + turbo-linux-arm64: 2.6.1 + turbo-windows-64: 2.6.1 + turbo-windows-arm64: 2.6.1 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -16276,23 +17076,6 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-dts@3.9.1(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@5.4.21(@types/node@20.19.25)(terser@5.44.1)): - dependencies: - '@microsoft/api-extractor': 7.43.0(@types/node@20.19.25) - '@rollup/pluginutils': 5.3.0(rollup@4.53.3) - '@vue/language-core': 1.8.27(typescript@5.9.3) - debug: 4.4.3 - kolorist: 1.8.0 - magic-string: 0.30.21 - typescript: 5.9.3 - vue-tsc: 1.8.27(typescript@5.9.3) - optionalDependencies: - vite: 5.4.21(@types/node@20.19.25)(terser@5.44.1) - transitivePeerDependencies: - - '@types/node' - - rollup - - supports-color - vite-plugin-dts@3.9.1(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)): dependencies: '@microsoft/api-extractor': 7.43.0(@types/node@20.19.25) @@ -16635,13 +17418,6 @@ snapshots: optionalDependencies: commander: 9.5.0 - zustand@4.5.7(@types/react@18.3.27)(react@18.3.1): - dependencies: - use-sync-external-store: 1.6.0(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.27 - react: 18.3.1 - zustand@5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)): optionalDependencies: '@types/react': 18.3.27 diff --git a/screenshots/about.png b/screenshots/about.png index fc74be5237411384e09370c4828bd5fe0083d98f..a15759e8d632628b3045c9c30c10c49ec6a0044d 100644 GIT binary patch literal 33682 zcmd?PRa7KRv#>h?gS)%CyE}usyEg9bFt}UeKDfI(gFAz}yL;pC^M2pE_gVktxjpBi zS5;J3W=2J2L_Lui;R| zRg<{KU%n82krWkDao0P`a`V=iPpio}R%IcH7VC7=Om;|$w&^LtvX1coD~T>aIAU7| z)xOMHC)s5Ul!a`k%4j0Rk@!U#7lf@GRm*gtYFn2+&o(2GzQK0?61|4#i18zE@@*>H zYm)Esb|X#vaLs8l>F9oNF97@&6Sn~=Tof^skPs5`Tfnc+11|Y*73z_R2_a@5Pama9 z#a#z4FdQL4L_{#Kub&5ZjIa6AVSA%A)iNLuC|-sfobdnpGh8%aF9gR{f}NuolJh?u z{-C(UjNhi)*573_I|4aO5uM-LIf*9{7N)Nm1vDr z_FwgST9FXzcsXqI691J^i3tH*!9Y4;wErr{$?;7%`EXD+5&6GzsxVj(ZLvb#&ys(K zJQNcgzFBJtSNd004*M0ik1Ev!srtVvKx$^=a41UsS2lqD95GXP1U(n}zt+jb`Bcei zms|?!zp@O`=ZMOb%fu@GeF}dk5<-B7?V2dnzcQ7m;IJb!nRXEMe+_JJ`>E1YbOt5a ze`UsCNJve&TtK7(_> zE=O^^B9@}#qT{L9tGRa;dO|tW|C$XEY%;9JBZaID&3m`lO}RL729)Brr#*$ow8Qh> z{jy(u+bEzl#PJAZxLx(j48Mr#mB>Ohm|`H<3dNah=?V5R+FpM>d{9$B-ssy!;O{+w zscLk)&GFL`3b#WL@Vi1{uMG?Jy_aXjtf+WD>ArT~1Yb0m{%0Pwe1V!`$dW|=U9Qug2gI~l#W@Cv4jL&Yj++xuF4;|okR$9?tY>@l>?FZC_TB>pG;QY zs+5&Q{akyPYOXCA50G;xxG?`!)Xmr9rVln*5c5E-Or_~-22S?p{Pi`*x3|;nBmEUz*)lNxG&AZQah_mRCxF1h2TOaoOC8_;!C83zQCajtlBtI_06RNkTI1Mg# zgztIiUY+EE@DVWIM&`Xg4Uv|H{otZ~>?(#J%{L)zaQfZ*H1;xXf8>NSQiGvD8DWe$ z!mB5O$GgMiqNw<3n;=7OX3lNwMzmOW;_&^H9DAGOIWTS~BlaKIT`*O5NuV+ZxLyE9`bdB36*SVXcO^*9J&S4Ccl?Y6OTX!p2BCoua0+BKyXqt+01dW zAcKMW)7SkGxcn2KZiSIdpf5jd5MJlj(d;p*w+9t!pnRWYiOGKdL=|ErPtmrIa2p!C zm|N31(2VbCy(U^<9G_VQA2YUHnqqhy%`!spGS)L%RNOT&waNY5(G;`#aqd)6!kQN+ z>|79TmhQY(Ul6n_l5>eZfwhp8*KnoRb#q;qm7}Lhn zHC$X>(Fcs6;1-_MPyf5#2v}no>+5_c>02TA;P=zgV}2*+eD+pW9~zZkAFJbf!nGjw zk7sA+dm_Q#bO@S`zwuu<*Qm!SVg7S0j?cf>jhXip(1S{TWow8XT~wR9h|nhP!Af>0 znJgxC0wc%BWrpfvL!P&Po2pq7D2Nj4cj8qcHZ*$q3S6Z@GhSS- zFUO0?PC#C*Jq)6V-={kLvHt0>N5y7FH!k%)PB^y4Xp!iNzn$z34|Djg>!)T%l-zEc zpXMu7DUrWcMDv*E14A^QjtZ#MhA1AR681CE*3<ti$tOSqi&&~%Hq#=``|)zVViO#YOn_|n}Q!`xb)4ZfqMDWwKlY=9*2eC-Fcg1R*QrpzvKxsQ|1-LA3U8t0Jc4<6v>JbK&llu8 z;4FEZXl5UIm{@u<<^ZEeW@Y;D+aPo) z((ysBmnn~imXP;=grJythaEHLT~FKOz9}&hFM?fV0ryk)SMg9h`Neu(^5VXqLl4mM zZ{njPAx!eTe-6#*V}|aQ0%L0~G$JV`L-0Ot3a7<+-&f~loeI0Zhj^SpxpZfKz0i9B z!@qn`7txXb3=$i5-^6ni;bCECmX{q?H0v(SUM3T07UeWWVR^{j}kk z#eSBL+i#Z}j8q0Td8=IU*QKF`^6Zl0!#B=u<&Qf52~s`n6t|hS$!BScuN#?9@59Wy zmR&al56mni^X3S$?K&zGR6;_irz`qbm7CTAeTRyNipg$1@pfPvTSE+8yURNXK$0ortktJ&fwv$&V~>iRE^+*W6X5|;6V0JRUl3y??blBeV<0?)Ol@&C z(&D%usp}W~*)x2}&)FXmU-)xk`^Mhg)(Sna4Bv;J<+9eCe(C!ZZj4e|#{Z`LzKINT zQ5k!+oO8hi8o!yd;+rDmbcpx_7M&|FIrh_gVdR6Sq{6D0^2^qYReCxuUaX+}PDvW+ z?mJ3^Nv0SwCQB=$`$+k#SM7yaiCez7{UA1=cnFReep;u8b9A9_DA{I!+;a^6mE@wO z9Bv(NNBP9IaYs~)JSjKtpMhwL4cZ%3BBH5sUADomY3(y6gIJeXcY= z>TELz4IHPtJ6uzoTuC-{3&nOfZ3=H0qyXEecO_t-=O57HC1fxVv0vbN&9w~3hWVL# z_%k^<*v;u!%}{ zOna{{zG#tsp5CK;-sMz4zY2}-xy5bTnLnY}oHV$Yy;YmvIgxO&<C7Ub-CfeM5<(|_+H770n13v!C%rsV>xWiG)8&5aD#Ikf?y3gai`IlFBf`ju; zq=xjPZvw_5%0D!=bq6*XORKzC*^!R+B-`P#k~`lQJ-5%Mkh0MaLcvt!up?`pPwQEo z04=z-Wyo>pcs=(pl(59uPz$`bZh_B>O=JFtM2i^xZz;y8VDFuCH*d2x|Ng!xI{lp_ zXo^;UBE?urw9*<;&G*`%q3q86Yscw7EjG+P(e{$%YHq*ky;r`~W|C*a8t7Tna_EBp zaSlU)TlW4XosypFzZiQ3|9SI{>%U0?BAA?r;IPsf@&A&3Twx2uzf=$AKZ1nFH#TyO zQur_O7Jd1L^Wy+WdH-_#-akwnlS4`OFC&BhBm0v^BL3TeT>KNN9bZK){-Y?EKb7D= zkWH!?MEP$6u1{?eb}2py>^~aFeEo;g2}g)U|84N8L0u-UfTGs_U_Rof{7n*_=wHm2 ziTQ-^y~psG|H%`6{gWq1)Ilo#+raUY;wgsY6Vd-oG`{_lXb1v-hW*{uj z9`^so@7sG1s$`FP+``lw8eF2_F{F+EhAHw3xW5|;Vjb#3(C37bAyiNTPDFFi_d5|r zhvW@(;NBuKn@o3K!ogSiNg<+u75y{2s8Vho=naDPq~~A9BsdNj*Sx}-vT!8E=m(Ve z5jGVlg2g!g7^HqpK`zybVV9R4zUZCb2%OaU;)@~Gimcw_q86=02><{vA;Aj^Fy`Xt zuoTTX}0e}qMc99!6CDpXIG=!V-I-@2&QYtiCQA=-hG z=LA_Ic$SX0B9x8xx0W~4kh>q=KrqcD7>WM8x%vILjFbtGdiM5H&f80nr@(bM&B&x2 z#f%-H?V@4DL;hqK%5dk`QhGO;y9y=ei%dM*JO!|*2P zZwcg|;ld(Fx%ykMR9`QIz|Z@Dv3LkF*d|t#*mgy5ydyN28d4Qh6Hi8q1ncI3rGu#G zkr@MaWV|_Kn0-ZDZo$Tpp^YN7_s5-03PD;{48?fWaOGl(5{|qEQpSnlo->Eqma^=N zPTOZZPiJbpHuorVSEfIJfGBSk!(eRnOJ6Lb9=_Ax$aHOd-!IJe#JJ8WSjP z*=Zht#lY%9fJgYZP|1NUA_4zLoq}y&!1b4^cp^~c)0ntYvTCqN>MS3pYJS0sOvziE z9vyfq*n=R2{Kt;^XCS_`64)m}ugkn??pgjmOqVlQF=i)Nrt-UN9H4^r)t@ZiV$!6hsl9zg^hD@NUG z-f4L8A!D4nn8LY`E*dEkf?^i?=f@L?hrRiCG-ZiBJj+S5u~MPo;)w;jS%M5XRC^^W z|HJo=CXwB5wCS~BbzJI536@Z8yqNUBT$*Vl&bdh81i1!|aPG>GiYqloXcJbP8Of4F z(e}}}!Dukuz+r%r*b0Z!R^w(SUk6M_e*_M?a<>swCH8m4oQR+?*h$jiJ<38o?>;_j zk87xaNq-n#-+JMG$Bs%0X?-qaju{; zRvF&7-!gtk%gNTRmlj0wY0)N25nh|{bFJty&G*=+x9IHK*dCpb$16_dtm2>RQEb;q6g;8{rL$l zCL!g|5j3#IbbWVBR@a%tIe-sHS4=*nNz=79?W2cShzmZ!N1Krd5V52HSztGuKF19e zw3uN+qFGS^Q591)MPEQ{uh$d;ysptnAFl+cYV#=9p3@d^4WTQL$0Yp~Sd_!0rq(2t zI`W#%*vA$gQ4MUytfn;-O=!AG(uSc?NA}3=^|5R?Ie;Y=)-JQwhOfpJM2^J)!OtG7 zR7pWVA;)_;TTMa}Wqqci-eY%deTud2ETJ@PH#K~?&Q{c6T23=Gq#23hwdRBrU;gW~ zBk^t5QtC?6eKNQF*);o?&OK~aQmONx9YwK_vyR8{&04KrRQLu`_nbilIMofB$5k2BKw>`*16DXP# z?%W+rYmDTzSZ#C*(=DKO|2@YqQAvb|ewi|t_B%=b8>tJkcc-NASCa4s{HWP^)E{HD zsn8b7AoSzqrv@?{rgFMHTwF497v!*D&Gu~u6ad%mAlZ1P`oNVC&hj;uGvHK$TJQvC zXAsNMbLakuIWl&{QUB#j_PBNOk20z;$^oYKpfI7&^@e7T`w#pli49b6EN~rxQ!!ZV z?~}hGcYxMLPo0l_{J8K85s3{9^c>pyrCv*ADz0z zSF^L=v1R9W)gFepZbDWD(y`tX{2ta3we{pD=`wuwR!a>Bj+| z69`vn$8=X^_*Z#bi`9v~S>KT+-MJ<$k`r}-IDx|9fo)qlHJp?c7FR?1MSI9A@3QI= zRQ=B3RNWQa{PFD9@j7;ysgRt8{@qK2JMmg)O_9JhC(o4a=Udfv(VjV&9MiLaon@t)#82BIBNzW$rH znbz=pG&nB#4Lb&b-*%=c{e^g_eYpF@6)hXw&v!$cV$mmusYDCsnUnhV+=yZ%q-O)ZdxpGV}ak_vupsA$fz_G9nZaD8!L_b zQ4F3_eCmvDdT7gOSVt~e**4>SC&|dMt8tj(HC>&7@l}R9(~w=cG0F=Wx;o)HrM)Y@ z2E87QCP_MMhmMYDg@6-BOnCU+7>L?oS@K7a-{7uH&!>Y)MT%EL`!GmTGuPxP!mRn|LC>Crl zD%}~v&PGSxuMpXPh~PW#$TzaD>$S`Io1&u1kOS5wAu$Nt>@*OHTtX^{wv&-QhPAS*XX3OGjshb_ST@ z5BQnuW}C|LmEE1@PmZJQ`-oADsoB9Qh57hc`Fbx)AsBY-m8LyprkB~E5M+u>;IGeA zPiQi|M^V$%%CV$cC!n^*TP8VcV&aI{5So~s-FCgV$Y*t`+-3VZzgqsRk0;iH;oH*KdWP>Hq zcDuyV0@K_U&*elD*2ef0{c>Ggu{Jo%E9B``j7;zF>l`?;=c4U?-6{Xb1&2Q8cFjhbSfZz^ z=R;$&f^Yb@PQ8ibc*Ym8*SQ1Q+=3?mqIrhwE6u~wlN?p%&{%y(f>IL(AON^6LD3cg z>8p%>vQsEACkDFU9TuGWGVt@q$pM|gX>N+7d2|%Ml=0lInOrSQVDHq1zjIrA2xRMz zqxE}e!^>eP23_A{zP*VKObaamOA*MQhWy~8U>t=MlH@>Zi=X*BPS(e-{-wG^9bv)mqJDz%_&1dc;=<-Qz z0>|SL5%KGGuQ`|2x~`)<1Dv|9plEA#9G%KbWKVl5V)8hsYolAR;3THSWjPV3kK)|z zX&I5GNwJnUrDl~GkX*B0;x5xRtadtL(LbYCtQ!Ajv$<`W8_mAvego+b1+~N78uIv3 zIvv$`!bRlYogQI_&5hfx2(z0_e%|e*$Wo>paY?$P5`GlAmq-5ZJ1IH&Wo>cIZU|Ss z^a5Sm{NrfzoFF{F%DBgw#tSa;10=T7R|l7`LS_P!*>3ZG(9q&0nspihI#jvSC?ttN zu;7zvnaT;Iuz5uR@iN3Am{}Y?sXv((|fMBZK3v zqaAR*gF~F6CKTThOqS>~rtMFIWts5~>-Ia`4zKIDSGsdih2^=wV~6P3n7Ya2_U+zw z!1Wypx>c?lFUgpw(TjwlG=<_6n;Fm$0RM;uiHB(81=ncGP*cuJ3X4pfBNJM{~ zgTgSmrUm%$cYJzm>w{rEXy+7%*N|K}LwHZNJpU+N`|o2C;2>G?_r*`4unrlnk)AJo z7o-={$gB}YtifrprE7OD+K^4|$IHtx?agl$7a0%dk!1AFe~=ODIt-MZwmHtKLO(Y* zQW8WdW!l98J<`*(F~s6Xcbx|gcfw3+jMrhDVJ`>QmqS~dG6A&6+6bk=yo%E&Jq?9;Jk#6V$mXYx&e?39mD>OkWG9{0g z<@x0F`b9*eu#>sdu7fQ}~8h6q0Tp!?FT#{cUA_&F;M?KwH3TFf;C?P=tZA$Zpst8YTOiQ|RD$BEl zn6Uw-Bi1@!50{ie6Y!`s_4rS0Y74N! z(N%Y4MQcZ_myAS)btS1`4r(vb^qma)tc+8rve_fG-}fO(W@uU2?g?YvOO?wQhuO?g z_zUvC2dz241(SIshU2f|wg`JAs})-*egthGy&jKgyCK(+*;9wr&e z#4v{?{+AudkvGyf~QwT-c0N~p0sj*qMt`Br786?ob# zje@xgC7f0-JO^`9dgQsKCD}6Uir8RC3fdS0NoPZle-odC2)p;OqRtPv8>BD^k|;5y zd?>f0G`zKb2+u~CXN8`bz!ABNT zc@be`kDTbocYgL5&)U$;{y=!GMFA4P6VfA{XSXn7op-l{Uw@Ty>y4C@bg?3vwdgr^ zp;K3geQgMMwAM`?cgUf-!LV{XN88tGr?6yvhzzkLR37U@tr>gR<6#8(!+8*!_1fzW zCpURQc0}uKs?&bs__$+kvNR^9GqJ4LQ9Ofau3WK#M_7$nm|;eij0kYgU&(PcVrjZI z`a|3AhEKctWxS8J?O1Q)`L>O1b03BvR3)pd&tFazSAxtD$z)@z`v$L&-?)Tl==UJEIwH13F zI!*LB@OgI#?Q72vg<$O#LkB2-`^=qQXIFMmUFS&bB2!RhbC!pjAA#}(EAllYa%ra4 z3*l2OrmlsMFdDjRwNG~E6hSPB%b$}I?~A8?Rm#~}%28%wS#qweDq_PD!__i@+5-du ztP}=aSMs0IIE?bugq>y1XY~MH|0=Tn`KG*D{Av=$@mZYgesp`r(}>_XRTD;RNdxB$ zEH=Qj5*j%!yb^7!GV1*`Xr;5`Y3Dk-kXcrI+vHZm`+6N!Wp1|Ci)7!T^X@|KcLdfk z=J8IC#LyD|pNJ#KqBHNhlPaU-^&HWBc^t0<8D8XEpCPq7F7Sp6jj5OvzXzljwFvu3 zicc80m3$!4ahn*j{oQh=+K6^^aPx#{O0y@V(`u>H>>Nyq+nAf?qBW&%#ySA=iaa#DG$4*v3~7W&dL?+g z7RHV?`mBz72LdgX7&_hPdxPgQ)`@MQ%g`#yiR{P?Q2WAovkhaKN_@Hf1(D4OH`$=X`xAcY%JP!U9(t8K@=yWT)Q>S19}^I|#W^FEW2(!d^a#RW)cQ4;FN`G`9JE zwdhdSVe^i6rY`ln1J=zC#a{^HU=2(aCemDMbNF4IT{g@OWb=XGbWDp}%c#^E;VBsh zn~;5R@y0nXx#50OJQUIsi!L{EgW{$WN^Uf$R3dB<9yP*jQIYF#K34I3@AesUq%Ekp z=I4VN9SZWH9N~L;Lru=szM2X6Ej@CcH-Tzha#M8KzZWVARdpc8(xxn&KuUg49;8B9 zh>AUQ;Z^uu92+l6j?_7i2q%xX1xKWjIiIhGjJBs7zzH!YqOU-#E=@Pu=(I-^PaF(Z z`yiA`23Pw*mI}zdOv0ts-B_%5#T?k2^7n2kAgy@GepaG&l5_t&(Y^}*=vCo_b7~0R zpXKFUKkF%HCA>Z)avE%@m=w|c3fmNg-1x^fIMDVRU?C|XYH(Ox9SQ4dw$^xeshvXJ zxd3Bc(d`K@HR6u+X6G?WWca%(h(PV)uRZ9#VxmzLvs8Vz~gJ_PJ z?kLZN3dgEWSANZ9u?k-}-CQl)Nlr2o^tn39g#)~fyCpxoKF6Qysyb@ziP5TZsIzqr z)(=GC<HgQRWypx9HL?AdINPJ23MjY($&%pp2c z)-<`+1AfezzAQBxhWQ8PJ0FTkpdSwbu;4;Q7F7~3;j`3|2X1XeO%#n%i%jP(1y86% zgy_Fdl#i4oB$Sp?Ajy%G)hX*>5-Y0?n)80wHp5|f-&v(dG;!%ePS;uo47-R1iioNz zihYClJD)a`y*?5mo~oTHH+S*7xg;u#8^+HI*~|$8soTl=A?tl`F+3#J4p)KwLCSQp zl#G9?h}1}kWmQ5bE57Lf9use8`H~S5#nK9<_4fHRj@;2a36%vgLAg@dC_Hv% zdUIS^-uHvFZrsQ{;d^7R#8U;BrY992o|>NbUTFe-_~))v(RNcc(`N-tn?R8MF0mr> zcuWdM&HGb0-_^_I6Gvie+R~YA2x%*T9KrFrgBWzNHpSBYG8{Vr^8=NcjRd5~ zi;ro&A=>faf*cN}tIIyaYSDl1IPQXm8F_Q%ia_EZ3hB8Xb(iHXj-RF!SZIVFlIaK- zBs33j=YD|%#VUEMlBGhI#V<({!Ap;}Nu(@es?Z9iX^J0qRW5O-FJG_d4<;Am@w%NEt ztWvDO>c8$xsO&8gawW>AYYHt8Q!uktoZr;A$|J06gP0r2Ep%FM)|hSeoWC>I2g|jl zO&cVhxB~%mp|d8t^EQ4wO&c|N#5rF4zFgIv(_+rHY*irJh+DdGhB4u)q+LwstB2x+ zzl&}=(Ksh+-u@94j>*HbagxZWI=12_VRas}U%MVU1!Aq#%f!_ht)Y&eWjaVmbWqiu zj&E@5Ze`AF&-SJX_ygB{y|auI&t>R4@4o_+xU}rTaIGLuB@mpX53gNswRsz#MjY-J z=TNT^9`wR>==-eMw`Pu(j>7Ve;+@hM*o?Wu`!08mo8MEK{w&niHNT%ClxjCb#b2<` zd9tP3RAL4&Z!i}iEnwTXyXI40IaU&hIaERAaD+8(`qNp8s@H$(?u)Tn5~ob_pW+f8 z=94*W>ds1k+E^J|#4DabpA{bIp3}Dz61BNl@-x06Hkdj;=e$@o}^Whxp zJoIwS`;Z5l`X0d3s@}#gjY$k5&zT@76<+>kU<#}VAa-TDG#I`t@)~1b+2riv(?0$g z*}4}qmEc;EQ`eTZv~QRmk{*sB9X=LdERCL??Va}Pyg%b>W`zxc_o=UQE7@=ei^QxH zRq1s?VRCIQPtOV;-KXcDkS$7EGEkJUY}KLyAqz~WPp-%_TYBl6ti4S#Y3;f`uou`u z6wxOCtT)A$SzsmN?1+kaUVDD8(5yc!sH^CHmqtBYWaOYHp*te`03wcdkY+Ty-5j|M z$%jde)VUvf(`uUZcM6qt8Xi;)lL01sR@NqVKc42mH1)b~>I;4XT10G(BnABAQby0H zT`$*%H}$(*at_?GI3q%b161@aM$&5R(C0J({=ykja3~8KZ zpZnm_4kDQ-{wu4=37VR6fm9>FHfufI)lpwBHl#Pg+?uSO-x)EgEOaU@_E6xn zRpRusC{Qhmy?Xv9Z~Wt5@2ge=+F*i+TaVm51r87d@F-(&Ew( ze0b-SE{fC)WoEl&oAn=0^$8uN6>()r>IS2d;zbK2PW>ZH`|%m^(KbWuyzLN6Gu?q* z8{vF2E6cZ39iB*CrWDIbELd!uiitQ5Hs(fs)v;3x6 z(H`N_tgvjIBTe@xpY@3*nvHjQuw1G-#Hxz4s&(4z5woO2%dJ@x40t&%UU#qUU2jfO zMN|34tK6s=2+R$%C^og=epV26z8}rIN-CL}kEEt>GHMzMIHOOj`D7=)qaIo;P3J@w0iBqXy-70kx!QSij(w;mU(7#OObkDpP`=Jc9DQ=!)5JkIZ36( znCgyZR>n_bv4T=JwKwMR%ogFU8;>#FpWT-Pcmo}RpPoy2I)(+4qCRzs&RZ<8i zGsAS!MsJA71_jt+d%4Zda{FSkyTsp>A&(~OM>yG06t=QlrSbw8I>P?Qwp+Z;Z*Nb} zRZLrbJgJoxFK)Y_0~1T)r4wKq8h3GOrcXiB-DYe;i{Wk^lJgGtZ2ASXBm^S7F^;VL zG47DDQnEDHg{r3C?bkq5g{P9)Qm zHF&(kIGfz>pp-;f>EekV08S64)C?)YU*ntUxMCC6wg?}u_L8zmGi5XK^zph{-CUp+ zCKetceyoCS6XG{X-cH$ZdaV8Zl;)Y06+bl(bP9Q!f6A(OJ`Pbkb2)R$GOC4>vc=2^ z!?@YA%*SxX)skz*c!N4-0^S5^LiM=2m2X*2%Tlj<&WHD?Z`uj~Zo$V#c7|FGWPC{$ zqR|$2Y?sEnC?7oNW-hjHZuHM!%)t?$JVa0fCDtgfW4-d_fHRVP^j{}?4m&=l$Jcr; zQ|}veM~sO))09l-YA_34$teYn7)^4WBFIjhrrG=FK`7OQjn;2Idg$*PU+NU_PoN2w zoystm>Ou$g@v|kAb#;5-ICP?9%JUiu;>?wzjbo8JhXk!kkaW8eF!YvqLDrA!;wP09 zW9n=2y2J_YO3a`b#Y$0OQuIRNc3?BS&2})|P>OjJR7Xk7PDs*`43KXhHhN;Y%5f$+`iVSBUY zd#jF=+rxJBtTGSYPZGlrFZk$MHq-+kWdGmMnX7g&%nQDIZL3N0%bm5-&yqaHlf7rY zNLI43(Ij<3WopRffTN__BdvQY+e}xo(|H?}DH^jYL7Xh@_Be@xA@Ze?J1hwkX5nlk z=);O0C4LO?2R+Xwd0cpH?sI5U)XA-_cru7myuerbmljOsGU+tmkBMPbsj?Rtl_+xy z7dT|O^GfOm;`q>7d`qLv=e~M;<+={zKkFr^)ZC7}XR)}%n>u|BYiVHOBK>U8&sw1C zlaIH!9WEM$j7WPB?r*yXnxlD7%iE%DNX{ePKQz5coqa$nbmdO8aiOQB7aA|IHivP! zA+znoy75UmG451BzVzURap~<>+!c;9lZmOAF=n<_@#5LIgam&X`TA)GsdTij$*wdC*2ugvaz?T-GIhG&emPOr7Fg5ACjE}EB?UqOzTmp695>^}o7fa%RtGwPr@zvt z6*6NwAo-Yi0%a#ODzikt3uRVP_#qrc<)s-0>KShh~57_=NHY z6Ucn|d3(TgdV+(+J16sV|J%abaf*(92iPtw0vFkj=iNs4{Nt#zH8~T(K5Tt~+6LIW z3q$ld9XV+={Ayy&l&SJkVU$T*ojpI9`|EDU6jYP6`v(2d zl?6YfU+Bjjs0MM8i8#y3*fa>b4hFUm` z3yM!$@0-Rr{}u~oxS_ct{7%jG_c{F9iw-W*4$s(Ow!`jCbGSKLapUnQZ2SD9J=MeI zN0|&1#7f2M#bK!1g5*R{=Ua?4CH-Vk0V_tDOz&55tiEX-kZVIZtaOvogsaT0k3sezk0{TquTRv!

Wu&YKrEXU+-1q4a_td@YW_TRl7i{lB|J% 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 R1Pbs^SzZHHEo&b7e*lIRu08+& literal 29695 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

=tmHHUvgw@tnmLUMs(4=QBCP+gRK7zF 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 D_TsZwgUF|Lj@#G((IwuCWljOnOJ5RkwRtWZw zpYGKlq8Hi|+lFO_7w8$w96#MrLFQgY7|&*eUZFa3BjZNKWzx^84K#1w2(qYN_}~)O zoF&re9flwkzf8@_ZAZD8+LxH`BLbR#nl$Csu^0Q+}D%jBs z*FtCNAsTjw4ur&k=>KWKzWH(vMq%^MiHo`lpN^tQ&i!*Hvz0QA$bk6gsn4GteBD$e z6Z_9CUz@4v=!k^9NuphcB5_NRO9pI{vARS&;v%l6t58xNt^ofN;lx8C>$u<1*G+5> zZ8w&%n~YyP1{wjE;lv}*oZHT1Z?xjE)b|ce=41RImifYkv#!C(lFlRk)iPBh>sX=? zJnPk~SJ>m7VoB$TSq#VD0X$Ekrlr*^u?F68XJ-dSST^G|3{(K)eh}A*hm*8y+w^c> z7T~p<&ysCBV+qzUpBFEt&4_sYogC=VhMIu|@n}I2nTe`PrYj$AX>cJct!$%veIB7! z3Hkd75^t}gqoarF>ex?LuU@U|=qRkIIffGt;JhOWvS&Lip2Ei)6_rMxTA!+Ty%&j5 z571KyIQ_S=cl6N$@dyqBrpRn%e-K6(jEIOxmGz>{@rhEDsPI`?LXApM;v3cBsF2IY=gys%RXT^4SeBC8)ceMb%iXL{OJ@o= zxihXkw^De0TlDTh!5Hb`f@LW0!;0`p=%EH=&@EX4B|EE`&z0?q+U@$sna;vbW>VI3 zw^8r$t*c(TaR6 zy44UL#yOpmIzD}!d+Pj>@r8O2uU05u>RuP@7*)b>Db>m<4yG54)Kkc+2}}A$whfri z3B@KsI&$2#B>yg2bGW2~`S1LYOvu7GPBYp$$Gm^PBD}t`B9{1wQCbPJBZ(6!>~fm% zJg;nJzS&)&Gw_}J`u!mXy}T9`;Q~D;WS@~w#rb!!H3c;_o8nI&KSrd{U(r`-b1{DW z`11OQ!X^H~G3$oX5)BWH@^9AMsH!Bwhx0p?E8oaU2??}wC>1&9{|w>hJ*sZ!S@JgG z^{;1aP9>UgWG`{wbIBEcLYMk@y12u!KUS}$=^?59)#B_&tNUAUq5zS9-Ox$rAQ1oL zEOXS{^zt{rikJBjvFG@PqJMlM0=FE2l9Pyl4f&^_q5pVNKzd>osjpqskBiU^_LY>N zy5WKpfpkuoTn35!P{#8L|HJu?(`xkMxC)Ou_F>jpC)xfNGlNt zr6@+%{hu7CZ9+zUni$ysS^NQLYa9s&J3Bikr$W!}u(=Z_<}?9-hne$;aeR|~F55QX zlQ%*Hkg|Ecrdw>1_FRbA<>uhVI=b%7`K5j2*xhp5Ii1bxONV8O)ag}|C({hcgY$&M zBiEI8?L$cMC(fbqN*^Bd7{aRZ4}g3%+JYT-5i9%;+)Bk=U0sPsT|{G3Qw35l@6c|! zd*|?gSsM4R1?`^FyTh!ToYxT;+%rXCvy*+VlFV=un5EH_g~u6d*iBOhW|p>zLQc6= z3CjRal;-Ri31Es0#tT-4o?n%kr9Yoc=0vPN{|}Z;1ZQMq5P_#h6Ab2#PeuWSLK$5` zeuLE&5dg`40vCy1;Pnk)5cg#Nu>#50pcfNWJ~RH&T+fcx{#ooK0Z-FSmL9HgxO zxxzr*9r~Z+e!#qe(%t_YWMcnmapgSXpC<1}BLCAQbN$sNdBJ4u#*Ew$FKsVxFeMzlu(@~oMw>(;Z zVgAF7`1Wy{-L-#)ttkxj-Oe$CNmyRL#9H|}hxK1W?`_jX*5AxG?54iWQzJXT%>WsoB_S*)VrxnPlZ3F{$y!BrV_ls` zfoj%CI7E@+#2IH#n?j+pa1$(SKK5DR_)b~yp<>?sG}P!`t6@`Sp{UijP53~@asY~TBnLMyrDsX2Q>2KwE>0Yz~H^A1dei65b!q+WQo@4cSc**JH^iU(T6ik@4V6_KK3>h@VOoC zGv++ol?=~r!2$hkjISi_9D)Yc;RYAe3+3ke3~+t^&2A3}$BDrNm@CGs>uvN4#K({h z-^aMVL`M|7(&ya1=Ve!x%9mwf5REJmQ0CPY|r)Mt{1l6LPB8-)2h9l8@`>Zn_g~?_okRF;%EMHA1Fl z&)&9OcGZJ#l7Xu~=R#)ocLM)FK;|Vx3`#VZelUr(>Z(4MupEC-=tE71b^noxgFdgi zq|&Ry%2d6QW>_K#$J+8Vmk6?~J5StXdHt5sVTY}lj0ZDRn|-Cke($XMwMb$qV825~ zQO@Q^4o4qv4Jq);LSnnkQUXDff`D<3W(y|YlFA(GsCx;4eZ5zD-^@y5`XZr2q`qZv znUUS(eOpj^Zy*?$V9&iMEl#fFGU0A%8_TZbtwa^gR!%6K=xTr5RM;2|IGIcYm)DKM zw?tQuh;VEUireE}h<2rwSNUFjfr(z%Rz%7Eog#x=-;6#|!<~=k+~#gxLdcsaU?+Im zCyl8tF3B`GdxV4f;eM6-aQr;WmgK3lbx9>*flAQvoT+I-4$x;?upha`CoL*&ykb@S z!usMg+_S9C0cyppe6w?JbHxj4O#b5DI1M-c^y(*a4P!aEQ5X_cI31aL=5PoJ9SQk4 zN`^GDlJp2y%fhV2!Pgb~#U{)&rS-Yws)#?P2$Xb2@MP64or8tLK{ro{ozQ_Tq2n|m zbIPT(@S<#Tg5q~6nAx^isz+(vUmUD&xrBgZ)S6a9A%%J08YX+a>R7q!=az(L;iIx} zPGPy_>Gj07pVg?|nHe5&l5H2wYH`lO99lj!S%X&g>$Tk?$seF$N+DY<4hEu%P912m z0nM3q2lefv{m~TzABGW^SVU?<>&B@nnK=WdL&IBrxsy0834IGVS0r$`Db4_t{u~E`HaeE z0*>Ljn^M28g%t+lWqN!deFfH;{*T?L0ZScIUv!i9S;$KaQ7+5-)*08r2CPx8qzy>A z4(OOSZti=Y`!pWSbwPCN+dG5T&lfaaj-ZA$fnn@yjF?P~bX_eS)#XPBc74=NtWKTD zu!h)!eG?RTg&wo96xiODksm7_7WriCyGe=rHUK}K{!zQTY)gLhza*ib{c1|fUzzurFZ5N~W0NYLui*}M$Vs6#}_Th0b>XhuNOrjY$036M)2y^u5 z@E~(3V>YYAc7y`QtDnj7(I`Ai3y+r8R75wg5|QYkKAw7{TK@nGbLxX~n(nS7haw%P z12@(MI7mRFRL(=%%lE^AXTNXPF$u@pk?r;)-1o|t$thT2-=z}DWOzqlOy@e_l$F!po6@&elvfMJ+^~|qwg=cgS4Iw<(|Tk2s@7DC(glP zW1a(xEyG&H);5f_#$?qidcnJU!<0?d{$^4$Q#YF+dP2dpZ{P<~u*TnT8p~rRWZ8vl zCT@>8A@C|P-w**z)P19S#ch%L1g4vYlQecU49fd{1qs%fRj#C+_X}51 z(T1weM#ZsyUS}WGUF^<2GHjYOue@oeXXccqF#VUsA9%U0imsg2LON_`1T}_K&^@z)X}FP&{YS#+UBAt)UePEtaOMQvPMq zaf!9e(XXWso7#_RPB0#R5<~%TaE^@YuTOpfQushFHfv|hR%Z>f_!Wo!@5k5-f0DTb@`f zo~TKt6WOozab7wZQ4_CqDJ?XH5>r-k6FuLCaJaeSb`!ss-@=(6bB{LS$Hf;F=*$m_ z(&XD;>9m|Y!gKT;y9VQX`>s;TBV`E(hgd3Z7U*jx|E=(YN4Za4k@@c+W}rPuWOLSd*>Z}^qXl%yJGq- zxJP(er9;=3<*uhW{Qg>`cmRI6+lzhMu~co|L6T9Ek=PL-PNoL_Fp1kcwOQFh=1_2T z^Sl^%22CY%?Gqwf(O+?uxpvG-zYd$%GRE}d_9^mG7b!u*&<%oHt7nzBL6wb+EIneb zKh1PJox%oiUfMB{aLR?rg}qaM(xE81b8MO%jLO@)I}d~CvB!A(;xpWL&ERvrz~9T^ zX=^=ay1CG)WyNu#thBZkDo^#dJT)WxE!Q8H&Ho&Zv-EMexA3)c-aw6^Rf@eftuC zL6SUl1#Lxg*=M?=I`;=J78ka|&^lY^d@B_x)uVNEV(%(s3baeNT8nu16;K9iriO^Q zz8ujY@EXdc$$#>@qeoLzxa(5}mymUxC%586;}4|8?br7qELAx%^wyFdGyta|go&>7 zNn%3*PRVR&B6zTr83ID{j|*P&)&JObc&^)=l!hSB!s)cMnrA_Ri= z9oODwn*GXwnrjbo`>w2}kpB(p z!_Y8(o8oAY6o}I!a7%T;k zE-|nd9>6bzR9b%AgDZWP%7pHxPMvr z!H<}enm?Aq6T%Wf%Bh9xwFm9o%OdsVq=rVmwRu~8Ic6-!sknLmOHzi6MfhZ`(xZEVdP=mhlYmMO}j%9#|LOLUKwQEMI)pf)5{SlMG%JH zCOTV2rh4i)(Ja+PTcbEm1(~YvXJoA#A8adnUJUde1WA=&j&K-gftNF%WTUY_>PGEe zex_Tc5S8|Xd48HpVL^GNz*#^mTjJ`S81!N9i9_&OrvdpLPdmOG2#9?-EKiNn3`q?W zKlMBXZza-qEzwe*b?XSbfT$~HDoau|(;9R~(Ptn<96>tK-aWm94BH%l9fpU1D36f> zdLD0W9Nktsv(xq%5oi|wUJECzPro+2Sn&g@A_p9hc3Bibp3%nTDQ~A;EUzEwrdLYc zEw2Gu_4M}AzZ??BR)OMr&PS_ z85dq6i&~)YsazQ$v`FT;pI6)6D-u+=qJIS$Zvq=jB7SD~8#gaC2_5G`m>gA?>1z%Z z@}o^H3pu};@6fYHoVz4Noem>DpfN&$KoqiVS<{o(?VFAlo=M}#z*q*Ngd;bbQrU-)?rBgVngQlmqf$Wt?%`Y zSz18{2bqUjoTeR{a-(*VM|;5X=rcK`%HpQZwF9h~d?CJEKPM+_oH_Nh4xf@*w&|CX z6gu}9p!fEC`f+~zroR#cV<5K{i;DRo?*-&)3jN9s(jTp!o0Q|9#-#;D{s=|FvVDIn ztOMb#t_t%UnR{0FHjpM8%g7OFLnJoaD6^620STAM|gxDu`vyBA>;-55nCl7Fu=g( zyKsO?k{|-Y*qs8uEp|7?rQrPm2_<4)hpTO)chm|k2;}K-<8y=-gGu!MsF#FgJJ4cD zO2obxEhC1E^BbkK;c0~@#ZFyTl3iC%?RoM`T9L#oIP7vh3GmbmO)!BtH10hN?t=T= zA67=35SFIHg@P@x$Vb?ifmEhg`%>+LONc?x2Zh@-=4((O;mPT5Qn`L7fb-Thq4lNy z@SJ>j_+g~RI9ARc@-ufiKFzZqwp;@6a4qqjgxF>W=<%l_d|&4dqxC5;(c(e%ZnHp; z_f9E(r92qmVL2wNw`5bQroKic)o+~b&Ik4Hwte+{v2$>kk+bIgXuC%zOM?yrWl#=1 zoU71~&p;~pG&?i@Rk{$QBMpnsM2kGSkmMI|Io z^6OR-DFS@bgpeu=AFSHmE^Xhk(EN}S4*QhtMr_9u;81(8_PngD@f1erxOW{;(z5d@ zuNZaNuT94ELq{Tl3?g#`)YH8D+9IVOuov^e7ahOksF$zacpq(~<*!|>g6Zt#xeB=l zq)Gr0s}Yy+nD1)!ZWAV1fVjyHyL+mhZ4cm-Q9AUiw9J8OS6g&Gu%b|d-RT@Ao@7@G z0C$Trn3q*l*a++R5M8Y053Jmcx(MkzFGphBI`8eW4^N{Q#jGyD#n^P|hNWhFUwQ8I z(R7=?%hTub+@s!DdXmRoZw1lx2r;1QA^gy`bNEU@*R9^`rjCV-jPurqtM%(UmxbqK z`Dg@2{|E(==;Hu$qvy|H8RxhIgeb=t@rB_wFS2w`H z`}vf3uXVk3Mctejp7heJ?%~@sw9EdjJp!{;CDF_$BHO}K=Lu1v?(TfOeTgF&et{BG zC9Txd?r;2S6%As_lJV#mH0+*Qdhg7N^piB9I~$^0aQw%c76TO*51(|BANC$;F5ui1 z(h44IgQ?#n4r8~y@7WHqk(G|UZ1o@IpgNPhgZLSPf}W^J$$%OHvFqMCGtgAIe`r_3 z`K4mFR-`S5lQY?X8ELY*SjBHXPo6m6w2OIeOuqfSbSxuVZ5*P!y;CZY`i52~{#*90 zzA{zMPHx@}VZmDt%BOL!{H(bvImJ`u*()AvD&oGdCX9={y3}pHfc3GG!1@mGsp-Sq zt8-M7}9G6Q2dDxm`=OE-x)GM&$aBU@%EdLQ#f(TrS);x&!14WXgi9gyd^bs zzVQ6oqo>d(moM+&bx^k^u^00k%%|o;rsMsj|H0xk62T5(!MK| z+TS&A>$ZnCDJW6eZ@u<;FJtByRh12pYUO_S{)J^4u<1CAko(;6%MP>N;Wjw?DA%5dH0xt*pIsGC@oKOsspD{%D(quD zFE%_CweD*GIV&1G2NiRnK$Z*o4+(Qf~181rhH<-(2rt%}awzVJSknSdtCl$-%(;Rry>wjr-L6^`(4t`@!xoq3xYe97(EY*^Fr# zY@|ObWKt|ybuu%a*clbO zIHn!^Xe`i?%Wf;KE`FPzt~)qPN~G~4CRf`o-}coH#gB6P?tM5kQu)JKHA#J^Cuydy zQTl$@8h0VVTOw*%mtd-Vl7bhExVO`zb;&T1} zYMEk+2TEpVjTN%JKHqYwfZe~LCH}9ja^h*o^n(fvQMX(Uo)rY9Ew5MHa`9=-7Y9!N ze0udSqhnxjm65O^zU>CON9qMf7Xc-?BNgSmu75r0E>H&hr>1x1pQa4|u78F8SBrnW z#g+eRkz){$7K{F4yAun#_7RjQ`G7Lu;4;f)O|ez%?E|M%rcA$#>q^F1?e-xkRN_3s zhv5!T=`DWq3nqb*;|=O}Bpu;Y=MmK1Uy;8@cOD`AQ2CnD)|FTH15W?x0l4YOd0+tE z|8F)Y_169E31nhw4}ij>>0SE&?S06FuK@4!L-)74Q+KoTaQ}Z!$H|-iSC2Szh7WQ2 z{(OQq-uk=GE-|8PUG#mc>d^iX8*NEdQo^Qp-;$jBs#y3-#dCds8kcZ{pFeF8p{vwvUl$v~Py8=oYw4t>zDu@hMWx9oHkUeSY`0p^EMPKno&g@s97xW7Cp zm|hWW`f&SG^=4P0I6+^>C$sHIfK#>DklUhqp&seT42}bZtNE;z)4RBt-e7iq_CVf*>1gs(`Ik(VBrM;;kG@hYaz zsJ>!uiDl635emXN74g+PHBY%sYA9D@85<@a(IKID3q~VhVzY0d{SYrzN zBh~B3DZwipkB1%Bn3Soux>lJU$P~BZh1y0y(WP%ZQS>I+D^wx;NwAuCkRpTf%T}Qw zpBp5U+Ui6s!ugciE@t5cPRw#s$%tsDvL&6`$y>2+il(36&4-u<_Am8tePeK4{CrDM zAgWu{*jM1LpK29fIPaimINaGX#_%nPBcUV9ALEl!AAo)x}ilyojv{^8{^PNg+=eCvIjA;l|uwk$v>#1IYg5B*2MU!f2K|-+{pe- zL}R`lihiEiBIvfu%nKwy3Vmj5O4W#Ga0v+-jic5*jw3nCblndxl+biX{7^~(lge;o zzT|$HPd5;<>r~L~?dJ!)VDP#STL0P(_GzxB@|WU#lkIZ7k7wP^*M7h|CEx4{jQx}^ z#2s|2jq9Dk{4smA1#{PO%$!VzDe3^^K3J#Ta^mYFLQ2bWZ;S+;t2aUSO~^B)1+Ic4 zI+R00c9Cm|AFEh=lJsv>I^TN*bo4_6>{&1CbPE|7u zHVzf6G!im^!uhUYrp<|`{V+-4cv#1hF_7~gXnR78&_{ia{fpPH(DnoN6+E2uym{|f zv+*5XucE1Xys&JTA&>$$KZ17xwuVg~3P%<%UdUK&JcsNn8cCubUzOa8P)I+;asYRVdnMXFK$FrG8b!zT>L(*|4hqb>(3d(OHCRN1LCl-sjtUHsRlg``8SKcjQ1nYe%!kOy%A(1 zH~}u5H+A2tp5I+V8`L$2Q)z~ir0Puy-t|&R(u_CmR$qaVb@1F7X^m1d?^mNAb3Psd=dR}nAz#>>C+5+8?fxOP0`$Z#Reby(%o;ib{eV<5|bpHLG z)hb(i>BoeZT#{LRUNl>gOjJ;TY68Qlg zyr~r;z6vA;&X%nP?DM0%g4Mm5xw=$as}=<6{v|ajy_4-)(J#yDIEN)GRf#yZ(a~0t zaabjGg-sDT?Pdr;(@&@M>;{FX7Ls4}n*9=S?7T2LvUZ&M!6I)|xKL+OtR`D0v@^*& zt4z@gtf{RY5GN`LJZ%XqGp?Y6zBSpo^;q%yev&44J9-Nr?o!|2$$j^*0dYLYu6_o6 zHoAQ5-Fd|7J(p`hEHtkX{Zy?U9Xxlcx8$33X47c)3*A7@LVWul`V!?45EYM|KR()B z)$D=Vn(byyuh6ForDyCcjBf7Za`TYY5`MvQyKjV?B#fZ0<<~oae6Eq1o0Hium%P-@ zBXbUIJr`mw8~7pXuoz=3(LW1u%Lo`r$8FTN`8m zVw){z>xd;~%s}D!d`Lj~qyGD!6Tt%54M%5t$lNS`L0LS@#KS5&0TS<2TJFV9lzL2w zdpY>#CEZ*>^o!-^_73H+`@7h)=?>REe(Bc^LOCA^w3}HODQaUD%rDQ4o^9aV)&rvW z{O4!#(wQPwMiRKvzU?RZT~&O)$lmnOX`AR3w({P8aFmv9Yt1B7=+BrhTG65fsW z*&g!D>k#n8BzN)pxqb+e`b-XX*Ko9l^wi*mq=SY5MzQs^g`|Ihgni{>h9@Ij{*7U< z4Ho?QAaYAaFU2{w_gS)iIEpb)?t^!#=XSXP80L|8TtV;HaOY&+40l$oq0ayuGd2c` z6~cBa3*eGb&j=BqvaTUuZeZtb=({FEI1H(=0Edu3Ky!r3K;#fU6#eJ zzID%w?e2!BrQy5bcQ_Z4`U4q9G0qDnKzVWa;)|*tm%cjR(2)MyYViFkzBX#_%9_fj zz(DQJ@aZOXKrGCcuh1j*`WH=|vj{^9DiGEa|*X49Ol`^a_XaYz0i)r4C8~wkaF` z279}0^j=B~_Dzp&jI!MwvTZ<-j$OyOchoKk_zzt2`bpyGry~8TZqxY{nxM#Fx$yeO=9^G>y);jYJT3{<-Wc zmClWe!6c4}Ro~A1-ei~}z>i?$b9!2DM97CgQ37&4Xuz}8>}C2X3pqJC$=%_xp(aP& z0+pCptEi0@+dG^9Jz|UF1l5klK7 z6P=0fh;ygrl=m#%*M=s;EsEVd0NUgP6{mPen8)uS(s7ZC*rEfh% z@|?8+ygE5AcSNxb%Vz1z1GVURmcg;5%6E#E0RyWsM7~9$WMn_5;hHk!ymFhO=4&>@ zTuMxlQM{1uMRW^`Ez#g4K0FTKeCAuQjl@-rjw^!)J|0js zIJ^maP-LU8shYp_M$u)_GY24YRx+$J>Hf}s6j_&?OFe+JVbqzW?VT)uZgHaCfQEPy zo%P3UY6i#r0itgnZxH(O8f2y1U}9g}vnpE6Pz8ukJBcj~CG-APRedi`__m)|V;~E| zH6=PugojKtu8M7yfX#Zo=0vzHYa>N!nA2O>R^&6(ugIE1XuLUxSzRS7?Yvr8yE)2a zvMHHA!G6-pQ57wSnY3T4w20^d!dx*#-w@MIh|WU}C)@%D#QELmqOie+40vsPdI68F zL!MLkVqr4jcp&GY=g_+d<{W+FT-G*8#yDRzz<*eE+VAybxq?wwV3gUE=HE$WKv+ba z>xiQVpX4v@KmyZcl)Nr3If)zs0W$lGO{be}z5ul>W%g;F6m!I0?1Kq7I2G~<3GvnR zNYf(($@!3m9YC%Tb`jx7CFP635W&`!rTB&0dCzWtR@?4j1HyxOM~@Ai$yea-%}1Fg z;tw7k$QTlb9Gr@WSrDV1pZmx7VRZ7@8P!BIcN!Yq%EFk2-I62YXai$7JkGTN6_@N`y{BF8V__07)dPpJm91G2)AL#b9n)t(}MsfI;> zo-G$bzqzc7zHmL0BB}3RA_X9${9h&YHrh1*O7sE6!wZm<=2a=E zyGj1Q{}s}t%xi#bBYXuA75;~G@Idfi=T3G~pU4jXC!?OeaJHK(g*o?xL&uSHc=2!M z{}!zof8t_+$NiFCfK7w{ExjbMFaQJjdn<*_$#QoNFfHR_XW*^bbzb_Td$S|2OjGQU zv&v0d8k7FnDc;<}e?{{ZQ@7ly(5m1+#Vxn(H^6}JPSpOELl~reRZrxpP3EZI9kEyc z(wlr})PCD#7RPj=QvJ{Nvx4luJNTdNPo4?LX#cZ)vys$qk?GI#!Dgk+?DpXT~^ExwZ^_?3kGLcX4Q+n10VWQz_C138QqL`aRea$&5^F zdi&X7)li+_^*$c)MsaA_Xl1OEJ$Y08&K)Na?#@Grit7bae}_xmZKq%G6Xn{G^plDI z$omBYu(C`)*9!CUHd)?Ie3XsUFD^2{#N{yYrf$XiReWst(h$^6XR0tz=|t7Tsea@z zto>KaJCE4GpL%HEcHhvDwM%eX>|D34t*w5sX>9Q+X=&-DvjjcibICLsf`WVebW%qS z0dV+gu!8wU@(lXynjM_M*r#oNUvL?ytn&``d2SgeahcudjS1x~FDVDn?7`#NF7Pe(n22Zwcn7)OL zt_hhcV9KL~5jQU&$RVkF4IMOG(IoPU-W6LlUwppt|50V7Duqlc1#aDv5(9VS>rSoP z*x-`Be*KZ99NBPn-dP7JRnmwSq_lb>0(l&t1xnor@N;sb4&gKs!9} zwlIskWLP5;aZLqD2KKaSBQV>JY7qz(d?y2)2RRZ3**|6ay(J;6%m=?ePiboJXUm4r zS*{Geg}zYOvuqW>3=}v(5?C=B?w|urAv2|31*I+;gQ@Zn80TMLya%DBrgrxl>`D|} z7+;=Ek#-*&-{IyqcXs%;z6@RHNY=Uw^}2_?6a6G$V40;#H4ysq#~m%Lu7N_MSh`^P zy|wYJjSbx*9esV_pc{OA@41AGzu;zDVib>ls+gI<=`+x1bU}eJG(R`@zQNa_!NI+8 zufhipWM^{*3aHm{-ub3eLp>Ydj-;14S42g(T4H#stE)H5qq!8rA|kY%A8VMHR9ES0 ze~&ucTW`~i11|2ZPwKBdA%uP~@jcpanW*;j;i=D~M}#Q)g8=gi8l9h|sZXYCq%4S@ zbeo>nQVuOI5lp^HHao}Wz9iGG99jdCjCKOE7nqfspy*m9;nns5w89P|G(Arezo4r& zUL?VxFQLvB=rL<=pAhJ`YtgHSmZ7Ee8LYI<_cN=l#MIJ`z z_kXmm^|r5HpL=-`a03s#ydck?|E#Yv8&HTIkih|&QdcVfGmPiz8z!ik|3+|&F1TWJ zbhJL%b~uZ0YisMnUx#L@43FquZE$!#(jNco;8cYA6OVz7*qm|@Ja~DaFf43zHg*}< z4CUv?rQm-U$qCR;rd|t?R~8miq&qkrkopq+rJy#7P4bd20wM2M}k;S<9v_=kI}&nANbo}~t(WO}NW{Kr@z?W?vwI612L^H3;E496v{i(Ci z$uyMm_UCL%6`Hmico}QdK@%4&A}q}HB4T|KOI+&b4`~y!3eYPuFfrL$F2*u5G4Yd* zSofp0>%(HON1JWEIa(pO`4C!;+sPfRwIHj&Vl%9Q=SqUx)Mtw(Q!KQwh9#MDsJr{Q zL^2(ROno@FCZ4H#slb0{NjGFt5onQ^KUsBTAPjOU^+*h+ALrOJD(asQp<@mU|G?Bc zs(o&lvLuU_k1H|Qd%)Yc;@aWY3CH}cGk{K@Cy^3E&xx+|+VbKNA%sdS}e3c2}6q14Rh3~yhblBu`;c&q7CB#XJi&dU0LqM2aePkErihj zQOBxG%J}>RmS)+a)g}fB=c95S1wbvr0C(dS6m$gK;%ZW@ITymu*B5~Cfx$t2Gc&Bq z)Rhp@5eEq_X}9f2QvA;;_AFL9W(36FAA7il87wwiAB`8UhmKIjuFlGwG?)s7B%b0Wzc$gJ9zhvo5A%{7uj|1DX$VZ?2`LCux3{PLqh+Hq}f(YnyV zp50s{D)ZxXjVSQi(rr2l8#T~PcDE;}k`0qst#(>w4XjwQGe7p#JIE@gTsB*l0|I2u zXEIrEf>+a3^*h4eCj-&K$Wp*rx@oXm>`Zs(;S5M@!PdRJ(}?jFfWo78Fcbd-b;I^C z4u-A3_%0gLHReknjEIn#N{?k)PC4(Dq0fazj3H4^);SR| zF(|lXQj_rny8)8IXfDHKT6(*Uei7(rRgb@mw7N z;%Jndl5qDj#LdZ*W@E+hdzh>>ciM-AJ`bRbkdR&wFp=^_zieW-v+rYvCqKgI%nlGM zJ$VH`=vg8??`clmLdSBoR3yOj*Uk1FYGRhFm5R-DcJEU4=$-HUH8)$eM+?EH+D{PQ zX>R3xX|z#SySIz)J@<*G4j=fKj11BjYySvW^;i+TbLS6l1;_$v5{W!JBO^MG*C97V zGeR|BqSC|OdcFn-+`!BfJ(~hCp2YRTJ+?G?zw1HX1pH1zc$QmwsYZaNy1L))%5aw= zG3EXH3@`9`WrG;B)A&el@79lRf$4id02=J?mu1g3ch}f?Kn3_qYM=8kyfs;Wdb>3XWDj*WMlcMdpi$?G^S4VQf|8LY&2SsIpUz`=#;R2#Y1B8t-& zFCenqW3E^aaHJSmxF{stl9c}Lp%Q9;WVb-r5Vc2vbk_%0-=s7>p!!`ToIZsEfHG&7 zk)2&Y_|e0M@8bcNv5+GNgTdRK(C@vyKv2>ONGm6;%+8)~jpe^{=No{~(qPvA8}$8=n$uu+AhnvkC* z3!++mB#2&7TteZZ+l!_^frlO0_V(Fse*Mz`;jH%S zoeKeGROQxQkO~a1o78-1ULA^_e_8 zEUoKcYVNyxfvxXpczELZouPx{1e{?@@cZ|fqucmXzJ&QPnu9P>nr+fiH{f2dBE~0E zgQ~|Qejc))JtqYJ(Dd_^m>~IfRX=n~B@R<~F2y7TZ#(|YVD1te@KBSKB~}g@*hPEz zAM}zsCdH$=6m--~V_IfcR)D3pITvr%s@~exCVKmJqYnj;tcD2un4w3+3mP#4_3v$5 zj|s*TNF(V}dG818ZW>zOPTbtwoDCT*ca%(g6sYlvv*E-grCg_Hg@J%Lt-AM|)OU5n zBQ-VE#Kh#u6Si$nQA9s2Ga^>#=*Ks5k55pGfT*yrgN-IGGax*2-gg7m0}v(@eKxP% zxG`j|O`HnhGOh8i0BP|Fzm{4gFYWkw&cn&m8~+V(SK5Uv?v)qG^vc^FUSmRC!*&&g zDYe{{tfJMFakp~vo#r?a4cyR~Gfq~=6OI*y5 zL?Y;+K?T>1TP)!n5$E#;Tr|;)6>K!{sEK5?TAH{EfPTu;I+pK737&=Op$ey4Pm;pv zQkw1(h4}dRb%1WJkZqS(wMB8t^F_~KiN}X*l1?{Z3clMn7JaTn{E+hgiDT$k8c1;i5+*jh}a?IsH`k6`yK6X5DxcbWzu$ccB;KM zu1Na~KBxQnJxl#$v;XQSI*}qWMpg+4QbGzpQdgJtD*6F*?cp0&W~gVo6C8C;QQJ*O zCpCg_c2H*aLR7>`Ee($q8cxmK?xpoifi2PC+2BAwZxH=F?;tIOK(iR2g&7L1wMv$G zw6(#$=#;2Ute4v>K08yg*SRU-Bk_&P&~OdyQ7x8i)>w;x*})a&a} zLE-$}s=ZN%hsVsXDrT$=I?CSXF(c-jO{lojOw>yV4lg3D9{H>H$Wr3Cg1m>dfA0>d z2e3UBU*$H}3e2v^nQnk`zc2I|EHc?erk|K=${NRGu8y^f@E&Fz9XWFto&MK~p%!1eO|0nJYsUZtvXF)$ zPA0GYQ-sNGl&~!j4Ewd?s)w~Mg>%dBp`Ynlao%uVTw^rx7={8_Z8u}2VnPGm;Q1Z$ zt<+%p`FQIsb@#!{xVS?>4xl)^mOSi6>F*|VgXXg&%fu@1Eg%tD%x85f_a^Z`xaqnV zBD+rDuG)dMb4^6HYrue`nIuxK&Joi6@oe`vhv42^w0Y3cVrMfpf#Z=i{0*rgc(S?t z_-XlO##SfkNaISYT35TGvHBe5{7DGSaSG5I1V!FM0e1OJ)eW3crt&Z|Eq6%tJ$_vx zg+IqAt(SAE+6FK|q_<1w;Yv-Gephfg+-w`_wt0HJNz%e8N28jg5a3N^fOL=v$RDmH zr){!?+p;=ts~|(%&gML(Lpk~w#aZHm8l0mhOQfM?{SSy$)v9N)#zxx z+=KeN#fy_8zKR%3c(YV`#PO%enEk@!>K&Txp3(ER0~otqHd-M zlfWx)enL5eGoS(Z7``Hkh`dSxEK238%5{0??HbhmgZ+kr-h;&^+R*j`74N1TCt?NPy8t`TC@ zT)(%sAretJWhO}~L}VVeC{CFwMaWPxWu7xDqB2!7Pg^q2bLOPTyk(vxo|da0>jl zfwxh+VsmbHjl-{I1*~7^9?^=uQxS?x7BXzm)LAbX{)+?}nVzc3!?#+7o-X$s4pNfC z7s_^;S>i=#y7DHfV})v03u{&d zM!utpg~{y9xSbBW2&WC&T&SkZ946WTemlzf$J9&h28uVkiL)b4g|-6?o!x_-iaZR# z86pel^6BETjuri`RF3N&A^Pe{_DqC5n9fYC1|hK#TQcl)DA~q|U#mny$A$nQo5w{} zwAziPS|zub&paO=Gj~|i7JW#FkI3cNp*H>hK%*AvY9`QT|cBdRiXL9d&iD#%vtS$fF^@w zW$vy^4@@?0kaQXj@Q9g*G;Ez3xqajQo}|N0yM12b$Fb))l^wmyn?gSZtn$jrZZ{h) zR5(giLUi!vB8NbwOXnRm?K^|ropwpL;LV}?>z}!xa#PlEud|VRqLZSeLagkgVmZe! zyBVNf-$RzOwuta=0?U)#Sz`OSIE%cNc6ZbcO@3r!r!luDW7RIya&Aca>tNidg`}LH zKD{O<7ii-V(hwU=o8kBKO=zWv6jjCZ?v2Kdu))sneZ}PVKEfB*!>!a5`^|3X5*$r7WLX%H^Y@6l8 z6e;s;n#S4OT}>VA>?ZB8iQkS2y>G9prT5OHouq}dDW}}O;gLW@choQVd_qlvzj}Nk zn%ucZR9#hyv+@c03YZd?BsQg5l)vyHTbf?MQ}$1)#@g`ATKgRPFuD}6tMsJ8vHSLid%^0huKY=8Z>QJ_y;IWS z=kd(R3DZ|uc0#}FHxJ{O_?Q{S&giR$Ea0=$RTbOccFiXH6%D=A4+YbWUkEHt;O*TU zW`Izz^bAK@e%NsGr`49YU-Z=Nu(n}=KZ(a`m*!$@rpr0AjDH$7F$>&i59Bd8!c92&~skc zG}Mr`Y3#bOwB*=jUb8xgSER>VchyHiw+-ejJ*w0X)YOX|FJZpuKV^25Q(biZZ~*T@ zQbx{Pu?^1fgY_3@&8~ai#j1mTbLFKU+;7%Kjm!j@kl+E~lKH+S4EY1AcS1%j?3tQh z04Eh;5EM(YV5B~3)$Bw4pvWt5S@Y8~PXx7_k@t^CXKc`IH=`kE;Hs6`xWxx6;Uk4O5%KG&ouj#1UoZ7pHQr1Oi5E=mQkS+VxaUz=**`k?S@)ZE-@G znJ_`gcQ^M_lUvnRn#C)6embKP*E~8cN>@FP@mP#URp`yfk}4h~^a$~mg!sOWiQ75^ z+>1*~^uyeO=}!mgw{o3~y&8B`rKnCuke2r65{lMYnBT2DPA=f6rsu4wc9P4b`p9H$ z@$v@}^U1O=`npvspS6!;jw4B&1$HShQA4eHEeHV%$r)B;I=Zi3>c7*&>a`9&csPv} z9(?fS0ha}bqu9>acUyZkDDE{mfyyr*9?+);E3gX1#^IZFo~P8P+Mbztq6-#1}CEc6I@wEoJF21IIV${$l&Mj~~l&65{82 z2Ze}@${lu@@;1kwk((+>3%BdBT=kH&UQT$!@waK&?QqeNf9>pfDFBKi6%tcZQze{N z5raEP;RQuQ`Lgk?jXi}tKNklSt$%!*8Gi|5DM0UU5Mu(p6dFR_H840)axLD`7nRW} zjBIg^d5rPLm+{kgirqvWb-msd^LKP|s({OUYUC`fh$|8(EOOM(l#N%P)^?RxvfHEV z9}-fIZ#8=Gpe00he;@UCvFt7PI?jmDv%iv^#H^5SQv{o7?bw7g4KO8AKZKP;j^Feb z5g;m>pNGhy&G1b>NQjl69-nNYEbf^vtoM5t^6-$+%+3_Q13EST0r%JVynsGZeNuzAQ>~DY2g5kNpu4!FS*qSkhqHN zC3(jiT3Q1U0w%ITQUC%EJ@)tUsfEH|se^(1Cz%(|f`Q)2WZ5DIwZ!F4)^-22E&F4@ z_}=H5|ITA60U##J8irEz7(Yn${gy2`3mD?F{p{wof0BI&z5XCO_m6-S^;h7%%lKKl ziH$dgQv=!l(IC%o*nj?2ZQbv@LE``94GawpdlySbXJ;D^(uHK$-wDPMh6g3K>VN+o zR$A(Ix04MLH?;#=EGJGB+D_%Za;#h)Z{lF76oVfV@UXRlQdt@&171?=X#XZ)3m+9d zNK2KnczStfl2K5&=^b*bTobbxbm_cW+0Z-Dn$f}s*|gsEXOyUO;{QmrMEzA+*}y7} z>Xz~#IsH$v4Ft+OP)CtI-G7~xBKmyoOGiiWf4ygvYt;JTB&RAdDJcR?cza*csc>oi za2*ug{z({^O&vqr(=@|MOYlh8{4`zZ(P> zK87^$ceWUw_q0@?w1>Te1J#iuP$+P}(#_SP>if_}jM+d1)y~WF9|0+amd2U)@5LJ1!e|RvlwvDt}ye!ts@lkKEn6@2hnG{`>Er zJRt1(=j|&MJs=Ma*Q%=rW5P-;J($wV zm&1ORl8!QC)H06z&R4rs!mu62!qgua7yxjAg^5X``{WrmwxcA~UtW-NsB5rG5pn*b zt47+@(7)h=LQ4T@D`f8f&O#iw zs&6-p#I>v*-5emo!)2LvnR@kp<{4G$N=+pW$Ils>HXew)z2emrGK+?y!e&gv_( z%=G5BkU)}5Ov$Z5dz`8#U%Je0&*l@~t|a=^Ys8t#_q1q*Ez2d2`;rd%eL!X-b7jp1jmZuEHm!A>8L#NHW!q zWW^6;Yde)abxm!$|HLk?(6nAOT2t#`y93p~B{_Nv!i^q-`NnO4_SlIIz`sIfz1NPS z!V(EYg@wCyd7nOgO0mO+xJc6^YlSr2kBQ1iplezYhs1bltM|tU662NlHrEApc|k z^_>va#wi8h1OtUP08UkzP=&Fm#KCHN40CUki;*%ho4oW0(Tgr8VQ(`&<|jgT<-^SC zbDa(qDPAqSZd@B*>HP2Fj`|4Um=opxOSS9c&!0c-W;zd%l3D;%GEib?(ck>xy6+|J zGNU_hgM$;=rd(WHV0WUYr*D@}t*)+ylv>Q2H@fbeD`t7QxtThZz?tAJ&ms)aa z>YbW*(2-+fW9^-t5ekWryUa6eL2S!Q4vx#07XgR|kwihNkA;PT{QNeMsF~Kf^Tzo@ z{}{~2X)VwZm2ln6w-#`W)6Ux4UT#TCOH(|jA|q4%&6ftJJ~B4+e24mc?9r{Qm0!QU z*VmW0Y~&^-aojc%B{7OtqM@TR=A~85d?3=DB+Kir*G!jl_aH(&d&NDsa-R^uV0(yE*&E%$;r7aBI5WG6&)1Bw!kSVscqf{X_+NlgTKE& zpyeQ`ntM&{-aQd6JJD!!Tz&Apd-tphUu~*Gjv3%gb5qm)a_6F4dC;v@S65$R)_e2_ z)IXDmjW+I{k6c+>3yo9nd04I;8yh>+-Tid<%L|Z=rKj+QDKOKOC3pS$)XL`i`Z_#s zU8%71YI+&Dj?Pfr!rHIl>YF`2QztRFH4iXketvh|1Pd&OrHfa4ba`P@@q6M?C`SGB zbWs7b_T~G-Opi@uP7Z!M?tF(WgxH+Pw%a`Wlw__Z|_E~zw!JJx{A!jARz>owdvSP#(a zkQB2Axy{uzXZWZgl&8VCM6l&uR+fdGUBQy&EK8Pk(Dd|lZ?DD%E&J5e6rha3!NK*N z_OsosSnL~EI%oBO3cypbE?w+^3lAl4^m!V?4&t}Se2-xavqu<%*RNl%>o0Ra;OMn_ zv_*IJRfEanS0*148x`9>)z$h{pOG4LOtd-I`bKW`7n3}J4)t9U?lLfb06Im$h3`3eo_swE;}f8V=={_d)=JuHx}!PAgBWta8BoIFZ6K zH+k}ekdTm3lG}%h#eSXZ5HWFkd;8eaK4P)dDw)8?;oLncyhZg{9v7#xm|u0a&t=&h-xmC>KFb2uO(f?mYpnxT-dx}-vadhNGw$nxyz zgr$*{nyOLhR(=8#%B6lsRn>lVRz30+onW9-HpPwdp7GGYk0C&<%+784aCr)Oq@f`a5E zfaF->NlF!=aBc-!M~A_68;N{ydP#@1=Cl6$@9&VC@n*q*u}(`(ecxcl=L&-dahMa^ zw0fhpaSA*mx28QTZLqS~w-;vKx|kql`5>Eu|SIy3%9+ zk>n3w2Q(kLGBjbRPcbreaDG|lJe+5swySv-b(V?%e4{fZds1R&% zeDY*^GREsxz|UKP8_+r!QIy%l5igB8=E8y!$+(s*R+F{H-(ySt6r?1wBo zLgXVG>g(UDx5${@QXPQ(4-7v{7Towa5Ifu#ztO<{x9V_Fj-QiF%=BFj7$LA6#>OcW zUj^fNPnjtgx{2F<;$@xw$!s?j2xW+*0>6tyB^h5h;#~W98>pF?8UQDs~nW>d7}&F*BP3 z<^_|vVpi8eUr|dullxJz1o1()Qj%!?j=w*`DfR4 zp)zJzb`(fyufr~_e(zoabI;`qZ|t_fxOhglABQmEU*CrCN}2l!YHGOdh^1`p*RLHN z0-T&P;6K3R9?e2T=KpcGkeCmg=l_aa{_|Q$%%FRR#E6Gy!NUUs;UDGR(PuAT3QtiS z)uKLjYz&8Mii8dvIbVVPCxBwksi<&!_N;1tepAF|98tMXB(@oEv~uVoB_Y|`+*}+# z4C>4m7XtTt9~|%@14lJoU0%5^c>5kz9ubkJTuHO9kQ@s2a|el)kO#z1(!$;5GbeO( zk#8XvBPQOz(yxI1E73-ynuO1Oy)k^AWD*`T{TCt1<3b~F0HtNRej#^6zBdYKWYlM3 zkCRvwZ#^dxM@$T9!}SX2rZhP$0#K;WTWp-a>NK<-1>*p{OX6{SWP180A~}8f z_a_i8qT=hoQ&dgAe;gJ*c!Biv;8`t!Q@1bO4}S;;BOrkm(Pyz7aEq|mTagPb+1c4Z zp4=D2{^uKEc!JIN=PE2Gqc&y6j$dPB=`e|%o5?$S_UyHC$_Tg*@&pHWhA2`Fn!%$Z z`PE_SAVl212@2v*c;4eyUkQ3UI_@>QGW)^p;5`(WE#ydr{egFhJc+H|lZB}0&&DBu z(0_anLfQR4A~GFhBtUtr$sJr9fdt@t|9lzNHq=`G*&z^ZLg0)-S$e>yWPFXol<+E|Qev=mSGHR?p0 z^3w7NIrdey7_gIQXxZJk+xywTY{<`{K}fHoVpgy_=Ua<)se}p5eY3BtF=y83DM+df z&~lN}mG1uxmKCq(XvOO5a>3Ut>Lo5JdY1iNZ1OFof$p~R_}H7xE>B0se72Ogt=1ON z^OC#66U8iUF=(Dxx!ai9FRtQN0oCt36Ja(7b`W1YR6)bE zbOJYrkE@_%TuWklV>;Ii+@ij1NqaW2pocCaa{pfXx1A}xzv>pt+Z1>b8_6t9KXd-! zWKrK(U!PBjCA%6T_KpFT$HCO_v8*01yq(qZ<7i}`cO?T0v*bq7JAU|ap)^k>c;*;} zL)T*-ZEyE9ZY@zkrc>6UAz|j!QkF6bMVXx>=TWG2+SD#i(9*Z+&52AdRqYc6U2kaD z)peg&y}@XB%@%#r?G6e93R$KjgqEzFB94KI9);>_rY(e}u27lme1dun5+@TR=ji!@ zf>bhy@CKa{MEk~T^U&m8b^(4tFghV&M3c6Bqm>qtnN5M-nz71J+sUUbUgE{E7<9|d zudj7f%28301R7}_P-kLp-cWNjx>+Wz!~d!WJdYa5WO2nJop-w5oJl!a?;D%|LfWme z-ei@Gil)7iwI}42TjHGF@lNIa%!Fnc@A)0HB-Bc5%@e->*495SOH$+0ZV%;v7D^mw^L zjuu6fPCpJAWvB{C%5^=Fa|tt(u>WCwKaCOlq<~zF-QYsyqjbAv?NSN02Ho$yk+g+g zM~CrDw0xxeM^@j6SP4MfQTQ% zU41%((kt)InGEIJv0PqWbLEh#H)YfOyX;4h8T{NL0CLR*@WrBa<16L_ntT*=n%kR0 z%rMXf>alFM=)U$3y+lSmw{YD!^jZSlJGEkCV6}XvFp6H)0=1oRZnp^bx6Vh!qzA4* zxDie;R}=USS@I}eJ0kqheCb}5@uE}cV|o;P$0T{>w4+oO5!pD_?s-W@By(DsJ5@*5vUuZq_H zC)VT&{eq&px7Rv%xcAr3#q?XKro)u`OH5%7wc`-kT>p$=@6ue2E@wRM&Sa%TyJVx8 z=)~aebZuAGme-hFOOPw%E!8^lhgsdk&0T@iB>l2WfP<_)o*A>JXT0d{`?BKL+vUm` z8#mfd*|RzeAv-3iUh`tx{V5`HG>PwCzkM~cddB7Mo3+}pQR2y0j2JVX z_H|hfxh(DZl-Q&-ih!X={qHXq$8m#G8Rved_82E-i{?S4oku5E-+O)D`y^HrIfPDF7Jk?>z|up7=5A5!&@N1; zNNO~&cK6KN)PbaJDJZ;*k7w~ZCcTYa93ALnStyir-nOY-%sGnf;;}Dt+1Y-`Mc}kV zJSkzgX|o)#r>e7BsXV(Ln6SWzL5sUARabK`eZ^H9?#6+}_39KARqXbpeVj_tVmz0e zFGe|vaq6rQA%2SzE3dlH9mMMXO_-mJ{n8ti;CSldxUt>J=A{GB4BO1G@rqm z*hf@`lTCPceBIZ*bZwlM2w$Ng{FaO@{ z^23`_>QIo*`(&@=xE7ZWL-TK++z_X->Z21ChvHFC%=H41Bc~c zh2A9Ce9_pn1Qg5@`HJs$VR=HJ)Hc6H#>(6kLP3v7<5ViDc;6=C_SCXXxmbET!Bt0M zYnZ0tX8;Y_K1!1BR*vDkMcLAhe8c^~impJG{(#r$C7A znhDWd32{!@zI}Lcq^deg`{nLmlIbW38+Sd$T9bxrZ)wmM#m=$ZZio&Ei4C5KeZ7-A zy4bQaDyJ=8YF*)llF+T$D{~%4H~a4nUt8Rh%&B(L#{(4#9ZY!wlc%(^6ZFO1NPGjD zj5ZaYz(0O-5*s_QoAUm^Q#gikv>`^;2m*IYkPv8WE;y*}H7(MxMC}F&s>J!QVD{d$ zEqwx!cx{)xU)U;@Rf5eGo-7@gz2u*1lyomOE4bEW#@?c@g)qAJa{vjrZ1m>J94D1_ z#VOErf+r2!W?lNuB3Ws-M#Fk-h>D6iCev-bV_A~*>r8FY_8Is%Y1eio2*h^=xv{#hnmjL#@o2EbWLCH)tbi%;$>O|MP_kgYk`P`Esmz6)dk$Iyb8U9Y%zJ0P z@}QZAB!0d0daADXKCs=o>%)Y);=tT<%@%Du5EDA1ByV`A656^pQ`yABLhhj@p3LR< zj4{YQk4%C6BNxbbE6zG(r|qQ7QV>MwnIaCzGUE6mNH&VVDV;DMm+zJ2wo^1!)J_v{ zTB>2?OLU0Jv5I+@uqa4LRhC{DT~G-3)U?<^TI;8*eVA3W^4_UySsT*{$40(o)T6I4 z->6vYb-HKQ6Ff0}vc8i|*nY_T_CN#6nVA`Fke0gdALJt#KQ}TW(e*^bJRm#S1f8*KrNFmOJ)J>qV$1aQ<)zap!R_as+QT_w+ zS*uOe=te9LGkc9#1cB>PD@ehOYP!D^Ke4hR&w5S1;7P)-&xV|tWxzfYV?uWe8_%G$ z7USb1wnwe>iEA@%meOIfSc>XZ&S|)a^WKpeEakRlD-28?kz@VpD<%+sCD!Rkrbl6v zvt_M&8qco9@z(eOdyq}jbV?eB34^Z|P$t0E-9r@;VO%k{QRXjq1H{A5TKa;=c z8|4E!s1Sy$Jyq2$D3E6@Rf6NPE=cvI`xVkPU4D+*86FN|iLtyDvUguUKixypMLN@C zcPp)!k_Zova01h@xjuhGX(K84sw3jZpOz{u=9X973k_4G8jCGr-znjqQrnn--|n>S zy+GnGB+?=1eP(UE3_E3(Iacss$B!K4nWecYhF|YeUY%n}ero;B+e!CDHEPG_M?jZ! z+3s#3-}CCMOwGfK)oS|C^|=eBlB}B=slmq?*$+ia3!JJJU1BzJ1d)v#uF}FKGdQxS zoT{^mseh$$nJFt{{F%ZQ9d_X-0}-AjC~z<3>oDzPf8ey=9#$LKn%cF0t=TySOjx{+ z+R$49#G;p5Q|F`Mu6{!&4brTzaRQA>-IeKV->Go-8#i~AO*-9j4CZJqr0hwsuT(am zIoS7-@Qsz}l%>f!*%1fr6gvw*Wol}DZLM5V8=&=Jn}8hvI10Y*(yw0_qAz1-goQuF z$IDR48yOqFr2&{a)#v>APzDMTU;sQAY-^*Yp#kxvk&zK6ClV532LBqN!Ea;a*!nY* zR7$??m3bNlM(k311BrQi<3HD+FckT*_t0c2n-H%Dw1c?O+R_Yk;r6;lax=T|U5@~+ z()IXk39{j|0N2H7Y6gv1uU}W=JKh_%uB{Ii39M;6Vj~YITD@RMD6X}WojHnAJ81!i z&T?s(O1gNh?@*DOOaCHo=R=PG@#2N6v2!;LZ%|(;3cWhwKF6?MTr8+VfCdS6jva>UZSdRgk07yGh z`xP6@qDXy?obP;BXJ_vNZB5OY?7tvCW7rZ?Hz`gl8-JUuO{V`FkPfZ=^F*&SpmvJ3 z=G`qdzVk|KD{e%)cQ1XhCXsIKTB}~83O>{-Lo&(c{xVB=2h(TWR#CxH;U2bHJpI*M z&fJ~drtsKEXehT%$5zThe)=e>V|9Rq3}SnCf5bTt)N7ZyEvEk*6CwXtsLhanXI=CNZrD@T<=Lkh7 z8SZ|ob)Z7x-$XNy5R+Inl4$(mK`u6xJcKS90Oahju z6D(23k=X!FUHtWV&@l!kxm_yC%618}q|4C7)M z^jKtHuGd%QtvUv>R}v6Wg>d-?$$SW=KDo5NGfh53(oaP;6U{X>HS6n739l%;-e72n zkw{BUe#3UG(ID^6>kjazCX9unR?=PS$2UC(zTE}bVPOPtgJr!-fljV5EH;i1dJZqt zfQ{Qt8Eb?el=ouSV&8N*&X9_M^jGdL$EIGML8~LAd!ocBk2*+F6@6is9}T(WAB2uB z*qi;4V8thurM`6tfq)?2?TecGCGGJ_9Hrqpa@9^uHE_!EX>qGjK6(g~tM0hLyYL+4 z-`4u`Q&#z-EPp2t?fgYw_(d+C!@b{i8@2)nc*d)#IbcT^scx~sy_=vq2rtpWQ5%!} zQ$=B0;)8`Qf;wSJxobCKRIbBJXD;Ja9wud;ousIyl?#pN=fLN?Pk${?JAf5T1p3 z7}qK*ok}fNW|O4*9@=BaFVsqKByG1v6zC)yns@s>8_Zp~?li80AZJog`i2A!ke1Jn zjMOwRFp!avF)%o%4;HEZj(+q?9g*3^ylUzbl$Rr9n9_%y!v63zWuZNWz|7rOLVglC?x2FsE5UC~s?qBqdOA?%%%M3K_tqu{t=b5kwV~qz?xXipa{u zu-=OH*3{5oj4{5|>>84smcMBoc~PQU3-C8GET|hdTwvrAm9r=djoApj&iq!75fklT z+WW(eb*-qrZ0MqmGr4_-SJRfCyTN{dK>*y^0-bJQHv)~B<1tPpWq>S)D;%}cdCxdd z(vRa7!X1yXPb|xXOmesayvy7(9Y5M|6hJ+ROahG`SghsLj|9jzlol7)nE(aND}PanoOSY3mKGM;n`|ArxNPcDXn6T3J<5Y<*2>MsuLpnn!UnnyH8)m^ zWI{OR^uyLouv%|}dukoB%Ag&>3^<%*g)>e-rEO2Y^}`B8u4*jXU!dg`;-O;`iBN52 zzRM68+(68PcE85zyuw<{uJ^98|J_hcytTaH$iR985o}h!q3vsg_;pgF@EF|E1b97q zbeMAU2j90c_=-fFGBu zh*u9e$xlop?|$5HX(a$RZ=bV+42@GixOqg}OjBlZt2`;VRzH&B}{;NueXw z?$B{_+ldm}deq23ExOvY^W2Xv+p6qU9?TYUTyyW>xi4{!Ky*I8?lIp<(}Igl4ey#= zjbU<})7$=4^p`uN7s8v_HO%U4wu*Te;Ay7%4PXcsTeyV6>-o3wZ{@feGK-v^;D@*5 zXA}~>OMbLu^cK{X{>DIGPIsY)yd=Bbj7!ri5W#TaJ0LR1I0a71gmF zRmXhS+Ej+?e$PlyKT%t*6(Wg=d%o7stITdxn#*tA6S@8H$!4YB6xoTfTwI&g5}WI% zxPItF`U+us(A9?jQ=)Pvv)wiZ&cN^%Ps3*y+tEy$b0E%ePJ3z~gqbOO&-Yp0U(-R& zX={&H;+_Q6-F3V2qfL=B_Ez9e|E-pj9}({gjMhGB4==nCLZBhGF!roEB=2};L|Q9X zI&#*irN@r;5MxKuDcdfePct%%Zcj{!@-GR`xkTeky&7hpFa1rJFMq3dL!+VRutdR* ziex*%rmsv|LA@d@&f=bgU*F^HVCK2FALAGLowopJAAj};cB`68CYHxk%vhZQs`GZ~ zm1GVh>+g!Q&E4FE;_{HV7Q2bI=XR->e#=Lr-wE->R~oPrCCe#QAy2u8@OAz-J$4^e z?tNBFrOGJ0F$`!{f$C<%yLjWbRZdJgc?W1i34xQn{r2qa(-=OFSKII>Zfvn;{T=9e zkt`u0Q(x5$1u2_D{Ll@;pJNgUdfh9ZDCDs|$a49@5%1!~k6Ba0TO`N1R65DWhHAEW z6mIvNz-%piIVT06SWDQ>L@Mbx+Fjk+mXm{(_#iStQUla@TCwvLMRzD&SC?+-RPgOi zfPsDeq3Bnj;y$#j<6+=zQC-&c)3s@lbMy1N&{vW%9uW!Mc7zjG6KI0HFL{&fzj&^eB((L&oVKZ^9Mz)(lbX$li$avIp-Ql;z%%{N87p?On!#wXI zJH{sj%i3yL`C_t4^u=5Ll1txdU{d078}Cu2+)aUK?xOc}Qx}THLTxjVJ*7hU{De4_ znADbj6pzH)oXg!Z7U9LiAE=Zrr1FO=;9|Fab&$gpMRGRK0UTqO3+=76ZpZMEl?=r< zwc2d>>@V>)0T`3b^|pd@n)LPjv((GE`PR529BfRc7v^oF2SOBZbh|sO&g%pv{bMhA zlue^rXn6MVz5V$glU>{0>*%vKyHU(_upL^vHxJzz8*QxQ+n*bq76CB9Ch}GuZ-nwO zBev)U`I=>aDx8my)*^DBAOp4l`cRyv@`K&ngDtTDm!wiHaknAaN8)+)H-4@uqdFR$ z)wS6Yo(5$@bA0P!@V~86s6=J7Ir;%~t29wnNeL0p7ZRg?Aq9*HEK}eU+W2L3OxNte z$tC*70q9rgNA2gc_AR(A_dcFt3#0XL^%M8Xk128C&m<@h(8c&4AS(0vK0eWlq_TeJ z-vF_IZdG{XBGGB~tL4Vw3qADKR(JjGKm4Wrm#oJ)EgTXp{CVI1J21In^DB;ey4skD zaeZIaz>v5>F(Hz%4X1SXP^QSO9`R_UBpaKy;^IH^i2I}Va4Dw>&C%{WalX@Viar+q z@}&21xe0yd-tWN?@4u^yknu0SGl=OCS#E6Z@hLR2motu6EqQwB4B|@t&Ub=cMc}k^ zq?;S^)Y3O)%O&OY)MDs%V?I5-^rPlQvt+)by%Rp_t3dBz_la9NDS}^gN-saQx9^`# zzYm8jG`u^61megfg2qGY54?m*6cauDG!FMIkIJH|^Np|GUzj5A2aMyA!ds5bN`1Uy zVoC$ia~ z6#lJ7f9(KE;_J6>b>zWl^6n4SG}yu)y677s_pUHrGdz%!I3C;``-pFaLf_e z+t4u6o}vP6fF5Yzsq6=yiFGtkcNY4K5&j%Rt{~RJshSS_(o(k_Cq2E<6qTe?mmVai zXOzK7(a<BD8)u&U1-K5h{pEBitE)(AV(5Bcl81|(}TpcBmEp&va=|#KygV9S9 z!~HKFT+Yyz;x00!jtGusnZ2!0kBWZx?hdd+-@Q8{XPL~O&-Z*3049*vVmpG>!`qe} zT17Vj`RQlmYB}j7XYpQG!CXn%$BLisvo9|_T|0>dt5!vbpKS~atdxKgiG@cybKrNd zP7xBf|7`CpVZKsQkf(5=#ZV?C)~VbLxOc!X0^%Et?=;F4B!V0tmjk^YDxE`ugSpjn zuO+o@tgqj_cMmr)(a@mSdES9kh0HFK3FwmiIVFUBEqIUEh3B&~1r~n;mPrqpka)lcv%h{zBdnR)Ha%4i~yF5>BeJq731h>9u@=+LiUzXpY)dh9F^ zvY>h%Xq&*L0@5Zs2M5)043Bo%C!^LgTPW9!C0sD4+9p2Fw1-bfNJzS=T`xPhc{H1_ zci5bQLyGF{c~bcl6|+O&R=n%(?uM$nD*^&N3NPH&P#NN*^?{V_Cv|rHds~J>+|3Zq zptpA}@qod0;ZEqwrO=PsR13ezulGbWG19H>VrN6K3pj!_CT zZpMidO*`LhQDiaoN&0+bGTp2TcVx0&W$(JIAso4*NwZhUdJ_*|Z)4fVI8q6BYYFF< zs5!=IvE@dkKi6?0!WK2_&W;01mYaLW+W@4yKzQb&zKDc`EAS~hI#%#gZS_W!VPBGy zI3RgA=OfV2Zop5}Yml1Tlrg(7&ji{HZ@h&P+rhj5ie!BOOs|ZSQ_1ERI8E^8gF=>% z8RD;|D>XI@SJYnpFznE|vYLi9s~BfgnhIgPzoVbz!UQ`E%$FYbl$13UiKz!W7aU!2 z%w`p3?K;uZ%5@p#YmPmDueQw2(3*BEhr2QtquU@9I#x(_&KXE``3 zbPHGP!1In!u#C6&Xy&jPzk-Y_ml>e+PXwoWFO zO^A?%iHVAP@&mb@yth^6l8r2$3gIX-o7}4@j6@lq*h(<4B+KkPfU{W3OSBKGAOR1| z-ASv%(3WN(yngr~P1a=6nVyoIYy|rwH}?Q&3BYrL-hhisS-&*wH!xkHegh(9>hn_0 zj*dr}Fu1=bZ_`yhWWkuZy~D&*o3UUzj;rs)f##21{?0o?{h?X+J8>0X6F)Xcs4;6< zT3TvqYMPsy%gOn`J~_07^__dcL^+YD+;I}S)bA^V zKtAG@h)_s^JzI}-@ud$@QPG}S#MOma=E&loa)6q>q9Gv?SUVV=W8)*);3aU@sS)#Xet)# zAWtL1f_B!#rOp;`OMj4L+<1lkSo?`fop%bUuCjfcnL&nKZ~ zmy&;^rw9CSKiT-Ea59cVg14QGW(}pPA0zE+7TNbUy@xOY7mnxg3yP9H^=}cH1|l|S z6=h}V=;}fRRt%dNq_J-LbV5Vn4#3OZzk@7pzpbz?l(lDR)TuE^4-ujGX1AEUHo-=v zK;}ox`B7Wq;7JED?*y!D?2s50w{xC8eg2BAf3>>|@U4|ZL2{0mn7F*5_tGFpdcOTl z?30UqtCR!jRM7)}KZGZMab>g2#p>~FJ%9BOHBHO)-cQ&bAHkO9fHrfAR@rsV%Nh*B zXb!8US^T8BPmz(H%#i+44M?gU5V%2e6e9FMRzVIDfCc3Q*C)K378qAhOq0bTk6t|d zD8}y)?jvZOaE{$tRp|@Z{cuD3X7N0m)i~}5IGr^)_)?5-%^pMWzl8984rLj81@p{@ zZENXrhky~$$y*E2k)B6fB|4R#A%Ox?K8X}iLW$eaUrI-vJl&O~|E_~bBmU9XBc5v? zvQriTlK6iX!~Kr@{(OF-W0;L1%0I6Q+c4B|zzZQcYWe;z<#xXr><6?*2&a&0V5A=S zDC!_=r2n5cppW*SIv{C&-U(E8>(1|U9rqEUYafMyRjU-id=NB31POn4qXnK$?_J!G zyhn3Oi%<$wQ7kPkI{MB%*4NjnbklOhWW0g0B))K9Lwi>i6n!H5ta(dQ(?l@xD2b=_ zt14aI$R=<#;l5$SNF*TkzfBECN5@J#SI|2`O7R~&cp#2KeZI?j>Cz=&>u)TbTo}X* zsd;2(WGE^s4j`30W_=^0qbp7l1oVkyP{s$Ah?|?co*n*~rUE0*Yekg*&$M=HjuJtb zs28dZq&a~j3`}8*+p#jz(w$j4l}{HRWvDg;kwK<`Bty0I>EZ~4MLu3$WqO`&Is}^; zY7*3#heu9_iZl4jZcN0)Ft0pEs0|8O$U)$kXlWIkAPP^3hywz#PbH;>`c@f4FhBUg zlnqlL-nzpBQVB>GgU@^KUT~{XV`C%o1#kfX@88~7hFYP}&@KpffGB>Tv|0>RXi#v; z4kyN)g`zYF7*f#*nRVcQfKU?19)YJ0{RtJ3)_L$A>}0!34Ll4C4D#~wASTLbF)}}| zee2e(+qZ{jX5?`mmp?#k6(7$Af=UJg`KE2shgrRx&igzN;@*6_cn~E+rRD!kbM`ly zw}JUF^3B(A8Vt{oJm?vi2@c_;60L-I=%eMOC8!Mo_Whi9gilXHuUk-Ks85`Lq6+0`(UbJ_5ulO zCl5l*sj6BC2|92uAqIc}=zrl|vvB6;#!Zh3C`E%pTJcX6Fc{|M znN4QY)YQSbQNh6--Q8hPQC#Q(7`u{^l4>8SG|)530)v;&cn{LPzBo;-nw z5h@uah%{Wm<~NWlc0MS3YIRU-29azqo0x5*hqqO&+op}Yyl=KF)M>S(@_^yk2a*2q z==04ym`#a&Ts`ceg3oRH6C)WCZ9y4e6(Ot}q|T}urFHAp&-h1>;SqE&%qyh-Sx#&O zEN;rT)tFjyMpC3Hh?Uq!Y+!oOA2K>{jJ4r$pfFi9`jUi%783^M089y-BK!mY3zBi| z1WxcVIq zM#xLpZF_yV+Gjtf6Xx>&kJTkAc&P3X^6mGSp$D_EryI|K;g7E-*^7c*E&hINf{Duy zHdR_##^_K-BQ~`scg}#1*4QW?;b<_?$S-Ed()*!@DZyW3YNYrj>@E9{Eeyu*q*oZ@ zTEW{jV%v_EaZLdDZm#A{Pmfhz-lKw? zxJ&Y<{3e7hB2R`ukjPa%rkF8cH_FS(o~{macLQSSM~H__wYs0w5Z%&a%wS+xn^Kxr z-s{f_zO2AhYcqlN=qhZqf0PC2ZjwwM#FpP3e}RziS($3!HD7wxV6f2l@y09KGZJYX zAFmm$UMtu0{I9ADx9U{G-)^s-sp)K$C#n7`5GrMtseVj`wV~k&i!OY)zT$|vnp9iX zz(x5-xQHP4S4H*}>`JV0H;s&vt>d8ntE?Y_8y?cIYyu62q9=e@zMRgpwRZv_?c^WL zhNn1&k>f|=kGf|J;9z@Z_=U*F!K7^%B2^gs*MV+vWwvFug(!U|2RxQP0`&tf;u8W< zHn!@=eD_pUL2nTH@O=ulx}M01D#rk8JE65 zhz!i!nSoq)Gts@uPSON~F)N#4E9^4bMxFu(pg%#EznOk|+_wGZA5A`>#A$da-M9hq z)!#R6yk!$^r_-RTLaY)Le;q$$6yN$?hnc|XCCU{dpS(h?(9j2BTcxwjfJ7|9GgRyc zL827c-j!!IBhm@{{&h-$%K|A3vsr>1R{jRcfBi3=q*H8teH`RKq(#Vj&)ul4sX2ie z2Fhx1zx#X6=#ysFHl2ktUCq19xlpljXi}YY#3!aQZs6FJ(fL=(sku>VNBLfiVo&{F z?Qru5`C3z>4Wa;ulYS9jN;>%ul-mmknn*-EY^B1&_luOwC^+M{AF=@yjH4GhGwOg)TGp6HhCZzoFy;d3mxSq{0%>} zh!zi|aO>o8KFa276weDn?d&f0Jpv+ZhTG3CUopj?M zb5aYhG{(mTTbj&bA|rh};&Lut<%E2vnl`_%)lYA8jg`w8vSMoH+~+~az<-IcI~UGG zDmPj1hw6&*#j2Q1N2eB#Kvx2{Jb* z2=GpqSWF`#A|muWQ)KvLDkQ`UGc~HL_MFt3`=GO4 z2vYF@GC7O|UAo!5`Iju^6QK;J*Y-<@gm*eOfejPsWRqK`Sej00pYGov#@&`}IPm4_ zV7i&b`K)8fIL!mnK#=(V;5QCB1R2^N)1jujQT;yueGgTt&Rg!u0nO92N2#A2$`@bz z88G=AfduQC5L4v8;ur=1SI{Se?22vUjR_W^31cB|3j0HfH^16KX|QTq+mv{Je~7=Q zXta-N8pAhKIn?sL9Ps8^v04ZtHS(7f|Aq`N%_r;&cN`%~p$R2HHpnPYv8dh|&)IJA zxDb6_3rdW1U#>AdH6i!wHR)z3xW77qxmG=x{JFsM&?Nh9XMGR*`);Av<^&$62$fq7 zmpo&kqT-aY041hu_J(YZN5k2K`36`=3Y!!kj&#UhP?KB3Z7^*dj5n9P!SV z+26I}n*NskRHL*C&kG8)*eMl?C`GKcZEGW%8ci=r$?*fG-RGo4&NkiaV9mTK@tlOW z|CzJNF_?UBO!Y-dIEiF~^PT@AW@QM#8OW~6tS&T@Se`!$m=yRVL#{}4YoPJ~Zf*$B zU!A!sY~X3}$ny7{`3;G@-thviPYKLeyF)sXj7l>kQdV_^7NKr0B+;}73_>rSYPEG$ z_^BuRjX|bYBNbUa>N4j{`pk1p4m_^{k{{QhSkCH*I1rTGzi08p{|Q}UpgP**yGY8m zNOk^S&tMpPb)CNT5K%ud6_QBVFqK1^TO6jNW}X$u3+&K-1)kb{5o0vsWL@}0?bhxz zzOw>#FTcPr180IdiOj~XnHXje;1v<>pC@K`&*%bIG_v)*CdK%!Ed5x#Uuj}4z|8eu z#80c=y<0O5PEjBc?*Z=c&!3eOlnEvXeAAVu`tHrsOO3Lkd06almTmI4LFVNxitz=R zuc0M!7$?a`vU;C&4}}=lJPlG|38P~%lZ?B#?mc%DqLyozXckD8=i*o?kd`P6X^9qq zHFTskX*DMwrTyiAW`svde^*vt#0BbS3))}493mStn+AEwSrtS`D6ZP0%Mf+{JcnPR za4J<#bE$M!*#HfpPx3h(OX${t>w*tHRa&zdW7PVdQ7_aHa!3%`#aE64xa+=f8iHWc z$PLaK$0p>bK_Sk(fJ12sn?|5;FpBLWCb37#9jM@pHf54vdGw(VfUBm>hB~1<$I&X0 zB&j^IIVo?yC{!qZ08NdrfF5e*SA47KQnHA(Ci8T$O?3C=($6!;C}eM+E!{wJ_iT`l zXyVa{2!_G^@k%<)fdhLF9MCH?S~M#Le=maN_oI1|XYJhOLr)lD`*jLMuFk#Cq>%NN z);kYFm)?F$*aRe!TmrCB!2S`Jgy}`&v8|}%$9Os>u1`EB`3vMMya;DuHa$#E1k*#tFhmXZ&4nWC{;F2-w|eJ0*X83kqeh-f zJu{bb+|*`-U3yEJ6nxKYX`lCHig4G#>EOWIV_Z#rp|#V$iisx%yz!`%%{S8Jh+%`) z#Jzs>;Q5sr;oQM6rPN(Cmt>V~amV-m%VW6EjW620lHdwCMGfpILj0RXEYDnJB7D#o zUI|4!*(pD7Lb?Gc2V@FyaG zo#k=4*GbCf7yqBYLW=*=sKC%t@f;a=7%!u;=Rfhns;4`4V|5P;%)3;}t86XD%AuP(G< z!Miu^V7aN_Ip>6J8x|WE|GZ~nw(80TH&>i)2R%5BuHdL>H`INjul#wn;g$|1>twOL z{mu93)$3gIe*zn#u%?9eaUjv^$}lJrplGM4gr4Gi-*d;b;w`7$6;Gq zr-8{k9csb%%lKI21rb!q87G717AEUfh6lcdKF{ju4e5O?B-~PH*Y5CAN~nRa(9Ku( zt>lPT^4pF~?uLq4(S(zem1*N)fo|fo-dK%sV^q2Pyyt@46FSA_a?L%>W9+4)s~gqT z)xe1cqnKVC?!1^7mqKv$@Wjjvz4P?k+^SnKpgTir-@hvaRGp;NTwPvvE!MMjhY~7w zc6PS53vpM2z|ScUa0gy?Fu&lw@{PJg<6WbgzHXj$l&b4#;c%fBG2eU2T^2GcI*K89w^SyN0leTDOs45U2n6_SsG zRSs}kt_M5J^+D{M6zP)&P_T<6m>_{;&Gh)VsP}e}Ub?lJ8NhkaE6CNN1m ziQe!i`TiXNG5h-T(%anusRZT>;2!{AHBBvp9~|se9?PxjP)J80qQ09GUD#_iI%G^I zrOVe29zY?E8YOaXZZ6#X(CWXRry%26<4M^74W8?td$fD4a<665MPI(6VXIb~@KQV9 z#cA|xW{)LVi9B6k6_%S_O`1Bt)!&dbT6vr5*}U^-kIw@a!?^SDC3Z?QHMrWstBXel zx3%|dMm5&gTk0TS0EA5sF>ML$8t2%3E$Zo{tK31@)`3T9ZJ+3v+1xq85EJ4pDlEL? zMt$tv&KDArj8xLB@SAKbEK39ruhN~aY;yyY19+0C>w5};Cx?zsiX{Hq3xIEXKr;=^ z0HF~0dk`j*(*f?;*x1Idch@9Ie{fx?s=cv)`@q^ zbau@ley>7qY0%(m&q#=fa9&QRsdp;$g!)I_)}?@c)93E7h&*U{m*v%_*W z+@jW9CnBq*g(SK<_$uvS;AC1q=^@9pN-}Z9fkrP^}M@_t7l7t5{I0v!Bl5L+C5n*J}tD1p+&OgL)@7a~S zE6i4Uq7N`?q2rh<%)majc`)Dq-mXRN>*b)m5s75mg-=Uk2C!~ZcOGw8zwc?MD?0xe z8S{bdB)pUMf=Ze(dZ$V|y{05A^;k(s&^nZ+CUZN((_62z+t}|)D?3CJ;q~)z5x-yn7ii34>0y}9wF)N>`W{Q&`a+Jj`TfsAG^Cw zfG0wkuMfv}di%GcBttgapsdfg{QP{I!#KL=llMr)?=h^UqO>%<`nUEt)U-S`o|IhQ zH$Ko3D2XaoC}2Q9*5b6)4rpk)Smp@tt+oK_b7A*(XQ&N^W_9XPZQ@w@5ElQ42A=+K zy^uSBA8_SvF?4QL*0tcDO-=9Hr>uJm0P%z}F`V9NJ)*FLwK<&K#~90f?p%@HTuseW zyI?oPLl>ec_-WJ{XuLOOa=llby*cHB<&NvgVrWczYEx?mqfhpn8#znn@b%8)1GrkQ zhokzJ=WF5r@9=sjDAU>x#)hbzI1_ei4ik6=O}QCGL-{$-S&B#a*x%BhI>0T)SS`xsmWv22B{;OMC;!e$RtH8h9yP z4PukB|Mk3j_|>X3yw}sI{a}QnQ?mEIGnCYxD|MdJv$tOaO!TU}JPoV(krQb1>@Fz6 z(LA>r6c{KhD43O<{UR_Bc1g?c0SC`HU#JJIQ$BxlQIJ*JZ3m3*Bg-VxJ*oCK&eOK5%g-L}Tp> zMtX6&FT1l$4&SL`k^| zLrhz(&BS^r$4 z!NG@xd7Fz482Fs<-Vl{YNQQOaH3&B3Zm^%J75~x;?IGYiD+SdWUAs^EiT}5f!Bf1e#$y=J1`0FQf%W z$KYTSbs82=UzGUis-Im8-dRqY?6=r{&_aHn_&Erjy3RUU&X*zV*j>I+m#NeH4GC-# zVukNlo3|2zITTjisV=i-UM!sGnfaa&{B?C{u%3!Dso7NkF>C1ig3@}e*W@`-a&^1R zIZe!&bB7a#7lKK{(ebN@na7@S%qVkh4+|de({Z=FIuTy7bodiVyWEb{PJ|| z*ZesYN~oxdj*~;1RFC_`t#xwhpSrY7_6{W;j79(Zmm@eVS? z114~gwt65Y2%7sO+Rvhe8DDX0bD*%R=!0NHSDIufheB|VAE9*rE=w07nU&lc9zOYa z$TG)+t6aI-I~~p$ zORLy+rgvtYw!KHGI8r8U+vefpCL1lQN;BiV-Dcl=6z^$M`K^P2+`<5}P#~$pJU{Ez zQk!8`a?@rlk2$O4wfI(1 z2ZMxmyQzE&ZoB5>$^8w2A-xuSh4hD0UDJ5B%9f+sW_$OHryhyDqm_0on8THiH8kwL zS#v+fL;|CkV9`mWjja>e1Ifwiv=e3vkCe{6?zN9$1$x!*p?F`rb|1TCGtX*cg=L#`Lp3^rMgK_X5v7<9XCL zE=zsi0#EC1M;P^fWIBEa;gO58y+q@)^z^mXBTI&y7Wls~9)rZrpE=#_Q}F2Guav-} zZ6@zecR#GRPaTrH?PPuquiCKwc#+r4PqpP{=z|7@3uC4!fk|PDlmIm*IH?x%Q2^Cu-rl01JM6~yP#n#bl(w^5Lf|N5eRtIfpGPgUqa(939}D#5IRVzV$k zMNHonb3XSu#lfT6wfnm&!X)3*%uAJgmD`J8+1lsPeuU1k!v2c6Eo7C3{vYg(Kj2&Y@tH+ii@c zrlk!yWQpyp!5!p>h4od8P08u?bsmv%=(ed&N>+Z9xR}^hZ)usb@~oJBg;HrBw_T6cD&C95D)XjJUks87FS*{^P%h*Q1>Sd&bX_Ev04M4Z-S_^bJfh zImo_U$F4uwB99Uhd2(%2ti4jK`#$pX*I9a8*xs<7-6BM|dvC9| zmFBeT>)KtwF<@sjTFOn!2ofesosd1Ec()o@w$?rN>I2VUPm$V z>)ETAN{yv1(yXMpm*2Dw!o8!G!K`F$u#x{U6^_aLQaR?uf9HrZ58!}haZ)58;Gn+} z4y6DiFqIcwsFMbT&jJ9XjH{3v!;s7@xQs-x4ZVWEue|(`$EjoDdS_xcg50Y1Ed3mx z8?j`bSynGCSx)co*;stN&>uD`oF;&t>x;5U6Vt)8{S5I~!rI5=Nyx0w$nC{RNXz^? zD~zPg$2%xZ|Ef2R2oHAwtI)4%j>_m{Hzq-9qrZk5Tjkh2J-1s5!Tjv%KvvxV6ms-ePT^|>M8CL!F)ShubXlgJ1ybg=YDWIXm z`lKu5b&;f6<$Pvs8`x*6B>V6~FVCii_*Rq1smVBcCPh6Pj9RY7eYNqN{n(b+Wi%j!p~VSMNtjECreVEI<~*=_iopdHRJ%{}xQ8nhn8| zDze;+3M$IV@G7+x7?P-zZbmz*tAb~8H10O6>$@5@3C_J8nO>HbP_%H}UME11XnGNeV{>`R8i1CNbisO&(B;v8|pu2 z!ba$Qrf?JKqa0veC~a%~QzD{DcNQ0%|p5?rLU4*E7R8@6W1( zocT%S0(Up^aLVzM6?Ep&=B*}i-C++JZi#3gqOgF8TVR-S>VB4PtMj#91BG*qz1hx< zh5wt0B!$K)u?f{|g2@j}R)I(#VG~CduKdu~dd1`=xBlH+$h2&0jh*`MGf*nR&-@o{ z1Oc3pC&h>Y{zieEo-i}VV%z8Sf_MJ`YQhLb@WL=HS|KIDq+T~L_|R#g3FU0MXP932 zmWo5D10XL`VXrd$DGZtZ&&+JGR8)$$?_?Kk692+1DJLU?W1S@GT@su<0jJw+1w|*r z86a(#fz#j)#qMEp&$y`&6V~7V`msmw47>^k1_rloB{qd;Cy_*f?SQUs{Xtm4EmQ@W zl)xVr{2Bk8L9JXcZDx*QVgxxkATelNKz{4fQslcN-gD;w&kg#O@E*_V0#A{Zl>(N! zr%w4CfUr-SN6O^EgS+bLu(6PDPM?C6j2Jk+*pD-yDagpEw8(kvftau!b$(YLT3#M( zY8)dgJnp%18_aLvc++bo15CRN)r{^LwPcX%sON9FQZEyt$j5;L(%gL8AB6}Khoq6h z7Y1LCca(T04uTBzDOLG4kM`KwB8+Vy_wj`REE*+hds0Fo!U_&&L>teOL!o9k>pFzx zyI?X;TbR9t^Pohn9v;S*TXfhFEb%of? zdpGbpq4*tK;7~4?p65Ily;cr-zbvg8zzE?fz`2}z$CDIZndo(#mqr03;Jjs>#-KWC z=(5RtX!fiuL0(>^l4AG*T;$>Zkd**&-a9`in;R1uxwNqbMQ>-IgyuB1@mqDZ+;L#o zF1r=81(`rej<~pCFcSwf6SQh}&GfI-gFkL>dl68p)14QkrTG+YqcPH8unRbZN{qCW zgao(qeV;O$)mhzz3`g?nM0L4%l;5gvpe1Ds z&o3?2FG>IY;2k`TizL)%2doE@m7p)$Pf%~~>=Y9cQoVWeCD1^_!bF6G904{44N_Uz zrI<#}ZRI7mEx-|#W26C@whE^C(-zl!fI9hH6X|$f>+X(HZ*Ohwh`LcA*mi?geV#zA zBMacq#df3q)x(KtX*6sBCGb5EJpqlc;x&kFA6v}M&8>6nm|9j=QRy#s7Xd0G+q>wu zZ^6=88I7?rGdn~X2^KfsUvq*9HH<*m3#4Px!4t)+584-w5z^8nCXH+hM_@$2G0tw1 z8l1u@)dQ8Umc-!|Z+?61@4r92y}PMtp)GR(dK$ES+;gKgqQS3U!(TvQB7Xf69_iM; zK4~c_-WW1mHY}G!5;%t}Odm5t{2e@ zOHe;wC}1`+C68xNd!M>B7_CEn<{~}ouWYoLH!oY|edq%e55ZU;Ja_;jz<{q~!r@#+ z+9nS)U5*A1wD2yn{^ZFMc*3DbpYF`O^v$u^St~L7s~sp-8!NimqkM2iEZ%^szY$QA zHwv({l@!TNRy43=i!N%t{aE!-C(q^*k1y=GkRFFOlJr~`RG|FR@IXtigdM?vRO@FW zoNsJ}E{|FvhoRZoS%5|5mE9 z`wE>Kp(;ZMYHK1P=3D~yRF8lTh5=aKdWBB)bE`0Cw0S}YN@HNAjmm=jPsU?y9u^N^ zsG3_?c;H7AyuC|{RzY4@hnJ`MPw_y6IGWoeQg#-j#`i1`4c6bVnzT$DARLj7;lF45 zSffk;W^Hqljx`iO1Q8$5Aoz3(INC%gi6!YlwF59>{Kt<^uO8+W9eKirC&5GCLjnuN z7b&SNykYh5v7<+ydXZm!EFn*Wdjxa?m>rZn`}Q4+h2%v@u^04?nb~!nI?x@!4*EwM zVIWC94yz9=zwnxBs;bYFaQFufFmT~|!+7Ed^Tqi1cwmu}NuLg7x@u2X4ig+Y=Q$AO zRaBrH@|CQCm)xSH(FV}-NV!;T{>x`&8ekCwUvEg1!Q!1n>QEyge>fJBbW+mNX*a%K zfb3H(J@uMN*u2PL*nAwJ{zXHBGNj8!Yv~sltK{|)({{hrB+nB_ zQm^P$(~bVLKW~gBB&V0)2_C^5^$iULy|d4sA0XU8Qj0NeOXIIvCf-chql@V`1bzMd zd}QcPpaZz3p~igA@WN9Qs9vNR$V^E&1Z?*Nl^EZ`z3(@#SQi`SRi}Uc>@q*_*IKX? zTO+WiWvyBmDu(gx5s{HPpbiofn^@g|B3m*$oA!jsC$K!g6o<+J9IOxq{n4P%4fCJ_ zs1EJ_kUxLC(!Zgh0V-Gg8Xc7rmDkBxSZtID0ywQLTvv8e)h&A)|0SZ{DA(YFP-Dm^ znY_N1t{U&QQ-8{A?VgS1K1hok@x1jw)Sx-6`ntW^RoOvCv5D4Xskgch6BS1DpVc#{ z_P>8@UoY`Z+{wf1Zfi@dRSpO155=~bf9^z-Oq@Z>_S{sr`4-tqR{@C70|@}sR3op} zS}Dnuz=7AML~bU-jHx#GhnT`I7B~+Z^4^q4Njg!OlyK?JE0urrK)W=0Ur}_S2sUxr z_{S6$rzXiTGqC8T`{z(D=P>pXiATn?4n^=vTx{v3>^4BLO7HiktjEHt)yB_etl~j zN9uz2H?}lAzgV&V^QOp>;_w3jM2=92u+Uu1`wpQs%y+hWvaXpGKoI!Pg-1jT%K*3e zzc3oTo(}Jy8}-F9-u82i5N>A{ROSOfayOdUj_?f$u-B^1ugl~&v3i8(0V)1Z(-*NUV(-IXdgk-v@}Y*W$$AL-XA}Q<5{Wh77T+G}R*`A1ekAa*Mt>rlnUVARqu| z)P1CaDHoAX;W!gzL}q8db8GH5>cNXnUZ;7OLjG#S0zp0Mp9Cdryry~Fxku{We+}0_ zBTRI7_`!TloyRvV9?@KD7LKrtLKS3XIe2;X4KMttuYcCc0;eC~tKd;=L09{_PW1PV z+JSt@4VM4=_wNIj0dO#wyO8dPb zR5Hrrp;>g1wDSyg&#D{M($F~Q_e>^0<$F-<=1rdP(5akF2RK(xNB6;?NwF>|s1$Qw z)+Z%hLJ*pTG1D`#&d(jM6g1yc9jx}YpB?VJUtiW^Qw{jc(*%;<$eM6O4Ap6hcPF-}qQ=yII;+FnvdgU% zF@EEycQbN!iJ;>8>={fYWHM1;ln%yLoy}?-vlox5WmX4+K-dnsG&=*xK45n_B6ePy z4b`Z)daGP_02m1GC4Wsj6^=e;-NelIkvTX%TkKqxp-34xXdWAv*m#S4yfH{V9HS;S z1w$>*Z0*`dx{0P0m;E?7$Ec`kafu+um3(%^W<%$gwTw=KpJt9)bpvFj916`pUnqus zgVTTjOz9Et^QM}|VoiEH;ZF)FdugG4W}|s%mUXHcx`*C3tU9tyF%{V?mOU{}RZea_ z7U;>P(A*N>*U$3M!d@oiBXe)3R+O6a+9UPg&7iqJYA5)8iK&te)&8>FXeYt|S=EcW zT>de9^_aO@WM5esWw5s#hELoTT54jjez6$&Q2rhloA=Mc!if!qs5(1X{sZH>s!>J< zXjJ&((Dib1(@_KJQNP;pl;=$uiDzP~vMIZ#(v@FqD>u|1J|$C}EqyhctRUf1%S{G8 zK5x675kLNtr#5JV0J&D@v(cnk7Vt=;1K3FOEm~z&qZ*NT^(e1_u#c2Jlp)nM?2|P5 zZb_RdR!jm8=pA-BtXDLk`1B?Rp zm~K2t2o`kgVzL=YI2A;HxRRSJORe2aDVW3AJe*`}NlV-5vq_^<&l`@1lNM2JF4KxT z&1;LdFQ;wW94;As{2uI7%sE@*jxZ^Sa5vB-)bVqKe*Q z4|l#Hz+F!&U2L5W?IU$n(`@S%{(gs}vgD?9iGIOL^A1n zoNQ2mX0M}SObDxv$rewYt#5u_Eut(EanayNEU0_(m+l6aNS-O#6Xm&-xO8v+Uh)O> zR9|G8_xkNzp&+r}0$Ml%7FCn*(q2*wet_+~;v0$pn+}D&!5sO$R@ly>OMve2aNohh zGW@h5c|Q);!+x)g84!-;Z4Rs)z(LmPRj$pI^O;p2?AqqK3Qf%SjQb4v;%ly+Bh6~i z=ynzv!7bfZYS>v?(zO*Us}`Y5)`radQ9-ZJ!Iv zNfkd@oPWSUVRk_Zz5|KkQoIVz8~zRy)k+Elokl;&5sG+1>oHm$w6MCR;>8Wgycul#p*of4jk`>2o z;=jB2NCb7(y|^W<>$IvQSLHPvLCUhN7ZWHM`f=h;sAKv7ZZb>G)A{s-m9=bt@26Yi zFHu@&l~=Wx`33%L;3V_WWLaO0vGqTG8tZ!Pe zOkX&2)MvWV-9`F8&z(h=cGhhU+-=31EOu3>MR~PzJ~hOgE^#%my8eB_>)*p6mAXt>L8Rf z7)BM3SxdixMEAr!>fh3K;>p=ujwjl9W6|Bj+P~r5*aZ{EN=Y>1vB0~(mahDN0DvJv zHFxYP@-0h`uL>diln})J+)>@gFM=!3^wQ#|ji7$Xz@3SoGgPUk|Ehkx7uQx&NXyPI zQ&`A9(EOxH_wmi(9?KIAYVE&P6oVSuiu2q3Nuv5C*9S74A7(CB6f$=CyL$e;w?g=7 zLH=cll6imWd3anKny$#!CLJek&SJCwJD9omT~)osVaTATy7Fl+XxMkAF1thb z87VhvO)kc70<;dO407#;8UHGZMe+}HgNzASe~2JeQQM_IbL4N(5?N_stNYKOeohF= zRGS=O2o0|feW~)-Q-OAi9Qf{If(h|Hf8wN`FDN(gj5>42N#U6oCnuD&ngh(NC-@Nd z`};5~xItOj(#85ZKaKgYQ|a-ejP(T~VrR_nUbSOHZ3yN>LxJp$sJ`#-hQ zZXERg$6xraoibXL21k;8IC(?0`}d!9TI?e*k?yV;xfo;YC&0Q*f1E_8hh~vdJv@YD zvO8{%d{oz7)X5pjg#7!|s?GP-+Q{h0rE{bX*X-@^0shzT+{uQNfEgq>;Oh7QZdUQ} zX7F?P*L}FA#W0i7jR3^}Z`TzG)kV?2c5k}>!2~TESH9Zc;b`?>F!z)tj*Cm1AI zG$h2tX7d@}tolOIUi1T9K>MgSBx8`LP=iK%SIQOpm? z8N{Tf{F7Vk6)Rb?06~7i0$2z5U4bV;l{bNZ#|X4`US4`0Ypv?~mgDNS8!Izl;0EWC zn|JSGeVX1lnvuxbws%4^5hyA#pH*r_U``W19RwMHWDZb$ zePrYTfMD|pX}X2NP~k6!gjc#pyKhY}1umT0F|Hvnn3?#WxQx-cZ+ z1&LJP?EcTp3%4<^sH8$+gn3~K?;b?~E*iv-g{0iv1%y}tg-7>@jDr`QLNFwRa+9tq za%XH7_LYVtMC^>+Dc#if-S*1OR-Ep?w(d|9n|ERGU4V_d`-ZjGVub$Ir25XF%%&f^ z*V^FR&h{LX#BNPi_g+rc-CKWu=o6G6mp1*z^ z)8a*~OYIpS9K5d(eCEs<@PPLKz2uQxgoQps5+RH>pQgMkyQ11^^1fCxJ(Hc7=|@@( zwa%J8u-6J_p4@2X10*?hoh_Iv&f0~QGdp#-esA1X<-PNpqn&e=A^X~o#X?8v7rETe z)Tn#`Vh5xaGvHd$(w^spMZE!U#Cz@#Vs;2nKAA0O48Dl*-dw1LW{S9R=G8~<~aK0i|6b!L-x9v+S*_qqDg_l@4`_U7=3qt5>f835Tn6J-PH$x@A%AMZ6!M-z$d^kJO zbztE2(i{wm_+VX<&$Ou+w0RuyoFNW3JUskUwc`iM&BRH*{B3-VdrZZwPY%Q94U57r z7XUgZUZ69mmWU0Zc9UAxWV2D`&iFVMx(UukAC>t1bk-io$o1fU4)-*5*od&vbB&G+F94%Nv<2+ z@3LpK1*|!Ch8RoN`gc}$wwgaF4q+if2t5yGFJ8dW=CWI~_TGTx%dG`X0)xOKU?gqV z!6ZKES{Jiry)#q#T?HyAIn%|uySYK`@7=roj0iWVh+h01WmfkL+(k`~*}=*xkVg^2 zpcWr79h*lzcwPjO1a~rGy#y4 zEbXOs24FQ28wjTREP&cfS$l7qr!~6s@_O4oyv1e%^Ls^KM+=#4+ej} zV%lkBEt#*8#!sW{{SO>|e<192faj;2Lqr<4c~Ih)5n@o{&S&)={DaNa4-$Y@ z2@2p_-htnWhGPv$Y9inDtcWKPJyGnN_%8*8d}|q%3;|uYWRb)>a=~^C@>Cy4)`nKc zgyeE~iZ5MSeI3+o^=v(*NbUXsvL(QV7#SG0Ct|iAy1BVMcraojv!N-7>biHHMaFA$ zZhLl)fq`Kpm5G@yHiQy6l*~Z^LtwY{@bFzeD}0HP<6%U-JjU0NVjLkZeZ*Rzt!8B= zJ%P9NrRN(^e_9j2)oh3eI-X|tv>zOv*#1cNKG%jL(vrx9K^_scUc6^~`IHJe`Cau6 zY)JlF%y=tP+hM;zzgn(|a3HLpz}!@3yLxA>jl}Z8TUbHzG9ibGEIG)E2PtvNo+^XP z%?g=KaL7TtWGc(R0TMEO$Qsn!)pZ?%;IE{_yr{?Im?Tpaj`*zDaw95HFm78#Jy@$=6|*WTc+5m zgG;dO&hj^UtCb=zkPem_ViaIsR|on>2~|hdK-!E zojZ%AFrT{hw_5dkVcD93HGL44973JGzakSw579)@T>xl&=H3JiMUE0vCbq4ZA;ii(PMM4EXK^_O?V^ zL~jPo9jt(wMa?2A&ncLGkJ>sxzF1-c|9B~vVLC{&k)FwDL^ zwAZW8|6k!-Z9f^3BbVQA*m-{UlPR6gG&w&qN;wJ(2QSf@1^G;rTOAUKEN#%@@CN~@ z5ROoKU@C@eNWW7NfJreQiLK~0%0_jK+IfPIg~rd&%PYE$)~bP|n5~~F*4fyEBl6+? z{3o)#x0cIt!vK^&HQj$R%B&|O0myUf2^-xZ!X>Pw4xc`;G(?<<5SS7Lw1m9(kR^UcnlprT7NSX=3f$#*BlV|~a>JS%4m`574XCB6CjG93=Ydmq>E7G)`s+^O-{x_yJlK?$+FQp}1PExXUk67LKq%LDknz^^ouyK?NvoOZ3 zcku~d7tjguRQy%gTzCENteOI9xE0Vp^6`7`dw(XjTCXO(udCdnnAmb8gZ<@KqA@hC zHifILb~$=XVXgwgE0>EtnJs#i=B*XyruJ@JXOY>KTsMJ$7{=*gx}sNPeF8wq{u_bCH%pY6+}RdH>dGm{N8P_y*Qn6(@%ee9M+s-Gj(M}?2kl00 zDIT(!N0%e^R+ZJh8mm(8TM>!DmmH+zp{`CjfSYp7y^L_GmI~$nCqO4<#ly+i#MCUH zHL5k~&I<|oz}cfy9Sd^W<*~XmpPPO%%w70YJxLX`0aCVJ>`62w=e?>&0cZKr=`1nZ zFdR;x>*~Zwy`*^84v+=v^eX}(4z3QTWwEZhA1RV96Hzt(CrkxUtQXMyGeO2chli*~ zO(}Hy9wcKCT7Sc(2(0)!Xth^U`4`+*6T*b;uI$Be%dRA)Yy1Xp%>Pf|jd5C|?U7h@zB6}{ zt7Bx?Y0nm+nBP|LlmtI7B-uTfOh@mH12)OAPFtE~1UM`A)_Y^!R%`ky|b^Y|d~?fS~s z7OOInHy`;%Rj{XYBrW_ZzL7^#JjC1jE=_*-4Pn?}DaPvs^yWR5cgksre}?cFiK+PC z1+?LZy8zU0>o)lQZ}Tn~6Ab(R-V}+<#ebqvBC(dp!QZ_voaprd5b7`d4Gr!7G{+co zhK8gFsgXovvJ-y_H~GV;M85ym=OOfSxQJ?mUFJK07`s2)eQty=NW6$*0t8J)?nJ!) z{(X3o7?k)!XS+^yMvKxd@?99t>QG%t!OJWOq|7ng>*)rGlIJdIq9>9X`IOxs)+dV- zQMk%_F8C;T?5}%%gPs>q#i4lmiq-O`-D*VajP;&f`mkUAN*A zdHam66P=%tTLDT`tlBzt@c>0!Q@G`Y!C$SWKZK7yck=|s>A=h`K1~k-7fBsENBRlZ zQe{J#mo^2rMpINQGC@lNOosP>mp?pv2lO;PenL)*X%4*q>Z9>g^%$l2l%IEXuh+8m zjKs%J-iS}h6&~lMkF7VxHkooHlrweYk@NxzTWf#qoM2LcnX0Ulku!~2%guPZwq)g> zr|IAR6EvPdTc4(;IJGQR!URR^dKiA|RY0bs&^$kX{ut8`ADWY%=`e)_@Qg3JsUCDIcMZ#)? z>3bXX$|jx*-#3=A5t~XrDczlxasWmk@j|A`9#al9P~2CSmaYXOoGoHR?#k^TNNbwg z4e@PpUL}Yo>PAn(sCm+Qx&fk{T03UYF~EOH>v#xuIEPDje)Ywa=C6*WTW^^|#$Jfh zRLRnIWR#gHf>4p)6jUOep2)XfYLBiqkphl<_MdkMCpU!@A25GxL@W2x82N^(+uXQu zBU04XKo!*m+yGi&3V~tgjFbBhGT?YkY;G1+55FgTGyEMOiVR>4Jr^M_n<|D*4pP0k z|3Bm@oH-gl1DXDL-C=IDiII_h;mRoZFgg@uotN=-z+uVFANm>rm*A*JZK0Pxt zHOD?aGsDd<^SMZ_qp6825WEQ9SihkKHY=6%?eMU$Ifv*YF-fVZ-GLyKF}OTpY)f}J zbD#Keg>H)+i2VwCw}DV+>I==QE&UJEVzIJD#em1n5~bysBv8IhosHeD7-N)y^s5a@ z%aHe)X!jLNAd2t;;Ve%k{Y-2%%g*@%G}9kr^e1;a3?=TAlanI_c7XBm^2x1W(gA!v z8wZEG=_<5qU}_f!95iL+@3kDB)8VHswj8vvxq8*)abV=zhvSoz;D2%3X=ZP=bsPzA z29c)&2TZDt{l&kKWUF4O7cZ%U&2t*YC^FJ4hz6bAo?hLJj_@xYjJv!A&#p=O!?-(f zrLS`>vvzaI2r!}Y&09aTJMSBmKloxeV%jmv3^DK4bPwljLr}3Sh?s-LOsY-0)o5|n zT^T^eW=;R;-|Y~hi-+REH&aq2*&H5RJK{W-^wVF$N!2VnL^tge!A#ev{r0mz!`&f; zg@pz0RxBixoxBcjS#f&Uu$l$6>{^e~VmVGZp37D=^REbR7*wxcU*bVYR6YDQCWfDv zH>A+MhSCqHUocc)2zMs$94eZ1O|h1~>;v+0zer4*24320Xnowe;bBt1$vvhovhUSV zj&Gj&Kv$ah&5qyeVoYTJxYj$>2APvPx3pa)FZ(oDzrij?RR6fWXZ-qt38MH%u7k0s zv~S*Y0YYp{%2v(xVS?Bt}^{5_D51K5`E`LjlH>off!Mkrb`_Vq735R~(K zX9rX~0j&ehZ(^c?q~e`BEz{E$Ktn|v<#bNdvO@}O0`%zM)O`YNT`|hcsqE3#)paK4 zfrF!Cz<5y(30VGm4^^6+Gzoxoe=c;KxC)Ts4kLl$KarwjM2Zecu`dbhARp*>_T!)_ zOxkyjW;{A|j~d~Ad|#Ub`B0`#neFkh*UY`_4KL0Jv);Hcc7+Dl{gB-6Buib5s+*Wf z9a+Qsgz)U4ucNBWeC-R@Q;QzOeOcgbY1tj4Xh<`HEvt2ZiIk8KnDBZpnI=~c=i3ZE zQVtUq6$L{4Tz{#)zJ7l@(4q<8W^y(VoJS(tr!39P8tUu0d3b`s79iorFES!I*z0}E zrwNCLpbjeW-thtx-wS9k_K0Sn8d}#-lV~yVwxwK`Sdo2pySEOpUBBoL52Vb?I?F)3JM*d z$fu_7-0kTd6jjFb`J%KspO8tv8Md3>{E}A?`{hy{Wx&;g*j9WosdNwD|B-0X^q;~j zkQ_}xF-w%gywE;i4hdyfK_E20CoBHFV3QSt7f;mocm&;Y)BYEy`9%@@F;@=5AX|?b zZiMy9by9QBcOUu}@eHb{Pa%?SOV^;bz-i_e z@!h#|DFB0K7p$+V0I^)7Z~F9V`| zx5$6Xv8pCnVE+}4Xa`4}m_y`*R3P`Vy&z$2N4ZIr2!VX5;hJd65j*NEi#&Q3h>sv| zm6q~6l|8m!E2i}?bCEah!lW8{g(swgbLGZF3If%C@$37Ye^Byg`B{qCH{RLV+18fx z=+Wi-US5A`lx1#{K*k?2R{)#_Dk-GtH=&@WL7^pnj_2Q*>pWeTfUqkFs7Q)&~wDkAM{m$0m>B*(of{Ij5;RUno zUM&H8L{c_Y@&M73foTJkUBPq$?RV#IARq?v*5msNKHPiCky}#wzGzCMPGsiwaIvvQ-yeV6q$V$|j>PYEOkI zioK!a=Egp4FUz$qBuUlDhCJ0jr~@hA<-ClH*q;3Nn<>@9u2cJI2o=CEsV(4T^T#V9 z61Bd3{e;OhT}vW9aM)3TdL6uu7K8HCn#1|Eu!lxP7yY#_lbh!2^QU*=)Y!skYls?X zWQu{iI!Y_KH6)C^$bKxae{OYS3l>jFyOC=n$a{MLpBt3e`lW2(FA{SI)CNB!FJ3$a zXfCB`6kjTKv17P8Xv=E{3JEDkODbT#tq$ev#o^TuI5M}J)yC*Drkcw zO~=W6i;XW|3!dlg)Wv^C3H9-MG50SRHN_5pl8(5cNsK;_;EQ-69D!;jABali4RgLf zj!=&i=)QNpBpdwTKT6{AeL<1p@n3KG4=^P%hQwr;;)DR zHwGvg@n)`s-^papYf7yQ457p_y~ur+d?`)i1fbgVn-jaS3I@vG{+9R2VdOaw_maIh z&<$p5=_)IG-MaPAEGZNb>A{te-C_^gz0aQoIXS(z+7F-H4LPA3RexN9&qatL=_d{n z`3$yHro!6$sTAA9Hg5cV+g(QAQ|7ZMw~f#3U{DBD#O?>>^~n$uoTzh#U!<4#%G0``*kh zdD||v#-{Qke^3a(O%O9QT)~=XdHGLF&*-ock$-DGR#XvLR#yEAQ5UxPtb0 zhCp(0t#;<}K^Q_pQv#V=3Yb!YUbnWYZG57O9cZqVle&4+$#WA0-I(rl5u~g~W1)q%0#ryH)day%oa{Y8_cc51g|~tQM#I=^N6O>Mp=N`>AJ|&D zEK4TM-GQGodIko{u*pV=dYTmfPW;er@dQ%oXfy=wC|V@nYTyNg`c~&=Uo1%LAV%V_ znCqb9`wF<%t-~D;v})jkstrGnKbO`tG&N5!j&ybwZfqdn{d9CT1ZnV`j7djecsRbd z6zux(9xR`hz-t3qxh+)AIS^^QFzFjcBnRP_V zGr?JgyIW{|Qq0KV-P^x_5FOScTVy`2cx z+aK_$VzAPxNMZ>Xz<+Fb5Dhhh?Yzs1xD(#x5i_k1NB12Vl(8K?{A?I8>9`7YHzPer z`2!89upfaX0OhM;fj`tx@ZDbtbck(2g@@z%GpHp5C~&}GIT~i|b~k<%AOMN;P(+mZ zz;T9i9?%5-m1(bDDdB@m;OoQU7cA(J#-qG?1`)0n6SH6p+qG*~2SOg*;|jxlW|6!b z=&8}sz+O)j zk(p$7eEhk+Px9E$o^71DHmM) zC~!}nPz6SUi>rbYhdu@^sMbTV5ZW>_G6G!_06yNT1MxH-KXLdwATZKdXTJdg1CA>2 z17P;e0Lqb44@)qOS>+4Mxt%;be79imvfcY{v&MRJa;T-l3%*o1sDpCbbZK~-XLSO= zoyZz<*dL$~ym=l!CJh(Pnzvu03&^Xwi59nsebA;uII=`t{thL%-GcxTzcxN0y7+U( zptRR>vNIPvg7`aUS2--b|L+=kr?E&fso zB+0o&Rcwn`0f+d}B>|wlRCMP!uN4%CWIQ;?hbS!G7;DSl5E#5QZ$@GdqUlM>;$D&D zlEF_gxmS3o=B@O~;hA#RhJ^s`$U0>`1 zII8V(888-{5)wckNEd2B(ARIIiL2)m$cy;H0(Zc9hU=JyGvQe#*cm10GjkV<8`J>J zX&{ZdZW|1A>?4Tb=-ucpHa_OSFWX5NP#8g!UzLhZ!rnYpe60fRG#};%0co&^PyVbs z_uC2{F4p~mCSGN*P>Ga#=K|yf8u9_nal3A~?WWMk2CaFNUTJ_^ZpoN2s@Z#zA@;el z;|W1ckc73gjsW&4qxjVC$CgegNfRbvnsLUz&>TV%Wm2tgCqef@ZyL8Vrg%T>sf}DPnfaW zTd(tYgsAv1Q7a8hD`>#B*(w^A?wkf#tQ9e>yScFZI^QGkhZyjp1Wkb7P?$f-d#kZ{LV2IDNu1>vf zVc`WsO?ECYi^H0{?jU|nzH9F8)fwxb_bDzh#}3`Jz_8_$iJM~WKbX;4&3SBAck#yb zMV?^2M`z}WjJ=Bac?M^$)pI;+Evk%yFS@UNu41I83WLA>i?Q9K;5$ip4t{5y8TOZg z;dTp|$;`&k@;jt+>*b=5gZDDu{p?@1JG+1VXThv|m9H*NYn{&*#}joXvtwcUY-ZAQt4e%j6KJ3m1}sGw0=U;yIjNfAAKZ?4j~B9d~(%Fy(Z>u6wC zF{XUgFCm}a+?&XM+wM1T0!1N5L%}()K!JOUNmXO(X0UO+7Gnvs^fgM@iKkxKeZMtd zO`;aH-0pXV@3n>V8;|LQ5*OyI zc_U*(v`)i06uuaQs{`~ssrW(l{w|{8c_r?_)YR`p>2fU)&?~JKs;DHM337mES|uy} zzDL+tVyj{(tcIb6!=D^`W4!`B`Rq=~Vh%oF*bjQUoV(-vT*7Shf1KFaA(P#Z!Y?5K`C0S->!DF?VomkB+ux2*BJ<#OINrWDZ94Xqxlv~EOkR%#_Tib9w8?Dy|^RT#A?G=paVJBp9*Y3T0R)SG%I z7}X}8sDadh%;SD_>E5l!><2+w?EUyU$!XhA@#CBk^i6dQRiQ~X?4-DmQY>^+UNGL# z)(E~loH$BC+d6(8hIxe1-phmUh06V*&QZ4#Mj)FobUDTB~RoT>uT_yNbHJ9 zx^~Ue*f_y{WFPc$&z)PO;Ky?-;RZ`;vfyiJ7#DhqLg$@&S!CEHF5cFnB2O3YAGj(@ zNs5{>O4au8B@%u)Gm{t z!(}G5J%4qKe0>_cOX{kX#ufk2i|GQIre>Nq%na_&Uh8_izbl)MN8r6oX}vnPSh$(hM5pS#4YMcPmAF*}_RGbg_OD zhmP}bcI7(Rs0EVDZ-9OazxrAEmNZhKs*}IfAi=*rC)A4Yxb~ZXzp#CA;MG{nWcVf= z2D4e|A6Zb~(&%ZnN{T!DzHno5q67Y85gzVNM&@~tY2_34x~^myq`GOCt8GLlu3eCa zU#?npmR+bC+r*@j?a}DH2M$oAn!s9HmVB6}?dSC%cw0Elw>~4hS0n_Lio4s}&qFZ? zopp`$jNIHw7$|@$yey{kC8Chy!m8)|{A1jjrQKXolfciR zA&U5Lv|w}|gIr#Of@W@O+G$MFY^pNzSp-lRI(j9r@ZH#0kV0jm{3u=@>-n?a^RtA# z;4q>vGgCH7l~B&&0l7Pm`Kqd20cB-JF>R2_P_v7+BfPNC^TNHwg-iqZ*!$wKnKW1z zmt`3Ka2DFRESK#+Azo8c<7p0!cx-_#Tm3|)%ChU-O4F$BiL|UM?G5rwO4d)gP@Hfq51|oW0fCXx_0Nx1`Ad=Gx)mu> zy*CTf#uaLvP{u|~z@Dh+efBfE$>rQ748H5Rv&=k8h1J01&XSyy@$X})@ki#FKMcoB+z)1%Fyu5rJ?9q@A_r(bjK<+d* zM@2>^H6_3&9sNp9K>_><_+&TL)m>KRO=B8_kjRv_BtJZZ5VmMig7)nUUR)`ANm6C; z6O8#_J_k{yq&Qrg*5kf@m<{MXRv);I(4dxNl*?m?$*Ndme>ltSNa%#Tt~1&r62Zz6 zd*0U~bR&u0%R~7Q$1p;pzW>z|zwBMbIlv?T?QFtGpn8j3&Yy3JZ5hkTwwvlL^#m75 zQBhG>zBT+V!NsM?($UogaFTmC>`@W?WPIG-6sa9?4n=|a{+9%Bl7s~W+<_EQEUqo5 zq+9f}0@~ws!zAn|`2CS{PG4W&JDq+yE3zt2EA!Q+(I)212Y{=}E+|MyP1UT&-XO;X zcjru8qgi%}n2p$Dbsg{!6Z+-C-By2nC(>?pNHY70@e2r;tl{p7CE}^SdIx`3-w_EF z$8iaF_e;e$+OIA~!?1`&_`0}&08ej5%NTTQj?0=^TFNUn96WgNhifq$AQ1n8qXob? zskxm%(3%-TTQ;(zYGQnsc)lVKg_zjyd!(c`Ez`l#c-_x zqD8^&FU+EmJ_W-w@fgJkGz^;;okam((NN0|^rrT%E=?7c?#|A!1y?VU`48xt=_QDj z5E2q%rY0Yv#esq!P%)V#pm2R+&NgOms(FtX$ppp{X{RZWK};W}yeC_H2C-7tLdmsD zh#loO#jU`7$DP)R?qbIaMO4i<^(a;T1rSEPmHXdnq2eF5_{ETr9(n_~P?eRu5LN)! zQCAli!&x~0Kw@iTBop)+CT@6hfhlff#dXM^-0SQSe7~{xy^~_lYiMC?S(l?25{Rshg&=KDf0==a3o(cIz^*OD-p~LDh)rfEz zIRLO1pv(Se=y3fCupS44zd@RXa?Dl>TL|Ae-5+Z(B^#gI7EJH%#a3M{Zp#0~Nr*`( z@eNyhjMB?P8IsIAl8DY5L9RL1VOdiC1fok7Bb`nPB=KsY>v?aG;({2w7Un&!P0Pf9 zG~HeX^jmp{x8_V0MdG`m9qykeKAsEo7T7j0+%-l^MYOXAB)8}4=F1cFW5%||142po z#$s*y=g-2X{MxXFsBFm2R%DW=KXhpJ8x6UESqkqWeajnpk+%{FJqIjOc=)pCP-C$l z8?GupV=$|suEv;qDOf0KsF{VT-t1EzX)5%V)dL=3_)3f1avF)K)==+wJKLiK+G2je8k2^X=46Tym)dZItU@#N(#oi0I zBesVU;Io-PSIT$A(rjtrEYo1#RCfDmqPX?&{(BV0xt^ZG8|&eKadLHcMY#t&`!9Yv zAQ@wjZTs-rF?&3dwE6*bkhj)_w|9dVxcgBY^*GdOOpW(<&HXW&RamC zoSjuxw;H(ByLPL$=w0UKo25vA5`*9?Mtjr1;fWoPX-04Bd>&$s>Ben*Ka4rGJ}Be# zLl4geaKR%a315WC4xYC2?@7e}oqVJ5jX)lLVU_Vu*UuRkfmmhkJThR&Lr}B*2l_Cn z$t~mGh{Mpq2JITqmbEm|Y76|JNc;!*rhrMW_vp@z7mUex(cS+~x~|)W3oIsB=Ll^K zd1x*Dfg!#H3|L;jPoiwkv|@(XK;s2H-*UbFhJNK)j*}7xb|(%M@<>2JFw6TSz-xfJ z1;cAwud^No0*qFn!+L8XDew#3=v6P(M~64-3Z5DJ2vFF4V$C+dyHK`{D=7|Q3W?fH z`Z-Ymn+4I6Ju*ZnLlDIU-U6kpEJlh;#so3kwV}1FsAnWHk>9MAKu$O-d(SvjVc4bW=}!fh;i#2Q0so8Q9*Qu;XP*cX8JzFq#pr>@&Y zdr8g4;x@hw2i_ha@QqVp>C|jUDNN1|7rhGtO{&X|ldP#ktW&(e-QW!yha?^cVLVMq%frTw9Jc^|FknqZ0_^;bmYS1_ z`;W;X)~Wg{VJAgvbOL6MI}XtHXj_ss;h0p2BZdke6VU-(i7Yb{U0+oN@s} z-=z^%G91KkSwjmIym9bW_sDV3l>-&f!{f|!au%6yBvKXe+_FFSj3Juge0?>RdwXuh zE_ZqJdKqAc{DlEWLxa4HfEII(v)qvCE6ECBejbI zWzO}CudOcuRa73h7UEL?Z#V9oAoy`$G<3RGH5tOUQku=U-fLZ+Z7Hngt+3m=+ggzK z6zQoC*Rj-T_G~=#%n_0jDy+^BniE>~zqTjD^HR}GcGPpPUMY9M@GK=|2WU>%JdY$i zP0{V3iG zTF}PZXOvOlA}Xovw9o8K(I<7C_MU(;T8$X_`3D|sNt z!J`i*o)r>B=F%ryA2A{J=9kKT(U8uj6{|>ddn5F`xr!?n?3>jxZML&pLo5q%`;Zz0 zveb5n-`)lj>N%j?ojF^__mqvXl(aM9r~lVAnVC}G#>19|17BB@KZuwwm!#J9Lbn4{ z1Cc4rx=j8Z&ef0v*SKb$EwZC-@)l=QdtO6lq)qJGL}PAsA5Bc>VwNiL z-n#1A$J8}l+$!!R(1+CqR@onjZ%U=a7KS`?IJQ}L1{;_=u>raVc*x76y(fGA zl}}x_cHBCUhzD~|r@)b8ZN@j3x%z!-%h;Ms+f4P^(D>dANq%bUn#@^bu8 zE_2dyK+}p$vv~CTZ0V5T$)2Z%uD8~TP*c-!zHS>XSs+}4$q(*&6{ag#tX>j#eCtN> zArU}7{Auk$nQOeJLpc<+nIR#IR)-9BZ*3pzgg`bxMG$L@lu#%zOsS%BnPg;=F6W{pH1wI z#p_JA*n-of+l5m(j4xX>l$2aSX6Z8^3_a4~@NjP!t!@oeq4fT<`g9?&_uTvdW_lP) zbFe&zf`4HQSe-X!^q>O(vrKYwa!TyaDWDx6_$I=~*E=fO(cXRuMie^>1|d_(d-w*B zV&C612Bq%iUYe)fN=tW1QBsFj1D!{1a(9+wds^Ziih=oY)Sj4wdaYGRV{ z{Nf@|r9r0;C41jTGW>8U!m77~gQw5&{ld!p^Aq>V^`JiktqN$lfLdAyb5lGGq#xbq zgoR(XjDg^Nad8oNnJeoaP^W@QxiPCkM61|!p*dMuJf>lM;iYItd{Y8ltgRSSr(Y@x z^71lDJwJ?!hKGeI{_)8m&5rQQt+FrVkLI3I8S~3qF<`fKj2UK6Gd`1O}AF?(l*j zqE|D|Z5oAOH{j<2m%N|F$k>>I!pF?a3`XNigJ1UhB|ISV2UjyFeo}*rV>78|!Il zLJ64sd_W#80vNoXm%t?EH_0trT%MS3v}m&_{xrC;xGMe}lijd><{|-@3Co zp!sqr%=xXzhlDSDU44x>z+ZdOwV1YQW_URCJ|e7ldU|w}hfe}8ULS&BjOO|E)uqUY z2q5hScZq-;a&&IFoE8U?R>%TdUs)iWutGVKOG|s^m-bwG)9^>5WJQ}42wq{~rZxpy z;Is}p_N;2)Zb7-3QM%eqZSC`7Vv|d&d^&5dK!REyeZiyD6A}kZOs-_qBDFOX6{mqT z+%N)wVkL-l<*n9T^V5_01|g ze5G5fmApO4$(20)|L8~KU@<#YbTiBiz(38-H4-s;}AcaDk(s6wA7;`#H^ zjd3cvSh|55Aa`R1m*PJ|nk0$sABwrf z;dyCz?#k-2als=E1e^dZ!i#oD6X)C&pf38fs`dmq%S4ZFj_xTQ$u?L*VcQy#{el zb_aJaWIt3)g3HY1!iPi}EU3R*{&H7cP=9l`6$moFlXUU5#34LbH|<_QnZ`YT%5d;WOV7VL31{=)oTd&{#s zI~a7H1oke-M53bXg<15;e9lSLa0!EiO(9A+Glgi<@x5#&DUNbGh)Bn_1{k$|Q=$L? zf!YmL5pX*FCk{wqYUnRC8U9RW$A_%^)21c(Um&9;j{09%=N{WO@@7gJ1Kr-^(4D);ki%rw%)( z8I1TG(?wJi1b&q*J@>c9uV)s80?RJ~wJxld;Ye zJmC0s2Zzw8H(U2`G!I0-z@ueLi5)}a-{}1Hx9RWr>&Djyooagi@^ys|_~__yA)ePV z*Wy|u()n}KPcE4BRf`l&_`h&(9cKMH%+?vE&hm94sPHNKD)Tjsb6gaQnyx}CY3vj@ ze7yIaJC(|R%fwip)3ZRdz*7;vjAL5B`ft7N@#blL^+B3QGhz*`dv8H2hEK`r#$S4% zUjo8D^a37^*Ti&7Ecf~G;Z;Xc z`$D?4mZvzIDi5@{D!*Ha3W_vJp`?a6uH)Eu#86wV_~Hc!>?yHVVT}uM74q^tjUpG( z8=0x&z4b5GGgE6q!#i$X|KSrCCqYfKK!2d9mnc2gXsFk;y0qzau2I5ZB5RRd+Qw#~ z#*f$q=&QYoH&FfL$OSiiz-9WZrJn5oL6B2Tl>du@DT+?3jg z%Z71qZmt9TZ>XD!iq=3w8j>mq>6L(i6@zwfi};lv(v;F10kF9CqP;~_3<4rHWtWhP z?N-2n_f`XpNiL1X?)tff8%`NL)9od*?tgD}}e;s@jV2S4Clfc=( zOiNGSzX{=rhJQp#~tF10aaNh@fB&1{3q`g|k6+N4fv@SnL?4 z%{;qfeyi5w`McIbAdq5W2Dfj27?}CFz5(wrHpT=d*uC>0U06+OO2Pz(Sz~EozRv27 z|0v5#nzPgVs&WoV`N0-gT72xt-{bT8%1Jasu?IA&eja!J%mlgZ&|g)k7J8%540D8| zOOjkm;sEkDGhRpjZo`4z>*WCcmGupfmV^Zd)6&qua2>pT_wOU1)Lfo5OwC;>f6`zf z7};pTbf^F_#;#ty`fZBKqqGzX)I<0prL3$BhJH|4YjD#;n0a_qRBZzvNRwuXc)VkG zb87l2b_hA!)&Dn@zrwrO6*9q0B$w)AboKeQ>g;79tx84hfvaG{8?_YvLO2 zBuQd2BUO_EKo_|cBo@#JQGx*L1_*)hw>O#LZJ;CF>1H1!$EBrNMDxlgT?1(a=TaJi?OBUimHVe`x6_XUL%XbBU7XFXX z_sc)$-S~-JF9Dch6r1t=sYoh}@e9F25|KZ)`3?}85fS>i@tkmr2;l34CpUoRfhnYN z7}El@6n>n+g6dB5AKcqKi$>1HIv}zJ02U-^q~>Z&)l|!~f@A{*{T^#SGvA@rQD_EL z4UH!+f;dr2^as}TOmHA?vA<)y+1`>X$#}H;972P;_oS0T!Vm~~z>QMySr(I0Ct>hQ zS~+m#OK#NsnEdKL-rCqCsR=5>`X~_&qi_tt23bP^x#lqqZEbD3V7UYNI^}*W6!uwA ze^X&+XKCo7;dI;tk;m3O40LfMIc}4Z9f>ui!h;^Na6mtCnqeKtj3cV81YyDcq*e4L z%x{f0f}orue4#**w=1Bhrw0mSFttFO2>=(Pc_)E^2BoBhgjb zQ_LiafTud7ce@trBsHn4s90ooz&1kywrBo__r5(@K9TY{H>aL*ac) zIgR@c#Ihbw_9v_6v*b{)Xeh~Po``6UDzrf$-vgHBng(@i(`t=G7cSe?99{nzG*$!v z%l2{NM3wH@gKJVWG{ldCzbdP%Qydt-O1(V_7Eq2<3Jh1W-X+))8rI=*#K9(^%1_yE zdk*}J_dMv>{+I)|UU%o@NnK-&QhkWW4P3@3g0kn&_p9oo&i$W;yRZ}R-GQx>3HB<^ z*%G@wU=UOz7zAaMV}$0O6nkZNmP*)dZq$NWu2@_wcFQ%W$&Y#_p$n+0%2bK1*TBD; zt(W#l{!u(rrg#S7y0+YZ4pTHa&4&V!WcsFDXEBB$=Gjc=e!@R!t!bZ)Okgx5|X1_5&fduFH-Y|q~GGo4&NA9}~*Y}k~VvBA=yK@Ebpe6W1-)b>#jIL;| z_j}W0yrUE@s9Rn479x~81et2j1Ol(-q>HWKdL6RaDY4-;Q}JTW`X^|GAFcSTOG~45 z*hp`Q-x7@%oi17n;uU7ecPYIo?l&t{FgzSXS|1I>?$rqgiNQXZ7x3iTc|5^4>Z}C z@A@08b!JXxVa_$5es23ArdRZF79$sVR24lY6LwNhQ3^1=4)!0{?i#pRBXV!~wgdbU zr~B8V2sG_-E_ZRP1s;-hpgd*{$xPUso}wp|6CWFE`M8uEfTz{(D|D&9uKDZZoZ0Bl z{c|%y*1@fFnsNAo6Als_oKYtf--&OTBh^p2gofzH1u9>1c|=+)I|RAOfi75|vFh$Q zHcz%QSC8~MO-TRv$`8HjOCxW)E?y+XVRdW1#`8q~YOfI^vwl|8CFqj)tN74|<=jx{ zDLUk$<2Gb@_+H67;CQvI8ci9S3LD`C#Pr_=AXAnC?Er{M;0!d7=G^slsaPPQ*A!5r zkFD}j)BlV>&ibV!Fq+*B@mP2#pAJgyQvH5ZMoYE)k)y=+FEEd`A5(p|T;IpBJP5Gz z%3d>%zczhIjat%=3XiLwVKZJ$`%Z-R_r?pAdsMiDAq?xIcC~db0(r+Ip#StE--JWH z(T#D_0*hK5u~HPcy}G-%uY-?tXJ*s7KhNZG?J5bkx6<73TC2Qc`nRZ9|0qb6)ypIa zTi+;6*;Ul*vvOhyi_mEFtB{y26)`eo(ve2fzB2#`dBx3Mkf^U(AsA1qa7U_@AgNb0 zw!I{K8HO|?>ARv@B;)IjR_FcRo{ zMYmv>9hA)_uf5xY^7>yGhTe=LBg4^$-J^9q&X_afQc*xwIW*LA7{QpXGxL3W8RdM> zi*|+P6yqZOe^IQO@7Mj*sF~>WVZb!bJNR6FALm!@SDgj-K@ZQiUWc^g& zrMkJewJ;~J&bQ^}RM^0#IVR85twI4WXRUmZ`nT(~^Ok?5g;yEaV9OW&dHZk&SqT7| zrXcjRa?o~hvT<6zkuEmBVHfSzn;$_LSnSH;<*DYdYnn$;j4vGGR8|AY%yC1-r{9?S zE5Q)+C7Av%S7rH84R;Njk#DgCt#8Esrpbn;goM)UY^6fGC&9sD@Sr7Of|EUu4hT{I ze2yN05BGPE^y#8Rj`+HYT5Cvm_ersk5C>Fgw<`Elya$`*EU!u%{Z>eqg|P7{@SG#a zelx`iYnvySK>Yjm(6B*4t`}#QfV$K|cuv|ZbUz?t+63$Bj)V;`h4|N%S3Bld?~i=Y zk!-Wmf4?{>i(gmD$|`j1xZXj%W!@m}#4InR-jP>ZYhqE&KOd1*=+C)K_q1|?CfuF3 z?n?5R*Mo_VnoUkMYh)YZ)m2@-O3b^1bvtFE+F!bxCU*#Qc1x|E^OK6uRtmdu$J0Re zn$ggQjoAjl-^$;8v3eJj{+YIQ*;u(SG4a;q8(HC|9O$4xDl@>%Rg#!ms>}h0ae>Bl zXR-repRZ8T5@M}%ztn|HTb;9%yOA#RD7E2KfoY;IJz_=Pi%m3ktxynGZeNk!H#S#N z+aIL_$uwEU!8tE1+Iq^=rD?INOZ#K>G^gCHc9P&dIpLlBzoj7^F8y0j*1Dj7U6EaB zz|K)5o)_GMh5aReFgCY!0vsZsti~ws0~uZyt90#W$-_59-G1sG4<61#0~o|;oWF)`8JehU=JDQrjobT?lGqv7~? zsqKJ~l~s=nDK21QVzaS{Z z1rAx)B-qm?9fAA=@Q)j>ECrc?Bz7D%U5!mvXqMKE31iNgYg(A-|4n_KO@;#s^a;h6 z)YR1Ff+)atmjKuo#%Hhu+!AhH+N1M0ZLh@W2FzpHr-9Bj#h?_fp+6rhB@D^%C&jbTJZwMwMiysg;vIoD_VDn7O=^JOkjw)H2K0=9OX5D?C@evX(eB7Jzx@D# zRdVS7k@ofLKf$Vibu7n>Uh|UaygjiA~StaHUxMO zxYfbvp3|Aj7I^|y^Qi*-_#YuHG!EKk4|rc#@Q54wElX1lsZS8*&$5wqY{Tenw+@SJN*Hlvpt_vHp8Uei^V$*s#z5g%mg2Cx8MEtC5Ck zZ8t!pzgnu~9?qQ${B$YdN zCfdv#AaNGl|9X;=ap!pR^YdYU>Xf*3n58=+qrn$2To)-5e+qQ5g!;e8bF-vEsj<2F zz+QZhVr(oTIe9cv$eM||5<-J-eB$Eb0u<9E%tnL!p>+Wbke4reV7xt(aEioUO%0?} zBhbiGR<^UUdZ`eW*9hq3?>`$XEj^L$c+P=6TzW$J;4rT_galVXP-Lik^3hENWu3V) zxMd4QYCk>b(?zq7CAOw;N`+?Ap|LU3J-h;_#1-mKsEb2La{k)@rNRf8|Adr1F=#LR z%I3qm!Af*1W03hr2TXCmNZ-m5+Y%6Q1+jvvKasEC21A>VWH>;`7N$CyQ<#jMXD1gj z>6twW;}0uuh%onnh*9uHREQ5vOhiGL01V6lszOamyS%=^aha(ikc0$O*--2yw~T>Z ztM{F)6pJIs;(>+}Sx0e6xWsv8H}qHnYRJ9%7AL#l1sbZW8J;!$G9$!+VVH!X8uoWW zNL>x%(JlHJ!!>ER`@>)wfJ^0Pk=mUwzkmEZ5ai009)ymKGCA%$Jqwi0tGjk>oZ+8` zE+jH5+jC;9^D!{G+N@6Al!JyT>sv>u0vBkjGSmpc-Of}*6%g+vtyT}U!yzzg9wfh? z>-1-Jf2A5)B032z|8o&X!7?1W2MeeA6ItU9G`=DJ{GgwXCR*X3D2v{vcc=Ph-<;p! zjkSyKi11Ea1p%u`B6)ME^4l(@KA;?bcjy?3D?6>xq|kjep#3Uclu1I!<4>o#xB&I^ z$#(5?pbQv)bT^al7JHkN&kk@IhjXNTe@no>9f&K)$k1`2``FqlnB9@qlwg{Rgf`DI zWi`dwgHPd}^kdl#nt@@qTChf!1Vs(Cb7jBw+jb z`c@#I(*loKeV&DTQRo1?f}=5db(LaemExn05}zE}!%pC%??-=1?5hiY`**9ye1ecD z!QuVbyG{(o^z!aKNSKpcTy+yqYDzA$wFHaE6&E-^An&rF0hry@-$Vwzq>4y!a8bb= zbfU$RN@qzFq_lC8m-oj?DWJ@`T@Fjwy3Y%Z@V7|Q5kpwm+aVJE3-HOeaIh2To*aa$ zqobpZ&1e1OgUrnP3j~E5LHpw@`V#J5x6)g{f&|77$5p+VQ$7Rl+Qy?HNPcos8|my) zxcu)7AdgjlUAfMCphj5nG` zzbZuiOv|2wm6o2OqV;E@txlS|`+8kn9l4hzEfz#QaK!?p6qM|LOQFgs^V-Hk7=tW6 z60JM-UWT2!v@iDNcq_#1?obajUdpTSqJg#-V8cjk??$$x`=xtJj-Q%A)&qbb^AZED{wba$ z3eYc)P0(#|hYFq0RWpK=b@)2Pb)EqE`iGdrg$V|fLo)+aU?74UKR%0GF$^9T5Xd~k z4Nd4%zyQ_irnA`y4<`pIC%%t2>nsXcrB84Bz2dQAIe74jVgo3AV6g^i($9~Lf_NTg z&hrd={cElP`a#&=N8V!xT0#xc|nS_pJpXc4w_v6H2moB7fZm>W6dttnGA=Y+% ztnEOoL_>Wjm_fVIuL_rVK-Eh{A1lG7>S4-ks}H?C4hq~v&<7$*uuW|Dv)pRB}PV>r+2s%Mz&S#0p(nJ}4*`wfV#fVY zn*0Y+c+P55PE+g-2o1WRWTLT(5>Wmg7ZSP(y-HSW65k06cW)@SSKmY=%O`)9OBNz~ zVODcKFDz*xIEj|+R^Lp2Gy5msypppg@-BykM`*{x;PT8TH}29riPXLz@T~0gD&OII zl_xk1HeZU}8vvpFPT1l$|DTde_>y`k<|Ga%E$&Q#NpT^5uYu752V_VOMZin|2^jPy z2xJtfRw_Mi*XauEv@c;@^-i|x&EFd7?G`48rfuEzr@yByPB(OyN}T85PzFlun+A}Z z{Bv^lL7EbHFu@muf@2n(MXj~Vj(Q$^OQfNmUmC$u;{CJPB&+ zq{KI+%j-swr8Gj)e+Zt5EdY`TG9i^aN(-#MC6cMEYO0D-N_~YJz5fDoSRlctH!77# zI=oUCN_$qcXImOwzv9peV7je$!uNSWnuMmgyp?VJ3r69e?##V%zQYu2K4H|7%mI5S zF;32a=Pv`ezy8&(e?w$^zk-mN#qfef&rS_S@W7BeJ2lfso&A+Qcxz4zpWK~jFE5|j zdE>cFqA5V`@cCv>9pGV}@~q9w%+HBwK652yr&I-O6?EnWqoU?hI@iwsjhhb$)~b=k z^yK`0oPhXt6!mHGe;RFUqtXqodc*@Qx1jK}gITUddiuoO0;V~avy_B=5y^}fGGFod zagP}QtdkLJlXc8Tftee*bZJYPUfz-$dKs_4g~J3`Yrf({jpf3tnl7S0E3e~DzYnDa zy*q)o+goJcr0U2MvW<bgQak z@srPEYaAH~nTD1_Gn6#Q(#9dz^Bg~hr_=#4d1|B9Po1!(k4bjq;E$ayUQ-tC4+ydL zb`J0>i%O7;2Gp;ENBmD+7yHXet!je2@?)g+>NQK+mK$3#M(EcTkk~_3XPB_>zGD9G zEUv!lT(r%nbdCW=xSC7&G_!Wqmj=Q+?e#=xXm+$X-!U|#b-1+=m~k~)V>vZu`SPGT z8!Qb-O>*xNyPhH#0S>pQCaZ#?zI8>=7uqOr7X#oT4R-mmmak{jo|BE*AtC;K8 z!L4eJ1pznJ*lw`B=>0sq)i-+#=m&nq_;k^d{`9(8`)tvqqPzC4^azdo+01~!(&gjL z9OfPCnN;|&xTAP}ee0Sf^jBb#SuRaE$4t*r>mPD$(>rD}lt{Z^zFH1Ysai_$S!V}} zw$5|BVtSVOP(|U>4~k^Hwk-(cV3FNVCp|!D?cU}4goJ21Ou&azwm6)WF0F zdm&UpYoY56w4+o{)7uh#U(Xu0mV&Dofiyz6x*&4S@%xb%&uY{jC7!Roc}=_Dtf6Li z&8&Lamg(Y$Rr^DPM!MRGW5*`C!XjdH?+9vM6enY^)XMG%iqXxn)-}6$@qtGAmb|<1 zSNF6XO#83PS+H-fUacLc#sWj}bck710Belf?3c@j-RB$O2L!v}19d2f-Xb1P^UV}5 zemM~{(Vi)lpmdy*^N#_Wa|1k6YiS`?*Wg^tpTLyd&kL6Ku|};4ebLUjnCR7XUQu3 zku?77{54X(ZBCBc^t`vp*3uk}TOU}TP8-2Ag$LZt^wv7%`~>yx;k6H>lB9B!57JN4 zmz(eUcwkbR|F(}=>hpFDGL^heM>9?7%ZJ$#D!3kBk_t#|$TyD~jWCW9F*fSVaAdhB zI{9q0qGI~l9t)v@sblx1kNxmSU+Uy~xR=DP)4R=ObpuNqVZ=a$7K+-i<=CXYh`)u= zAhsLUM7OJVnQ$r+6FFBd#9W|+QD*GXZDS-6k(?xAQdWwD=%M663>o~3U%G!HB1+s} zR3<-7h}b_0OQF0@J-CQ`_IxBzcHirMXG#-zmF+J*UN}-Ckc4W+-`TCj`rruhrT9ir z-E(r*-$#=-`309tU;7#vIy8~;xuZ^Ymy*0J)ow3$bz9rL+l@;|qlk6chn1B*6&@}! zH^MbA5TkUmf9UR`k<g~Ad9ll~**oEU4CY0nl7K>G!KM*2?m-OAE4=XO5*UToQJTzR<&Z2BcJJ32D=$VZ=twRPS2;T!(s#)3C&SY{#r@0%pK3pYPs zyF3@7bLT8oLsq-{xCnJb{lLvvX!q*=BkYDw3n%@GDfJ~Z8$<=8wGGcoQS3hICt`-A z=y7$tc+3}~iLPC_BB$8!5RRYzl8bshZ~f%?C*s+UEN5J-+~giP8rd1jC4@Q~=RZkq zmJ!oEq#Wmdw?~=obgTfArJNjf&%LdC648hGqnwj!6!uicOM&4LK zIxos_ykkVb#AUMg4n-UL1sj*T=+BF%Fl75X^g8Vq&vhJS@Pk)7Mr8uhT$R&t%92DX zgA>EDsGEZmMy4_Gsk>f{(daavXZ`9Opp$9h_<(&MnZ1lms8+^@F23zk^M&~5A>XG@ z(`@!*GfF4D3u$!DNl-nSv_b5lO>UJUD*0rBrz%663<4~9gq&MGb+~U#bQx%IXW00 zZ}LF)SOW*s^O0N~UB;b~4Hg;HMthJd#2Rsr6=h|WQ?rNelJRe^=ZNGi7r4W?n#U%B zgZWD)?LH89S~i8}|2$B>%Y&ADe)=zz2dV9MdE<<(y0U8hSMKW~2hU76#>lQk)OKJ93A})-JyZ`E>U&X6N6WoQ6tH%p^EdLUkp)pC71n*Y6fv3bZ6bY?8(n17Vb36^OD_q&*?pvyQb-lTFAI#jfaxE1FYBj zTOz6?dcM^1>7?7bA6)LdasW&D;K=s(K5rTv)e~+L<_P;3_q;f%qromApU-U!pCdJw z%8sh}(doU&9{9;}DSTDX{^u^7!+7hB0}I2E)~@@oR}+GC9vSh$wBo!9H)#$d!m?xK zOXZOE;L8A3PtKl>tcCqGt%}49*t7JVFRrFXI@-k{D`Ss_>**{zPcPykj%V!g%iw*S zfxU?0_dt(W&opu5YNU24l=~G%$}J& zx_;*QmrD|n9TtfayFVr&ifvsOO;M%Gvxn6V z2Tvy6S|SzGX-qUw`@t7gp)Uf=our}@H!53YJ^TjwU3$fFIUr4|;M_sPq~U{4s;K7?*#_rclJeW#@>)Ksj6841Pnk;2 zFLKXD+O{i_Z}Wa@U1)mD&zy!d!PvHt%FtowpqkR33!O31u3u_M^iSvBU=IELO1MHq z91#<7o>?;O`^at7CN0(A<-HU=_QQqWSkkyTJKJKs5TC4;!XKs8Qjo#nXo#Ybv+UFQ8nfvSLtsxsh@FS z{hFAqax1W|PR>VyU321^=I(FyoGG1lO(HfQZ=#-?*-1%}G)Z2-KcWmWZ)y2`)uJ*{ z)a}Qh1;gcFA(;vW#qse^GhT4!cs)@>h?(n+)ec|HzbjH`);KVd*Kh_ipI6$bCBmgh zhVcJ&!+LN8VU3z&8A&&eu{23-xq2hG#SG?tbXQN#oenfyt23l*^)?a->&vB?lxFwo znmVs&X%aXt;6}!;{UGt=RZQD{8u{I@lS~C8Px+pUtYh-`K7o)&*2skvS@(*wN3ZY0 zf?gCh?}F?6$$-OAFNrk{@XeKc>^puoq!U&e9ZFPxoVdk^b(1Ca&w^0i^GM%7YI`Uv zKW{Nx_=+~_@E*2X5^?LqDtTPWJEro!f!7j8+3EiBG>u8&K<$CLODG|J{_6*%%lTK6 z?utG9OIUxJI6~MY31Q+%#_ysqeCp9{S1OahlWJiM3vPx)#K@GMGh7m4-=~>=yTHST ziIG>-)0&1-Rg!!5P~EYQD*mD$-v~yM91T|K8=!qY7p|3{6xanME%5yK%Yq8oYhKs&92cCaSy6}TS-K-d z;sOabn>@6Y3a-5QAlE>XexZxOLZ3H5T1+NxL9!Shyfpuh_c&Ly>NQ@Be^SfhXG}FC z^VC2fIGr3*ng(wQwT#Z``-Bhz#L1egdg6@ov^b)G<7vpNFZz@Yl&jFbi-?E_T)uMj z^U}U8^_AE{7j$Q!FB>7YL}HIdP9N^JH8xJH9|5$PbW60D*d8TnKczy2iwK=zBY_WX z#7@1wjd47K0S`-5tqYOH(!>@-A9`wwu!PxT|mo{4#e)y1q zZ&>2Hvk8`2!LJ=qDCh{f9fAJ|4TI%Zi;eQ!J*g^8;4k#`y-P`d`m`Ref?ES4 z>&Z!hNVN=U>8yCNN<{^ds;yQ}A|uS;WTTxzgXR+XluFQH?dv-dV`tJ=Ai>gUGi6PW-PMBZgX*|M=}GCz%Bv8<>zOX)Zm(XA5tHY~ zL^rdSNHTo=Mcmq#86F5}$^I!hvRVSoBUV@PJ|!g3M{{>6QSx{*AAY8j4OU{)?kh6| z3%$_wBl7fHU<~yZlE!OjIYe<6cPJ1Bqd~wNyS4YC<>P*&W)(`q_jO@G)^$R|%L|KN z36ktlO=B)^VYw@VyxFwUvcw3B#Rl!Y7#J8bbm)|rnCR$wD^`ww3B^`Yz>t6(n3FTh z8wwI-4ug{Jvpw^JST8Rx7})lt0+kG%FJZ9mmY`h0eEzo6?b|&LW#*M`(|*vFyV~!^ zE->YxC40A-mnK^T^wGw8MJBm5Y=FcW=G<%ap%~2W7bo#EA(1NSaI7?h*TVJ$8uH-K zP>ppPy_V&<_cgnWhfla;JJ`KDtjg^rvDnQXANS1$NngS-rd#v z@uQY(sn9Ut@f7J?tL7%1aZCp)J7Hrvi<9gwk;27HOF-5v*Yx(RP}S5s4yV^9zi9W5fVvkU;w)_SdyvV?w~5#&a0 zYqN$KE(9LSz^RBxz%K!3amJmUosEsqbe&$9hX)THKqESN?&$a0^2!o%+VaHsuHoR= z2X~T>kAIsrAWv>=4?LN{?jDKiUiPr+K)D9{bj^W3m62>B42>H?G8+_f|+M?w%QdDbBCjg1m1!ZLl78wlznb_D^ zU`nig`!+r?0k}MHDDc(-SI?ZHiAxJp%P53{S<>XBxxB2z`({NN7Af}!vogTp2VPTh zGIO@x*9}#dq+>!mk-`Zk!DA8VwUZJ1`+U}ZXB}Ch#f_a^CFD@7$&B?kiySKEMH(oz zJ80=VvQbmJa-x0MGrIQMbZs*`}S>6Rtm=i zGs9phNEiRU+w}zltEHJ(@BmBR#cN!)zK-8I_dsZoZ+DvJ+4YD zshauli7$glzl3%Rhs1Un>x*FcXP(4=OxkOj7#k*AeH_6`7^k)-kR&1tDbI6`!=g&M#fQCUpn2oWk~`vfIuif zq44J`u9;A>aT*B$=Yot-!ER|P1S`BWId3gaEBB*>Be%Fh*ltDU#11064+tNH=a?<& zHschc*D5+;xh`?~wAGK@GPwHYoy2F|y&N4ugGavfmVIpO)tx(ctXjCIq<}{t^Zm0w zOo{|!7=CJ`y-KU9p7;jsX{PvL7d><$>!4&Rk9xcWleUcP=4mZjwo~0dqyj4%jmEr0 zNV?KjbmT-;K|lpJ3rM`Uqq)oP7O^Z#rfVXzU$7-6##2C1uqU>ZrgBunkOLKP{>29ds}UC!F$ zKgB;vy@p^{unq=x!tXmB`f*QMJr%Dq7&V7C#@vI#PzE`Z9JXV-G!<&YXmsH3F*$3s z-)&zIv7lsos=Z`OJhnr|>YOaLYk-!{SSWvg{rMt?M}niypwCap6=al?+!qz zittxy9(o3>es{!%KM=~wLsrO=oM_%S+FQ7fL#mvn_fCuLyke=g2VYzxMSrHyLKG8q z+2Dhli2rzK6o01B>wNqpo?`E$8*zVxk)<-cD=M5Qy86am7;4#n*OD`<(QCz9=au?# zg=fWIU2GmE2+S43z?mD?D(d#+)qfk5xo*}wu(=tT4o?lU-q}9*R@gb`I8(+edpX%=?U{ zGKDU6B`3B!Z-?4NWbweEL5Uk-!;mm?8pp8|OwpHr_@<#U#Gf}s?}R(-mUr;GDu>^N zUt5y$ARp(n`9-ab@25xO{lk3|>BPT3z-|%kFd@0gP>%;L* z3Rab-R;!FT_50A6v61u#;-`^Nl970ItW1uT8pp@U6c!~V{<7!yc=68)EwxK&Z93r| zm@61*Nib5^_MLZiy82p5w)1AdEfS`Dh^w{Kak-i2uau^44ERu_h!#JF0?VPU`66`& z$-C|;3Z1|HnN`Bg^iD4f6DQLsVM3LH%f^qjh1+_B7@s8|ksP3qQ5d>$ZXrd_Ciyni z`jH{$4}W$1(`cY% zT$mkfwyB}VDCWx{pmNo+yN~TmDo3tvs>qghLT^b&6#;<;E zs_D@T>Z?y#ZmlC&-%l(?*tY|p+^*wM3PSj=TkWd+sc?hjCa`DfT2|rNqUz>kq98Gy zPvz+dyOV!>H07(L67$(hVez~eCOKZVz*MxBcE=Mj?IusjQ?q*u5$cFX9Y_vso{Bh) zQYgBipiR?E5c%j)TypXu2z6*lr_XC8U*J%E6f(++kvN9q%hD=^iFI0}D-1ukaUMak zVoy1DKmPbWS^{a-lB7~8??b?1wPYN4?-DZLn>o{*{*GWrE$)JMxrh9bVY0tI_N%8V zGUHBk4U4_#9Rbob9E7M;+#T4oKsY8CrVSTt~sGoAJ-Bc zHgrUg6>l-d+fS80DoY*x@&&5v0P0ZUu2w#ZEJvw&b{0M{JuZQjP?dYm#dYo43jrSJ zl~pP7@}-l*zM}@}i_mL})%cTR z?=>)@=V8t7a$Eco?wgh}gXHFWwjg*;L(456i)$xHQ4uPSA3u(aj~8-Gj(nor2$#|S zTG^Kuj2)T(>bf4!0HzmIe?eUP7mc7+Y%=Sby?OeZ_vqbw=kwE4pL4g}<~scnH%u$E z#kG-p{xVcFZ0bM6cNnxndh*iB<74Fq$XId%veRz&3LnD}8621jS!&W(R(`YPfw?_L znc0&c{2W{YQakOhtD^LKc!^ijhfCR@mdEVo>6xkgw5_d+^&lHUnBLyg)B4pQ@*s*b zf>+h?1N&uXRElbG)6MU1s6w!^@?@znGKRw{hT}A(wR@TE$>&3q zCHcbov7&>-tL)+1Q+Y=={60&ynP@O3jW``| zcZzH<&XE^YCiz-=-&Xi=yhoP4;FIZi7@m!5x66N!{gG;6Ruo@bo8-C9U98>iWs`6a zrR9|D6;?b+^6PX{A%XjnatXit&qS+jul}FFE{UgD*Ye1adp#a6$fIW`3ZxWtFP zl?Np25@don+{xF(#p3*#@+}`^Iw_#WMyZ>c#CKc)5u}6A-i|KYxBHh|>(@KHHbvM% z2aH~gOOJc)KFxfCH+iE7?}{&B4|ByvS~c{lev; z!qfsQ1(pie(dGR0_1(qYA(pVxVV>6KWLFR!&AJs#!{6WLh=lu9Ma7#}8I5k9T?QEC z=g8y1=e>Xj{?P*CvaqygV#D5@QD}Yc=v8iEFMKMvB+LB&4gC2l#Qqso5he%`n3|ea zLs|0cR~x~)x{tV3;-I${*?D%{=0KFgPk|=Ej!5`Imm9xh6-{afz${`6xOFV_urH#3t5zmWTRORcF3!p4{&S{8+F-!&CdU zkHk+*&CIm4w`a0IEm*a{9cS(!7!NM|05~CwdKoDiK7WReQfCv}018pqyv7j~>{W7aQ6BA!4iI1M7e<&&~Ep*OV3qWR_ z02yd^3+3JwEhtW}P|Bx8#@F^ryn&e{^Sj*@P_WO*py2SRk8^misNBcoD)wnVuU_D%(a53hHF09<7 z%bv{nIKIZtPNi=^6LGs{^Ujr7@BZ$~yfo?@Y#@1#D;L-9E-x2%HZfsbS$iYG&(E*L z8^g}d{=BOfI2IA?P{@7{bx8eu6S@cJHNmRbc!UT!jy6s0?N%4a8_bwA9 zJdrL>E1zqN{ehUVhDGV!hty}l&8S^hcRk-UZocg~*P&HCHco&)+Q`-aW|SZhTReD5 zh^6(!*{&jk;O!~3*F_iIc_QKs9OWgZz8`BouWDI)uaF^lR`*O%OydBVRha?RguOXc z{PomXLpJQu=K1_Pr)WZfs)6>x5gz*g6Y=>4+e4-|SFjhe$eK3|k~ujjc$Eht%!(kb zVlO~B*=S@f~3^e4v7elJ4J>gCIq5&U>+1*{cjV(0U+cYCUN z*KiK{i=7T@e4)W>=rfWZ8YiQ4y$_F=P@Z z@XP5Q%jlj74$RzNtdlMLi;3`1xjqWbkz|#rHdN-w64CB^2oT&^DDN>zdmd{zqNPp1 z7ZU4XKC&1Yr)CdDW4mli8!}W6l3kuj*q8Gcu$)=tALd5DG^K7K(s?v8vxwG%>2*9s z3R@yOyXGW9NmOiEQapkwQ-~?qJw^IxPj4#B>j!7+5NkFJ13mqj5H^d19o-6Mc@@T& zw+J*Di#zuf*Agb2=UjO#f3}*qSmjubdZ=fO;64JT%Z_o;Dg3Xlc%k+S>v2$})(Nox z#9Nm)lIg(ite9vnMZ~L*F4Bmt0?3xFJz)K4N{LVieD8?Ow!^w>}R6> z=_)Lg6YkC-nz-Zy-SS{wL&wZ_&CI0B-WU(;?W>4?DEBgj6vS-&LM zXE=yr-)6jehCH!}g8xoiH1O;Wyr?C!}*zc;OP#pNBjdo&rBp7zm`%xAcnY-o8KkD(n2~ zTJpISdU+3~dyUFOP{{dxk6!H~LlGpj^NM0v=*%`t)iS=A)3iW-9doGDjpqBKR|u<@ z>;suX=UXh!$!a3Q8E|Yz|6OPNK-j0yDUm!c?qx5%a=IW-pGWDEeojed=DY3##RJ&J z32iurC0BsQA#4X#BB<7x1VnZFYP#PZ^7(gVsTg=4kry?PS*?93-M%P!Uyz&C}yX z4U+g}MMR>hJ?w>JX52O;O=XlWX=-Y^RLEzjoi4j6ne_gOgD|~eX)0dG828GdY<+DF z%I;xts6g~YHMtM;F@mPW5Pk#g18Qs&W@cLgh4oOxsyeW@p7s%Xa+6LKyF@iv3g)$k zgUL>q{#b!%d~@fbHRZ1bk(q*~_&{%rN8gO1xuZ?7h9lEgUqPMIhA{N)^RBOrJHR3u zHrLgySEfmk@d3$Q3OsiG5!uk-ltc$w3#TLb4Ba)My zL5sM%>-+TOlN<#?!*;9N2Wx|f3#CptooBrv2^uB$L7(LE5Zqo_L4$a;{47u!GLb9C zM{jX2r`)XWWLnzD;bbnCG?%M!x>+M1QQK#!5gOKEE`9AUeD-AFi67M*xB@1}FK;Fb zm<7huqqVm^qpjI%5t=>(Z74~eD&ZvQIT@HZEq5G&dkR;`hw0_Z+|gRG+Q;0Ll>>Du z&`*>B=O1%Xx_%u`IVjxH2+C)UIs)DIgaid$W_!+=lcQ2JC!M9|Tnqj0RXy0;Jg}S4 zUQtm2_b)Jy#>dC&poVg)z#lk;lAR+J)2-wXEF%y>W(Zl&Vd|7xPZPEaS#+AzC00ti zMP`g{MiouMyMcDwEm0>3B~u38_3Qtf2t;~>zJp+%XiNcx8A2#!#gN%gE+LW})j`o$ zK|VLWh-m=>eE_ zY1A3{qzR zUIws&UnG2vd(m{5TKbZc`>=)nBNCZ}zO(VkZ3Z}M#*OhzWNOMUg#M_yca0Z;^wPQt z%g&HxE6We#^p40{I>{w5rH41(zrQg)ruJj-{dbg-@5UPeL+W+y?KMTeyjtDBqlG{x zB+py?7)Vh_lBbhVT3QOs_T`aeX)mB%Up6r@(bZMIq^G15V8YLd--@&mnq{d!VUKEn zJ%Xt@O&D-2-iviWTrc-pw`F2t5>1=~kr{w~Minkypzakgt17;|WD&1XgL2Zqq4q+=`vkf9%0JNbe zq>7)PpYJYPw%(_%n~m^?VeX(!I>@i9okm{6k3k1-l$Y6d6XxoMLMLJ6vu7v}z5#g4 zz|0KK20hF3^hsoA>dYDXCPnToOCF4P2u1X(+=r%$mzT=KSIGr25$5yLc}Gki;|~ex z#1uzMnX`F}qd(1j_Mx*mF?6>w=?o?tE(n#8bUs)$=g44y4|+j}0Y^<}6!ByB>4m&@ z1I32P0*c}WAteUf=$tj%eE@l?9T2k5xurCb&-A!;?gGaHbH=7A(!%8j9A*iLiB^IY zZ!PTY>?}M~PAVW;AZt)1ZQO<|2aIQ*M(*PbeTJCkr@!)Ox;i zA~9TR6hOx8bx(zzesX#L>Qi^2L0xMZ-n@Y$1Wd9U6%THV^4uqw^Sx(}C-_vCCi^6! z3d2)7$F~FuRD&u5)1Y3Hp~sI=a@Wyuxp1K&Q!}w~H}I{+H?G3z1I7M#1|1frrY@fO zV#C9}Ta3281}({9I<-tV6TwDr<4I8^fC-h$VI2Zwbc){O4D51~u@ z=kV+@SP8(+Nb%jI+Z;n!6Kz&ZCc|Q{aTyQkvVrw<9@fCiSAy{xdHP`FvHRt1C~$`= zZ!diTDE@Kt{x|#A*VhdUWR2TTo>GuDL`aEXIPDQwPs>Vz!B3x*d9GL9oy#Fen)!f^ zAr%6~>b&di(o~5dxU~2zeqFBsd*BcB={UKGI{ydP53p2B2aCzKmP}n(c=WD8?ATv* z6Eqhm=4oeh(zp{)soPwoH+8pnu<6(E;c!Y_2EiPboay3v@5Tv*khHkI%CxjJQ1W(W zYSPist?rEF$_vbNB#UC=uZd!gFi9#JBp(m?4pwyUS%5MmHgtOh*c0HsfIpRhy<#O8 z4^*N4elVkiIAv;t!sVt7!mAIxhC?$Cqrr$I4qS@Wtdx|Lc6MkBfJx*su9m02wba0J zgq%Fjs6>rBIx_D_C4r`lr6|V70pVqjaOJ{F3ih(p4cngq9+M|QlC`j=^_R8}Mbx*> z4i-nD!;)7G-swlxi!$%{ycE`O`YXOjKqnW!zLY#6hY;G$>ve2La#~lI7zU0FAOOMFxPIJNJ9xWTtM}bpk2eY4&S)!$ z;&AUn`vRYak>Y~6`7kU3Spa@hQyHkGP6%e}m4fmdoS6aPotRpIbpqs_#YLxEw{CfP zZ9&VPopz}Ol?=hmgPDA1U3vUzaP*q|N9*#*rvL^|7I%Ja@zSi)E%-ZXxvge>=#WcQ z>RStGzumio4q3Gqv-Nh@%8)?a0N?$t-rnqPhMpD+pxZLPUSPd5OL2fvTP300p}%eR z)=v!It(;l@r!o-szQv&x@~M69DIh%QvLQ#oj2r3dUN14?qv1K%r~EXc(KaM4SKrpw zmZxIVt#?-#V+HE~bWy2;cxo4b)(i9V$tXlix=y@&Dx8D&>PIloo)*83X*q6Pv;mYj z*sJgYo*Y~YH|-HcJ?NN92Sn}CI|gWddD^!kCr3R`pE|-+Utf86`2BllFlX$uRcF_U zYI57V(BBUagEQot{Au2!zUva(NXheJNu#%q_wI!>e*pP~gpyMPczE!geK);D`siOYTGNIvZ%${W)Bu@b@8+XW zw)Beb9yR!YcChVlQa1BC+DkAeqkd1K823X|SO9of;Nv%!{(@}wF{PHoCP}x zvx`+*yaSdb#KLj>?5BB!t>^SQ#Q0SjN4X(OEzJFNm8tm3;ok`rlmefKVs;!9$Rv{L ztoctJ3}&=r=*zlj%@?uXMqvvcJJ}u;c6#}Z{iQ`bc72u)5Hm6zO?=)9njFyzep#!e z+JecZF^QK-@$Y%ao{GkH^i+B)t9Lz!O^)Aq!dmI+Yb=5z*?>_u z`yro7N`@+HFrP{|Ut92VF-2(u8e3eemfmly3jl$MB7(W1!Q9VTgR{mo)N>4Ma;CWzK#EFch)V)TyP@#iG)n)QSRG5GzG~$uX%m$VXKc{)1G+}bxL75N`*O}=Zimg zZ{qz%_-6uMCxY1ziy)$#^~v-iSD~u>2R8~m%RiFm|q6Owi�G^z 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˻olQ#P?Bt!>3 z;aqqr&p7y4VB@&Q?;yU5m#JVTBz3@$Zmo61L(Hp|71^-)K@>UIKJ78=GJPjZJZiZ} z;yE^DxYGyWyz$0y>fw#;8KcUWEQ9-7y7A32{9W~$1f{A`t{f$@IdK|2sm|4EecmHB zRvWm>2;U-@Nb&gA=}~L$IleBS79{$M^Ea6t3|Z*6eLi0fQfz}4-FR?YYF<8hAa9eiVBqqWJF20}pi?;Fz8 zm$M$d&V_^jq-w;qmzQ{Oym^& zk%)^Pt!K$zX}MB;>1YpTYvU04IFHX>hUz}6?u}>oV(i*|gjzhzMBv?T)sU`kTDUpC zJE2dKTlTV2l5Ee-s}3b$CcYP$tjSMF(1c$~xH#^;PY^zy)-Sl~?8c4@P_@o z>8+@gY@#7UUPLZ+1XK#+gfUT6(sUg0i^5n}XJ=$X5Kx4lc;*4{Iyy>cYdn?aO0yZ` zh0{Z~W~l8rWc)bpwbi3y(~3|rdvJ9vy~ga*3k|ob1bFP@Q{QIjm8u*JR;i*MXrKOc z3nLwrkI#Olmp#!XGZ#^0)WN`Ex|c9KQagf?3;s~V#ZT~JN|ue;q|5l{CN^86^C8NE zs`h*y$@5g7(vIx8*i#u>KRzOuJ{E*Wt6ZTKyj=oA&2$$@K6$9xOD?mvdlc zIk}i}+lDUV{azWoS1ZO9m%k0r?C^XO#~R5fy6WGZxIVBq!>?SacJ0A=yF7`%>bZW~ z*Rdn&=+!@6xy4yks~WnW7x}OinB+dgycKu`Q_Zdi55l6WJ&TjD?E?wKwEBR-X0Q1d zQPKDImU#QV3B1Ga$3ThCQ*ftEN>h+tJ$z2Y)5zYo*H0lB#-;4lf5YM+FozHZYTH~F ztJs)8Yscq%P2fkx0xRYZyD3O^=>7D5NyL7=s(S!i(|d&1#SKR*qWRVND$~(Xh5bO7 z+4FQzH2}G_7L3yIM&WpeOpXjleEt4vHthXKw|cqz4J9K7PfSmrXw(}m4-H*O2C8ml zweexBgY1glL7WWV!jTvK8nCUV)&Xz&t>SWyPoP(DJ|?cuLfiYDPIopgcu=N%4rns%W(~A|cVUOg0RWGZzRV0{U&s=7?L2RzpRh>niqOyt*x5l; z=gORGi&^0go6jal_uY}%&~z-~yj7DAarcSSn&&-!Ilsi#+Db@un=mmj3e?$aU`6CM zBc625F3C{@I^UctpL%Ta<^=-ZT0X!`wVxag@z?w}FV-W9V9gOVc!uMy^Bb5kUIkuU z#SBzo9iBclE3~Wmn*I%cjBEBEdJe9Tzs}3*XHXL^m%iZZC`(%A;+UO7jD_kh_WH~V zc>TH8@2YNi*lfq>wgpL@`nb~FpW^qMl|h1n#4=s4 z&;d{CzS!Ph(-;)Ywd7EY8_=uKNUxSubptv2pV}2d!fOp)ES@ zkeb;aYJi&tY}>xooPOMr?4Sd8Ju%B=zcVf8y?iIcT6%t8wK=laLAtB6gNvat*k*&= zhR8n!PHAUP!ubciY+4!LfUi)xnu6L=R}&XehW$q++i6*AGm9JkdfId0UTe;~HQ6_| zbG%c7N?UGUOR}9IWVtznS!nVa0A{m!42FhG1EQJRdoGUK?-SOKr>r5dhaRp^g-xx8 zzfYLoAIU=x7(|D)eu-|$WeYVHU`xyLfP3hA;#uSChUmZ|8~l7hyqseVhs5AIxu!#F zKJDLY*@{lsddWfkEIqo^Lfnh1rX2`*q9uRI?r;z-s|5eo_y}d{{BPiajxvdL#%$a{LH>h(#TBg`11R=f(d-Ns<3&u zyhI@yc7UWJz0>Fa|6K3SPxg+q@*v1?52@Qv9AjSG46y;cI{{Kog)s|(bIr7$ zF@9CZXOu}p7UjAA^rn#MI!fbeaZq7)f^54N{{8mhd{B@7WG7|Q3tZRzCvjK19c;Cm zH2$XJ5d?iCY^=EPV=k7jSmbaI)wx;!TZWcH&_K%fOke4mZX%3_jhMVy9=g1k-3KO?vve6x>UY#LRN39;+f7ngK)eqJ6AT|kTo7#Ho5G~{#h>k z;Yv)r#4mHYr=1Sus$UX6JttJzN_EmC&~pGT%nu<95BTp`Lj-8HHr5CvK;KWb1;ck0 zpD6ac8wkXGs~%q?rWn=gK{Lxc1==J9Sp;#%3_Y2sX|AtPqLg}fTjbtp0Q9nPhnS^S8BhKXHcs(u_?>CB%GKi&M=cAV3 z6rRaL=5k!mtgJ@&Uco#3($|s(hVYYxc_!|`d6No~nu(x8!=ZuCaD9u16RLv?q!Irn zjyfF1oq*=}owWNgQnaDz2)v=HJZA_<7qR-97`OlypfFzsMofNhNMY`4pSg{4e?C;OX9#Sob!fFa3?JaucC+~XZ@6#+@LWNn z=tyrAVkgI(20IHm!s#$T#rSrA8Q`rn5S5fAp2*x89335Q4^tnNM0};vB%%)jfk-}b z49NEk0L!8Tz7)gg#%8svqwV?sjg3uBmY8{A!tNKy!?@DRGz=8gM3z`G6)+E?u&%?) z6YMMEE}0eql95=CXmGF9Vj3OLz%&}fs-iyA#3~+zH+CWG5|HNq0GLCQWx1pdP`g4W zNW{Cs(VDV-gB{v=+wAA^$qJd;wDD^%TB@5JquXTo_dx3Lyf?PiPL* zs`^GWJ1y}SJg7cokz|0GOw`<8wc9sv6fdsls9^9XMG%M3w%es4Itrv{7b-ehJ^|%p zCm-+C@}s&~7vJtXcNOkunyre9gXLeP*{J?eWaGXvBA00r6;Bf;aHp>9z`Ln_)PoCv z%2pc{7*noS=gu zqp`dTMbRPg98tb_TFGrhkpam-*DbidEf1uFoWq@u4j)b+#GN6nT=Vz%`VvpG9TOSx zZyFjuo{nw;Rk>$z18^~T0+@m35$f?2Kk{Oyal=5%aA6O#;R7H`TU9NOZyk8JHMe+b zg^gd->RX~Ak;;&de)rIkRl~Jdm1x+d24Bv>KDjptQIm$Rg2yUbjF4vmz`CFL^ z{6Q&PhG|xc!CGqDPAEtgpvl@9+am&jjw`x;P|za^O#y=k9)<^jxxL?9_wM6~RK)~; z;PV@9tr4dq!Icz(0hLk|8b%EQeW3ylSQv&D+qzYP2MiSVB_z!@Aix9n`i;y77n>p1 z|9oMybB%f_@R4D>Tns-~k{zAzSoZmt?Ls{ju&`V2Z}!l+l7jg}iZDgsdPzWz{6AQp zuQ4Crqo;C)uDYG?c0Ad?jkYQ-ZW9Ua>Zbue->DM)l6r`el>;1u2NV@d6z&e80sBat z{LQW+wF)vUgwpx#nFsn9vs5Wp*6%-!hja)hlB-VLuIKHO|8eO*_wYbRheA-2TrL0c z1sn3mVQ26y!}2TMGwXcJDad<3w_k+oRy6KaDK06yE)2oNNIEo1Re*4T(w(6?e`C`H zWcj`fM^i?H(ESc`^63EWw+xxu4@BJIygLQ> zNx$*6s?Ps}C6$pRRx&}S1-oYsAOUm!hyfEb_Y|Fza7JobTD+pdrM--FiazfYoNuNt2a~J4< z>h4ZT*e?a6@rxBNl_a^sN>SX2c>xPcx9N4_JLFWWwY}kYUs^-GvVY8V!svdd4r?F` zQgdZ7c2rz6=+TH2&+T90_$xvz_m$rBy($3T0p2LKC+HM`@yq>A$%wNkJ7MtYh?%^N zh;C?rU~gxLGf6D9bSJ{k?>!s&5%>-ct%P`g#Pg{`$to}IN{}hKmd4bGJ^x2{FU4tU zA%U11c*i;a%^v=z74blO+hS6KhmrStA)P(x`y)mkYl8a~_s${V%&WzC5G8MS69&3t zk;G|T&nQ7;@PY-4XG>eDQsKmd>#ZaNo)=gieyTekqq?@nWCo0eR(5G5b1k&ZQ_0sN zyuyqXTvscDCkhJEy3u;`kCy;Ak57S=qB@W1;BpN=gBg9W?x+D1fwFuwXg${kxvZ5% ztW}g?SBJi$`IMaj^%UmX%A4~GBJlGF?h0Q^b8p{41}GvCEq2y~Ku>(-n(l`GHXZ)y z3_|G@(M+{elPTTTg!${Y*qS*-vM%e)w#%B_^*fK5*t)=1+gOGH8VQScDV?3Eug zlv_s94S|Y+LJ_>|CCAN{C@89FnQs9$l}nTmFFXJ_{}2az`38=Tg-GOYsVDMF`8?wq z-#BE{D_e`Zz7y;w%jM#(@>(a~s!Cl!^XRSMSWg`Lw@1zEe87Ps{!#QUZd2z`ty-C2 zcsir|JYB5B*Li|9`OR4MN)oKN6qzm z*<$aOsifA}`qn9jKh;3@!t=y0KB;E6N56outuuc2hLewPoA+J|8JkdNv_*P4*PW8# zOIs@cdomJaLpZ%{U$R$p>^a>A6*LMvKfhJTZ7I5S6DDk2yd`zl+*>S8If?akn*S!| zJS)D_ecv5zf4E^-Qro_BK1I1XM~~$K0LC$yAtn(;b8(Td5A&uvuaJ)e)Zewvw!ea# zBN;%mz@UF&7`vduA`0jCH_7e2H@Awcy5d3^2d+1~mUN`e{OSB2l^Otn4hh#Y zjR0{@c!*a$zFC21_T^QirrC8R!wY8 zjoMm@WXGGhZXb^wA_}c<4$HOdPza1%sW>>Ti#J7?k%c=%TMn!rQ=z)&O6};%treHG zL~Wf6jIRmy-s>ZpgYo8fYJz*QtLmM$a&j$AN~T(pulod(an3b=emiIyvkEyMv429p zEu}bpUiv^0vUi?MYGssT{YIQI?nIE?XmpIP>^?!W+k&Snu8#0Z+F}LcrOJunf#}Ls z@5L@;=WWSb;|=^DKX@rCzukV~M&-W-jB!7MNUcBR$9mriQ#&ib=)y?!5`b0MhZR~o z{tmW#A$&l_C&lTQS`0sRv;Fe^!hJ-JbLY|$6aCZnVwwDY&Sp!Z^T;~B`M1=}8iDlN z#(&(bhf;1eaR-lSM|2Lz%*%Y?eay>qhElI-KCz-49$_m@1|pt+Sn3_s{h?y+#KXxn zX-Z+D3a_5zBdL_Slo{>WrzA!fvE1{XRYQt0=@gXlt~*oDpLUV?xm4NN+Q#Y?3nEP3 zB7UkBfq$niu2{dISAE3B>RAWxY5tw^fkH1hmsjxT!6W{_1xyf+i#Pf^l3&GAP+o$IEt4q;MuGT|%h3qv|P2ct%WQsl&8lZkvvrc%;d~n^no-=2v zsZ0bY5|R3)Uz{XZ2g;e(l}8YM>O^|BYtx4iKYH5z+T=6epf_1{9i6_o2a@BRy`v{Q z{;WbRSKrW@uQ%HcLvhZ4i?TRBA0MGx#w)F4W_0NRp?M(4m%a!&g3aHGmoZ3#=X8Oa zp}6xQtoZms4MqNJt$+&_g}efpE&-XXs~~gkmuh|q9a#RXhLF2g5eKRTnP6>Z9QD57 zQrtU8LEczOVh_lhK8~qc@-ktA`$(Nw9tstwF;il4U zrs)zu9#q@2|n>s(} zHVRl@$`*;Pq@EMMu&)f=X7N#-zn|Sk$aDmJdk2oEI8@G}(N^dr-Knt5X+p##+SFA# z#J+2)$|d=PydHt>%5JY|t6EP*b?H7!jLeBXjnIx0a^G2GxpuSt1tmUr>DDC_ zlhR@&2gWpc)}L4Y`NRKHN_EynQRR1nbgN_J?7)Ii& zTup4)qqF!sV~5IR;$&_f7z*DMHcpz5@B>!Qc6y1*OGAa9=+dRAo~$06J0G)@MsNN& zGq^LYq4N2NZ^V0FN=fj<0|JQR$ZQZ@KQ;D2IZ^nQ@5u*~=ZMLU|$wmhaOT_1r$XUOQFP{&;u4 zY(8X-c8kO`dy8a2*hE3VW-$V%HFZ9Pcx=)<6;!;B3Y8+j zoEKYiO$L8)WOM(xr{_VW0ldEvRtjW_4WP-UN^$1f=c}mE%kQm5)ZC+puS%YDK04{~ zgS<~}(WY{@fxnXvg$(z;!qguldC7G*yw&%Yf0ZrOb6|$Pk0!u=P`(MXgsV_}^RA&< z!n;&Pj5&imP(pr?7Xn2xq$uVZ;~T*h;&Ym3IFSk&#TF5w^~^qP;dd>(s+?X+;5%?uex42U zZImRhe4TO%T$Y{M=cD*gCt|}n(U3dcLH5#p1)J^^?aJ!ccQw2E_?i5hbo3bYE*Oz# zBf4P&7+$Ylkf~(sffuJR;vRWQjj`S|iDLVt1b*a!g*A%LCuD**Aoz#FK!Cug<;XWm zkalCcj1GC&w?t;3`X(uYeeAz(5a$R2BDZHILQJUYYZ7k z@$jj;(E0ac^|4pV!=*m6N!WeH#oaZc7P4 zTNJfgi+}ZO=9@0nKSORlJmQIsQC*%tIakg?;qh(MA=Dd#o|8#ddEw(WPQ_i(SYGUn zUYyx|q%^FhZrx}!o!c8YS15LvP9$~7DyAd~S;pEOveVWfvZTbKF9O>l;yp2^dZnlc z$7}IWuYQnW>xwc*1~pI=s)Fw5nJPRX2+nhVICITw0XvX)>KZDzVPqrzf}#-Bg(bRX zK^@44J1vRvDP$0##(RXCq5I2^wO4dBbYl|gOlS1LHcU4`U>JiT(c;5)Tpr1OL-DWc zW0HDl;<_&xV`@a3S9+?|g}f3jgFA%FMW;Vi;T$2J{^p^{6G3%KCtDSriY=}Xz!!&0 zDo#x)PAOsSkj;F9z(3k%@x0Pb$?KCMi9_c8g4rP&9E}xz$Qu4LpD6NHY~?fIP!2dK z2j25Tgh`?4TgVr~Ph_x_4(>1_JCcGw`IN~dAsuE#Rp|zW>3)88XmoS$fgg!wdG(mf zsML2wpMqYUP55+J00p9U0CCe$In~(=vSL=qXF9>`NYh6%3nHWdYy>QRioU)L>0$Ck z4;Dy~*0*9|5;{%(WKOc0EZRr|Zo?5G%rgegCxnwoKz)H8TY4yrN!s72Xv&j18j@lu ztX zUsGA=J6jM+j2TP)#iT3pQ<$J`3{ZktqMb&kmzBMRO;V@m%r-%>q3pZ%@>w9Df_Sez zE~?31jm8kTK~f=aw~&EAv{@18SatE(r91pRv2o>r|6^eML~wU)-&gZt*L%;i-Q)I$ zg4_rMax8gmgg>Yc^;?S_)DmUpgx~ z)`wI#%}1%Xgo@&y2T8x(>{3>7oP_FLj-Vd{z0P@NtztjP!NYh|c*#HVwAplpbmw-F z1!y-KQqC@Uz}8MKZ`u(kk`uH?8!+HiKtB{1ZRBPmv_%4$zEz49M~6z;8P$w<1Vzzl zsR!{3PZB`OAW&2SBhcjHkvo@2ZQnL|pedk7i++SmhU?V@)}VF736epE)__q+QU7ha z?mDK^DCyC~MD3vLK}9d22!*$IKyaI+pIcE+*?-QCuHr4*EF za2$&4ZO_G=D*>7leQf`Q`iJT~Pn6Z(GpGw?gZZao7%h*CXSf|Y%RyIJZ;^eW9}LEC zv&FIC=*AWiIKt;*pBe^cnp{QSE9#fg(yp2XR)t zZUp2?E|O|AG{Q2j70x$LM(o^Zqqws+y)@7&VS_|V%I8iw919)Qj+EZ4X;zzc7hIm7s1AIdaeOph%wL?wTj@8u8nhi|m@J&ShH&9Pu!u z7Ue+7#24E+qCNk3g1H?ev)bC`KLn|2~=^EfK^&Mqv(^W%jms6@eX4lSpg@kh?34wm^}U(VSg{zBYCs7gh3iob9C8S0+~F8eEX z(Yy(uW-23$Dm72@+g~tWymV<-_=*-M6z*jIg%_hadp@vM*skpgJ+|w5<9jZq9==Yg z@KCr^4(-9_r+)9WiYq#`$TAMRr0@-h9pW($Hm67bx&0S{7ftQ5q=Y5)q1*|j-#bt2BQ3p3w<+;Iu{>Lf zH!UO0olF@>kfN~zQ_<~Dr09;k{?eV+_5P@1g4%olm7BmD-0;M9Eqzwm0gXeE0p{~nSH zDh^2SP8-0)zf~RLatHgP8N4qlVIFWwOCgID!}ChM>m`sM*N%EH)R#4LR6qmgae)*h z9AJd+QfSkTN<6|r_Lb3x$DG|?o>b2oX=BUG9< zzbDBDTfZlB?^YZO?xJBdzPIu~OJT~Vg;FfFO4QS@jB8-9^8WJCpi$XPfrH^Ory;Kc z2D@lWT#SIvrE z|F7w!Fky}@*yH2cfE!jmkCAl-wMbn8hxMxjC54F`t6Y(&&L}fZP-Em z{hHKX@=Ji4Yysy=@XOZv(0-LhYBfv(E&mApLcrqH5AZOl8hQzC>CGi*R~df(ApKIl zx~-M>Z;#h6x6z%q(oN?JVA*tcGK=4gpXMyAj%s<9gwlwM_-bQ3c_SHJ=Pgxno2H`W z5#E`VQ6W05O;w*>GUBK~M5Yn#45v_0Jmaw6DUaip#I_q8 z(wE?0JqNT-cmVhWd8}OfuZf3Aty>2n2o$QbiB9&&_L%$#2;Ry(_PDm|);ZK##p8nz zj*3se%CTGhze;{!`2{3;0Wb31X}>M-hzWWzI6;(YBj-y1qFqHG`?S9fYDY_FUbk|M z*&Vo-+m%`%F;#%w(B^K9J-(7`n09;UNgR}60oT!V86gLu{!l$9-IU0SQW%gM2E+NI zO6oOgqdH3lwZzee9PvZgBRjw=iGO_}B4kme0gTd9g2mD9&if6d)ctt7s1fu-k9FO7 z?v9N^Ea%*Pms1-0G7M|)LsQ3Lu?p;iS$^Xd{iw3oJI31A=?3xT^_(O=poM%^H{--h z+PEbsv!qJHa)Vk$NVcSJ=BaQe)0+sF za~wW{7V+KAUXHi_5eT}Y^$lkwL^|}ZFoh0&^?7{&-4c5KMIU^mw>{dAvx((Y^&keNZoedt_E^IpZ2$aq9Wt*$#DY@W31j)yvDm&LNk_O zlM*WEW~gEq!&2~)8l8cjGH-QNsA+nmSvPPOToLnckRz0ci`UE7kPZ#=De!z(%XlEmreWHU1XrTY3eg9P+Uf}`-< z1^t&%_B7LKaPg?Jn8AI~K;AZF zwd5#SY(t`F@i+p>F??=Rn?^GAN~bYJ%@_%$tn*L3W$P;zr2;&!EiO-J6rD6mZ$jDr= zD7)lNxs$St^pt&uOR=P6tQmk4)nE=cq=r^bP&_Gp5WfsHxYl~ zaWpSwu5r+4Ek~rHsXC{s`ahTCWNAZJII~LoNy5hJx**W4MP)oz876W9IMgptzDj%r!Y4lPWe;gVZHC z!Xk2anb%!1xg_VL+>CJT* zTY1p0BaVj{Zxz9x8ka|^g-Lhw$etyf6nJ0a;i~(%caL1_UFT-SwWU2Me|ONeMLyBWjj-3Lk zcj`JN-eUuR1Vw1F3@W9Z}Xlv&5^3jhc3=;^*3R=b9c{AxxzaBPa2Cx|*=T z>5D2MQB@l@eQ0l1Nmm9EUpb`lWkB>E=OfSWRf3*(mWtntx;uI5J7k7KF75u8OE6#2 zzZO4F$p)9b%#<&*QubBoYpTkmSY}C+H-45K1;GTbxw;SC(vdY>z|vt2cEYeNY|Jh| z%jRG^q9kU_Bae{Iz9AtSWP`;@-jni83y79vn@Kq$d%W$nFT^>pKmXRGUssTW0}pGo z1FL0FwZ3yG8O5w1>=P;nOS+nBTEr#ksrs1IA8J`WLAwx+<-s!p8U9! z-I&70+ycx@jD19nIX-mRpTgo!744F31j=a;G! zQi1{jx+Z0P`q;2tn6`zs&#uDF6_ESG7(&-W#1VR=$Os|tc8E-4h=x|De56%WgxPWWl=PSCGeqhuAvp$Vp=zLynyA{B`{*rHW|vvM zQpYGQ#{Pcx*E@?3-aWf*mMVHw`_Nv&hjY~9uSKuhIcu`9NCy#emX}o}^;P=RcI`nW zvn%$L3`9^BZNl@{|0O?=8)qv}Z=c4{l@XOR0M%@}7R6E8Rl8QRmU~MZQS9OR=Z8Rn zLjcS>juK{LQR7=USSXvUfX$6HXt^AlTq-(;9Z01Q-RKGd&slv{ALX1cmG#|k5i0Mc zjZ1Z^$>3B9=bnLw{4h7+qVre}2LG#%5J|?74hm>=BFNsH$Fr{By(pPH$o1pKfC8-UGB|G?9t(wEp}IH<%lre1I2ECef5^ z!SIQ*Xm0C7YGw;FHy&d0tH;Q$Z^6uRT;nz1{u5QDI1v|8;W2FG6UI|E) zk=La-G6fEb&Y0?%8Y%A_trp?oig^m*D4gR;0aMNz32afMAY~#@nF>!hUR>K{=Sx0r zVrXa>LqnsOeewd!6E6_DLdl`)xwLMza#m6pb}C=w*w}CWtmx9-R*{OOOn9~6s*!i4 zainHx#A(SwVs+R|L=`2%aqN@0$n#Mb`cz7iwFO(Gl@dyP@Dy6S{ zQ0`a{)wiM=RFOt+X>X~rhYH85(#r*qD4ZgEPX&<3zpL2^t=6`xcK;3~lwKqq+nQ4) z86?dWfGrh$VX*G)3MXeduS*4xsODWMTj+b16{Rwa;?>oL;(l`GAn8}R&U@{Z>I_=- z-dg3#p^Vw;qpr%?FPIzfolTqA_m6vxo7E+KVo z5F0n1VE)!)q?37^xO@huM$Y5elV@1@zwaWyok2W|aCz)f1nNFa!ZeUwQh1z9JJpM% zESvcEr4mDxd!DYXlm$|b$@L*1(KJmoG&CTYNFcqjg-j+>8qL9cc6cC>z)E@-@x%7^ zHeA=0XT-~ZL^=;h?i=Ovhf1yU0kZ$Sie5ruQneogjU656@9$?vN_J%xi?egcZ*6g1 zcqOYTkc(Ku(esG3HKFZqKfrMB0G@q)4|8MB(C8Q_8ZPYS7zVGOf!)!7<=;NU?AQxL z^Ew|*6u`o=LzVvHl0?!uM!!=%2`kRi-9Vy}bIOS!t3&MJ?NWL!(TS^7P_OGSge~e> zi5{MshGNviv2&-;|Eue0Xlce)E`#jUB9_OVAw4+*Z_Kd*h5!H{07*naRAURCCj)tW zdDgx}h1+Yd?k9T=l#>i3l3r8Rl`CIe+V`bBDE>V9YrkU&uuVgjLZ)sbp$bGIjLYRkP@mZ(Nbm3G}b5Q3rDaDc}S&=K# zr8K6B^HKnbI{`;JD_9R?xh3Q<$peIexyj!uzQ}dwG8CZNzDV=>e z;-Pv;?>LO`}i`f zcm(e17M5n`kbd|aQ_m-%ZFww;sl`qyK$_=YL0h%wD0005NDK&YVH-^9PL&`n_wJKG zz0~I`=XNL{QTZAf2&*VDz7JTS=)co8FZ+B-dVo9!t7Y#l+fE~2or(7TZX{Y8QQXd9 zeRUP-)irKoWE=J_8+3ntMVzgLsE5v>ZuEY77TqV0A>SN9!E-=i<8)?vSqE_|hQ>_| zH$VRpD}NqGQ1B3SZO(EdzewVA4=()jI*ikenA)6YpsS;^6W!Yl$fdWjdTR;yZry<~ zlV<(HQ2m~L(KdmYUWlfU?H&G>0lNx2rSxstJ}K=XnGmvTuF@Bvarsq2cTomiY%=cc zD6=|6MyC>}QZ|xjn9H`xwWc5#h<<|m0*R!nu)q?CGh>1kiBw`utARw}{~_HbPFbCa z1h-UJSzUtfiN1!mPc5uMc(O%Rx<$v&fAuTae|;Vnsl=a8;Lo4`0mb<>*6k^4lxV5j zCR%@T0>^*-0kl*EZs4Qv^>cjl`#&MF;Bf1zqMQ)}i86sel&fXKPzD{!|971sRmln^ zy?Z~PwPHV)0+7_T!hS8CMOeXg>3f}1smYOIvT>{Wc+ln-+nC}p$PJ_pS*pKH z%BhOzqexZ9l!OzzfTlko*_d~I)gdVln<-jvtSV}9oZr(hQ37UE;8J0zksdv z6)c!*a7`Z}KZFhgszVh(b9ES@hE&mlrJ1l@9ljoNWKJehL@pixO>s2$b^%U+t=W0_ z8)ST2NV(*23Hg2zdBe>LMK40obEyd~m3#=HbKgT9Hb>S{RJ_4wj}sFSBkNafeD+s= zjm|KRFTVH!h3z~B&z?Zng;5ke2X`LaN7gUm+@*`?*Hd_Q_aWwPKZ2Vt!0K(qkFS4- zYftfL8C{k=%^ zwW6iH4QrGc>A5g`AgWu)JzK)qt$WD4Sc30*Ff<)n+(KhlD+WI}g^rP4WDExznKbgA zi>>V~6u0uo=kv&K<&n+gpz8+QViCn$0f8GJEO-bD4zQI0isV3&Au6N*cp}m_QV`&4 zLr!PGMp^u+5z{R7#$w^i!jI~XtH@Iw zfn00P5WOI?Q6H`uAnZwCbrSEo`i9VBz^3A`NlGPIh8>dI1xEx{b_} zMZ~r(7y&raEASmQ%00@64MXH30$|KrWJwZ`CwQ*Gi*eK$l|>^3kNaU#fsoMWn&=`f zTkuuU@VLb4#&86~r%z#dWg6M3d06XF#B>{pg2qn6yyc=83lMa~(Rt=1I*$!teQXMI z<4=%Sb`TU?M5w2eVPK)jMPp|xhX3XYPK}Nt78uAnMLc-+40B)HL-=?VmTNGtdKSg5 z`^d(rqypg6983!>TS8Ic3_MPLAeshnv?7X$5QQcUW+D!c=kfe3*gF@}tS8Gf{D^ren?&b~W-cG;(-|+FQ7pu{ zIcpi&tBeQ~&0mG{k7RSi1mX!;@fftI*zW=u0`yFXVkV2$X@~nIMT^9PCYu3bE<`z! zx8!t`{gfz)K-aL|?O^!ANwl6G#r?5|$bIz;a09eEq~OyOq>D!Z08~i6paxJ6Gsfx)u=* zWz%P{$@4{_BIZwDWRHsRklq-uY+jEOPY^3mFph@iyz22)4r1WD_}x^Ng))OFij|IF z(H8>5!_1QgzlY=???ux7EKa)c|4b}$| zjtS2t+6f1`r$s;xa^0Y)-6E2gJ8=2>b)<87%zg0y!NN8QVTedPj?Rxqapv-QJa}>! zxBvJB)23mdABpaEw6wG!5sUG@E*4!lP7#jhz|R)3zOanU(?uYiM>J=`2uThKgDf4> zVNm(=3!NAm8HV0y^BNUh2fpjV$!4)Nn?`1G1z0M;@=V0s5SkYt9~b-CIn{_yKKU5k zX9t)+HuBrZY;R+Abrp;A3)os+M^L0d5{5uTZ!&2hR;pkus{$Whz5tMHFBG^u52#Ob zb0gYD1~7DL1f9Lzu%Z#pb}tqRSlHaa?A#n??@b__nF2rq8KI@^v^QwrWjfJ|l^L3RtsY{N{(&^j`V!Q&(7 z>+V7_9tU&-rfD(XW^-9=Y;9s=V;yVDYgk@fLVjii*x2C81Kk%c;L6nxkeOY>%pV^h zx0U6Lb{`H8H*uyHqu0(O|8xyE|NITi#UeCv{P-s8JCvl+NYNAp1@FLC8Oo0V^D+ zKL4zdJByqr0wg-@kud7 z*UQ!{*n!A8XFHMlH)$F+qAvHVEyO9x-vAml7|8^Z?FlqCH6gI6ESp4Asf3&1!^{}i zoLYxAu>y0wfT(Wr{-OEmrVib;Syx?&5x5_6@CfOoHYmmYE<#ngM>1YlN%fu?3p}K~6QK;XkyyzcHYezfNq%}7vv$O0x zDp(q4bemVZ@xiY?Mn_A6b%8=-6dS9nnEcC4Jh?vxZP{U-upt0epLm3IDj*u#%)9Iy zW%5fo>UoPa*w*b#4j};%`KC6OH7S3M!LMMpcbN=xy{4VXwM|!#ev$w~paUXVLew zQ`Be-Pi{TN!p%pp7W0UB2KyOI>02Xz12+)~90t?ZV;T6QceNWxcC;bVmO|)-$gSm( zU)lh&0cQ_cj*h59z67Da(Gg8{9dj531InJFTw%ymfsTUzY1qUwrlzB4x~$VvNj=Zh zk<5f_1H}WA!3@^R$gdH4EdOb5xLSasMY=I%Fo|c8+*A4j>A)OB!6guZgfAWIq>C^u zMUVoosNwIMc555k(`ooC+c0tlOwB?fE9_Cz3fV`($9hG- zZL-2>FVnuIH6=SZXND*mfvAw9>o7yo&wVr&OrE#x%3u&WH@P&S40MxiW|+rCqy(=q zzfT2_$Y5g8>V`@iic-)yxV(=YS{KJhoFE9Y&jPWx$%bLGjOnwpI!+}B$AAg>A_$rw z-JJq{^bnh|0>Y$$lmGftj9xy6;NBdT=a!J@Z^huyF>FLUA=SNIwl^B7;;32K>l&6q{VhUD{jZ4>RbzR#KewO@5WgSPep# zbwXNxT6@~tT&+P9JVm+!J$se-nKZ~)9(p{1c%l)x)lFF2A%pv(#SJtJb)x^=33MOpg>6Te&t~pT zATu)ub3;Sm`Uss6wr!zc(HO66T2Hedpw)cp}LI*5`&M|Vs=aBSoHx${x=0XnS zpHRI#g}yHQTc*^N*CQ?pkq`~n`f=@7AHg;v_~Re{i2N5XSSGiVbB}R>2sBeePRk;1 zxX3q?Up)eI+eO0F5T$L*DvangN&Mi2q<@7-Cx!h{h`T^r10wxR=pP(Jsv(7qxm7HW zPa;3N0llap%^ zqW#Pu+S@uXIr##)+Z0@_p*?3I@O)UFCCXHj{vmM{f#Z>n74XRxH0TWR*(c1%$Elxu zR&tIIGek%_u?i7EK*pKo!60Ra($0+{|19cUYj8{rj^`mo4O#>0H)XNG=a@M*p1DLK zO-7X*VVjSfQY3s$p}a|?y9^}KyU73(i!P#Ah~X-MM5_)YR=6C$kKeu?kSHT7Wr;}J z&m*7S!06vxKucdcJ3Qmb1PoJShj*}^!=w8Tv3zeDnREu0Lqf-cm5iX{!XSpPpF#7; z0D`Cq(*<(d85E{AFf%!g%_pBIUzJ;bJy#rgl}6Rgjz53UtryBSCfv?4j& zj;0QBv_z0dB+wFXWO@We;Rxm^ArhACfY6|mQbrD+K!-sN&TJNrxEx*lco&8*oQB(OaR12F%nBY)JjUV|k66&hsr*Ak zL+nB?E}glEPP+x;zq^TJ z4RoCxz~J?BXg=Eyt)&63r6ZLNu=HXUi~oEZkMBN!?TTnD`|1Eb{ngK~HDKcY?K_xz z^b~_9j$`EF1vD@Fc=2~%RD(StJ7qVO^WY>m(1>Ago-eKf-k zTmrvK*cKh2mIXT+WkVrMS%}7?u#y&Hi3E}-dT{*gDQq?eSX*Dg_{4q8&rGv%5pQlm zb6YD~+dI+K-HVn;ljskySzyII^Jo_9YwL)a^!ym2ZH;JXX@VY$p}4w*l?Tr;_3#OD z%Ny)yHR&9qgEjC=87QP#(IIU3HbPT_eWn9H`RT`q_cdYZ(KKFsaUUzQ^N0jG95P%q z9VTaK2BH?ePFbS@NTlASO>M0h{rQj3)768OxeeU?^Vi5tFO}=wOJ$u-%jTvg^nQ3A z{l|wecjq~7etQFnyf73ANF+s<05|0@){Akz908 zC>G(lK9}B$rlM$UX@C`{s7M2$uERAbV^%bDC$QAcnwXoJ#pd7N!Q-*Vh~=sO9XV>r z1S3UC0*Pdl%ODiutG>QI4E(DL=o;#ykCUu4pe>5QbED|{$tkq=_Q1Cd z1glvrJ(|Mow~w*-bOy)=NX8Rr`sFEH{O~H$eGcy2y^YDq=Nt@j_55XYHnt&HE?{MH z9^Oh8vBo6Yx;tREGypLRMlobCZR6&1Hj=}YJnv`Ts+!I~A~`^G(?jf7BZfXZheWCo zH*VfR{^0^rSqd(oxoBAH^l7wII)x0Rl$^%-MDncEm;`Y9p>$ z2=gwiH5Y4BOIW`@gSqF^NUyGRL~lyc}xJ&NDj0$bmv-lsrR20S;C(>zG?!#L~(NM>2PJbz<<; zNi;MyA)Y0te26%i(>j3)J_lcTw#FHL;dBS ziA+D~YXK5oN*F%HtOzXOb6vwmE`x=c8N7J%1e4IJ0U!HfrCPK497pXfKyF_xcQfFuzGV6Uco^E zQQ_dWgggn00esIvsJWu|jP4`WmcsD4in#>>|6%@c5VYRzrTyQdry$r%CHB4T$9-^+OH*eH2Ctn%$3PE^wgg(*S`b8RxCCJ29V|@EV)fezJR5(?KnMe&B%|X7 z5?xK`x;l*E4^BbvOu#Wi6m%a2$}GyeteXwwTk-hb80P-|7S>kRz`ZsDU&uCQ0s@Rf zv~;(k=NA_-boCt4Ne4F{+{Vlu$^vlEergaG&R@V_suQa-i`cj`jqS}e?2s}{h2zD~ zyKr&^czFj~n_F;*E~P-Cau%3n+dKyvBlTCOa>~T-WP?+1FQH6&M-1mb{1MuIc^Y__Cl}Iwb8^is(cVTT&Fo*~+VHq#052>Q!4C=6RRF;SbIVl8?XmLg>^^Nse#wYvR zapjlS5&LM69i&9V)V7YT2XmPE^IfbynnK)l`8Pov}f5ZXFBuo*3~+$AR( z@rxtRRGsuT+KYyZWFd~+Tn5Ykd+40S^uQ;&DamJ2JIvYu*cmcADo0uDW zfrSUpu|BnkATRn*QK?&6M)r#_h@djlg}y+%MnIwf38+7rKx1g_N&ZoPU_GJnnbG;- zd35*oVExH7ZhZ4CoT)X|GfD4cYlvSdKz^3>RH`{@Vet4U2LIJncnJfyzx+McR##yj z@5ZGI7jd?!6PqjRSor!WoO}^+J>^AG5)1;koINcwR+OSd z&O{ooOSY4aWJ4pG+S))oy_w$T3>^xTA({y1ywsy;o*Ypxcu&QomJ3@`pdLrWF)z@1 zgq*Pp4&>u|=)5MUGE(94-?ay=~4C4JJ_3_gbo$gwTl|DRk(--$sy z!aRx@2Yt;0Fz2^1_xT;njJ-g(?ZBjZfb`I@b9Vl=R zgbp|c^jrXcy@1Tz2Bsf8!~DZ%aMM|~V`<|m$D$%D@iIM|oPZ*Ghah8-K*I^cr0>Ro zp5Z=>e0T+ooo(2-{{r`J+(Ka`E&2tMBbLAu0-5P)mWhu3Zgd?VK%y}vD!}A&a0&(Z z1&2X4652Eto%8~#ezUTT*_j!HIRU7%ZpV7C%x+-4lCbqC`%FCQy{9<>_|?R-i2sS! zI_bxjiCA+IsbmUU>uF@RsehZO`L}L*rtUIt$^0{HVk}r8N+9equ6Ib zvorxDa&QFYY!G0i!Q9laKC_CI@x9W-asU7z07*naR5>iqE@EqZ3586K3R4SRna)s> zJr?;uMW71Ni-G_sxX*~0Nc6U$rN0|JCx+3|*^aH%O{^@hATu$KNy^&GQbsR<<1*k- z#uL){A%atgPgSXYvBs?9lk5ltXs;V0Scmm!4QT9X!?EKda19Of&!(`ou#B+iu^mnJ zDm$@dz+`1lIT6@cwp@hHFMveNCjglODm@teaeVaI&w$H4xbfs6pB-){2Lb|;sT7Lc z2@H*npnIqf-7W2i=K{E!1-Pp@tS&6U$>w2O1{zW+SPdyOwlu@s^zq={1I&K&fOQ|; z6MPz_#1T5fIA))0GZA1YvPf0-zX-A<{hIweRTXyFzs$ju<;r^;5G&%4L^)BCr37^N zKA)Z2ap3&L3+Nsk#?<5_=EtAHrz%YW>GouQiJ+DC{tle{^cs53jj~Pu@a{c4{lg7x zudLHS$ofmvjPw5SOdqMXCUpP!6pmjx3$Hf{-v}_fGKcZ|_pvrV4>J`<$3P!?JG;@_ z)5CVS(7Z*$0ZV2ItmiT5Wri>plBP2bhIGnxOnhz9E;yPfV-JNeBI@JZg4i8 z2skG`NH<#;JUN0hzq|%()5YXJ-omEs;ndHr!gD-)`}Ma7AI`zdI4pB1aD1ylfYtf_ z1V+!E!z1L*~^2zvW^VGMPlduR}?rw0*<#}LiB zcryMNQ(xT0^ouFzc~1tZia=cPulSnU-c|v}hfboWy$@SAW-#{u zeT(gdMGoHCj27_gfAQCd{pK|O^wpOretsVg(e6?&e)G%UAiC+}%YXkl3KJ_h_3LYB zzBa^w*W+*QqBy^Sm}bKLQ9C~R>=)?Tj^XyVx3Kbecah8G;pq{?QZX2fI=V*&(LB|F=n%B|^lLDWuFe@8jvO$Fa7y%JvA+P6RMW-yf-@R2_$8 z82P3KnsDitKSA>+!$|8n_7~83kp89CgDuJ$-LFj+ko>wy@s}HqtMeX z?tlI@UfjG7Jww4FvU!P&hNGY`$8}(iG~(RFb7;OagqFSztjFoR5a34lWmfud{My|DyWNKKi1h*H7g22XP>T+?dsfPIV=4=e2$m~ z+B-XWug}cPARrw`>UE^M@OwhCX;lwZA^RdS0E&YJ1D{)Pk&eUsu5@?u-RK$`N4s(5 z=hqRr)PXIdfJ7|L&xlV3czEwVW~;=1sjSkjx7bH6Dkz!cn`mi7TTd4v%}I3h zbfI;yM*)fd0Z+~($B=Ho(WB5B2GMlxKSa;aDTH>jjQJ}oQ=UOaaR_aD1><865WDlg zvye1Qa#qo6Eg_W2z>`R3xpa>xjS}ee46;;We2>a_i1;GvFVM-Dk=bPw70qQ$1USRd zTYHrF+3mQi|K`1=fQ(8Zg@?XX$t>(>mqu>M{ve%LCbjI?*vjD*ZX=)Dz?J{zSBQ3{ zV5~bRWU_Gm0&n0*Z!?wbg ziT)OJ^mL*SmkO&P4meJk=iYMC$wtZ=XEKnpIlxvn!$#Ge8@ExoKhK~^I1;p^zz1{5Q0?4T=`lU6-SeBTX?} zzJ3+)Uk`z{qkC%sfByZSkXhV>ZHgW~#-11&tKr-@4v>%#021kFbCT)`tvLJ@tLcL77Z*vokU2^zy1~^ z+Z&O$1FUUrVrhO^WRnn>mP)~nMp!A0#1m|^`T<3!P~^6aq!kyD&TB=?K7Ii_-@@~; zr^v5wA!0|MIh66`v!Tb4E;`V1yaz+Sxq?_<3nn&aQ7C3$M=dlqweXkC0x(1~8Gu2E z>~fAXHYwt@y{8kaVHVHsKf<#wZXrFn3M*qXuoclMd&6TsSZ^r8Xo#TqUtYq+A74S^ zWFEI~-G=R&IC0@L?6V!n`3_!WmpEH(xV;<3N)fB~rtsp%I7g}_&vfD32N#hWYQW^e z44yxEh*TnlpO1dZ5l>J5;Wn3gi#ZyMR1_V@`*G$MAHZp|G4|pv()m@44xd9$&j1>> zf$gFf?8y0NK zf=Ssw3{r4zKeAh%i15?{F7MHBu^){+t(fySF#Y@)=BFmvNN;XxMtgf3m#B+clo>=0 z6bpuC!t!)hevQ12rI|Ube)WjU3I+}tA40bzIYLSc0f6k}Z7|T>*@jemGkSV^V4P@Y zhbbN2g?s^pY!2D&E%>=CVl62|+8U7YiB($p{or`2NraYLPow)d$ zYq)s!0^$V=Stk!IuAz{3apTKdc=5-(FlRGxOc#2}M#m?oaQ@S47>su!bUYMeAy!sb zasS`_8FOQg(Ye|rBVH-eiL59x)QKQK+H#SLX$bpLIQ!8RoVt1n4K0mgZ~H!MkANqN zC^y-7wu?SHHjA4Xj!N_#7j|O`iIx^bDXS}D!3s^zU@mND8K?`1mgIEvNZ~PHCt|Gl zlbKIWz`|Ar+v{8GEG=a6?4&r@)bBG7No8&dp)s(PPZt83VF;j@qNFs(42m zm(!%46dv_D*xE*JJq>R&&2#`o%BB*CG&LdB)x=pwBrC~S^NM)}Sp&*a^BvgnC|Ww& z(c0O8&W=tbQw_*&XR)-ngq5)un0)dK{(PEJZP{t8C8CI(?#JnKr_p=7pQSooGkNQpeA=1}{SXT>2 zN^dQ#qBy?}Y!SF@0SqdWwFjUjf@pgy8qaj#^oi3L?iz-k@sWEthpEStD6Hp^Jl2ZA zD<`4%Bru&`#LC(dIy>5N&KkhS7qp>1rzOw%^f5p8J2>7QKY=-s8S z$FTDCV|Mn1T-A=iTyiowuuKD8=SR`q(~a%*O-xNqBfnWhNIEk|NlK%NoMnauJz*o( z+<@kjJs3H60+GQ6I0OJwwiQK2&+C|*n!(av#xXNB3qMbN4nAA_BI)o2FtQJTiR(4++Me}F}HtZsv&ppG^`XYMT`Y<#wjP6_l3ltpl#W?1tXAy6Wp`oz> zMk<9^G{(T@!t@+AX6E4&=%#`TSY@HWBB7cwI2<#POeFyVkGi|j($m9E@aH%0VdCLq z=y_S5r&LwNM?nwKKQxH;UtPppW(`j!#}QA&(L30U-rioccDAzPCF{DJg}J%DiEL&Y zQQJmOTPGT$NrcmB%-(x~)u+pFvJTAV2%1`&(9}cvLnC}mM?RZFemRZw+B(v6OUUJO zh{R%iPR3gs(bCg__TDZSJ&kaT5Hkyt`0DGgu=VXULfu5~U!TJVKe`Iz)&_2U`7MIQ zZIP`lGo(qtvQ1^`h;=q#1w4K}jc4E7 zM`mIHPKKOS0NM<+4ENyd$3Mc*iD7mkkuI1k7Ldtiu&}z!a&DlbAHAtI*mHS2x%&t& z?mmXrY~k$ni%5<3;_I=ynD~b~aF(_JBM(Gu=;zvT`SN9SkM?nZ%iR4Jc=+Wlgpb$R zArqrW<3xzaP%B11xPp@x&!a7jBe%AN{7MGf6iJ=+7zlCNOq{)N9@eooY;JGk?)ZI7 z&%8iKXDd#fIfwSn9>i80EI)jXjj?I0&Mv~;6z5kyVse1kvEN+A@slSpzVaM@`R(t4 z`%4^L5m*6&mE$JaZUC+5!K2_G3h-@6 zp`o(_ZN1%S>+XUTk0P7TVdd!p<|e1%PtI{KdOc}l@ah$u{^Ub6Y7uPwaRSfA#?WM# z=(uzo?N?49Xo=zJ@(jD}ySsZ4zrTWqU*E#kqh)ra_+F8Nn#_(E22USH?cu2p+`ETdI*tFIz4r=|EWOghzU$IgWqIFKUG4oCj2|qp3t&x%%MFrif@DxH zWQY>=KuIAg$QLPsFM8G^xe7(-MOp?!qJrEN77K8(9GJmiFs5hPdsE(<^zL5iJLms1 zvue7hyQh1Anc16&M%Pql-n{qR|DOMx^L^(#$?0*N`q~S4Y5J&uLDgHESXo#`v=*XR zD50=ck$azMvU@=17qSrL1`cvFLpc26N#st9VQzN~*MImP7A{{$vso81f%no3UU~Ty z962y0K7`MJbT6qy5d8UJCW zhFY_ML?(@aV+YW4coL1IgZpcXxc0$ESY22|YjIuo_G(kR8YO1);PhnR5B8&HdQ`ft z*$LCER8ZY5ptQ9m_bfRvgcHx7!?7bLgd`Fa8ra_4LNJ+^BIAt_I&D;&bRfG$4hn zX4n++L0GTDOC*F?TOzzxFJo?F3AgUv!N$^>_<%$*DZqove9-YQJT{7vv0NR})-h0^HSc9KV zAu-T{Om7d;{e3ub;v|j_91ud@*88_G&v;>ZNAhX{tGS^(G9$g{nHWYeki*K(8a}%8 zF}B{hB?QMbk$HChdny%NPLnlk6hA~;RR+WZNe_ub<2d~4Ih;Il4*h+DNU&g9t)o2P zqfjg3?mJiU;ZNU&d%LXNx#KC!e(gCN`}#SI^zjD_H;OZSf63%T>8fW@CYa!Lbnx9-GAQ>;y6g2axDVW23o?xy55BTZ{C8xq)H)8)-5Tdl=qMr>%;J=X&gFs3}e#=Fz93u)Iw3tC~j;6nE*qh!>INL z;)`~hWyu#C3VUitEIWSdb;+A*m8#|g%_fTL+gM#(M&-&XqEZdn9d?lzB^S=#J(j^K z?l&)=6Ull4AJtY9JCzdFH`j6d&TUb?~gW#{kcfKH)}?%#DME0NAekAT$s$I(L36Y@uO21Ix>O& z@d3#vYUKv%l{#uW^yk|MMtU(gJd72ujvF^_VEL`{SiFB9pmLS8CVD_NEnVEwVYpR- zQ*6L(dB`v?6=57#I-yD2klEh`C8cD&kPS%cU>!^;kh0^{e7 zMsf1>*KzW>bLeY$Le{G$91M()qJRc| z_|{uk|KT-+>_&HMaQk|3=(nE3t6zH=qi!CBg-tB{^eV1jyv`mS@tXp`8<3qqA3G3x z`|?O<(gI|0Js@lf0as%ETn4#`Vf6O*33*|tzaLBU3%L2-RTNjZ4M?xbSkfhC)|htm zlDMG6;i2(y%zph8vae5~x0=A*d)ILNryrrb$YV6L8!({q302h~zB)?z!h~(W03aCg7RvdA$xw?+a zKYj;yKKfYqyWTAFv(xAsAHneWh}3MBNbtP3h~oWugq*}6`(XplzyK061DKf7=au{P z`u!!`|L8huch*G7giMMkQ3vuV3_O1X$DTWd*_m0SQyKJ?T&YcLUth)6#s<)+VD#_- zq)tzW3Kzk)J0D)h%EepKt%hoZfhxPz9BkwoNcQD0`rD`R&0qfpvh!uU^@AUxd}URM zKw(PulvdP0m=19E^_MX7opZRbehVM`>02n?n#c3M|C@N>^;fX_-Ankz+dqf1Q`6dP z(!N-hE#Pr#3xfZ#Qe4hyU|$%33(TdKm+KeV7;@ zLnf6$iq~HYQClft^UgBnKD>doJ1Ytt`9Lb2K<@Au4xc`b!53zw?m!|3*Bw^2v2cGL z)y)DbwHi{HEQZI%(Kj-J%uo+n(i1}!wFr%JMbzh7eoH`EHW99`Z{Xd({23NLx&Ul( zEd{z~3Xi7>AH}@nr9Pu<8S;9P9C0B zrG&XUZeO{9?T;6*ys(BKN+COv$KdQZ#!gIPs{0vWgM8E!qDr7aR7xtdcW|{Ebd1exPJ2* zwi;WK+jwq5YQ(v87DEF=NCYW(RI-bF1jP_hrHR#hiwY#3dQ)d4QZ|VTkw9Ya)LG1a z;~ydldQpvX0u72JmYLJlKv_`uegoM8r0Er1YE=q71z=B|7OWIOg zcu6t^cm`>)X^EXmR0J`IQRpv9Z2@jmaunsR%*ERSJ8c;~6WDXq&H}T|TuN1^;%?j%Ex^#Y!u_i_6_RG*olL3w#KZaMIdlkL8UL@y> zxO?puDyRFA?didVKlu^v-@c0>N-QzSGRI)t-}D_+y$C@)6!{gld^51FiG&cr3nS%_ z_C%tj{CqEFhQ_h*{flrK5z?V)%_p0Lz?qqq$KX*-&HovB7mti>G77bQnz5KLyo~Gy zYnE1OL!^}$ps_Pd0{YrTh3MrjNloy5@-$1plHA?HzT zsMIREmumF}N>xf3(C6?6 zm^yk8<7bbcRj%U32OpzwV+F}ABLh`)vD@2%mro+~;xvw(JB69a5lQOq{M8M-`}RA? zuK^i9g=AgX0!s^@b%D8g>HtxQA5!G9(@M=cN)5x}n?2a_Lw3JQFeLxsQ4K%zEg?r4qI_w?u-kS|%`6Ms>F)&z@Uxq+nZL(+=UGf_8m5Emt=EHk01W zb7mqzX|l@ua0BS zpG4yLFm9~R;o6Tr#M=B4`VWoc)i++p@$>|)Uc8LCe}4h%Yis21Y49nG-$+@r2ZnLv zr85|P@d(O63)kkZVQF<44e4ZKp-h?Eyk5>?t*mcLLf|)|b{8tUu$u(P622C?=o%`u z_E09DELz`_0-CUDt*pKokWSe;pWP%Pzt7iWsJ$Ldh;+|`P$2v^76R) z-p82x!AIEHp~fydj6($J1X7dzIP>O<$e)?S&E)qWaghi5i9`CvfZ= z&*8|S!$>*4beSe7S8bBHx)Y%mwy?UriR-s+;Qo~xsO;<_;GEjYaU3~$5|d*S=xb`{ z9ohe6!clUGK8^XVNRQFgN!+Z=Sr%9#k}bjz6|$RIm@-UTl}93bQsv=9Lbg%I*3J$# zHa1Z#mW1s;Ix>Qpo?)cC0Hu5DSY25~vbPu0hi6dEx!5Y~V19jG3f~9DComNB2rGQ$ z;$3Vntjk(Fbo?k%vjb8*S*mQv^Ee$Gz|Q(6svoc7%Ein2-YjJh2Qf4|A^T*{fgwpA zmbW)>^WJT&oWCZ;n5$8^yMPjXn#rXvU)jCFICgG3dQS%}cy{5=Hv+JHx zjHQho?5=F1O4;M2BQs5%;f!o2>)tLF<`=MVZ651)7UA44z-v;zB!Nmcggcx;?#KuZ zoIZrH@nJ~_7yjxBu6_71CQFoIBf)@kxLlE+3j-IsL0yyy>RwCsQ<68*lp<w~lz=2WR++M)^+ZT{pEWxEZhc?(M0Z&lH zNaPij5T(*rL6{JBwQqq$VN)xz)$l2g2J8k^glP|nnO+P{j3C#Ok?z65M;nNmEsPXJ zs0SXU_oQ&j7=ZH5O_URLky5VqJ4j9rV)E=!3{Q+<_u?WhesCT`OA%Vl5Ite9c7#)j z#K?;YNiQIQ%ScuV7sfEZ9ZEWzd8IbKF|w0<{&@*?GS(Smgp~1O*AgXQ^b|=#;C1tF zf*@r+6jVj<8xoTw*}cH$B=k@VBJ8Fc@W-+kJ2!*b6SK(n=TKZP;_9Vq*gZdw+HM)6 zyGiNj2@~3jBkEZqEAPQ=(09=;Ds^O8&bk{gKhaK0<~cR)eNI7uB4D!SP}A93Mv_ zoz#xUjSW=ScTp`B5oA5g9zTIIFTRAaWIw8RH!y!^0l8!rgQvz3^rvxa;~p+wzlx=` z6_E-Xo17G&HPhE4g^i7cb=>*rDq6RY_lrJvf?)4j}?QCQC^g+zLd=kx60vB&w$Bnnoqp-Gx zWP|c-4iZgQ>{84$kY+-4(VYMQAOJ~3K~(Yn@EDF9KPm)+o*oiJs#sm$!sRPBu=@UW zG?#bKUk{LTlhUosc%>luFt9u3V`6Fw$G(0RBk4ZuzH>{G@$rM>n0@&q{HZ(&ErQ^4 znETOXY~EhN;C3KIYjG5^*2p6EP{@RE6P^HmEFv}ylU}kEcG3kNQoSrJ_9C6nhzbK` zy-2XBGM?o#>XDS=24}`O(74o zj`rc?sS`MHXhzm)5Vd6O6U>nuf%_VJal#PQsEJ~=Dw5Oe)*DF;h%fTDO4#1sf}c+# z+uMV8Z(PNNpS_RNtpd`Hi%cuRPBub)EQ5)2M{)4narj;mAAIl;YCpb?)^-`a73L~R zbmDxuf?pTe-|}cu3ex?DMb2)_B7XqOcjvLQzKSD<52Jc?0EOLMT>t1}T)A{f zaszgi6Ch9ZWRdFcfm^L(hoqRzlH^-LyrK`RYtdTc386a4^IP+kaLUeE>6GytRMIZo zkvxVDOd46UHI$Qw~Gsq6*FnMqa2aX>? z-(WwIP9X7;_$S6pY_=1iuz;=YZLDmqp|G_haRIxpnIBVehiW2I!(-^r=h5Gyyk869 zy)y3Ixrdc23)tP>hF_IzQgR8pOWvQ6*-13T2ime8Er!i}rf zQQR(KXm}XCQ~em497A%dPe`#1q7)LlQN>7lLy@4GO}znUw}#@@j>KolR2qrXV^~{W z#f5+WV{9(0V070Pf=7ii{Ud3NzkCemPM=19Z?6!5L>inDo(NHFHgN0KZEW9M!tEQk zkX^0GUP>P)`x2GFoTh-HVJ47{fKxA>#ni!R`JP)Bu3_`$k`QT9eMtdkQb)&;&*vrQ z7U@jp>%PXQCBK2qW(mvl>$v#gMQq==i=^u#I6Nfef^!omr6^o_?>>svGR9vyf$Tsp zZtu+D+WCuEym&*@gs4OyuS5VZl`wTbRY2jm2KuH{Zx1F8Okw))VT=q7qbHFv4F?gb zXra)oU}bF`>ziAmDls-XhKX7aHWyd1^3D}3&d+1?>_NQv%1fwDWwE}!fy;L;;m+N= z;%lZRCowiUj;VnW6{Vmd?^|_tarM#_%>Cs@sBD)oQON4E$_{6d zBaNgsK>(q^{Wt+M20ZkQ4r21e433^Sj?thWbw2a&&EwXsTgYTm7@L{I(1`;`_Gg5Y zzP_`Kxus>SUb}~l`%4Hnio%g2DJ)6i;AjSeLxboY8$g;w0C5v4AEfrK_klt9a@4S26UhV^ZUI@51}Idg(IQQPMv?g0bl- zOdXh#?@N;G-Ds%PE%6moow3ax($Q;t6Ys0O+sLj3@RtzEqRqn{$Wuq54Z z3?D`E+^hqMWi24aXFI`e{yJJY7a`SaT9j_rfQpJ#{GMjYLI2nYM$R3>nX_kcu$48c z47_hS0dnbYI3k60nXueIX=w{<_m{A7cL{5Y%b@HL2?6YadhksvLk zJU5*{v0B5GTeorZXCI=xxF&?O{;^>kfB6NG7HwTv$F&QWF>r7cr_Y|oTC#vk=P%&S zdzVpPEuz1W6j^nWptYvf)wN{wsk!eG1Z{VYN;jmo+));`p427<#@&=e=ljxiB~}a1 zUpn74hG!?Qs^4&aG8=l7@XtNHl8+F)Fo=9BgSCtIkSatXN8GAcb>t3Z8F`KkB{XtO z1@p75R9Td;GG|8c;%i^S@WD~3k?z*ZV&ChO(bY@p^^;3gpUg9 zs0>pk`y_f!j$rZa>!^RU2DjFf_(4|;tC?C*SvcU}%nRo*_0lXFtp?UtR%G6EVQCB7 ztD9)8*Wgz|8ReC3Cg9geVAZupNn$=<^|6$wzEm0otSl@bRWO~_HId~(i))8D96JHB zKBA<9X3faB+vb^81sT={k5A*&sZ%&GG>uB31aF-nIDvj20TqC7@HVFZ6GC`n*9d`!NCtC^~>F` z-G^N>%86M>21+@6&qFcc$T*Yr-EFC}tdfWqMVL4^jYEeHi~Xf$MCCsEvXFDKq3m@_#OslCUN9z$1!+jLI@gb%_44pbOTp^@ez^>4LHRX zTy|815!MIl$o1wh@yZdLJ$Dw>pf1lE)#FH5r^*Do{Ym=RUfss#y#<834McU;pf#tC zPQCGWOkk}B;Nr*}d>zBz{ui<=={i&bJVdft0_qiP{NjIuU)w&+D2dFLH`?=OBJ<$nIoPQSCAinfjs|NRxTfYrm6R$XelljKjp zN`owR+^{IE$w-L0*&wY}v3AcHRzO(5_Qo<28Oq#H7J=!Ow#xHm=0M92WRCXs_oC6P zOG3VEP#B%%ma|&!2;STxMo2;k!UeA6zOD=2UHpJev4rtwegeUcoLkQK&cwp zC{zl_0(G`^n#E0-8^Sb~MMZ9UlqHn!W|GIwVuHA83z6dCakv>M8U&9Zpvb2fFw)FC zwNBMa7psrqF$rR5Va~p6&}0AGg2mD)kxe0acnIfSd;vYT4(NY#*{F}$x=03W`1MPOsVLC zFlJSL*`q`!wWArJ380`IpY~eF@}ne)E7O*>0GJ-mWB7$5m^?KLjAl5@0ZpfYDicfq zgVhXH?k!^N`{%KAXAVxylVh;hDRL7s4~OSrfCm9?Ld>DrL()-l z=kg7iEy2W$oizFxm5Y(2&CF%9_d`14t-A%>ymlQMmzD%Dl_@%uU5gTMvMz>CjN{1KM6>R5zIx@S!&b?JEymb@HSLdWtRc0819i)6_ zRA!SioU%Px_&H_IJ7m!&_$P%%v4Z8fdFdvkpOpPT02ecNB?yqtWso{Jj$AH_N~MJI zP7&3u9bsH@t$K+-WFh<>79KNlZ0TyIOdY$hHp(a#i*hdPVB+jrAz+a>;sj`w8>q0` z(Ml1~k8@5OyK%=@oSDSP^GDIw-zPxn;=Oqkt}M#TH|pJUEvIuS^c@<-*qMXqJ30cV zFA2XGVrzK=I~Nx)fBQaa^Cg)dhuH*+O|9m5jXR;*J2{Yu6XF<5h}j{2zU%{=}4+GOpy@uzUG=1swOi)As2;=Bf8cW9!w82U9G%^*INTbVFi^pysQwu{H^O#MP z=O$9tTGWwQ3-rHchN(S|wql72H$eth3X58%ON+FbpY%nwpjnNiV?kung?wUxF^$0} zt1VclDNu`ylaNO`{zzxbX_p4$3*LJ^UkyiPNtwW@ccZ`Io~P1?hTo<|88d$)H_#&m zI=*3hYYW@k1(lUFa_)9SEMpCseJg;k#&DD)q`f3u5}RubCQ^tJn=u=w!}tX#g2 z#(GIeDl%VO1|vowp3A}kyZm)eq3`1Qpu89J7rj4v-<9+xYe%HX&39UNnRA$M zs4}8?loWSmM4&IT{#Y>M^J3>(G4|!lk91ZG{hM@?7_UVo6~-+DrSl_0vJPtNTPSVq zhnyYN&&nyzlu_^Bt>LOwWabo1i1`c&K)n+vAMF2#)cB( zqyWgSJ()8f!5z>ed(lij5q5&MwnFm8bt~;zmj#} zyV8*zOyn^(HG$OeyvTpndnsM&O0iA9-^{b)ScKg=DyzGwEbL(I_9C{H)9!0x>z=O zuPIA&`(-YFas+42oki~Cq~sulkLGas!waa-ZKBrT{IM?i^R}fj8p{=p#uL)I5pP~By>H}?jrqc!yTe*OSvPaTDGtY1`YdUic*t!-fAtsA&@`6^J* ziiTW|6f6lErP0&hC%{u8mxSL>f0jjWkP{N>@;jHXL6wX!A%K#~Wzx^n??^A6lBg&T zZ059av=bxT&=2JvdsBHFIdTN~7bnrv(<`HQtJ?)^-=YG-8a8gMptMnx8F^G+3mHEc z0??j#=>hSSZpxbDTG9~zo4&`*Bn3=Qo*KjS@gr!Bda@oV*(!O*PkDbnhP%A09n;M5 z#0`dUBv;9a5)5+BKl*}kQ*pXPh}ZOZD#s&$d2@4Xkb|U$NdY}us*kh zprHU4XD-^-D4C_{B;-CaCXsb4A))oV>J?>v9^n;J2O?ljf+eFQ?mPXb4+$ZcF&}}; zR=Fn9#)X}20l|Am`jML)mX5VxFfFkM2~m$+Dv}aY zP3>+Z;W(8+woeV)>qe%b`o;kp&4@`#W^z$grE=!4wZz9p3wmu`JERqcxViF{=)~j z`O#HG)v7$_2-Q9$5AYyjQ&WcQpmkNftSAaGPYL> z_~0kMK9_mbJKAW=4`6usTW_AX-7bh?=b3lk^jQ`SWRcx%RW9#k)cGkDyl-Uep z>gcVOkYyR4QpUH%j(z4C$&}RP%6FG=`}S=(La^7`hSVrRZJ1&lo*x`U|HzQ+Z>$x$ zr8>4L@g77%$m0HZ;?zm34Lc~6OW3Y#;=tGx5}Y%;TSxKEI_|xH1zR`f#n#MW8hqBw zwXs`Lt9cQU&7|BnndxX6lXUK**<%GfRzDO{MT1?XW(IR~`i;M(AcZ9CTx|pr-@x#< z{<#d<_4#{qpj>MU3pX!e`NMyQ{)~gc-};~6W%9W9um8RR5iBCfuxZZON#gnEU&Haq zKD_nmmfz3YG%co-ZSLbX(pVk_CvoOQIauq4i4U~oXX2iGZ{>UOG+2N=XgC1wB+&X(ur=ftbmrZ9BVceCz@; ziH64{T?+0D`^^w`10?QwevDi!3yA{ebQKeA7oi;@&w{o9iLx1K!B`VL)s@%aMLQEI zW1_{H?x@EW7j=?J4BhZ2)yE{x_o3m(zZK?N%04IxkUfl%x0>nlF2 zDY)s9lO&yhVKPD`s-qm#L}EC~C#B1@f3#1u@oT?W6*kX6HLd4Cxo&gbrDTX&-j^9g zt&EFWp@@2+h=}r4zK>ibkb*+4th4S+EGRtakn)n578?Blf|-6~oisM?EhDwe;oSuP z4E2Bjv+H4LpoM|q0SulS#`wepqLi|kNDSbOX4lNt%>~STd==huMUFwxMiM(oR_rVr zjT871ztgU2TkP*ifkazovNv=ekSJ?g0~rezut!2VrO9q}Lu~d}(LX+f^wCiy69MXV z&a$hc(Wt=LtYK$v75+jKnJ|e=m=Yk6pEcho1)-W!#iWqPXVyxLa?J=xPbCf(f#pN$ zK{|kpJ}bLP%1d&VX+!MB_iF&AJq(-FcxU!;=5of_(eGV;}= zTqC8y$P!#jZAb`&sT@Bbxt z0Er5C*qum=5^~%wsrtP@q68vlXuWQ3n#LP|%`klE;pnC3yS@<;`H+q{*DzP4zo0V4GR)?Zn#bO%fX#`F)<0+;@BL4O_F_tsyaw7Vxe8 z`K(G2)D+I;JEc&d=gQ)M?&C^kvMnv{RS4kHf{Z93$BZ zgtSv|t7s%b)JHw^jSm6Y0Oh-zXe<|Q&Vsvkf?V=_eFNPlFY(=QMzLpf4CkJBr-Kr5rqtC%0oFJ z0VSa1v-l5eGle1eLut9tLB3-0Fz&micq$;C?YuBGbHf&rvyj#h}G`>K>SQ)`!pL_LX`fT!hIH!*Le~6u)Dzd}9sK7&E zYYT&0R3c#~t0~-?y+*+tUas|XJU>QXs#c;Nr1R7&5mcX}JwT!X^KF69>M(G|Czn9Q zQ{@PP_ad9cb;SAR%tf0a(oOa6S{zp?T;?A#*M?z#2qc@bqGawg&&q2x#Fph$D6#ME zkcUJrh17^jZ1xQFi9$xPR6yYu%P8DlM{<{dunPo?qv_7oH))K~b8Dyt=MIyc=Dga7G zA%IdVlu%mP5=rBpRuX>QflH+sj`B%Gq6E7r zA>mf{>RQr~=Ohf2+%>$-8Cwyu&5Y(Rnyl#FEQDaLS(SB@c!`9thASW%i-`2u9!XQO zX=(eMK>+{&AOJ~3K~xUfi5xUW5%RB)PF(47VQg6%aAmAVD-nqVY-7DB^68@TK>*VL zzWQ8g?8)wIzpgQ^MEY7;mOPq2FQ)5Aq!LKa4ogAbUnZ&1L$=mkkJCBI?Ks42Qd|#O zThw?#RA_{(qju6*jUpmGB6d0P+6_6U71q&c)v?NxT<7MZc=gzjU~9 zElX!@JEIo06u!7$^SP6FoS$P}!}-9RAxZgtI7rq)8R0?yBDJbLg%!r~jHqrl*BdEfa=6jB5hJy}1h&_Xbu%y`zBLYcYSWG$PcafY(U5A?}w zZUH)cC5X}8lAIsa8$xKI3RA6G6PpmsPG*uw1^$A&Wd0`RGjV+Ok?60cQ1=pm+ zJlrFk5QUT~ZcsggwISv;7D#l=^V$p?)EaecPC4l5&!e?ck}kPkO7yZ*UK(S#<#%Fp z0ai&ym)e9z*Icg_NZiZI<@&Ypx2grmoiv}*9ErA7pKRkfyBk>`QTIh#RMF+2WF?K6 z+Rqm6lWCr?&Buo7AyqTkw;BI>R0QJOSKW`>=WLz1J%M&S-B~z1*InNbx=AwPOl*=L zSQ4?MHpfB5*E#B(7tY`#9lBC0>1`whz+>|`2|CRnlG<7+Xu?VG*=@qFsWf9R$@t0` zRbOb>wUq%m>m0K8%gH!ferzzWel#&cEy%f^eQ zhH2)6Y#(iz;Ee`x+AF*4OqWTq!$4R(_nA}sCi zHcd{f@ubusB$l?Zh^f`ee=6|JYcxBF#w+q58zN^Hkx=g=*8{)92sege`g2NeGY;TCBHvE>?PVX1db~ z_KsP72a+ONk2fM&Na{dlySA$0ENg92{}pp2UU&;s`BSn z6X~kWsZ4D^o;f8E(C@N#OhD1nf*FI-=Wy?@MRn<}-paNlR&Pw_aN@)XJHPhsxv8x#pEpe{K+=@Pv z9O}t)C+~Xxjqk|vsJSeP^CEoY4!(x*-}q+;qawm+2d>dgHrb2q%5tI#HaAv~NVMSl zT`X&DwS~v_Y2xzYof-UejA)tTmQB{#7&WkLcN!Q(#O{-H_8KTC$mTliQqy*DuTDp* z8;NvVRu!O$63XIYAzO|kEKGI8bS^wE#iO_d=x0P8Q0Bxbi$FszTL{zZ5Z1bh81%D7 zVy6>B>Ly|$T`7z)VPoQIjwOX!Q@Ij0nK>@?$(ig(P|l#uu%~Q+a6hA6&uCxJY`Pq+ z)jKxT-?R&-n(CNtJH1zua$4usW}qo}#TRFn@MXP)!Sl2OpuLUg`G|~|6flhIF@->~ zy^Vq7FBZ{gN5X+NDCRw8_X*iREZnzSL%II zbu|T66ZFW87MVY(T@qYZmZd6{1St^l3t1S^;)^8JrafMY5zVG-1O=B2I#Vu^kJ7vLw0~ zYGz=3TONSSMuS?}AXe9fdB)7WX5zyaXQarpC$(rT&bPrylhl)ys|lTX*ew&wEJC|} z>~qZLOam5`-<9i<@{=otGzs_(m|zQ(68y4*le1Phhl>F=Lqo$#=Oe`=ii~ASu}HUq zl_HU1h^`OqTrpk95;&TbE1hXNi-JKs0UCY-gI($4iR#iZN4$nQk8r>}bDu zkF|SEk_eN*TY)m$)xrc)Pm}jp3!icynwkjN1jTw8YZW!lT`^sLPEYEil~P}VW$Bl5n>ybXo$q^Rkc zt|)e4MH=W+=8xaV$H_8Z?ONm4w!7uHF6)F24 z8d~Hsp17;P(>X8OWz+5mlzoYxRvsJOV?QP8m%U?aQhTSpVN0AIN%9Po+d06YIz>Y&!p?Fh_urb@gV7y)ROz zAmApXlg zav%}&pZxDAhS!bZ(A-xE zEwo*oIg80fd33W^P50yqcMvGt2K7l7)@2IG*2`@SsSeF zcq*HVD>1|rHoKl@OHHxP4poiI?HREP? zn&#q^JZId{&V$-(bZ=ctV*{JZXu+0yyWA({cLdE$4M5{NUN?)cq0A0sjf62VrT=V= zfa^un3rMz7C|HWt5=-h;s=5Tb!|B;g$ByI>Vo!qx$#*Fj!u`WO6LwX%G%)65<^AQQ3T+4 z9`-ueWvDG~o#(oswH~?X>RNi)#AMFVT@M@!g75i^!h~= zL5aBvAX=Kgp1rldd`&wx*|S`cj5)(BfHdJG#)XEHqwD- z)aRsr!9{C`MI@ETOn1!_<(A1aukyN*2Z@j1*cI;MBtdblwamy7t;veI0P6&y)}|zf z*J>H+rd5;swV?|cw`WH|(fqm*2`Na32)b(ssa70R*PcpsG7>~&%t?n{#95M3zcv2Y zsyH#%Qvh4ngQ#Jd8nBIl)Jtmjxd6F_i0hl$Q9I|f`+_C17~+%GzYHPV+)2A$xTnk7 z+rQ)t(5U%>x##vhriP}(DjjbnaerjuO6k~@Z`Ac|I^Pt_7H^FpB7vPb5OXQk%oVsw zKKkPQgdP4Z)UKz{c;zLOzXAkZ*7dtPDpEE={n`JYU2P= zzv{$Ef@?@^N8ck#N8%fFpH`ef_a8Y1eVoTE#xgeyh9)%zv%AP9z~|nKEiyM-VszOH z7%TA}+nmD2y{1`&fTS766Fv9FZyF$x9Zc+eP#|&aTmL;OJGZd2@)2^q6Y%^VEdS!) z!Y#~W?B(Cb;M8GkCypX=0^IxKKah)}KCA?kGLtHe=U@Ide($$lM|tBqYLx&VzJDF3 z&*X6L-g*4hFXm9Ja1m2V$iZeJf#XMy;2(VZ5bE_1fByY<1sp1rifX{F5=$F3-?zx) z|NQD$KmuXUMSpJ(!d4Tdave>|ZTi6Y*qF#1i5yN4V4%Mrl~O^wUM;vMopQRJ5lGzF zSVmHSL}f%WWK2h{aSRd7x59%J!6EQoG=z&Va=uDFT~Q?MSC-SSLjRNkj}= z)OHc>St{+&OYCJGys#zB_jY$UNT6%L41#PDf*7`6d+wRBac3jiza@1NCOLK&-OB!p zovkX2a0y;nXyzT(4b&FGh23mxgyuN%S!u7gchKA?a}VwP>X<=+47IfdMeLc_gQ(5b zmP|N1T>`v6x{kQPc*>+<;mH^(Xt$TzzYYa3V4_M!sI<(B ze20;oZ6|12Ahc3taxZ1Q>O#?Em&|ev{3}d6xDMMd%79WMn}V`w+Q7r! zq*oFWDY|L#uU)8@eMzNn6~R?0Gr51J7@<3~UEduU4^3*j*SB4GUGFggi>*_tq=R;W zDPY$99ILgpbI2rT*2rB+o-q1Mkj}SBfyn%i`;kZ+nJyQepU5sVAuuyYcx_}n3%I1s zo@h#uQrT}Jjb|9N>=04#!dgrjqn)$XR8B8LJ680O(ZZlWYSQ(p$FNLNstM?M>bl|_ zPz`W3Q(Z)UkmQhiet=gcH|b_m>c!ZDR<6V(;678!hX7`Y7mt$!2$Z|2@~ z)}X9FGdtCkqinIu6d0t7lh3U=^>)*U0epF5Y**o)`lZ}IlN2bfEYE@_#<~_%4u!I9 zwtA(7uy&x`o>ixx7ckH42eyMrZL5n#gHQ7q8n8kB$<9y`GCaG6^_~g{#v(X7HhJif zwp8hGYZtT9=)Z7#2Rmv)Dj@hf|ZNLtXg{-(22b)F?zuO_+c0Ey~TwLGHf zyTO6>fQY(Z8yLZCjT7tsgeT=g=m@SSeb{5R$mWqP!N!!hpjrSTI_Tsyl6O%!=qtDXW#qH4;%-?v=7<+Wfz?qIE&Lp%XL|NkEcl2Ud@UgyBmah5! z4#BcJ%Ior-mJn+|BrWLLJwgdXEY#WAwR|=t95nN$ZH-FE;mj!|XX*HLeZMQRT08^m zvjjqSLNw@0r7J_pZ#FJc8=7%9ZTVweG3)^UWL~A$r1_efpXV;MF6me>*DTAXD^53n zuf4O9HAumadAjN5H!+4l!-8m8$vu6ZM|yhkl0o_xfFm9h>b;f6Jtr7 z-qmTOL@$j(^aAQ2ekZmzumvlN9Z)VZl=5u$f`YEItRX?nbKbb0b5+ulNQak(byU$-t=+%0G+8iEz~;uauyvEmNJJ-8PQ zuBC?DE!{0eZDVv-B-iqmswSU_! z<+sG|S-)2lTa5RVJnIVqoEDmd!n;cTN|7QPm~|B)VnO^guSecC$}4nKZC=4$9A^Qp zdIPalyCAT4n-Y~JM%c!m8wA*g;SfwhGvZ{W`K1YuaD8CYuv)oLs8RCS*2B4WEzzR8 z)7hPpYfA+&O`$pBSMh{~0^N^vQ8r?ElyEm@2Om&g!QMB!ER@2XuCv=taxDQgx zPa+kvraYBA*@MB!0=p$>S_F2p$0cNgBf=M3^##4UiiPA((b59>EkY`Zv&`!G$RoR> z@3@ClbEXSMd3@sy7PGh;C^Qru6nvcee}0Ct^QDgs8GKl^&k(x&b?N=%O018${NZiW zV#15knF-Xh)}EfpIV-n}9H`km?y zC9>{gAN=6>MMQF=*PT$nRaLV0O*qb_1ug7lfKZwnxw`5d9`5)MGH9;-f z?xcN;ebr5MF?tnoOkbq~jXClb*c-6u*^a-m7d%Or0XZpQMT(M`%FEM6wc`y4sL@~C zNyDgYw^ioz48I+Et$OI>AbdG9Ff@Em$sdOkr(81$CcwjEDDUQ>_@l}hM`f;C_d0~~ zvzt{&L-q+#$^Y5n_<-QyWhwDT81>_KhkuHfYvv{V1ms(L=(FIuH4{5Sb_ z@M-lXY>;x{#v^d}#D(n&J~6!}dvWwdQz-XGY4oA!&!@A=kj+xna+sB_`MmML1N}lz zz_ht|{HXJhXc^ymdE7<8#~0RsbwYOv-vq{|^U51L`J+xgZQ8JlY}M8~fo-~mH$mi8 zU+1;CZZltSpN}nxocyVyZya;A*ysXx58ip5(}yB4s>K7W$;h{s+MlHb!(YK3v+J%I zoR6EdUA8*#SMDzYqmNv%&|Pn1okZj3rOj{Gjeo_4HPK5($5V-hAKd(XFVLNlj6vG| z{6~#K&&fEnR#nrg+k9ghEa#_MH(Rf2w0ectg^2o@^2N5~ zG%m#TdN`)svB7O3@7c_Ke+CJCrTFL=!j0Hy^hd{7{}TsyoWQQ!jF66rtGGh4QuUhfS55u@DPWx&O3c+Os*r%k1^P<)oem>Yghj+ z-wYWo78^#l?VLIN4w+3J00(S*5XVhuJR!fP*p#0+gLj>~-E0@Zs>}YZ+H%wWYZBJZ z+#q2eJcnou!4j#gS~9+UVXf&Sop@!up6s*a`!L7g)o?o3z1ifo8X!^qKp*^<+Ko$; zch>jS*(Kr0a&}sszRpb|67_|_#?M|7F9R%*Zp53-fNj!y|53mAJ1OHMKWG)@9aYne(+&0Hxm z6c6ABYpe_Xu(ux8Qq$o*Ck9y2ZSFjv{E;F`6@ZyplJyYj&&n6Il%h95c7FVLYgKbz z+2>ae^R}PIUwTD1Z-?5-N#2iVFuv+(VXvd3ytHh+zx`#7)k6}=hObZazjvhu#q2-- z;`B~uO*h9f{(EPG%y4|`ohNb@mW2f4_`$3BucTbpM=w#yik7ZgIy#?>SEXuqn7(bj zD!D1YV18@OyWjZG{z7Pu@pedqK?`0vq{!@S;^`;IVNX9T8;tgPdG>E?ZS{IOmE1~} zPkh@7@9K6u%CSBy&>m;OPM6+0y}@2-&>FQptrf~GC%Kb*YI$`nPJPiY_XHG-{8jKT zE`Ux@CS*!rbY}wH4{uSQo;M>GlJVD7S2Nmj8cPLjcl0K=QCGJ9L+YKw+X%CiAsIUz zVE~~KQd^9m`tU}?)s#Q~+-9^%eZO#os*$%`{L5L~>8U47IdVxIuP!%HNaOq-n^EmMJiIm^Sf4PSLVU_)+S#zRdR6VsG z8&z=pPs^4Qy)I`1R9~eQU?)Ne!sU^>Be)*-Pw05~V-vTOnwah)0y~q$zidNH&OAh0#0nj&YKa4PJcibdUno*@2QRXg&+M%+@aT9%kj|<@T zo;VcQGI2RdNTDtN$e-6rlv)-O-J|*w+%}g@A}>t}xf8p8c&KjC0N3lK$_#f_P)sZ2 z)USmcO#?6 zsQ&>1|5?2b?NP)B2Bp9vEQ{!Iq<+P(`4}W}LtD4A?y`B*K8UZJ+YDF|!+1DD3{kt>=`iLvV>^4>jYSM8Rzq8tMZyF846kFhOFE8Lg3#wD_5~_a}d5n`niRFqU}7(1XKCY1Ur_ut@DT;0vpfYv_!%79e)D{L2X z!y3F_jq?G?NT>VnJ-1()gc-7c{hwUw$9sFj_u0cx66oUU<_u0B0bg-PHD&t4eA`}f z_7B5QRZkI*i5ec>Q$MdP#^tQ|7j^BMUG1O#w*g&)55|}XCxKz{ZntvT9a)~vafepk zYPjh}DM4Pf^Y)&OV%8rE?Ox4WF$z8Cj@<5voBKC%%ZYghHZq=0!#RKr%d4>G0xRi> zs4)Vn{C4B(*Jmuv4Zu75|2c?BXA$Go?)|oY2fj^Us)&$)o-2ER0GE@-)r-}{GS=ST zcz0eMzN$s1GC;U8Nc=qYgHzZJkcijs{ysU+A9=Qv<<=^$R3eW>B0j~#TL(c7J!$Tm zfNf0=x!w}9X+pOsb(wz)-xDNuz7mMClC%6B*_a}6QcD+7Np0vGh3|bh=L$8fm&8KS znNwj}O#I?3ecMBq|M?XR&u^fBXJ|Z*%f^38XeOVf;f`n^1%V&`uINHOIIfiji=!Ef zDLMI3B&f%#7A&%w{Gd@ayJhq{I|4c`Piixj^tCwm4diF0xzI({p*xtjZqw~WamQ0iVjj$mv3g@UEIQrn%e^VK>4{amo$ST6V;yBoii}tB_Px^-LA}(} zGKDSSYRBA=3-4qy`Xk7oKZ5QcYE)^EQ;GYBQ&?q^D$`ds-$)8>aO+IM-S@VGfV>(W z*YpwryOQ#@_|&jByNjZD;@=0)w#Yuhbn(x1?FUBZs?XN{3+whDH1j_O@w7>=qXAK- zc;!*_35f8-_n{dTroYt~k?*hQ=N zlsxq5A^F?rb2q9Z%`4NlU!_7ibkkZ7M@0hgkxxZFMP^=I4>mS1a(0`;TjVYkyK9}N zqkq0v15|v(8up=-Tt)C2eUipjqLAU@sz??nY{|?ZXj#D^3wO>l!cSog8aP>dTo(0} ziB^3LCBAygkbH_kK%=gN+VpP}iU*;`z zXQrb;dr?UvkfSzkE0YKKFi48_lpA~qD@k<8EHTjoG(lMypRPYyd?N9dOk#s$TPmwF zZO$&xTCV8p1vvBnrTaAb-PFwiC-@b-bviKNHiumfMUg3b;JEwwIbiPx3lZk| z)#KMdF*~_*er#EE=~SdxpvMPMbiYM3(82*tI9;{|O~j7%%W(tV=y+Rm%Mw+8xavI> zBCJro6bIxH*tV$h;YadK=%gw!BqclRlBK=w6g5(QAepiY_KIKAnZn>Hk zI*%7o{}kG5>Ib!xz^V2?p@h3NRehS82V=`&0H>vPbs3;aULY6@3>$VO51AaLy;*1n zZ=saXSr#biJKrt|`SBy|X?`%(k!xPM`|&V>B#hspA^}RuEb{YgP?L3WL&^}ps~TtJ z=yW)HC}F37Ls<{+tiAt#2Ic9WL9bvDxYXWjsSUz_;dYO-2M5o!Zp?<|S6Loe2lE#7Qb6`qd?Siwvx)MSYYGAyEuPnes0?#E-s~z_ zBCr#`9pT%TIp10u`N~73lN+`fcuZGS-ild*IA+b;@&m}H`(cr9Jt@5fy>0D?RS zh@b{0k2+|Ss>?T~)&yC(izaTUF9`!hqC)$R_SpBn)s?3EZkOt8*Hq20V;8v;n#+F? z*}a7j(dE`_CUp%uW<-h;%`-dbfVPsDnr9`8A5S{(mYJ_AHk7((j-+iA?d_rz-J^Iit0@f{{^`Y;Z`jZ#c09!La#?w?$C8`EbDd$^?*-v`O7OplmF|(8U!6e>tel|nw(ag%IbMd4S^8x;rvWzis#iR-NfaUav3u% zEJCUu5T@n{?jEjW?7?!U)Ci@W&R+xorcEriRTG>q=3vhmBqnZ(usK!s-L|Ru2mM>_I>8Kri-b6 ze+MtLhJwkdkE97;yv3lUQZ}!n)M6qwQZBsA@L8uiKfnF-4~Vwalv>)FS29Kg(8571 z%O>Tvzw}Ev1XXRA_lVbxL76B?3Fu;+S*zgQ`Jb80G~;RPf7U9%%z}ZUwtWtUg2TF>`#YDwKs>RnL2%EyD#wTdCAwevJ z*`ANGStW+|T^tUQ`Xj$GR@^4A+zve~AF}1nd%XmH40(~9QcszcMb09XfpBz8osV^4 zRZHpe5+hXX(rQF?@!}NxVyA5;{q-eAh{WA%P(U{=ai28`-IOg1`ZoA*^o4jS)>{(n z$O@lSU(r4MU&-130)TTA#<%JOrrr=+I8MrPJ^JF#c*g>#?e!5uMbblKpDYUf2?Q(xQ-VXgl7DV8u&74+~EO< z%UkdS2fH>PR^p57QKUSl2C$Ig{(1{S?J*Zqp%dozSrbfz`49jgb~rSrGo{`2Yq$mdeE+5kHV%%7qa$uv zS(%w10ZVA*v188$mx$kGiP_V8%z}EuH{p)CO-(2cM{HlRvcMnNSgr+P6K=`R!+ICcy4V9k%XZ=3Nl z;Bz1JFbFcA8q*w;caSp0P(q{;Z$j7pKG3!Owrb6i)Fh#rIZr|tQvSC#E&kstkYQkT zal&nLxIQ!6l1&`DkKyLui*LAy!dS2$f#3TKSV;)gqDeW7h+e3~16V_8q8jdhs|I}1 zd`rK-m{=lZ_8LR_J9}=D0rq!<5*uf196ooPM(7plr8>fBTU$iEJ#Hlvxm8YOC6&`^ zb5viE&#utvo>0=l%7zNICRzN~m8bXtsxKatu65ya8fBucxvvFOEMAF5-pl)X+Q zX~?ahY|ia;SCnGK&Ja=vLrf|wg9viQd|L#0JM|aptlrO9yI+4&&WD$yD(HXSh zZ2EN=o%B%`YU@7x7WN5R@%TIY5kp8=_^dYj30wT-ZZz??MbF90ZTyQ)q;KGrc3{}3 z*n?7(6)_N?b-;bDx*Ef(l^FAujmaPW0-@3Xw!MX;{e1!l5$|(|EROobXi}VY0q%#n z;G>U;L+{XKHms`+xJdWDMK7e2e;LTrbgllJYb}eDSb{;|;wW6rM<_H+>`ob~j3nwW z3SngggXm%VZw_A58fj>n<-QN4!leV?O5fOTU7)y7_n3j^h}GWiNA{DCjcdi1?vX+Y zu}(!Oi;ALsAp(4%`Dz6}$kTnC=&qk%So;v(siRMe%o$C5@s}66kYU|wYucE{tHj!|Cq=T1_Tno=-*#1Rd^5+4%);X%J1@? z>s=^I&F)ySv9VvWv-{+4J}_i;cwR%hqq0!+=IaBI#hPN`gU+XJ@HE)pN#ma(z1Ak}~ee0Xm>u*Vg~ z-@l|>etRT;|B{>@3d(@=tV^7_kA6NR3=eH)6km)Dk=a(@Ljio0l;b+sFP%q&w;RzM zoT4p3@889Ende8UAD%c$GvE!eesp$dB>cKTIs3qT$N;P z55H>?d^~7>bSs$QVWNe0lv(dA+Z>k^{f!T}q@(4XbeARwUVT`(>8W?y8KHdaHGV{4 za!n2uxYy2FfO61s%?Zi8yS~0Qv$a(y*a4}fBcH-xY1U3o(lt5~{Q5m(9Vk;%Vbu+7 zQD>zxmzUT175twL`-HM$MzL~h84xW5TpN8~xX8&5IDJgtM+TKn^AV^mPAy=Ly3~Br zk<~D-z;7LPrnr+r7^M~mgUBV_+}^-dkg16^KHD$|Ov))B%(zd2i+!E+7PeO^Q(j>? zzC_+VHHB56POpYA4jRu41uII!J5u)att>`j(IyfpGlQDb(fV0vjo$Q%H@%irbs+IH z(t{MZ^U4t3u07!{J$;QxUjR7zGe$%bNQu4fJhUg6b;NhWx@mVg8_yRps?;Di(m?-8 zD;lz$in1kkU$wqXl8d#@iU0GD)P`M)>Mi)+xZgn--$W7n89-U0yi1z`unr?BU~DQf z4Zs=#0=YPU{3r{im>7jO+C?~CZedJ#d_H**GivjLfq}FWnco+J^ruN=#AK|SVfTMO z#25u)&2OW`_&?E~$DwKGqgRsG@%}X5k|I@7lyfjtiezqoypmq>d4OxRRTB?WjF2>^ zXmXhODC4>}?bJ-e+H%ISGp1tW3wKy}gOS1hv4puH8M3SAMlH@lBowg;1R`vsEGB>4$AOe$F1q-Q+nO6QR#(q&?OJ952`YbkB`UT ztyc6qi9aD;3>HSEbq9ok=LEf)Q7Rg!+b^CMZ=X(#`VPA=_tS;Bc*Np=?@Ts&+q^Q5 zKiS-_>a;#0&fDw{y!i@Q_r6lc3%DB0-pGFG)W6MWe?f0gAINX7K*~Q__S#V= zo!QCCvEGa1<6GkpCzqC%uvCQ*9I|(okpj$=^&{8l2Fjg71FsOe? za&*w8Ets^4i4`SIj5_z>$H&ajQFY2;Fhc5QhCvpV^f1j;vw>y(<5NqYikc%Mok8nM z?)K0IzK!imM0`g$3dy2*RBO`NX#jQ|M~dA86~(7s0XVHl-D7STTR(K`Jg&??ek2#A zjBR|mk}fBOhO$w{))P!k&*taWNEVPN7fwXZ?sEgV-%Yh8c4A_{+Hcb&90~q@l-0OWzN+^#9z^M5{I?zo z9M?2(J+6u`CgAUfd<_7kl7U6lZ3L%pK$uUr{o*7WK$u?LiTGo;IJ#*h@Q7bS#SvHT zInEX@e6yac$p>lp4BtrPS2Wm_5@j*ia%N*c^#EOtQpD?)FFgEyY;`q0JJ1jB=Sp*& z-v;wv5)v-x70E{{($mMg0!RcoZm;u|0*9X7hSw#OToFb@ZfAFPEPP_(<3nDm30Q3o zKzO;0HZ~KTfko3vN6y(ODXExjZJ;a25MEr^HS&*zPA$3l)uu>3tkD}B*z|Qjc#gZD zanA_wJHHI-*46W`hb-d2Re#GD<3C#EADdeec3}%KrzL%Pai5> zrafZW9ZCFlO?WqQHt(lOcTa14jboS^rreoRpbP28Ul=b>@z1aD#2&DzANvC%_vUR1 zFsWR>@@74qJGtDfo0scrnx35Mb8a)qFa+GRtP-2?6#jm?stSy4zsqRvJU{<|PpqY< zvGG9}3^ep5!cgp@rc^EUEPcAZR-vUyx?NtF!5(GHEh-|ZsGuBhw2v$qfeOK_c|UDU z06(XF#&@Cy8yi#S(MDSLot?SZTg;3bQ1?fddV}AzjOo~!*<8Q=FdG#KK5%g;K~r>_ z)OoA3yGQ*=OR0;UczkJ`4TztSv41uk5|KhoTwl+(d%Q2<>)R5k2QqaxHANWz68>8r z&?=w9b3+8M8B&K7xWeeC`lN5q^vUMZmc#_o z(!K>G+5W9jmSiQc>GcsOb)7oC@;3zUIKh0%8lxuemI!cfq%aavN1-d645ZoqU`V;z zZLI5u`k&`T{uQuy{3bM^J=@v|@^!}k2gfY#k{D6z&N}EY5KtZG*Xr~2&kgEn7*-qgK3jvqH+Zn@PWWGqXV4jP@LW?_mr-B;M3OaCfb0d!0E{1 z>}jp>BcSX93p);8&d(3hu57TP5fV@urcV0E#zOB0uj{uTYw4%z-GONklZ{?11D^vS z*D%E6<*%}Kc04Qo#VE!B-lU3HuW5@@6yLUB!P=M<-H)*;rDq*CB2HGjYO^2p{lpSO z`R;{pi%(+nI4^L{-CN08uY`3PFMi&hYm3Wl{W;lq5Py`85xph5Ga&gLRt^!oi#S>C zF@8B(rSsTcyq#GTs1Xqjeq)FO5p9o_d%f26Al_kMMv&-*-L7pMANUyfY&;eC;9p*( zF;D=|Q7t~(8gWWwNlh4Mne`VL+J@v&!C)tC50zg=m@|5pQ4sLrrCUAYuMo>EfsKFy07~F2{1Qj;LVJ*8B`arxj+4 zx3TNL9AfoBC0Wiwj_zjuE2% z`52J6i%F|SC}jq@c~4q&KL^2UME9X(9R-eR`5iQyyZ`1}ekV|`4l9~Iy5Fr;`k!D#VS z$w(A@1IqNN^ZmEp+gr)>)KPwAGM|I$NR-Be%X30g3G@ug{>frl!G2 z>Nm#b0)(Yb3NT1dsVmpL(ev*7jUe>WJ>O#SOQt-KDmuSiAnxl|T?=j*t{zv~#Cs(I zSK(<0QRzGX_42|pEdL{iFTZXC)#)ic9MCH9JUZSMB`}filK+YkoOruIJqaf-}B7g8;8`XJ-#g%0qX9# zp03f+es}SL6MZK8%l&}_!mq?ie>%_D-XqKJX(5$pCN+yz*s8S$fn`T(E*B?25cx48 z8d|lo)eB_{i3*8iAbaleL-e$qR8^gNLLoXLpVV!T3V~xQDQ;DO*!iOhtZM zE(VDL)7C{~pE;yR-P+bs+D;KGDh$VhTbJwux_dnz&~jha1If_6HV+FetEjWkpO2JQvHyK>T(wch9AdTF=@>DysaFZr{5+`HmwbFq z=M{6<8)h7yXu)zMvg0y27v=7dQUD3JX6OYpmmCrTC!_cvBvN_Bt0>#ZeF!etf+yB zUC+9VgHD3HDAdVu6I)b;^!9jET9ECBNENhbU=sye(NW~4sCI;dZpq$ZS8OLgtEyP< zoB<}i&X|KfSWP{J#Jylkh!Y2iHVWCzFkwsm45>f~0%8CKz#z)1m?o%7F6!ir80wW) z9r)u$>}x_}YhC=*xyu1d zwAGLT`fJS=-y30kt~`*;FV9U^nOg4*cl4^RPrz@;cO(c$ouhin{L9Dz7MP93FvV$B zJ{M4mkK-~qAi*9*_y(|!s*@bQ*!c=X4!S~?UKK`^g|DK3~FZZ zE(Y70j>ZMuqid7eD4FU!ua=7UOozi*lgbna{4T4&wv!d)uqdHfh)C2!=;-hNtmyDB zSai(ObVzN?e_=e{rp3^t1F0v2pb1Q}c_XPT+V5FO&JO2!h%ixTQ#6+#N@oxyP@;51 zamJVllA}DPFcpRc2`_-S^^@i?SQu2A==Zm?y3pjqxxtx0DeqgZ`0I-tZ*pt^@g8;- zV;tkSs(|uYtz;%0LceaG(+I*ENJh4LH_61HOB(7|Lz8Yh0+?te&5>k(aj6gVd?ze-WA$Dyshk zNCZb(c3_58fAI`UAfu&C{2SlJDF0l+X{^3Ml=~~gFgdU2#eD6{@Pi!cD`~oMa(18! z!9l53dB7vb+Fz1flo9$WE_nH%Me&yD-})6kyG(A>;7S9IN) zP-+xI*Z>D@G*ejjiyq0|jvUW@jsD`ss~Z5o^CbShjC#3|O<^b=m#AHXZSaYupVrX{ zW(!K9`8>KKgKSu+jNF$BGiXssU#*^?8Z__GJOOJi{3W4Syk5aNmkfp=cR7%X-293} z6#n)0^N~OFJ)~gjdwqTVr;OdbqVsxpDuA_2l+Nrj125{$@_m5avB$pr`(Mi~KdXG> zO>{;~%r@d$b; z0m{&e_-YaH@K}x)ZLd3CkSzG{{d7aYM!t`@-p5%6^nowGv7e-71)<(YHxkQ~pyOOHAiVN@r06&1$|w;Ll)W5Rr4W=>H#wZ?JV597D; zyLnY0KP_rL-6ph_i?l1^veh_a2q-wAM;c(1`bwYKq2%6>o}lLbENg^LF~va}Nh%Jd zpSg1y6Zm$lp&1_|!t`_DpJlNq=@P%G612H%a=3FrW)u$CjnYf1dp&K{CYqVkr2XGW z_gi#$JcGh3wslBQ@&Qti_&Hj4$55o4_aUz`kK-2k;A#9MK8|!+uDmiJbC=X8_Hywg zRr%9ez)zE5h*p169C7`dBNJjJ?>>_)yM(=fMPa)K;!-8XpoBM{c7%ddr1B6J%fgNY z(Y?8!?0S#z-PvU--Y4Ncxq0PL6{Gc&7Ax;~h5fDNcUm(3x56=k_D`C*UnKiMjY<|G za&vl(AOVYbOTQzA>y6Xm6)`CMy8P=cxlipR^Pha))OTd%{$xb@dneodhs3XaLzl(F zH54UPZw?HT0_66<@&oxfl5(rcb<2ih%gUHgKD4aAT~OBDEL^$JpSA-T=ifeakhs}S znl~ahV*K0ZQepciZE`f_+;7APzcOZ2_GTw`C;_wIe+P3DYZU`;z7uzOM1KS=7cWc| zX-4C%d^YjK7wXiD@TDw9p2^`HkTMhGd!3}i4$_Om)xKEUZ7JjsjY+V6kTM$}1r#0* zNNpHt^SxyLmy7_oaSpzKduu%xyo)`ZgKZ_{C=GHq>Z_2~|@LR4D2BAi>Uo&{6({K-JmUU*8O zO=~;6#y!0k9DFR2}^z1#16FuaMw-2k_cl) zc=bf^(`$iiU+j~97_Y8(;skvmFNvaG&OY;m?k+7V@WXZW&N|5%Yx84+Q`3MKIxRfU7_JR~x#f4C)ZYQw4Uh|BxG{9ii80dp+C9Q6a} z2`MHDSvc+U5ft}1=G{WmW^FdtKXjbpz4a*c$Ck8&zqB)Fi;;A zmoAi@jgdll8cm=sDRnldCa&SCc5hhS9Q2(Bzg!?^B7y2qC$Uu%#F>iMQju-ZqVG;n zkNL#(CPc26$VS|~sN%$f;|IGh&Cd!K?Kau#dtaHKl51(-Dcca?k;*%HMB83*EEuC8 z))GJATx`co?PK*fBY5r8x3BfS^+}hyjx+9urI-Nxt%}ovzQ^jYlITK2S|@u%g=3Mw zlK?zRV;k>|0}~i8RdyDio{a7VKeYi+yv3gueWe=ow8h?duN)msI9+mQgR57!>p_DD zD#tUktq%jfa4B*2L)L+ah|MfbVEnUBp&f3uN)iO``ursLv>Lv{4G>f5-8P&`%|Sn@t|Ra`EOa*SnR zpNjXpCn{yu$Edb}np;4gIK%j;LWa2PYg&24k72S6E(<%`8qXWu?$kFJah~Rga*A+i z&)*)jvO;`a+6v@m-=Eg zspM^`^srXzsqKyTkM;^)=(2H=+UE|4z=0U?c>$F}pH%FKVwt@Ifx4C>C|q zF@G*X)v7+s!b~Jgzhhcu-BQ1YZFaR94U)NvJ^50AjxZi^0grW8{fj; zRTT?w7zov~kmraE4#@10L)V&wa`eae@UjjfxnQKO_o9OM0>s#SI+~|Z?tG^ig`^(WqCaP}7ZcWyo_$-j5qZ>*`p56(CRoB@s3t@( zO}icS@)2p7U?8}u)XCoxx7$M#Ck*!MD`ON8buCnu!b;pFYG!fBtYADVV>$e3M(8IT zOm*63N$1LV>k(V0{@AP1<(I(!Nu6@oT8KgyGf%T7G5`RLqCE{|fwM~ta zcT>dhsX`Oa5o1M~M2Mdjy!`6Ch5TLFLE-h6U4Pr+sUih9FBzGDDSRH4s|H0 ztfe`(MRw>3Qjm;8GnIiT$eya3C5p>h5AxA(;c^wZN#0kUeXZz1(FJOU7br7BFe09v z4#(W=KM}-icjBlkD*9OWXv5JkIm{|&a7l*2*%7@YP1k2nYlQ7i7_XW0#}foh(8XYYB<)~f1CD@`~8wgEki$h>TKpHi4IwM>yVeN??=6;+OqWO)$5 zX`rUHxr-b6Wal)aoA36gL~$RsEllwHm9|muIh1zi@5FjAcd&;1KTr^4TViH7#YFhQ zJiwG=RWk<#0Ni)8Xi3PtoqKpPk8QMB{_Iq??;BmT3pKOZW2cTw-2Z+no+jff1qJDY z)+`7c!t3yNEpY93Il+s6w&wSL5dj2vm>LOes&=Qug+c%#g}Zxqh{ZbE+uqN4Y^(A* zvY{JT6r&;ezn?_l36eavbqeh7iGHI0VB$Oa*bY%4|8jhS*LR_jOjATBGK8!0pHS&r znrYu)1777gbf36MDf6hmSa;)o&8LQuCF09Lie}kGv|D3hbtHQ1PVBG4@d=ax7rc}W zD*VwySp_yzpONOasyNlA_H=%@44_G!XJX8{nB3YG_S72QY5Uz^HW~H6$Ksh>_Kdtz zh$BP8$N;G{f4~W)+S}T4u1gpo(PF+x`r(n1-FsS)Q1Y&{uB3Aa?~iO!+Z!YJ(C}rT z$TuD(_)7AKcrRUr{G&-tet9`DI2$0Rn6hDOOcI3yUoUl;kk|-qoqOrI$Zcdf>cI16 z5}uxnX1OJEo*uS{gD~VM(2vAi8z1xtDQg>ta2q`)3#`a6x`Bepl8RVqfuOrEdJ8vi zaiCfVJ4L94EeoGI+h{U;;fZ27)AfAE7w1FMBT>|ub^n_9XBmyNLwMty7?P6}hiTY{}EdrdPs?n%~*acB%t|o4&q&$1l41gf5#(G9B3G zYeoxrQd@ZShf5n4RBa7LE@;<*SqK48m=6#{|BB)?6H7VR3EvAXj+5cg8V{KUxZo-( z$`hSW0E7Dh0(l$lMVCo^QGrhFD@Ow`PfW-xyIbf`! z(C%asQ=2i$bt)nPz^{K`BZQHH1uL#iTVi22{Y#jjn_R#V{P_A3j=Y|cnGaA8cOVfa zZH8h0)FrZdse7Y^Ndk2z5_vlL_`*U|=^b&MC$#I&h>42}hRx#A5SV7LY=9qPfZYqU zJ~<4^95mspiIfVCA;9wQUWT)~WT!Yy(xSZXp%iddp@ob6JWHE=-e<-PB11@CA)J_r zVkjpc(;c+ECDZcZ7s^xXGE+HYb#-G*o^k`KE~Y;7hiRpR-P?ui&MdZ;?u?1Ss&LgY$Y9 zCo45f1|iQ_vtz5% zL08i@79Z4g^i^n^vH((fkTY5zwukbGSa_AUUjmS)#8X7!%7;@AM{cJcFxc-SlKWLG zF!r~C5Zh$Bp|3N4)+Y*7fk=d!(0?DIWsLi_ zl;;#P$r(y3>Z9!+9Mo{~z+z*|hpNfra{YI?Wt;WaE9tA#6=-UjLoK)&C$fJ-(F?^;*{alGT;B;9ARjfcIztd$;{8-s)5uPxqwxFi3GF#`~i5T*QY}y2{E+-m= zz5x`l1s1xC8d%77;0^i0!s7TOS^wM|7SZy%1^tX5KfrG&Dt-J9hD5N%35MFBrZ5@kB`%Fcc%uBy_0WgA5Bdqe3pg|D#%ET-N;IT=*kW0J>pQwa;sY#2$nG^B*$6ENL9{5u`)-`~+1V!VU z!iZB8aHZDK?_2b8ym1*wXU1m6VUZueuI|L5Pe^diN66+}wTvORf_^1!A|y&i*`b21 z3q$r=Ni3Umg!6|R(?ge!4>8Li1{B*+($k6&Kp1~^y9Xa@RU1Ss{jlZ^6$q^ndglB4 znGk)tXMA!6rd|`;n@XX#D1;!)*wgm80X=V=jAF{fGyvBPey%Kx6&n{fCpQ;k%MCk1 zx}-GL+{Q-!GJ?cm9I-q$76MX3SesNhQ9mx!o6DSB391`<_Et|%jFgr-j&2rh8er@vwOg@yAA zJh-ENbL(WFe@v-0+5PwL2a98zXj*heMn2>sCBV{GgG&y<7`T}uRvz~oB)l`cCXOkv zV2v-PVw{?mnqbL?nkIx6sk4^mrs3=$xrprk{eML7W!#9??=1CAuYa1wE5y)wzDhlY z7sW#Yhax9~Ce4G)51d;=(;_%)MY~3MKD$nmBos4t(qx}*orI2Y7#+@F5G2vmlCgSI zrBBw)<~_BrN~C}PE-nEZR(Gnyfj#mu62&Q{ugyxU`nOkEmNhJev1fUJ#kOZXr>9Uo zF%&JtFgkn$KNt4A^A*fV7I=|vZ@aWo28wjog-2%fg^s6~J11Y3M+fp*1u6|$B5H-L@Enq%GnrrF9}VT3Bb~Zu6x{e~ zKzppEdF9|!m<5sNk5z#ohKN|W--VGlY3Rlvl-3C_)z-xYzk+tWI4mtWHKo^MLPQdR z90u{2#h@4L1Y`56+vKv^9GlZWzcakE7 zJU%XcczTLbA|?Lg^ymmxx8x3GNsF4A8q{xKR%zKawn~%Q!WAf%q9q*~j=VI-l^NC9 z_ast?FyjdaakmBqARgpS_Lwz;-z!>nIc&T+#qw%|ZRa&cA%SQ3QV=ORivLLpLpj#* z4pCnZ3=GVZo;nN@>DMpP<4vwUfV{jU)HS#8$Dq$F+HK(GBSav2%m1+e<}3rmpurPH z-V7mGS;X2UXoN_=yYr7U(os8mb%R3~Kkc;eHeS2G)cq|FieQu`}snX!ydw) zL=y&n{*ap|uG(fO31r%vLiEy1H7j8kcv`pIh*OH%EDBm~Fu0yD6jSQr9ebBI<^R*S ze*Y6Oek=CkaUVdN1@V0khE(``Ga;dh-HA-sR8&+1Mg%SQ+>T?lyP%1U>wBD{&V265 zxzSVM<>KPD-%-Fcn5O6<((^FE;9Q6nG$;iX24mAqNW&A!&xTSokpVBcR(;sQ?Aq`* z&M$iA9ynU)7k27o`G1aB_s&nF)+eag9h`gRYa@z~L}tc}Vp&x6ACEDQ(I;iAJ2$b+ zggZHWoP-SQ#cd93Ix{fWM`gFY-72d(?`6}pzuaa(=!e#74V(|+Y)jViwd{i&SD`Um zL}TsABKmp0VGFR;9ht#4f5PGkvV7jYYiMfPojLG3JOZ_tp~)}w&L5#*LRNB8)XU2; zr>A+5B<~sSP9gB{exRRpF#Erx11|qRi=4&TI0Rh^{2{}K30hz^bgKVKROV}HxFbfk zAk=bYCe2$Jh5W#1$tCVOY} z8jvTZW@CTns<_&|Jq&;L{3rw03IW z*a$XCNw*jzZ-Ha!##u)cC{!06ER5Vx2!+q*jnwBcy|EHg^V5FB z{690w`uhP8a|_WSA+<2PhBgqHG|+edyPX#7^ni=!f#!w5R=>8C%p{>=eqRnzVBR>^ zvE@%&)w&@lIf~ty_=)U>4*dHL?B4#cr0fH5fJvOY{aZ}jZuO;*=44xj1uPK4Uxt$uP(BXR$G14E(#aFG;9_-io+WE4}Zr9W6cCn_Zrk*VH zQyaZ3OwsMi0mj-Z@6fU;NL4IM3P~29R|IGZ>0RUbb+V%=U?3RADcniVkT<|ocYV-; zDgs#S0^D+;|hR#?mPTzXR<%vl6(li8zFXdJi z18~^a1x=i#%Bwp)_Mj3~6^B#-fR+4cHK#8^hPMwd(<}zg(hn}W{BCb zZ~^IR+foF5M;l^bDOzFI2+RdwRqWpze zU-^L4R1MFoVRAD|+uqyJPih)E!QUzb7%d@52pm15zc>B3RzeI@3*g`xi^JOL+&U`t7AGqX~Gj`)G=qS>K*?VFq5*^O1->X_#UU|(Nf zdj^25VHh^No)2;~+t_&6-B%+_7HQah8>R>QQ?pB-5xI22JB4C{`T}$e+W65pRG@C$ zf8MjulkupWu417TOLfIi%0#GrT5oqoqh*BsiqGG@y9%@#6jHsX2!39P)<_Z7_QJEU z+)<-mEH0@SRMq9{iNLYVYigi?9yrf5S$%%8EH|mMlfYj-Znb}$K_S!S5o+1hJ3RhU zJ~^)cGHkdT%H}lN^=qS@R3VM;;W$OCw^E%J!c}&L^g(Jv@?#HMy@hGgnFQ_4gaLp- zyCVzoa5>Zl&9LABWS-`GQVdMY=-Xra#}Q?%)BC@}Rs26Gc|a?T>pk12i1n-s-Ro-o zuSIc(xNZIoTwj82N$VRMwjUQA$7}=tzQJw`hSBr)j$ARdapBo^-m-;)(L3JuKh8V? z1ob6_m}a=qy@DzIqH9T=TwD;+0&B^qdT4Glg{RcTcGIHZaq0hA*Ch5vmmcV$>Owhp z@5S`LLUz|ugg<Rfapj2mt zE^lrm5U#Hgo9wTdAQV5WD&kg85GqY24MNdI0%9J+moIUO^o)#IeO*J!PIE$x?5uun zhIkklm^Ce_J>xj+!mmP918*;nXf$-RIc2S|F2e(>ySN_Dt*Br%0~++0k`ov*a`L~s z!)W?eXe)XdA3Og3v%)%1^(g*tF=sgqo1+((cfp#F5J}Gz3VZt!3-W9jIXFm;FXJWH z$jFOg*3^2t^!TNi1?i}1#wHC+OkjG6f2t+xLJCRzQTBU?KGEw$hM8Us{bhhSKf)Gt zfUm=aIUs(COW`bdh{&R*u0C+=fy!Q~ExhRT7ee+XCnx_T!4`h+`hX3=0(m+8g8E6Z za!^EqVIe0k1+Lk)wuy~{1kVm-!Q%D7q`sQId?#pmiUlvJYW;^akAKjTI86n~^wK=F zLXLv9NbRpbfBr!9l<=G!A|xavkQB_$q%Nq&`SuRvs7R#*_#wU4HF{|;|Nbb(CW~fO zw#?!%JJV^=(2M&E#HXmr+YW(q3|ShV4DFK*zfPNG?gWf4@f|d+PLTVANWi;Q!?@c* zatb=^A0@0MCM`u5_m{!;X(fRIlL{n(kk2Ke7bFJTq|3ga{4`SLzwQ>>;*c{R$TB zMrDn(ZFj`O*Yfmw$GHq=kKnLbZ}SVoeA;ruSD7)F&x<}5h`VIEJN+&q0wb`8P;`JX zHM0t2 z+%LTW+K|vHJmD6qx|Q@wn<-0thde{h~b{Jga5!FKOD_+ki~(XJG#$b=CP1JZJ;2_(pYs&ES2+xo@B28Z=A zvz;XcU-I%Wt7FxAdSSl~)qZwDYx(Y9@~b+G(~Jx3mMNwxF^H8&38WSd43Knx!|UY7 z81mWfLJj69fSWBzZqocQ5z$q*4dVKooQb$Fx7kEzwel?{rKK>;(Sri~W)Td(^{JM3 zsxk8bRAbg6l~Xv7Js{ggRp(h(MU9V##_{E!40_PP6Vlodd~?jg+sW0p~W6h+V&TuUsJXLsgomP%wnRwB)E#6__* z*MYp?sHYBL{|dIJ{vwdpSdw4^kOgHI0)2mkMGVm~j8Q<^b}0DneE-WLsljLT>5&b9 zSM@|SuR*(>o}95EwEgsmRy>w1$^Sg*w7`gppl)Y%Wq4>eRETOR zdH7=s%daF(7uxO>*}MR%Cx@ZdE@%xvS|iIY7uczn;)yz!XfD9c!jTyOIP%K;s7Nmm z@JTbTGo@ORG*C+N!D6UWfGw;xEuI%}V=UJ>ERyRroK*HU;72s*@y*dtN)HEOz#siN zF#o*>%-OL3o;>KG>B4kNi8;j>C-f35r=Aib7(7?1*Ux8VNK=HNS96%$zY7rDH#)p< zw6(Q+^fHo@QKmsgO)wE!fG_jz{=wyH_J{XBBLp5~`i{BqGDVnjVIc+I#P9jbX3B(| zs9!vEQOd52G@>>~+A16$%W&{0m25h2A99NgM11nR9X#@wJsj;CCwU+@Hu-{n^v@5BCa^M`l-!at*woM6m61S-H*j>uk#_hk zA3wLP?}kTK`WI#JoVllxX|j%EFeCzZZ6o2^%Zq%076;JY=hTg!%x)Dr5O1@ys;nVF zHfQxf;8~((9OcfxGAO_5XVS&JK({pSdqNcOP*UEfY?qnxzJx+I;5h9nuUJ?`U_;7o zx>lp9tvNca97{t=7%@o;mhO?acrq0^4qB8*BJu7lk3zS;UvS%Aw|NyGZ4vlSVEl66 z4-p0q3{u^rO~ZYeJ_I0_IRtPVq9Yl4;b-KUrM#B&2A(xRzxCV4^y3G zKjCDA0Y6eY!(>DuxsmoMI{;yxzo7$0@4s0i5TQ<)?`48~<3&f|U>{}CMxhEg#9mc? z%TQ&={;mKrCR|qPD2(Wejbvn)p2lvSGx;%#{&SJpjaRoEWwKW}zd1cn{t|3gr5H@n zsQx)=;83gV;ATy0q2LcHf8XIkp?;t=_45pVmbYnzSNbSWxXM^O(;v17>vI_^l8d(V zy(7Aq36Ekh`w9Ehbt-f5p0rzVyF(s46~#? zN(1VtmsaWJ`!9CR|>Uv@=J+8R+5e+fnNPmpE^;076u zRJ08lbn8BCp(0G|#6^{hU0Bf+0Q zSo=Lg7`~H2xbSF!3<-gi8(z5;QI3Q}p*oE&-3{1$L@k(0)AB+ke2h4pa&>a2Ep*nW z-FQvLgc@!!=m{quk;=eMGU`A^LIaaI_=}c_?j8rX??M57(>qetJ3=)`h?5LLjNKLtXBa}YLQd@% z`e?_0l9nUmIFA^aod{fUDp#(CFUq&!VbLR*pl3Ql5*6<3%`h>Be(+!31Yuc~6&Qkl z`Ji=(Fef~pGjkUafl?hj`yfyjZe>G0>~ z#64k8(8-S6boiXx?ZWQ{*_x)$*%Vol)I0-$5&VG2h*=!JSA~o z!w25m3YI~;2X-uq4_pl|xUhW01pi7li6}%{XL<<=FygsZ^1MT^#;qIrAE_xupzy}8+393Y#ENCGrES4vG z`Cve4b34)R#WLOs0d=zBqN~o7X5`u(j4s&l0^TOM?>IS9M~LHM0u?xJSY$!qn^!+! z8{S!*SUi<#0WNG^s-kyZV4);G(QvouPhu`Da&0_ zto+XLh2nYazy#vVcS6r?lj=J{`dFOZbF4!UF37+U%$C9&rCjBXf4NzRtee=Sk3p4Q zh+ZKo37(vQUe|)!mBV~*CL!~favrU6FBHI}pYujnAcZF1Ti`2kVaJqHa^*c;SkhQq z+NjP$W*oG|0xPoMYAF3c+bN>U%LIp|LihIyDJZk|^{&c>Oc#}&&=T}RuNy&ysM|S# zK=bV1DWm@V6Jko?b#{S^G|4Qaq#JbN~!3NT4w`-2ETV7ILPcg;NVc@q47O> z&KX!FNny|hfd$V?=4dBjW!lxhh7QY0gM8yGRL$v%Opj%~WJ@!56!~j2xYf?ioOph! z2pUY;WaEsyd(9KTDBrDD@bz@hk{(@~-7AZyeKmyQ21MKQ&f6408VfGVU)n?Ve!2~m zCh80ci2?QX%>J*hK5QB1-*?v;{m!7{D|1Ik)e}F@vOoe-#JzD66s3uINm_U?4Pi&h z^>`lF8uUb(A{f$u>Hc=89YR=1|EL%$crwUGz-C~}IN83|Ptq_5DM@!uekkazm*gqa z<+D$wQV6lUFRluhb$jdWYf2xuJe@Def%)-DR|trDNso?D6YbVf`2G^1B77h1Tda_l$u7V1GR_s1{9mrHYZx`Upr55F z6M42?Pw04eGIfufdbJsOS)X`7hKhAd-Rgd~GCI!kv3g-)__nPj@vEb@%EI;|?{S;9 z=7DRW`|hAXcIhOY^iN0xT$sTZN#}s`fA^8x1^M`bnV%&`VxX;^0TRiNP)zISj+^+o z0<=pF>XR^6`^x0ny%S)LTJ}w9K1A4E$<#e-5veUDu}h|8z! zCVlDTqROz|bkBXqf(R0JB|GC~#}7uL#+Aspmc+QJnR+Tuh_Ysd3~@MNta#xqY^!q* z>Q`0Ov90%NC7D{6ldo-DyI5zi*xJ00li5X2(%rtvHcTHMQwcU7vEv{|MgBkBlEVRN zHcu z9BFNfs$?fVFB5x!#HFaLxc7SZ@jj&V&R8!`8Lm_*?9g|E&B+az^8J;O zL*(&QrS;?2y^roz?qyyVCXbrmzPJoJ+8CRE)d@DR`P^pR=@oQF+3eK(pJ(f*_~QpD z91_nbF}WMZC3oYj@m5+Pcs^RHW0h}NKCnOK9%l*c4r8{4osu;tWpAz4-s?Or)`uKh zT7tw_Lgl>3S)@bqgo9pl%=b{idtl)*)f*Q_zH{RRP-katoJXoH!S7{h%zi>w6LS37 zw+SWg*v$48el68`rLsBmnv*i-lFW_c==SuA+9SfrijEG4Q{>mh60-u_wB6bTDOO#ykeO=o%;n`BI6qZr%UY@F*{3!RVO=c z9`<}1ODETRo^qS=Oc0_=uQN+~yTfs~lJEdOT8d2H_#Oq&xK@4z{&|P`y8Jgwmitkx zAwuR)Q?8~)-YMWUsA3;Dp~pdL!%&(v9muxmc9W4PJaTth!x81G%ArcHL$hHUMZxTMWFY)CJ) zmT)OON>X4mVn>AP)rV7DYW7Zp>A%=skB7tPq-Jgwc?=*Wa6PQm~C z#nGxPZ&&kS#Ed?zJp;)bv&C-U4D#Jq2G7B`JZhrpcaCMNv;iY%9c=#u`Q7_DI_or+ zzT~^XodiTjc6{zk#ar{%`*NnDwWZ87oFLedf0a|kVqNdyNS^DEeXWuPM-cncwl;aK zkh0XhY`hXU7@Booc8i4Ibf>{Yi{2)+uvA15hiwuS^(T zw@_+sIH3?tFDN#Gy?QWDH$>~j!uCa63pP*9MlxfMjPL5K5!xf27~**ELo3v`gswUg zt*034kJ4PumdgAa7jA0^65A%<;HnjUe9|FNPc;G5^W*7q+d+JE@h`E;*l3PANwljJ zGK@Q`j-p@Hp9FK=j$tr{p^5Puc?0^6wP63!9ATn_<;O>h=^A?-j(A7XX8m|0DIQ>-1^iM zimtTOW&~MwpwMItdwARM(OoP7LMDSJD@Ox-?-WrU^$>`3FcneuUXLqNjkQuGbS0SD z#%;C}w%|Pk$+0D-pe#Pp*;%xls>mnP&h6<0H*jYn;B3(s71ce&`k0`A*Kufx49}?Y zy!#I?Rwc|VktoS;>PhlkDxqK4xDMq<*r9gWCoo!_{B+Hyhqmba6e@6uLQcLp-|d|7 zNCyQLXSU)c>|SM2M&Tii?TqC8KjI~x`0;I90WgM9#rTgfw2~QkR3pET5)sT?6tixj zTz?q72-*MckD2k4bDFARHJZRGHfw7t3>?@;G%aMaDEIVe?Nz2MHwNj$2|OnWD}}W_>J0M#P79Rs;}CEv#$-D1EW>E1WS@WMEG*!*0Fz^(X9^7iu2p2 zyEHeAB{9-ChBu$Pb#AfPYo6;DV-xq;^MLEqh=pnrtQ1OtTCC0Y`kP#X{K zzG`dr^`D&u*T;&{HET%Zij7S5eqq4aG4+tTzp1}pf4)*qaliLc=lx)ICkSXJ87c({ z?C$!mAA<-qALlL>SI3pfpf`eR=ezmUI=sgNr;Mv zMJQrE4eY5h@m(N?m{Ec`_NSGHH7{cE{q(ypn%l+sD{}1e=L9$ zf-sxGA^anEO=F2c6~?m%Q&#oNq#Dzpo%-Sj6`Nv2(NBuIJ`EvF0;MTirYC%|+R{`| zC2;w`zmpNZ#WA~lt1TF z4ckQ@$m2n6?=iqh%D*!c^BP$2KQ-e)@TFeKTuL((Vc`HR@Ujsgf%HT z;{vBK@NR~Yo84l?>Oo8=KW(}}8*+#LaGz^h;F$|2T(;7S7R`rkt&G@*I==H3 zk|`;1hb8B*Y_v(Vsv<0yV7hWCB`N6yM6it_gAMo>T%6DIuT zXLO(b!Eo2X+AAx1oE%CI@GDM6&{rT;PD0(XWzkg(WP@3gsS%hX{aai?Cg!9{cm%Ah zbBgC5J_Rj6jxD2v)=D*mL_!IKr;sMf6w{ksjdhxFfd|ggrRklT2tlcqR*C}{EzmaO z#3m{V0cBf4(R8^K)o|FVbIX#>A^9^P&_Nil%CxaR-Gn5`YR`lwD!7S|#`)y85EI>m zHcK^VkWS#n<~nxm525&mc6tn05Krl=(nO}PGM{Hhn4{8Z7dxFQY!Z+oq*Ns<~v$KnFUnGz+sED~!WmH#}E zHj~l9i9h$5frId56~nj}KmcvwF&^-;{W*ss9y+HSUf~aV^E6!y#Z53*DsbH#RX)Xj z=$VA9vVXseOzdYbp?+Q0&(<822x0kjm#{#g4>eR~hpCqAVMB;bn82%USh1z==ZuAv{U&#T^wBoqfe@^=Q9(l*y z*NGm>(|dul8$?EE6rqGYw7CmZnzTT96YpT>*%Aq*Qu=Po1aG!5k#Ak)Rw2(H+cKMM z?DIgsrl!8|lr~`!nTQydSM0z~EzpVgtrb$aXA%C`ghsw(4;7?Q*E+{&zqm>kqfPs_ zx1&RUP(ksR!12^`{$h`za46@%!8tLD33}g80jCRg6?#Y|RU)O9#-cojS!BbbsOZHDIT z75{g!W+lR@{a&-7Ejq$rvo4J>`kp_^B^l95(;ZnjTBs%;OD3ckVWrVxRVagw!K?QQdDZzd;5Lz>eH#i*{u?+C zk;%TXQ6#FYIQ5G#qUIM4M$E#Jyd^+wY7Pn=BH)%I_B}#LUjuflkztgXZlOTu`MS?(kIG`r#xSS z)??B)ZJ_U;8dtm)K#$s=GYGe=73*6UCbWV+8uYp2oN5tz+SE@;#nk_lNeoKr>Qh-V zM}EsRN%?MW8aBSOFxi2iJOy_5C;zs(mFaDRJ(TR+5BCPyLMUO8gu}RnXv~#)C!9@M zeDx23TGhFsYH~EJgry=pCHpAqormd|eWpr|07X2G2v(~=c*|x`nTx1@llHcAQb8`G z1er{c;zZJ_cDU4dG`LgFmJ8S0Q}EOGbs`(bq0drOpGfV}g_@*3LWf?QU8SA{%jtoH z*_Pz##RVI-WFFzI8I6olf zPDA~Q3mQHTeW817a*KQ*7het;iM;5fRutNfI*Cj5SW|*lKz+C%@FR(Nuz40N5tCx; zCs|O*`5Q`(BSEqjlkR0YQpui@o4c&!q+OpPRFTi!>@vzPQ){h49AXB8idaeL!J?Fg zmj+0f{PrvCLj`*47j5=(I@hHj`~;OS`PR@JFx|sLuMDr|SY#>UUJ}v0e9keNZtrp> zkJJZwgi;$(xV2B29QfM3IE)28d5I+DLTE8cB%ys=kbgTQ(ERQhRR#t{bFd5WAPCL? zB`ds!EU*BF$z=C(sviBVAfafUvcedv6GD$lo#rvy_n&ym=SSjM;?4Kq zi+(@oB{BzuvRPexRf^NkcI4MVY6>xCCCvRlQF_?YM6{g5mRaf_3Q5dg8?dGWBdTMG z?@a~hQgQ=ktHqflPfrp<$1O|%Bt}XoO36)V{^)4WS2n8qzi@J(W60r*re7(qpwec+ z7?BcCu99J+_sjG{re!e1fUtmGbH8x$gAYiE@#t0QDE1Cqc7uk=U}*UDl{S((dizds zz={HDXshU390HI1g?biKS))?E@t*}(P@k)+{I>JD^dB<*90dWQOiS%5xogN^hcIX- zl5#Dhr0}{cV3{78dol$Q?YHUznE3l5Pg&^(X&kynL|3ylX-x>CAnYhV9n>m9sm=Pe% z<)HDEj}8PqsW+5W=TLI6L?H1BX|dG6-9uSnamYttu`x&f4MU+r4n2-Phl%WC3tr6A zPCOCPn%icos*yZ^ZKb?3uCWcRbJ6RK$Q1_T5RIy9PW-CJ?y)lASTZw=iB&;!seROB zL8Z<`l{lSNnUy+@`Da%D<2zmB_^AmtZ5KCLC7#G=A(%?xiico~?~@VrvDfOWpE5(a zqJ(y!6rM+a85bNPM)&FYLW%Lka3od2N??PTNfKd)wh?!W&4^i>qaH34pTrk!ZNJYj zkc2U%%Z7EGOjmI8R^G|QK?zqr;Kr8><)b^?e7)q}8yy*4wYgs&{bQgn3_`4IfGbbaF< z00wBvqJsH532L*7sC`1j>U}e{Z8t(@3|hNxaO>;&2TZ;1XY-rC^rTs)``l`_c^tt+ zz((brmtSxGaaP%eLaN=0?@Bt{c+&w+L($Rxpb{sM_|=dvU23q29x6~1-85o$!ZAef zvhV1&O2;{PIWtfFa-{1e?sBmL@Q+7_PR9`v&BRVT=DzfEO_f2Kw%4O3B_>YVcuPQg zNuu}8EI5N>OZ$K(osvDM4B5pNAsneT63RhG;ze!x^clad4qH4Dtyz=!AM)P8g)g|< z(-f{Y9Rmk^@1KDtDl;&Dus3Z=2$295NYaS6ynTV=8HXO2Y`VEC z@=(Kx%S?1&p-&-%fP##asaCZHDzUx6b6lW#d{Ur6oPy=70vXw$eR~*EJxHkL^gOmP zH#+9Q$sxvt94v(B>DBl`x1 z_@eP^pxoIFp_--X3>^tc1o!h4#nXyPL2z&rKTprfLgmio@}-CNYU{o4+1ZC>I!a3o zDnE#&DORExkryr`7AHZtO_5u>&`3jAkhWF?KEnCKgU zhqM!8Y~gm&0D#FD(-K19ndBG4MndfuwpmKrC@jx6mrOGYD>yKNmY?MOt2eQnr?J$b|T(^gqEneA$mKblp> zo1>$^dm}Up!hz)ofd;6RK4I#{Me5Hn$xbu&plS3U>zD1t_~eMDEu_r{Jjvq9!lX6* zD6hPq%vRXIuwv}|@=0MeCd1g1JwrjgxxyrTeFGc9B%uf3C#BN@$Sxmri!0Q5dPVcQ zsAEeg<(%#|{m93Ke2UEeOOs%)&ktGWd1W$oL$0^uE03`3z4mFUe=Q*Q-U4%U>x;Qp zN@f2K+3MYintkLW%NY}C#q?TohMcg+pMsyUgju2Cut*LeiEHdB!%*F>DoYll_e z?hr1M1%z(1Dan>D)uqOV6Jv=KfX^mfE$cINUxN! z2v_{wOavWMAE`G&->n-?V?;LX1D<@yV%)11i|7 z9G}L%^C<7t|Ngz*K-mXI6FX&a)yE2kN)*Px{zw{rJhdnjrddZ&@Ln3pTb{f4%>Z8? zoSS=!#j%ahQJnmZW#qc$<{qI$PLE*8R zZ7FDbEPbh;PVXylt?fdA;{|}+ir+zBdF(;wna`ekW73k+7HDg)R4zOmV+?d&n)|G| zFz}lL^}lM4i1a4}{jHY-_o~izr@8|uKD}zSyN6}(|70|18Y8st7amK5)T?J7>7R;7 zxfm8ItIkd>OPKKluACSC?jPNwPHN`D?IJTmfF|uWmQ=Ig8a>xex}U04{GoL{J+~}i znA6iZZ*Jy8)W;|Y1%4XHOR zD?R0V^q)>fY4l_lt3n**s3A4sv!gz{?TCqOCBwkeW){}QOX{M`t>eAXHv2M3s!u^* zB6BSIwCKX!#P+=l2~}@*X@vD*i(sZ%6xTI#1_gBtGGU~F^?8>_O4j}n|pb*JH} zvC~TNlD_v_3)!V^d$}s8>k0_if9(AloRXDhI=EFGI*3HQlo_P&31=M`xcFLIu`+U^m5!4UzGjNUmzEV#k>zu>K0_LPeQDy;A zn&5^M&Z=oopWD|3GK5Zn2_5MAZ7p)hJp{IoOlRhujeu zP&_X4JRBEazpW8xp`5BQ|eV~i(TWsPd z`B3Q>@OoNFK+%5%iEg+DEtfmj2IlrppZkrQ*e_h zHmCdj*~SC|lY)lY&;xR@E&Fuy&$v8KaPBE9>F(Fu<+jTxz*lWbg2c4&I+z`7yI#mUJ+KrNg{kTJ06wU z#@3wIRYPrkAROk8bM{H@`QvfyQ<4IwU-`a5JvTo;^CbG#YMRorz&Mn5Wr`c}8_dtJp#>++NtHKGm}*6_Gn`WniDtbQC{6le=YlAEQ9> z25oydm*Z25`e2I+h7t-RLvQOq!p4rWchaSf5Hca&vd=clgd!nH)b87HKF znW2}H(+KZq?!xSRaF4$tgS1*5e7_|35i&8snB>0)Ah(9CxIRA|hUnzhazvF(Kc~CD zp6t#y%CFT%A!H@ZFR9mi(%WY6i%wFrgG6!zGyKJvFWn?KJWoGly^JTxk1>ogdwJg9 zj^-Py+Bx}fH>VsKH5mego(JnbdOs@q{P?wpY*`kT&gYd1F^cG}TrMFQb*7Ws4$Ti! zAvyz<7YytkN%U)PRs55Q0 zGmVw?Ii59gn;pI>V0Q5Fts1#oEvxTcxzn6$?UFnrx+^RjLUjKUv~rjE1I#+ zsN{Bis8g-FzS)VoG?Hw$ifVE^!S7ZB73q2)7hxFcd-zs)pCJYGUOVwz!A-W`Fd3VZ zt-0D>Ci&cHTa=zW5WGF)ah$RR|Ej#4_jw@uHa_^+7s3zvxv|yLuSr7#<9-a@LH}kn zvv%)W<#|gzHa^ZzBcUL)EqlkD&gJpX*?DOK{tW;NSs|ypkgKaVqhlJOtwR>4ecHj@A<+W#=~VrBi=jwAi%CQ438YizyL>3r?P#KUGV^*3=jQ%y{w&JTBYzV0rj z7&s-RIRLvs(;6L*41JAQ;QTFTu&Br){6K+lffF*ywtWbmt2xZuPQj^_6$-*>wX5x? zlTk#Pj4UB_^uDZuz^?!jUgm(KfRxjvx{%dN<;sn7U{L`$G!{T63Nth`PFO{g6x1lX zbfgh2lv=GYuT3UwOj_^#0G&C;f}Zwa25G<5<9k|W{&@*;{J}R=?979^l?AoqwAjo9 zwww+hkgmE>E@8AcM@;c%Z8OC;A%i$VfN4s)PF{OOYV5WYFow`i&v*WF!l&n`=_Lg^ zaK9$|c}+*Nh`c+DD&2;jD;cFzFWIf;KAX^o7jw) ziJ2#k_IBRG$KUBZ-s$yA-)+`AdfDhwaZc>91O`l#)DZjaeBEQUUiW1Y`Sweo1Blxj=q1dZ*UfCz3uV$ETtMrNl7;_ z&<_i4ubA6Al8QwB8-eIbNkO$gM+|Eeun6fk+GKyimhmh*wW<`gP*8&r%2d-P@>3t# zdwzH9c15f6SCpI-5ixPvP6$p(RTXJ(rm%cIhUwNHluOL39rer#&VHoSqz_`!*#m&z<~p$Nq`Zp>QwMpuQ%9A`@Hk zqGc*IoO6RwO)>WqBA80;oX*y?J4pHaDlETP&<2s;*!xRXXWz{!(lY_?SOWC|gEV>P z3pi>RfW3(an?q{@>TAk8_DACi>F<}1CG@4uN|CQUjp6Q`ViPBY( zpzfroN-fRlS(>iyKD*3la(M5eEQ|0X%aYk@XJqHJByuXsHE%48 zpWc+Gt9L%{LKN$1%PkyuSf#ln#SM*-f=>RQPo>GCR+Lx50rg!NY>oh)TQKI4V`Q|h zd{@Q$9@R4!H>EdM*h0sfGgTA;5%sbD6}^p^s$h;F`gmT?m+Dg`99D9TOQWJ1JS4di zlfV6w-uSN0M3X$%EHbA41u4RGFCn&u$vw9Nq-$@1Zj{0#9q;GwbKl=V#DCj`at}+A z9E^Hv;h3db)0r3R?@?v&_=lQp-!MyYAa!k3+v8=8m-~sic!stvgfur zEw$@U|U%AZtrl4@##efAD$3^ciu`VD@=sv+hItQ(uw;-gj@;rBcjl*Br4$FGyNz2ia$>fmG{)4gcbWV-S zaf_7l*?0^_d1m|zo!1VeI*rTDbpCSqAd-Q3Zk$?Ce_4iSxWs{e<8gpLchBT1=c^AW zY~vMUlJ_2IXD7gEt#z{Gt5;TR5^a zc))f5``hO7kL`P{s+wP+uo({2xT,Q1@+Zf->Tv5!ExKidDrM8Af2F~m3j<^Y!< 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 CyLF<`5 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

2h}zVHLVS4gcn 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Ξ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

bkjh-CZE!#gh9_ng^u{fp467 zzDI^iawC%*rRAODe%S`no2J`R45@m5Cr8Os70_J5Ce`}wf5@PPl`uzW3{dP!|3cPB zciP=ZOhI1c0Q!jSi;mT)4Z{MZYa8JZfMr~UydFBOMF6ZRRB?;XSeunb%B_D#`p3(q zR5PIiNrTbPJIux!mis&>khKZ=CwIlyZeshK_VdBn)QSTee{vVxtbPyQ%1D9K0oRWz zH^1ggWwG&3U@S`P19nOUe$BesQjguzp>F_&hV3>b4&TrQRSE)Gsk*rwXmvfbr zH7BANH!pUlsf_Va1nF3Ea-y(fB(Vf29jyos-dGhR)Pr&}1LFluIc=r?zZt^+e~@X5 z0hkb^3Li?q>&WVZS8Hi4p=fKm?zCJdhv_U6(dGM#j*krO(8C!Ly4Etv|bGSi- z7h^hk2+U3C+f~sm$Yt~6ZjW*0N6c9B$II-zi$YhOFt!nEp!c1m)l{DR&8Qk9dmgn9 z#G6!wj6jHt#^e|s(pvF^z4#Wo`Z2dX1R_p-f#1uW*MW3J6gKU_PAg&EUeMcBx#R$a zR-M7v+|<7uaoG4B9`sTv_gJ{PWvdBV|Bj}k?vBd><*UAww6wcD;rFDtT)yclZ=u0K znvA@ z<;ee|TJ<%y34GlD#==dfXDIf|O^SozoFJf34fFnSGXbSk6FCVfeHUBV!rrO~i;|-= zoIE8ow5v%|A-A&MF5Q46v%jB&W>!)`IY%9{7pRaMXb@-!^7iD0@m4 zhls6Zj%}z6H%;bSEp5q|zF9ncTT9n6)8S3XMVvKW9QcaQ?}^3GQA_!F(d1yX*eJ0V z;OBA+)y?nCky=UF`8KmB@U$v$oAt26fVwHQMMCVg2Q1h5W5X(~CVJ03SODfa3r@ zkF)+lO6lhl%xfTMk}t5xBDZYvEX?3Km_l%X75DhY>Ii_H$5;s zt)72*&HD}!6itqy38B4(py9pClsFgFoclUm^aeFygAVJ_J)WlceT27>yQ>#k!Yn;V z9gn?+^h)?n-N*B4Y-9)0%X$+c&2dFZLrX`w-*Dd~OLbQka_h$V{I<6kK%i;OK6*z3 z+g$Qq-EW4lV`a9CPLY>jjzedRLfuD7j^q}TLk6nS3`%xQ0j)JLKE!wx%5r?+9ierc zmN(5Lkqjel-FtkiY;3+>>|jn5(5mTNP4bZcQ2?Y8{F4v~nQ(1Fk&R0+#Yx^%L5E4B zIDkpvvT5(}DyudFht6KLX-S>tYFVHDGa*Nq9irgVcN9_UGO4Ac>W|5Npf;(0aL{VW<&bA z{>pj=EVliujN^9n|DLNk>!OeFNfu>DXzWv`!_=;_Tdn4D$h6vLbo~@9Zl5mk{_51p z8Di-B2)&EMI^^q88iPL_NH$lt5tB?yGyIhCu>0Gd`R;`8V!Vkt2CNI>J{Bs#B<}cQ zd71N4qt_LlGWP0fDp|Bmn^;Peq_OPqKkmRQ68><_OElfvz3v1^wyNmm{(K=iqQ@Qe zs`Kuf^yB5uKiEsNsl3$Ar%fcKoC>j|ORpjw7bfti2lQc_F%~4457vvjI_7#11_b>* z{Zrlcu?XMveJw4Qfm=n(sNZesQ#3IVr!6Yf9dMcq9%tBn!`CnycE$5LOUGw5A#)4( z#8S*UV*7X4J(1IuAt}F@R3Qg7Ga>PD`qp$g9l^Z$gIQ_;{`K+VFQV`Z{C6(gHrUEQiCv@~K-ZQeuUF>_W`3)deE(E$qe* zY;Io8wwV$5LHgP2%lsL|dxxv;h*jxs(@c|fNbA7oz6VCKf!7}lnO)9bWWyv>zg7RzmMrZ5h4yi(6$#%8xqww|AI8P zTG~A9V$u8vTj6W1P8)S>Y6@=MKCr4+cH*s_MTg!m6dO4A<5*Dvk2eOI`2%?AzdF+_ zPfj|yCG$IOfR?{08<`<(y4lHiD28zO_f)qsW8QH5zZ;=jH#eCV)-)#L$$QQ5Hp|^u z#_WGY)!bK>$E_VFGnJfrEuz|c9bc9p)wT%a{La4hhc|rI7e}cdS4w;90x0KMfBBty#+v=1 zIlgaZUA+rqW(ryh%^i_gne=R~vs?14wVanI$M-eaEGB&THmll$Y&VKY|6;!B2p;+8 z>34?BB$u(4IgwJ74bDE1SkDwGx9*V!7WD^k*-ZcR*KgiCU!O`Klx}?Nwc7MQ@Ncf$ zP*RIjTPyl8{3vkCy;btdTfKCfVfjyp@V}FItQZ^FsxiyWxABv$7F=$mi}8uU?{v{E zJCl{%)4#>lQ7)Fp?zxQzY&UZYxfM#Z^D4NB>Bl|pj-3L>t0-&nguGauVhH+fIrC>* z)gd?iuN>&d^)5ET`M6e6Qs9l?{vElcedOwCox?{YE~? zDEf2k^IS5WFI+#r%=l*BjvaP9b!C2)_@xs@^eO~f_+^6OsdLSF(zE+&kb7E3Jp#L0 z+Z3z_usQFDj3pHAT~v(l)}V-PBHwv7Vt(}f{EJv53Gwy6yf3d$(BLG=6WLS}D^ysT zzff70wtD?R5VFx>Ye7O$@Bi-96*u-f_4>uDib~z{X1=T)IMT`!qkH>B4gtaWu{3>} znxDn-C4G8zCaU_AZkdTOH!^3!qVwtUjdrUA)u=^tFBXU^&X2L;Z-LytFP@$iG4or? z>^z0-eA5%}b;nrz^1GC;1V&n|7v6Yd@%7~!sYn(JAp=fYzj_;d{d<65RW>bZDonXW zfpJ`KhAjIpi$%N~!GJGI#Ijz;iJT)BUEmI7INqf|2&HoL<~e$JXb)@|H1?DyX-%`B z@7AkvOux3Rl?!A5~^|H9o%Xx3PDz0iL`29ILe$GaxNT+ ze8yeWvV@t!#|@>Pt#*c-ydVa~9(uW>slgd_>B3eE0gk|LKKyGpuhmYwV+YmFzHP?@ zWl{|>=38E;XCfJp_2+BAF!r+DzJ5RZtv|R|T|Mv(vNvANFXA@cIt%Ik8V|2yqn!<(nW?jvdQM?#F!C1ACqi6xk~T#fv?aUe_=PSp?Di*IkZ^=H{BKT z1YkOzNJON3aB!IOgIC+_I`797TaC7m3|-i*JF2M37zL1&1I}#k+c^C??^v{$^1_m4 zZgg4#5QWy8a4RvT%tzC;i){zAH0(EG3Cf{R8PvZo=r(Qskm13&Xl&w$U>SgC4p~5F zMGAc2-V)z^5cE3AZMoTI7w&CUUEz8CC%p5g#j{9L&b+dX7%P;5%5-*}*>o^Q;r#8` z)0qa#>}9o{yzUMy;Qiw>XpVR#gWXB!-&&N$`-bp8%*h985lw(h3Q6{BjOmi%+kOr+ z5zy0`7|XK@5pF#*K_*&AvixRuxc&Sm1`hB(GcYLh9eO&&>zM-{U61tDx5MS(p_Bz# z&QkWWl_eSitdr6=qbl^>pLd!Ac3J~@%n_vL>i;)9?A$Hh_3HvAaqP%)rC zoX8GAK4Z|eOR!#4=vNNBy*gHkGhJ%+sKPK5%+2^?!ly*k5vo4S$5QH!RC4`IB4U{0 z8#M_H{S+A;6Lx8vWUGoQ*m0YBr=`ai`>5{nC_0ElITgobc6W#i+-I_|fta8n{twKI zphRmFE2=9ha7MymGz>L88Leqm@RVo38t9G4ZrN6_q~h4X!JwPNTp&s=l#(cLK2ejG z@e6_nVrmP9udgdqM8&1Ojyu!AJf43Wo-rSuUaHpB6?OUsn0-*xwH7mL$qHemVqo>K z!}mxh4flBV9{FIhL6q;WT8KA$sDKI`1nrjKhtv&K%Utk=Xm9<4OV_W&iG@|NIo2*# z5pY8x$x{!FDUspeQ*$XSBbh7qz3xai>ixG$%joS)PHpvIYWyPF#Nj=&x)|3T;0i%E z5la^e;wQq@D2`Esf^cp#cs^v`64kRqpfESEar!Q5>_j-3&mZBC-BPsV(3_CMn2g)y zTP^|U4yt*%Yw)?)^f3N}GB$fluPpfXDl`OxeC`y2v=-kaqqn zj;mCxgmy_x_aZfI?SqZYN$)FavspjLY=0kV_+@W+W>qDA^-;+E!CR+RCE&Woq*2o| zsZKs~AYAUmE23+g{iW3+tL8)Y#ay-dQPyiRe`p8!3{A)p0jKVTO{!fXRGxa5607t1 z{chRq98186xte{HT|IzRtSHx*Nk9lQ@agH3)Zjz&dF%6i>i8Hu{jFpgRZ5jL7Z0Q#vj26^OxC@J%m6~Ff-y?~G~>B-JVg?7Z= z{2 zYNgCDbeJ9(obv96#jF>@fNO_VCsADd4}(6ASfW3aB8AZ?^dcL@^l$O^hBxWJu&nBI z)qG*Xc;R*oo*u{tWTp%b|F4sI{INOfNasRYh%M$kT13u}g_*Ig$PyD0^a- zoD&rij6OE_R^YHI;GVSez701?pgr=V|2^RZ*mUGbDM##gIjQ7$+V*N2g2wpw|o_75JXseljvdgmASH_3B`tq4_hgoZ9~NEtUzgTWjhLzw!15^^<=Se~u@6W$z`dyL~Lp@^es;3#R=jDPSGCx_8)2+e(LpFvm zIU?dvQNmnW*mZbH!a#uo)7{dO|64A9m|i_9Yh~s0@k0E}+Ddf4tz`zclTdT_Eqjf@ z3taOR4|KZvKgA51!T+Vs}1vmbboCo{kG7EXASp9zKLA z!6&ySi38J>kmwn23&N~+J=M-8D4Mv%oq1MFcKNb?>~H4oQb=WSf2E4+Q5`dmD{Ol& z9r#r{y-OoycE@ z7ft$xKp{vlw)OetW}rWjmjW6VHD@@JT~kI*R>I4Rhaju2_w0csB9@X3r#RgYnX35K z`S2jWaMZnEALvBEyc!)sXe80GBhd$o`fOk?c6t4I- zy`2AZ{+Slf-=X4+J%K%dzPsIzue$#E{jEGPCo!V5n4`1jQn9I``1*M-UUWvERe%9A zFAt^0*Ap_1v^rEdy^t_vHDzJw>+fYC0kDOW7Kobs13pT8kTUS)2roH3Vxzu0D7=hs z;#+pyX%*i&gK+FGIQ#~e9h>hC;-1Q=?wM@4nKI0VKjblrqPjwG=SBIhl=m9Spx8=!XH=y56H`}k9_-a zqY^{c8un}MNDB*`BO_5EbCpHS%|W~vxB?Fj6WK!0?xz3GA$_1~5`!Xq=Pxi4Ag9+R708ku0aF%ANzd|6$ z{A??nuNi8*=h1k&!Bfzqvj@dZOE|9S9_f8|c(T<1f7_!BVA1U(sE7d~WDKV*!ZW}D{)vbo`w)}jMN+VqBEB9D!P zwC7!1qO_#I;zX^fi3GJK*!WK^z_J1ezU(O+<#LXF_Xy-tqhKd zXtN3na$LkAA>tR*BDX&Xm56QW?FtW##DS124@}kRC-A?hW z1iN9RGBB^YU-|g>R?#axO>~A>YP?TXsR3!+t=H2vvXQmI;flUWg}*GY`#HJ4kq9Hv z@gas&62vo;MyL;an9n7`Sj6E0KmR?834Ff0MdtFS>DIJ3EXUJf4w>CdsJ7~Cgj3iZ!sSn&Ev=MwKZ)|kEK1=sBInS_< zA-w2!+VUa15F!)cBPq3vPCzf)*-m?!DFc}POJd%cZ{E|Wz%UN*VKegE;dPg=G&$Lo zv=L~BcGxh)>#-rBSDN2Bhv&AM$bwj~BomzcYOIXNb-5mXc&XIt_j6~VpbTWLWlKk5 z!~yU7m}36X!SB@w5yHiS<$=*z0L%6BY2X>t0qs9i6oTF`gG|U1%Rf7PD_NWfP3J=G z`)9l^$&#dFLQjx}>oz)EUT(3UJ40d1t?;$d)MZq` zQ>(1-6HhUG40s@)&%8TU>bHSsTg&B!BeHer0La0)AQEkea7&Bb4r6-J!4<@cMRV5g zV>}#0TameM%4W+W7y@mFrUpG4z^$Gh_^z@jY|FWV{^&71Zt5x8u=-0hl$5K!_fqA; z+Ad_)2Gg2HgI2Tc`$~2muWdY^SAl6@b%M-lV;E>n>WTf$1l+^_A*{#c@M_*@(=MN5Gb*z{JQLg9Me87P{Yr zL~2`4|KQWak%&da)5L_1&T-2nd0c_lmr?u28v9cj zk?^RK5xTx#X}Egg2?W2EqSi`XWo{hXR7j3CMmTp@A&}ZHBgAKis%|1WkSG4jo^4Bt zeDa0|awrMPLCI6RP86e(GYjK7b$#GeRWtV$-(VH@1;Nqj4m3EwxZ3HS9 z_Hf{~eBXzEd%Fue6@cr1DfIr-^8vo=1@SZ+;ZXl(hgC5-k+Cum%R;X_KNsiP*q(~c z#OTt3k>CgHiiZmWtuY<#+c=)Me8HUIcc0v-wnSLdd0OB0scG0IPQv1&>jN%gWF zQZ417apn)4&3Sws1Fc|xr+YGK0H5vVPY9vk=;vcZ8`flhuc`!n}ndk|`J0sbR zL<~5oyYG40i5Y0|`z2840v<4`_};W*ilPIhZX*mM()RX90WWt<;HWARglL21#shN@ z08ni6?p5P^_k*MP$^3QJ$z(MHiOBzxH>Y(zGO6@C+79Qm?$Xap_Bof6Sy>_{C~5_U zzH#9I^hSXB;_VDvd}RNF4=hf~Bq+>*w4k<}SevlEpaC|c|GKR@>3%y@88{f`X3 z(a-y1JRSgi+g=h{K3;^eU?3KIneq@OD#&~)h|+5(#I>Zn++0VB?-s)>@gPMtpc{RZ zvYbrZZg64ljr>T_#FqWr`C3%kp_G@;+Vsjo(D0%_=*3%{iLx6YEnP|Ue4%)`^9e5M zkTGN@Qp~qLZg$rohp&mS&HOGgBPq1Fyeg5`4@%3*dVBcupScYCIE(ErRE~h_7fdNs z>9XJO{Bb$bpvV&$w|3`)W0!jm&*DF&_zin*39R&}q&<&ZtJ(Sm<=EyA#qm4x__fbo z<0Lg(mFBPaVvHHwNWQ4&;^`fzj`^r<&@YiJ7AZ~}KoVYzJ5q!2y@R)cH$Si4(FOZr zt)-P1TXmWpwF3{1CU^7G_xj$A6Ul>}=n;}c1V}*@4U^pzvmAb3lpjBS9G{+wdU|rt z&d#C}5=N^QyDgKFWeaWggPEt<-F-9u;)8+fXOxNp`Tw04^Ug|+mL*n%bc`a?7<`W2 zmZ8Z8pQ%e{W7`#cdrMp)bQDrGv>gOHOio3GuHS4gy77lkB24xAA!KAU-aOP_Tbm}1 zR5)m8h&qOl8>OwS?fC3$sQcp}2J`y0Qg^pnnE_xxtq^2R|%BUk|~(#q{mbp`yAv z!s=K7?l-EG{(2atMi{uLGsUB(Fvo1Lxe?pnx9HzH8QdA*cYd`%UiVMTVC(uah>!1qzhpTI=~1gSVvb5<%&lphaA4d0iv4C;S~Yf?5?U5H0U#|}CL+!35D zCGG$-gnN*np^HuXLyCtz3o9$pUnuWX{dWk8ij!C@R{bIQu{v3(>jyIR*Yf&OT&9Ixf!$ONyFA0`f zHEGQUX#15W>03-$HC%RG{WN7k4!75=$X40wlO3@@Lf!E{Wk3itrVf#VR;;XI)UUbr}-F5?{`y}5`$(GCERLUZI5C2gw>_hwoF zHpj-YEp1?eWRrFPTGoYv$rFRbKqX426w)Z3uv{q@v@@3~Gcqx>Zi|_Sf&2ILw(V_W zx%mOgVkJy^aY9r=g2(6e)%kqcg^1rM=(@sYBBKXQ@ZqW8`XD66?3kT7R8`_HiVbG+}z$yz^BtOC0`)w4!5qreuph) zRIc~%p=Wqd3RMo5Q8GY_dJt5oe+KFcfpvbb!BwZh-OH9|znOJ18d#BHrdH03DS{sq zapJCuCj#}2=~-YYNZ6Xfte!EONgq(f<>dk z$0q^)o1}I(5{yyDt?P>%>tCl+XJMqqxeVyPgj?N~RB)wGA{7YBn{@QTWWVX{m-J#Z z9zqScXRLcVGMP9gVx%R6q{Uco5C6V&Ha)X}Y^=>(HI);h?`mp4BVkYr9;2LPXmcU5 z&dl)mJOcU$3~bswQ2y5D(q?dwzEDc--ey)GU)qMO%>l!Q&N4LlFe&e(8-+3I46Dt9 zcS~fG(PdTRV*fmYd)1k_xpAD8pll&uKREK6%L%OAy8j+xI@7>~<|W8v27OUa$GZ~w z(Lwh1#p~-Raxd&aI#k9t89N%l=ynl7Vuut2U^KMk{*{=kxi|{Fmob$L8cBUjt*TN+ z_~4R8if|)5!A6GqwG@D*Z2XfIQ%*Hto3@>PGx+=>v`}qKI9D)R0fZ=64>}7{9g|XY z?Kf@RVXGLz6D$G>@j|qpOSGN@*%KUV6=nP+xNryuj2|Z!706%^Wew-UB!()H12#u# z;SOEDtR&zygZpDzqW)e~YWDt~n$Dv3C!vP|AuB78WWxao2qr+v+ ze3?7S9E3_-q^*Kga4{8Z9Bwznp;h7Mg58im4b^qYAXSwLzw=OHlb_|q!ESS-25!2Gq5ne@#9FAUM zi6JtN(1e5$lJqXIIz_zgAJmoB)$|K@_K=x*?x5csJ)xOMq!xOp74_LLuZpk=(mWIo z-$t@vKjfekWWz#+unQfeelzfwqHuM)hw3k*L@MC17=$J~N7TUY<|IgLPliflZ3hq3 z(tbcWaWSc}$xF*hCfd3n_LAds*v^`ihizD-nY;>0B8^TL%1~RZpfCSY+fOJFO#>V^ zxs(>x_>%D(M9Po=d8v|F*yef{s;ZZN+oJk(QFkUKE9R96%FfJKGuS(ZD zxk`_n_HI%?5Rj0|#&7{E(oTIpm7z7{&%J6gbl zf}96m*D*sVu)Wu0A_U%b_iH5NFJ6e(93}vpYra}G%z}(X56zfp+BW+@0v{tGJY8Ew ztyB1nD;B-Lv}I=LfqE)SfWEA`AGX^Di%=9vN}ljGJ*$k3MQ3z>z*RDU8iP+ERxn?g zMo2sy6lq66&(p(NVCV_VgX5K^cwnkd1KdPt^h~qJ>`xb>Z<4d9ggckGp8fjd?ogv38Wr+~nQ9 zt4OefPCCDv%C)_%*>@Y3@`nFP9Br@@QL``1oGeuB-wL@rDt-CcmWXHw2h8_q zio6qt30lL!Py`~U{YwEId(ukiIIhTRPVn+P<3tuK{$?Iuj*vIZ1u}8L-+U`4*cGV+k7y3vwP-k| zL;%rj34?gKb+IoYpMs{O-69-TEYr^_UzfvqbPmpWQFhAZ@MI7Zj*FE988sIQm1XoY zl>g0$*1SQrc@Y5f)h5fz!Ky>Q8J$Wrc0-p!Hm>N>rmqYhxF^0fQ#4rxp_43Z7(^{h zXsw3|2fLw%g(N}2M~!1S*v8{`j7HFjg>PNT&w`w`m4DfXqxs0wWO9QH5q)SjSBU7+ zaFfw(g92WzOxg$FZuoyV%O~2a?p;ZQJ9a7aMHgLrL7_7B3rIq}%>{pCtWFkpEqBpk zM1f43ZFizcI)nvbq7n9VZthF{LZmOaYlV59QEMhCLqQ3d@}Lj|sQ-#M6!{V~jfufj zlyUsO9yzr$j_t!ozjI;%u+O~Z>J zZFC&vfl)7X1I8zqj_qG2?uhYgWb(kl6Sx6xt<8)X3$l0+81(pLcuf65AF4*$`k!eh zoAtjgVkksU9~B-;EUp=7Z;MgQC>_t?U$bUaAuG;8_B(7p_!7 zW3Sk{SQs(j<+?@YXgAjN>HGWY%Xgji%aQZKf?vMulC{odx`zXOaA@d-h4fIMgqgL ztRFLj<_3Ui8A6e}si)FHLP8COhXQ*mY^$I!<`42ZroxYMbz3ACp{}vAeT8j*NP2(K zgjA*o1+5vvf{w&Ug4S>`Uit^QWf%L+kwp$c=E6sp+G! zXazpDTUyvn{&m7RZEc;ydlfo~&er_2AeM#qOXY>apZ#p3yrCdrI`EQVw_40my4SLF zda;6u<0f%iW_ec5)TtK^_6(x$MkkcmaM8N9GzBL(ph!Im@{vIHX78eKg#VHCySUg= z`DlhDWTS4Xxt#ZT|9-+vFEnw6X#obNDt}@ zIW58ewkQ7)Vf{o7K~4j3Czee?)+}X@%>Zx5CU)Kf? zw!ttTFTRrF<-xF<&l6$Zz?;%MMVd3AgL$R}(ww%|J#Idb-BBVAP~j7euKq=2ynj&m z=Bpb-__LMKM5kR2ajohBbACStfYFlQ=l&ArRB~dk}G?L@-5ffmQ zJu#NWt%o%r{jNNvFkX-}Ti)s1eXr7C)J~?8>87XV*i@zd&@IIjTIJWv7zSS3m#LT2 zY!dQsfW`uB`G;iW{s;;2Q&N!N-OCmV%MLpHy!4mYXYc4zl!?2hq3bp=>ml;+%ib4B zI3V|x@@~r$H-slW046-1sY^Sk@?Xojsm)-Z^Ge` zV?`HovH5roGzDUN^)_L*>G*1^Zd=~hge&1IJbbIFpE=i7yB=q^=PhyZmlo*K+?8)3 zb9sKJtFN(%(>(*wjHNgjX*)8F#sro()7rL-DQ?H@UltCo_=PCyc6nan4LD5L{m9Eb&OVLD+*6l9@H&W)r%1B2s3 zMm70{sSSk(x6ZgRz(gOK2~o?@90iqqq6o#wHPLyHfiuLIsS7$z;<>dw#ME#b<}Uy9 z6m3S9Y+=CH#7_a9XFumBv^WqyF^fmf&Sw%~^m>iPTGGO2GHuwmaY@3~UxQRGc`;C5 zCZ~86PNTo}3nz46OM%05#fmOUhhUMPB2#T_$1)JNM^BRQ*`& zp6U{08lVk$8-JJFxY&i^Jx4s7&^ehPhJ;D*=N{)i9_j}Kd;xVTui+oR8@^kqm*q>B zc9U>?L#kS{zFXsnSG7}@S2SO*qas-yIHrF3#SXzogpx+WmlF50W5bcmI3nXE^1(MY zUB|qr78}dF7CL9^=u+`acv%<$^Ym-(SVDt~MbI(smM}2E84#{kk3RKUiPmaP?jizM zEcjd8HDdwha_bvd1oyekCp$|PWkE1wmU)4`7*9}*A^$C6sJLd9G|-uG&~Tb@MV~F& zW}%%8mtvE|y$&gZ#HcwR;mDKYK+`Qgedpx}?6y~HJMP|xD$$Edd7QV{&G~Gr0fx;& z{G!%JWd2K8BWn*qK|>863*h0)L`+nl9~qSCK``H%bDR0i zU-_kw3eac+?-4fyl1K%{cz{AdmxFl&SyvF~Oa>AqXV9S%dCHqcCGHr)bXMd2gXKD^ z9_R5;KCD{*oPno&YE06Xj3pZ72&K>kM+5)>pL^cb=2-#70RWH`DT|``og5LvCjtip z_JiDYAn)&6G|zwC=Kl-2c4nsh-;nMiq2Yx@9z;fiu*I+!hsEySo>jqBM3%Del^El%4H%T6U?j3bTCx8=p`v%wwr9<4{X>Lc{rfw zYZ>?uxW=Ln>~D9HKQOrh*pyglr*pdG5n7L)8h?LpbDvy7FUP3};hMh-tx6aXdkYEu zAlppIgs7NY18-&SD%J%|IvN=b7`r*i04N{-E_KNhc`t0f?Q(K`d_@f)S-CX#SY(gR zCYjgZ$EN*wzC*)+onp=xS^W%vLA!tyK2(GjE#I-1`catmmF| zN)?Tsp54Ode^wtW4LNU&>NH0*O?%A922Cy8aFJVBwQPSnDdta z`seTpUXf2M?MWRu5@UHacwY)*A2jhK-U31kTC)BJ!YV=d#xFrh=z-evjrY+0{0j?R zveA?)!-@`wmCr|jUfy0T)yd4i)v8o2N!lG#Y9OGH`5;VnkrgAr*1R}>_bU%HvtKUH zu!y_sIq&@uF=M7)DSDslTnf{4wBc=^xG+^Mzy~aG`zqA7{>dh9njMqXchq%ic`g5S z6ZRPweStP0Ct}beR<5gOVZ?pZ=qfUG4SADu8uN`jTlq}17 z2DG)FmEPkNKEZ^ZZ63OhapoDA6B^6wzO*b@t!o=Gj>lckzl(8;48eW3Yu`zY9j#)7 zMSmYd^=#+KcGFBZko}bF5kw3T*8kPu^i`gy*&F{Y`i$pmG5%QhIi3b=3OP%*xvI(X z%c)lWDJ;Mwum-@LGqDk$1TXNw7T{q4Ey!GdgrEoUe{c;g=@Avn@679b9CZCjG_JMw zY1NAv=Y1bvb2RM?+~B0Ht?=ttYtzkSI$CNjK2YQ7xA{_p!KI0zJ-1z*W^*vVpYO{T z+lCc(cUc=#Y|{Wjeeb}Fv7GFFNp3Dwt@_isa@gMt=p7fE7Uex5-oE+*>@2a$tG+DN zerOxsn)jCilA%k79Fb>~VG9{QHyj-@98LbUGRIW6AlsO2R1m-4lWrT_9Bl8ra6uSLp zJhMG*BX~->|JBsW(F^t`iFZ|A8#P|(g*?TK@4f+tEbEo$O=7Wg(*y;)69jfYWy-K& z1)f0Q_qr&Ibr=yE1|ZazE}JWctl9MdzS_7sZ6A}62nbx+KAs?N(Z1QJTA2P3%X@kB z>HBUV--~}{y+}sRe31=>W2sj?VU!x~7Y-htd#6MG?f4J=?J>U$D4^DdYgPun*GHeE zc-EN8sUJ>%aKyeQ`Uv#zJOB7$;g>2*#JPEfn7o`~k7wMjWqipMIC+bl30q^1>^eBX z$r6YRk>f@2%ogv7$OGiEnEHDw};JdAL`c+Ufuy_V&D!Dy*7&hT1@B{T=K><+V?>f z)^t2OKf(yxg#sm{RyTYrCM;P=o5RA0wmsap)bw~9N(1o%_AS5I+o*RdI%9OF|M|w- zKTu{yK?2JWx?%TxcF@}R`vR-EJ8AVVH@BUU;C}<#AEKKvF#glH0Fe_OXU6LNeduwS z__li|)MM_m;IO{!na2!#gE8=d_r@-=FXT+l+unSE{f5N9_Zt}kWWhA|?uN*Sfr*#u zZSeBXz$kEL-fg7r-I*k(;m!5PaLAFpaqitvbAaeoF>>C8HQ0rjSKl2S%`wluiN*>{|ncfuZe#8AyDfouj`)>ru(HaKi$i5PJSyxvXcV;J&5j_-{*W7c-@Ky@!y!0$){1xyqH-1 z{M3kp%>8W%BQ_X*&zO$J6SlRddB| z;n70AHWYF0S0el3(2YAkL#DX;y+u2TMgb>`MRA2YpieDp%dsb*(GM?0SW7wD^GWs7 zF13l~hZ%$Qaj1*PdnBGJ$?;De?}cJ?56`C0%S*^a^OCtAyXV{R-E05 z7E+_Q%J1xVq3}+eftj}Px6c_)vidfkkyRswR z3~JVl&0)4V>we6=HbHTrD7SZP9!&g)n_`luH(Y@2xQ*0BCk+sOUw6{GPX&HbE60($ zMFfdrMk^HF%`1@oq89V}r}Tl`FSO9q$MqGxC%!htcCQ5(K7a#?JVs`^b2dnmouVoyL(9Ucos~IRn0VUQO_b0SYf?vnp?( zM-oq}qNVY_4^j})hdf50k_YZ+waE=bzmUV|&_2xz+dQt@j=7#4$iYud%xL=Nr|_>J zkZAI*sMc43*wtg;3m=3r&%haAVne}}N)~UJ3XiQ>SB*uU$hXs9^De!<7lN{kH!CEr z3T~iWAQ&yKf~TX~L(`&aXvWekx?H*3{YZIRH0&blB!aU6tGl9X6x=RDk=y zcQHiF%>tAuMn^(~kQ;^%!lmA*H}MYsY83;&h{0c@+8Nc(F0|y{`*Kjsn2o{~g!*Mq zc`b-E+l-P0#txT!TIDstd8OpQ#F|HCRYr|MNkfik%M(|%$oxjQmLU#3VcM>2SN_i* z{RJ=qmVHICG$zU+Hp8)&xjjE5EwO>+pDd(9*)@CRgEV0z|9bht|07p@8>~M2*ee`3Ms4~_?7~*oVb+Y<2L}DnF*5Gnrw5Z8J)Z>BV0|jaHze& zS#f;g>2sbCHAz3CnKQ{kj+Nxi2+AtX$l6KDI@Ry}AuqzS^mEM$eJ6&Fon(Qlbb?rU z1^=pwe@Lo69j|&u2&SgrF>Y7E+VNWzn5uWgV9Hd~f08m<YR;{TOz{9F3Tf%&lOS*z^g3!n0w`MD=B#-|RY znW3*p+Tj?W5_rR6H*X*qDV|b=BG*xz{PjC~wWw)k)LB3ssmr%P>Wqu6OsUOj=IB8M zTdN3Dx!5Gs;W`*`TE!S-^1nrvf;bG3P%-5@r?eKF$sBO;hY^Qgu`Y*oXxov8vY`=I zG~oY4z+Kf`GRnecZ`=j1uZ z*MId6$+KhY^!c2FuhbR##vT}RP7R2$9Rs=F=T35`VB^wbUMJ06h>ioD>0EI+-vuaq zrvRN*x!!Gfub&f@GLGZna^#|BDWv&T#La9QTP16l>RtI=#_SA_q)3yVIuKI~GZ0WL zfMg5`bj5f`-a&9C(s|qN)0`yi>w%70#LA!6rQ$`)oWyButA!XZxgYB1K00Ov3e;o~ zKG^XA&g8r}ztYp-H!Hj}`;_+EbJUI@+2L>gLz}$VvsT`AYI-2xoPKj?`lUX=2cZ0{)$)S87jPGFv`b&Yz^=N0j{b7saEzqByiv@Jd`7f@Lt)@J#t4F)!UH}Oq(rxG z_}oqmv+tR8fe;!r%7{aqe1%jEsvg$I5`1&`_kz7!e@9h-W&5+y};jQK&i0exld{^S|3oaVt$< zls0k-+xjMV-P32{saVRfQp_7DiElqhcd%?z>vXdf0-Asr1pkCTU9o5KR8h;@-Aq@P1 zD2h+p1pWKMRlJ^#1G*~3%p_GRiR?~LS`iB4@4TKsBa?YV zWm=XsSP8`~E+E~mXZHxV7__9}7DIjh_L0(oR*aHat-ShItfg`pOmU!~=h}{JvCu%@ zELM{x(@Ail#Tq}X=R8t6R>ghn58{2R zv11-b5-MTpfyK+1CW%kc&PdCMt?v0K*YvQms-FI0f9S`ZYMV~uEYtI4h3-$7R)&(R zeKv9yA_Sn(k7K)_niY}dx*Jw0^|R?p9-kS_pH47bG7t4*C)QK`^Gv6T78p#dGLi-^ zade}C6nfVWzFS<<0f>vyLi2iP1-25frx`mFP#X0O)Nz{u|Cxot`2P}u>UbhTV7Gpo zWT%CHh=kBmoCiVdJh6e4G}!rg?viwa@2m>`SSy01X|5c{QeNgfnIx*QsOW;>@DLCT)uxF(e#4d@@THv{L`Wl7^V@ zkfRTS04M5Y%^egJTJU_$_(YtT+mTcKFHa z8^Z>)iSxa~y*p`G5&km=|FpY`3+>5be{h}Q7jMdDMe?34U5=j7mH5 z0IT?CpEQ;psbeN3qjo+LdqS-q8`*~+O@w_-`UtMY^8G-*`S1+y6P=X6_+i5=4;C1RwfBmloB~|L)l#;-K-gR%1Tjr{@J*gv9 zI;}`tZ@?Bx-R19ci=y{J`A>X=nr-r=n37o8B!s`hZGZo@oRv&Ds4ej+C?ndsU+!? zxhmjt*sBps1gBJs@^*wM(u{t28lcwMPh9%mK2Dc6oyx2ph!}lc6cDr_sq|_?F;gur z${3oJk<$-e4inXI=LK*`8MvFl6SsXLwMVa>SIz6{B8Zoc5J3zA0>8V__9f4{l=3@g z`Qi;;g2QFSB_$aMi7KY2*Wl&9*E*Ax7uF9`vk(0?VS;WU>$idbL3$(yVj@se%qT2N z`@M$_0LjhWY2vqca=lg@tM)0=SK0`qwpn{q9Sr%glhu@+f~lzK5j`~15S+>nEjpqj zQmk`BhRE$fxZC+uo42?5#ry1p8QQ7j=5?oXkvQh>?@;{ZaYut?$3U;?COEwPjpN42sh+H^_qQV1wiN+Z=18z~tMlLh~J*y@9h zgN69o(<)?Di`HPr>x)pVot@2s`N_D=lFOIk^kgxkS7dc^st zhB;byR&Y1XQ3$e=-i#(2 zfxdqzS0I(}@j_SPsCRVugQlXEX(UFOK;8-%TaTegUP*KxGbGR)9vS~Jb`(@|Ldd|# zFn#2tv35c$WC|rH5hykMpx=og9F|hs zIJED8WiT!wPAUKw6OuV@6R9xzW9m@K)|iV$w=5Kmwl_aNHY+6pPGV>e*?ea9%*>uI z)|};+RA_Q6zI%+yT?8xBq`Z{p6YJkJ2CZ}=MP-jnNSN^Qo?jM#ai*A3SQuuRN|Kyu z5V3hGD!ktO>hEv;fy-2jLUh@ouYOmIkwgOU5Ct+luNz&?792g16r~U7o0qUc(B_=} zZYkz{TLr=zBzAZs6tMo|tASaHHey;a9xX{>Jwel!^_ zN9MyiFzy+bUb=jF@8Qx7b`nD*%~1^+gw?*f@;7!EQ{dGg^$J`LwKN{-moeLGp2fmf ziwu^tl%(E|r4ovTGDN->=aMnJYBucnIr-eWq)x!x11eZYEW* z9fh6^smcC@q^$${O59GK^vwj{Gxwo6$tmYO92dok@ee>*9Q;qwA=4dm1kJVrSheI( zM24z&N11bC4C&XX0|f>BM~>vmfpKZ98PDnuwT^&lJ%O-$;ibT27DR=SPXm`dthyb^ zKTb{E`RSj=PJ-q54ykeaewXA~hzU>XyMOr2@o0| zf@l1;#y2b<=08U9Wv0Eh=mi6V2L)>==7V=UhVDd-6p$+CkDE&Istt;3?xhK4eHV-jd5 z`*xy(=q)StQbQ10dUq7L+wpgX_WM}#W3z>c$xii$>iFadcs`a_lJ^$$?A#jM_k1T7 zaRr?FezxYmqLtgC*dE+2KUY&n!J(KL9I|fNf^uCQ=Ts{7nuzJwJAd>=>Af;=+pR^9 z;b{{LhFAV#jC1@Fk{KK=Jcj&G~zc|}#Q>Vzvl z&F}4;ot?!B$X~v>0_#pKcj@LfBgsOx`Ox!CcIkp{|5CCo2f}`8Hdx${Jv_b>n?d0v zM&le49*h}`A@om*NHGHCV8z8LSa=GQTUS%D_`n%a(g$Yx3CUK$D3guo5%s=D{6cNibJTnQqcXX z#7`)^NUp%LQ~iI0AIbRz1g6W2DcQJ@F5X`J@~X>lgp?Y(`9jV`)L2Mv#gf0CfJ3v zebZdd&U1NPWvJ?HoY>Lh;+a;xRbDeSt#SA%Xem@kKBy^<*O$IJfswrsEH-Yc3?Vlw zJEo^+LHEZLqS#F0Pu6-#tQx8W)47*`YYH1^?F*{lV5^U+`9Ks#0dCEZ$T> z8x;y$EyO*S7x`c^N2;gaD2Pwzu;K({p7W_lR*iDusOOEjW=G`~r_k)c5afy`NcvfU zSO}7Rt|ynuNs1O4a9GOQ(Ab<;R`jtz5+}E!{o}vP*`=iqzjH?fQ2J(-Sk1=4{WSeO zXScc6!;a1k4HSO8yr2P49|6fw(C@ZUER+~TQ~fVGCU8yN&ora^*i<~qy(bxWdCJni z5(8|268tlBOOaT#3O^B|=zp7$({ji0VF`Fn?O08?=vYVfhn>nbaO#Vh9kUkF7KtLd z<&CX})|V*c)n?~wrFPzMoCR(h{jYe_qK}DyLlHM@vmX@T-*6g7r`hnKlliu^LO8VU zQ6el#?Wf_oLZ*lxV!C=L6XT(AnY_Pdgu3x;KjPBemM3sf(r7+kNyqv^ykZ|DC5}4U z>=Q=tJ3N+$5>~+Vd>|Nke!2`i{|`l+fPAI`trOo$kuox`^AdPj|99G4gwk6WrMBi?IQ=&A5B90hPD#0!n$ z3)I7*vuOMK*ms9!M=QKM`uV^u9#=!n4y`Wx(?e_Tt1b$z#c(`Pvy&oa5RS&9z?uVe zK2>L6I4s#xwEd_tjP=fY-iJxKYa=@utX5+hgS-4t$Hwvxx9SzOcUzj%&)QedJa_rk z)u`z;Waas}%Co63MsIzLtkBx@!uvidcJ$~#c-;}Ns%G#>l@M4#_KVgaYlCj(;JG~6 zmXZ@shY5zErLQ!2cQ0KdsTGPoj-%=1QrS^&8=1#hull+*!qoL1sGIZ;gYpulTnywF zyuN@)Z46z8pm6^)15_?nYj#Rvq)w zD@B6nMuRF}N0l{Vi9-Bd7cZzv{$dKg?n%`?&y#KS*|txcNnXLHW0PV8TQ(3=@!d^O`fStd?{UDy&tU z*RzLg9Nqu2-Ab3iM?jSQ{;p^);~<|M!o?9j?P&CwxUWu;)snES{5bEnMUhouU?_uo zTTB}*+x^u>((A9Z)4bVbr1_5?=>M zJ6cOVu1kLCdfTflZS^L+AZBMXTLXG~%~+_YsF9aB@J?n4Bj3-HI^TIIwwZ7l5qI*| zy}CGOFoeXg6ax1gcy$jgjwJ_1kOW^R8V}EoByHuXB%_HTZ6PO@kAh+0ijGezAGz%} z;3bg6zt1|1X9&O&^uMHVNv%Tor`IGXNo4}3v#7GlufPzEgmt@{EW^y z?~7jFqpN{8Q()`~-ce^Wm&BYk=2X$`jt*Owie&}cjCCLFAbuxpcn^sHiC_bTB3wkmXIS^8$WLJK+pg2IOH5Wp|5T@9EZQNv*HSkLE^cs(qRZI$8=u7>@A6B6R+3))H;wPbR)jP!F z?kBLH(T;e-jZlDxL^1{{_$3`!ef7qBpEsY%>cLN#QA2(%k|oxc5Z01%S)o3)sjaVz zymO4EV{bRGyrM-c;EDbEYDYsEk6^jD)8MIfhyY0jR}ledOP5y#eMUrYA1-@lyL|jxGN;3t*>es`+~uG8{84Ydb1NE=TezLlaIr(eIXmI&{1yW=D^=|x z-uJo(wo#0~?CgG*0ydJ0%gQ2e(=vTYRF-^>yT9DN_#;_QJS1j3JhDV9VRW5It)a^g z@VQMoj3J3C-L;8SvAXK>dz2z}D%l<>=?XDA0_Z)RS+|@ zA-0Y8a|xooMugx^NHG)JyXhcF<+86&#o~wggATtf5?qh-Dx}MMbI#5J^rRve$fQf< zg}o{HIv6_jis91Wkt&E>VHPR&d?XYGmXnZ?#1+=)MkYD)aet{m?;)tp?fUXX&raC) z`ty5aYKujsMm+{Ii4U)D7vk8#RR73?z0u-TghNqOZ-?{_D>p6jM*>6M273Xmg4s9$ zS3$kEt$HRKn|b9iiZO-LC8a=nksEA5=AoI@KQV_7aEWA9zoG+a6%$i}Eq5gQ&YTSF zAHNiu4pH#)%)x_*vHW@z2AG`j&0wuMku+L!GM+{j~j?Q9ODf z0Mfvj0gj&zPzMs?75~m5t5^|NhU5v-D4%w2!@+HjmW`z!Df%J-y}e?B?4QZ(yV^-T zl+|+`{?cXgx&I?wX5wNmrcK7`X+%$~hrUK~HarpDmo$c#%a@-=^vn8lS76vr1n-lt zc$VDWAknWY<2>9BdZ5aUp^+-F4_{y%$1AN;kc zFbKscKR1g_qB2-=)V1Ivs|y7*uzot8RLdo*3X_NQ&XByPbl(4{KNg(wdAF|8EuLZ( zzvxt-@xqb-Fd*C>FDV$2(DKpp4J`8AXC{14AS&T?%JEO@~|B(@5o8hLkmQTI>2$fk~3TR0)+Z&Bpax)OIq(|5fwfHFEC>LKv zA4ivi7)OUDts1wFLMV_x+V_F-iWa17s>3du`Z`P{$W_gEz@lR|aBh{pCmMPPWkz&I z?xc_hMD0n;9Gy(5 zvs)j^6!ODdl~}e#I-+!Aq;`X=p{a%}+GFSZCQTCqE-adIj7&`(wAQ?`kq9)XLY_;$ zk`?R^4ziX!T5bN^?db@!-sVj6 zd91Kdf}(15ug+!}$@P3mmY-QmSds2Y^ts=@m=18O8<99`Fs)nQzPUxqCjHZV5w~t9 zsex`}E6c6v`}MBG>58n3X&Ngr5>kZwZpCNTQhR%b=rgmauGa>O-|4esTu`6;tSe)w z>3VBgCDo)k6_vX0^GHxyax^nA!`_xj-8E{Q3ZP3FeoVk?g7CZetND8fb)|NDNS(8q zE^2S_`nf`B+#!Ph$(R0MwuC{aKYi%2<>jV}cW1kJ)FYq6t```!d7X-}V+d_=EO>YK z@va|`{Uk3~vRYeiue^P3*nG%3mWQgVg+x_L_?H&t)ZJg$q5BZ%b1;Kgsb+u^NNJZ*n{^ViBQfk{a8{mquo zX&=G0o(1$B>0##nO?O@=Q_u^FrW*`*W)lS8|Ms2gJOWJTRaLeeulormiAAJl$BT6r zH{?iWLSI^X(uG0^I4s#$)hc(oyw3Ob)Zc?m*uQl5_z+CH{WH`|VmPs1t2tfkjQ6%4H*IwG=6G3s>Jc(;nT%u%*--1vfyYs! zDK;+d)r%7c-}h6vb~GKcwbZNA<+>dIPJwTpMjR2%r}+mDmVd{cmK&eMqjegji99h- zn=$>Y&-dce)s+Ql0Z_9K&CzV z%>91-F)<~H!)6wC99n{^ity{Hg_0@+Z+#giv&?tmO*1PT>5kSSmVXb%wtUY{VpRWr z)%(%rGlMB^(sw!AYBg~mnmCIjR;Y2=7KcU=Hbg`fWJEw6Nh*+yXXa*h6k4ItN;?6| zF_@s+ne{PZ5rg@(CrDAej6tX`pIIub>2#26jU&^WhE1%>^sEilW~m`79VBxI20nbYqC|W{M#KO>7Rf$-5SY;roQw?3F*N8F z?trS2VoZEbb)M3-(P&ONBly=MBQrByU45EgT3nr9zE6Pn67ul;wYktryReO zI#7CCJ(=;JH|0ci&A(^U;mICsN+X0g#=2cE;!QToa!@$D;@*_z0m_$;oH3?hb-5;o z65RWx7=??An{cPblyGZ9@@%yOt~f?7uHPp;7mobzQxtBxB2O-y_|?_;|>zM4Rb1x z(eI4MeebAbT5&xeAI_dws%pX7=?YXwxhVbX2ju4CQTEs#49&5}*wD9L?EtR;{29MeZxR7{LmN zETfoOvk9iO>==RS4*A__Vy4o1%J1I;QFBg>FI&9uIZ6ez)j#LkE|MlT*bQmh`+{w& z`pDk5oF{XHB-0k7^g5p}Kl@!BBqm~C9ddB+`nDxTJ+pAw?vlGQc}6KWYDRi)GA5F( z_03decsz~FsHkS`qhuY;T9bD_wRfv)ntXjpDJiRzsWW<%$aa76U0`~KsZguNwFT1i zL~wpISnjW5qCvfwXo+*Xnhk9y;CE$r8(_ic*_-ma3{&+6fF5lI=;AN^=Pv_Rng=l5q)_(=S|{+{*6%fNelAzPr={rP^Ug!$o8 zZ8q3Oz!_=cyVK+5ygEK_p}-E!eF-Nf?QA9TC-u+9L*=H0O7qw_xDHeG`c%moLowa= zvRaKc6Mi0s_}WcpCa)V-gOQ{|*(9L^a}`3SLp=2sV|t#E-Jzr&-0kY0_YVi0!N83) zUA~Tr{}sdpF#N~K0l~JliFDEd=lykuA~rydq3B{wPO(`A^RdZW8x5bJbcWEH_mYys zg#t#X=1}+d!%d2Cty`1HKJ?kfD2)p~9TuUXZPOFhZDs-yJCw84rtR%AtV*4>pukYH z@?V|STEcOM83V2S(|%l(iiwth!75g5c8j&nzogtr^C!(p?MD)-RBVQWBO_yb5L&W) zr;=#U8n&t`PDl5pEgGq?_!P{JhwCJhOw9TAyc?cVS4YSQ))0ry(zhgcmPMU5XVRo3 zobhboB&c?5+#eT&f)G@i<|ktNDX)|>0oMdMFJ3!1?d?$`=~C=W=%6Fjx`Or`5|zPh zp0yvT-!5kQkc6#P-&Vq^W}7=~(ShK3t35l%MOy#MBeF)55g$EcPAHjAfPYp-;K4K# zKGABFEM5*}mJB}U;pT|IC z4sT$s_$R>4Y$2WLZ8~-3>UXroh#U3yos0&{_3-znGkeFP{D2p+BtgKjrtJ+~9AOa~9l(@nmupvu=`Gg55#@B=o-nB6E9C zN&L3=?5DtxwX6N-4YzTG^Q;YJ2O>pAIaNx@NV@k>+c%!oom!7WzcULBDyPnlL=uUUc_3*9P5$r@BmaqLA{&CZ1qKNu=H1tvVmAL40dcMMzMAz+xe z4x{l*y%tH9R)~0_zVO#3M`^>vJsO7rw0Z|tj*=Lor}eYaa=8SB!KDsY>Q!#v{uY9J zc7q3Sq%&Hzt&+>W_=e9jGmAO;ez5%;+eADRBaq$*uDX($-F6{K0DbE$SiC+jupYma zq^vJ*kqItnAYj z5W{^=T_fQ(7)=1`m%(xweQ>{3+F0?1_*2ndml&sC4c6#y_x*0P%47fel}B!I=Vc?X zgZ%=QA1$l*bi}x|#hj=;I>+pC39-RO9c1c4P;!@L+P5Mv_`dmV!~0dW35e_gjY^xF zpO`c{qPYXJbIrSjiMt%qw|oTZjrRoGnocHD^LD@{nse!BC&C)TJwjgYo3#X|>&>Ab zWxxKeHI~Z8k=X8)_?ya`(M}sl>v6|ftqpz8Wi`8YfSrRv!=0T1O4|z#wGELUk#Uvc z7Bl(23dyTcB9ArDMOGW$uAo+~XBdsAI-YKLB=&T!DEoS2MKqn3JU@6Hm4$)m`!fg8 za3K2OU1Y+bFwy^9zwly2_c0(w_H;;Je(DBRc2KS~TS)o}ip{q656G63Ru9uAv0MHr z3adQDKbXqe8BIbO*N_dD4YydQB;>k)E#+O&; zVe4iyx?kWNHJ=&;mm zf-FBm8omhDpk}2X0Lr?41=k^2DWhGPRaq|Nqe`%)nAoC9P3f!L&sQw%&!ssf9^B`P zpX_6+WbLm#BCE_7I|w1Cl-BX!eE5Y;sTi8%j2~R@jUCf&?M>&a3asFoV@0L*GfX+|9@&kW^t!zamZ%9h+@hoO}`R`qvrfnmfr8r9d#C}NJ=5CoXbhyB} zNHvf!APo#8^&*Vg zu$w8T!WGl_r)OIv5<8jq@lkds>WE^GUYD&_jEBu<(pM(7>F_(-`9CY>5ik@nOQ z!>pBJ6TY_R)8V8{Dr+;Vbwh!$8B^S0DF`pjQ@SiYV#|c_c{_gcjGl#_VyLV4F`|gL z)BR)LI1OVB2Z{Esp}yewvMin>B~9x<7QNkk8f8UdI^zMaJGXSi!r6!;n&aDx=JjfH ziAO!<_$)FppGQD?n9iIkUb1&kuAm+CxtxwAJO67c{%_(#fk_ozghaWT3~L86tsJ>z zdh3riNg#l4bwwg3(w?PHH19xJgg*iYy{s{5;T-tm6s&NUw&;}GaFBM{BTZb_X+Ucd z2V;ysYZ8@wAK6XgD76}Fqz}R*GPs#jg{z2&nxL7${4?F~B(+PCq{p>64(_ZV)Bw;h z)|s)avQ6Y2GB8>Xk1+#z zK35mNs<@;e+^)0{>y$U zTVP_sw#?DTQL!w6ztOZ2?`SRH+9|VNqiIk9GM-9?J&GFVEb|L&4F7{oUxm{FOR^ND`^)3=bwybPS0Augzmb_LEF1_J>&at7tbQt{2$XGO{U9manIGS;=N@ ztE_UN5YK<5GAML8N$J`{^bKjU|I5Qy^TNUfSM+9NAp86n8Kyt2DX1(IO6jBGGt%%B zBN(wkc(39?;~{(`9v|c&z-`%x3IWyDtL`LHyQuGV(y|GDni`p@GiTiDM%L9|P!&T* zUyOaK$!B*4XT-(>KRJ_yd0BQ(fackn%2oq=NB^9=TmRmaM6ESV;)k%IY*Cqz#@$^= zXQXgvC_M~wq=Q&9>9P%X((`x{|h zpBqi)B&yxP zz9xQQTgf@DJ*mlxVkUJO4hj`h$K$x4e?`e|O=9CUnX9AaH82}vOw>xp>3h_fCvU~R zJd2+A>rVm$3$a1TQ&f7azeXB`xcrxjL$PMA-4!oO#>48+()e$`n2oo@BriKEl>r47 z-eP(FX!akO;4WV=MHWt0Dkr2Ra;8XxzbBu(wA;&Cqkbm*AGZvHGFB4-`o|6pTHuq7 zjiktJrGey73I8ZY7+cu^Bu>1}$Z9xE7}Q9j5K-0)XrTzox6lZ*>RdBscPvR!M~ZQEqJc&Ne&n7K`ZI8%tnEXm?A|#l|lKj98W=Ml0YPS;C5uG#`Dvw z%u~F<{u5H~uUchn+j5bzso=XyV~+p`rLqq@D9nN4=~eic=a{C3{U)rRU&niUnjx9bDO|1tOST6A$gU13kWXVv6%rzf zQws-|CrI3TlP%<)lHk0YZ3q)>Wpj=2}0h^C(7ON-wir2_}ZNHkMgN_jUG!%Yk$ zoI03&b}_JS@1SO{s09yWxAo2b&ZN@jg$-8%<2;bTPbQU9gf zr^Un`u5#66$p8zK(^H+KBHXI-ekbC+;LkivuyK*h)zr%r{K{V)W&-1N)ZwWqZcXYI zQ<8#PCc`ax(-29QpYS>McIGyVU!UVT^2mS7yje4@0dK(1;<6z@lV!0?*cAK}rkYUm zYaQH6F`ZGPKR`xYY9q}SN9aY@W5}ONYNSL>X`uGIs{Uj8Q*>sxGV|HhY}M383fW!| zME;*^(+%J+2Q}AKiDhSrK8oY!f}s-uq~cPCmc@^t8f||&`l2+X=91fHcaw&O<1!D* z?+5r2Q7R6_5EDxwL?eWl#x>dET;t?_-skx0%9QtkwK$<>o4tvw9#cu>qoZI}EHhhW zDV=#J%60{IszDsVos+jr!y{O6fIy8I*4;O`^bzfe;4jkz~cH1TNsm z32(Lc9CgvaZT7lt=+c5G>@Y>1)d0|xS3UNygFP5SbjCF?$aMS-D z*ckGiDoTJZrW@RqlSf#t?ekP2b}@N!4tp|WT0#Oc-)>u;f)+2rd9>g#22EU7I+K;H z3}eIlDt13~Gc#UAh1nKXoz#lCBVdlfxtD5mTE|E7AM+CEbLx0{&#dQXhq)yo8^5}8 z5uL<>UsMv1L>}rW9iaD4>1{+z$&J5p2|)Eh0{FzC0h`Y>h86}gyfrnZH8q-azt5QezM`|VRF1GlTDm8JlU9l(?&DrO$AZW=; zK6GLn_y;e~79b!JsitsT>YVEbm(!B9_J_7HF(Gomt=|a*#JudLVitta&K)0&?IPiC zLC>xT2DRR&f;(~ItQZBcikG5?^MPMIY!de3WQxMHZZ-KS;vP)T9=Du=5rY16ts%j& zL%=FmfkYGl>IVk3q(v$Kb>zv5D<~i7Je*L8>9J5Uuf`0ENK`p9MJFZIel8_?q0}PA z67KK=9a@QD1T-dHq@;nd{i84bIqcR}fipUi(9AeoRmnXn@(VOG?za|B)c@P2XL)czK!^R>|D!G1Ckcy7 zH)uqRRRd#P;q$q{6XvnPG9{-@?3^Z@MTNh$Eo!{?-t`6=0ChRI-+FTK(gKjLV-_Sgwd@1sz82Ep^)V zkOal7Gz%ULJvKp;&^Wsk@EBW6~a|K?yj6)EXP{+ zm}|*3-pV2A{)Fep_UBf|iP~4ufi3dic9@lqm$+dUl0RRF{I1ru%*b6v`?UM63`xF12?CRIDj(fY)Ji`72`{FE< zaLQVGgs7ckRHL1!x!@kp#~08$a&!Jj$z(4D%M_Vk(D< zu>N1Tw_5J&Zhr6&11kKhwq?CiuQF z+7fR3Kf2y3xUFaj77du0IR=@TDaM%DF*7qWQxc<0vdqkunVI95IcBzFW@dgmRqxh) zRqvcyKhpk?RBLO^?wOwM>9V}4CmZSXHRy-_OEr?3mA_O)bj8W2Q{A2_{OO(|CB*{r zM$Q)BBLn+3NZQN4_()>`J$8#el`&F5MdFnkC}29Oe7w65iLL#Wgg^DKN)#8Z7(m_# zZ8I02?cg(YK<#0{TA5XgVc_bmt4ewqIkE7dQxZE=PtUu!+`A?Q^WRrs3g(bK;F8zR zE$0g-;;${m$^V3bI~#wqM8&Ml4V;}u; zd+{_T>59D)4msz7*2YUTNwazX#~zYPh*Dm(OHmoJ#;Q2YEHs32(O?x!l|-u0f6O=X zrKZBl=!^E(?^{+k^%+5$e`h)gT96{A!gA{HfPu#LEGFzh{TLqL#aX8#-K-pQiwacl6|_3%mBe1a6!Z0qWr{BYZ=(Euj% zN5`@8#7JhdkD1rg3KlS|ctw^ih1Z@Did4+b$yXVJF|GNmA*!X&X{&Y7EtYr*noLLU z_bJXX45Yjt``$Bfq%^s)s%gA$c3ooL-B0`T%4#YF3vPvP>vqb&U}xSg?_>M8&M1oL zTvYUU+!!vXU6&U88s+YMv07Kch9T1N^e5U;=J(~|yT6^y%K2FZI<(?GvC|N}0F}0* zzmw(gR5I zCRq9Sz5|@H#J64u>YBgn=l3%>S0aXc;S4Xit5rYOJ!v3<|110~#NDr}yK)OK{uuf6 zm9SVw{o9c8N-XzT+JYTDDtil_p(4Z51mQ?H9vvFqy+WPnU0{{LEU6B1EMq{>I59MhA-yye=W@32nl7;&6-)WkLk9 zZb}BQRYlt*8!2I%bcx}RVRc}Sat6$^gR1Ou4+$Y{*@yhLghwXabcxG1xj$~&JS{cV za<-qCcwd=lo)+8soqvNKa}LBRW11nWIY?r2S|7=|K^p-`Mn3F?PJ69+irI zp1C&rqNSBuzZfC{YjD@H=(@=?TCG2jye9pHYMuj}#< zK`}t#pA>yD`KO(y+Ej8D_uG`9ZL8N9qDd=~*+iL_j>-4X%t_Q=x0q{P3bm>@C`AE> znqG<$83WVmE67{Q3$1$Ho#r@Ty4NCTaogNCV6q};omg#~=Y&;@U#$O(h=Xn~C0qt& zS@AP%eHcTZD0@{+r&>fG&E>6KqhsE{Q8SIukVtqLGouLV6Day5j7&SZmA(W_K!;V0 zzm0;EX*5J|Nl4qd{)}k4s}zjk95B21JL$1MNAnl|xJ2`7o6DQpb~M#T;DOwIS1yCT zs3`pWH1zP6>xp}J>P3q7e*1<4Pw;o)uWi1+i%ZO&BpWLT*AEhg17e@RT)0b zZRFI407b%u?x?WAqy3a_r1__1qOsosYh-0}?WjCH;fksPvoSE|^6!;zOE>%$Z0NB_ zHI+HoL^;(;w?xKAPov*i^Rux zmi;!%2tMyvR^m0_t1}SE^n-6VIRhmaToO5VuoxCKMXvHRgVg$iuaK_?+)Y@q40-~P%XPwbj+pdYHR#&!G_+Dl7wPFp_VCt&%)q(N2QC2G9s(cim$ z3>#j#A*v4PtlvX9jOl%G;&wAD+@Hf1I(HF(FMuD%}zqnOH(*3v+lV z`NCvZlO>4?nJMsv0D<``vlFV_#gsNiadZhe1sot0-mt@gqxv=BT|d@f3hlQK`vhRA zNQs5G9*so5u3ySzr@m zfZEl>$GAUSqwT(GXG{0p;^1+RBg!&-?lwCZ>ugETymyo?ySY6T^I)JbwD=X(h8Bxx zgUz(?9obOm=a9$kkKzb5kMk#rtjshgIH^!%WD>rSoR0Djh`F;-DyttLz ze^Q~xiv6~XGrdT?we+<>g*nyy8~G$r+>a+=l%P)~qhBvKKt&Hq$i2YrRC@a!`A)s4 zi1L+kS|e34z-2N>XcJcazk;x?F$5?M+THkxJ)8A9hDs9l>%!(UwQ>NU<{c-n0>Yx1 z-<7A=V%UVXM%08fdjw+w6dJnsQDo!sG>ZTG%fO0|*2+oSmo`Fi$jd*As&bpprk2_s zLofF0(|vkLpAL#?Q?>gwc0G3zT`eJeIFlsJ$ZP42V}a(N(a;sy1o~J;Bg3A_Lv7Sk zpU0zL96^BHhHdT`W@CBW2VMrWmPRk1#&XzNF5rr6G+mJym!WpP-GM|pF+@sr zBRrMxjmTFXW=F%eD44AuP@1K`RjFS;roSDgx%Q?*NDo`G zvcUmZUUrYwS5tJzLnDq#4Y*_rs0}$DPNjaO|9&W!$@%@);OylsOoMPrc;A?YxmFBC ztu_iK(b|+=f_Git7B1RbFE`HXfgz>2ac9W@FZu2E$yY(^SWdGQ9@^*zBMyWKK_1JO@3rU-O6w1*LR)N5otM8 z8i}MhU$How{(O|PBa*AyqY_<>5}Wd{C(Z!rk*aT)TF_6Jm6r};Q~$%6Q*(97p>7$b zknE_QC4LASaBI4@C1x=;T(2!hSW2r^iKdRMmZmT#?Azy@gfzn|SyfMuezo#aRB>YF zolbrmqCP93F$R`Zgv)7|aBFA2HAxw4idrnsV<^{wVkF6lQ5!bXU+i5-AWcjd%JMhQv~C%##d4{6V_OWSU`;00 z@fnzg26nBwHT{#{JiXZYnn4aKRHiaihSksELFkwW|8=z>!f<5B%$~{W4(4@x9e*fS zGDYM1URCt@;%5fbLt+K4YRJ)2S33-Y7Efj0q3$n6 zEqJW`Jr1YBmSg1JH>cQDaf7=|$sm7Q8Y_rH*36fuiGrOFSajf>5SNV_JdH&>r7I;H zxiGwS(RS>D4SCk!Nw{8|Lr3y)WN}x$;C`i}r zu!)j?jKWiXpnrbKGS`mj^UYUirou$06Fl0r4J5wC^$8qTAaruf!BQSza=El4kF_ae zKxvcQuC1$G%5YLEvjZJ^9USf_JNx%dIr@o(%$ea(x&1g^N)xBzncDu!z~4`PB#Og9kmv@~o=?c5LGV>5i(x4L($!;pdBU#xL1h6+*OeHRO@X7P z%6YOXDq3xPVQ~phQIUB#C10;0!pPsdkPiHkfvT(}7I{0Sgoy7^k=hv}3uMh*e2(IP zOZknNUU-!MdI}IQTapmW8Nk*hd?9Fc40@_b`X{vCpMfh35Xz0$0LHQ(i}43>{=8ae zA`E4KjecrJik+;|z@sC5px^hn%!*$m3p7~NNaTncO?@QlOITk-(GuxgMX+!Yh7zqs z>B@G@5OJ)ZMILKAaA=xbwN=4sG`E}i+11{5<^i<#mDm-xLeIlX-zo?6mTHdTdnvGS zfxmJJGKcjQaO)9mKiZytH((YO$*tB)baLEf9CII&!M22eXW`nm;2=vD?_ap7e2B4t zSk1O_0&83N-oYs|F2dsdXt&)sLR;ADZR7BAWet-ovL}^>vedPFSqH4oKyR`BZ<3cZ zr%N(+mm$9BS3bGFMg;*XTm8Rci*N=3>wo*=W+Z^xtQ_B#e2@m3V`LH^Ad&U*KjvBc z3%9CJw<^mn;YapGxr^cX2q(?`=U-*k+vx~B^wnpVr|~3a0XwNJ8;4y*Ak<~WTp1ju zGjGZKjl1A8vxI`ZG$yTVql6%a$KDc+oRzTX`(>f;p??%V%SUM7t72A8@ zT%wy*gxZLjS41qS=rS2J9m8L#TEX%tgGs=$+UDld7CGCqHhKk${6w+8v=jl4b@Ri)X11iMrhNx*;sLX}es4<^YpW>n!v7j8a8L>pr{7Z32#v#p556va-u z88&EW!`(B)m*>{$RQ!m)t3Ryn@n)E>_6;Xf!k1;ChRN0+*J2hgU#&%0hrG}QC4*_9 z&s7i$aLyaBf<}Q7+HkE82n*eg-2Iu^*H_EsLX26ln=M2USP6lIIPAG?ms*oCL4RHrS$*x zzfw;-$~dq@{`qICcCp68ho66)*m=M8$tNV|mfLLVV3I8K#~l7{p}8;L>qq zwxN`p9#SpLD#{F(NGdrMHNZWNI5yvG8`r2s8J&*I3!8^92pbbnb$t5j`9?y=l!Kk=k|7ozjYbFEJWL_j- zx#@Vw>^?S^TqH+%zG~-T82g4~I$W)IqRwej1q)I3YYIU^_alpJa~qj?CV>5W7*R47 z4wYbVgaxJtHKK^*YB|lWZsGWEP0a)fxjY8h&9b|+zHS9mdWvp^kxCNW`bdGK5u?%Q zbOuA(Ef@;*5DcfqrV!K!2@V->iD}tZrX~1JA-}9JUc9eLGeBXvBR1l2U*VBmP4AO& zmlQi-_@znyxG9TJ#02hCcc(63h5;?72ssG9nJZkgp40QpR*i~f;c}|CmUGypCvi0(|E*dS9v3|o8wS5Ap zfG;7XrsrP5;F7t^9!o+@i(`8~b^s)Fcb-mPJ$ey~z*l4`4{ zwdb<4dHCh_yiVmavMKWVtt?+f2%)SnrY zSg*0$lIjlPTC+Y5K(U=I0Ad(9cYlMr{yo~tW(yeCTl}0i% zc+dUSyi@xPAB2n&mWyL-N871`$c^^G9)4d;iZXtSZhB9g0|3jtFaZzX=F7}(bUAd)03oy1kvOPxY!R0+;D zXutS7YlFk4vb;(2_p{AWItBbd#TK*e1e_XYx@mD2- z$7$d3e>J|zc_4?r7u{3CAVMTa94X=cQ?RM~z`#_i{>Yvf=DILOj$h}II!f@dALsj!_^FmMd{ z{%hUwpFB|15(}srCZ(ET)^PSW7+4@ZQ{X1OI)YrGR~wS`jFzCVyogoUs1Ofnk&~VF z2x$*PvDgZs-?5VB>vZBg4{C{&sc%xq5&X$=y7Jw?1BlC*Qs=iH7(dJvhOc!Q6P5vB z1wh#&kHB4s`S;^SlixKqH^g7Tjl@mgTle={fU5fPiS&O9-pwtQa>SZ5UC~JS|E!QP zZK>K&^I`p1;C{%bqMB9ekPjMA=mt@IiImmQy4VsRK3K2kEqLao)X|eSX^Wh`l2_N2 zov&OQDorR_qz8^6EbU5TrI!#oX^2m7LAkshId{hV_~FVI!Lw|^P|VM8|7aNbYdl~_ z=j{ubD3tVXM{<52HF+G*kNN)ln2yW4{c7|UZ$_*9JzZmL5lopX>`M+#?TTP{y` zgnbXPUQ=ZrnEUeR7wf;8`MemueWh-2sUr1Lz=T2ppVL&qKoqrbwC_|~kQheoI)79| zS~1yCC50?c*3)ZMCjWg6wCwi$5RM!gEbhZ(LWz7(ipXE0!v;CDRL^%c6k?ue@3zdf zXY`A$j>kw54E*jNGVT&PrmYP}?3pezq^6=#bi-FsFN3QZg71w^&L+p7Xky6*f1zu# zxetlnReERK=A7&$>J=J%)+nujrjr_O`rcUrI6pI>y@gkH8EBXbfb7`#$i+fR9-uZ^ z{3txVzjmvbNR?Zs7bT)?$E@)j;1!G~I%i70cEoTKH-kCcW__Mei6uB`hX>G8oS&|2>{SStLm567 zFOR8)SY~dwWHS}$$$PkvFfvQg?C8*751c`8_- z(J;f)WSR2n5MOFUNw z*eva2r74R=Xc=RaWAWmWMkz~Bk)d)8@~JU55!i|crc+WD!&BF*B;c}zdzDhKx@{X* zw|~U6JifvUPb;f@HX`3r7A#(mm*xGG z4VIWrs`#qfe=s~V{Ixr6h8D!`zl@E;%lL0(YJ8SPZ7jeGQyN4K93G;a zqf}9tde-U7aV`)vzWxe7)NaQB5`G}}(_|TlBt{`viUMf{jfLH=$k0e|V+@hF-*PPh zRMhBGIeM1WaXd+NxapB?EFhCW>O9NV?{(r%eN4Ns6BQ4Hqp~iWd8pA=q`GM_I)*mND?zty&7}eMp2CdxpHoD(gPrmBs`IaZvO`mtJ8$RAKVkK=@ibhRU z83>$Zc#zHQ%HEyw4@HMR)t&#vOAfq1%2jhe4g*lx@H7Klsusfb zN`ZFY1qCCtjQo=7bzZ$qCq6U19=@Az)55os*g)*4+Tdgb2GZUH!x;ZppABtgoX!Dy7xpTdQ z>4<9tKc3DZ-C;>VU}W1^zWlpQD(tqbnnd80SuE*#e=F*H*=(pP_GGmAjL&ZrnplFq z>70OYHNYrT_eeW{R_MP%lv$D7aJ~#;$;wds9@m>!@ceM793;j89=x6m2>li;V6tMP zl=F3@suOsPfYfMPr8hUmBc<#l%4Ei9WVpA#HKJeLoUJ=mkZx)klA|JBQLri8Ju-^G*@tQnM|^dF6)q zf{9v-pSeipvg`Wet^Y)}@MQ0#uo4q#fq1y-e@2x<*6_WJ=TR7x9CkbJg^dR%%rF|s z9j7{#$)NM+CEsH!OX-CsiL1MM)6}d8-~Xv4HrOjJ#4?wZRhjnJ@h;T`(DqsiOUo*S z6m`GgSIzQKMGKn3wkouF!EO6_eL1TrG}UDGayI8lg&f0_47;W25C z9mU`uh}4j|WoIgS_U){02CHi&ZY^opF9-hm7)<+Jdp6ADn1>_4+YPTvAjLtWGL6rN z$gJJ!dj0v_ZDVF~i`s6uip653tGD6E2S3=?i`r-R6C+-=`S!A+H>~8C&k-Tqjjghx z2W-@aUQeyrp!XZ73#tV8xbJDdrRCHi+otG6>hn+dsllly?{m2J{gm$4+k>8=OEVW1 zts2)UtbEudb#)EbhcU@L!7Dm`!+51E0hg;W;q{E}RH1MZDHO6lnc>BZE6a>Uv`Z}} z#AMYgV@XG4&;}_%IPe7tbn4cBT{&Xp*gy%2l|G*`y_|2++5Tj-XrxpwyGS6VFN&l# zhVRCh8O=_#dESJdc>zd-sZal2-H)eZEUrZg{q8+iIRnUPCz9P-zsf;*uUr(uInUz|`ENyui81d6tvjOC76 zJoVVgEAEoIztJ>&xkTqvUHP?@|0%mYh))`nW4`{(SX!2xuBSqev5g)74YfYkbE)$5 zLz)7Woc_tM(+hvVo&Hvg`HssFG4F&q@k#Dv%osT318s z48JiRYuG$_cwmzPWK@rxNw;0miFkXk(@yC3DSeIPlb61ZuV4VULrOZturi>k#GiAthojw;q@K?=4`^}*S} zPmqgvBOkeczfv)LSpqG#RqM}*y1x*4dy#^*oz4YqZx9Be3Hy&MGKOMtuMVzz`Q1!J zSYJ;*Ph>H}^6DupBzgmAtK*$c`hZV=O1eOtTSGVFf95E`_oAbmK#C5rOS*Rw_Eb+1`K9T2~bxZ>a=ci{DQ$~ zEB|*ibwhf};wQT5u`Fp=$vpH65JgntTEm%UK5(u(Y5g+?8)ZGOYjm>cu6U{Ruq(lqeB-q|r}u|m7pJd?j6 z6bfEGG=8@n(_;hGqQ{ucgq<=OwMi}|w^fO2enY02 z^p5~{>ed84wo{pYX|>y-TK79z>f*xZFE#I1!AXPbjXrEC8i`wGgC5bF{p? z0+n!I5q$M0$bz~?C@~wh#ownSsU|S|n_4pP4(idLZlu}p-h`dV;?{1r#0`2%8m?IG zh-z)Gc0M2Kx{)C>+iZJnIN=NxUXxpFaYPLG+FYP`ZMw%|IMFJf0>p3J&I>2qJIS6q zS!o8nQA$lWiPI|Ol;(S=u#|7SQKzgDID{)1o9671Xg5bF!cUs*@ocZN7nCry_f5>~ z`o9vhey?>ymQP{Io0;_+nwT&dT^3sPy4Pko)9HCrOF0y|4782?V*cvqHeEtGcy@wCb+X$mA33u?0HPQ;!H{4S?X ztAM2p!v#7|su|@uZICuFR#9X{sCv}?sK=jlJ^Euq10t5rh{uXQl+P3l_yK#3Uu!jK zh=o|HVOf^0nf{-u6BT5tMk@cU8OBY~c06{K`|cw&7Z^P9Af{V;6CZOYECMGBKJ{Ub zooGr>;n8YLXCu>s;yheLsyl>owk?`iIJXJab@)*TgnmAs9Z7{-^0zoVIsbd0Z^PH$ zEW!`^lWxWz{e?50{&PmHbC1|Wj<7Tb1+_b11W|U5uzh@8icp&BDPV*Hc~0Q%Rpf~V zs!KYU`IF0(wqvbG4MpdB1=OQZwU0*QR*0xN1AjB#q}u_zEswI$GcHfieRsXt0;$yd z)W+X>KlpTMWs?<}effk;0l!F}^G-<*4b@f^M0%E{kQP<_K`Oi{?zuC2$@~&`ZbSyl zWT^btVry;YK-+ri>%u=72V4u!NNQHrt;IJpsx{l)15Ir1q4D9}*6ME2VowJ;pgMS9 zXEupyFgD9#y@NXXXd{QB)3yY20-oq)c@xxPz{W-(yH8D871}^wHO2 z?<;+wc%J?m_;B5-58(_;>EkFXajhd{vm^p-tbv+*cz~{Q7j8)*QkBo?=3W+h^i_#QdpC^ebO4p3y)-a^5M0de*_62VJN$dNn-q(Vm*OrM z@iX83u$*h8o@u|^!^N6np)Cq&peM?yKx5uXoxG>)(N|%F{8;84y7egmH(Z>s*`wicykrO(v7^2}xL!BPZN&ir6 zJuQZ*f7hJwmq*9v7wgh~z+K*gjRSi3p-Hoi$X$gTH&!!9%piND%rYimU+H4LI&k7$kK+MkuX~g6C+_GVZ!WUR^`Uslm56=-(!I4KB6hKe z=t>8ST}qc?>q@fCz3|8cGjZzcU-4+d?-eI1{X~WNFG%e?)wHa%BgCvhIh;kf-mR6X zToy>2XIZkl{bpl++bI?ARAS|KUYGkD=~BmEHoz zI!JN)*$1g+tZ$y~`b9Jlj|9wwnJlIptmel#5DQx_(fT=$BQUq$?7zPmy=TPgXxN%~ z!9JJ|I(nW84vfi~Exq&*ceI)cg}zE%+&+gT7`UY3?^kcCGLMn4N2k|uc;-~^tp~te^{I7BXS*eHlXWO~IJzz0l0qFX1eTipSgQ{E`R9m90M{mMtNr1w-q*jX?J-Na_{c;p``Lz&{gWzvu3XKGXxf9ku0X=s|{yUOHGBO`fwZ-vUK&c(WbefwD;rkg)X(uU$o{N+=M79gnVNHqG_{zV=LcZHu#+n*eJUn zd7}xIa~#MzhendfIBjPY>;-!Kt~Ve}G%@tlA<>C6Se-3(abr-a5F<<|N2}eUOt#Nk zc_Hf;)6HWM%JoWtO5Z#4(^cF|Ay2riX{X!~C>vPde~bPevMCOCahY#)eVMF$z;BN= z95T2rB%Q`(Pw=_T9iq{`M|G+j__9q_@3zk(S;e!ePMC{Ai4`!YL?e6$A5A2Z=GA@u z2^ztQr$@Cw$sGD2_8kSlJ{vWke@n&mYxUqFr%th)o^3kE^)OyTBhkZtd2P(sY?K)a z-s-sh?n{TK`?KFyj~iN=*#!`xZwuL-!%pk*e_luc5NYIsOo>OIuIChy&AzFb%*JwA z$c*`5sLHIKnxrWN1GrnO>Hi$`3R~~n=Y3d|;%(P!)AjU{K!wJZ!zHTbP$osdvi&063+F#zH4@~y>{_Fr5J9=b&LNwIbK@H^5e!Li@ln!#(fQXiMA7&S^cGo8=Bnd*8cFnoBe{-?h>JX$?jY*BsL zFvM&!dWEWxJ(UufPRV501Y}I2@36+C9$&c-$w?btHF}Tt0NQswt=9LUPe8rbQ5#O) z$+Ox#cayyKFvnd9zt-_4z}=ax)KOO~X(&2It-riwemhz0_?1_1ZaUc|)X})pa0Lw7 z`>9f)TOWpbzdCxj4X=H*hh97UhbIG^k%<9sj){7=;C$c}Dfk4U&%$7N9tu;xFFZ88@xqycggku88C`2e>umf^4|jVq zcD3=E87V`wMmqs0?nr!<7ujpb$Q5K-N^Z(gQROAuuOfmxS#o_M`WW*=N|Z7e^Dlxm z%>Vl~7fkV&04PyPo3xc~-sm9TO|ORV{tjHvsal&@oMN__;~1;Mwc_*ah_mVz$f#QT zdR;B@Dy@_~Ry>n?IF>1#rGYH++jAt1$L<3y$jIa|z7uMA!Cv$JVQ*a|8R|G*Z@MHW zCqGisR4}sKFu*WT(x_IUOtoAEd`XnnWpjQZ@kJpTPQT5dTf1V&lc3kgV`}p}p5Vn- zu$uZKnP^zGH6^|0LJ@?HoKivEJnEbsU{54D$a59mtR>n2^){Q_>?=(;S#OI8Creir zq#Vw0>KvY_x5Q9hmPmKso_?%vKp5oryAx#8tjsG>mn*cE+;TvdETZ!mipba!kgbL< zp02hE5`J65T5eLdnr_gn(DjUa&C%ra*khwnAnfxy?;M1}f)a*N0MP>sX4@FbQ!uL6Cr_az<7ui zBktHd5$)}amIB?T{i7_wOLGyx6kt8zl+L1`<$YTkl#orhTm63a4yma4l^5JKF=mtC z^BS5HH^-E4c@HwXKMN?7;IvMjPF5(O$FC+S2pGgWS*#U*O-xBKDw?*BmM~3A8l_YY z$j}L1zFM7pGn&X*3{OyC_#}%!zH)ISI)eGqb~&_b z0bv$=&9QWT8*xeQ(v!MOg6V!Z^#W>WT{QQUpn@+U8FJt0fpz`n@uXlAquJ4;Kw4?XwY z%9@VSZ`EWks7_42z`0}LqRETm$Fl{intPwfVAr)*$IXk=<7jyJWrERLaxXp>A4KsFmPT7CF0fhX@fiDz z9!3})Y%P%esa7!H!+3jiq0i(YmAH=jUeH)==ZcMHn=bQGEU}7?ZPv5G=HZ~FtM4cy z(|QA_DuJ{hsGUtOr9)vMyZ2zV5gmEMK-wa7yg56oXTI{7o+q8%WsA!2rTsm8)lxC- z@izR?eZ$0)w5>0ZB4DX|hk1fl3?=88Z+GyDp3ghv5Co!p-*TBq)?`H z2^ELp__Vo@_q#s#b^lx3rWGB*+eTPz`1S1q<)-Pjjn|u#weSRdmcK#YmG8c+Dr|UG z(hCHnH|7|cT$J$N*yEaA{t@8_T=rJV;_oj%C(8?Xz7yoP;;Zg_q+#~E+x*F|w)8M; z2c5|9m0M6&ldD2)&tb*Vr#I1p2ap){E*Iaq~qz7+N+n0WE-(1#=4-Ewa+TY`QNSm<&z z#*c>6=W~%Kt#eaPrsDm`!3+;FS-b$Bta!yQVN1Jt&pl_dROT=2q^W=5A8QIdcU_*6 z*b?GSI}D%tzj5{dhFLGnPcca{4OnOzc^Ic~sKz~G(gzEryrKBK^%tG^OKqJvGb3AR zR{PFIDQJTCtCM`bCv={c(xmCUAo^ZuY$V#HhS$%++AZl5r{R~sSD$U>sZ!?b@M~TB z!*TRms5F*j;+H>wy(=3_xk^huB*e<?_jO7(8QzhF!QfhW2$}wWkGk- zgjOU%7=AvkExc#dr!XFH86zoAW(*~5>-kt+!cE`}9Eg~IsNb8&mw!gX$rPDneY-%e zbFK5S{=@fn^F-WmBpB_3KK66Vt>bE>oJ7z&M4JOKxfpLIvTudJzAB|93y(J<>Zwv* zzU!@TCi;XUGIE5IN$=HezDiJ{ql`NNf@V3}63ArGmZ+G)^Q{hc!*`GMkh@8U{?KGO z8K=hfR`>0+0DCM=D0rCCFRsC?Vxu8ZlFWZ7X;gBAXijv5H4SPhHWzG15-9<7oG`;s zBvO|Wb0-$~HE6|>>lQVU$@hqP-gU2s7G!Hmxb*Vx96t^W{`3BFDdJlCkAof@=zj`? z0^tT)SKYpKA7!0aD&flTLIf3T2u>XCAGH0uw6@w`(li`)Xx=}}V2w1!ow@Ci| zqo8mh=dp-?#;aACOdIt4_)eZL$lw$~Y#45x1WC0pxbY-SCFwG}r+0JSuCZ^<=Aw?w ziO5(D##YGQNC(Ft`?=CSmObj8mHt0PSo9@BmqKLC!ms`Y*IQ39fnKm_AIG75e6`Rc z%;g_fL!VvTx9yELsIAV5bKp5v-~+k{4cecm`A@I&12Ml(F@RN@ z@mv9RYP2Erp=FYj79mNeKQ@QNm`hQ%tVkDyL}!8W)Y|z|^EZbJS%Y0XSqa3z;y!n>{QH0jOsS3{EI!=mdC^x$RGyc`q3DZO`e`3l7$9(>`uL zGaf87qboCerT8$S;KZa&<-tbpD_ zg{17V^=p1?cDAV*8jdw7Ldk-`(L)Y%o1X3e(d8hlVq5u+kR+HRq-uX9g>EX2-76>7M3W6@v$<*pP~U zT-Du|H2ae|X7it~`Jj&vyXI3`R#uQs0o=uzUsQ>VC5P{r%$^7rawft41c~ud`nUu~ zdVB_v=|XZ$UnZ9nC}V7$`IwHCb+^{4TXIk-iw#;D<9APNHq=+>{`l(s&+fYR6f2f& zMW&}nUcIkuvHty2=W{H?uASv%u`cUuZCbnQgfcdVMT3XNQ%=6k{p9x;=9;(F<};3Z z-{C?KIj7}ho_M(+12XGnPdxfkg9Emro8Hxfneue2A1Q7UHPo3Z40#{2EidYVZKYV~WYWwwu@augB0s&792VLIHn?r2>i_K@QNjoVGrh&9yas#m$ zLr=}?d(%Z?>G$Vy3um3$wKlYz#R}#F2j2)Fva&Y(8N#|_O%|i{?)qg4rNjT|vFuLN z9bIIi9K^&WJ)JJyUwQuj;PpR2wj~_XI$9#%jv#w$!lPdiYm|K!WGAOjTZ+}6G?{S> zw!t&}$B##^wUcX6Y&esz`gsh+_UEuuo)?PD(7kgvX@;_q_dJARJDmuaN>LtVeKZnMVu8#-)?qIFtEw<)6P2+W;{iH^? zw(*uJ{isIkAW{yCdm=NWV&p3g-`5R2#~%-jlWanN%ZD9+akeZ9n+rR zkfH%4CaQB)LqA*6WTc3mRR%eQ748VW*srvhS9|vL6E6$e5q(cHv1Z;LcA=!G^B9dR zvH|KOp+zYQ7^--KN79uh6k_Uh9r{{(n1q6)wYE!FZBb^!9f0>^`oEnnvln^L&FLUN zz$PaC)GSZMplsauv4IUNJA@gmwq*OLYELhCgd2pLt`b|rh}>chvM|;~tJ3-8m{~(O zNE0KYt7bW#Dlz1)-Rb{Div@q1NQ;d*#oU)HeZR2~sP=7Ul>`!dG@OpAr_=b#mrOhu zjgQSqNtItZa=iYBoJq3gOL*B0o$LZNH@)zp@mCkDCUFls>=`zMR@uh5h*pa~3bdA% z7%pZZnFK0jHPKYyvuy%4Q$g%MupJkxtbDdj0CSxX>^c`&EJ$Vu;kP<+sK-uxi^AAg z(ee-ga8KKa^vY~mc4gtkk3IfjAy4v!iQ`a1dmKT>|MUW+ zebdd)j8KbV?(c`)bxxJw_%=PJ8C1NoOCS{iqf>`uETBQq#~#C=&lnnlNs&aAoK?82 z*xLvQIb~>MFaI8kg=f-CB(ND5Zy%|MMJvg3ifgQYMR7-}=e(TwP0NPU)s*BcU+04Z#Ijh{K?S*&Ka^?uIedwtt{AB)$~ZJVNkLFolGS=6 zJB6$r_+v%ZWZb4A&r~+1D+R1241mhnnmScz6VIz4<)n%W$&BLd^EXv?9a1nIn|x-i z31EsRgycsAhVxI)l)mF*PwEFDk)+zFx}*C7_JJHh!_A)B7ch~ zguELKOxMD9Mbi!1fW9&tK+=hUV^ux@TrbSMszm^RIY^ktw;)k4yjQji%Gn$(Rt!uP z0IPtY3In))d?k-a{wY!&i`blrN(fN99IrDm@P`cfBe-O?XkC!l(;N*OFqN@?1`Bl- z(ncH2ftKk1{!VONcXn2HjS%dnN*-E-4y~GJ+vH+kFJ!5vyncn++eH3m1k`$-SOYGv z$DW^7{osDCP+-~NTe9}j^8Mk_X6a*QLN>k z298!AK>s4MC|l>T@C_+SP8N}nVI}|dw~OG--(GXt@{$TVi+u>XPLnOOg=ZMxL#4SY zb7Yvtu^LIrPe&aID!tw&YNK%TX{p`+hpxAbZfl8_26F?BIc7##W@cuYDQ0G7W{TM| z%Mdd&GgD$_$IMJIGt=nKtoN-oGxz<}A8D!2>E6{)Y7lp~fq)u$GOxh=f*# z57$@&(oJ|1Wz5`&uCZlF+4+06)%6NCU%1N%FE7G^sXwPkEn@!=Xvzd)4G+juNHy-X#E1?#Vf;Q!F>GW;!ZoxRC8fTlIz=h6-Ik^o zTMg2nk5tp_h98*@}m0vtg#cFQ2bPo;zStjdsRb?G5BfvqBw6RD$6 zT;+Q_=4lnR7}A ziji_xcpo_ef@rC}oXmwojR>li`|0?}(u`$RI`ZBgflhPGoHmEDRwhZV6&7B|So>GF+K%7q7GW%&zJdtBcoQSpn zD#qtyUn@xncYG94Np|3-M@~-srk%sAI*=wQw+{n|OmYv978xba-Nq*cz$s!$pY%tz zgb{)TB@Nb;wL&^t>LRd*-bd^`DIS?aEaF?@QNpT(S#%0N-lFX&)sh8wT%vUaiBfY| z+sIW{e}Bwt_s%Ja-*o`Pu@fcfD)nc|st4ZR>SkFbIq7X&eiK81C*6OgH-h07@fZP} z!zSvo^Owe0H8f@Rb=6)PkWT^wKjZQm#`4{Ona-OfU_GVBp28$5^yFr}mpN{g)$101 zEwO~oYBZer0c!U(1O1GaiMIA9Y4ezz#exmyCRp7=08{F1o}XL%apq{mBgsA{QsLwg zx%^7xmRx>10wG~0CoV0)qC44&)s5=V3I66Y$Ia`I+2%T=5l^q_*!`AD+sXX5zY;&F z(ejh-9Pyi9ols^YNUvmNQZ-q9MCClOrEdT!IqtMSiG1MnpY3W>nk`VwlhTIuxd<&< z^$N>Vh&NKe3MHE94n;zAH@-5ic@>_6l=7lqbLe=oZ8AX}_stL|M6*4ZpwU$slwphu zG^uk=NNIvjl}nN;V(D@yZD2{9^p(!jvPGnShOfd^YQ)ji4(A{p4d%M3V?HU6Ta}q3 zAxFFP>DqHm5;}kw#P^bEcgJm^HIsW%_K(We?gZ;3NK(nhvu!s8*h-tF%URV$#F3w) z4YLC2B_N50o`BRDa9IWFN{QM&QQdG-AGeIAxmP_zcGZaY`xz*4E>!lSE@e1$)eX2p znn%Gl3|~tRY2d|alA49~EB-0S1e)|qF2nVhH32q6%;I7J=;<~!uXLEhe|rYp5(>hD z*H8%cg9{8%InWlR6d%Pf(KwPcLWkP&voOPH+47?4Pi?%?ZB0c1mD>sC}^e?hVgL+nB46N-$x{WOM7WH{-vZp#D&X?5w!vY(8J4! zE-Rho{}k%bO;?fwu3OjV`VsvX7QHwS&8bBL(^Qew!eOc;k(5gS=jEu9?}t$9_|ZhW z%*&e>X4lG$m(-n+6x{Zwc;fp`3{O#k>)lc>LS4P(I6op3!mpKn!K7_Fl%3^EqgBa8 zmc%I?sW4IzDG9@Z1wGXf1AEsgJwj7Qj+FYjpy_i)8u7EXaScX#8N#|FGxtNdc~_;K zqTt5Qx2Wt8kM{ij+nwMdva*~uTjIow3hP@O7lc>~I1FJWM&-szCH*fFuh&P-kOj5q zXh{`4djV8`b_kV6sg=8Q>-?WBn9r(moRzE-7|@GyD9ofS2$Y9cc{Ih>AAsGAC~4_f zjPMkTov(KNYt6v+s2o%cC=2FFB$g;(62W`2r7Ng~7tvdr{|7>qhbjXisp@l*i%0va z?wkQCG0uut)m7F@Xw@;gCc!efNsNOA;$ux>%@tiGZkBc@at9sPkss4FS6Q3M^_ z%v*kZ?_Y@00+>Pzuw^6J39s zY1ioo_NoO#Brt}H!Cyj0@*HglwsQK4lueU{GanYPt>sMhw{#_JedaR7`F2z{u-nfL zbhAr8fN6jc-hWa{J$WemKXF=QQR%Nr_ z{u5!@Ap{Ia0byK^_{5ClaPjp!v3>5VtRvlhzrWb;bbb6oMz6Bj7pXCOz@(xxq%1s# z0*+Zs^HL;IobU)pUhb9gz1xO6`gcP1nWFWKmU2+FfbaHT72l{bD#IiDfr@-vdEj;9 z2!q0m>N8#Q9Qm7#yzq z$wp0M0GhS6c!YD>>_G+j;OFuWGNpy?w>cL~m_4euDwrZS^!%?=>gBSCPgirv7P8Rf zrf+3Wz7-r*UiZ+hUIkK1ctG#P_Gy>jEcS}dbAfv7Ke2ojspUSXam94Wq4&0>{YA^! z>_M~~YEa)}W$8M-b^P{H>|jS>O*g=1?ih{v^J z$%bPr$;DDQ$R8EHBk=h9i#mk%)-B|Bw(!(0iL;0>uH=m`OQ}FBHj1edtHQqE-eu*G zjQ#Z?rpX;u^D>#F{dLi&)F@RXff?EiJPf7I#v(+lERxY&!BSlgZWAOHq^)c+c2JNLX?yvQJ>xyn5#m&kUt1l zRaK8!(9t%XiW@CSlbwxSg&T$cg~r)F-s63QGGHoGStMO~UHQ+hVm?U~3|TM5&5N%6Z@3ZK1W7@F#w<@YBcc zSLZ;S_=ObL;lWUVkX|}F1r)K;c{PghhgB~Vp}m|geOox>77z$Pbwp7r2TMII>p*X< zYaLI6onpXw|6lDO$l|8KeMv8gCTX0~>_BX_z+qa83E=`eJNA6)s95juZ7Io8o?0Bh zxeDmew{wZ77i0Cgeh?}5vC)ZMZ=EM+KyHnCS^q0unyXbfC_AcBb#ucf zW&yOFJiq-EbjJ2EJ5#b_#$E6J`q-RyuDf}8n0ax?f686|-q{M{=bP-@AO45gTnStn z+L#}*h!hoG>08Gd*dCLvu0N;8vu;OwcFpc{^%wq&Lx$qGU!8$M#^h+{z&9jxu%3l3 zx$W1!BL9Sc;I@+nAv;phN^TN@6Z4-{}{16ZP zoDCklX2&UmIaCRL@PPZ->f_zyyt&0^JZM%6=%e9ekW~GpT$cW>%f*597z}#lk`G3qHswy6w7t<2h4<&TjtvtdHN!+o7b9&O{U!5iMYO}oe<zs0=7tq+nqK%}?v< zf@Ur06hf{zMz-}bXZ zV0tzCFAVUnwmK_JLel@STpo%Q{O)0m$kQY{u-b(>OTi#9Z#I*k+U{%|($E+iV&+D3 zs2q7ag%FP!$gLHg<9!aTaEkG0AdB6k{j2c4W5Q@=aqoKEZ8MOOF5i)0HwD^x=Tv@n z3DHO4;^Ym$7BaFbHY)6!(e z`nE6S`C+|aw1D#eTmSu^i+9%u?B_C~$0~1onDXw2c~`3=7u+tMT8?GI`NZh!@DH_I zNRoq_6Dmsga@!=Fei8T(L{0=jf5!*tCETZ{v$Uj5_rz{pe#=3ll(QS&c&b&{k`wnE zXZZ_O$?Tcf%48epyAaK~_vJ;pvUxxXRSAR&`lq%OUr`rRjO(-5-7?9(n>#rG%j_D&z6(1e8TH_iozRg{M7DBCQu_xXx-0RM%2g zHh<8GmAo;HmH*`WPhf1NAUB5#uY1|)d1F*^d#m7)_soCpssKi!e-yne@(${^bY$SA zF&2d+``c<0Lc>9iz?E;+pC`WS6F@v@DInRWOZIyeLi{(XBm;I)gX5Cu)tSMP9oXR7 z&>E=tbL%bZ3%Fb-5ah&1ZyXU#l?c>=aFU_fo9eaUZ_F@3EPiZJ=FXC#1zj3*ts`(c zIY2A)+T!+8Ru{_WBkgoQQsQm>hO-nzmL9~8caz0+CgCyS#f9!}{7m0^zI1&4Mt^hp zwq+LsHN97}g|^=#F7;CFcPg?-?{}0{A4a_j>|fIz$Q2aUl@HJ&3Y^JB&w0W$ ze$7w*DY;imdN1CEj^DO+RPWmE%DYT^ZVdnmcvM@jmgohqF5a@f3Me4>Lwxo0wf~oQ zV;MGB1S&;v{4{9fBGA46zA;+;jibcEz5Yu{7~b_3pD#gzrPZvbJt85mj0I18ns+evnMy;&rVNW1`|*5&AW-f* zS?v_c9L*Up{Be=y?-*W`eQ<&G2>0|i!1~IV)p;}e)mw*R^KE)}Ej1cGLF7UoGJZso zXP5Z`uN|GsMNdMs0t6>&{9^7>m8{Bu39&l+bK`s92F*{;!3w!!u=ESeb$QP^iPD*? z#WPH%9=WE@j5^*KD)h|YAs5N_zJ<;BU_0}hT~}lnrO4Xo`8uM4&`IO11}QyI^bFMNR}f4V&oTRmd3cq~Y&pLsKXQRU!7c_|5B>$u@)^&rDIL`< zd(cW`h<+)_%rhOoykUZVNbcP;9Mo7cr@J#};(sd6sqx7!QB!3H>74OJUCssi#+ZI{ z3wmulTz)eH{%UAaOv)N006krb{SHyzu?Hv45yD47u_UL>dTUnu=`Q#}xvT!21{Htx#A zY~-h`yE1s@65VG#s`GfAG_Cs;ni~ddsN6|Yuy~6+!ycEd&D|vpGMaXAkPVY$2|R>_ zqG6(hkytF`+mBbov~4Lx0x){8b~N>CqIFu>~{_iVdB)L~SUvm`3{ zil(8S^BAFk8N7^ryAsI`T|lisq30`j&ehl7qEQjmY+};mswr=Q94%>Av-G2VonhsS zw;fpW1{-Xh9I-6MvS>pEBDWSI&)$L|0 zC9vYWZ^3y7|FFuP);^9UxjKPDRv4x$Kmc zN?$;GT38sLx4|K`$zdi}6_h~osHleAL9=Wr(NuY#&XM=IWS}+6ds_FlbBK;SeDJ7w5I(~8}xbNuzt}4d5-1*bwQ(N@P_q!Qv zDd30|o=?#sSBjpfjxK27R#K=C+4fh2is?}}K+)7vr(9A6;qrh4O}9R`f=LeOcbE|P zCiLI+`=Orn&my=_s4+i2F*CGME@;!$7jyPD9OT#!Z$$nNG)wUnuZ$WO1^g4e$3Zrj zEitZ&N~S8pUGl>sThg3mj#4{Cjt=Ge7B}tiEgxVCVLOaJPZ%BS-^A>28p2mWNa}|) zTA!Kt%a`!WTqQKeJkY-qy1__w3D3SUtfXLorP~cWOx$$oRBWzj`1^06>kB}e^H6*6 z&I>Fz`q@R5$4reDLZWmXCn(|RsF_L`*h(S!p`>4CpM?9sjub8E?&>DAQE=V)JNjr+ zicKY8>ipX8(+F$n;~aYpVmU>Zw);e-Akn!Pp!V$=x-Uy{M3RFKQNa{&Itx{#X#LZ( zZZ@+OfY)J`xoHziz$Jptx0^zIq>uVAkP^N_<{DF0Gij{UXhTY-cxDkui8`IOY1FEa z(!%bD{?dcJYR8tUb4N?e=;8}0RrOLg8YA%_^OyLolc6Eq*X;5|XZMQ%vEIzIG=C zx-~GvN!2YuBb~Jk&jTe+8mY*tl6K(ZBCb<7OJO6J_oKO%f8wC`Hbzxc_`1}7n zpILV|4>D1b&iuS5E6ccVkgB5Y4Yxq!x4BHyMN%6|f<^LP-E~Qsa76IP%LS#%x*ALS z0EvX9dvkOM6YA#>p(^X`P}{Oo4Ww4Iru;Hw4S<7vU59r(SK=m7x$<>+RT|L24n9P5 zF{Y1L$A)aToyQYyPq8P30*J19)HVC6t>j-BgFb^qJLT*6DGyrv_%Tsn*TF4)qkJrb zWDYt#TS8(Kpt!z7zNjSF-{+ju@@Ok8fj}4a<%xkh#1l|_IHuw&t%>Flfg5msm!)V<1O8mUOn-5qTiDQqS9Z#`LALo_$ zhydmSHIiXigi?_oP3c0i^3k9qmNBN_MP`!(&md{{Y>4DdPL=;!x^_CvM`=ke!Q)=0 z^4GDS_~>97ZxG+JFF>B7M)>F2czC&BGN5*Vc%G4pA)S)3eL`i4mVE?dI=p2|#kZ}1 zMWfytu>d4{Y%2K;6JtW4`3GcMTBTe(-XYtiHLa*u-A=Xe+HS^5nSM)xP*${BAVUTG zTq4Iuq%rD+-qie0OH=mjdosx<319h4^OnfatS-#|fx*e5&u!QjugwmE*sFjB)@KGC-I$QJbe= z$tcu8X}D4@82~CJ2KE)2jl!Q$dZ{@y-WuO&a=gnVJ7HnM_qQOR4d(j=22B&D;%|#2 zZyO;l%kR2`WCTvIA-xfjaunpPZFZKpwzgi@qAeWAl#U#uH{oX7iRl$MM?BMRjabFg z1HZk4nb<@r;$0YYsxk~IW#`@rD08=U?Ru2+=lL01ng=9-UMw<<)B=(d$vFAO?Tby= zlh-Bpq%PQnf4*5Ht%^m?tGT2&jkbWLoYUP=?_D5D3T)+0duK!hRW3tyw4gwUUamWMYd)TF=})sm8Ds4*xyR(jVjcd8JR!CW2!#Yt?ntp0VM(v zHWPE9IuTjVII}@3Y6xL*Ax+Iw-?0;9JD5;uWrx^GBbFAM#{BXw(B;*4UtwZQ_I zQXNWTC?&(cREls)>D09#4vmeGkduFrx-$~B3scxKjXjijriX8r};MLN@E^hY8`aQ1B(kpw50J;?b-leWn`qKcsQiMxZ&}C zFtLl`A^?S<+Xcm11$d<+A1@qK$vdC!_EPNy#SE{y3rrKM+&p0!Zv2h$i8s;Ko@9Vx>ssMLD% zxoUd3g({JxnPK*e^)Fts^+1@oHA{gzaAl*$WtIK6Ba!gZC*LJ8!3ffPa`E$`3pbVN zYnPc`y4&GO)ha`Zq*BvLz$Pq3=AND&T0H{vOkv4W*4QE@MxrNcujS?FlE zsU}-qBEJVz9v6$rktA9!#|J91P-Nrd3EPwP#>hH&#m9MpQfsf;#sL%Cv_u>WE)jXP zv=CG;m^VaL~)I$@FTG#-C9Rs_!Xjr%t9EVy8b zb8~YS6c-z(qm)M|s!|+Bosv^LI0ukAhZ&=fPEagYa95VmfK>*$(8~9R?R^r$o{)iL z^qJ?cYyQRR&33ZK{E69u_x2f8REmX5s}9jmg}c4~I{$e^*U_2;~NW>O59goIguQOSKYHT^zLGS&IFvoh5h+wHZS;76R zq%{_vRhQpBS*`N1y!$D(fPE6kj@AQhs(uTS(5y2e^u9Y$OfN1#CAAEO<8`D{f0F4N zjc?eEj0u5mWc&cXPoXScyOLO4moUYXS--4m$5HD|IV`Cj5+LelWUzIR5s(kVx%#0Rsh+H?xKe8Zezt_r~*yhc0$A zkOD&E!@B+oz51A|s}a2E-$Kk`1!f$EJ;9E$c@O*QDvKf8sTQy!I26=jhL=3mYmsVhic8%;I-*mN)FVB`YQr4m;H~`DS zm*%4M zhQ%7(ooMcwm@8xQ4%)xJZVU@*PCQ)hr0}}KkSAAPwEI6JJ(^E$h7(qA#A2EQas{D= zSGVR{sUzM$KFxQKE~ObnnJDWI^+n@(u6ouqw4)jg7NpQt9hJV0L#>es77{e)G!El| z8{8;7p5TTC3}k}EaWHzZ#Znz%Q#z2HJ|tt-rOAG6WMCYhnx6i0+pw!Q1)yib$Ib$n ziYj6;*c95I^#7q&sdY8#|0W?8ua)Z(AD@zgLA&nQ483Gr$`G#3uuS6!IDJ;8lrNHU zLsqQ?==mIaA`@~6i_rP`UPdd4;m0K{1V3Ky&vd)%0}LZGS2(W*dcOS?^kJJBR3Kv| z57KzVU4NM&UaS|)_R!g$@uOc%V^KDeS>wV0H-hpg$H1+~OL9&R-xL^g)T{Lp$?2~Y zvNj73{6sW85eIKD4yBuEZ&BnTyYjQzuZ!o19;E zx|}aZWBb1ld{KulMN9>TlynDx^U^j7^#3KS=3$!spoA3!@UM!cdZ~_CFwhXb7gZoP z31&NILl)6g$$tCs+RmBaXW80aDQy&z=W;9vnd%L_*s^AvD%SUgL9!nW8+CW!>L#Go zld5P4cMykZ9Zz$-TqRF(ovm}m-?Pt7pxbql!hNh|(=svL!pg`6Y@tn9N{MU1qxO>1 zvsMhXnTWxKU{5F#4wF$fe?cXax3Yp)Glhd1TK847km;SB4}W5jLuyJZoS3(n(F(^;E%>Rq!Ul*v;f+g89S0#g8YL%E}sA<}UmSi*w3e0AzY*m)N z0#BT_F;)An5lTsh_r_3)pk~A}f&cC_6C!h(+$oXI}QFfG0X!_5V?zOG{|$jkx*J>{7}%0!Z%djeD28>23sCf|-8E36TB%Ny+Ho?d5OQ$YR zxRcPUo^tzVry-KSu(vG<5$A!5K8>@tgsa?$v)liaoU35R@N*>3u15yPdUQXvIiS0z=SwMzmw!HxOvlhtr4n@Fy)pPEMhny=N;#mFL6C^u2&$J zuCqUeH*txM?HH|-);!t^*RI3k7_Tbx)9Gc`vGA%v>bg?Ux2Z3R9S^|lFgj|zmS)Ln z_8k|Rci@3i?)Rb$o%t#zrPTyLvn_9GSz7}ErG0eA0WdCS-oy#S856a(=G?941RD=` z4ine}yOrvk&aE{K^8>TdDDrZKn;n*j$DHBKez)+Syc$i;3gm5gl!U0NzlOOH8N6P( z`dkm!29Lq7``WXrN6^4>>*1^8XFW(P$!?s2@)Tg^qeP|lf8~&*jn6(4Z+1rw?fJj| zmEbx(fQ}kJ!kzED)PHbzlxNT!N7X*5vHdQOMcqx`9E%+lr~* z;x7RVySYqqPkh2*Z}lXN=o*%w@;isSS;dKp2mLaxioIoxEetQjtg#Hsgr%;dPs^?r z;=szl!g9ev=kqBQ%nZ}>dAWw;wFIe|qRXT6H_-DPq309$D-|{UZ`U0R?BqJ5QKErZ zf{8cw(Iw{xDp3mgT)FZl3_ZF$>I0U>ckuXsxZ| zj}z|u_0IQ1SXEp-7rCEO*bPYr6*3EMeFR?4eC`iRJKhG4`EG8zzr#x{^=XBUgY&%~9F}-^Zi#z$6-B{3%u4x= z5fKxY1MS)tYeo3IFzx^{vM*$6L*>r?)cF@X0qXkmc+y? zl#=fWGmo-8(t+v*HNrGqLOqUdRp zMd?8DCERK=W;eL(|2D1kI8#baN^X7gf8c*qJsfNGC#q~6LSXEaO;`^~W%Wh-s@64; zM3Zh)cyWpC0565 zKXLd(Tn7pF}lkShr`PyyCeqR_`_7zvXv_X0m@p39yAsXKReFH?zqT zuxwI~q7&~4`6SR-<$9f$`0B9OyHwt*P@0Za`OEJ?!AFcWUf121(CCK4#WeZXW#_w9 zO!!C}B~em&B5v7pqI*bsLjQKGpxNL=W?vuT5DLqPwXmues5IvV&%O(@;&U6O{=s6R z20fHgY3>G5X-ZSmdptk}Q#`Z#kC4*CU5UqQ1nw3644D1Botlime*0IGzY0Cj6-8X z3sN*OBJq5V=iSOAMe8JKJNQyW1uG^J?pl&~X*pkkYdjd8Z~N?4q1PJN>3v_a>q{Ee ziD$e0up1ZubHI4xndowariwsd^be`8<@%VxV?-mJZdVKqcxK2ox`c+7&FRSoCKhLS z%eCBME$>UvplfbLMGKaQX|MNqqo>0Gt{D5T!JBnp(6REiVi6=v^9qgYWh zXAuqxsrkn@6w&N&0h|t>TSSHfp6FNCLLRjqdwsv8{6PhyvM0~04r)+7HQ&o@{Vu!p zNS55_;YK&7?wR%Az<9go^O`A}{ao;+@_^Ib5UQKavaFaYH1%D+&9Y0^Q3$A3K52(I zUnG31e7nN*m)ndhmap~)lHfBm1!rwMa+pU%?b2 z4tFqmd(3o|CH~Vl>*2y4WoYLuU?PjUr18y_tiaUN>D~QJg!?FSxe2`0`!roEN|An_ z3CNm`I@+d*A>^E`#c7uu?jYT6YMgV85hW-?rHr$(k&DD8;&QSO*_dSKX zA1xaA1cuk;tM|ob<2LX z3zNh^oBH|WCI7isN-^tdOA>xu{0%$pE+yK!piCHxql~IeX=vEpth}Niv0%1XCU>Ev zl<##{REMuga}JTu^9ASFov`pZ$C1li-pS(HD-G#rw8T>R!D0(O%i{%Zq=|7avw1x) zZ_p#_^^TQbOE;5zH{hJZahe=u^fYlkh9w;^9QU2P2a&(F?ubtksjD94$D zi(z8Do!Q|8zPn6ZXwN^XI0b!Oo#xGIqz-kYsJQijJb{pwTGVN~0+Z3K%EY;#WdGji zO*T(d23Bi^Mps0B7&3(00)yZCZa>Lgz~Jhkwn2fDs?#Y;o_@yjI)JzqsS}pGU)ZYsyi$wJ;zV8 zLMDtU&>5)=j;9MT+Bm4?oFA8O6bTkvlN!V1w3^RHzI6#iE$DeCeeHT=E0)jNnNy$t zQz_wHv?2Hd{Sh0s_`vaT*0QNeXME@~|F4I>#JZ0+CN^WzRJ}Tpr>9&hYX?96LbdUf zu*dFi&6cn0<{QHZH`(v}8CP6izE1opwq|g9gp-cK;JL#Eb#w&QO|!V24j=3e&-lFK zD=&2z5_T`C4Fo&p|6M6c{D$Q}76+F=-Kx^FUx=T=z@QO2d{HAhAf_m z*XyEs`**!N44}u&CFRo7UG?JYA<^`6l*!wPC4tr*4K}T<*Q9U!V#sxc@3U>$IGdL1 zkPP&&~QGPT=K_j zo6jUcL+X}|EluQ0U5*5?!f%OIUcY=p`CSpdc;A<5n1fEq(BB zlyY-JR`7kUhQ?T+leI=7B#$^KYdgPOpsF!widVFuDL>G0%Cc;+SRU~WL}!ac{$*ys z%-H{K2}8)^ve`WOnz$VSFV1%Z`-#`Z=+&9g;p7Fkj81*dpf9?l%|j_eGVo<&oyiE& zz|VfF>kr#k_4|jgaSF#Za~&%JHT}ATB_Pv6OBt)HHtuETF$K<(s8&x_+4vG-|4t4~ z9qDPSOLzWa3KXrRrs^1@q*Z4zvS2(rP8{r4E~cVu`Sm6$(W;_Rvizsq`v@H@zDkBB zx-d`MdF*g~ya#6p#H!Y%RM`6cCANN#QgHXr`dUVe3C+rm5Lo^GEkr2cac=^uE~7G% z4j^*&q+Ft>u=v8HVko&?QjyEU=Qhadz3vcm`@iwYE5YhJ!bQbprS>m$UHi7VkMz^& z^-m#~suZ}A2)^@u!LDNRx%fVe2CqC7h-|=>*1gl)L4rGR{AM> z$VphezEhYe+x6c3p777UW}_LMAYqZF8@wEY-R1&gmvHGEyu?U~rWBePR`0;Xc4=zo zFj8{es}1}kwz2FHoZxdZu~NoTboa};zN6VYUZGDF9hEVZ-Alw(L>weVLyYzccKMSH z^$RSFBR++8!G5-jluWeEn+0H;+i*%xV@3|Kr6E6Y(0j>;&GPln7E?|*o!!7q_+C{g z0Yh!kSqTbvNNPDwNAm<knI!Y@unr8$ul@)n859M#MJ6pTCw4i5=C*(T(|Log-nsBv0Ep%92K*i2lh)!Z*_E9LlGOu+LJ)2K;|rSr>c=uOVx4|x?`>U=3L zEGR2`um~{*&WJ=2u%-=*hB@*ZSD+0IBC6shE`?q|b zxwJKgHz5hN3PhCt_jUt<*Ff8L_r2ZRdYbx4I{x7;(J&nLHsb7xjh=`e3?c&7D6?Ys zI*(pAFZmQXAg=jnSyCc}k;(4<#lzU;$x>4)J*BKx^Kp+pBhDCp^*i|T+qm&<*{#j8 zPs|xV57R-wZ-YW5Ex2}0JG1wB!W-PxY=PJ38n08ov}TLJMMHk3Rbb}wz+#Idp}kGj ze(j?(_}oAJ1wl;qd0+#Dv9#bw2TrOus{Y_xLVRX4YE0crLQ)35GuEJu^f3qFJ2w5j{lkYyKwYCb!wLaTjBa>l+l@KDQdRCh3B4d(EnwE6O47T``RC=*<^{}X}dDz ze&G-?s#}-dK<4-K;hCeV$96rC+Ihdmd4Y6)%=gDP|GRq~F8_d&TxDon?MLhEXLT{4 zp1gu$DzDLV&bR+oc>lnz!hej08jV!=u};sAf8Ts1CXesvIf~sVa6MLYIh;;yEWdZW z>f`2dA0?xu3<_$GDdF+>WX3nqFucRva))$xvNri^E5EeDMWfb-Rc!Bz$$@yPfa6E% zpZ%do>0I7-1ej9jd)2caAQ}sPN~qvGc^86_9(qj3qVolYTJ?(fVN5a{xpZmxF0w+JVYb!7}x!PQKk62&ht$&El zR#wv+2?iT4wci8rISGN)rpUG{><6>MGa3BeKf6SWFBb#t9n79ZusZLtjQ7WLHS6~~ zUamxFcHjoavPCzLPc93yx96r<+;JW zz!SSP0&?w3z96&59}=zIz02Qu^clSBLvjD;O3Hxi%#Jx^S^N50_02~frhKlB7`eS` ze<;pSIPcq#9>7cE{%Pl}yiSY9A2OT>aY}M6UR;40Tr;-C!y%(hOAqK3zue4MaB!NWP-E~* z2D1(ri_v3scSrZg^hXh_sY9~TInVkdK&j8W_c$Cm_%iyBU*ShbaQU5K+V2U>`;Tl# ze%9NjbNKAMK{9x|vSZaw3o8KfV`%s~VUmOluT_+2N%VE!OXB-BspLiWH>YQ1v%L1T z`M4X%1Roc&Yj)~>-)Fs2BsVeaX2#B=Sg}WCt*tm*o)Fz-w}9>gc0BHCsWCDZ$4AfY zj22T$5zlb;_YSgmep7bIh?$bVKHuzK6-Jt9Bfq~S=sV;}TOCJ$_6BMVpoZuXJIU_i zDKZCMP#$OJlAfbw&!UmOfjDeqrtC4%j+8xTa6zj0az8u4lMAig+b9XHjNNC}cyKx0>rz4hL^Ve)PIcIKv2YP(>EQ6aw32JV?h+b`XmK|`e4h{eKR37Z> z*Vx^A!N=4Qy>vGB53O?P%C_cl=yIl0jPIG-8i5*C9MIo4I;pTk92?!di9zydaQkYG zAj34SQrLDPXa{698{ci7>>fKgocY`Tg@(5L@ifxjnK`TPY&tsC*lVhty<#n1;hN*s zF*XC}KWIlGz#cYRl-hB#-Hw5!M@rpDtlhN9s}ODc7;0NVHD<8Bn9C;E7#+KCR&bC4 zRoHqVWM3_QCdfl;2}LaKM5;2SMrV5*Q~|pf8p0a+>GG6-0KHx)uj|)klLmRS8e1{K zS0|!J>^oT86S>*04*Z$wZLOA2=dcmzNnuvzYVadBmnS+1Fn@Bonsr>)HNUv%Sn5OW zoB+=pq+b1+Iazt5KE|%jTV<0nF=zFh{jkh7X);`Qo58RCLypLH*>%60UjUM<_u%gK z(`=^Q{Tl_6?QockfYHM)bD|E7+0IpT6h2pYk$4I;@Pfr-n(peG&Hk@qC`aQ=;!KpG z;5sD*&xm<(M7;5RVi_W--z`<|O9a|yfth^zd5kJVw! zq)y4WWthgR(Cp&*K!4M~Gx$4Dw)`#M;|LNaDOejTQy9|Gyic}jfwu+|om0RioRHzR z_$a{*txe$h$`mGZEMB*B(>Kv?wa1Ovi>=O-wiJ9re@TNhFR%T#mBhHeOrgZw92}cu zvpo$93I6}`(*7T-*vk^Cg)Atq1}^k8e!Ahy>!3oOM6p|1p_se_jJ<+D8&VA~+{XXsAPWSi^im-8?5f&pC(@Tt8hvU%~q7zor z>y|pb(Fp-1|KkPVY@a+EKkIxp+3AZxr)(mVv5SX>(-rouOVxa&BY}%25x!Xu`1Y_o z<~xykP9_|Ighq}wjmu~7&we`hbr#S2eaHE`4-$l1=5b^Pzt%gAxkD5v@wBAt7usfr zpWw`z5|;$>NXZ=8k>GaSW~(B_whKOm@G3Com3y&PcY0AjmQf#^w9;_cXo%j1;AK#gn-oENI1aae1&9*x`b7l%Hhk$hI+KwE zY7zGKnV%;|$VNFxO>n&ge#d|?5SKo(Qt38C{DC0H5HE_^Du<2HT2khGob_Q5Q#y(w z=WBE0CreHP!{SmbJEqF5OgeT6B(mf0*Bhzgrmy>ly?VRvL=NrHPIiFO%46To zYwC4KV8+N`xSyePp;Q5*wfuV@}GL~N1*UTZuQLY?P&51n;|xKm7CVXWgHKNA%l!&L_ty#;oOEh5I0`x zY?qHhHbb(YICG7-Q%3Cd1Ci+9pAh;+Fzc^KI6qfD6mVS(swd&A({&(DQ0R#WzhH=< z_dYWXbaW6t=6JygT~o&$`ozLLlj9}v)mrkrQtNHD=iWFyO6G6^{sIayrnq-;WKSL% z4f0vvvD@WEPsq#3Uh?C<{+qu_Hour%=4z zz+?tcM5CTh@Id{E%<)2+3hFaQ&t!t@b=f@Rm(jk)jE|CWf^5wJ=&WbB%RTpL@h!MI z(}(_AtO^LO+!IjJx2K3HQ*NKxQP8j>@dJa;-*1p2B8sp3wgK{4CBYuGe-Zb3H#()y z<6>LO^-cT^P42qxl_zE!>aMm9hQlLGOWM;bzz~&e!QpfCoFB*L}M3<3Yt$Z>kUn<3*0h6#gnZNY;-{dVx?bU<1%%&=a0B_pb! zpieJ1ouXqjnFHAj*}s}}pFPbdajzm}h8Solm`8;0tqkn<$ekkgnfQ4!LX|21#zdLE z)nU4DaLlaC$mqK>&dnWo+?8UUEb33>A~WJAPf&I|67b%sE3w$KbFsm!ej3YE>)G=4 zmK+^3^g2Xb*7lZ?k)z~jh}D02IpDjzwco~bQr2FP0R;zb<-*XEPbjOj{Qw^x-ZV7C z#}j}z&bRXkRmZezKCZ1Wy1(r0TV<5+O_aAUdz@@Fj)fEFY~OI7fYGT_n53Sn?~(4D z4@XR=DNLAc#iloCVhptor1`Qxo?7*Va}HZX=eY)>mxvm3kA0 zo#)fsOk)lG)+OE3BZnh06?**HaNKY=(H>ILGR1{&83dcedUZdmViJ|Oq|>e4f2pfeuxN>=Ae9srTUEV5&ob5^KljF<4%NR1IvaX zPbP@6_`8yZkdx2H^)54pEPi3zYgoPBiIX~!ZS);3&kNYh|BJD=42rYswng8B1ef4$ zO=H1=I|OK;aSiTHaBn;~jk`-AXn^4EZo%E%-5pMU_tx39&pq$1x_`T>pI)n;)pO1@ z<``pU>P7_8veo9L&|@p-!zo+oa(>%842{YLUqsN)ciT*>W#_*-qW$SKEKa&W4p&?W z_93>KqG&U6YOwvP(q(o9o&k*urK(?Zrvs%P)FyS9vPQAC_9=SDCRabuOWGp zx`i6`)!C7ZJ=~GtprvcjsqDJh+C)J%&cwn?mThHaB|}IlYQ>PYog(`8@9MywLUtEv zN-lMBTwGdohvYpd>dFT=WlS_+Zl%s5rHy z74&()qw-_Z;wy5|Cx?zJdccpW`0Ri2p-O*1!T*GXPmiiL;i1jrdB8T~f_5*_G7oA| z=4SmsiAR@Jw00p1L?%)qsP;JF!g8#l8_t$Cg5b20CKn57OKB&gsh}7hbR7f+^DcN= z2zz_Ke2SH|ltStyEQY*LO;2a$mMqCRet##&6d4#C@2ewh(v8k5wH7*9K-x!-)UXLXJgg=uEkdJH4B%xC0?&SrHk#^sspZ0zRulnh)t*!z zZEx?fT3xE<9vgDhX6~rYus-r-@?>I`>S@!Ix!2pR`OR5Eoa9IQO-RDvd;3Lm=w+c%_&FA--6P;jYK*Hnadg0(qYI@XlP+!NeFiT z5Pg<%pznNdYPC6?c2!bd52Pub+5Y@oN#{LxJBHxbsc6+-;GPnJKv77YHU$qg+)kIp?;FOzZsqbr_aDK0vVhXa1{*s{ncH zt+!TF@IZ>HsGcpJYr3D^&-B|}3BF)4DXFo|o(r)+o=o6gPmZOl7C)La@}*R`>2S9F*X=ASXYhAwi%8-F}0TpE>C^*kd_L139cPJ}+|J0d0c~0TyR=Q(>O) zV%~7xC*If2wN4M0mVA#ZJ!ZIyJgn*9!r--;Ap-@f88mn(ePfk(a!Pzx>hI55 z61h^~P-UzN95EPqr&{0G(J1jXn~jaIWPV9 zH!OO+TT)3Z*sPVsCZ_ol3$EzNOA1C3WPjQyo3#0%!vg^J_8LpM$$Q_N z_lL;gm&Q|Yb3*+T4^#BW)65b_9qEa54@AnAbU6Y&Aww@zSWMv}N1r&7X$Fyo8~;j# z^2&zpn$0^8=%hDUYNm(jq|9z~+Yv?VqaWv@1IsO>Ig>08Q`nMo=X;oNijt6LoIO}U zY1Hi%2{VY8+dmDB*u{2S1PkEO2WBlAF09sZPg|5f7H`bRJ@d^osjFFUMS-uk<~W~S z(N>H%zXTf19b)H3543GyDFkMGu8?0S<@z`%DU4{e}9?t$)ZsChI-+tEE;L`2Y`UcqO#Xi3(3~D zvLWgA^>88D-NthcPcQQ%dR7_Nl7kNN)pt@eMI*F*f@orN;Gk*AtMwTUfu`W@@yb+D zKc0v+`+yaiF&D^YUSye}lcq)lt$1$Dw_7RBn)!kCN)#+Ls!6Mb+Rd7bZB$l?d#^z0 zj|IWnf6a}KCqPl+GD>7Lsym{LM%a-lkFAsDng zF2xA=ewCO|$I!+{Cccsw!*HmK>?YtX`>*?NsCZKFN{#cR5N}Gj6RD8~h+DG4im1tO zeIf%ADERxJGZ>%M4O%&2iW1-4LK0y>1ds0YDTFFv`r5KovpKGUCo*30Gf$PLFmpC} z`-tHQ`j;Rh*=+}$01#pt^D$oO;7qwETwOwrBWvzM>%6tAqa#fvWDqbpImP2pl=187 zc4`ZAP{U@@N90(*{wFp$e*dC*o4B;&dzZrc^HqfO18g<)E?hlk`xZyZ5cTS^d$swzHbWE7IHZ&hdDF^+1s2%lI*RixCzpS|emU$aVT!+O&w$H4I8>@S|Ip zad@jra(EHx2Iaj*|F4BgB!0}wErk{y0-u&9UFJIRgVNrNS}REdrZ_?r8EszHBW78# zcxYKK+fr1APKWI#zGsgt@#q(R_Xl@I-5RjbBW(8B=JUgM*{-snHc0BHyNP8$E%m-p z3_c4Qk)8^pvP&blra+T5HLSNqMZUn*3ACsd%0*3Q8={`UqUY>2n`g~bzKs&atoIKp zc`$gJ+2jR@zEq<&?KuJRu!+4%T+q^z0qcx!-98cEjepeBBU>gblP>tb%d`JP7>xJt z0jmIGD%WOUZ_gcsCyz@i&UBMne;=DuhSTCcNF*OP^C6LL?EGQEhOfKnCd!uk{b%y2 zj4;={LIR+#Fx70Vu)L;V!9I!~m`l?`qbDw1-XBlmHt9mvX?`r9BCE!0M&objjC27OEPzlp zW;%vGEXDybf7_iSlv{i$M@^f-?pkCE5lp*G>)3`+=U27{4VxQhKGW`*WAy97ip7II=0_1r_u7^ya3_yph&JG6we!UT6O-gi?>0F&g{UIrTr&e0)zKxAqPO1^nQ|_B zdCBJRR6f2uK4OrJR=(SBj{RW{?WnQCfP}G~B4PWVFOPoC#7eAC*}vc}GgKw@H95C( z1QqhwfHH!TogbUgKp;oNW|tT5&uLDziJ2L=G|qpt`_aZ+;p~kr>)@@z2dbqu6;aI` zm&De!1@3FH;IRj_;vZ|Fx{O#Q**cNz5cxWZIyQZgxc6pt%#cNWq(Ad3H4I+7aosM99ql!<;Ehv zdV~jMj^C|^c$l^*(MCqL%+L-0gxR0fnc>MS6Q&<*_)fhLT!R1ovP1V_yZNL6h-DYC zu)NyEe*gUh*N~KOj{r9nQQ$4I5?QWoE}Z+}XstGgO|DNm(0}}_oyZ)yFT-DCW#Xc6 zqWpGNT(Teg52l8nwjMrm!)^7~e-6K+0=~wq43vGyV!Cg9S{Ncn1zhYRE_+9u)f7}yLX4cu>wU5!pbisfIezAvREx_=!_8UaFdqPPkpcr%IZ@7HIQ>AU_T9!SWA7;tr5HpoL0Yggp*el|r)ys?J2 zZhBO7f^Hd`RWRwRxNRI}7lD-?))J||ll=MF`i$N7S70%V5WD4Mo`Rayp$f8}gMUXW z1y=uS(Hx%Bta$+ARFSyedTwWD2g^<$m!-I&W=T^Xc~wEs$5QH3S(8UaO? zYGUOMl6hR(+XX_4u0B!pFY?2afS*Pj4hR-i6L1dpour^`iEtE$%ykOelU(Fi_ee16 zrh^Nl$@Wg|ywFJL#GdC5bE&ZW_Uf^v1-!5B&|*NMS+HP|HgHSM zg3Qb}1N;uOlxTIpg>8D&zSy0xuTu-IsvsZ|Bu+S?T_@ zKSO)Q>I8q62zMYZlLH(We|&JoMdU{-_QK)9=PTGq!b|IdSGgybF@2-uVH_G@1App? z{qj!?%|D&M-$v^Gs^W|JO#T%pwYT)4nMmA2Z)YIj>S&({Y?g^X7zgF&dJnfp>I%F4t^T^F91*<&Z1{<@`j{KRWk^VQ;=vTIH?cdN^Y7^d*{_n~ zix0G?QedZ5SpCDDuvCPxY~WUrk?-W#$`ft-6T@e~#c3s$`DcKGHRK~}vl6MmZHYii zHpJc$C`51{p%3%h?6#}-TUWA(tK{W>VBNuW6Kv6Q>i zNk|v~o2X--oRu=avvRhmcNIk(F4J0WPO_UfN0i=9XOpFKpB3UyIk$P7zn8 zljNsMe>b4FK9!{6t@m#@2i=E0^7kS}$5Tnqe!hEe2^a(sQY#VQ=Um|@T0iEZRa7nZ z&k?qfxgB&x71$RXmAv|-^^)u1rC?QA;{F{4*VF*qLXS&`)6Z4+E9YdEUgf{q0%b_aL>Y^e%Z+18w5x z(-!SDP`&?!3w0BCz&-{yq+6|CXGBx`-F$RB<~2DLRk_Q7vZM;!-VV0gX~L_}jmbJs z^^|L`HBkj4`GZrkVc9xBK>Q?;( zOzc%F3qZGIxC=gPy`Z z{jP*r0-MsG1RHeo@_E>NDg1O~Cg8W)1b%yw#>#wO(1Y4}1)%{qqj>Ip8+I%KNps-7 zNy|E?nxl-sFNjd0F%CUr6RYH=hUY52xeqcQAov3!$ncKHUKI6* zYrBS2W1u1cCCzlLOizwjo;tjnUcM*2Pf&dlkUuDw9p<(7>Pw7P4!~;XsV&diMn@Dl z2W`UX1%!dUIvBvX3jfnW@7}$GO*C_O4g4^2e_E}mIc5PCiOX-`frf*e;thf0M_>a z04%ju&K;J8f=vDppvmEfiJ1IUN1+2br$-qg&I}ate88Y*wvi&=3F2^2co~%zX>&6sTFI-h~ zwx9$x>wi}5g`9CeE2rARz(uk*+*qEZ)xuHIx&L>$+5dl+dr~SVUZMWMH2@~Qz96LW z2Xl-p?ZHfZq-1mKdwPP=y6p_6m_(JfQ(_?vq#KtZuA z6IMj?XfQ$Go<9eMeTX_zA~D4`(B}N&JVfYe7z~uYRKVJwT?;nc37h8R7i!1n5N@Ig z6_$Jrlcww$*&R{`pd02G=H{Mm(i{5;wHQG>S6BI8>*lcEgYG5$5@{f^dg^$yU z`L+o?o~zNEhQTLHxXzi_om*00=Y^d~4eY_X^73T^ciX>P-Xkh+iw|9l>!06wsP{~l z5y#O~K0zKhb%FTYaDyGrxR-*krD3zP|s&MHxBpo_%6L)WfHCb&N_F4Q&Op^ zhyBaPfjgVDKOp+^zg8Ug@2Spvfmkny0iXwFX8pO7D)=+At(scPxv+_W;UoV4%>@YM zc5nYH>GI-03vlSc6H!%$udJ+ec5yLu=KirpWsF{ra;;Svpxx3x+>iv-#-OF)isH>z zK|*tw)bkFz9hIM?*`1hJ5Vx`-*2~rxc#&IOJ5vzD14TpXE<|q!>KsuRM#iu@S#V=5 zc*=YV+{;CYNXaE_4JikeSUGf?WUbt2HF+0mYU`(_m%VOUN*+YuyuDgr!naP+KEda*S}P!2MxkoYLV92MQ`U`M20 zH;NV|W@J4uHfFxrL&6(HHt&{kAIQU2y~X3xf>quRn1Mx);JQNjmrcMIn8OU%$zqaW z_0xyg8ZX2|&$-#$%7tSRxXlotU`ztFH>S)&Vxn$vLTDd!lE?#+i-a@M8yt zR51_JzoFho@^@3EaPX4<`jvRv2mk0ynoZ&y$jG-|$$~FG>#u!mKh>L@v%%i-^XZFA zGf>V&Mc|C{&aqveao$Dg>oJ$1q=7U828UIDAcfnGAV^Q6GG_Mu(OSTzK4>9s$TO>%2_l6}MW?w0ao1-K~n&=*6{Nn=ui4~#Ia*IDJD0dshw z%Ce}{$YJ1N*{&~wu%o7|q$Hwlmwf{)pm&qDEPxcVvZ59S1r-}l!Ew?04Ni?aA5-SY zX2gIYMH1T0$xv)$g5#5%va&=?ckChoB`GHefyyOqyu~Arp5|aEWd*uM#(;QeEra+v z-QnoaGqW~>5$!KXWh%EXQDx-?#2fvwNvpx$2;WAq8`UbuBAEWD2S<(mU6-n!e9H&Q z#isi*nx!8N(Z>#5b-gv3ZMTlChFXI^s9QEOI^@%u&(B{sqrM*w{#(nT<3-Xy2MZcU z&T(N#%f}jd@bRNbK9KeJbo00on}f^s3)^OFBQo`YInVY}11&HuF8!P$e zf`IKFp0Glk*tD3iu_I$QQ{H$-+N$p}SjTBmN75*0k!U@IeU94$4x{=l{!3elG)3f2 zAx#)(>+gLDF+L%}pEtdgz1~uPa;*#wMF{Fw(qc=bCnUU&H2lMnk@L80suQ4QHEs>? zFztmCnWi8$J%6=w6k>b0LQ$*a7vC(`>063JuS-zA7DbaX`TQcB+E2JE5Mi#8+An@P!WB)p~$eycv%7; zH5LxWxx-}ATy~$w0~~QIlXN5uC4PZ6zI|>l-k9T5u41ES$dEQtWo(7JArbV68MUo{ zjxY?1gk$?f4yZebjLHYL$26w$LLw+f=g59tT2n=Zq7F$T56DJsIrl0N9esDuCAAa( zXE}@zAsPlJh53goYrF6o;XgV=?odw+3=f-r;EzOeQ6*aqqUdc!RMwq5WV>aC_-`lh z6v$_bis~R!F>^l+yIpR|uo$5?(=*boN6z_*J2^8+qI(@~E|?Zf)|@Urjj(DtFqK!j`(fF`1u3(cq%HDMNHdKJ^ea zaELKeX9zj`=2@c}6dw5$m`ExXR9y2TDwVRjVTkKehl>^jmjp5KiD8WpWY z>9e07_;9GiXrpInVO{MB^c;&rr@8ezj*oKli=p1QC$k^Q(1xqRM3y-~sf_-zivETd z&{B_k%!tGI4B>`kk7l^~6=ozrxGUCnAyf{dS|I78p@{|0Gx~Fc!G8C>Gp*2wUw4fm z29{J;*jF!A*+YMO9PmQ#OAwoC|K6QUgtd|KjTHeG9JyYL(Ysu$z*Ih0)|jxoZAZV; z{;ex83`ve}B9os;5Rl4JQ&V+1wvaRE&VqumDE7Z5Pf2x77Yj6hXO(lQo!5nPu5+{B zlqC%k|9I_MujFz|qSu^4AuhKFT%g7iPRq#Kj#I=ka@8l`8W{}q7nm*6hB49a+jl+? zNBWI@IX@)Q)21^#aA(Zcm2fa()Ab%fZS>qfINjccA~(T@Y``wv*9Y$ zu6wDDxCfH;e#DXE`xH{=!)4l!248ExP&4N#vr+q21n7H`2jh8ZLw+A+X|*G80wTZMof z3e+=#M6TpKUKDA6EbFy`HB{%HnKfx`C3%92rH+s^=lomn27S;9dn9X&;5MU?2h=Ih~B{6^wPFQ)0!#jsy>M8xq#eL6_6i3$Hs35(9$PIvZRS* z+FNM+<-fZNi)9!qX5AZ31OM2Vtjf+ZYD)yq1sPs_%CfF#&rOSsQ8knO+qtwL;b?@@ zs#|uvs3LEyf{k%pvHps(BCkO@J1u=ZQRpXKOH&QG2}V8}S;JZFF`0 zrARU^Bd%*Jnx)^+$$vE3A^%{ptY%U>Qk14ETUp0v(Rl&^T^Qs<;_=EUy zb47SgETvfjE;bpKghfbjKl|U&)-(I2-$cohJ#3(VdL2fxI2$fJRTW>ae#kZHi6lh)F8nENlko|fizbyT zd50aFGFobCl^T~e1ok9eaL>u$QyD!aO+GlgS)Y4v6oyGUVn$`TD9iOnPuxTjJL*$p z5r{bXa0#j~Oijs$)D1^PC3n2k9yBz>fQs>SzSvbN_vXqFb#%^8&I3^DK{zFm{Y;h53({9$KVTYp-6(*8`$)ESD12;ZMD77woZ z$n|o^W1Ipzk*TS!t|!akOQA6-Pk*qCi7=(0jJoYf&-ZHtCN^F52>zXS^3NG1R4#UyU;1 zAKC_UVA!+OuF73wIpQ*BtOfE;f~*LU;yMy_X4J?=Pg0xzaU3p)X4}vS^krF7{ANLA zp7~kFq^)k+HtLI`%v_bd8WO+H)tGE%)^LR8g7u>2;1?taJfEdy5j7iod<(q_k57fD z^bOfcL(2U<D^p@rovq{KC0I5zzpxojw3|Ma zGo)x#LjG)Fu(|fe1ULDMr|7$av9TZ({$e3tKVF9HUb88vOj!QKByL0$WmthiL1kf* zasSxl{50YCr^CQriAv`MvAjZa_Hp5XEF1FJ#MI872(at%qL{NX^Fk^%kBUR=vJ~Op zboL;>*mqG`JVOTzqV|AQT$tI_C4xB)G0kdUz8Z325WcAC)kX4I_+ zW<-%d(mvpRjR#m1Lu=oT@a z`iM@-Ol!BxruIi`M6$e9dIzdvVF>-3!I`*FP2+gs#pF`UH2=(pmP`yC1XD0^ho_-L zHiV$fGBMMzbNY?8845jGuxqK($lx*cn7PJ(!k07`E9=R3?-;Ggbt6&9J=8~+@+7dBf zpSjlFoLRlc{N>n1iZi2|7y!*_jBe|w*`z=AezS>-H-Oz$%9BAU@VLw! z8ii^+msC%muG{r@lCjQ)xzOuOb6F_bd2KQ%ZD5*CrzSLHw$yCzY_0d~=F->4lSx-s z@Y&UgAHz=zr3|Axi45=pg_h}*6m)jV&R0*eYa#=E~CS<-93nqI!FLk z@<=LVZR*&NNe5>I>xLSBkJ)V|dS9dglx&lHJ8+;zTP@w^#=(K{3EV-) zgV=OwthjTjDkPp~q-hbK*pLI`{!-5r`M!~JUtX#E3}yY+laa$yK@Pdb5F+=TMa@bbwcX*|;Yarm(&;w`%aPNm#0tGPb0R?pRlu9C3{FH@p4E z@4MCA=~wTXBiDn!_^5cuD-SdLM@`4XWD4V&=ha`gru45Q0F`5y{c~-vazk;;&)sWp zg48ALom0_v>+45v0T^{;Kl?*#vnHkqW;1a{GrHN2_8cw7Xb$;~pEv4XQJ=nYrgOIj zkg3b6xuVNB2|PdEaPw%)&}YIf#n;BD5jXWd@Gy-2@$s8F0RrN(U@RR@D4?>oq913N zMnsjzfrlzI>p>DIzwpB{IyNaPCyUvmY0=X2&fT(;K3OyB3-U16c_e;T=afC(6j#T= zRr9?2I$&zox?@IftZ}Ftn3PZf86jGIT*2EN(;m29{&IV|BBtexK4qSCIa`^Dvzxck zsMxuIot#p6O>dnRtx^~ri<1u0Q6*ugrB!TmV_0FGa=%VZwh(HD!q~KGNh3x}r`|f_ zjvBT0%({|w6RZ5P!b&)D5oQ%Hz-D<@cHa{jkjE2ZZ+}J{!FT42-E1FU^mM9>a$#+W zw;$naK8oG`?o*l^EWx{vu2{buc|4oqURSwlD!{u;y7J&*+~31@O37*BdJ7~?QTjkO zU_@1yBA4AtUt0M~ZOwkmw0+hlQhZKhGP62=>_LudW?PS+H*Q0#kZ4yIou>t>rRD(# zw@X|~3Mg=78)W*CMX>V6Wq1F?FDF((KBhg-0Tys!{oVVG%8fu!u;ZT@vBhmdkc&=E z;VWD1s)cCoW{URuM*rI*m8fIVRnHc&ef^1(8x7C1WwD%b@rdwuWG<#0YMkzRY_hs!l&PjsJic6qt*zPrnvqT*$zkXt7LP|oED8M%IonR`@ ziui+Pd!GBxY|Lh~Y)lbnYh;Y_bcOHIB$6z%@q(6E>?AMN3ss3bEYBZxS9DeNOAVFf zNxW?kcvWX42F3KLCF-X%wfm1Z=^pt@7MxpR>DlqeHCII>jQC`@O|B-AFzC;!W0T`Y zfKzfVBjUT|TJm#0L%5P9M7|R+nWK-Fh^sW&<7V!SZ4&PPD&mO@8#3;XCk>+)1iQ4XAr0GSUnpy=*JJP_x z1tiR&U!B-_XEudDSy+}DD}VpcKgOY`X-rZN26e=VwTa-j0?IYvXhdl!O)i z->q>k&HpDagkEbnkZ0y@77NrEQOk!&X%tC#g+{S%Zg!tE=X-DeG*>M6hQtZT@1CgI zYnReH1qx8TfI=3awP;h2Uh#)42^s}C8aHWy(*lMJ-kR(!d@I6h2tj3t%;oq&{rg1` zz*YPwDCsS@?XoAnG!sYC+>X5Mnm+J5)w~wQRmqx7Zb5esb3#&ZHl-vmNOX{$HX1x7 zZf7lIW`XnI!6f{2&|Xo<6+}1{=58i}%I|i%yW9xxwJE5L(rK4=AH*L6uBNi4&2N&- zJA&cSCy&(?Ww<)fQ&wn1#AdIZ7ecd2KmI0wlIwkQdo;G>Shdv^tBx(c(8d7J+6gVP z>V}vulOLgcm^ujFL98|~Nc`OGO?7blnGug!p>JiXH&N_~$MF%hj7o2L{`}CDq`;3k zu_HnIv5BV%aL5T39PHuCOP)h*hMyj#opSrj;#TFvYEGb8QyXNl(mRUA*}N=kV?iZ`vV;>Kv{;83+T)@3#W_hVx})zY z4eXnt&~VzJh3<1jd|~egxW&b$3-}DdkFf-VxV+8%7(A^`v?E-k<+3xl6>(g>1T>N?jr1l0SWrPNz}ubggIy2maWcBy zYeucb>ASz|?HlIpi=gCGYiJH?fQTf|RPvu)Xm9)kJP`pjBWjFHK?IEA`y~9vK&wmW zwWYmg=J=-PbMu(a@u-qtomCJGP6$jnbPa)Z{1_RUSRR~wKEoWdUClqxhrjyow|u*= z1$zZYBL0ew&GGru@|vQGj0~9AMa(ytB_h*o^;cP1qhzEHi-`dL{>%~+D*(;89nL6Y zq-}%X$;{bocVpb1KRdA~KRjaa`dlMYJ+R*&UX6`wwT5)Q`h-SF&%a#Ux7e=U-;|&>zs!po zYx4QxVNV+IdgiCL6tlxr;t9cNTFRGBnpIvOmAF`%-A=krf?bYgFITnCsx0&fqhi(g zf?H*ETG9l`TRKy{$ql13JHjOOIvOCseJZ#KZRd!+SHt^u1bqjH)Pk-l(Ohq7UFP>xsiJ0o~Y>@nP=*lMQ8x6xe*43S(XA=~xz zeG_yvV>0hrZ-AS!Rh(Odj!iGzp5ifQHg_mGbLE>} zX=qxUvj{&@Mj=_bUospKHeC}Xvv610Pf(Ev&w-n{p?hR{QXUO`U+uUnl`vH^1$$?0 z(kG0R=~IOQic_(R_*}x63Zp6=R`@*RGZF&smK&?@SDrjANWzRem$96GCD&*W_u1p0 zoeGM9z2JSTjF=JXyg-qu9fvo>?#5?ouP^$~gq7av1SDi#WH8y9r*N5#(6XT1vDY5H zb9BVI!~A(*9($z)*P0G%;w5SNywL6f0j1^aU_xzjN6;K=pV z^sI!f1FI*7s{MsD194I^*3Px#x+>L_+34Q$E&IMt=AK?k@363EYW1kp-)#Pn=Nb)o zK5e>Cm&>KQx^b(%isPIEkR)XP;4VH+C{|GWj&+=W;8D_?RbWy<=Mo1VA>@9c{S{G8 zBNsV6vBr6qMPJGY!UL!NXPwN|;rLuc8HI-3=GBW?&%$(YGjY4`(f-Co`e`O>Rd030 zBjFoEQsql6`Q-G1_^Vh6&~VkO7#cSS%_zSQ?KVGhu*954HMIL}h`6Nh@_+P{|4Rhb z;Vo&<@(C)cOS^dtFh~>j{(O|>nf9SM5=9bYiydA{N+IugonAhRZ#dK!F{C?c#Q5WZ zdh0dQ?pRiklAB*Zf7GX>R9T2BpiGwxtLymwsd+#?tYc=WbM>X)!q&!oz&!atpymf# zAL8~?GbT%MOLI`IrDE*-NyKh!KiM7zo?LEQapYZ;TU;!Gd{lMPnr}n&uwV|)2&HJG z5BxPx*k0LsZ!N4Hq}$?DL{^SB^k@V*s&C&l(WYT(y?+m-3{d9WJG5N?HNp==5u()T zRzA@~@+8DRf2Kx)b*sJmcbH_ z=c%^GT6DqWyF$-A46`;AE!8l+e7#XLhUsqULQCZtYPC$^#&2FJM=w-XdGh94(!4_> zL9*;3TJ4`%pu=Psek`r5n@ewPLk# zI@oknK%`OQQ7!~bdGilFmgyUQt%V15v7H4a5O=%!dBJ?WOEaqd3h#!Bi20aNN271( z8^iq725|%d6`h|E}B@ON+Q?Zx-2l~Bjq6}PWgJ?(PWELGfB z!gZV>v)Bx^LhA)(E`ci?2`h5k=G*u*Ln>y<4DM9{SrsQ*J@2odb8O&2p#ICh*{`@C zvDu2~-(sBbnb{iiQg-{)>TDN8z7I0a3SE68TJw!|^1Pk2CSL>PCI+h=y5x5$PMbCh zMz!CwyhTPHz>)BJrL1M6nm@#8@}&E8v#iaF*ZTsvbOOi4ve~2JOry^WiOn%5eS_3* zYc`N%b{>jzp0~;0l0v%@OV=u5?bR9NLjo2Tot`gi*EZS2Oqw`i>#S;4ZX@Afgo-nP zk45Zl{~&s40hH=Pu(U4alnaIZcV#=wFnYPBC5*k9y7}ATCUd(0SXVOYRVJg1t8WV! zh%z#H$FaB#O~K$^V@iOJc%lJa(%_{BvrKq{*#GR8=D$k@9VDlw8(J$c;`Tn`e~Jh=hWmk68!oZXAV za<}v4>alUh0~|{z(y{vjL+7!Sa?eyd%6mg5k>UeM25kMA|h`X18_OR1vaBm#QY} zD9v`(QX8;19EmDvs;tG8>>^Gi{BqI0uAo0!tzgsh?TY=oReDPOx>8i8T~4AbnJ!Pj zX|*Jet_ytjngOIJb2JLC>Dr{r2rE@TVK~fmc)|PlpM})an#|u_=N>8CwSn1hw){4I zKs?3_36q5A3=3{Sv_i<$x~V7@?d6^ozDk=7yZSY7*3K(o?#^b5*!LU@xBQKPv~}g0 zm9dJDt;_;*LS9&V?Ui@s6myv?Gq%DSCh6V47-n0zxIH>Q?phl-)o9}>p-$dE=Gx;fRUr>SGEF(QNm+0Ppv4q*EUPme^t*Pk2URGA` znD6UnTI+a?1_LteYOISp*BGnY%VBQriIU!{@vu~2e$e>RIyv0l|KMmih z^Q@cgG|@v^0zeWb*x1DVcfR-D!;9+SG@f0cT^3#FsVn>Re5h>`uPcOn@If4BELC!4 z2VGibZ`{L-E18ExH@Q=4H>d{!na>G#Kcr>*ZN=?CeMVX z+W5%u@Gx!ue4faW#^h?3#YfCan;|~=kI75ej%xc0d}QlNf>71F1i zb)g)MFn7AQCx+AQU-w{dz+FOA@rr=t<~|JYm%TRQ#R>o!C9|gyR-0Yn6U!`Ad1E|T zkqSJVvssfiyW1TZk`!0ZH#joGmF+=%?Qn1Bt@U(-<4==NT3XN8Fv=DOrsq9YpHIpM zGVgS0WCtjhcl*vhG>j^EWH@MPzMF9PzJa_Zl)NCZPue5QBe$6+*Sdu|1n=hSK9}>GbVrT!n?!)yu zSU=&M`73iJL3Y%9N%)1qfpx=LqdD{!ZsCIOIZroLt{m?d&UW#CBG>^+KP?Fg{^BL- zrHl=kXBnLnk_tj1oZ@;ck)Z_pOS1Z;Ad0Na0RQsJ>awLjFpCv8{cd`96#>8 z;Zsd_K9^gsgJ*Ez!9RhVel?#m+g5FrRpJ(WuPG3QhY8Q!;g}Ycbp^tn+a2sx}+VO^|~dT8Be7e5oZ^scZ%vcj8j!r!im zSDW12dKxc0R4j<@^t_MF+ejP}>| z*Ffy_*Nn2}--C2M8U0+08$BCt?w)5zMksri>iVgYOM@#dUKQbJEU!qtRE6$Ob1YYr zzK=rWHmgJI(E?p{Oyrf;qrkSiXyM!(bu;u}enc*q&}@(QYv#qruj_wcyrwf2c-sZP zvdqs$2PWY|)b&3jrlD68q_NH%$D1ZQn4<8b+SzP2^T&p=pLNIL8mAu4XWFUEmEmR7 zN47NNSbFe3nb#;c-oe2jW$ zMdDdpHmUeb=bExl0rbI2@8{7bHM6Sfd8E1e_=?(a4q0Rz@0I%w!dibpXKpQZPY?B^ zU`E$=DF@ff)?1#H!B;KQ;NJY|$BFW)3cA$prPb|U-sXh%M+SFW_KTXl#xNv9f6Vu- zlT58lXQ+yhY&5I=ukiCyv*VtEMqvCx??=WAt$|7g??CgU_4b?hOw^L_48or9>`-nQWB>2PG*oVARI`yvvPxhJgoItQNl-0_nvys4~u z-sxuOvD8&S!gl|d&va>K@V5kHKA~?!gu;Bu7Eo_*SdDvSzaLuN+xOFl$rN1m%Us>t zPthQgrwyJ8O7eQCnyVd{6v)~uuFc5!EC8a&`2OyMB}Szr1U{dr7-ndy8pE(NPoR$ev_GSc+CP%)^T(%W-mj!$U4qpi;lj zJF_(KS&)W*8hThc)5|X$OMv#$;v~Z^SX!>n`{Wh}PKL6&UB4L2**P1tZI&=9oy@8A z6me)YQNqhQ0u7ikE*vy%l@5q0h-A7kGc@Q`r z&Xc&lo)@2e@tj3H)?&F)KZKmQm8&dGi-;T;>3KQA+0>dtwq7tSk4}wOCt)`Jb^Z}w z_{ruzwL_lZEU)hOoASLX4yXW5=9lM(JAuwK7mG*jJJ+cwkY`w%-bo$26Zp;O$4_P0 z(AOK*_TAp2E6i-o0q(Y2PUOir0tYvzQb+8idXoHh{sb3dwF%R79uAnt35v-I)2L%$mi=p_so#8af8!}j|5_C6OcG^jS@%2;Bk8jUt ztb)25t7mt%+@WCbn(c zo^WE@p4hg(J{SM1@BG$#)2sLDi{5)z)mu+J7&e%flrF0M@%^^zDVAUjF7ftV{$p)R zvBg*ScE#f_Q`QTD@m4nJ{Al^q6|oOU9`@>B zt=kN2c%H}$OEMD*jz~{()Taih<{2-;ZkZO&b}rd>vA-*7v6V+*(wa>*%hIhk#|Re; z&a@bLNx{LxiZeR?{UhM}u}BH*Mb*hW*GZzk&ZYsd%UF+5RA{n7BjN%>poM*34=zK2 zvSa}zSN{f}cf4(2s7089?blrjkwU*$OG-BoHaQ7GVy`{9o zZ9cY#=9rzM%nJiCXw^!i0*wmiZ2B7zPS?_la^CRT<Y%$Kz_=r{6gr9wU%7^dgE8`JL6@;dcO&Au30W z#waDN+l=otWbva_f7tohv-PXyApZmUdphS}xcD%yPbjkBN2sJS*qLs@Ga0?e3%FAz zOQ+SIz#vLUZVO2bGlacSfMJE@DaQ;QU?2^6} z4lvj1CeP?v9=kHwIGU>o3#aY2IdtvP=*)Q7_{LmhXUBpVr8*?Xi+S-l5_Ymv1YyDl=?+kuUI+7mQJTDQ;2KOh;%46$w)!NGkQuuTY95y0*9rM@))PA zA3wupt>t=U9V(KLIbM40!4qis_|S1b1DG_o!QrZt&lavfk)SsGBREF1h?M zO2Z~+v+^gJ@c_k6Aw@ikxBer79Vks2nX?=-XC_x~tW3Dk_PCdH8AXf&`obAve+|{_ zcMY9GwQkGQD8#SlTg2F81w};_T&3O8U^+xR7==49 zs{c+|5;593Ux*hIc{2G&4a1wo38(!LV+WfjnoGFc9;Uu`M0(=Alev0ZxkTUl`K45$#%GOzmZEKE)^+nkPwqlYh$1Wo4BSFE>798 zFPI;R;_uf^Kvub=7C1AfNR$h=YCrBn9W(*q1Plc zG;n%?sA*h;hJ+bzPFsPN9aJ?7AK0fz;y{cl#$`<9k|+;0fPd9@h%XvSJXx5vipxq9 z4!+1eMeU7C0QL!=f#@NKQyRyKu2W?_JfQZ)F?;UlETLTdzo2!#;SUTHC5EKOW~IqN zql%;lpmu?T#RB2@e^%j~NU$f+0`p7E%VD_z2|vT&MrY`(`JqVpCGH1^I9%WGgRj>41Xc$V~b-pjvFcl`aVb2j?6|$Gsx$SUx|&*mPIB2 zHVCdo*ik94ww^5>AuHtyDh( z`bz1UTp!NWY-L!uk1|{;3!O4u>3Bl|-IK^xU!@ErDN-?ZA?9a?VvUzDFo2ClyTb<< zBgMIhzD&Cy<$yZg@Beh?Lz94AGj|pil-DTKi#zv(Za91Zw0e9)k+je$!M5p;0WAM? zqM#&WP@1mLgy=Bk8dz2zuIyU^jU3p2NBGM^mz7pkriu~SDz*3$h(~{y(V~iq*?dpU zuV)#bNBJ_R5}-}4f$`jC4MGe6tt(yEntaPkOiY$$GB-TIwA5;X2$Tr{hiE92$kAgm zYW2^|J)4WerD;Jeuc;Qf z;KEew=|zamhyOzB(il3hTZm09W%@lSa0*bf)`XFrFCy(ZlG+wcPYkV-@`I|-5gEoi z*=U=mnnZH|Pwa$pYsDeurv(bjK*NA)M3KqLNjsT^9X0N}WI)jV9+3YAW_o%FPmO`}>Ht<5aM`m@4yK?&NU+5HuAS{7Dy=X}-K zVrU%2rrl*bi14fC)3x;?A$K$?f`Wm>ypys>IfjES@>ppV+BBj_@UJ`-H-?#3b+w6k zSV#!-bgcA0(PHSX)WwqP&8nG?KdVtmsvU8D6jR}Mw4iI#A2_Uuamwh7DdEW|a}>Ab zO{RF2Ps2u2F?}tMh6uJ9Go?jH|7*LW(Iy393rVMHJ2tz(ucXw$Aq~5j?{=9jr+N3F_r>!3HM0b@JbjNr$M%&R_C zDYO|{>eFPJw8tzWB9E2AY9+^AN2)pF@As+YHAO=dQL8sKKb*QN&Juf7L}*WNqxV-5 zr*~*d3y6KBPVz)Hcupip7$lWJeu{5Zy{qkmR=GmGZRRkl0g`Mp%08EQJT-N9Pfd+r1BggzBchZ(>YA!Iaz^*2e%w! zg&!LBnA2qeepoq$VGbIJgku?8N+Nd4td;SdmI#^Qk2Z&z`y_rw?y1oCRgpxu5wDSM zeHZh&y?!DZ4U?2)ZdDlBpc+o~vbWGM(X%wiVZMX=wLp;2NpT0mD8Oe+8H2~x(tjI( z*H$|qGC#HqM{z;LFL4#?9SM-qy1LCS347yln8y|~j0edq%CXu9`Xu=h`*bNe&=8$y zNZAv2iX2EV>eSwS?-3Z0b+1r1jG7G`XP-$#)CRc_XLWv1xE^K@2*kN7yT37hM~A$C zj-zslpik->ao@VTu=o&AyYzati6o{QbuTZGx&Jyw=+7_U2%Qyctj0( zAn0=>2;}IPDV79*R)#4kjpx=>%O^r`Kn3cTdEkI7xOCmYNF3JX4ik80%D{?mk_}@$ z;oGK-g|2e^zHc?3p;kj0kE@>s3olW7+?$2;@C}@0I+|vx4}qX@XJc&|zOoi`>}Z)C zSPNel-o*G^N>O#a!+fB*^mTLFjO#=4^KCg9Y|87A8>_t+Hlcy9jm!-szBfLo|Ip%g6ds{aS7 zqs>Yzg}@A989(q1WvF6=wM;y_&owXsVw)_pds^Yq@zhe z8J$^vhw8cL=ohWIM3)|gZIaH%rxX0}e^i&Kq?iqCs|`PI!un7CXm~^eIg}y8Sr)wt zbA_l}d3rSBf|G#xTxv{Gy3bd<1c z;c*P3ETv>}>&TrSNd5aNCQga+u%xT(Rf&z*;Y>hlx24s>3r>Wi~6wpGL;pPgFH;Trhh4mgUNu z(RCF@KqZaIR&?UhlXjpsD7 zDA%_Rb$-LnF-^oO*^U-75@IoOufcniW0sx}?y!^Y6;f%_`)g&E5)91@1dOktxchMb zvS`weDu4C<u*>5T>C8{t4yYL-lv~Z zb?r+Iuq)ZHbH3;Vd%l!(aMsst9TBP~Rcvw-K6`fxB&d*3T&j|W^=lt>R{+A)jSdtK zVRbLme{?@$gtg4gy*pXaLFgxgI@B>zuAo`)+M>m%uvH9b_%!PeCtPoLDS-J3@x266 zx&IKuqa}m~>LyJIEAP|~sk$q{=2749jcilpv5FdB!BZu}I%c{02 zYJ*sNnT-QS8Z;7+IT!JzzgSbt4Bl;V95OFHO~4K<(`jAX(&Mp4OJQc_)tCwPwP(4sQ*lD?_9 z&*MSobl#N7nQWz?5b2$S7^foX2C>UNbdUKkrCIH$+4yjt9B%bV{Q1xNI|wsRMsh9P zva6bRV0-Edb{{B}!5_hA&;wFoz#e~b0(KE}fV+?v^Jk#peVtVHF{ex*y0(L`}n?=47|uXI7iQJs?*D0R+Pq=?!d(cn}? zUARjPw;s<3Sf~k^I7MO;#!&$$eZW8QDeC8Xv@E%iSBpZ+S~mCcpzdtb7r0=cPDgMk z5AE-dta7CPhj~M~SeHHi#HaYYL%L5*>_D!y3r3UPhenc2&22>;mIkn|NZqs$b0^4{ zB2>T8F{lcs$9;(RrsR@>JJlT(^YcIIPm0JNN>yb{?abdc{@??F$?o^KopB~8g5bQx9846g} zyH<&p-(_KtOoBXv)CP zH%uYvF)4CVxh<0p|cIR)S=2%^Ji=;IxRyXxx)JAciu2AOi{V+?Z-^jAnjsY(607od3A(L-hae zBLDs70{cr2sFK2Yfe7F-)UZv4EO~z|>vMS@)^GVxK?56ROegh{Wha6(aZ=#)&I7>V{{ufbJK8L&0sl=$~}ibsA?~`au4L^oR;=nwCsaD&ajzDjE44m)%|Q zdb;0o`9|h2*GdZsvb|!SquiI7sTXDKGQ4G*w;)1w8|NAX0xivG?b06GL4Jz^sMlZJ$3-cY^Nhj=4<2f zj{7~647F2*W**z|%&JbY6h?Cs6$TPr?M9h2nvpf}66I{~jrenP-9!^JzWGR)uh3U% zDqV_UOASrGiNW2XwnOv)wMXRX*Xw($W|kcpsnp_e^Hm#MXvNZQ#ge9AnG5jz&|p=V5>%5G1KTb3MUb8zc(P=iuSlDe5CpLdeHsp0W5 z65HA^o~cR4yE++EDet72>MR(P3w*N=`l* z!6Q*{F$f*nef;G%QWe@?c)~csLjH~b02;EdzPM8S-s{gz)Z4jEZuN6q(;}!oh2|L; zjfbvA%hcR&lQy4%jN=`}Nu|nESDSK^hebon$?CVwuV)yU65&$|;KQ^ociLQ#btFBD z_mig)xpZz&j$*K%yS_5fo9`NLc(?km9ooNRi%R!DL!XZ^EF&V)oU8QUjZtmOt>j@wX+49}4MNgGGshl=(tFx(?AOx?nEtilT``eb`dm&0Zhjo>nvcACH0|({MYFw1fcY(*UDutL&~fg( zTu+BDFi-!Zyys9r;2FSImc3wIm5-gQ$8(3U`2q@?=^W_5#u#F7lXf!W*nOI(wePWHQ&K^z>Jc-B(TWjA z1zFr;Ff&c|J)b5tQKBz)D(teu(DR(wy~3${r;tJBgE%ga`By}nc$Odc*3QCjteTHX z_XA5;R_Ye@E=xUos8sdj=Pv{=zD@tVMtVbbi9lNN#Az%{dpDu7KQDS$RrSZV${B7b zUcQ}l@?HB@AK$bG97uUn)a9vYB>}4b;$&O|f@HM3?)n?5=~GhL#0eGdqq@tpdKz3+ z`1@$pX1j8`exsrFsZS4G@RH2jKDczLHyakB)k%=EO25s*o+tPyfLU=IplQXk)W2qJLRswkORC|D)<8 zlHo}QG0eBKQdaaqSVHu!E&n&J7FORZa(HD^s1}1c7N2rSW)AjR$)x2s(ogluI3*VF z>ZD$~`rOh{gVM@R!Ys@<(o0?_=l=$%7nep_v|NijfTng#kHT}Mw7xwuQ|y@db`IO$|Eayy;{L zMI@UXzh8j)D7A!Dr{HWoZ7*P6Y~Bs0glq>1k*xI+!SPo|IYU%wYjtC0=|vjuL(gr) z&ceU-8e`p#D(|`*ybjTx&r)YD_u9xQ0Sc>INzu4izOh*&KTKmPojgrK^=Xkmm2vT{t!9xK$ z)aje=6kbr>hFs{#T6h?TFW1Cl0#seqspO}6xGfG99!nkFAA-hQOHNkFyG9v&^Z{X` zJ#Q?}`zxXS6wbft*@o`UoDKWCA&eNSV(U%F$8|>qC|&!c0Q55qu-fslLXvB7EJKpo zZ4A}fweq?pU%>BDDw(7Wv!&XDmpUp~afW`wC|O2Up_SBu6)&ktud7%Qt~-yHp+471 z!*-zx%+J@dq>C%1JWJSo%g=-|MxeBg84)1;R713RQH@?Y*?jf<{b=i5wd^IUqYlFi zFUpFZX4@WZrY{GMUTz6HzlVm%O&24hI7B;MV^Mc^HDM(lm?7(~Hi6A49gK{OtHA!s zx2nhmQD??`#2Fr$`qa)DBT1_>Ch-I`bn$BT+x!~v=NvUm1nfB)0CJKQ%Y;2HJiTd` z<0Djt*HWmGMq-{&iO?QLnSz1QY*WG)kNm0B9AurgzBeT9X0m2xbWDst@h8z`7E1b2 zM7wa%B)7ePytQ|8Mn_Bl+9s;C8kL<5qPe2FAxR>bb9x^PO&e%(G@~j)8fnqNt2cyY zU>wPT<0Z9G4WkK_O=3rSW@Rop)3lwVe&T%_8mOI{&$pt5kPYP^&~^*UP$CS;rJF96 z#>ZP@u_w?q*jdIb`x5fAOxe`Lke4GE?{-u}43;_@rLDrCLN^x0jUqkkDiK9QIfiOV zU`SS#i^}_oDYk)6M6LFASm?PBK8?Dte0^*o6ffGcJr_;!G4%9TKRcmm^o56WKLOqb zGgDJ;%M8jm0oH^AtWn-K8KtXX%^}26v$W_k4x(g(iisTh{t~TfXBms{W!nB^jpMDB zb3gI!dXtZsoi~J9Ohn1hd}-1d-bo|##&+zs>5(alZOTVBXH3QzO{A$vi+bj@MaSV2 zD(vGgclGq=Y!HTo-1fA7w?te(s`3twm%e0Lrj=Lm2NhY1pk>pCohM6hjbWFbM7e*( zC|G5C4f!8k#2-$?U$_!vqh8e)+%RJmE;8#4RoGt${6$b!Fq0`&%T%d`&|b>_>X(@HA@@r60zn`B7iNqMr2kJuiUSY}|(%@V%U}Rs8~`3i#G0!zxs$lRBP!0|60sCm&A}e*i%LZbuM3p-QK!RQha9L zO@4>Q3^0uVF~h}NHwEQ9WlHM9B%BRmqpu5CKp;`%sf;yH@|o%~KV}H7AJPhp%q{7< zv7_3_26YWYLck+oj zOem1SDWn|dlO57HrJ_iFDUrFSTth5tQ*{w<1#5@PfyoT}iO*wbjmugZ+&Zb^xYru^ zqFAq$!B)WwI)MpQBm+3(`#s>KF;fRq_M4dOwsm*H|M-Zhn?oaaJGj-pkrq(FaQUbRww(ZY{{7f+G0&(UZ22|@m zCB|XO$RiSEe$-#_B2n)g=N<#u(al>KtAssiQrlW(!~HH({Y92A)@ILq0?>45rdUFVb3vQM9ULJ zDOt6o<5KE=P`71;FaYx@`b*}g$Z2!Y!U81=nze@eY{Bv5@Va0J zIz?hwf{a|!r9lT7tTnj5Jka9gBkFk+IjMSE@Q1=GvPx3^SqdI{6abSRd&vbQ``sM} zGMAaqwE^tlSZfh;IM|~yoL!c>-zi~0U7J9Di+wQ0D{%&wOf46*Bw$H=Gt9hSmlt3% zMxi-Wx97Pcvt~)MK!<1jjkeGTQnt|^_KPw|-HsW4`AgH$0HPfl$ubqm%mOog$WM~3 zLRn*$=+X}~XQg?6jA*O`fRylcMY`~~lrSqkn=;Ig6{e90;eSGYdPZB**yv)8fFZH6 z$k;-jWgNnh(g77e42b1K4#g!;VDm+wN=`In?}?tK9L)?!1@+J9*KsFDWr-rk`FoDy z6n@kMJze!OY}&qK0Vd^OeboO2gIW?PpoR4V@oDIE&k%P&)~IZ32y1|sNO%8v$#O0k z&cfkf?;&z@wwWvdzP2?Qh4?g)p#(85WvK-u!hkppiJ~=!WPoq)SwIoO8g4O8D}_Wp zTnf$9BCEw(7v6oki}7cVQ6j!RvRY~=>L5a>vOQchxv4WT5E!N!Y!Gg+L5iBBG=vmd z!3vGpj3LIlp9XEh(P*(2eVFYR7uksJI{BIU{tcP+$1>N$cdkE^y6seqH3~?(;rnHg zN-nZOQ*xTx)cLQJ6F@OeK&5&AKY za#u(gu0VvuFiGSi9DA1ij)iH2V2!(rLJTTeZ$FdjmXq$+K37nKVxcBmu2DGuET+gZ z{Bc?4v=vI6hNK#8UO)hOM;O{y-eO>OF~x6Ye$`~heD#O&T->i1+t}23D9f1Dd8mu{ z=}opHffC>Iyb{{1wfRzXCE$-{7*qPzIO^<{kl@zCA>)o#aD(#8n1T6?b(F%=fgN|f z`dVR)vce_(PAJo}jB5`7xbd1CkW73oY(@c>BNcPR^sWfDcxXS(w4|ax4kC`kUu=_2 zvlfQrWvM78VjYn>s`NPjeW`3sF;*j*_PX&PkwU%bMRl|=Afmk35soGMjuuVwZfrJD z7Xd$5r}L9=$Wh0dG%)WtbyTt#qAwJsMH#)U5+}JH3rj*d^<$nja>}L*C{**Y+s-#% z%-#p>a>!}dIWiDl56SFz&Ibme^lb@lYpQ?2sHA{of{Rp$1s04=P$#37sxmh`hR8zR4CbB%|)%{)ei) z0c)7yV&+1M0duCxVZ0<(MG!Nkq$e!*9yQs(>7YZvvN*nWM419ceJG^J{n274mfw5T z5DYd&kuCG1h3wkZn{w9ERi7L6 zI6OEYIyBY7Qi+qE5v#zYpP3-*rxA+m6j_N2o+B^Y_MhSf^wzSDn*hNQ)qt1O;K^_k zc#p#kH!8W{Ay6sG*Jg>q(O_C}I8B%I8=&p?NrKgZK~Xa*q@kHe$0CHr1quYi`ZS^W z*d0M)m#YqYbPq~Pki-yxMFu3?DH4>#gg?b9lbDlIAU?+5uRcOlUWCGgh|t%=#)`vb z&W0?HFDoZU;X2^aY@<814gWk}rk;~_IbNJ!>7Tp$9N{mI{xe?*qsf59I@CQbEmcXM zSI%Jr+8LWGHI5r8!mM-ZSP797o1|URtm|<@{b-3TAly9hd?=dP?ZH~^Y&D`t`ZrLL zijjh%8AsX1giGnNa48XGf$;PYdq3=NWeP@PN{E^2>=fybeNg;jnI<9*i!l}H8vAb4 zR@Xop1Tn1*9Wk5gcl7nd%S@8AltnpSWDCPht=MV`Kcp0yED=!W?%n zpa$2C62`_5qBJ!f=|-2 zys!B@hsExhV)gF(*f6o^u$-)D!!5V3F=d0<#O|N2fPQPm5%;)Ul~8d-I4N`GIe2yT z1&|dtW?P;jrnyYZFSMgsFRqFun_~B|ZyH4@p-@)TA$xg!Z7g6nxf4_4I;%&_kEhLU z$8B$FXw7gc)Dq`0*`XPwzt)}pfh-WFA5<^>`gs}coBsdG8F3x2Ti0x}WN_ZJcgPw{|9frlSVahCGW~M10 z`@SOKwY`5XpC{&`lQcr!$OV9DCB{X%0-cuB(+>gG->+beZlnMy$-`RVR&0y zbjIr&Tv=SDT3CCgfV+Z&^zFgMY(Ls(FC}q1P#LXUASQ6mMX_T6T`YfzI6_Iebgi7+ zkK2cb9+!Ow`Exgi%jt&f&=3UyDvXRD6QDS|m(D0gMzi*`Z{M{9kLR^}FDi;QP^DpK zvMCKrgfBUA#-9(di^&!l@|0civ1PZLaL<|>8neeo9J;aI$<9|O$X66CjwBe$LBjU0 zN~*VC_(r#GpNgv7HI}RQ9;8()cSrTg!FXtPNw;TcplK3EqpVtXqD16y^@qt!B2c|q>10|ITwhgnU% z&xNVyMW?Cj0WWxW&zFR=_4>eS-L~rxPQjh|6{p1VT4xfOYZ5XwelR#Sb{6|)Wkm+j zB1+~-%(}o9!jSKY11ye8n*B*p#XED8$DC@)VFq?an(oPhglBLE!>0@rY^yGQiKwEU zERUUj#V%vTk&^_&qwhmhOmmVG?ZexMcwG9Z!tg~uR)odK#Qn1^C&v`2^z3)QL6$y@ z(Oq=mJ1(TaeDXXx=ZIDtGz2JY<{O8>$h1F|OCM3BAxHpZ@h~7?QF?sS=_n_hZbu3} zF?y#$TE3c0yflZb`E)JR)~6ifrq7`pSQm)Nib7;vSmxvp2v$zC6_?Sq(}LcoE@L}e zPOHnxa4@}8<3S>nkeuIXz+o*drRwX4?^CcOo41nMps?&31LhYMC1{qwPDSDUc@}Ng zX!Q^@xL!pgQG~j?!i=E}1r~v1+_$M5l|nfS;Nmo@IY^3000Z1(%8?Yie9{`o>a3yh z;eqM$(uhpnPwp>w6DB=xE7ogmUkp=*ClO|Sj$pK!JwhKhpJH!r9QVa2t`|FFEAl~6 zst20rmrN`FoW#TTL@UqMQtO7q{P5Pz7+^KCZyfG%WVhrz?NOd9aWHJPtAE|}?-4isY1ojv6+yb3Iv zZ(?%=YmIAsZHCKodd!^E9Cn_#cRYU`mRc85&(=s@*6|5=!10M~%_O`Fz%@?^MiXzk;j{c%rL%^;@E@5+$0>t9X+c}C%Nj(qcij=4FDs`H& z~gmYqlt-$_tPfAWHxt+W9;!SB1KrN)FVN? z=?S}3lDcvi^1L%8ZbmTT&vVY>YIv4PY`xhLeXiY%nqnU&SMK=Ka_0zsZ%e7*=m7CGO7g4)LGYMpj#x1TbCyVeP7>vPRPXMD_T z#2_lJFXV4>CfNiG(`%HV{oR{p_&~8)#Qv;;$XBshGDr4Bd~hdP#?eezWP9rx>3yep z=j36n-v`sz%w8YKe{NWIyrXMI6^baNOpNO7f~!+e+zsuooFquQIcqRR$D9Sn zgP4Aa~o z@c~|q`u)gRZe!@#rwp?YL#&*G^WPaGj7aL3CB)k2KtCz$KLLwT+rm@NVAxwrsb_bo$m&W_^20tzo zD^zKNX)!^uBYu?@a&~+x@4$Ti&TI{&} z5`@KQ&@u%Uk-ai}6MVv2v|bSp*iOLVwPhQUBlscoD(5^ry8ZVzU#}@Hf%f8H?U+WR zE2%!6-e@VaGB%$(c9{v5M!hqvbo1GEQi?tW{X(v57JjqsnwX*is*!~3-|;+`ao@`& z>vjE~7{kTAles1ta$qB;=n};Mgv=YN+k=VSi6SVOG^S)TFAn0=xDk&rgYBIn>$RpB zhp+7lu8y1yYPR{yu(491kqXdR5{RbDrKrx6)$-E_wN9^eAD2^pvYH9Z9asGYGN%iX z4Q6u8kGCsyn{7Fhq=W}+XyUUZ{C{Q>%vOAF&}BpGeOHtyWW7^OWixJiE;mhP3neQx zU#K(O&hwsQXYMZMqh@!pj@P;=Esl98a17b1hTnP6r^bU z{#7U}v|}C8AYBdQ7gY+l{{p|8$w;y%PR^Hvm{T8+wCoA6b2-H>?``3;26Tm z?cI>jRaA;7e0og_cfUm$4!gB-5s7em0eVao3&lZ!ckHMPw*6f({pkiHN(#iI8j)YV zFDoUP6A?4HV(P__XX9mk87qw@-%Ly}G^zu?V;ox7kjwDqfe_p+oXT-667pmtqA;Ct zd6JMiL+UNh#SJqy#beN8&5UoJ(B{&nNupY|GK95q;`pA2vEkgtO$S5t?Qi_KbR|@C z#{raClkZMxOpW}!EtP-s)pE+@eIqDLLOgDp^y{`2XzouIcc!v0M>adZX*7A`?PKJ} z-rn4Bd$P18I2wHnqJ$;IjjFf{U!v@G6hS5COTJ89ZNCps5cH20S*^7C!tWap>x#{g zio2)$-~(xS(lCA;kD+5=)p_>O5Q6m*A?X}OJy`G>bEP;-)q#^v@UTOJ>4 z;PiUVWX2!8)53hS1*Fo+ReN(&Sn)~95t1=-2eWYnm74W|@D3@h6nO?o;}C49`lV0W z?BEDTUE<~3iCS2`hjAymk0rmIr@R1kocNa!j5J{g4MmYHD3a@HIwHxu0JnzqyuPEk zcFu@ohCGV~QyE`xCQAA52X5UT{tafg(~YGhfB;hhC1qvz<9)2(Dtd$8tkkd1z}~$! zYoU*Vy8UL-i>NIk&gZKkSVPXnGiPoq_Jx6^O2VEJQ6zw*QptYV%}_Z&jo5M2Q$_bX ztjlEeUZ>lZ!KnV5KcVhw?RJ8n@!&Ydo)w}h;o?8cBw98v2ws7bU$tt_8n)0TE&3V{ zyQMv^)%4!C1ouJ0+R(^2lQB7#gMP0=emg%Do8gA4hJb$)5pujt3SNObx;T&@vX%gd z7BB)l+yroROLO+e?r}~>dLrgVj_PH1f|0PPM+~{C2OxEFA}!U`x_l*#6ETfGs<5j=}HNydolJzoyKM{S< z{Wz)m?WHM4F;*$&-*r&g-Ta%~=LNyb15RIST~=Pa$^UTyL^bXPyfXRx((7VmcDxOK zX1y}b)aW|7R%%0WYs&aN;3(Zhc)YgQ)ib7#46hs9O;PO>a64JBJ}~35TMQTZJI&7` zsQK_kGz_55o^bAmg#~bXxN~YF6O>5g_&Qg?2{LNb}lgWCaV*UJ_Sm}q|<@VS? zIpwrA9-XIbbtcYnso-R(bd!dgv^IK})lFQ%nYWusqs)H895@799QM$8x*QKAQpa2g z`gO;Bbhfc_4Q;z!c1WB*WF5`52p%%*&5{u-pZ77_k+i-<68S<$)ewBiob7DHiFyXf+TB5Rycw;F>|!Qt){wCgE@!)Ri4$g zcNdcC$8-Et=~gZG?vT9k=G#D{%~~`e{p#&WMNPb2PO)T!nlr&Ym0GL+d4)Plu9;i9 zu^9?tu>)UsC}ITGwm6OXoE24o<4~>Z1K!zI=U3^9k_&cYYx%`W&7nrh?y*guSB)l9 zg5LpcsNp&L!$DyAV~okt%XucRXYfeOV{YqRBe*9o9GPOQ`0t)vJ@nrh9c|IX{%fPf zR&X4e_*|tJ8WVmmR1hRv6gNCpz15>F35KHe8pWI}DK-L&;c9{N1i z4T;IixcP35jAU%|BZRbgN?0-+KAr?l63_Pn4Q4cXXtzkW*vSEGQTVa$Rr6eXn%^W=i5u=7F#5j6H}$s6p6yZ z)V^;>4dx#tQY09l$O;u+5D1ycdK~m}cPTG5x0>4X&(LCZfk{R3+hxvs`i*&{fO7Fn zyDIHs*M}#0oH=j&PKha67!670*A34{!|_7cz4@8~5jsp=u7Z4@iH|*}=)v|uL+wC_81|1sX#mpe*)0wE`D(zacUFe&stX*r(#QbnL ze=QKXq(}sLD6j<@0FKOXv0(LvP|op*CjY#d1JfHVQUgJWeafYqg5YD(I~w)8_3ge9 z0*=h_GrFM9f&8!iSuC4gRybyIx)7w1v=#-K2rLctPm3-3xnk|j{q=LOIfBAhv&;|Q zyN8FIQu0o5`0no_jMecCjx!&*wE#pHYKkT&DFhG~uhYd~TD4wa_pK>`?+x;2)d;!_Z;QhCu z?hiDxF`ip}-+l|cOiH_FgK|zEZW~BN{`FZ^%M%E{@vZDc?r!|JT zkEhOc7n4Hp8G;+9X@-`Lh9rf{-Wa~kR7pXh|L~L{R<)aeG0LVlw zpNBrkf5UaI#b&Zt^O58qD*GyIbUasuXxi~lNq*phG?v56sNp!Ne7(`N08KdQ+i}j_ zcGHsYAur__|7Y~HX}dF#2PTA_ldC|ES^(WV7^)lTOyxX?9qvmu|&Un zg&I;>864?-qS^LvG`Strz5Dl90N>l*E}c$eY_(5Y?c-nA(KCE;#LKa2S$NGRUo6jD zB*sYG!QVdZnm&9nCEd6Dp00A_xYPsTP+(?oQAYDzu?wwQjn1EAr<5Z>ZwPWMQfqyW z(3t?jUTWU{-YPF@Vxjh~^I-x`|EBKGRJ3nM!VMSRpHsfW7dE#eGSb9zzx%zuPvz+i zQtjXKzIPZ^&x zY7q;JaraLG8U}MV%lXi}-PL;21sV$EDU+UA#52A;=3Mn}tMlx&QE>IAL8+W}c+bxq zMwG5!3;dy+IF+?XHfh3Yo7fEiS7S&2A=vm=#9_aL`mbn%MQL}oURs?(;2l5BA!EPc zFg6*{W|iIS^q})>%_~$PxeO*RyaNv7i{;fFRvMQhHdeA4q1K@Q~?l{D843 zo8j^_Y-Xo>BKv7FDs{!5IveLcwKC|0vN7uk>1~p2${JNNPl0oC&##^cX~4rkk~;S> zFY_4^xI1V*zGu`AZ*ROs+s#otTKSn4D_TM+-rh{8(-C;iT)E;ebr1}RX^rO`q3VR+ zL9It*b`$(UuAK)mCnqdsixN6;oQFl19l-TYV8sEw!|2X2sDCUc zz~lTkKyW;l3Q(}SJ2hKlBHjG)q8<*Si;K;L70b^Wjx(QU63f5yi+h7hTduSFUfgv@ zGShIVc{fAJX15k=N{`oIysd`xokv*ioh?b7?NSNVlxR2=%U8(uQ{e35$%W&5MRsUC zPlqnr>8}d25wBaZRcBivyT#9B%$;{m--*d8Z7P6GmA$nbn0{^^ig;c$3s5;`{<`xQM)Y>`^VymhjS1B&OaU>s=-&!Y@@wuR$ zv|j1kNpm{l&L?QXGrrm8wNklj$P`D#U)$LUstqsXeE#M)l#6t}eEs5Ae|g5-CXYQD zR)D41?EY1^&DC(e6qQcDK2?|3jpn7>62(;Yu3yn96o_25=O9ppy7f5|^Xywg)crQV z=x%2f+WoY7%k>I&bK%aQk~|kV*P;fFPB~Tg=B>ptzdeooO3YsF{d_h@rf)~w-EL;~ z{ml6M@wXQ1csdU)jXP>&5<$_~o4MxWjD1qVaoQPcVk*vk=@ImSXhj6kB1_g*JnBI; z3cS8&8Yk*;m7lG>b)QwjLrGko+aKa`{_*`{F~^@)vdtz^h~~UK%FMpTmd^duEKz(k z>En#sEggO=n)m!^;#k z(&FyGVUE$`tYGcyv;$A%3#Lu_yxDcTos#9yF~1`hw+pVr+cJ(8r;N#Ia1uTrevhL+ zaJB)P%N=HmU@q+c(e;*5afQ#iC+^nWxLa^{*9IDbTks$W4#C~MaSIUKEd+O$MuS5F z!6mp$a2d|5Idkuw`Ja#dWv^a)y>C_Rs$V^i;F?$HiUQ?;`xfj6Lcr@O8;92_~p zN=nJZ@mQ29{f0P`2{I>_GsouaNvqjL#+mK1Z{k<7(U(89m5)V!EWa85Y}*@kW$+SR z?F)Y*_b$Wy(|ieM5$V?ZLujbdUx7w6@|Zz5mQ*nMO-z5VLstaCj2tE=^V zKBfMRx1}FXBuY*MceyXF`QGGQl_JGkUlUfyzs!sw%S9I#JFe_=;~P~vL3+`*e1(Rr z2N#LWv?qkhz} zd@`jseoE27)&zdZ8uv={M zVfJ}06??h^;b;F*_^vy_+|`;V+07dRsggJm^)!VwCfTod=ImhVnn!1^Lbs3_tRK`r zl&X%s8+5yS7R}RA*Xv8%G$t0eMDksr`n=rifJM0)|LeOhkIf<%A;)@ebqeXTZ{v1> z4~nz6>PbY?n`|0pII{DQ@^SQb+T~z&T&j^Y{o11uI=>U?Wz6w^4BzLyG;g#3*anFP z#CybV+Pm*Oj~yXYXT?d;&ZmI8$fn-&;kj* zC5z$HAPveR`ry0`;JPB}T>habHFw1jvKND=3RISxpZ4os?3bEz{5RPmaQ7!m#Pb^L zCsawBN%~#q7+r5hR3&mdLNG)}EI<#N@-crr_>_92+cMrQ>vx$m7Y%iT6>YdH+(U$% zW0b{V!J(-7Egs^7C7<;)**5Wt?OE3Lx&qH?l)})^sSl?s&2G9Y`{?DL@+s8*HskYG zRL_zqz88s5?K#aErQk`Vi^Od(8$h|l7)wnae$zT>BXpZ9o=Y^tBI8T^fk~(#%9zre zlU46`K#FrL+P9BRM=b7$xQB< zQb`7@#*xxFA-r?;j#YDwiO3xQ5U9j8(?M$t;(0dNr=WXXR3P22a(L+K_o|2)i zzOHo}C1AN);AX(9dnr4$G_+C;<33Ri6EzXGXWxtdseo_3DFWLP^F3PW|L z1FwBUCuKI0rlg#Kd&0ULudywX52=kl6JGZ-(y1SXM%s&n8Q&YF68~BFE~Yj0!z5^~ zwBGFC$bQW^qdljT*JecuL$xZOt0z(QM6`a}34VAo{9v`4ds(omSOzmGITxB{67$<( ziK`aS`TQt3%X2Fyk8?6SL-6II@3xh|w zj8GcL^DzBsA~LjcUo3lT6)z#k4`Qc5bdP;zP-g^;)PS9u(dTK zIT@I3fW0r_h++967ysqYILTar$?|VEhu?4G5w+P49fh~Ahrv;6i3G7HuE)J8tCNSg zu+e_V`=ad)>1*4$#1l-(EmJuZaYiwNpWvM!46(Q7>h{rXEZlrZ~S<%2}9NoN}M39(5(HTX=ZzKtF`Jc2lkML`|- zW_$5Df8&pbD|J{&cwM-D>V7dFzHmXgu~vw&kGU%0aPZxAIXrhB!+5_h*~o1*NntuU z{we;={#9xR{~zxE#DRSwMc_|{Xs?7bC54DxtNTV^kw6-D^z$QJvuTm5Cy}^>F?u|;K+n=f7gty0WFdp9eMTS0aGnUvb&Lk0-8Mo}Ap-HKT z=EcXipz%pgC$w3UDf7WS3Y<{QDnL48vHz+4A7$UA4jI$xjc9hX*jG_iw5Ed4;pv)> zR6F+FU)JFXIgJ#h6_F>FJS?})15&ev_bYayW31O4xSk^uN*tcuIC3N>sP<|Y=~%O% z#$;T9eY89V|L8pz@fz!Ccc=2EpdiLtd*{j?KW+BMuH*HOL~ks9y!kXyvg!!fhJ1pc zexX3If#LdWE@mJS=S}%addr!HvMVwAkaXc49c!c_h;g>r|2=II&+lqu*v-jf)S%nL zzcZBtlhr?+&UB*9j&ziQ2diaN1Fi}wxw(@w4d1_sKk`O3ZGI~&yLhd;rJQ!pKKQ!v z;~YroqKX8chy;nI&)^-TM@brM`<1-v;3;|*YjrH1`_F6Y&-ORr2AreZwPw-QAE&y8 z6veueK;|TT=G|-%Eg&kJ%A~yZ(La#j91oJD0!1qBaidPg{l~KMptbtAdiL{Y8B!a| zLp$o53|yH|z!MS*N+BD4{EAxw${e3#%)SrnN$?=>Lsdllx4FHIQ-0ROtgLX>50e(l z&9~F)3z&JFQDwQ^R3sz^UZ?B13RYH(2rqp0ORWIUiA?05@mm`O#;r?k1UoxBtxg3% zMd*QEyFK%5N6VVe`^KY1v{NO3e~M=58b{KD?z#CXiMQ8&a81EeHI|p3fc*&z4wY~a z#Tk_>6F!3WrdR2SD+u`IU*YiGO9(P%t+o1J+N4+R}^~Nl$ zLkqav{xCyDDn-p6i71q&%qdmggZn748b^VuT3|hdz-GvYsep6R7mi5;dj>|T9Xt#z z4t3GZwCzd*l2A>MOC6*%2*j?&c{4=ZS+q+i-kb~GG}Y7ZO{`Y=akB*0;G~4TiX46v`cNq)$9iwdkzAdR`i$T=` zB^#wUbblB`e&tmXx&VLl6byLIN}mY>v*k*X-LkPDBe(wg#lJZxtAIB$B5kHF_p3tI zIvSbFZib~Xw=JkUo@3=|M0{j?vcF(}F)1%Ew31iU>A?>>Q2>!OP21E)j>djYW~W5y zSi%kGytL3ylQ8qhj>~sO6aQV!4D?r2bq?e#Yv4MD&x*oYUpvJ9?z#i;<;25S8m9(NE^X@i{_k zi^1Lm4M}Fr=4X8t5RQys8*guGNmm^9nShlhTaw)m{JEE*M{FM&X`9hPRP>HrN2>1T;XQokA>4}T3@YLZ-5!HPJBh*Qp`lE}fq;cnv}?&b{No2eDD)l}}#lQ6H-gA75XK_OFC zc8r@EF;R7+*>3-@49NzB3MlhAed89a!v7d@d{naXsw$~5elhoF=PLw>9svjs4FXHi zQUemv{-2+{4zi|O=unI@IC_LMTf)l9NS+V<%}^Wk0eP(GlgPsT+NcbM6q0vU2&TzQ zAo>AFF@NbxfGPG5MG+YYWc(Rkn`a9%*pk)Q(#sM^D4B$90a!F#YS2#;I~d zD8s-z+N<2%tb<)aI}_3qG5+`E*uDjm#`dHW%#tIBLa^TgDzC+I-@W|dR3C>#afi} zbQ&hJu^R>!d~NWJnKT98H`pX<;p;Ve75CX@tm?Hk>|!H_xT?MW(y2o;d@I;S^g!Z?$v2mboRx9^4z zftXA5rxzz3ZOk>;A}Gl+RoZ$T>NehKq(&GHZf!F~>j9Zjc1}OsQ(BvSUkWlC%J)71 zT61p$K)j;Ma)(H9mk1eYdiXZxL4xd(3DBy$=NCnGgzUff-epOvJDwQzgbPo_uBuEFD=sQrSd8KBVRuHEfJD`5R}zYN@89 z*Bn^8Zcsc^M3PaiB%Q>-lp$&~v{Fb@+QF*HcGi6CZ`$xMAts|z+Lm(ebu##i#FM7E z@B-6pIg?UsR@rdr?-FXP{^^L1I!UQXP{oqZBalfc7cS&BWRZCCNBrRgZ8}mY2={)` zCFQ@b_y2L*gKJ0;K&lWotrI|?%-M4^idEnF^I5e;CJ)+DMr#tSGXoqD6izpvc=eLMl}MfPd5>i34fP;q@q z{gp?{$;AdFw#VuM2Ay;LBu5COSj_Mv>W{KnGD1wzl*xo~_63#Bcq*#XQ~n%SiFKGe zgFMYW)O^kh#!fyl7XNR6jShC;P$;eV4Q`YNa8;}J6*~dv-Kjk55L#85O zdU3E*@RfP!prFp~+L}Kp40iY3Gmq8%%T}Sq@1Tt^f*Lg9Iwq&^(^VB|hpQEN*$&5V zVjg%OCp)>D2q^c$V{0_vu#YhNpZBBFH6fj=4LM@#tuh-!;b6hzF$yn=JnJFL+Pa0kgf-%yv*|{9`TJ4|jz+her<+ zv}v%_vbLWgni2IvI&HMuRT!k678}j!@<{Z?g8Kp?t)VraNz`CGxk-Nn60f(1)qVY? z4vFxPC~raE^zz?8ghI(3DycmLf)5h>sU5F3n9>URtguB)F_9;No+bDt#F2kiBAY?1#x)V8U0t=3_?v8km8Xhd0dN;we@ z(_%ow#0=Db(};ykk(Np^pwPZ4I1>w`Nz2(dPa(%|C?ZZcEmQt3r7_3wAb=f!umO zkFYbrvfHkbQoe=j(MMEff`mMtsAc?BQAbt7geW*-|KtIxVSaJ!N)yMrb7q>P7~b}^itQ#Jjrh~Lp)vNoBD z?KeqSrYwUaE0Kg`(@;;QwYNguR0}HXXh*VyNz!kf$k{?Us1_LZkeXdLxWi#&u((8! z78(~=szH5B`dFq4;Mm^E>JhsA#g&Zei_F&a2`HWJW#@P6C(H0Ay}pj=ns1LTt)evN zt@4Ow>P|hZt#m$OZdeZ#;wd!MPzlSWby6SI5IWKqDI;UYEi@?GAZ7qx?#B|t&nAgp zqDhs>59OZ?rbnSa`wRLDyA?div*Qz)h&i@e!t`XkqJ!Z2C~%e;G7^~{LO1xn{Veir z7?+ksTrK;l=yK&G!tFowD6+Nca5_|^x@WR5?ZtezqL&&b!}Bq!E^hV;qd?>Q2YKga z?5~io9epI6 zunm@%c@r287rUN(2V7sMrY!`!+fewo9+~%gLXufl*pj6TK9o?wY-(_OR4oY3du* ztUzoD^aTnaRS-vja2j0_Grc8WMD_daRCgAvEkTGfDSo_BbC?&i>dPKl=9BJ}%5rKlkkVIJwftVLJhv zk;zZ7QtocLRT{?!K|1!enpX=PCd8q(yKeltss601z#Jr;f#IpAVi_z+=gx#m{F~?7X1oAiR~Doho8l(UsnquQ_+)gDe#9>@oLkoIOn>)WHU&SB3b8C z0WIAY2;}1h39-Vj-q(#RQEWfUwQG_x4Z_*DoPK#IUWW?gTQp!DV9`6)StVJ14S?wg z#e8Ixkv8hMTd_cY+hz5dwPN#djt?}ro<>O13~WbiWbKT8q)dHX974?~jusxaBon~B zF%Q}f`K4-$CcmH$2TditerE-cUu**qq*e~l1Kwntq7~-W%uUHVQRT#_fH%sjtCcPy zA84#al0qhF^Ekq*Bjrr?sOgiFLsRNL@LfG%zDvK$eqjW{mZFkZv+Mm_ z4>f?Q6rF66RKs=(#9CTAgafzR^qKDV2eLA(>{vn~iA3tMWd3;$rFD=KzJs+0(Z zWX+06k)&9^y!E*#`qTj-HP(N(24%$l<&@?9&dv7HU;p1Ptp865#()6Qhv0Lz z8ZpX2Y{1hZ((Bha;+hbAu_(MF*ipk3fTnf_L^c2t!;7@7=fT1eU}`RNA!D7B2q=!%1G*aB%XMCZg@PTbUON z?1(x)D*6RN&tl930BDH&h`5K20QKbU&HS%M5&R`NHfGI}QdvV2?HTKHCa;)hNHbtdzE^D?v(3 zTy|jUqaK*OjW#8!d2_bBFtKInw3RGW=Ba_1Cs;R2bsp!JmA0~8-(f(xd3btxtxKHjO0Yf!j5Yg znC40Ic>oU6O&cjEJ557;3{&yfWv{7Kp_hEvV?JmafVc|3ep1M$+2KQXCW#My7iJt& zw; zjXVCZ#C@x3xWgbPK4IFa{Kaj&HNx~F0Exd4bw-a`@Ot4|DyZPdiF(14-sOORk#^BP zFhPS$1RXlQisg(~M2U{tI4rE`NbRbw3?NTh0Im>xjxn$PtgTvJ(4a`;EObe3H2IW{ zM(!!t2dY2b{B%Oy3&b+MwC-;TgA+&D+K38v- zN$g&MY}kiC*v`#tXIwpzcT+gVfqCCKejs&^G)jjp%resYuJ4O9S_E@zgu^zk5>oyo zwAv9OLZ2A0qP{1HY-8-O;9mhr(yTjy`k3L)VFDBM?0pdhbFJGbsW51(1sqd2Oq35i zHn%2$w895>AyX)t6fay5FAU#Cp{VR=)am6^@50FT@YN7^szPK0U_$>Vs@QJysBd%56K@FR9Rw(w4s*G2v#oknuq-}zRVdGcs9H;0Hx zSC`xb*MWg^XANT853(^lo84}6w<4e)4NN+%2jROUU~ax!Mj02BE^+klMlxQ+TeKJnRkr}#xXyU(HOvJ zN~gZ{S(ZE|5Vt_0KOZx5U89sd_UzCscc#uGEoTOQ3cB+>U4G8oCt} zoH&O8OynX2lO}lr$WGq=A?u+lP8`*C0WNmOwOJ{$-nHNE5eB-K zi72UU!dNe;_po-W(RQO$PH9jF>yE&St)UUVHK)lIT)S~|+U?9L!#o|hI5V~<9kTgsRn+B}g4|fSZjZE_HK2ic_ zw~jE*U+7M`1H1)UcBdD}lIQ7&$y8th-5Mt>cL_OA_ydTfDtnFQ7ro`v+QnvFt&Hj?|p}kiPfsHCIa-hGJ;gtSird)&aMB z@{)cxR|$FLD8>cOk*$Q81Jxpqj6S92=Rn=Nf+Ny9G=bnA}fWi<3Pw>eWs?JKAo zssee>JZN2y;*;vuE+n%ooO<~ErlxjdB4#syCWY}ipRt8w zGWMLbiq^1b+(e~&CiZB>w!&e~j3HRt#q!N%Gq_2tCs^h}?LVAX8w^*(C|ev>^~Qz` z#a8$3{vGi@Ol|dT1qN;tYd#j%#RzR*EHpb}FCE1tY#*6?Nv-J;7L&i67G$B1$DejQ ztpa(TC3&^sINiE_TXNfyF!bILjOxvQKUI$xC$A47H^_j7t~~T#p>XaBoXC@i*~I)* zdiZAb;?RBd4YeznLWrZkBJNTDy}Ref+uEWc6~~P*7oz`fAN{{i90WCmvwp8=xN!n<6Fq;(q+xdHz}$&-?p%&T3u|LY27O1R(y22KIeVqtTpM zgN1Ox59~P>d|oatAzt2p8l-b?VCEfGdiOB+u|zENKVOai>i=8NY+z{jIK;tfSSmrh z&wo$+4LAx^X!)V#ptAT?cgO=k2Vddz-Wv^C@3876n$udL^5vMa0yoJ^{+1+0qieM0 z)T1aKR>L8)(BsNXKbCg!Ray!In#WBn)bk=n$R;>fUYzK~Ydz}!?mvtk{_B--b>k(< zY)wewe&}q~N8OvqvoQ~p#env#UZu!v#ckhwCIqg&(1n9%DBm8m{cK(({?@g&yBqzY zMvW9eMl*v#AnkE95A7K&TX(}ui8e2#6Ubt@6_!gilMcp^(?Gp)V9lQ=5KvZV(_+uT z%@=y76-lLqkG*?T5VGQd#iE=Mjf2So?)$t1%X7@sbQY6pf+?<8xTsj&I%fB;rfC)H z?U+wF4;LO$DyJM8g=!IJYL@Yc(4D?!ww~7=yq9eob12WJ>JLqDwArd82u|=lPSRhE zM9(e4IE)sJ>iKxl=k#q}H# zhu9I#U9GJ*@p2o2ha9cd_?ub>^G_?85y~Rx2}*v6KY9zd-z>h#rwU|RJ29O_ElNYW z$-g{_~jEd)_`P|`sH9FE@VjCNKei&MnwOGXE%69B`KQ{IC^Yc5a3R?dI zys76M08qhP#P67p4(kB*f(6BF#H-h7n!=XzVtei%iX}7s+H~VP&j%jr^Luot^Sh*+ zKYrHmn;g{o6`=N9nZW4~X;+w(hLB#JB2z=y{_xQl86Gf4uH2F=KC!$m5%ONYE`o%E z{<1Kgkb3gi_G(0Cj}i#9A!0dp5Ur%ft~IZ-zX&>})ORI(XRcQYuZZAk_)Tm8_ci^qs6iGo|B;{;XgcQpO&)e(@LmH(ZQdPx*hl+AbRv~x4-8NfIWg1%+syT~U zULdNKNqo8Z&yZ3y?w$9H^gNu0`Tas7*|eCl7cx&yG&6sV9js*}wz>tcAx3+lcPSlw z7xg%j51)ZR{`Xuh3n1WVfs)LPgQCO3H~-}bJ~SixEK?ZZ~NtQ z-vVZwy@Ta3-vovbundeUMMTdQ^{o+2v^JUenNstXB0fLf6Gu=2&GU9ZN%mac7bF(a zah4L8l{>+RsJW)@p~;6 zhT?g0+pF#K&-he%XRo|%t0G=WzNK$D5V3C*PEsC?df(hbp* zy6Bqu%i-cY=Wqo~s{^t+5aN>HGP;B;oUpisp)ONC5B)_WOI9m_+cZUG#zRKIvJT^g z7DfdTeX&d|qZeH2cY#Sy0D0(7D)4ei<4W<9%n&^A2v=E!X$f3nXH5j$_C8CyB7JlX z%X04b#Pde{pYnGrG7^Jw9C%Bbj{uN>^DC;)YAF986o#YBJGO#;fgVi}RA@c@175y# zy}rPXluyeEMJl!pdrUxX1s&=JqryAteTw;kWm$N+7_!bqaK=voReoL+aI?-fwv|GR zNzY7SXHjxTUs60QN;&vtH+WFEAy9M zv!U@u(ll&1`O{?=RxZ4$BugKQZAxo|e@c>3k81ZVxY2<`M+#Rz>(NKR) z2^qMm&%#>>E>DPK3myAwE1zvqNDRm?3vDfrt3suMvy#x95>%<^ko+dOm!CO}755li zQJVt~+nFJ3!nbaZuQiv^(Y~w516W!To9Ki(y4HlLIApYn{gerVxvGRt=ucaNs@cp+ z^M`qy55M4Clrglh1EcUyshYQIDNmG(Z=4wI=y^UUTL2~1fDeU1z+lzvAK1k4`tz~- z(>l0(k(j!vT=;UsL0?DLHRc7lIap0-@MgJ=qfZ&~0g{ecY}sv@8CmFwc`cPL>rwLI zhR%id3{fc`^7XklKeZHbnWKn38C9&$>7B;1PU=a?h;lEb4yw=LFETU**3~FUV?N=M zNf1PlFtF7m43lJvhUJTjJQv~v8I@v^7QBD{7xMy{!)M2~P6i|yiipCqXH;guw1JOI z#>h3-kf$Q#uyfSPXmo7E&b+9w{K4G)So%b7WrH9XI`muw0BIDW00I^F&GnQ23DiXu zUFW5;=*Y!R5f-IZY#HLHvM8|5z~1{ZH`CG6BK$0cS74Jq7)Hbm59mE#dX5KP^9ez^OehET5L;&S2%%u|An7St&&~}q?*GP8{I44DjkE}c z#`cB*fjYk6>-wl`##nw?){Kaf`#5mx@_pwO98GU=z||*KC}3X0v>a3P=f0 zCq~^-^P@=`!x@CI@#~~@Nk5T0S8{I|J8aI=&yxeV%n>LPh-ku<=;0{`A}SZB@e$#_ zz>$m|oZq`&-*^_)^iU~|j_#jQNePh#Z>9yfT-K<7X=a+CWenWxW2I>Irqht+YjbdP`122pmC5Xy zQDzU=V}|VGD3(6C(QvG|Gi&;oc@zy{8-f%d8k(-AHT-vJNhzmljxtQ2Stm0B6J*g@?YpLEnS<6DIi60JVNiWhCNm{uo7|<3w zkCZ)cfGOGnX&p=ExJSDkysFAZ&EUGMsGNy3c zTBDM-l!RJ=T%|DEcO5W>Ws)CtRLg`POzxX{Dx=Nw_i+oE9(yX=JajgSp?43Pwu6b} zC7-ApBKY9rtNw?JnpQY_8e0SdYQ-G~Qxu6uPXJ2E+X_st$60^D^%27dAjeai+k9nc zY9)=MFP()`eN2R_Fys?+W+LQtlKTO6ZgM=eT#c?jSGohq(9>&P$H^z@8NgMLq$H+) zqtA{(?6Ur1E9LqzmF_I;T!}bZ?p*z!LfI)Dc71g8e4|@&iP!WmlgLv_U|<(p$gFub z_ck)AF_l~))OR*#aJbd}__FXnAx!^u?VPdz8C-935X?bXQ-#llqOyXSr9kpCy85_o zW0Q8#kKqXEKRIk$s8M@SwV0J3bP_W&qdUj_=*vOuIZcY>Y1?d~hz@3yxEx*>Qde|>=@`{$+zvnQTT)?#W$azh zr_*qXB6{cQy8ZiCRN6+7>>4mq{|=}oOaxLYk- zpkpS64XWL4Zw>Nhh_}Eg#ODk_Q*?;YvbgToD6GaPs=shSAdH89 z)Ch_d8QEWBgQhT7Z3n|K(`SPHK9w+9xU{42lFBN`^7>c+!ZHj&Vl*A|cWszu$6?O& z(UkByP_y2f-t^pr{$n;IKIDJ~RQ7+rtf{yFyzr}=q$+QlP_O=P?=hS0Z}CGxr9k`R zQc{v;7^;9|Xhj>kP*Wh;q@cU$VO5(@_m{VQ2cdXXR$6jc{M~MWg({cAR!}nB7a0tt ztFCkJpx-ec@NgmlrBWy^YzZ2yNlSGukmcmbpE z6q|a8hh&^ZHIZHp6lsT>nDpF%Na&G5_O(~o#}b%WBc z;VP1yj2yDwguokw!_gPjI^M&kQ zd}j*-%Z+cKOs+0)O5x3gWqY53%k(>~L)DOQBXd8Lfxdqnze0lPa&S-KkFhJas{ufj zVmb0E-43rvj(GFO;WS`#&{Ge)UdT=qkp-InKY&pGHw8(}l@ta}!kG*?`DgLF@SR@A zCxZIW1GKviXVX`Y;}ur~3S)mRY!`xL#Gy~!{cq66zoXw~ed8Ej;sOu8hZjqt%Xt4v zOjj#^F^!DMW-bh!o}6u`h@;N@T8$w_wn=dsd8d>8NjZB*bNLkhSh=&VOIz5@B8s7sZr@nHd@Pi#OaGczy2U04t>FykEi;@;wC>M&M!TDQmlX z`N6+!UBSf=a)6a=MUqpr&UZV$(9saX8f`vj19^tyX~?lR7Z17?12>p_9++ryQ3Xn< z-QHWSZdNI`e0b9-$jl|;b`1+1(U;dBSR|>u+f*Bu3eY9>=@6Ozwc5vQ6Dva~16|A@qU#cXVy0nKY2B%hJn&L8mI zJamgLT`=n#tf=$BNgGh2U)?32`nn|vuMP|^$0!J8v^|QzTv4ds6&84p%5Lq zyB)Q_*c2-};UEhs(7TStRtYYeF!15rCr5J{jlFJ%MUnajm-FHE2SS8kINGS%-1C(; zl>h^S4gjxBEb!fw{oos$4`BVkF_1inng8xCF*`XnJ>VJBezhLC?B;BEiQmroX58lQ z-p*Fp`YTR#vwMM>;s%e|wd)U!w|3dKC;8V8H^OvIMQ?o1<%G&F<`q9aMumoS+Q3N> zdi)=e6(xO+UbtV&aQXsw2rB)s1}|mjx|nM8BAR}tZLaPGhSW!oX4KgOo4<(0PvRig zsSCC*XKpGgN}HI`RB|l1fMMg&if9ZBy;PJ^@9j`+m-z9CJ{5JG;v2NnK_>Hpv7+ni zWFczrLlqXLSAPXn{@gQrr!3c3{Kg_@p8L2LHw$h}*QM+$XUC2r-)AI$m3LO#M{}1& zcc)o#LH;L>p?O(Wz{QDew@?Kxer7W9z1J~1V3FeD#%>rRm|qawR5E-s$CXx|MD%%J zN17c=1WcRW3Lq+a>($zV8cSt`I?nu$9d!ZY%rf4(BKEuy)w^`ojyBS#sr={rEWl7g zkq+AZQ`O#LKS^`MujSPz(srWQr)Er*=;Aa4T0cwaM1^~22#rrZs2dx{6^CHF#eLd$ zqY(tq{Dx8#c5xKKDt|P1x)5Gp$-xH|%neNN+wEtZUbV6~H1>v|JplHt`$j>G)47?@ zdK<08tmAM8nCZ1!enAu234r2TA2n^xDr96VM35gDimnZUv#ybd-;EG)Gx$SpD4#;< zkb4(#DcqvtE%ZM%m6iFB7*Z1<8wLVAF_?9S!4tL1SlU!wv0v@VsJRDWk7p-+_gCov zoEW|k80tPH?(Pqxgs-f+rVEB&Xlcqf*M7t}pK4;Oeyj|Q(B>1wHoeNkfXVz=Y_kwF z$TS3$j_VZbatX6(CbVx2LTHkZSTO|7W--+3E+xkG>nTM&sHFCi@}%P23sgp7v!7Yd zd%my?aRy$IPCi<0e^U^k^*fd8yu87g_td&!)!;Avh+gQlWdGDxs&C!yM0}?Ez4f)? zOQEuGw8L3f@XA3?Xm%AAeuabe(J^f5V*ts-4+`~0B#zfmy?jM)?$t5Z~c;GMUbYYHY&W+j3eqI)9V$y?q)a^mVut#7v zT;q}N=R>JmJvMyf@aSZ^?j6>TZ=}ZTUNuiaGmH1IF?vDdSpwOx0#7>+?MaWJ6hm!Kjaf@wgK4MRwm(%Z@H0(-jc(%kO^A-+yrs* z+JeH5TBzCIA!9eWXBepm&Hn`#ds*CVtS*>+9`6lJ5RW^(Q3`u_RL|ZX=KSb~zUm_( zP^}*a>!x%I?{8fqleesoF3kkpy-~lqYgb$@bDl8I%K0aRBwT;LN+RJ&arCX^ zV7kdUma@6`8_!HbOu+fXP`=pUi+f100Uf52Ew2N#E68d_6FuVOfmO7qcJWn)e$DN1 zxo#RMbL52#O~T&+Q)Y}iaBI4+G5I1xI+?b&n#{j>3@rE;ceH}_P!q;ix( z*A6l74F6mIjHZ1`LZC0MBr5+BRP`NIg2W5bDQnE;VudT3no0P<;GZZR|EQ0h*8+zI zk*(1ZcidK^EMlE*7!QWF)7e}EgLACv)keLz-M19SJ;}i~T1w~RrQVByrd071k|Esz z7?-N1n<+wgs_26*3m+6A_{6ikMN8qSld1JKB+)q5lz;J6hgQE6((&_0`BV8vRlX;} zi$PbQ{@el#|6`8?4E4I4dcpG$Lf~sQJG?gcn!`H&;|?b(vBLPauEND0b6=x#D;XV) z95dtOfm=VTrLHLfDmB}ndR@3(OlmoNVbstNBk6ek7g^rE6lF4Lam1w!`%s6+bwLj8 zS1d8x-MZKFdQ;Z_#ZNm2k{XR=!-BP6^iQox?d*`28@bL$pmWAFCcK=U6;g8&@s}kO z6gT1_hmQ(*{lzlz-=@P?&5iy1UAOS&m^)jnk;*@b*gUL(zzqQr##?G{-9Wv`|?kN^*?l)8QRZoFpn|{=nA7AJt!Fe{qG;J6@r-QfC>`i z3hxXtI{1^-xVzDWj|vw4yo#G!tj!^?zn{eJPb9(9dwst!%edSPxOo~t=QG2R=aas^ zcpChF35d26DD`3nIwW9gpkGnMgtYN7nGZD`DrhuG|5sgS6%}W&ZR?;7!QE**IKiDp zx`Vq0mp~JO1a}&D3vNM!JHg!vZowUbyE`2A!yWgGJNEtG>g9i0V^!6xIlonha!V3^ z79PHRyLrg+ep1i* z-SwcO>c?+L1wkhrhp>m(R_rw)djn*@P+EBfUiz#DCu1yHd!n$^@n|}c4;k<`nS3AU>+-|L$K6mV)FI2_JRaqRf{xS!DKRD7HF{r{! zR;HBxCWWw5krBXTo?j&Bjj}uTQ^I10{4HC6&Cz~EtyO7=49g}!SovAR_%l*_^^UlZ zqn;)UrI1Pl9gK!>6m9)_&S;>UfXx1)o4(bxi7>`a zCm?|ZT75kIJEi&w=Z&quxgOmxX^V(6k`V}UcR&(KD>+M#8ShGujeI>Tyj}lW9FIbe zpG)-tSowUmo1&%`N+3UUwqMOmr)U@n*>>M62O)GQLktXf+3WACeTKI}4FqGpD-&Hn z2^}7VR#c*vkI0`6JV6plChSQo4Y-M#QH@i7Lm*8-dxG}J)4NGsHz{sk=jiEn6E|MA zi|@9_VZQy8g~54xDGS9dcR~F6?(qM=uBPzrKV8jokwIY;d>@H4-+KKmCBr(m;e@V& zROo5H>Fz48!G7b22|W;OB}gXb)+Sa4+azH%;==N{#nCUa0t=|1bH7a$UGpKJ9*wy# zbc%RQ`{H^nm>{^>JFh(_UgNLr)QO|^g=d+y8i$=s| zW^Y(}W9ha$ROkE&FzPsSJUSlf%fkYN`qwfFEu~GasYRF#DCLy5VHH3mX~IlLV!iVN zd%IXGGF%c`i&QIuh&2)3uJZl;L@f?8(r%0Go+x)l*rf($%o_El5n;Nh z%}YX6(18y+wU(3xifo^h?LI6n>e_F1QtQ9HbS8kkgFX^5`Cd1+3#bNyyCYs$bXzE~ zV8@37Pp+>NiO})Q5+jr_GaFZ387Eu>M@8k8scNSctL4j1Ld%WPqDm2@rDWg0UtgYf zg0Lo*|GrTw2 zk@ar}dcKDB={5jPHD2SdG~2MLZ#)c9%fDg;;0R4Ga6v#sTMZc}0Drik1KV6dxzsIk ztk^5rpP$umDEJPl+r5_bqzBBC4;w~s31};`IR|12sSU`bMB3{sSunsp(t=LCI|V5z zAe(W2uTt30F~p@hw$ZxhlUL&k)9gqJ4OR@vLjF}=Y^S9IvZhkjwmn(v?KA!7RTqcc;?P& z*K^*nf~?*6{mWT8%f!Nr9|JcD+)9LN)+@nK0fE_lcdCGUHO|XOs*E@=`p5@3y*t($ zmng5gW|`gU#oo+H456v&#$t&mgKbZZvv->aS zu~#->G$S&&bY|Y=7eZL7`dQ~JeCx%1bODGll1j?p}|nze@_a(#h8YDJHfcP zg7)baCTiyj){76$ZjtO>7aJ^$nMpC5Oz~BZdDBJZwvv7RT)&_NoAGUciItTo`qIqa zHnO>mi5sJw<^ocAEmmnyWfbW#Ez{ZMR2e1Z-u-8Qiu(hrsY$@#3Zp%0ID?uY=WpZp zM1NBxX}qN%F_}>drF>>PaZz_}w-SL^FJxDiL>>uVZw!3WH8&?}m!rj?Hfbf3t%sHz z>GZQ%Kvwtr1mi&i`j0|Z-`BkmrWg`8&H;ScrEMg3utqdALiYYLSyZ>}?c`ViXU-(n z&*)gD8O1};8^hKH@3ndq7@%IsFgYwe&{HZ^Oy9hBBj(# ziH}SK=G?o+3$0%#%EXQy{$aCg&(h#J{&9^%V$v)&=^>KoEP%eC{4P-L(;I)g5K{Du zrW0=LOY z{9I;t=IIj~`%lq;(p5T2yMDQBm%<=${Xx~o%9<1b&n_>KvtF}CrwbGp8yN6b%!@xe z)oqtQ`E5*=?<26p_x$y8>9pff41XnnO&Lap22~!=1KneGTOfWrF$n1yC{;C>i>N!) zq9(*Jd-bf$Y0JSx(DlVDYIgoef<4=7&Cd2o2b?EaMBb48W(jW-le*kUg=Y1&mF{ev zzP_BQF4=5=UI#$keab#z4Gk2ZzE4@P>S;tTqGhd5r0Oc9w&Ab&IG`PH{Op!LJ?>vQ z+Qvsjjs7UntOdNvwwXGC=n4UKVegKIINjN#|ls@09lr zK^)I-#$QRk#-mjmAAaJ|bf)b~2PnUqlP_KJ6;yNp)R~#jx=XXxe+*=T$E(-Y>$DAn zKi@FEbq6Eq6;D{mu(nXuQO;{)qANhT9PFu#!0z-d zg%`%@^RRO2=rndAokT0hiPSEGTf;qdHVpJ8`uU6y`hXjo6d!`2cQW7~$C|OwKmj}! z@UJ-eC>ah|4MZH#uX~q%x)N#Rbt`Dyg&3XGGKv%7rwcAHv zhx@hrm5WDUtN`)gCaaLG5ndAXzvtB8!a5rv#x$s5U}p6&QlpQ62rcWr)^IMJPe8RN zwxVV?>@2V4fJ|GAwU^7$6fG@(*wcxtil$~%+23uq&1&h`l5`c$cVMgPZJ*?`+s=Nu49eX`*tNv8jn~!rp@2^XDw5+#PnJkCDFP9wYkYM%AXtpx z@bq&)v3j96x7i;#<$;p`V9pzh&?MnQn$wAaNmsFxZL?^y}1j8FNqgMjphZDimHg7zam#lPqAX2Zdp9$g7lW_&St zTqE03Xv%`UhZalJ*V68TzXyuk9A|6WGdw$g{<>eeeARiTyW3V2{~OYVviHhV&?XN&9BRZv8zu8@M9tmT2qLMrjk z?~*k1EWF2z@Nc4U(}ToFrs{jz#gNEeLvm0JLW_$uHn;yB=w-+Li+SNj<;z1yc3U|b zJrdd5HXlP{!6S7`9GZghCV#*)5G3Z-?nqkM1sp*IV8WWK%97Ia_fq5>}=MeS5;67AiEK0J}PCXEOc=Ns`_e?cCg1K zOG``N#O&@;=DWL~tlWC{cj`$nG}y%>S+(8L+E!3c<_nvis;>Ts4GlLp9nLK#X+=H zlKDlC%eJ~nQ;AQNOW&Q97A0=QsZi5(`!~Dvn%UolAHc6uPn0$g(Lz|!&7bA{-lp!u z#`Rv~qE5`M3&p3C1i|@QFHEOAth2`=&rJZx3xl5C{BBWQUR7HgO;cW68%nh5wQU|q z!>3i|i95Wj)Ro!b6PPzMs#AY0%bLc8WCa7oP2*<>c@xjr5L%2HgaUm|Jyq}t4uSe( zNXeD@)&qGlB8arhVbRe_@E`UX80Fj#j zYiiQJ4M^eH8BdP_d3`}%ef$onKd$UAYjWg-PSX6k%e5>O^)mpJaGLisvogwpM^gie z_QQ-v3q0YLz>N5k-=qklDMiBpl&x4mn8!QmMS|rPFPtasG1Gxesg#Z*NV=`aFTZUYPYbS(~N$@$CUso6ms28x@g+vA#%xa|2CR$T#LG7FiCiU?T* z(P~gdw1=Ap!3X8c&de=MoyIF$>l6^i&vNZavi;ubE$PXx^c!#boV1=JE$*{W+(gWZ z^!U1(G`iz^hMfB=K?Rnq$Wk`#Dkuy=ZR&%`iZ{L6@u*Qw%ZlRmMuoteuUcBDM&xY- z;D7TZN6I5IB*n^*M`Os({D@19m>NWqk-MT@Tnp}hV6HW+!{+8mSknyr^^Yz?6o%O* z+NScOHv{aWM99+zhRRb|Rldmp2BCvU3um4(R#VsqKygJ)Ug%e@KVp69-F0V;kz;k_ zVgHza6I@w+F4B>R?!CVI@`?k-e8ESlqt5+6O~}pZYz61w;IMw{HnFe}JT!EP0NG4~;!p>ixEo@E2!a^1l6d#+04}L>O?Xrw_A70aVs=+a{u>43#3EmupsF<4Q zH-3l9$@FpYQr(o!@l`t*tc;EoD9D%pejKCr?j6ROELcKAq^`?HTGOt`MbF+)Oh|WW zVs;fXemYGku6lbLN`Co`F-t4B>`FtY8e8{Zd`nK91G(&r!jQbQkF5|X8L8m&j_!XEaj?<#l=-Kv9OBP!5^BNE21*HzzrSCZYlLl36;T$KR{LDp;lEz zNYUa$__|P>!6~>v+BwtzDWjyNo0oU+oL?*mtSYDQH)AH&=p!mU1aekHD&%pXqUj8` z(N4uP6LF6=d}kOPGPvLWBE>ppZdE7=j+3_L#))e@gi2XN-%wDT)XQ+aW z5Id3XMXoItJU<6rbI>nHEir~`fkicFaudGeeJcaRL#4!%Qvjj~no~kA*fq0~rEb;w zaHWHv)6by4w-@juEr99%v&>&j;tfL=PA#LOkP<9sePJw3%Y zEs*afD@T3XT}%<$!Wm&w4i7B>YJJ$5IH0*@X_81@QcVuIq?9?Q=slN%x0B_SGczMI zGgz($lf@!CznyZFnt-K2EUJY|@yPwso!kykQ#kgi9)uiYO0#ePcGe_dmU1=etLbwy zHHUA1T^_e@2jMiOeoo2wfi$!C$@LQHZ~ekqesZ?Rv}9Qe9Fef%7osF8LBX(y*izB! zqVjTxl?%IWv&V!n0}Y2@Xl*-Yt;-b^CmT_LoR1_$e4OcCyKb0^FcL%^N~5SEL7T9b3Z#!RH0>uSJtjOzJgn5Ya3!0Zpg=Sbm zvJ}XoZDpU&kEAXqADgk)Rl6zs2BsOf@s_T-mBiy}gVV8DK`B7Qj=s(-R}855KwX{< z)Yh{y0ei7dsR`r$=Z!~sJQ4cizX$%qeE(yYB)}23Wu{|TRI$dZLT9Lx<>oehuK@{A zd}WYeBs9zH;WPbWLbDL?84gQ$=AD%KdQO@e^2~R<3Z6tApAf0+_&1_aB$;>ChW=Jt*L;T+@rw_= znPaRmUy>GnxZZ>jna4o7mn*xN^ir?a_p8!J(J-}tq`4Y_-Trr)c(|aJh98{1nKdEb zofrRO%Yc5l|2)AxW4IHSBH^$#tx3TtcWJqxvL7M38l-u*`qHq@FKs0lBR*6Tw z=Zh0pc(5N13vxA>^$wnq#APP?fa~2>)6XWH z9_Ea&=wT5Ttnzbp2rtv^}4B!SFJz1Zz+G>H%V7r5}?i39!7S>x- z+hv|Db*L?Rt9W@x^Tao8(wi_4%7gF;J3I#q-I!wH^By%D0ZGUvDVJRXXKqJe(O$G=Yu7t6h-lS@pRrlwDy#X+wNEzSJT>f>1* zg&jV3ezSsK`IqNn8^VJ$%yQ@NPKTBT`X>W>b!QEUT12W^<{H%%pbulN9hF%x|7^Su z%lWS!Z(*e!Pxs2rUG|5wuRxI}um@xK2K3;xz&W~u*ZH(Df(-=R5BeucV`^%gpaY$*Yw#osg z=ig|+H{B&-uG@+!-1#M7Bm)2ol#Vy*LjRV-2WJB&0QPh^C;~>-agT| zvS$bME?R571w3y-RJ;FJaPV)oU#itH%mhwrD;!;OTi!gKoEYxY9-=rQ3c*wOMT<*k z(@x;*{hwv$9ArHq5bTx?Pi+Gq6%oGYK#e1g-Vo)q0gFT}%7j>oMAm7Qj=7?T3aCgF z=oe94bs|w8b-EJEo_tmwn6n`X>ksJsl28<~8&m6XCqtCX!AhozgyUG{<8Xt{KxdAJ zAnzv={2?t8pJDp_4RUnv&`c^m0*>(8S>((oD@2_`f;}&kjEU05 zm#ln6BMYy~2g}Izt=38_3+{QJV17DYziaq}RhHj>`9kU|a$#aq@wjm@H8o;H^icY8 z>k5UG`ukGmps8kt;k&K++wsyPo7)l)wsxoj+%FWMo~Y>B>*fv&!5l5DzQwMy`N;v=tpZLE>=*Xh)OYr`;+ z6tpnF;bv;_K#a|Cmm zOc?>+z;^*3t;ZLbd0Mu1x*)EP={OsJX+9J&S(0>oUWmV0O7c>r;o_CKQpiiDuwUmK z0d5Q+DzDe2W~Wal`(w#^HWFfM58;*pJ-&CF9mG1(fG}h+iB39kUUfFWUIt!WIH}@sDSs52eIT{d}q!QOb^oyAx%ZVJ1w{uVzx%@%e1AS1z+V+C5+gmh&vnfpp0 zcsz|ZV2ry*L8iWL>aOG693aw=eWI6@_9itTRs9=$GSXm=x%)M@0Hh<;45MKW)DwiH zn6xi9(K=3+k!W5>q0^1WWOLwO`n3>8X~JC+v8F^qJ)2%*DmyA_Zmt?_PwC>vkDbf! zUKA1G>*nO9XovzDB$vt)%4a-5;G37+(Z;Qs)2R8zc{2q=H0ntVc{C+sn-L;;}Vo`-k#?!o(OkI$9jbfdW>vIJ<+^0!e%`rUDN9G1m$_c}4Q=Qs?U=%8 zL_ldRi`@&s3E>KPF2d$B9MX^>rmrZ>7Jyku3qR3Xt5#>olNJT zrCHS8(7+CvbaCBq380kT2iinj5;$gh6$l; zBt$~}aY6FssVP>#2z6iux0*!$BlYCdu|;F6m!I%NQnY~x!ep;x&(OdZQ?p^@DDw>( z8)g>{7<)!kms+RnBR^4@IaL}bW*&ZnToBj$K}02C>!FE^m&)+LMg+27SULZ@wf^t& zd_{mSq@xpZ^*h@!B_r`-m;yIa`lpDdj}Px1Pp9Lv?GaCovXI)V;xMJJ?{@fwwlT5n z5ieHa;<~RXAnVnSP6AMwcReKqxedR_ohiLM5zg5@;c6yd_$XXbP=z12JD$0=aAZVY zvV9kDOd>N{4(eU1M=;0VIBN*t!9j;9wxZ}7p`ML@dU_P8UcF;oUN^Ck{Xyj>FOUKQ zMbxw5@kI_%>&Qw9{D$}mUpfzw)^3wB+Is4W;(mtuQN3_6ZU=D) zmU)hsdJ=4UMCx^}D)+T_l{&MhvbVcLG8{dM}|QDgpu`3K|G4(tGb+r4s?^ zQX{>Cw9r!S4Ep)~XYYN^zUR4j`8gqSn*H=g_JMECyUCP-j|1lS6z|c{}R8%}W^xePhKf3B(ckg43$Z(k)BE!r(S2fD(dyTSqrve^zSeR%-7; zJRhEZO7KEqmuW2snG&A8hMrj)dh<2CLw79KNoTmuO-ydk=na(^1kfcyGLvoNYYHKO2X338#fRW?;bUs`c>?ts)o9{V|%Jf zLQtT56YcLWzd(LJ^|2+|;w<{;@3UMec+uWN;nY`B|KdLL`!8e6@3$Sn{eH-OZO(sv z+{r(WsLg^6G1s zIbPkQ`PFIsi9B6)u8mJIDLbr=ugt9Eze?y@`H zwhS3Qfh(MgQIb~U_85(c$w(&bb{DwD1_j({Fv*LZS+QPDqkp2QH$~8!uA@wk{BAZv zLCkgGi0}QPq9WI9iPR>(McJCo_J4pir0~L;l2M?}pdX3V;O6SqD88DKifx&P|^+wiT zvyE;7S>+ofO%3ds4g&)&(tZy!?k*#EQN>Whud#zaw<@1U&L+cmh|`j&KAx_z`k)iF z!k&bMPi{rDc7Dzq?Tbd3C2h^$bIM*(&F>Yg+yW!%Mwza(#z6Vn*4Tv^gPS*mrU^H2 zcBYRiE7zYz+!5LBO;6^P*vedsiJHEltKnT4m$fd_{k|S@%mxP`Cpw^c7u16DtSi7vLiM3ch4b#`JU(xF~3m+7M zg^Sq1F)Z0IPFTglR8jPxT-Dwz<99abTIetEe?e{4QpK_)pI&m=;eiviw_X!S1*@y8 zQ;XGQId{QG-`~w*?T_qb7#TTbXIIhVAhoGE+d-AO~E^4<|q+e<| zpnd|s_(;vaOCC@{#_`H(Ur!coafNg-41`>)FKwjI1>gWJ1EQF5N z&WJi$<;;C?>{xrl28(yCJ5l?pdTlQAK9_&4Y&7mwY^A_G)zXh+iCcxVp60`5nB!Mm zvprNgvJzL-;+HddzT$8<^>a7xLy_4iOG87$ks-?!)rE=Vs5IB=1f{62gQ%`pmWV8@ zllQw)CerbFWoI3Iho4!M;*26;mYRd`snaRe8L>9=jiK7frhSzD361rbjXShyuYa{5 z)2uy~hp1{|lCah`?$!ngW^&=;?&?yOA`MvO3}3*Rs`g=oroDUZBsbChw8N6OpA`1B zIXNG}y7b%O=RW4nRou?&8RYJPY()1b5<2Z#kHiqIL+S$Wd>Qd7k*{9KDkxb@x49wz zM1C4=o-3T<>KDb{QH_5hT`?gN_Jmr$R{A;7GBUxs!o{Ybvj8fWLqWqWrEZ1m?%ANH zGxFImOKWTA5d^VKH~C${(lhum-}`>XnTAz6O4^*KzV)05+K}Ok7S1L2&RrInHtFc< zL*w61Ud@AEhVr^5j}bP{ReRrC?8g;BORAd3)VPTOg@3IM6|~%ZgB5mLdZHdYR51)0 zcAYGV8J%Jt2;nYlD*aVAgV3#y-`zH>GmtP+=6fEs&iTHuE7W-$cWEK~rJCb+1W`#Z z`Q4RA9$z8-i-oQQCc}u)p?)W(Z{Tz9jGgwIV7dY2s_v@_S*`!iy^;3>sK=N8=2uH!xY@ zJ7;Z0ZO%pXp>Tyir*vc7TI>|ZwGU7A>m^4mQ>Fm>Zg*d*?obD=4xkyeMpCgzOyJP)_Y92 z{VHbAdzU~@DuFt_nVC|JPAefe+S_1O&4!15@bw~xq&5m}G7L&_v||3shdMr2TOXWH ziOgTjDl9?Cx{`j`&$!oX3et4~M{)dPblS;9R(-L;4)KW-Q-@ozQWP=#c3bFG1zUnG zEiE1~9itc}cp@9#a&yQze!lobrswO#8iI-2)R)!qj?#M0aRYo-=pgIy2zaEom9&#X z|LrTTOZt`tPy_< z`%BQ*?b0btzIujNJc`Q;A4iLw!@WfzkNI*KR;jCMaA|v>X5$Y^AuBOnY6whv+U*X z>GSLB)qMC3Uf%4?Ok$f`bysY8Z>COd)xBRH8qxnZEcgeN{BiUz{{WnS{whIRaQuN9 z|KZL*sABjpTpfY_*IO?AQ~uBO|4XR!e|pvb5r6rZWBllUkKd)2Z?oV`Z~ipv|5O>aglHJHJjJZ&;IOf7*Q7w)xP&J3EQu^O)!-|!hcGU?v-=K z8q+*WJjQ)0zl1XhCUUDODYexFoT85GcUo8CV_B z7K;N#0Lh&~3GwrD>zDp~ewfXDwV6vg?%OhHWEq|U7u__MDjU^puiC}uLS5Gs{EWF_ z)x>5ltbEICR|+tItaC?}ZzERELjKByUZ2Im$g2q$*YF?~2pLbeiC9&z&M&964TlGK3^l z{cb`<3Mu}*v-7?(?v7wueEit3oASfUr)qCqe%Dm796MBlqO^R-Xkz&U5wQaGvFgpd z&c_EUvg}FM$Y{Y)53JX!T={HM270z3m|1FTh3~)|{)Uk4Q0XcOOP3DUt#Gxuc6Ok| z4u;ZK8~D($Od*#xG&IzfI8QWnfW;le$amED{@91d*9sjcTRvx3tgg`(+vOa?@T17I zU-*3Q$5|Ibft`CMm0fe{coA2KOav)wx07{Yk&+%N*bkFT3{ zL!~PZ8hhmBc)GBpnQ6$&vQXQ^|BJ!WTI!@^KKwXkmx;SP3xQ!O+u-|9sXyospF#k(!#H zzZ&Py*3#Y%dTERk9tSTs&sg~pD>XaKz1K!5N>9o%sb6`7lt5Wyrx5VSjoA&Ubi>IvAP?{;^{^26%)vI@o*B&|Gx?_-4KmOA@A~6+VSfuen5*h6*zDbo#n|gtZG<>h4-Lme|+}2shRm0^k$qn*1K2=J^T4Ed$fcn0rX{L zWF!+46F{h!N+660@SdI?K&^mdu-Il7jIFpuXCkYZD?dN~%lmPFlfc|DAK}4qu>HVN z=G26gmy2q)`=Uvd{v>$0T_bxVc?yOi;FN_*C_iJf8ca4&D;+Qjj!&`-N!kgf&RxqK z@=R>jL%A*(`6mpyT|!yKZITzbD4>wk_PWP1h1hp6g|Z7H*0$%sYF5B-l#r*{@g?IS z_5E`msC8wj-JXqkA<6Ca9TX)61zRIiD$O%G-funHI2Lu9;@+-lqOeO(*;Ga$z#C!S z9uh0HsDkJXwn}V1s~BN36mq^jD*ACJr*dKun7*Am>!bGC0^;cw@BIr4(!$eS*40Q1 zV48;>n%$nphC3;Kk*V5Ul56ElvXRY&{xOi)^1?vY-c-@ z=q1*_Nz+TH>*}V*)DY2i%{`2hecZqz@$m56xRDRN$<@!@6<=Y6Nfsg}{Q#?FP>ife zzIn=y`8bYa>V2V3G6ZahS@Y5L)V5NQL94|jcOY19=mYwaI3k{JVXkR-E z0^6ZVgi|`2+A_PRPgB#<5G(bJTAA7o#H|&us)%IZ+V<0n-}2sBH)(tWrosm_x*CKO zIn_IJT3{wl$9me%_hwzadbPl;RTL~i8=EZQQG-0_3rfkV6zA1hxv$R=d0{Jo>l^gQYusHaeRf`yMhcQ3McHDfENb=T;J5l< zUOO=4dchis9EK(Da#1!JvFI9#6o8X2U!#Z9)To@cQvh6U$5!0|Qn79VbRcOY@+P;3;DGIPh6e@)ChkAQXZhE2%a0!`BLdEFy#d6g z_k*@8X-=AB#3>z48$$)dEbW!6WOTKXe$VR*cP7l!G!^xw7_M3MK`;rcFAb5~lVJ8F z-R^iFho+hh-W%tjKD$ZZ^q?!dlBZ&;e!+e^eo=Xx@d6w7Y{TO)zW->7A;c9~obUEM%a zV!I*DN3mGaYS%!9)M>+{F&}m^Q44FhlSAPiA@qLOGrM>Ib&lXTygRA^echANM`=_> zu4t2BuS%=u`|J*kQ?|efF$7B-Yn0lZm|PywXV@5)BCP=1&_q=ZHTL5;XvYx2*S-1CK_jY zAteP7rOnMre-w4OE8W;c5}Q+2ro)kzk#R>)Y|R3$>!cqo&XKKoBJ2+8UP1Fav>eOn zzMN{?*vZGRqlcI$@Q}@~XVj*&REI6~hYj_kp2&;RkH1wI6KqEH7FA7(k9m5PkmOmq8b+-gL3tCxv#Ll z;8}2w?a6QuCfsQkHz`)Vtjp)QZ*2&AyUbbhz*LeE2ACHuEv*uldCke$8=^>J>3KwY zMh0*NgFf_B>m5oGQT0E9v8O z?vPu{!69S<7DaBR>`cSNhl%*FqfYl(Q6x2E404OFhTCFC!2+30ZjTa!zEW2VK`Tko zjUpjyaBP)e*vihmtgQ@OlL>ZeitdNVg(Vo4Z5OVdtKgdvzm%X9l1htV;SLjhp1@-| zR|7v?aqpGL)l;F20Ww2FoYMv;ifHcl9I%D_YKPFs-@g=w-x(u_#Nj zd=V7#Nm<@YtN^Z>qp!=kBG9^Kca4@OOyq`gP%G{!sXCKPct;I1bJiFEdU}rEEx1+?)H^W#uEuot=;X4pJVJvYS$~4kI#e z-=l5!qDZMzW?2VKi2+>6H6+3yuh0c8v5!E8Nli^X$*Nyu$CJz$+NSi~hG=A=!hiF! zR9l>Kj?Jo>1*TiFa6n85t66m%$2Vhy>CP?_T^=>U@L%CrY`m+(dAmMQ^cpUvF-Gx0 zr@m^}zy&R*wkS7tutvCpp7%r2ZilIkLB47Qfyg;&fY<8HeX$A28|}72NBpyxrhwZt zcqxc8wGdY=f?Dchi66l<^Cr#1D4pTerm@?n)=X9(v_f>U?>Smv)baJ`)BV;M8TAnW zidZ;5ss^{>9^*KWuQXa5*^f-~Towmz6pA^0%y$HYnN^;ff$0-vz^Q3=tj~6_yZ?Be zrj`;|nUR?ZZH9PH-h}m6d6qCl^nL$cYV>)Z!wi5?Fw&e>&$E|+e?P{)@xzCnI@x)3 zIm?>em!4Bp@tgD{ys5r!a!M3s5KqCw$n$)X8=!|rEy*)>8v+k_dBWVI7$P=jH<)=) ztIAWKDk-Z8SQ4;)nqEI^D>f>ilGtU^CZ1i2oMFoywy<@`=Y-{>-p+mf7r6YZ>JPU- z0Ok~_AH9Wp9}CYXzHmi1M?Xep*XnYv9c&UoBeUV1?!=X53yNs(MIBBYb7Uwy|4z0x z=S&FP0~qV6)+jAQL-ItJa!Y@rKbwcFQu_P%?-PgV!%aV~Yr|J2e8gkQ>kcsJOsa5bPzS?90ut*b+VvMNE&iZWJas;^lZePz8YTf&d0gXf@+c$ijaU6vkQ9*cpHEUSGg$800q|3Q?fYLwWz@otOhx* zOd*^5n@EXnV9B%8zxhy`ySzMI5_-XBQ0u6|MRwC}eEu@&2aHS#1Hm&FBY6yqU)UPF zl84#?_=7@RQ*Hy8l)ZvLAA_gNZc9;`S;^N26H}$&%yj-yQBi#Qr5CkAQz_*E8criy zaTH*Vr<08_(b8?)QcX*^NLY9ZggvyatCBXF*fNzQ7D<5yjEs!5MwNVLbs4D>w=gnf zLN|X-ETjKg;1$JiEtMw+MDhv}8{KM-gQ8^)Lm&cCke63=RBV^njOO1zAV}{>@jGlw zG7Ot@s*B>`2`jGfDJMy*;?&jEIn>hZKu0!oCMis2b^xTw?@6#OIH+olF6>c*1TgPW zG8Tk|{xNY!uHyoz#3bkMjwcZfkN_RdG(SJTT7UYCoJ7%%_ho}pEaL7zLHrf>xXGFt z&*gc0_OOnq+C9yJM@o3QUc=OpPs;T{(&S#&B7S~83>x#D}-foLcWx;AB&mId9Pyi7OU~d@BX2EE?xzn!3 z!7+g5cwSjqnWb04CT#z)bnnQ^TLG}}Ixnwbu{9p5=j{s>8;W8|Tcfh{kbEwUM;3j$rAaFa*GV(E#idOm;hn|M zDHhH4v=PmB5l=T8Enm6 zh$+my^!%xI)V-zoi&O7q&Kq7kt6PI45x4nFmzN=F9w4#;^yW?SNF}1+yE3uk5qnl< zrt_P|we&rnXQ+a*=u@*39!pt4g@s;4reUSvo?LR}CBig!r*x_A|C0i;xiUbcMG(h- z-upx0)r!w<^^nPl*V`mKJJ+omVKb<0aZkK-8({zP>w1hZ+>ApN{a0CNE~;^dnRD!$ z_^F_s9A|>r;j@(5Bd%otHU67JrgGFYznE+)rTlNFdsH=*(*9gM>iHf)HgY!s0FfIjCA!)4D>C4t7B8?b9zOnU_R zT=>$&MSre$SA?j#f*eLLYYVUui7O9cy8*})w$aVo{Z$C)2tLB@2B&J=CY$4agwJpS zs^4($4NZ}+%+J$Yyzc88#*P&BZ(u~I(o03FX$$vnpV}H@tcmaySpy2ky8$~{z2pV% zaEsF9&#V(apNhwdI!aR&0>4LW+jHG%UDVQLoD+H5Nxwvhohu<;J=>?;XX0{9jmhHc z-?UOmeueOsHphTeS{fR^e5)JW(c(6`4k{`~p9w_(EhN|8QMHD{Go}AdGKKm|N!3Hz z$<{Z+UzP6(H2COFf%uvIYy?+0`3L>6-_`m=>^xBMR@~vpZ$+vf2a4qt zK*xaGu~#lX9LOz;++o5me`!_E7JOOPem(Jj)3Q(Ba-p?;Cu)CdT6>}jwZOlMUB}De zOp;#0zu!ZZT7EtDzjV;~P_)H?|5oS+oi=AN>i6@yVt|bMTYcn@_>;`^kILy+3Ur&} zf2V1Ef%NMp8~;hEqR#4H;wg0@{Z?1ClYun6ITG}@BxsEJrS|=u|MVZr0EOvyZWvgV z|L^j}{zC~Wh4{NM)P?_d{@I^q(TUmUi?hUILu#WfR9!pEt2A;dg=Dpql_`|nyfD{5cie8Cr z%+3?=Bp@Z-2&)7^#P;^~+(za0RMe`?K#x+C;3Cj{f`fx;8Mxo_1!fo&K7F^-*4e4L z;?f=VY9j8jEXfkQX!S{YdV1YWbl+E}7vETW&nSzl=;$B-MFo(=_wU*o8Uhxb94nvF zm6Hg8J48W2!9)9=gNP}>jaRo&mhH4gotm-)9QZNcH!LETRRanZN2>I)(x}gDv;ZW> zg+!olk#;wQ1O%GPotK8nJ{$ck>N8Zs7kg~XB_}5zq-BW6@KzbcS!$Zq=fiQ`JH6RR zLHr81R9;?A<-xO(ib8M~%dDRjep#sQ!8s-kq}^iN0?rXgYkfUEtfJ1ck~N@>Ppz%> zL6>Oy6+8xfS$bC13E%s>ghkJ>04~#_?FME$XteOV7v?%=1|-VlDq2B3Krqsku67-9 zNGE-9AcsgK0%Ch|rXwN#;W?XY=X zbu}5_X$FyM&rRD7lL%3lxz9d_LqISA8JFr3#w)T)=;VVRkU32-7@hU?p2XE>p6>mQt@MMwb3Z>(2TI~izPC>~t#d1-uE* zg#ua{G+Ib6nJd=)eq~L1x;!!!8g(yH3PlEd1U;*0p-Cg1%5Z?>B+xC`Jr{1T#L9(n zQpdmY@+uL;3!t>tO)r(!Kux3V)k%byyd_9yd@;Qw=CM8l7H)mYkq_bF>=GV@+1YKMA*>>f9OsX>sIWVYuYi0I@Vzsb zJ-eIBTK=-|RbU9h(dd>-sZ?Yq?w(s+ZCC`IvU+%A__kuKID$eZxwyDY5xxh*L)(|P zR>m{Z(iP)q^LGQbxTmKz;Cpz+4$5J%ac=oDVkg?b}_+AjeW5q3CF6YG?$rOCbR_3Wvkbiu*_2nhl%J zALKrrel87Q-??JbBcBU<0zwMC|Xot93m1o@e6E;&)2X%*AD6JVa^) zg;NJMP(#B7L@U?TlA9Po#_vMpLj<`Gqz{8N)o+e1;(h^$pRs5#GOH7iZSY{A&@T>G zxCN<9gUJ}5oxKE*uu2XKz)ZtE@ehK4Su|~j*Z_*2-&Epb(am_yQ~N#X(AUo&N2Aef zU}t+INAaP!zWIhSba&i;bdW9 z0h$n@wweo8`!?rQA#zn zfL#m|OGV2KQI{lQZORyP2nhNsb!UK80?MzJP&I5GO70lHTeKcig-*kh}S`K8eka0yCDH9vZrpQp1XGndig~? zBe(X&aHF(l1qB88cVi2SSzwbuny5}I1e0^&l5^w4kGrZTzFIwf`c^tl*JuRHUl@wy zqOxp}_rq+zrlw}^w{I8sbxD|EOg~(U)GlMJKLaEb%Nsd^e1pj7ESN z0=XrTg-GOLQQsRjiS&n=7`yWAaY!5Fs?9S%Y)EJLt1ha4Kj-e!Bn$`(2@if;LYun8 z?m_{`KCra3^Z=YTXl0Spw36}uYwmOc#+_VlMm%9i2*vjj8lczwqw?jz*2i1Q()27r=siv=fj)p=Yd>w zmh}a=UlI&hBpd>}bN?EE`qm<#v7&$)*6~SOl)rVJ*dSCL#~PaV!|`AOlP0ftEpY2q zrWj6WL@qFn+b)1p?%}&kIXAwN-zxa%Cv8bWmR9e5kPk8)7qGCmvS=H=x9!$@Jq zTmboWkuU30C17^cUoy30rE0RXvw1hB|=`<4pVK;?%{!1TuW75+D@*;X^?E zh-pqY$bJHjgn?W8=a5r-Pj7GF5==uwqrqke#l;g=8|K-_tp;2j00Tf|F!Y$c%B>dZ zw$mzv2mDRdanW$A?gj3yFTS+ z2)%F~Jxv{*>&Vx*(Q!yOM1=ot8@hRD?vfLIXPnxo{*T5Gb_z(4ZpL?>qTdC+0>XRD z`DdYZe?G9qRCEVmL~2%+_oy$U3@_Zq!g&u+`hYlYAP3-LkRV~z8Y%EZXzj|QH^*g- z+f;GEHmLNtz%NW);y^Yi9hbFg*2*!odDHJyYBneTaw!q`i{-U#!r~xT zBnH3>yZ-zK>sg=?HK4V54E_3U7K_0ViUS4aA;d`?os=|#%WEN>fh8C{zS~ZdCvY4L zjA<%z`3Q7|w)xixstZ~({5lYCj=SL5f7l@3G{A|(ZMZ$qR}IYTmBr`^%W(DroLqz3@su1iDLcZYy#-w6hr znVDJ0X5f9%?y>SrDpb2SK)&bI0%&T6BL^nzJ}-lndbjvaSuNV)`?up?rT+w0|6vo) z-yz$755WFQ?D$U{_$QwHk9&mvT%{%<{#)eu->ZQ!!Gt0?_JZKMfP(y$V(Q)v31$`h zoo@Q`?{EJ-@$`2TPR)Vbd%`ooBmFA%C!nWRYwy~Bx@ndUP)K?I7iIrF+4cG$=Jq&- zKp-?UoYdKS_cJ(?5w6f_+IAcbY+w5t8W5br06&6gEO}M~+3IS^+FRun zp6MBzKry>+ksb~sH*BvhH;%8!Tz1%A{g9=1b22V8GzIUyDOOcgHTDu#3Q4o*|U4pn+wl%%OLg zcID+M)9I1vDbsUvJvpyg#f%3EFNE_JrTL!@s9PC7G2z6t&3%oV+tNlNN|s}6r8F~s z*Xh1;zQ@K*UChGbqT&E8c+RPi#vXO8Z0A6wl-f9Mj;iT_OBx=VR!1DypH^)d#uuit z*8jz4;F_j{CEGrFw62HOrK~g>00YurfDMt6+#1>Z;_LgM&_O%-UI`S`mm#Pg(s2b4 zj~A~J7Sb%NiLP?ZT_FA9h=2P9zjuWV=Y?!ly}fHc`?!E&?UG^?pn<>l_FWuwYZzZ? zYYPEvs*uAIW5rgoyEt-EKBavVvK?(HiF*lpow&UzwKH?2C{Stl<%r2psa%ROs_)io zr|Gu#=xu%Q9`kl&`>oeqDK+!A)&SssEk&%Az5H&AP@)Wt5;P$bec#De7@vO>(ksT& z(#E2+Tvn{Gv5~_kKaM=1lY^X99c&2B!HR9hT4D}WI*TL%yj2CJ9iR$IN=m@@hE8kE zpo#@vs%LS|)7)w%CaG0b-VLwKS&o0m-I{}T?`-6NFe(Q|iF9`?JOovow0B&DbRSYR z{J?*I)u-@`prBv3!;uY0w=`tnv1g6Q2ja!G$~!dfhg8lb4yr zAIOeO;QB(3se$C49O^!4;=`Z`W|132&LkzHI;()c9LI@Z1cGHihZw2h~0dz|23 zJ>SoVy2SH8h#wv(b1HF_O+GEXPtG8$03Q0G{>8x{kP8Z*&q(ex!+^fxp)iCe7Wcar zK<9cq%*ZEb_(77O@0&Mo)Wr}soj;hAyLFw9En9}CvnHQG&&^&gj7kIA@ zEkDk_#*ce%IO{mWAR2Aol^pnV-SP6fnVDB$$_esUhE3v_@}Sc75DVK!8yt~GPkktL zM;4xG*-j;Eb8fs`eY+C}5bcyS8r|%bMu*u?gQ?KG2YtjZ=r<%(0v49kX^W7?aZf^b zgn6Q;rzbsICUr17A@$=cb93IUtz7ko1IpfkhNI^7avGlGX><2}-&(w?fc7k{QofGe zTu7UppFgu)SOA?mBRb*au(BXK;UjZ-WyEtD=DFz7&h2vY1eV*Km6f%^P0P(~lbY2O z+*%)GvrFh{cyhayQHN~@7&u_r_PSgO1Z+IOM8_YswUMwo|B#~-0FpLfA=ja0F7DY~ zvRApoCP}B!X$5QJAlTTQNqA~!w_I$akv~B{8Th3WlB8Q6s>|uG4$Q`K4Jr=Y79ttQ zc{%`?-)mOkBW2Ok?B9Q+WfntS;aYc&r+Yd?Jikr9TXw+<2;Bj0mUNHS9_UuYKk)mm z!@2efXcV6mzQ&S>yUVbXDyvHs9eg#F>j@$8z{-4jF52&Q%HUx~2j6vs2Ur;F-e-gB zpX5}RZx5Ai%AB~uH4WE70_)S$vkRPy>4rPe-Dk6xHzl$iz4pMe$_4?}98i$H0EbaG zrTe7u)h52?=SGe>5poB+3_sQ1)4%NipL`Q=hPK4IzqhY%6HK1A=sbLxa)NQ6mODG^ur6V>Ln5Hf1!ZsLX2ls?at|Ny8GDtNuwa1^w&;Qg< zd6!=9Q8P^1=`HCE(mz+lSSIu6>};IA%2V|9s?JHHeXL5twnK>yRDSX$C?oDO)pz8ZKggO)aki5cvA@LnoojILN+ z9j9XzWeKVuTiIf^TWE^)0zuKvl3P2l`v9L!sF>qS-R=u*>uYNl!f}G9&VbYbw>uJc zw8xSwH%AaF2J-T;=$qp;YcUMlvBMP^yE{7r17v%`!Zh6Y%&x#;pI|UEGBmRm7l^{| zWLdVU6?cBANDqIRPzgCcPEWbDT6gA- zU9ekrpXS-70l5Xc3>zfR7XnN9>RiZ=qg0Di)K_e(EEyiYTmNJUc$?{V*weln%^Y-G z&Q0b{ADQ)-gL-B|-H!MCSRQj~(pK%!VZG#~rC5+Aq8uey%Z#aXv6=)ft&VRZ0>#`dZY|}y za6sT&Mo!Lh1)elg(B9@IIWYp+AP}l|m)uw`@4rJ^F+G3~9k}#ilH_QI@soW3vXnZn z8Caav%9Hy_ebQqbj*pMJbm}-Umb#xHV22=6e#HLYYSx}ox`#4?~gz7 z6etEzsNXL!jD|a`Xiqx&wIA$10b-`RIDVGj;+i(66^{Sk6|YZJy>{;ab@;U*?9Y3D zWa}oM`mK!pkNd#y05IL&_vf+yj>DPC0UP+}*TdHRzZU-j;xw3jzuQCAhpETEigJGk z0)9Z86FL3o1)$#loao{BEx~Uf?3K@j_U`>4!tWDTw8i>N$DjHFQ?mOfegMBsusQ7i zcLT$M;pG%E_$7nAlF#7z1yJlC$*_3<;=KfZ)nM=1Kh0Jxfnpo~jzXySkk|g)a~sG1 zZHq+K=4J>eWMnkAJOuVd05<6E#ZSZ3mfFjKM0}1;+w%`W7JvJ6n0LBS?(?rJ>G0Jy zT}~D4V7+7hwc5#l6+%B7Nk2sCJiGJ&iTpfT`>4F4;zaGt;^GP45sWxOzEiy`g%K;f zVryrYV|;#ic(|zO@Y-rakTfl0^*UH;AV#?SuGDLL6{JQfapKAqifNY~y;@LhMur-n z&(r?8ylIXgr1RwL3f|}chz>5Qja%Sgp-WL2Sx`-KtI7AO9L$FXD!pr;4w4!0RRnc< zug$I2Aih7sll#o4Z(LwQ4p1Ln54uc!#ej@pk;F)GL<>+Hx;X-5;z88IWex>Ava)iB z#|+5N#S_gg=&&9aY~nS!c+!40l0`MGD&8pf9^EZ-rz@8sG7)k@tuF@*GtBKJ-xnd} z(6@`Ulf!RR?M~();$#y^fPhIG@N~i)_peP?J9c27jv=!<9)g&1!#phqgNnJY(?16g zB{+cgQ(3tZC1}GyL4U|{mDSMMHWOz9L6m!I@@(p z#>`_eYt-xj{)Dl`8%nJ5wEnen^S3wCWMDU<)^w~824=6&Sg?zQFw05@w;|SiRqhy9 zp1Z_K?W{_5{iJ3GJT~uU7#K5Q7KZ-8p&^!lLbXYc5y#9I;%wn>1v%J@9k)#W1F)9E;H>vgSv$i&2JoxCTL z^+Akh0%Tw?e6nH`PsYWSw|=#wKvgO!DgqF!ckcjE=*n2q*jVV$P~N$7dH~3ebyYDQ zzG~%Ogpt)%EK^P@rj4g?I2w{_^IbQ-{tOeH=9FfLsg>yNmAARK@A;= zrUE#IFjUjvdrRiX>#WQh6|a^f3V-g|`LX5la?y9AtMoWO$yuJZSB~?m7o2&+#_WtS zN1E5F_kK!5yZ|lN8=?ET$M1NL3d05LjW_B0bq9=IPqGAw*lLIypf`#*L18+ zWptdJoI33+!_zaZi-t+3m(Tj@)0H-{y)k31G~O_f455RovWf;gOj!5IgFU|#^sT;;8@WEQzEe3;UMeJhu^9S zTpdzSQ=0)CC&>4FY-eX@W3vcY;-d6%jQNzz<#+DlRDl+AgDqsaf6W!t=<>TGhnQb{ z{wQ1Ix~O|w9S;&tpLUFap=ZOHcZ4zYra(!A=$;s1B$A4ADk}cc%?2p%msPylVU}3Hcr-lsVl>b z3F_dSv4iDJIoIoLd8(lDm5oN*5y%*|x5if76>zg6PE$b9mW#jwup*LZX=8Ja;pX|{ zN?1d2PeK<62lBM7!EV2?6MWUBxd{nZK=bi6upRb(iO`~WckBJn@7gYXm+%~6s2~Ky z6ah_nZH-pE_^eFU`VjO{sT`c{pj$E2(q{JRGKY0#uebA=2y?ofP>`1DAI+o2Wb^b; zmhfXeL!qt@bZHAq;RW!PGVcUgdiNCd{cGH~#K@8-)k?z>^y0#}2@PDLZ^WPka~oRD zVAiz6?D|#QL^7M3h_2v|0;q2Kx*3M%(+QCDRSJefS-BmkXk5^UYl4n5v$J6RCb`#s z#S{RY80^kXiA)7dxXk5K!4HAH1uD$M%v@jNH1!o^`W4s??*In_kXn)tpd=?e&Dx?h z^gM3?B4m68Fvti_EbEP0a$>%|XjC^u=gkbl$hp%IVK2|B$#c4M^|30Bx?&w9E^Pkg zNNtjqDlTzD0#Y@+JWZgV8>8!2FvU77>m{osJVbOt(!{Jq7o=jHDuzb(_Vhu-y)XWpT0GGV3gH@ApV0LWvR*s^Hm%mZe^+kc ze(U?i6u-JDcR%C8q9Vm8O^#qij?*e?%`}y(tBy7rq2)gY+aPKC*X{#CW-!;-N9zjR z*AEc}i*s*VY%?xxqMMU)nl7rP``KC8#819CIAst8M-H$8z$MPWo^8XJ{oz^`t8!$^ z60)oWI&%I-Q33S&Ijz>9maT^#GOzvzGOxlA5I-DW|rP($JqAhBHuISWMw`{m$~=f zckk`0=vL@Zcj2#1*IY#96c-dY?GkQ`=6_WwO4Cht7bk-#%eFti-t%T8%jGc^K|}#G zNn_bEY1$gYLb$%+XO%xFS!Ww2R8(h)u|qWVLqtmAVrNGpqO^cM_O@jeDWb}Z)inLb=q(BKX~FM>)+4*5d#R^n^xqI91P58E_#54grC9$Wj{$_M{Ri_ELu>s_{>HxGx1=7vKL7j=K#YMi1^hVy zFk8g&e*vA{ssG?i9lhj^gysJUABWw*uV?MGF&A2lo+VosgWUU^j11hZ$9{Q(f{~Li=@|_C&jhFgq|J7V@rx zEh{Xm@8JMWTK1ega^S0*6%9|<)_E`&qwhheQSj5FFggmjdYCHXTrr96oF2 zpjNl|xc!IMHLHr){H=pNCwL9Q_A1;Uu?o5qU3T`!fx=9CHLj-D<@Td5A`QR}O&9s+ zca1((m}10SIsk|ZsxMS36`rUzNBV;d#uqX0)Bcqv7#WEuLQIn|j$d}I2Gm*X4$c`! z){n2LtK|#{&QSW-Rh~T1c?#B@;12hcUVg~~40(#*u4_JYxoIffw43knWK@e* zi)vBI-HXqL0^sbDNPyQ74>*15MGR$9UMacUe5w~&Lh|k$ddON5$4`b03(zx`hhs zOYV=i&PI)PIJo4?I#@J3=hCRUY=lHe+x1#*BPV~w-3I&9kX9x=LQ`UtY7_a(KjW~zL3k(Dg?14V{%G^alwW$ z3#)Fb0_n3dt;c@oyZkXx&(znS#MAq1;>?>*UbD7dBHm9GL<4KTR@JRiV@DPimXQ=! zZboTRDhjHu{{E=N$lfFtR&8^~>L_j7{GQiOAijiFVe09~iCX?B<-@9Yms&`sasEX@KcMA8_VXWf(n_586t(crh(PUISg!}W_CazTeaGI@;&C^v8v{kaU*X6;B0a3(? z-^tfZ>DSO(z%u`u<~TC6Kz3|>v_HaVHl0cFxt9F$pLK6<1jLn-EVGF(* z)vCjFx*x0{u!%Z!7^0d<;5RHTDtc6!pwD^1&1G)7=c*%ok_~dfr;3{moG$dCJWXz5tvP%+WDPIpQ;8s6iio^-U1t}~HKy7I2~=FYCu7_Y|CNGX6P?R&lqKP%xFxEuJr%9#gI-=x}IBK+6E% z^TE9)9sh(iGsqhRtQjbS?KtwCc02mY+$4Up74W^V(8 zF!fD-e-IHdC21xn-dDgz=9+xLsD0O-)zBZ5x;9ol07&ud8J;nqDYHEswX!q0w8hF* z99CG9x&-f&j`ge>mFxO0?585i5bPLO2uJ0CPjal}(7Bh$0q@hhZ55(=xfCpeO6yMh zCp${|TyIZyYs@vrd}T6k>u*EyebD*QjV+hK~w94+mQxkXwg*oJc4$xy>RLiUl_mVu*vLvy&JON zxkb)5ek&1RK$;{yEKD`=MNn3jnv9a@qXe5EBuJAnClhs#x?6^369+v5JmZU+e?M|m zKxdPrBM5+oh4b5xWZb={(Af11m4XRszh;j=P1JFA;#C!D!s#SDsTk@VED|WW4%#qvRWvht zwoaju?-Y@zYKqux*<~5R8eElEj!DdBYbRfd$R0iN$7`{M2DKHCMwget zEefy6y8?9nmtr7z)>)D^X$Ez*Dr_)Ua^8HCli*;_>Rbf(sI&72@cw<6`x45k4xZJP zeO_8~z{07R+7wz-1RQf|3`{ox1$2n=6fSs3;iGgr$6TVuC}%0c!_zH0}u0 zgYS!=#u$wTa5lBD+8o7q%)gGX=54uS4vs5RBR4NUKR+eK=;fnOKE4}TTJlsL)?Yp! zvHl3HufY7~ji(ST7EGYj;^MFED28;Ii#nbPtUXL$`C$z%3j>mg1 zM@}^pCLP#6T}a~-`Qk$WZ1BCSfqoS8z=;@+Uz!n1qHJ>@JDeh_Tk811;mjEFDs-8# zjtY8*O^9yj>f#cGTY5{L@HfP(Uesz&9kg7+wi02L>kli$O0C+Vucq->BoDcQ;gT=!phEcvPvTR2uFIK=BJp(z z%N@$r}4%haimz$ZTa8=uIanSD9%Tf^4}jcJVhr<5-2^*`Hr0R z4c6g;2J7aySj@}==cAgsPdBQNw3tKhEi@S?bR!}zGs#Cub|MdT7dS7Fn(t2lBT7-x zGGwRo-ocr%GVNSmTFMtF3H3zJ^%bb0!z_zD-4X&R1;*AEvl^eoJAEiM)Nts1E@`1p zYYs)5s;^`yAX}03pzce9r!mi*orI$T!2QbbWmh~Fc1)@uFDk&hnZtb8lXeFyWg#gFfX4= zJO84ET0XqMArui9RuODWh|-?8$$z=m9q~~H4wD9dkc21+SNETAaMTQ2@H|3xPF|{; zqAZiJKo`q%oSo@v!2r}jVDe?>9Sr+BlIk2A^BXtVvC5~TF) zEleJ}dp-<|5}$lfdrExF-aZ`yH>Qvfz8^Y@^LqT-tV`^&NBus6(G?EVfsy6Efrsh4 zGqQxpH|26LMGFSEV9L1@DQ~6n%zzTCSMGv-RbLT(M`!B8n+^1kAI1}Jj__#X`YIn* zDeH-cSZSy>8qdd8a#Dz3oR#K`xyf=muDi!bFsV?MVuebrC+|iKDcd-}C^l!C)Ic4e zkQuz&j`>QQq~Wm*j~%V!>oA7xb=*aGVZ+IkR`$VG$j~_I?QU>b6%`dh8qg+FP2a~k z9pVv(TR`bxBj(d!wAa`D2+ti@)z2En!DUP(qe@ES0g?;uf*AZ!mo1*4FqH26jL15a z-=F)eBDkdScDd**LOI#85jDG#p>rflEKR@=Xkf=`h=CJmg#$gu=IViYsmiK2Ul2hS zI_KMn)LfP?Y^3E zKG=b=PKp~)F}mP|ns2<^V&?vK6a74u)fZq7Y7%*S0RJs0wJA@IY#hb^``MdYv0%w()=I6-$cyy8jl@}yK|_3+lGF!g_($UuOnx6ZrlF*ca9_4A!@LLh~b3+ zck#}#3E<9CZa*7+Xw8Sh@Et^3_^T2PFXx2qW{3XP;s5gs(C_@nM}{uY0kB$G8C1i` zHz0CPmzh8-sw4MC|p~rj2eYs%S zNh~k#<9rMns=~);z*zKh_&U%}m5#zR{;=%=*|7TC+SL4;#fg_p@)do3ec+&SbaXVI zfn31cNWR(}To1pc06LjhfHeHIsLmR{_{eY*i%lh*Q3>iq!aVbiX$~q)^1MyLvsfM% zPoF+D7*gTOj4|;8at<)!Hf#7|h?Aj9&OXrlr241naHiS2UFlG$NUM4hQ08t92Dl{1 z0*?4ie`Y}-W+o;SgRjA&9Qlq7;4zo;dufq_+n*c1wr1jw?K14Tdky+%;IFM?Ivw!^1{docqB~|oA3=T*1cn6W0HwNGB(JWZ|Nx7MrPcI zcWmAXakb-W6YJt=o)$6FVrk#q3dG)LyP=;Cmu7Bn(JjYf{ScE>EBHChmp*NdcCsY! zdQjJ^$OLJz7%$PuQ-Y1V9D_tXi6}f;+uDMF;u&9v=Cfs_@u%|0b^J7_Qe$7+KJXuu z=$fI=0CDyE;2x;w5Ot?^YJfLT_*#{NxsY`r~;D zhHu$GTvD)q4~RZccr4UJvM-=e*1cMU(zlF*B?1-XOviRiCvEYSCbAxeT?qAZL3+-N zgh3+&jm8$xiY{5K-V#11sUMR_TKM=@%37-N2i;zR60HQR=!M2v87khk7?W{l`tpRy z@$?$W;d$7QwN7${5?GO`cY_o?47n&+bJ2EURdc>z{uF{%j5(?3@vuPii{xLihrmSRK9v(|VD%erHP#^1Z*99~pVcw~MV8LfW+AIx$T zHqjMr@@Mn~>lc+MmoY3LLwe9(2P^vo@3;2#0SoN|wSulkO*^8H*}x`rpWngd6WO z1p%5oiMmVHMe=6(+7;=H4uF>4B`0vg64GgjDC9eirC0St_ith=hE_Xu&P7B5BnSz;O7UoA*6(p>d~N6 z&;El>o##sLIukEVux}6SdjU^tPxmsdkpO)Jjm9+Ee;V)#hf1(yeN!(Fk}_3?cEEZ8 z@BJz`5?-+0KH=!8(E5od7Yvglp9$jGEgbY+IX1~$PfE*nDlU+vvwV3gy3Ccv=?&7C zeDoyFZs^4y=c?(MLXv!QU0JB9{TA4Qd!VTn>W-Id1;FY}<0~}EZ6w*UzXXZ4NjWILVz=A0iAN@?AK%&ht;Typ3)=lm14v{E`>p^Z&kiHA4sChB{ zm?AI9%wwU1k!9$#n&@_(0p7ZbhPsQUpF2{3eM9O685761rqxEs1psm!&aI!L> zT%nd-uuCxGoIOTHs);txA+4ZS)tAib-vMW^=(L*$(SeMiXf1Cg$JbWD+e)=YxKX#9 z&7eNyo43KHb2oBe{49=MCcP{ygilQ_LwLjo)Q{d}Vh3DaXF{)GAUvG|I4r9jF7C=W z?#WraI|POBQ;<3H>?>!Z}vz|zk`E=#XDUFf^Zq! z@hv3~qFPRt;=!U~Q8kQ~_-D{rksE-TNf*E4ithC7eH`KX*1=7okkfQao@Ov-uFTzd z)vwW~39e`i{-i_qjiM}KpT~9Z9amp?6V%UCQ&CM;Z9c=ez?I9$#==r+^nl4dte#5v zZLl`WCI&NP$B&+Q{AJ@SfE9gg-o}CTO}OKcilSUK1(V%?4!&A$PG<9`qHV~pYh*iM z3y4PE+b4fQDT%LLmOGFRc*E~2kvotJxXK+S?~djce|aYa@q@LSW;-UZ4GG_BGrEUu zbA>QC zpzeP2;q*WMkiMU7|H{t6@4|`cZvDxf=ehG*>^#%I-Qln9%Fp*uWjjJT9(?{#)&I?Y z`OlAk@uu>%Ye&5cs61LbJ4Y8*q0!~brppoUWqgh#M1wrcM!K;6b@&|!!{ieCPxJ%a zi{}+JwW!cKl@vs`&?&5seAuFe6I8tCmW&!am!$<1X3_oxOwcf$hN}-`yq<+ja{-%}n zJLIG`U0`ZX4pRmQv^}WfqG*e2%55s{)q3YQGzUeGo;)3R$l%R@+y}a~C-HaHb|h-z zC)=1)kWb*auLQV|p+wxa2!!CyvwQ@%j84SAFtf1mdugCmc=l1!$JiU#S(`#A5W5Av zDZ|4@!G}lW36*dG6Bu*=qciM19LcV`1JC;#5T)%LsaJ;g2h3tILYAhOwEw_YUiH+? zx?D44)z-^_W@#M-TA5shXg^g4Mut2lFClf^x7@P~YEuBHp)0>qc^O zh89t=hT16;7ESMUe4#kxc*r1#mK(kyDlCsl>tIw9Nc6ZZ>D{H3n8&6)cK53JhLn52 zn&zYr<{Z-`)yNc*OPoXQ+od|P(?VePHzda!rhtixZE+lHq&R0?0Kw#y8mCFFxBveb z)it}Y%I|e?VwQcIN?yIXHa2l75fwds_qp`y$y(Xn4lVA@;2u|ypB&PpCT*!5d)bVRaVwhfSP#drmlxb%i zFYr3d*LZrs?^boh8}`ur8mYBK=?{rjSLEV_a+{B|V0??6sm0yNG^<9LVE)OP1gmiwkkl}CWGC)4Guj43tLewU;r;$v1@@cl* zl+2AMJ>*&PVII=1?v0}P@y}&Uux!dp?MN(HuL;vR&kme0wlo@4!rmCpysKdOC)a?6 zBt{2oV>F)-Oj!V8`jBTnj80f&Q#Litu+oDe0BeiG5RjaOG#8{eyRyOt&h(g)g^Hdg zdEIof+^D6`;Jc8b`j&p}l-W~6g_b@8;NA+2jhY&HOp~vkRTVjLkis_Z54U>K{`mk~ z9P6n^Dl6jDYdW|7!drHfV@j_lO_Dg@zR8G42(H}(nwa`v47wx?T0YM~@l2j9uII#z zxx!59iO!piwT{=I;unk=;mU1V#(45_-WQ%3$$tejTiV312xUM)bb($H z2~Z;lYY2qOhu?uW^nwN>_SRUVCOG0@DC6j))--YvrTJ(vdafz7v>v1_oYtig`N2ry026Dg^IMrwvZ>l6$n66UoQ}p1H z!|5KQ{&hUh<*&h*ZxrzAV#dWLM^6YFZb9Yq@>uH9lqc3=4!aKAZsS0*bgrx$Raqo=9IH!IZ;ll`J&DqB(%@wBA-J9p) z4-4e;4O~9Z)lfgfWYh=^g%F!sbrOt`q~{k9D7MTwy1y|A^{DQ{DVVc^L1>468bbhMKvW=SJfm6WDjkIvd+bmkhyr8)`` zthV5ku;gfoo#qdivCi3p2snM*|~iSS8eOPJG5oMIgIzrsjZuKpwPrG^73zR>o1PbfEL@^ z;A)>$!|ZYLha?BYAM6|*0hspnQTDW8B7AjSJxbT3*@6SC19lnjgoFA9&-?%l;Q1sdB;xr(>*u|5jeZd>wupr98LQM=63A|u3(NXAlNfy~Q8 za#(IaX5?ixkeMfypq>lFd(b9kdlrgrVp&+1z~0=N~<|Pl@u&O5zz+R}psYQ#0(TJs+)@O_@Xu-OCC-ZuS%m zvKBys0{VVL5X#_rAo3Jn=HcZ95wr&roYu^&LHYugked8w$U{>cCb|$4;2&i%HTgYv zBB7F%-=TQ!$m{FwX0@UWG@Q6Y*4%btG1n^I%?lDS9alH*L2&vjrrl7~x)3XPe=VM` zN<4ZrEahkzRM9gL)r+Jm?1Fs^HNV|!&c{GkfVu}I+0c+{Fl-D2)~r!}XG2`lXu)-I z*sS1f_XaN;U?dX+fUByiz(ni$--c_Fmh8oSbY7K4Exj>FSCN3r0bWKNGmD!KS9DNSk3Z<8Io{k*A|FbzGCzJEKi&7(o1Y zHA0QkByU)*1S;?#=9LSMNuNp$m69o*+XcHTm#e_pI5Qh&8=R%7 zX&Jr|!WLYHslQh774Pn3peM$Sz(Q7dy`56gyh`#h!|Qykk$nNBUWG-BVBo;1IoK$D*S@;XJiu^A+-mM5RuWOqH*B_U!;ocGrt07gE5ofNRuP#DFrE(DIbmY8Cw;0DzZ&RBx|FXOktkdBY}E$K3Q z5u6JMoxG#Lh-1-F>CPMMVhdi`)7YVZ!2W5YFDe}(UWd_!!c0Pj-OR)fRcr54UQV{) zd*6n{DsL*^&2mLXYC>rM8hNzsG5|Dgdmat2Y+#(vESBwkpF!znq%M%5_Ix$X)>yLR z@{Jg};13sHoCP_h)%ZMIHtthEySsC5!?Z~OeIApM#Q^lDq8e!+dwv(A+u)%Es z9&Wf+>c!A>Zy~D?uA{e?rC*(sJW9CIX?Z#Y2u!G5fc7mE0%%I4J211;ux<4Dx>BnQ ztyQtHdhc_SPMe&FRuZX?(>zxzKSz1#_P)2bPtK=t;JG`^%~viM+Cp-nl^6v>;ZayUcE^=2EgS+cP^t$h40nGrTmzfN!kKw*AQXLDX< zh<5UYD;5_Tk$mB&&ZVfpd2Ara4)x~}$S7rolY}SU^*Xck2r^FI#MIA=!kb6=w^WUT z>EBZ|M(FrPOd39#CMlKxYDOEZAFp0X%zcXDvxOwEtg!7^W^eC(P!(wWE&Ga^xK_iM!iZ-wmtUU*5XrG3=P=fM zAuYK~MC9lbQbk+!0C&F>M6kMh1CJ2SP`$V;En56a?Q{E7Zl5vj4$7tq4-KL{_0bye z){KulMx*%ZC&TV0zQ3>nlcnT`{U zO*uva=o(MMfH!f!iwfiZT~Q3a;`{jQ92>a!!1q0HuU4KdUi3|F3zBRXOYK1u3) z#6pin$N<=Hvvew3zeJdBB1@~cSua$H`FI<&yh`fUGV?x%Yn$%`Y#B(WfvLcWg_y*zqB3^QLPs|9mGU0(Ql~>TfU`eGA?F zhCn$LFy4jRq3LCe_fF-p8RA&=4)Y1aMBjl5xy@QaaS87)`m%@bkMPP<2nt=<0dK%r zdi`)on&Rv}ef}Z0Q~kuP`li^~9g89y=T48(F2{!d3mn~ne?Q{sKY7i+(v-ie`#m|;Uo?2^jXFYP?jpLcdVP8KFc2jk$$J&xd=l{b#y-@hl-Zn({H zePO{|uf`W(Qwi5kLm~`X^4P(B1mb!7GQivfb5B4}{op`2X*>EE)ybOMn0nrXaPr`t zkk>677jAu#baX7fP7a7N?u`J1Riq+JU;}=PX%Nb4xCs)<%gc+F^>qiGmI$Gth{X>n zn5gEF_v$b>Gee~-XsL-HjQz;MApL$2=#r#9-YQx&{1CD+3_j`e64DDI$`CUa)TAo; zRIGuKYQ@d3-pV@+^YqT{D{cn4(Uo=a zn%Gex`qte?vG1$gj+|6zHA&r$YA;iC7+`hrl%J7;fPQ>pX&T{lgCSt-=(dwn11Oj4 z7HB@8z_l>-HZI3;BUOIz&Jxi4n?_EniW-ds(;!rw=Lb^$?B#DU2IIY9>QQQ?E z*~V+RIj%z(bSfj7bAs0SVY=nHE4vzKmF6nhTnWa8Snb251+p&O=dZS;m`JI*RK?C- zn4K+89duiv`{J_I$2TzdNlQx$;#bfC055Ru2kRQn<=KI$fb{C!%@zc1(w_VmkU5jr)$ghw(2<0) z*@D6H7Y_aUCO z3C6-uv>TPuF>=BrERHbKB&PlPy=DuTw-DEOb9}iYC?Kbl>pA)nAj_a}{7p z8JX_MZyWJql6)|-w77T+;@bs*M{wtkUoTLD{oK-|>XN;`kcWvIE9D>CY-CcGZv^V7 zqk;w-Lcxf+_5y)Yit=%-*1WCg_CG{wF8vaz=@Ef*Lw-$e2G8jiK|RS#9@7n}Yt2vj zLZp^dT8%>j1R)0tmLO-F^#eE-0dztV1cT(o+~Qp5&G$$XNtpyIj@7fnkT$e?W5Q`I z)o>3~+vDIh8g3~7*F-%_T+=MdGkXlxQq|dQp~gbf_y;@r0&jBu5v{pSe)n0k{?j<{ z)50*%vHB6K3)FuzTiSX59<~XVA*+28I>4!xp4QPIoi7`25f)kj;y0Oo@~ca!XACUZ zl;z}nsS`pzm`V}XuaYeJhp45Y;y1DAyJk7ZcJ}SK4ZEvlXRv6k@A`1M@E^m3x)DRQ zNaubixD8-~F-iMO(X{7dZ_pHmo{F~X2VS*44<+4+)Rdqy$$nLFw9#h$BouN|oC2fU z1&8VI>*NLlt$?T)<)i2uTVNC~wNhbG(VsrQdn?`69aaVY9AlDq@HfoS5sZYL_%_(o zS+7lij?_)CdDpP8!Jg~nP(VXJDU_8GsTwCA7)w)o*JStLam*V?+)U}?6F2CZ@8rhbJQbD4KfrZa41yX-L@Q zRt#tGwIEGvlGu%~Y$C^T`+Dj@;Dtl)0?8XDLmfKC_{f(Ussy0>=H$wmX9Yy?03{1f zyqNvN)dCJW$7cJ~kS5>wQAU;CbM9Jva-y33+T{h zz^O(+X@HNupr`QN`>3{IEMW~q)OR%QzK7cKL6M?#(aM^XoLf2UCq$`~RY{#A7y;67 zNAs>CHRLHg|Hym0;iI!4cSXC+k-Sn-gD`j>k~-LpW?xh!x}QxNPIp$Mc=sBnh=7qe^_17v1O!m-mT<2<+VZm_?$6(Df4YU+V&!2>J?<#K}^U6*jTaH6nb_1U|!$wJz;n?XsClI9@C zxPxXNJBMx9)Y~jF$()k(`!6SFoj1xv{XQIBBgi@=gu?FsMDyA44v)_U{ zJJv%xwjaoF=ht@N6hSx$Njq#HK)b(L8+RUKbEbqV$8S*d_lGl#LBYjy69Vrd7YyHy zNT251Iv{yCne*wtEo#gc{sOhQoIV5D@BpRV# zaP9xrHyHpU1UJS-{C5rl`=dtm?Exq#-+B!JRqrHu%-D`aKHj_e?#*ID`*C7=2A`sRh~ZE3WlOU4Dw9(?B|eWBuNq3? zJ(t&pt8z*4&PJSQ4~+ViPpIhGbXp!#eAdPadN&CH4}jk-$y=vxTk=+Wchjy1dgx1y zLWIGp}gsEG}G& ztz9N`@az2B3UH0G^FR8Xu*7ulH>oazfB~sbTIbE=Fqk$TWvjxAGMtWc6yCJ zCtx_XMQG_QZ7s$l2G^^M#-1bWKjH)xS)27^DmxiZb$Sswrvnui!#j6#^$Mwc*j~d! z&<2Yn0C@}WduGeH0Q8;+((n@806LN=Z6|ya2=RD`Vpr^BuGZH+Ut9L9Rz9eK z`p@}m7U2O`YXvRs(;gzhe*4O=COK4yeV2NWka|0OZ&HOyO-7)kO?o?Dm5!+oDpMXR zoGy$ktj4z*gXIvP$T65Fc8=^A>*TuswYT=+7eA@PzxggJ&u}N2T*PqpFWK}#cy~c; zvHxJb?dO?^&J!9iF-b%rWurjg`f7adW9&eP+QjPvjhi~P%VqoH$^~^0%YW6arlMlF z`kLV)Pi8hB)Y(H)7wY$}If#j&>><(yP3O|VXUe%v%myKL^Umo5q3u;2>A#ly^g14o zla9}rxmM07$KVPS9U2-)GVGQFF}cKpUehEG)e7_sG6zbJr^zM$7QA+*Z=>;f`3q2_ zc-`@jI`*mV?DY^KFohBFl3DWUUTq%}?8KVroLgjTHwM9QCXh#jxVfcbTGjXli;13? zfT3dwOwn5vhsn*)_VNW?dV1g@kH(MK4T!NnSw~Cw#*+=rk-;iM9KgDhyslPgi z_fE!2>x|JKc=@ByCo6rc(5Juyh6LE0yH70&?#X7))Y~Hj&3Pj*$qQP28eg*cssI!D zDa_?VqH-HsaG2ojULT~GqaI>tCwp^RGdX1P5iO7puBi0`w#*N^gi9$H71d=;vL##z zI;2cyFap4Rh6}MByI5omF3H?No&UPLXR;4UY#91mU;2bWMv*OpyT>$acl}R;q(fJF zPEJb;$7hZFtgb;wTPsO3l!wAXy;QF|FaP2d&ZARKfVHyH`A*wV0JOAbpY3!z;#oD$ z>$y^Xs4-s#(jA~8^3Z+Q{i!3?ei}n7Y0xYNtvxtHqDGm*Y;p+xuegplVmlPIam-}A z#aFnr+R9$LDNgTiROuc8DrzGR?5=FE6(EFqnyHD2o)YU1Fg@U<1d+{B;8P(O?*i#W zWjU39mZ(#i;MgHkrzc2r)dZ^&_FG*K0;FpG>L1~n#9T0Lnxt=)Na38#oL@ix zwT`FmawMQfL;^8ecOHLY-mOSi+(;7TV1@P5u4T=w^mU(B{jJv^IKNFU>~$D&*v-+q zQiy-$`?VP@*&6d9)6V-=fv|K;9ChnVzARkE(K>;LFcXJk7odgD?5+x{!K!l6m>x3s zT4#zyh*2r&n+>}(O9g+Esi%d^&Tv{SbHcI-J#@8$#FPGQ{Ehgrb=a+}^(p^4;O32t z#+PSQU!c-3U{1{ee2c>%h67|^RO>GF>D)nZ7~+O;q+vs>hj5QNRa0_ zm*23h)!F^tl_GP$Gcv_=(Vdu6bT}LfnXO{K?H9<~1U}j^#jg zK|E(AZ}k>>O4CeMtX`tpK}Oqz*TwX^&eh`j+o9=blmTK3AKuAA8Po}GKG-gZmkf6c z|G+u9va#Q(ceue`Bw_Whac;_A5!G)@p~Mb<242U?pLrMH4Sxo6ejFBNV( zvn-XJjv@9Kk>||dCo5)OT(G~|cSNwsj8YzW3N^-pYL*gJOck!|N)-Fw)I4VR0@(;n zhEQGJUx4PqW~ybS`LXvTDd~ah5hz4lzWPbs$6~!cJ~~UxKp?V_=9^_gxR@!gUOw%w zu#gEsZssM{>n0pKy@ycW@rVbA?n4wfU>7^nX-N*2Sf2E}hsv`(v)33~d+0zB2I?jf zrT>>;P>1A(iG_(wSQ=RUzxne!KjZM0XmDG{+aEi(8hXA4vE4AqmF3v9Xf9md%4fH& z569nn)O(tmC`0~iClL+oHz?1&ID?Tw)J=3s_vmBSurhEJ-W``|W)JOm@C6f^bO*nH z80&Kz*8oPr^vqjaH2#hb?iDxXoZ%KQrsGT98$w|yk*WRVD_5@ItL$$DA0k1>Gg1>V z&9IbS1-=vNyz`vnrMP_ABgy}Q{OsBptmN^4RmnP(5gLjT9h~4PLH&;I7OAL7cNf(V z&h&g}x5Bt8V(J}K*|}U^7*7wh%!@(F2Fc|Oq|>ijH5`i&!9llK_i6t}yxKQIoWDq) zOm35BZhq4INn2tq%`zazheLH+lr}x>`hV$sj3vEa3<7m?b2B*hPq3;zVp{ocGTARL zp)nF21yXfc{V7TUy7BR(ONncq;AzChxfQyFC`UJ+G45rKG-Led>c>Q$JiJ!zq51Ns zR*k0-jn+=1dtq_@j89|JfmRz5c%pd0%LgiUr14bX+O)SzU3Qg23Y2-2meSYC>S#m@8&2G~2!K$~ zH1K@rbabRZ@^k(BPc^ij_$@rqmGI>AzRapc3NagD45VO=_#|_wvs3q{l`9E~m*=0#>OPh2&A-0-ImHlLpx!|% zHO_o*ku7*lbIhSTs)3r-V0^7$j%8^CVtN9GE0uVrPzjos^rUs+@|E`nDYeqpah%_J z_yH_m9Y3Np@XfV$Xw?_PFVtY`1E(!4+vH{k_ur@M1f}n#f*TB*fk9!mwskISvEreu z>8PG!2Q;=nQ7MeCUXfO_1voI*-8W6BAMn$Sy+U?&bi96e-sTo!f|l=8{I|e1mO|MA zS%8XT1{etzG`4XCSE{o-n4VH#gl(Lf+78n+KezaToV~jg0TGg21+yin=$Ld=n}=EF z!xu!vjd%^bhqeisR$hLAfF|Rxc~iE;RAqf$HunY*re3fJe19t>H> zx_CMuQosS!`Ip&l!>usR4v!MwDv=a6+i{%?RASL|gc?0hmhHue?}> z1`B*zhFkHU<1q;_f;$0i!8DYTjvB-)q5HtUuGqY94124n|1~XI^@DI|5otfP1hP># zeY}3dTwphEmcO(A0q(6%M?=1)al)1y^O1mpE3^2Pgi_0}%5xt&pk!;=+;G21`FQIq z7<7C3@-v%Y>#RM6QQh+Opl81(T4x=h?~7(kL*>a@gf+<7>wlvgUUmG2;K4l;S*SxR zYry*6+39umjD7jGhp}mH$HOQ|34=c3lfmN$eLcT4wv$J# z@&J7(vp$XG&FV6-!JRzZDBz9TC>DS^?W8&FhoTj>IndeTfeja+cfc1@O3&mSfX*Ih z`k^vQ9c{N*5xfagVX(*P^@+rlUH5C1JRI<_B=T&$(qx)6O*h5fS<=NbF6qz+5D?*q)f&-EBy=+oJn`s`dfI0)iOMI|Nbx4L3DSr$)K_ zXq1g4%DFnK4`nXK2sC2&x_xiEx#qP_mC1G0cS(@SeppTP7#^+q~g-9P>Zmp&e)A?_}N%4EmpL|JR3?iaNe|bo!t5x8c^EXWNkUH%RvP zKQU0St#Ip)C8HX4h9B9L{}?d)V@z$F9ikl=!tEhMP`3!g_m8Q#+aqXy&ePowfj8=h zJM)rY-1P6$l#HQ-60$nK|M(xKAe9}%c)$DmBPE$(J#Op_<9$7d^o4QMKW6XJp7)A4 zZ~7`z*|Vz4aS*GSLL?W#eSdng`X-s2aDL&mKT+juBQ^__@530ZBdV+yc|L7DE#=#2 zk5Tl5`(#>9Kzex-(4~VKWi74}TlXXonik2PBjR7<6(qPhx792+5ggpNz|eQ#X65 z52+%gG2hZ!*jZL1PgX}Xq57?Hiy_s^%N5b3**q#!vCghBOH2!7>JKlg`Yh}pn;mFe z#!d;B6Z`1Uw_A1@t(=?5;nC-{K8Y)G1pmP(MYarDE^OC&ghzs*as~Z^YZZ zw!t3qs{7JG{F}gB!`80+t@n+2Wj1!oWc|8gkT#<$E9yqZS|8I?4lxBME`+yTFN6Qj zA(c>5*9JX-i+)DWW(VS1WW(#FoAtx|r-=rOb+5jA{f4~FE<^3R_7M?w_&FEniR-pyiLewbG`}>rryDzu8km) zt$k)}6{B-?%}bMXFNTZu0scalqs55x^Gmy$Q`s{!#m*KyT{ginsk17sdQ;s&J52E_ z9qBp=0WHpMA>-^NQ@Vu3oMUbA`TJ{;Jv}-w1m^mE%jU-q*&JKBzy)bHu%=GDkspD9 z_uh$V&jK(mRFS!FZ!IY2${HNym_S~h(e7XC9ysHWf2)^Na{EOw#AcSoVWOD?-Whx^ zTUxH;8EA?;cCCnt7Dl{>#H8er7<=s-8<;L#3x%j0TpC8dPiIUlTiyX5tD*xzV}7pe zviln}LRlJvwz3$$WBy&gdel+G2-|Uheu6C+)7!cbs##pkd&{?!@vgX&N>~y@$y72-K#}}_37#R9T4p^M#Y2YS_DrHt?~LbP2DQ^!^SzA zt^LVB?e2}U+Dk_c4iqIMJVXVRTJ)Z~+H>kte(2@o$WJ$et<{M;W_bIC)jf24r5~{I z`058+Zyv_^#q(1gv|;Y(t~626$%ao~?kqmrFvhPG>|fj9zgVS5Yb&-j&J>Qv%41y` zlJq9sS@u#tQ|L+Ydk7~ITw#rkP1-UkEUvY~+QZlsac+@+^8V6#>6mWPXi)n3^X0~Q zRNZzrO?#%}>#>R+q^dnvsR!y^?Zz#JyBXzdK17tox=fk|GooG~sGmkj>2>u{(LGT3 z&IUT|MhILH;>wMKFK6$a!4IMTuU5|=L*aj}%|HM3_f`JqgAO?37GIz74>-9!5Yg~D zHd*#8C9ef_&t3dqhqYG8UvU0)$64|3BBO2}4Fag$ngE9|*okY{8D6(T@!zmP!T)w4 zIg#9mbDW5CJSykT^PlJDNAj|Aa`JO>y4>Bf|1Wniw=_iGbpH1{9PTeG!y_Fjae1-q I%lh~JA2CInx&QzG diff --git a/screenshots/plugin_manager.png b/screenshots/plugin_manager.png index c9ae9d1297be594785c7604f48ae1b0c97f6b9b9..131d90942f5a2dc95b5a5d53641ad2206f6628e4 100644 GIT binary patch literal 62153 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

wuIK_T~%S}e^RRC#ycqdiVf9a%42J~0(4 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 literal 29855 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

(St{D)vTaqlp1|qXp>Tq71Otyq{BwyvGXx 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

$?%z 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 G{{I6W6*3h7 literal 45979 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 O1_n=8KbLh*2~7aDxMz+4 diff --git a/scripts/create-package.mjs b/scripts/create-package.mjs new file mode 100644 index 00000000..eb3ffb71 --- /dev/null +++ b/scripts/create-package.mjs @@ -0,0 +1,307 @@ +#!/usr/bin/env node + +/** + * create-package - ES Engine 包脚手架工具 + * + * 使用方法: + * node scripts/create-package.mjs [options] + * + * 选项: + * --type, -t 包类型: runtime-only | plugin | editor-only + * --scope, -s npm scope (默认: @esengine) + * --external 外部模式,用于用户在自己项目中创建插件 + * + * 示例: + * # 内部开发 (monorepo 内,使用 workspace:*) + * node scripts/create-package.mjs my-plugin --type plugin + * + * # 外部用户 (独立项目,使用版本号) + * node scripts/create-package.mjs my-plugin --type plugin --external --scope @mycompany + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import readline from 'readline'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const ROOT_DIR = path.resolve(__dirname, '..'); +const PACKAGES_DIR = path.join(ROOT_DIR, 'packages'); +const TEMPLATES_DIR = path.join(PACKAGES_DIR, 'build-config', 'templates'); + +const PACKAGE_TYPES = { + 'runtime-only': { + description: '纯运行时库 (不含编辑器代码)', + examples: 'core, math, components' + }, + 'plugin': { + description: '插件包 (同时包含 runtime 和 editor 模块)', + examples: 'ui, tilemap, behavior-tree' + }, + 'editor-only': { + description: '纯编辑器包 (仅用于编辑器)', + examples: 'editor-core, node-editor' + } +}; + +const CATEGORIES = [ + 'core', + 'rendering', + 'physics', + 'ai', + 'ui', + 'audio', + 'input', + 'networking', + 'tools', + 'other' +]; + +// 默认 scope +const DEFAULT_SCOPE = '@esengine'; + +// ES Engine 核心包的最低版本要求 +const ESENGINE_VERSION = '^2.0.0'; + +function parseArgs(args) { + const result = { + name: null, + type: null, + scope: null, + bExternal: false // 外部模式标记 + }; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--type' || args[i] === '-t') { + result.type = args[i + 1]; + i++; + } else if (args[i] === '--scope' || args[i] === '-s') { + result.scope = args[i + 1]; + i++; + } else if (args[i] === '--external' || args[i] === '-e') { + result.bExternal = true; + } else if (!args[i].startsWith('-')) { + result.name = args[i]; + } + } + + return result; +} + +function createReadlineInterface() { + return readline.createInterface({ + input: process.stdin, + output: process.stdout + }); +} + +async function question(rl, prompt) { + return new Promise((resolve) => { + rl.question(prompt, (answer) => { + resolve(answer.trim()); + }); + }); +} + +async function selectOption(rl, prompt, options) { + console.log(prompt); + options.forEach((opt, i) => { + console.log(` ${i + 1}. ${opt.label}${opt.description ? ` - ${opt.description}` : ''}`); + }); + + while (true) { + const answer = await question(rl, '请选择 (输入数字): '); + const index = parseInt(answer, 10) - 1; + if (index >= 0 && index < options.length) { + return options[index].value; + } + console.log('无效选择,请重试'); + } +} + +function toPascalCase(str) { + return str + .split(/[-_]/) + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(''); +} + +function processTemplate(content, variables) { + let result = content; + for (const [key, value] of Object.entries(variables)) { + result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value); + } + return result; +} + +function copyTemplateDir(srcDir, destDir, variables) { + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }); + } + + const entries = fs.readdirSync(srcDir, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(srcDir, entry.name); + let destName = entry.name.replace('.template', ''); + + // 处理文件名中的模板变量 + destName = processTemplate(destName, variables); + + const destPath = path.join(destDir, destName); + + if (entry.isDirectory()) { + copyTemplateDir(srcPath, destPath, variables); + } else { + const content = fs.readFileSync(srcPath, 'utf-8'); + const processedContent = processTemplate(content, variables); + fs.writeFileSync(destPath, processedContent, 'utf-8'); + console.log(` 创建: ${path.relative(PACKAGES_DIR, destPath)}`); + } + } +} + +async function main() { + console.log('\n🚀 ES Engine 包创建工具\n'); + + const args = parseArgs(process.argv.slice(2)); + const rl = createReadlineInterface(); + + try { + // 0. 确定模式:内部 (monorepo) 还是外部 (独立项目) + let bExternal = args.bExternal; + if (!args.bExternal && !args.scope) { + // 自动检测:如果在 monorepo 根目录运行,默认内部模式 + const bIsMonorepo = fs.existsSync(path.join(ROOT_DIR, 'pnpm-workspace.yaml')); + if (!bIsMonorepo) { + bExternal = true; + } + } + + // 1. 获取 scope + let scope = args.scope; + if (!scope) { + if (bExternal) { + // 外部模式必须指定 scope + scope = await question(rl, 'npm scope (例如: @mycompany): '); + if (!scope) { + console.error('❌ 外部模式必须指定 scope'); + process.exit(1); + } + } else { + scope = DEFAULT_SCOPE; + } + } + // 确保 scope 以 @ 开头 + if (!scope.startsWith('@')) { + scope = '@' + scope; + } + + // 2. 获取包名 + let packageName = args.name; + if (!packageName) { + packageName = await question(rl, '包名 (例如: my-plugin): '); + } + + if (!packageName || !/^[a-z][a-z0-9-]*$/.test(packageName)) { + console.error('❌ 无效的包名,必须以小写字母开头,只能包含小写字母、数字和连字符'); + process.exit(1); + } + + // 检查包是否已存在 + const packageDir = path.join(PACKAGES_DIR, packageName); + if (fs.existsSync(packageDir)) { + console.error(`❌ 包 ${packageName} 已存在`); + process.exit(1); + } + + // 3. 获取包类型 + let packageType = args.type; + if (!packageType || !PACKAGE_TYPES[packageType]) { + packageType = await selectOption(rl, '\n选择包类型:', [ + { value: 'runtime-only', label: 'runtime-only', description: PACKAGE_TYPES['runtime-only'].description }, + { value: 'plugin', label: 'plugin', description: PACKAGE_TYPES['plugin'].description }, + { value: 'editor-only', label: 'editor-only', description: PACKAGE_TYPES['editor-only'].description } + ]); + } + + // 4. 获取描述 + const description = await question(rl, '\n包描述: ') || `${packageName} package`; + + // 5. 获取显示名称 + const displayName = await question(rl, `\n显示名称 (默认: ${toPascalCase(packageName)}): `) || toPascalCase(packageName); + + // 6. 插件类型需要选择分类 + let category = 'other'; + if (packageType === 'plugin') { + category = await selectOption(rl, '\n选择插件分类:', CATEGORIES.map(c => ({ value: c, label: c }))); + } + + const modeLabel = bExternal ? '外部 (npm install)' : '内部 (workspace:*)'; + console.log('\n📦 创建包...\n'); + console.log(` 模式: ${modeLabel}`); + console.log(` Scope: ${scope}`); + console.log(` Full name: ${scope}/${packageName}\n`); + + // 准备模板变量 + // 依赖版本:内部用 workspace:*,外部用具体版本 + const depVersion = bExternal ? ESENGINE_VERSION : 'workspace:*'; + + const variables = { + scope, + name: packageName, + fullName: `${scope}/${packageName}`, + pascalName: toPascalCase(packageName), + description, + displayName, + category, + depVersion // 用于 package.json 中的依赖版本 + }; + + // 复制模板 + const templateDir = path.join(TEMPLATES_DIR, packageType); + if (!fs.existsSync(templateDir)) { + console.error(`❌ 模板目录不存在: ${templateDir}`); + process.exit(1); + } + + copyTemplateDir(templateDir, packageDir, variables); + + // 重命名特殊文件 + if (packageType === 'plugin') { + // RuntimeModule.ts.template -> {name}RuntimeModule.ts + const runtimeModuleSrc = path.join(packageDir, 'src', 'RuntimeModule.ts'); + const runtimeModuleDest = path.join(packageDir, 'src', `${toPascalCase(packageName)}RuntimeModule.ts`); + if (fs.existsSync(runtimeModuleSrc)) { + fs.renameSync(runtimeModuleSrc, runtimeModuleDest); + } + + // Plugin.ts.template -> {name}Plugin.ts + const pluginSrc = path.join(packageDir, 'src', 'editor', 'Plugin.ts'); + const pluginDest = path.join(packageDir, 'src', 'editor', `${toPascalCase(packageName)}Plugin.ts`); + if (fs.existsSync(pluginSrc)) { + fs.renameSync(pluginSrc, pluginDest); + } + } + + console.log('\n✅ 包创建成功!\n'); + console.log('下一步:'); + console.log(` 1. cd packages/${packageName}`); + console.log(` 2. pnpm install`); + console.log(` 3. 开始编写代码`); + console.log(` 4. pnpm run build`); + + if (packageType === 'plugin') { + console.log('\n插件开发提示:'); + console.log(' - runtime.ts: 纯运行时代码 (不能导入 React!)'); + console.log(' - editor/: 编辑器模块 (可以使用 React)'); + console.log(' - plugin.json: 插件描述文件'); + } + + } finally { + rl.close(); + } +} + +main().catch(console.error); diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 00000000..12fa8f10 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noUnusedLocals": false, + "noUnusedParameters": false + } +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 00000000..4757370d --- /dev/null +++ b/turbo.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://turbo.build/schema.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", "bin/**", "pkg/**"], + "env": ["NODE_ENV"] + }, + "build:watch": { + "dependsOn": ["^build"], + "persistent": true, + "cache": false + }, + "build:npm": { + "dependsOn": ["^build:npm", "build"], + "outputs": ["dist/**", "bin/**"] + }, + "build:release": { + "dependsOn": ["^build"], + "outputs": ["dist/**", "bin/**", "pkg/**"] + }, + "type-check": { + "dependsOn": ["^build"], + "outputs": [] + }, + "lint": { + "outputs": [] + }, + "lint:fix": { + "outputs": [] + }, + "test": { + "dependsOn": ["build"], + "outputs": [] + }, + "test:ci": { + "dependsOn": ["build"], + "outputs": ["coverage/**"] + }, + "test:coverage": { + "dependsOn": ["build"], + "outputs": ["coverage/**"] + }, + "clean": { + "cache": false + } + } +}