diff --git a/.tangled/workflows/test.yml b/.tangled/workflows/test.yml index 430af44..6b76179 100644 --- a/.tangled/workflows/test.yml +++ b/.tangled/workflows/test.yml @@ -16,6 +16,7 @@ dependencies: - erlang_28 - rebar3 - nodejs_22 + - git steps: # Tangled injects repository secrets into every step. Gleam 1.18 and later @@ -28,6 +29,44 @@ steps: gleam format --check src test gleam test + - name: "Check every extension" + command: | + set -euo pipefail + : "${HEXPM_READ_API_KEY:?Configure HEXPM_READ_API_KEY in Tangled repository secrets}" + shopt -s globstar nullglob + + check_package() { + local package="$1" + echo "Checking ${package}" + ( + cd "${package}" + source_directories=(src) + [[ -d test ]] && source_directories+=(test) + [[ -d dev ]] && source_directories+=(dev) + gleam format --check "${source_directories[@]}" + if [[ -d test ]]; then + gleam test + else + gleam check + fi + ) + } + + extension_manifests=(extensions/*/gleam.toml) + (( ${#extension_manifests[@]} > 0 )) + for manifest in "${extension_manifests[@]}"; do + check_package "${manifest%/gleam.toml}" + done + + example_manifests=() + for manifest in extensions/*/examples/**/gleam.toml; do + [[ "${manifest}" == */build/* ]] || example_manifests+=("${manifest}") + done + (( ${#example_manifests[@]} > 0 )) + for manifest in "${example_manifests[@]}"; do + check_package "${manifest%/gleam.toml}" + done + - name: "Check every example" command: | set -euo pipefail diff --git a/src/tiramisu/dev/extension.gleam b/src/tiramisu/dev/extension.gleam index d1808c8..e045313 100644 --- a/src/tiramisu/dev/extension.gleam +++ b/src/tiramisu/dev/extension.gleam @@ -171,6 +171,7 @@ //// { heading: "Entities and Commands", subheading: "Entity Capabilities", name: "root", anchor: "root", summary: "Return the stable hierarchy root for this entity incarnation." }, //// { heading: "Entities and Commands", subheading: "Entity Capabilities", name: "object", anchor: "object", summary: "Find the object currently attached to a named entity slot." }, //// { heading: "Entities and Commands", subheading: "System Capabilities", name: "system_scene", anchor: "system_scene", summary: "Return the Savoiardi scene owned by this system's renderer." }, +//// { heading: "Entities and Commands", subheading: "System Capabilities", name: "system_active_camera", anchor: "system_active_camera", summary: "Return the active camera selected within this system's renderer." }, //// { heading: "Entities and Commands", subheading: "System Capabilities", name: "system_renderer", anchor: "system_renderer", summary: "Return the Savoiardi renderer owned by this system instance." }, //// { heading: "Entities and Commands", subheading: "System Capabilities", name: "find_entity", anchor: "find_entity", summary: "Find a currently mounted entity by id within this system's renderer." }, //// { heading: "Entities and Commands", subheading: "Command Types", name: "ObjectChange", anchor: "ObjectChange", summary: "Describes a change to one named object slot." }, @@ -348,6 +349,7 @@ import gleam/time/duration import gleam/time/timestamp import lustre import lustre/attribute.{type Attribute} +import savoiardi/camera.{type Camera} import savoiardi/object.{type Object3D} import savoiardi/renderer.{type Renderer} import savoiardi/scene.{type Scene} @@ -744,6 +746,7 @@ pub opaque type SystemContext { SystemContext( scene: Scene, renderer: Renderer, + active_camera: fn() -> Result(Camera, Nil), find_entity: fn(String) -> Result(Entity, Nil), entities: fn() -> List(Entity), ) @@ -810,6 +813,16 @@ pub fn system_renderer(context: SystemContext) -> Renderer { renderer } +/// @superdocs Entities and Commands - System Capabilities +/// Return the active camera selected within this system's renderer. +/// +/// Returns `Error(Nil)` until a mounted camera component has claimed the +/// renderer-local active-camera role. +pub fn system_active_camera(context: SystemContext) -> Result(Camera, Nil) { + let SystemContext(active_camera:, ..) = context + active_camera() +} + /// @superdocs Entities and Commands - System Capabilities /// Find a currently mounted entity by id within this system's renderer. /// @@ -2286,10 +2299,11 @@ pub fn new_context(entity: Entity, instance: Option(InstanceKey)) -> Context { pub fn new_system_context( scene: Scene, renderer: Renderer, + active_camera: fn() -> Result(Camera, Nil), find_entity: fn(String) -> Result(Entity, Nil), entities: fn() -> List(Entity), ) -> SystemContext { - SystemContext(scene:, renderer:, find_entity:, entities:) + SystemContext(scene:, renderer:, active_camera:, find_entity:, entities:) } fn option_to_result(value: Option(a)) -> Result(a, Nil) { diff --git a/src/tiramisu/internal/runtime.gleam b/src/tiramisu/internal/runtime.gleam index 1e9ce36..f2f2969 100644 --- a/src/tiramisu/internal/runtime.gleam +++ b/src/tiramisu/internal/runtime.gleam @@ -764,13 +764,8 @@ fn execute_command( ) extension.SelectActiveCamera(entity) -> select_camera(runtime, entity) extension.ClearActiveCamera(entity) -> clear_camera(runtime, entity) - extension.MutateObject(entity, slot, mutate) -> { - case extension.object(entity, slot) { - Ok(value) -> mutate(value) - Error(Nil) -> Nil - } - runtime - } + extension.MutateObject(entity, slot, mutate) -> + mutate_slot(runtime, entity, slot, mutate) extension.OwnResource(entity, key, dispose) -> own_resource(runtime, origin, entity, key, dispose) extension.ReleaseResource(entity, key) -> @@ -854,6 +849,26 @@ fn drain_pending(runtime: Runtime) -> Runtime { } } +fn mutate_slot( + runtime: Runtime, + target: extension.Entity, + slot: String, + mutate: fn(object.Object3D) -> Nil, +) -> Runtime { + let id = extension.entity_id(target) + let target_incarnation = extension.entity_incarnation(target) + case get_entity(runtime, id) { + Ok(entity) if entity.incarnation == target_incarnation -> { + case dict.get(entity.slots, slot) { + Ok(value) -> mutate(value.object) + Error(Nil) -> Nil + } + runtime + } + _ -> runtime + } +} + fn set_slot( runtime: Runtime, origin: Origin, @@ -1313,6 +1328,7 @@ fn system_context(runtime: Runtime) -> extension.SystemContext { extension.new_system_context( runtime.scene, runtime.renderer, + fn() { active_camera(runtime) }, fn(id) { get_entity(runtime, id) |> result.map(to_entity) diff --git a/test/tiramisu/entity_runtime_test.gleam b/test/tiramisu/entity_runtime_test.gleam index 3b4e607..9577807 100644 --- a/test/tiramisu/entity_runtime_test.gleam +++ b/test/tiramisu/entity_runtime_test.gleam @@ -60,6 +60,24 @@ fn tracked_component(name: String, label: String) -> extension.Component { |> extension.build_component } +fn mutate_after_set_component() -> extension.Component { + let component_kind = kind("mutation-owner") + extension.component(kind: component_kind, mount: fn(context, _data) { + let entity = extension.entity(context) + #(Nil, [ + extension.set_object( + entity, + "mesh", + test_support.tracked_object("mutation-object"), + ), + extension.mutate_object(entity, "mesh", fn(_object) { + test_support.record_disposal("mutated-after-set") + }), + ]) + }) + |> extension.build_component +} + fn scene_only_component() -> extension.Component { extension.component( kind: scene_kind("scene-only"), @@ -149,6 +167,16 @@ fn root(children: List(scene_tree.Node)) -> scene_tree.Node { ) } +pub fn mutation_command_sees_slot_set_earlier_in_mount_test() { + test_support.reset_tracking() + let runtime = new_runtime([mutate_after_set_component()]) + let entity = entity_node("entity", "scene", [value("mutation-owner")], []) + + let _runtime = entity_runtime.reconcile(runtime, root([entity])) + + test_support.disposal_count("mutated-after-set") |> should.equal(1) +} + pub fn slot_conflict_and_entity_teardown_dispose_once_test() { test_support.reset_tracking() let runtime = diff --git a/test/tiramisu/system_runtime_test.gleam b/test/tiramisu/system_runtime_test.gleam index 60b9992..65a3c79 100644 --- a/test/tiramisu/system_runtime_test.gleam +++ b/test/tiramisu/system_runtime_test.gleam @@ -44,6 +44,7 @@ fn context(entities: List(extension.Entity)) -> extension.SystemContext { extension.new_system_context( test_support.fake_scene(), test_support.fake_renderer(), + fn() { Error(Nil) }, fn(_id) { Error(Nil) }, fn() { entities }, )