diff --git a/GlitchyEditor/EditorUI/BeefProj.toml b/GlitchyEditor/EditorUI/BeefProj.toml index 42d9512..0fb9eb7 100644 --- a/GlitchyEditor/EditorUI/BeefProj.toml +++ b/GlitchyEditor/EditorUI/BeefProj.toml @@ -9,5 +9,5 @@ StartupObject = "EditorUI.Program" BuildKind = "StaticLib" BuildCommandsOnCompile = "IfFilesChanged" BuildCommandsOnRun = "IfFilesChanged" -PreBuildCmds = ["npm run build"] +PreBuildCmds = ["npm run build:debug"] PostBuildCmds = ["CopyToDependents(\"$(ProjectDir)/dist\")"] diff --git a/GlitchyEditor/EditorUI/package.json b/GlitchyEditor/EditorUI/package.json index 305799c..0755078 100644 --- a/GlitchyEditor/EditorUI/package.json +++ b/GlitchyEditor/EditorUI/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", + "build:debug": "tsc -b && vite --config vite.config.debug.ts build", "lint": "eslint .", "preview": "vite preview" }, diff --git a/GlitchyEditor/EditorUI/src/Components/Menu.css b/GlitchyEditor/EditorUI/src/Components/Menu.css index 2cb46f9..4c4348c 100644 --- a/GlitchyEditor/EditorUI/src/Components/Menu.css +++ b/GlitchyEditor/EditorUI/src/Components/Menu.css @@ -14,7 +14,7 @@ align-items: center; - --menu-item-height: 2em; + --menu-item-height: 1.5em; } /* Remove indentation from lists for menu */ @@ -25,7 +25,7 @@ .menu-item { /* Doesn't change our position, but does something to it, so that - following absolutes are absolute to us? */ + following absolutes are relative to us? */ position: relative; height: var(--menu-item-height); } @@ -105,6 +105,7 @@ background-color: var(--menu-bar-background-color-click); list-style: none; z-index: 10; + height: min-content; } /* Show submenu */ diff --git a/GlitchyEditor/EditorUI/src/EngineGlue.ts b/GlitchyEditor/EditorUI/src/EngineGlue.ts index 711341c..5dd97e3 100644 --- a/GlitchyEditor/EditorUI/src/EngineGlue.ts +++ b/GlitchyEditor/EditorUI/src/EngineGlue.ts @@ -1,4 +1,7 @@ -export interface IEngineGlue { +import { Entity } from "./Entity.ts"; + +export interface IEngineGlue +{ // Titlebar Events handleClickCloseWindow(): void; handleClickMaximizeWindow(): void; @@ -9,6 +12,9 @@ handleHoverNonClientArea(hover: boolean): void; onClickCreateEmptyEntity(): void; + + requestEntityHierarchyUpdate(): void; + onUpdateEntities?: (entities: Entity[]) => void; } function logNotImplemented(functionName: string) @@ -54,19 +60,38 @@ class DevEngineGlue implements IEngineGlue { { logNotImplemented("onClickCreateEmptyEntity"); } + + requestEntityHierarchyUpdate(): void + { + logNotImplemented("requestEntityHierarchyUpdate"); + } + + private callFromEngine_updateEntities(entities: Entity[]): void + { + console.log("Yeah!") + + if (EngineGlue.onUpdateEntities) + { + EngineGlue.onUpdateEntities(entities); + } + else + { + logNotImplemented("onUpdateEntities"); + } + } } -// Globales Objekt deklarieren -declare global { - interface Window { +declare global +{ + interface Window + { EngineGlue: IEngineGlue; } } -// Initialisierung: DevEngineGlue als Fallback setzen wenn noch kein EngineGlue existiert -if (typeof window.EngineGlue === 'undefined') { +if (typeof window.EngineGlue === 'undefined') +{ window.EngineGlue = new DevEngineGlue(); } -// Export für einfacheren Zugriff in der Anwendung export const EngineGlue: IEngineGlue = window.EngineGlue; diff --git a/GlitchyEditor/EditorUI/src/Entity.ts b/GlitchyEditor/EditorUI/src/Entity.ts new file mode 100644 index 0000000..395b36b --- /dev/null +++ b/GlitchyEditor/EditorUI/src/Entity.ts @@ -0,0 +1,16 @@ +export type EntityId = number; + +export class Entity +{ + name: string; + id: EntityId; + visible: boolean = true; + children: EntityId[]; + + constructor(name: string, key: number, children: EntityId[] = []) + { + this.name = name; + this.id = key; + this.children = children; + } +} diff --git a/GlitchyEditor/EditorUI/src/Windows/EntityHierarchyWindow.tsx b/GlitchyEditor/EditorUI/src/Windows/EntityHierarchyWindow.tsx index 875343f..21af3c6 100644 --- a/GlitchyEditor/EditorUI/src/Windows/EntityHierarchyWindow.tsx +++ b/GlitchyEditor/EditorUI/src/Windows/EntityHierarchyWindow.tsx @@ -2,7 +2,7 @@ import {MenuBar, MenuItem} from "../Components/Menu.tsx"; import "./EntityHierarchyWindow.css"; -import {MouseEventHandler, ReactElement, MouseEvent} from "react"; +import {MouseEvent, MouseEventHandler, ReactElement, useEffect} from "react"; import IconVisible from "../assets/Icons/AntDesign/eye-visible.svg"; import IconInvisible from "../assets/Icons/AntDesign/eye-invisible.svg"; @@ -10,47 +10,61 @@ import {Updater, useImmer} from "use-immer"; import {EngineGlue} from "../EngineGlue"; -type EntityId = number; +import {enableMapSet} from "immer" +import {Entity, EntityId} from "../Entity.ts"; -class Entity -{ - name: string; - id: EntityId; - visible: boolean = true; - children: EntityId[]; - - constructor(name: string, key: number, children: EntityId[] = []) - { - this.name = name; - this.id = key; - this.children = children; - } -} +enableMapSet() type EntityMap = { - selectedIds: EntityId[]; + selectedIds: Set; [id: EntityId]: Entity; }; const entityHierarchy: EntityMap = { - selectedIds: [], - - 0: new Entity("Entity Root", 0, [1, 4]), - 1: new Entity("Entity Entity 1", 1, [2, 3]), - 2: new Entity("Entity Entity 1.1", 2), - 3: new Entity("Entity Entity 1.2", 3), - 4: new Entity("Entity Entity 2", 4, [5, 6]), - 5: new Entity("Entity Entity 2.1", 5), - 6: new Entity("Entity Entity 2.2", 6, [7]), - 7: new Entity("Entity Entity 2.2.1", 7, [8, 9]), - 8: new Entity("Entity Entity 2.2.1.1", 8), - 9: new Entity("Entity Entity 2.2.1.2", 9) + selectedIds: new Set(), + // 0 is hardcoded to be the root. The editor must provide this "pseudo"-entity. + 0: new Entity("Root", 0, []) }; export default function EntityHierarchyWindow(props: IDockviewPanelProps) { const [entities, updateEntities] = useImmer(entityHierarchy); + useEffect(() => { + EngineGlue.onUpdateEntities = (entities: Entity[]) => { + updateEntities(draft => { + try + { + for (const entity of entities) + { + // console.log(`${entity.id}: "${entity.name ?? "deleted"}", + // ${entity.visible ? "Visible" : "Invisible"}, ${entity.children.length} Children: [${entity.children}]`) + + if (entity.name === "undefined") + { + delete draft[entity.id]; + draft.selectedIds.delete(entity.id); + } + else + { + draft[entity.id] = entity; + } + } + } + catch (e) + { + console.log(`Failed to update entities: ${e}`); + } + }); + }; + + EngineGlue.requestEntityHierarchyUpdate(); + + return () => { + delete EngineGlue.onUpdateEntities; + }; + }, [updateEntities]); + return ( <> @@ -83,7 +97,6 @@ export default function EntityHierarchyWindow(props: IDockviewPanelProps) - ); } @@ -94,7 +107,7 @@ function EntityTreeItem({item, allEntities, updateEntities}: { { //const [isOpen, setOpen] = useState(false); - const isSelected = allEntities.selectedIds.includes(item.id); + const isSelected = allEntities.selectedIds.has(item.id); //const isVisible = allEntities[item.id].visible; function handleClick(event: MouseEvent) @@ -103,11 +116,11 @@ function EntityTreeItem({item, allEntities, updateEntities}: { updateEntities(draft => { if (isSelected) { - draft.selectedIds = allEntities.selectedIds.filter((id) => {return id != item.id}); + draft.selectedIds.delete(item.id); } else { - draft.selectedIds.push(item.id); + draft.selectedIds.add(item.id); } }); @@ -180,9 +193,12 @@ function EntityTree({items, updateItems}: { items: EntityMap, updateItems: Updat
    { items[0].children.map((childId) => { - const entity: Entity = items[childId]; + const entity: Entity = items[childId]; - return ; + if (entity === undefined) + return; + + return ; }) }
diff --git a/GlitchyEditor/EditorUI/src/Windows/MainWindow.css b/GlitchyEditor/EditorUI/src/Windows/MainWindow.css index fb7590f..262e07e 100644 --- a/GlitchyEditor/EditorUI/src/Windows/MainWindow.css +++ b/GlitchyEditor/EditorUI/src/Windows/MainWindow.css @@ -52,7 +52,7 @@ { display: flex; grid-area: title; - height: 3em; + height: 2em; align-items: center; } diff --git a/GlitchyEditor/EditorUI/vite.config.debug.ts b/GlitchyEditor/EditorUI/vite.config.debug.ts new file mode 100644 index 0000000..68caeae --- /dev/null +++ b/GlitchyEditor/EditorUI/vite.config.debug.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig(({ mode }) => { + const isDebug = mode === 'debug' + + return { + plugins: [react()], + base: "./", + build: { + target: "es2015", + minify: false, + terserOptions: undefined, + rollupOptions: { + preserveModules: true, + output: { + dir: 'dist', + entryFileNames: '[name].js', + chunkFileNames: '[name].js', + assetFileNames: '[name].[ext]' + } + } + } + } +}) diff --git a/GlitchyEditor/src/Ultralight/UltralightMainWindow.bf b/GlitchyEditor/src/Ultralight/UltralightMainWindow.bf index 4067c9d..c682305 100644 --- a/GlitchyEditor/src/Ultralight/UltralightMainWindow.bf +++ b/GlitchyEditor/src/Ultralight/UltralightMainWindow.bf @@ -1,6 +1,10 @@ using Ultralight.CAPI; using System; using GlitchyEngine; +using GlitchyEngine.Collections; +using GlitchyEngine.World; +using System.Collections; +using GlitchyEngine.Core; using static GlitchyEngine.UI.Window; namespace GlitchyEditor.Ultralight; @@ -11,7 +15,254 @@ class UltralightMainWindow : UltralightWindow { } - + + public override void Update() + { + UpdateEntityHierarchy(); + } + + struct EntityEntry : IDisposable + { + public Entity Entity; + public String Name; + public UUID ParentId; + public List Children; + public bool Visible; + + public this(Entity entity, StringView name, UUID parentId, bool visible) + { + Entity = entity; + Name = new String(name); + ParentId = parentId; + Children = new List(); + Visible = visible; + } + + public void Dispose() + { + delete Name; + delete Children; + } + } + + Dictionary _entities = new .() ~ DeleteDictionaryAndDisposeValues!(_); + + bool _forceEntityHierarchyRebuild = false; + + enum Operation + { + None, + Add, + Delete, + Modify + } + + private void UpdateEntityHierarchy() + { + if (DoStuffFunction == null) + { + Log.EngineLogger.Error($"{nameof(DoStuffFunction)} is null, skipping entity hierarchy update."); + return; + } + + Dictionary updates = scope .(); + GetEntityUpdates(updates); + SendUpdateToUI(updates); + } + + private void GetEntityUpdates(Dictionary updates) + { + Scene scene = Editor.Instance.CurrentScene; + + // If we have no root, or the scene changed, rebuild the entire tree + if (_forceEntityHierarchyRebuild || !_entities.ContainsKey(.Zero) || scene != _entities[.Zero].Entity.Scene) + { + _forceEntityHierarchyRebuild = false; + + ClearDictionaryAndDisposeValues!(_entities); + + _entities.Add(.Zero, EntityEntry(Entity(.InvalidEntity, scene), "Root", .Zero, true)); + } + + // TODO: entity.EditorFlags.HasFlag(.HideInHierarchy) + + EntityEntry AddEntity(Entity entity) + { + UUID entityId = entity.UUID; + + if (!_entities.TryGetValue(entityId, var entry)) + { + Entity? parent = entity.Parent; + + entry = EntityEntry(entity, entity.Name, parent?.UUID ?? .Zero, !entity.EditorFlags.HasFlag(.HideInScene)); + + _entities.Add(entityId, entry); + updates[entityId] = .Add; + + //if (parent != null) + //{ + UUID parentId = parent?.UUID ?? .Zero; + + if (!_entities.TryGetValue(parentId, var parentEntry)) + { + parentEntry = AddEntity(parent.Value); + } + else + { + updates[parentId] = .Modify; + } + + parentEntry.Children.Add(entityId); + //} + } + + return entry; + } + + for (let (entityId, entityEntry) in ref _entities) + { + // Root node isn't a real entity + if (entityId == .Zero) + continue; + + if (scene.GetEntityByID(entityId) case .Ok(let sceneEntity)) + { + bool changed = false; + + if (entityEntry.Name != sceneEntity.Name) + { + entityEntry.Name.Set(sceneEntity.Name); + changed = true; + } + + Entity? parentEntity = sceneEntity.Parent; + UUID newParentEntityId = parentEntity?.UUID ?? .Zero; + UUID oldParentEntityId = entityEntry.ParentId; + + if (oldParentEntityId != newParentEntityId) + { + if (!_entities.TryGetValue(newParentEntityId, var newParentEntry)) + { + newParentEntry = AddEntity(parentEntity.Value); + } + newParentEntry.Children.Add(entityId); + updates[newParentEntityId] = .Modify; + + if (_entities.TryGetValue(oldParentEntityId, var oldParentEntry)) + { + oldParentEntry.Children.Remove(entityId); + updates[oldParentEntityId] = .Modify; + } + + entityEntry.ParentId = newParentEntityId; + changed = true; + } + + if (changed) + { + updates[entityId] = .Modify; + } + } + else + { + entityEntry.Dispose(); + + _entities.Remove(entityId); + updates[entityId] = .Delete; + } + } + + for (EcsEntity entityId in scene.GetEntities()) + { + let entity = Entity(entityId, scene); + + AddEntity(entity); + } + } + + private void SendUpdateToUI(Dictionary updates) + { + if (updates.Count == 0) + return; + + JSContextRef context = ulViewLockJSContext(_view); + defer ulViewUnlockJSContext(_view); + + JSStringRef nameProperty = JSStringCreateWithUTF8CString("name"); + JSStringRef idProperty = JSStringCreateWithUTF8CString("id"); + JSStringRef visibleProperty = JSStringCreateWithUTF8CString("visible"); + JSStringRef childrenProperty = JSStringCreateWithUTF8CString("children"); + defer JSStringRelease(nameProperty); + defer JSStringRelease(idProperty); + defer JSStringRelease(visibleProperty); + defer JSStringRelease(childrenProperty); + + JSObjectRef jsArray = JSObjectMakeArray(context, 0, null, null); + + uint32 index = 0; + for (let (entityId, operation) in updates) + { + if (operation == .None) + { + continue; + } + + JSObjectRef jsEntity = JSObjectMake(context, null, null); + + /* + name: string; + id: EntityId; + visible: boolean = true; + children: EntityId[]; + */ + + JSValueRef idObject = JSValueMakeNumber(context, (double)(uint64)entityId); + JSObjectSetProperty(context, jsEntity, idProperty, idObject, 0, null); + + if (operation != .Delete) + { + EntityEntry entry = _entities[entityId]; + + JSStringRef entityName = JSStringCreateWithUTF8CString(entry.Name); + JSValueRef nameObject = JSValueMakeString(context, entityName); + JSStringRelease(entityName); + + JSObjectSetProperty(context, jsEntity, nameProperty, nameObject, 0, null); + + JSValueRef visibleObject = JSValueMakeBoolean(context, true); + JSObjectSetProperty(context, jsEntity, visibleProperty, visibleObject, 0, null); + + JSObjectRef childrenArray = JSObjectMakeArray(context, 0, null, null); + + for (let childId in entry.Children) + { + JSValueRef childIdObject = JSValueMakeNumber(context, (double)(uint64)childId); + JSObjectSetPropertyAtIndex(context, childrenArray, (uint32)@childId.Index, childIdObject, null); + } + + JSObjectSetProperty(context, jsEntity, childrenProperty, childrenArray, 0, null); + } + + JSObjectSetPropertyAtIndex(context, jsArray, index, jsEntity, null); + index++; + } + + JSValueRef exception = null; + DoStuffFunction(context, Span(&jsArray, 1), &exception); + + if (exception != null) + { + /*if (JSValueIsString(context, exception)) + { + Log.EngineLogger.Error($"Failed to update entities: {StringView(JsStringGet)}"); + } + else + {*/ + Log.EngineLogger.Error("Failed to update entities."); + //} + } + } + private bool _hoveringNonClientArea; void HandleHoverNonClientArea(JSContextRef context, JSObjectRef thisObject, Span arguments, JSValueRef* exception = null) @@ -70,6 +321,33 @@ class UltralightMainWindow : UltralightWindow JSStringRelease(name); } + private JsFunctionCall DoStuffFunction; + + delegate JSValueRef JsFunctionCall(JSContextRef context, Span arguments, JSValueRef* exception); + + private JsFunctionCall GetJsCallbackDelegate(JSContextRef context, JSObjectRef object, StringView functionName) + { + JSStringRef functionNameString = JSStringCreateWithUTF8CString(functionName.ToScopeCStr!()); + defer JSStringRelease(functionNameString); + + JSValueRef func = JSObjectGetProperty(context, object, functionNameString, null); + + if (!JSValueIsObject(context, func)) + { + return null; + } + + JSObjectRef functionObject = JSValueToObject(context, func, null); + + if (functionObject == null || !JSObjectIsFunction(context, functionObject)) + { + return null; + } + + return new (c, args, ex) => { + return JSObjectCallAsFunction(c, functionObject, object, (uint32)args.Length, args.Ptr, ex); + }; + } private void RegisterEngineGlueFunctions() { @@ -98,6 +376,10 @@ class UltralightMainWindow : UltralightWindow RegisterBeefFunction(context, scriptGlue, "handleClickMaximizeWindow", new:stdAlloc (c, t, a, e) => { _window.ToggleMaximize(); }); RegisterBeefFunction(context, scriptGlue, "handleClickMinimizeWindow", new:stdAlloc (c, t, a, e) => { _window.Minimize(); }); RegisterBeefFunction(context, scriptGlue, "handleClickCloseWindow", new:stdAlloc (c, t, a, e) => { _window.Close(); }); + + RegisterBeefFunction(context, scriptGlue, "requestEntityHierarchyUpdate", new:stdAlloc (c, t, a, e) => { _forceEntityHierarchyRebuild = true; }); + + DoStuffFunction = GetJsCallbackDelegate(context, scriptGlue, "callFromEngine_updateEntities"); } protected override void OnDOMReady(C_View* caller, uint64 frame_id, bool is_main_frame, C_String* url) diff --git a/GlitchyEditor/src/Ultralight/UltralightWindow.bf b/GlitchyEditor/src/Ultralight/UltralightWindow.bf index ff3cb3d..64722f3 100644 --- a/GlitchyEditor/src/Ultralight/UltralightWindow.bf +++ b/GlitchyEditor/src/Ultralight/UltralightWindow.bf @@ -112,10 +112,28 @@ abstract class UltralightWindow Blit.Blit(_texture, _window.SwapChain.BackBuffer, viewport: _window.SwapChain.BackbufferViewport); } + public virtual void Update() { } + public void Render() { CopyToImmediateTexture(); CopyToBackBuffer(); + + if (Input.IsKeyPressing(Key.A)) + { + ULString str = ulCreateString("window.updateEntities([])"); + ULString exception = null; + ulViewEvaluateScript(_view, str, &exception); + + if (exception != null && ulStringGetLength(exception) != 0) + { + StringView ex = StringView(ulStringGetData(exception), ulStringGetLength(exception)); + + Log.EngineLogger.Error($"Failed to evaluate script: {ex}"); + } + + ulDestroyString(str); + } } private void CreateTexture() diff --git a/GlitchyEditor/src/UltralightLayer.bf b/GlitchyEditor/src/UltralightLayer.bf index 71e5b06..d5fff86 100644 --- a/GlitchyEditor/src/UltralightLayer.bf +++ b/GlitchyEditor/src/UltralightLayer.bf @@ -77,6 +77,7 @@ class UltralightLayer : Layer for (var window in _windows) { + window.Update(); window.Render(); } } diff --git a/GlitchyEngine/src/Core/UUID.bf b/GlitchyEngine/src/Core/UUID.bf index 2806620..73c9bc5 100644 --- a/GlitchyEngine/src/Core/UUID.bf +++ b/GlitchyEngine/src/Core/UUID.bf @@ -56,5 +56,10 @@ namespace GlitchyEngine.Core return .Ok; } + + public static explicit operator uint64(UUID id) + { + return id._uuid; + } } } \ No newline at end of file diff --git a/GlitchyEngine/src/Helper.bf b/GlitchyEngine/src/Helper.bf index 690d7f7..f73df5c 100644 --- a/GlitchyEngine/src/Helper.bf +++ b/GlitchyEngine/src/Helper.bf @@ -49,15 +49,39 @@ namespace GlitchyEngine container.Clear(); } - public static mixin DeleteDictionaryAndReleaseValues(var container) + public static mixin DeleteDictionaryAndReleaseValues(var dictionary) { - if (container != null) + if (dictionary != null) { - for (var value in container) + for (var value in dictionary) { value.value?.ReleaseRef(); } - delete container; + delete dictionary; + } + } + + public static mixin ClearDictionaryAndDisposeValues(var dictionary) + { + if (dictionary != null) + { + for (var value in dictionary) + { + value.value?.Dispose(); + } + dictionary.Clear(); + } + } + + public static mixin DeleteDictionaryAndDisposeValues(var dictionary) + { + if (dictionary != null) + { + for (var value in dictionary) + { + value.value?.Dispose(); + } + delete dictionary; } } diff --git a/GlitchyEngine/src/Platform/DX11/Renderer/Dx11Texture.bf b/GlitchyEngine/src/Platform/DX11/Renderer/Dx11Texture.bf index 4be0a5e..742cd34 100644 --- a/GlitchyEngine/src/Platform/DX11/Renderer/Dx11Texture.bf +++ b/GlitchyEngine/src/Platform/DX11/Renderer/Dx11Texture.bf @@ -137,9 +137,9 @@ namespace GlitchyEngine.Renderer if (slices.Length == 0) return .Err; - if (slices.Length != 1) + if (nativeTexture != null && slices.Length != 1) { - Runtime.NotImplemented("PlatformSetData(Span slices) with more than 1 slice is not implemented yet."); + Runtime.NotImplemented("PlatformSetData(Span slices) with more than 1 slice is not implemented for already created textures."); } if(nativeTexture == null) diff --git a/GlitchyEngine/src/World/Scene.bf b/GlitchyEngine/src/World/Scene.bf index c9e6550..3ac90a3 100644 --- a/GlitchyEngine/src/World/Scene.bf +++ b/GlitchyEngine/src/World/Scene.bf @@ -1086,6 +1086,11 @@ namespace GlitchyEngine.World handler(entity, componentType, component); } } + + public WorldEnumerator GetEntities() + { + return _ecsWorld.Enumerate(); + } public WorldEnumerator GetEntities() where TComponent : struct {