Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# Changelog
## v0.10.0 - 2026-02-10
### 🐞 Fixes
- [Feature] Added geometry streaming support (d78a58a…)
- [Patch] Fixed flickering issue (16469ab…)
- [Patch] Fixed input fields to go un-focus (6e533e1…)
## v0.9.0 - 2026-02-04
### 🚀 Features
- [Feature] Adde LOD support (319502a…)
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ let package = Package(
// Use a branch during active development:
// .package(url: "https://github.com/untoldengine/UntoldEngine.git", branch: "develop"),
// Or pin to a release:
.package(url: "https://github.com/untoldengine/UntoldEngine.git", exact: "0.9.0"),
.package(url: "https://github.com/untoldengine/UntoldEngine.git", exact: "0.10.0"),
],
targets: [
.executableTarget(
Expand Down
193 changes: 192 additions & 1 deletion Sources/UntoldEditor/Editor/InspectorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ struct InspectorView: View {

let sortedComponents = sortEntityComponents(componentOption_Editor: mergedComponents)

// Geometry Streaming Section - Show for any entity with renderable hierarchy
GeometryStreamingEditorView(entityId: entityId, refreshView: refreshView)

// Static Batching Section - Show for any entity with renderable hierarchy
StaticBatchingEditorView(entityId: entityId, refreshView: refreshView)

Expand Down Expand Up @@ -461,7 +464,195 @@ struct InspectorView: View {
}
*/

// Standalone Static Batching Section
// MARK: - Geometry Streaming Section

struct GeometryStreamingEditorView: View {
let entityId: EntityID
let refreshView: () -> Void

@State private var streamingCheckboxState: Bool = false
@State private var streamingRadius: Float = 100.0
@State private var unloadRadius: Float = 150.0
@State private var priority: Int = 0

// Check if entity or any of its children have RenderComponent
private func hasRenderableHierarchy(entityId: EntityID) -> Bool {
// Check self
if hasComponent(entityId: entityId, componentType: RenderComponent.self) {
return true
}

// Check children recursively
let children = getEntityChildren(parentId: entityId)
for child in children {
if hasRenderableHierarchy(entityId: child) {
return true
}
}

return false
}

// Check if entity or any of its children have StreamingComponent
private func isMarkedForStreaming(entityId: EntityID) -> Bool {
// Check self
if hasComponent(entityId: entityId, componentType: StreamingComponent.self) {
return true
}

// Check children recursively
let children = getEntityChildren(parentId: entityId)
for child in children {
if isMarkedForStreaming(entityId: child) {
return true
}
}

return false
}

// Remove streaming component from entity and all children recursively
private func removeStreamingFromHierarchy(entityId: EntityID) {
// Remove from self
if hasComponent(entityId: entityId, componentType: StreamingComponent.self) {
scene.remove(component: StreamingComponent.self, from: entityId)
}

// Remove from children recursively
let children = getEntityChildren(parentId: entityId)
for child in children {
removeStreamingFromHierarchy(entityId: child)
}
}

var body: some View {
// Only show if entity or children have RenderComponent (but not lights)
if hasRenderableHierarchy(entityId: entityId), hasComponent(entityId: entityId, componentType: LightComponent.self) == false {
VStack(alignment: .leading, spacing: 4) {
Text("Geometry Streaming")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)

let hasOwnRenderComponent = hasComponent(entityId: entityId, componentType: RenderComponent.self)
let labelText = hasOwnRenderComponent ? "Enable Streaming" : "Enable Streaming for Children"
let helpText = hasOwnRenderComponent
? "Enable geometry streaming to dynamically load/unload mesh based on camera distance"
: "Enable geometry streaming for all children of this entity"

Toggle(isOn: Binding(
get: { streamingCheckboxState },
set: { isEnabled in
if isEnabled {
enableStreaming(
entityId: entityId,
streamingRadius: streamingRadius,
unloadRadius: unloadRadius,
priority: priority
)
} else {
removeStreamingFromHierarchy(entityId: entityId)
}
streamingCheckboxState = isEnabled
refreshView()
}
)) {
HStack {
Image(systemName: "arrow.triangle.2.circlepath")
.foregroundColor(.green)
Text(labelText)
.font(.callout)
}
}
.padding(.vertical, 6)
.padding(.horizontal, 8)
.background(Color.secondary.opacity(0.05))
.cornerRadius(8)
.help(helpText)
.onAppear {
// Update checkbox state when view appears
streamingCheckboxState = isMarkedForStreaming(entityId: entityId)

// Load streaming parameters from first entity with StreamingComponent
if let streaming = scene.get(component: StreamingComponent.self, for: entityId) {
streamingRadius = streaming.streamingRadius
unloadRadius = streaming.unloadRadius
priority = streaming.priority
} else {
// Check children
let children = getEntityChildren(parentId: entityId)
for child in children {
if let streaming = scene.get(component: StreamingComponent.self, for: child) {
streamingRadius = streaming.streamingRadius
unloadRadius = streaming.unloadRadius
priority = streaming.priority
break
}
}
}
}
.onChange(of: entityId) { newEntityId in
// Update checkbox state when entity selection changes
streamingCheckboxState = isMarkedForStreaming(entityId: newEntityId)
}

// Streaming parameters (only show if streaming is enabled)
if streamingCheckboxState {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Streaming Radius:")
.font(.caption)
CommitAndDefocusFloatField(value: $streamingRadius)
.frame(width: 80)
}

HStack {
Text("Unload Radius:")
.font(.caption)
CommitAndDefocusFloatField(value: $unloadRadius)
.frame(width: 80)
}

HStack {
Text("Priority:")
.font(.caption)
CommitAndDefocusIntField(value: $priority)
.frame(width: 80)
}

Button("Apply Changes") {
// Re-apply streaming with new parameters
if streamingCheckboxState {
removeStreamingFromHierarchy(entityId: entityId)
enableStreaming(
entityId: entityId,
streamingRadius: streamingRadius,
unloadRadius: unloadRadius,
priority: priority
)
refreshView()
}
}
.buttonStyle(PlainButtonStyle())
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.editorAccent)
.foregroundColor(.white)
.cornerRadius(6)
}
.padding(.top, 4)
.padding(.horizontal, 8)
.background(Color.secondary.opacity(0.05))
.cornerRadius(8)
}
}

Divider()
}
}
}

// MARK: - Static Batching Section

struct StaticBatchingEditorView: View {
let entityId: EntityID
let refreshView: () -> Void
Expand Down
Loading
Loading