修复大错误移动到prop

This commit is contained in:
2026-08-20 23:04:59 +08:00
parent e67161c90d
commit 81204fc188
51 changed files with 685 additions and 403 deletions
+3
View File
@@ -11,6 +11,9 @@
## 状态与接口 ## 状态与接口
* 每个状态只能有一个权威来源。双缓冲交换后的当前结构就是稳定读面,跨对象直接读取该结构;禁止为无锁访问再复制一份快照、View、镜像字段或同步缓存。 * 每个状态只能有一个权威来源。双缓冲交换后的当前结构就是稳定读面,跨对象直接读取该结构;禁止为无锁访问再复制一份快照、View、镜像字段或同步缓存。
* `Prop` 保存外部可读写的业务属性,`State` 保存实现向外发布的运行结果,`Private` 保存实现细节;三者不得互相复制并手工同步。
* 每个 `Def` 定义层自动以自身类型生成 `Base_Tag`Prop、State、Private 分别在隔离的标签空间中复用该标签,禁止再声明 `XXX_Prop_Tag``XXX_State_Tag``XXX_Private_Tag`
* 依赖可以选择 Prop/State 的单字段或整个 `Base_Tag` 层;字段写入必须同时发出字段级和所属层级变更,使用方按实际重建粒度选择一种依赖。
* `Root` 只保存一个最终 `Private` 指针;`Builder::build()` 校验成功后创建并挂接完整 Private,`Root` 通过公共 Private 基类的虚析构统一释放。禁止直接公开该指针。 * `Root` 只保存一个最终 `Private` 指针;`Builder::build()` 校验成功后创建并挂接完整 Private,`Root` 通过公共 Private 基类的虚析构统一释放。禁止直接公开该指针。
* 能从权威结构查询或计算的数据即时获取,不保存为成员。类只保存自身职责需要且无法推导的状态,并检查每个新增成员的读写者和生命周期。 * 能从权威结构查询或计算的数据即时获取,不保存为成员。类只保存自身职责需要且无法推导的状态,并检查每个新增成员的读写者和生命周期。
* 公共接口只表达业务语义,不暴露 `Private`、内部指针、线程状态或缓冲区角色;接口保持正交,不增加空配置、未完成接口、无消费者统计或只做转发的 getter/setter。 * 公共接口只表达业务语义,不暴露 `Private`、内部指针、线程状态或缓冲区角色;接口保持正交,不增加空配置、未完成接口、无消费者统计或只做转发的 getter/setter。
@@ -157,6 +157,16 @@ public:
void disconnect(Root* object) { void disconnect(Root* object) {
edit_dependency_graph->disconnect_impl(object); edit_dependency_graph->disconnect_impl(object);
} }
/* 添加 Prop 字段级依赖;source 通过 set 修改该字段时目标阶段变脏。 */
template <auto Member, detail::Bound_Dependency_Graph_Target<Bound_Object> Target, detail::Prop_Dependency_Source<Member> Source>
Node* add_prop_dependency(Target* target, Source* source) {
return edit_dependency_graph->template add_dependency_runtime<Bound_Object, Target, Source>(target, source, detail::dependency_id<detail::Prop_Dependency_Key<Member>>(), target_tag, target_dirty_key, bind_node_data);
}
/* 添加 Prop 声明层级依赖;Owner 声明的任一字段通过 set 修改时目标阶段变脏。 */
template <typename Prop_Tag, detail::Bound_Dependency_Graph_Target<Bound_Object> Target, detail::Prop_Layer_Dependency_Source<Prop_Tag> Source>
Node* add_prop_dependency(Target* target, Source* source) {
return edit_dependency_graph->template add_dependency_runtime<Bound_Object, Target, Source>(target, source, detail::dependency_id<detail::Prop_Layer_Dependency_Key<Prop_Tag>>(), target_tag, target_dirty_key, bind_node_data);
}
template <auto Member, detail::Bound_Dependency_Graph_Target<Bound_Object> Target, detail::State_Dependency_Source<Member> Source> template <auto Member, detail::Bound_Dependency_Graph_Target<Bound_Object> Target, detail::State_Dependency_Source<Member> Source>
Node* add_dependency(Target* target, Source* source) { Node* add_dependency(Target* target, Source* source) {
return edit_dependency_graph->template add_dependency_runtime<Bound_Object, Target, Source>( return edit_dependency_graph->template add_dependency_runtime<Bound_Object, Target, Source>(
@@ -206,6 +216,14 @@ public:
bool remove_dependency(Target* target, Source* source) { bool remove_dependency(Target* target, Source* source) {
return edit_dependency_graph->remove_edge(target, source, detail::dependency_id<detail::State_Dependency_Key<Member>>(), target_tag); return edit_dependency_graph->remove_edge(target, source, detail::dependency_id<detail::State_Dependency_Key<Member>>(), target_tag);
} }
template <auto Member, detail::Bound_Dependency_Graph_Target<Bound_Object> Target, detail::Prop_Dependency_Source<Member> Source>
bool remove_prop_dependency(Target* target, Source* source) {
return edit_dependency_graph->remove_edge(target, source, detail::dependency_id<detail::Prop_Dependency_Key<Member>>(), target_tag);
}
template <typename Prop_Tag, detail::Bound_Dependency_Graph_Target<Bound_Object> Target, detail::Prop_Layer_Dependency_Source<Prop_Tag> Source>
bool remove_prop_dependency(Target* target, Source* source) {
return edit_dependency_graph->remove_edge(target, source, detail::dependency_id<detail::Prop_Layer_Dependency_Key<Prop_Tag>>(), target_tag);
}
/* 移除指定 State Tag 的状态层级依赖,不影响同对象上的字段级依赖。 */ /* 移除指定 State Tag 的状态层级依赖,不影响同对象上的字段级依赖。 */
template <typename State_Tag, detail::Bound_Dependency_Graph_Target<Bound_Object> Target, detail::State_Layer_Dependency_Source<State_Tag> Source> template <typename State_Tag, detail::Bound_Dependency_Graph_Target<Bound_Object> Target, detail::State_Layer_Dependency_Source<State_Tag> Source>
bool remove_state_dependency(Target* target, Source* source) { bool remove_state_dependency(Target* target, Source* source) {
@@ -286,6 +286,14 @@ struct Root::Builder {
} }
); );
} }
template <detail::Dependency_Graph_Tag_In<typename Object::Dependency_Graph_Types> Dependency_Graph_Tag, auto Member, detail::Bound_Dependency_Graph_Target<detail::Dependency_Graph_Bound_Object<Dependency_Graph_Tag, typename Object::Dependency_Graph_Types>> Target, detail::Prop_Dependency_Source<Member> Source>
Builder& add_prop_dependency(Target* target, Source* source) {
return edit_dependency_graph<Dependency_Graph_Tag>([&](auto& editor) { editor.template add_prop_dependency<Member>(target, source); });
}
template <detail::Dependency_Graph_Tag_In<typename Object::Dependency_Graph_Types> Dependency_Graph_Tag, typename Prop_Tag, detail::Bound_Dependency_Graph_Target<detail::Dependency_Graph_Bound_Object<Dependency_Graph_Tag, typename Object::Dependency_Graph_Types>> Target, detail::Prop_Layer_Dependency_Source<Prop_Tag> Source>
Builder& add_prop_dependency(Target* target, Source* source) {
return edit_dependency_graph<Dependency_Graph_Tag>([&](auto& editor) { editor.template add_prop_dependency<Prop_Tag>(target, source); });
}
template <detail::Dependency_Graph_Tag_In<typename Object::Dependency_Graph_Types> Dependency_Graph_Tag, auto Member, detail::Bound_Dependency_Graph_Target<detail::Dependency_Graph_Bound_Object<Dependency_Graph_Tag, typename Object::Dependency_Graph_Types>> Target, detail::State_Dependency_Source<Member> Source> template <detail::Dependency_Graph_Tag_In<typename Object::Dependency_Graph_Types> Dependency_Graph_Tag, auto Member, detail::Bound_Dependency_Graph_Target<detail::Dependency_Graph_Bound_Object<Dependency_Graph_Tag, typename Object::Dependency_Graph_Types>> Target, detail::State_Dependency_Source<Member> Source>
Builder& add_dependency(Target* target, Source* source) { Builder& add_dependency(Target* target, Source* source) {
return edit_dependency_graph<Dependency_Graph_Tag>( return edit_dependency_graph<Dependency_Graph_Tag>(
@@ -327,6 +335,14 @@ struct Root::Builder {
} }
); );
} }
template <detail::Dependency_Graph_Tag_In<typename Object::Dependency_Graph_Types> Dependency_Graph_Tag, auto Member, detail::Bound_Dependency_Graph_Target<detail::Dependency_Graph_Bound_Object<Dependency_Graph_Tag, typename Object::Dependency_Graph_Types>> Target, detail::Prop_Dependency_Source<Member> Source>
Builder& remove_prop_dependency(Target* target, Source* source) {
return edit_dependency_graph<Dependency_Graph_Tag>([&](auto& editor) { editor.template remove_prop_dependency<Member>(target, source); });
}
template <detail::Dependency_Graph_Tag_In<typename Object::Dependency_Graph_Types> Dependency_Graph_Tag, typename Prop_Tag, detail::Bound_Dependency_Graph_Target<detail::Dependency_Graph_Bound_Object<Dependency_Graph_Tag, typename Object::Dependency_Graph_Types>> Target, detail::Prop_Layer_Dependency_Source<Prop_Tag> Source>
Builder& remove_prop_dependency(Target* target, Source* source) {
return edit_dependency_graph<Dependency_Graph_Tag>([&](auto& editor) { editor.template remove_prop_dependency<Prop_Tag>(target, source); });
}
/* 从构造期依赖图中移除指定 State Tag 的状态层级依赖。 */ /* 从构造期依赖图中移除指定 State Tag 的状态层级依赖。 */
template <detail::Dependency_Graph_Tag_In<typename Object::Dependency_Graph_Types> Dependency_Graph_Tag, typename State_Tag, detail::Bound_Dependency_Graph_Target<detail::Dependency_Graph_Bound_Object<Dependency_Graph_Tag, typename Object::Dependency_Graph_Types>> Target, detail::State_Layer_Dependency_Source<State_Tag> Source> template <detail::Dependency_Graph_Tag_In<typename Object::Dependency_Graph_Types> Dependency_Graph_Tag, typename State_Tag, detail::Bound_Dependency_Graph_Target<detail::Dependency_Graph_Bound_Object<Dependency_Graph_Tag, typename Object::Dependency_Graph_Types>> Target, detail::State_Layer_Dependency_Source<State_Tag> Source>
Builder& remove_state_dependency(Target* target, Source* source) { Builder& remove_state_dependency(Target* target, Source* source) {
+124 -9
View File
@@ -145,10 +145,19 @@ struct Publish_Double_Buffer : Double_Buffer<Value> {
} }
}; };
namespace detail { namespace detail {
struct Prop_Root {
bool operator==(const Prop_Root&) const = default;
};
struct State_Root { struct State_Root {
bool operator==(const State_Root&) const = default; bool operator==(const State_Root&) const = default;
}; };
} }
template <typename Tag, Prop_State Prev = detail::Prop_Root>
struct Prop_Type : Prev {
using Tag_Type = Tag;
using Prev_Prop = Prev;
bool operator==(const Prop_Type&) const = default;
};
// State_Type 用 Tag 标记每一层状态,Prev_State 把 CRTP 继承链上的状态按层串起来,供精确回调和类型约束使用。 // State_Type 用 Tag 标记每一层状态,Prev_State 把 CRTP 继承链上的状态按层串起来,供精确回调和类型约束使用。
template <typename Tag, Prop_State Prev = detail::State_Root> template <typename Tag, Prop_State Prev = detail::State_Root>
struct State_Type : Prev { struct State_Type : Prev {
@@ -207,19 +216,27 @@ struct Is_State_Type : std::false_type {};
template <typename Tag, typename Prev> template <typename Tag, typename Prev>
struct Is_State_Type<State_Type<Tag, Prev>> : std::true_type {}; struct Is_State_Type<State_Type<Tag, Prev>> : std::true_type {};
template <typename Value> template <typename Value>
struct Is_Prop_Type : std::false_type {};
template <typename Tag, typename Prev>
struct Is_Prop_Type<Prop_Type<Tag, Prev>> : std::true_type {};
template <typename Value>
concept Buffer_Type = Is_Tagged_Buffer<Value>::value; concept Buffer_Type = Is_Tagged_Buffer<Value>::value;
template <typename Value> template <typename Value>
concept Dependency_Graph_Mechanism = Is_Dependency_Graph_Type<Value>::value; concept Dependency_Graph_Mechanism = Is_Dependency_Graph_Type<Value>::value;
template <typename Value> template <typename Value>
concept State_Mechanism = Is_State_Type<Value>::value; concept Mechanism_Type = Buffer_Type<Value> || Dependency_Graph_Mechanism<Value>;
template <typename Value>
concept Mechanism_Type = Buffer_Type<Value> || Dependency_Graph_Mechanism<Value> || State_Mechanism<Value>;
template <typename Value> template <typename Value>
concept Tagged_State = Prop_State<Value> && requires { concept Tagged_State = Prop_State<Value> && requires {
typename Value::Tag_Type; typename Value::Tag_Type;
typename Value::Prev_State; typename Value::Prev_State;
requires std::derived_from<Value, State_Type<typename Value::Tag_Type, typename Value::Prev_State>>; requires std::derived_from<Value, State_Type<typename Value::Tag_Type, typename Value::Prev_State>>;
}; };
template <typename Value>
concept Tagged_Prop = Prop_State<Value> && requires {
typename Value::Tag_Type;
typename Value::Prev_Prop;
requires std::derived_from<Value, Prop_Type<typename Value::Tag_Type, typename Value::Prev_Prop>>;
};
template <typename State_T, typename = void> template <typename State_T, typename = void>
struct State_Layers { struct State_Layers {
using Type = std::tuple<>; using Type = std::tuple<>;
@@ -233,6 +250,16 @@ struct State_Layers<State_T, std::void_t<typename State_T::Tag_Type, typename St
}; };
template <typename State_T> template <typename State_T>
using State_Layers_T = typename State_Layers<State_T>::Type; using State_Layers_T = typename State_Layers<State_T>::Type;
template <typename Prop_T, typename = void>
struct Prop_Layers {
using Type = std::tuple<>;
};
template <typename Prop_T>
struct Prop_Layers<Prop_T, std::void_t<typename Prop_T::Tag_Type, typename Prop_T::Prev_Prop>> {
using Type = decltype(std::tuple_cat(std::declval<std::tuple<Prop_T>>(), std::declval<typename Prop_Layers<typename Prop_T::Prev_Prop>::Type>()));
};
template <typename Prop_T>
using Prop_Layers_T = typename Prop_Layers<Prop_T>::Type;
template <typename T> template <typename T>
struct Member_Pointer_Traits; struct Member_Pointer_Traits;
template <typename Member, typename Owner> template <typename Member, typename Owner>
@@ -250,6 +277,14 @@ concept State_Member = requires {
}; };
template <auto Member> template <auto Member>
using State_Member_Tag = typename Member_Pointer_Traits<decltype(Member)>::Owner_Type::Tag_Type; using State_Member_Tag = typename Member_Pointer_Traits<decltype(Member)>::Owner_Type::Tag_Type;
template <auto Member, typename Prop>
concept Prop_Member = requires {
requires std::is_member_object_pointer_v<decltype(Member)>;
requires std::derived_from<Prop, typename Member_Pointer_Traits<decltype(Member)>::Owner_Type>;
requires Tagged_Prop<typename Member_Pointer_Traits<decltype(Member)>::Owner_Type>;
};
template <auto Member>
using Prop_Member_Tag = typename Member_Pointer_Traits<decltype(Member)>::Owner_Type::Tag_Type;
template <typename Value, auto Member, typename State> template <typename Value, auto Member, typename State>
concept State_Member_Settable = requires(State& state, Value&& value) { concept State_Member_Settable = requires(State& state, Value&& value) {
requires State_Member<Member, State>; requires State_Member<Member, State>;
@@ -259,8 +294,6 @@ template <typename T>
using Mechanism_Buffer_Tuple = std::conditional_t<Buffer_Type<T>, std::tuple<T>, std::tuple<>>; using Mechanism_Buffer_Tuple = std::conditional_t<Buffer_Type<T>, std::tuple<T>, std::tuple<>>;
template <typename T> template <typename T>
using Mechanism_Dependency_Graph_Tuple = std::conditional_t<Dependency_Graph_Mechanism<T>, std::tuple<T>, std::tuple<>>; using Mechanism_Dependency_Graph_Tuple = std::conditional_t<Dependency_Graph_Mechanism<T>, std::tuple<T>, std::tuple<>>;
template <typename T>
using Mechanism_State_Tuple = std::conditional_t<State_Mechanism<T>, std::tuple<T>, std::tuple<>>;
template <typename Tag, typename Tuple> template <typename Tag, typename Tuple>
struct Has_Tag : std::false_type {}; struct Has_Tag : std::false_type {};
template <typename Tag, typename... Types> template <typename Tag, typename... Types>
@@ -334,6 +367,19 @@ struct Is_State_List<std::tuple<States...>> : Tagged_List_Check<
> {}; > {};
template <typename Tuple> template <typename Tuple>
concept State_List = Is_State_List<Tuple>::value; concept State_List = Is_State_List<Tuple>::value;
template <typename Tuple>
struct Is_Prop_List : std::false_type {};
template <typename... Props>
struct Is_Prop_List<std::tuple<Props...>> : Tagged_List_Check<(Tagged_Prop<Props> && ...), Props...> {};
template <typename Tuple>
concept Prop_List = Is_Prop_List<Tuple>::value;
template <typename Prop_T>
concept Prop_Chain = Tagged_Prop<Prop_T> && Prop_List<Prop_Layers_T<Prop_T>>;
template <typename Tag, typename Tuple>
concept Prop_Tag_In = requires {
requires Prop_List<Tuple>;
requires Has_Tag<Tag, Tuple>::value;
};
template <typename State_T> template <typename State_T>
concept State_Chain = Tagged_State<State_T> && State_List<State_Layers_T<State_T>>; concept State_Chain = Tagged_State<State_T> && State_List<State_Layers_T<State_T>>;
template <typename Tag, typename Tuple> template <typename Tag, typename Tuple>
@@ -384,6 +430,15 @@ public:
}; };
template <typename Tag, typename State_T> template <typename Tag, typename State_T>
using State_Value = typename State_By_Tag<Tag, State_T>::Type; using State_Value = typename State_By_Tag<Tag, State_T>::Type;
template <typename Tag, typename Prop_T>
struct Prop_By_Tag {
private:
using Layers = Prop_Layers_T<Prop_T>;
public:
using Type = std::tuple_element_t<tag_index_v<Tag, Layers>, Layers>;
};
template <typename Tag, typename Prop_T>
using Prop_Value = typename Prop_By_Tag<Tag, Prop_T>::Type;
template <typename Callback, typename Tag, typename State_T, typename States> template <typename Callback, typename Tag, typename State_T, typename States>
concept State_Callback_For = State_Tag_In<Tag, States> && State_Chain_Matches<State_T, States> && std::invocable<Callback, const State_Value<Tag, State_T>&>; concept State_Callback_For = State_Tag_In<Tag, States> && State_Chain_Matches<State_T, States> && std::invocable<Callback, const State_Value<Tag, State_T>&>;
template <typename Tuple> template <typename Tuple>
@@ -522,8 +577,53 @@ public:
}; };
template <typename State_Type> requires detail::State_Chain<std::remove_const_t<State_Type>> template <typename State_Type> requires detail::State_Chain<std::remove_const_t<State_Type>>
State_Access(State_Type&) -> State_Access<State_Type>; State_Access(State_Type&) -> State_Access<State_Type>;
/* Prop_Access 按定义层的 Base_Tag 选择 PropProp 与 State 的标签空间彼此隔离。 */
template <typename Prop_Type> requires detail::Prop_Chain<std::remove_const_t<Prop_Type>>
class Prop_Access {
private:
using Value_Type = std::remove_const_t<Prop_Type>;
Prop_Type* prop;
template <typename Other_Prop_Type> requires detail::Prop_Chain<std::remove_const_t<Other_Prop_Type>>
friend class Prop_Access;
public:
explicit Prop_Access(Prop_Type& value) noexcept : prop(&value) {}
template <typename Source_Prop_Type> requires std::convertible_to<Source_Prop_Type*, Prop_Type*>
Prop_Access(const Prop_Access<Source_Prop_Type>& source) noexcept : prop(source.prop) {}
template <typename Tag> requires detail::Prop_Tag_In<Tag, detail::Prop_Layers_T<Value_Type>>
[[nodiscard]] decltype(auto) get() const noexcept {
using Layer = detail::Prop_Value<Tag, Value_Type>;
if constexpr (std::is_const_v<Prop_Type>) return static_cast<const Layer&>(*prop);
else return static_cast<Layer&>(*prop);
}
};
template <typename Prop_Type> requires detail::Prop_Chain<std::remove_const_t<Prop_Type>>
Prop_Access(Prop_Type&) -> Prop_Access<Prop_Type>;
/* Private_Access 使用同一个定义层 Base_Tag 选择 Private;该标签空间不与 Prop/State 混用。 */
template <typename Private_Type>
class Private_Access {
private:
Private_Type* private_data;
template <typename Other_Private_Type>
friend class Private_Access;
public:
explicit Private_Access(Private_Type& value) noexcept : private_data(&value) {}
template <typename Source_Private_Type> requires std::convertible_to<Source_Private_Type*, Private_Type*>
Private_Access(const Private_Access<Source_Private_Type>& source) noexcept : private_data(source.private_data) {}
template <typename Tag> requires requires { typename Tag::Private; } && std::derived_from<std::remove_const_t<Private_Type>, typename Tag::Private>
[[nodiscard]] decltype(auto) get() const noexcept {
using Layer = typename Tag::Private;
if constexpr (std::is_const_v<Private_Type>) return static_cast<const Layer&>(*private_data);
else return static_cast<Layer&>(*private_data);
}
};
template <typename Private_Type>
Private_Access(Private_Type&) -> Private_Access<Private_Type>;
namespace detail { namespace detail {
template <auto Member> template <auto Member>
struct Prop_Dependency_Key {};
template <typename Tag>
struct Prop_Layer_Dependency_Key {};
template <auto Member>
struct State_Dependency_Key {}; struct State_Dependency_Key {};
template <typename Tag> template <typename Tag>
struct State_Layer_Dependency_Key {}; struct State_Layer_Dependency_Key {};
@@ -563,10 +663,10 @@ enum class Dependency_Graph_Error {
missing_dependency, missing_dependency,
cycle cycle
}; };
struct Root_State_Tag {};
struct Root { struct Root {
/* 所有实现层 Private 的公共析构基类;Root 通过该类型唯一拥有最终 Private。 */ /* 所有实现层 Private 的公共析构基类;Root 通过该类型唯一拥有最终 Private。 */
struct Private { struct Private {
using Tag_Type = Root;
virtual ~Private() = default; virtual ~Private() = default;
/* CRTP 默认:Builder 挂接最终 Private 后按基类到派生类绑定最终对象类型;Root 层不处理。 */ /* CRTP 默认:Builder 挂接最终 Private 后按基类到派生类绑定最终对象类型;Root 层不处理。 */
template <typename Object> template <typename Object>
@@ -574,9 +674,12 @@ struct Root {
}; };
using Buffers = std::tuple<>; using Buffers = std::tuple<>;
using Dependency_Graph_Types = std::tuple<>; using Dependency_Graph_Types = std::tuple<>;
using States = std::tuple<State_Type<Root_State_Tag>>; using Base_Tag = Root;
struct Prop {}; using States = std::tuple<State_Type<Base_Tag>>;
struct State : State_Type<Root_State_Tag> { struct Prop : Prop_Type<Base_Tag> {
bool operator==(const Prop&) const = default;
};
struct State : State_Type<Base_Tag> {
bool operator==(const State&) const = default; bool operator==(const State&) const = default;
}; };
private: private:
@@ -666,8 +769,10 @@ namespace detail {
template <typename T> template <typename T>
concept Dependency_Object = Attached<T> && requires { concept Dependency_Object = Attached<T> && requires {
requires Root_Derived<T>; requires Root_Derived<T>;
typename T::Prop;
typename T::State; typename T::State;
typename T::Buffers; typename T::Buffers;
requires Prop_State<typename T::Prop>;
requires Prop_State<typename T::State>; requires Prop_State<typename T::State>;
requires Buffer_List<typename T::Buffers>; requires Buffer_List<typename T::Buffers>;
}; };
@@ -680,6 +785,16 @@ concept Bound_Dependency_Graph_Target = Bound_Dependency_Object<T> && std::deriv
template <typename T, typename Bound> template <typename T, typename Bound>
concept Dependency_Graph_Target = Root_Derived<T> && std::derived_from<T, Bound>; concept Dependency_Graph_Target = Root_Derived<T> && std::derived_from<T, Bound>;
template <typename Source, auto Member> template <typename Source, auto Member>
concept Prop_Dependency_Source = requires {
requires Dependency_Object<Source>;
requires Prop_Member<Member, typename Source::Prop>;
};
template <typename Source, typename Tag>
concept Prop_Layer_Dependency_Source = requires {
requires Dependency_Object<Source>;
requires Prop_Tag_In<Tag, Prop_Layers_T<typename Source::Prop>>;
};
template <typename Source, auto Member>
concept State_Dependency_Source = requires { concept State_Dependency_Source = requires {
requires Dependency_Object<Source>; requires Dependency_Object<Source>;
requires State_Member<Member, typename Source::State>; requires State_Member<Member, typename Source::State>;
+69 -29
View File
@@ -6,18 +6,20 @@ concept Object = requires {
requires Object_Root<T>; requires Object_Root<T>;
typename T::This_Object; typename T::This_Object;
typename T::Prev_Object; typename T::Prev_Object;
typename T::Base_Tag;
typename T::Prev_Prop; typename T::Prev_Prop;
typename T::State_Tag; typename T::Prev_State;
typename T::template Prev_State<typename T::State_Tag>;
typename T::template Prev_Builder<T>; typename T::template Prev_Builder<T>;
typename T::Prev_Private; typename T::Prev_Private;
typename T::States; typename T::States;
requires std::same_as<typename T::This_Object, T>; requires std::same_as<typename T::This_Object, T>;
requires std::same_as<typename T::Base_Tag, T>;
requires Object_Root<typename T::Prev_Object>; requires Object_Root<typename T::Prev_Object>;
requires std::derived_from<typename T::Prop, typename T::Prev_Prop>; requires std::derived_from<typename T::Prop, typename T::Prev_Prop>;
requires detail::State_Tag_In<typename T::State_Tag, typename T::States>; requires detail::Tagged_Prop<typename T::Prop>;
requires detail::State_Chain<typename T::template Prev_State<typename T::State_Tag>>; requires detail::State_Tag_In<typename T::Base_Tag, typename T::States>;
requires std::derived_from<typename T::State, typename T::template Prev_State<typename T::State_Tag>>; requires detail::State_Chain<typename T::Prev_State>;
requires std::derived_from<typename T::State, typename T::Prev_State>;
requires detail::State_Chain_Matches<typename T::State, typename T::States>; requires detail::State_Chain_Matches<typename T::State, typename T::States>;
requires std::derived_from<typename T::template Builder<T>, typename T::template Prev_Builder<T>>; requires std::derived_from<typename T::template Builder<T>, typename T::template Prev_Builder<T>>;
requires std::derived_from<typename T::Private, typename T::Prev_Private>; requires std::derived_from<typename T::Private, typename T::Prev_Private>;
@@ -33,26 +35,16 @@ using Impl_Dependency_Graph_Types = decltype(std::tuple_cat(
std::declval<typename Base::Dependency_Graph_Types>(), std::declval<typename Base::Dependency_Graph_Types>(),
std::declval<Mechanism_Dependency_Graph_Tuple<Local_Mechanisms>>()... std::declval<Mechanism_Dependency_Graph_Tuple<Local_Mechanisms>>()...
)); ));
template <typename... Local_Mechanisms> template <typename Self, typename Base>
using Local_States = decltype(std::tuple_cat(
std::declval<Mechanism_State_Tuple<Local_Mechanisms>>()...
));
template <typename Base, typename... Local_Mechanisms>
using Impl_States = decltype(std::tuple_cat( using Impl_States = decltype(std::tuple_cat(
std::declval<typename Base::States>(), std::declval<typename Base::States>(),
std::declval<Local_States<Local_Mechanisms...>>() std::declval<std::tuple<State_Type<Self>>>()
)); ));
template <typename Base, typename... Local_Mechanisms> template <typename Self, typename Base, typename... Local_Mechanisms>
using Impl_State_Base = Rebind_State_T<
std::tuple_element_t<0, Local_States<Local_Mechanisms...>>,
typename Base::State
>;
template <typename Base, typename... Local_Mechanisms>
concept Impl_Mechanisms = Object_Root<Base> && (Mechanism_Type<Local_Mechanisms> && ...) && requires { concept Impl_Mechanisms = Object_Root<Base> && (Mechanism_Type<Local_Mechanisms> && ...) && requires {
requires std::tuple_size_v<Local_States<Local_Mechanisms...>> == 1;
requires Buffer_List<Impl_Buffers<Base, Local_Mechanisms...>>; requires Buffer_List<Impl_Buffers<Base, Local_Mechanisms...>>;
requires Dependency_Graph_List<Impl_Dependency_Graph_Types<Base, Local_Mechanisms...>>; requires Dependency_Graph_List<Impl_Dependency_Graph_Types<Base, Local_Mechanisms...>>;
requires State_List<Impl_States<Base, Local_Mechanisms...>>; requires State_List<Impl_States<Self, Base>>;
}; };
template <typename Callback, typename Private, typename Object_T> template <typename Callback, typename Private, typename Object_T>
concept Process_Callback_For = requires(Private& private_data, Object_T* object, Callback&& callback) { concept Process_Callback_For = requires(Private& private_data, Object_T* object, Callback&& callback) {
@@ -61,27 +53,33 @@ concept Process_Callback_For = requires(Private& private_data, Object_T* object,
} }
// Impl 在编译期把 Buffer/Dependency_Graph/State 三类机制叠加到继承链;每一层只声明自己的机制,最终类型汇总完整能力。 // Impl 在编译期把 Buffer/Dependency_Graph/State 三类机制叠加到继承链;每一层只声明自己的机制,最终类型汇总完整能力。
template <typename Self, Object_Root Base, detail::Mechanism_Type... Local_Mechanisms> requires template <typename Self, Object_Root Base, detail::Mechanism_Type... Local_Mechanisms> requires
detail::Impl_Mechanisms<Base, Local_Mechanisms...> detail::Impl_Mechanisms<Self, Base, Local_Mechanisms...>
struct Def : Base { struct Def : Base {
using This_Object = Self; using This_Object = Self;
using Prev_Object = Base; using Prev_Object = Base;
using Prev_Prop = typename Base::Prop; using Base_Tag = Self;
using Prev_Prop = Prop_Type<Base_Tag, typename Base::Prop>;
template <typename Object> template <typename Object>
using Prev_Builder = typename Base::template Builder<Object>; using Prev_Builder = typename Base::template Builder<Object>;
using State_Declaration = std::tuple_element_t<0, detail::Local_States<Local_Mechanisms...>>; using Prev_State = State_Type<Base_Tag, typename Base::State>;
using State_Tag = typename State_Declaration::Tag_Type;
template <std::same_as<State_Tag> Tag>
using Prev_State = detail::Rebind_State_T<State_Declaration, typename Base::State>;
using Base_Private = typename Base::Private; using Base_Private = typename Base::Private;
using Buffers = detail::Impl_Buffers<Base, Local_Mechanisms...>; using Buffers = detail::Impl_Buffers<Base, Local_Mechanisms...>;
using Dependency_Graph_Types = detail::Impl_Dependency_Graph_Types<Base, Local_Mechanisms...>; using Dependency_Graph_Types = detail::Impl_Dependency_Graph_Types<Base, Local_Mechanisms...>;
using States = detail::Impl_States<Base, Local_Mechanisms...>; using States = detail::Impl_States<Self, Base>;
struct Private : Base_Private { struct Private : Base_Private {
using Tag_Type = Base_Tag;
using Prev_Private = Base_Private;
/* CRTP 默认:继续调用上一 Private 层的最终对象绑定;派生 Private 覆盖时必须先调用此实现。 */ /* CRTP 默认:继续调用上一 Private 层的最终对象绑定;派生 Private 覆盖时必须先调用此实现。 */
template <typename Object> template <typename Object>
void bind_private_crtp(Object* object) { void bind_private_crtp(Object* object) {
Base_Private::bind_private_crtp(object); Base_Private::bind_private_crtp(object);
} }
/* CRTP 可覆盖:写入 Prop 成员前按基类到派生类顺序调用。 */
template <typename Owner, typename Member, typename Prop_T>
void before_prop_set(Self* object, Member Owner::* member, Prop_Access<Prop_T> props) {}
/* CRTP 可覆盖:写入 Prop 成员后按派生类到基类顺序调用。 */
template <typename Owner, typename Member, typename Prop_T>
void after_prop_set(Self* object, Member Owner::* member, Prop_Access<Prop_T> props) {}
/* CRTP 可覆盖:写入 State 成员前按基类到派生类顺序调用;pending_states.get<Tag>() 返回对应可写状态层。 */ /* CRTP 可覆盖:写入 State 成员前按基类到派生类顺序调用;pending_states.get<Tag>() 返回对应可写状态层。 */
template <typename Owner, typename Member, typename State_T> template <typename Owner, typename Member, typename State_T>
void before_state_set(Self* object, Member Owner::* member, State_Access<State_T> pending_states) {} void before_state_set(Self* object, Member Owner::* member, State_Access<State_T> pending_states) {}
@@ -157,6 +155,24 @@ private:
} }
} }
template <typename Owner, typename Member> template <typename Owner, typename Member>
void before_prop_set(Member Owner::* member) {
Prop_Access props{*data().current};
auto callback = [&](auto& private_data) { private_data.before_prop_set(this, member, props); };
walk_private<false, Obj>(callback);
}
template <typename Owner, typename Member>
void after_prop_set(Member Owner::* member) {
Prop_Access props{*data().current};
auto callback = [&](auto& private_data) { private_data.after_prop_set(this, member, props); };
walk_private<true, Obj>(callback);
}
template <auto Member>
void emit_prop_dependencies() {
this->emit_dependency_source(detail::dependency_id<detail::Prop_Dependency_Key<Member>>());
using Prop_Tag = detail::Prop_Member_Tag<Member>;
this->emit_dependency_source(detail::dependency_id<detail::Prop_Layer_Dependency_Key<Prop_Tag>>());
}
template <typename Owner, typename Member>
void before_state_set(Member Owner::* member) { void before_state_set(Member Owner::* member) {
State_Access pending_states{*data().state.pending}; State_Access pending_states{*data().state.pending};
auto callback = [&](auto& private_data) { auto callback = [&](auto& private_data) {
@@ -267,12 +283,36 @@ public:
void for_each_current_dependency_graph(Callback&& callback) const { void for_each_current_dependency_graph(Callback&& callback) const {
data().dependency_graph_storage.for_each_current(std::forward<Callback>(callback)); data().dependency_graph_storage.for_each_current(std::forward<Callback>(callback));
} }
template <typename Owner, typename Member, detail::Prop_Member_Settable<Prop, Owner, Member> Value> template <auto Member, typename Value> requires detail::Prop_Member<Member, Prop> && requires(Prop& prop, Value&& value) { prop.*Member = std::forward<Value>(value); }
Impl& set(Member Owner::* member, Value&& value) { Impl& set(Value&& value) {
std::lock_guard<Lock> guard(lock); std::lock_guard<Lock> guard(lock);
data().current->*member = std::forward<Value>(value); before_prop_set(Member);
data().current->*Member = std::forward<Value>(value);
after_prop_set(Member);
emit_prop_dependencies<Member>();
return *this; return *this;
} }
template <auto Member> requires detail::Prop_Dependency_Source<Impl, Member>
[[nodiscard]] auto get() const {
std::lock_guard<Lock> guard(lock);
return data().current->*Member;
}
template <typename Tag> requires detail::Prop_Tag_In<Tag, detail::Prop_Layers_T<Prop>>
[[nodiscard]] const auto& read_prop() const noexcept {
using Layer = detail::Prop_Value<Tag, Prop>;
return static_cast<const Layer&>(*data().current);
}
template <auto... Members, typename Callback> requires
(sizeof...(Members) > 0) &&
(detail::Prop_Member<Members, Prop> && ...) &&
std::invocable<Callback, Prop_Access<Prop>>
void update_prop(Callback&& callback) {
std::lock_guard<Lock> guard(lock);
(before_prop_set(Members), ...);
std::invoke(std::forward<Callback>(callback), Prop_Access{*data().current});
(after_prop_set(Members), ...);
(emit_prop_dependencies<Members>(), ...);
}
template <detail::State_Tag_In<States> Tag, detail::State_Callback_For<Tag, State, States> Callback> template <detail::State_Tag_In<States> Tag, detail::State_Callback_For<Tag, State, States> Callback>
void set_state_callback(Callback&& callback) { void set_state_callback(Callback&& callback) {
std::lock_guard<Lock> guard(lock); std::lock_guard<Lock> guard(lock);
+1
View File
@@ -16,6 +16,7 @@ using double_buffer::Def;
using double_buffer::Impl; using double_buffer::Impl;
using double_buffer::State_Type; using double_buffer::State_Type;
using double_buffer::State_Access; using double_buffer::State_Access;
using double_buffer::Prop_Access;
using double_buffer::Pmr; using double_buffer::Pmr;
using double_buffer::Root; using double_buffer::Root;
using double_buffer::Tagged_Buffer; using double_buffer::Tagged_Buffer;
+9 -10
View File
@@ -7,7 +7,6 @@ struct Color_Cache {
virtual ~Color_Cache() = default; virtual ~Color_Cache() = default;
}; };
/* Renderable 状态标签,用于访问和订阅 Renderable::State。 */ /* Renderable 状态标签,用于访问和订阅 Renderable::State。 */
struct Renderable_State_Tag {};
struct Renderable; struct Renderable;
/* 最终 Private 提供 handle_event(T*, const Event&) 时具备事件处理能力。 */ /* 最终 Private 提供 handle_event(T*, const Event&) 时具备事件处理能力。 */
template <typename T> template <typename T>
@@ -28,8 +27,8 @@ concept Prepare_Data_Renderable = Attached<T> && requires(typename T::Private& p
* should_rebuild_prepare_graph(...) * should_rebuild_prepare_graph(...)
*/ */
template <typename T> template <typename T>
concept Prepare_Graph_Renderable = Attached<T> && requires(typename T::Private& private_data, T* object, const typename T::State& state) { concept Prepare_Graph_Renderable = Attached<T> && requires(typename T::Private& private_data, T* object, const typename T::Prop& prop) {
{ private_data.build_prepare_graph(object, state) } -> std::same_as<tf::Taskflow>; { private_data.build_prepare_graph(object, prop) } -> std::same_as<tf::Taskflow>;
}; };
/* 最终 Private 声明 No_Prepare 时,该 Renderable 不参与 Prepare 阶段。 */ /* 最终 Private 声明 No_Prepare 时,该 Renderable 不参与 Prepare 阶段。 */
template <typename T> template <typename T>
@@ -48,29 +47,29 @@ concept Paint_Data_Renderable = Attached<T> && requires(typename T::Private& pri
* should_rebuild_paint_graph(...) * should_rebuild_paint_graph(...)
*/ */
template <typename T> template <typename T>
concept Paint_Graph_Renderable = Attached<T> && requires(typename T::Private& private_data, T* object, const typename T::State& state) { concept Paint_Graph_Renderable = Attached<T> && requires(typename T::Private& private_data, T* object, const typename T::Prop& prop) {
{ private_data.build_paint_graph(object, state) } -> std::same_as<tf::Taskflow>; { private_data.build_paint_graph(object, prop) } -> std::same_as<tf::Taskflow>;
}; };
/* /*
* Scene Renderable * Scene Renderable
* Prepare Paint Renderable * Prepare Paint Renderable
*/ */
template <typename T> template <typename T>
concept Renderable_Object = Attached<T> && std::derived_from<T, Renderable> && requires(typename T::Private& private_data, T* object, const typename T::State& state, bool dirty) { concept Renderable_Object = Attached<T> && std::derived_from<T, Renderable> && requires(typename T::Private& private_data, T* object, const typename T::State& state, const typename T::Prop& prop, bool dirty) {
{ private_data.should_prepare(object, state, dirty) } -> std::same_as<bool>; { private_data.should_prepare(object, state, dirty) } -> std::same_as<bool>;
{ private_data.should_paint(object, state, dirty) } -> std::same_as<bool>; { private_data.should_paint(object, state, dirty) } -> std::same_as<bool>;
{ private_data.should_rebuild_prepare_graph(object, state) } -> std::same_as<bool>; { private_data.should_rebuild_prepare_graph(object, prop) } -> std::same_as<bool>;
{ private_data.should_rebuild_paint_graph(object, state) } -> std::same_as<bool>; { private_data.should_rebuild_paint_graph(object, prop) } -> std::same_as<bool>;
} && (No_Prepare_Renderable<T> || Prepare_Data_Renderable<T> || Prepare_Graph_Renderable<T>) && (Paint_Data_Renderable<T> || Paint_Graph_Renderable<T>); } && (No_Prepare_Renderable<T> || Prepare_Data_Renderable<T> || Prepare_Graph_Renderable<T>) && (Paint_Data_Renderable<T> || Paint_Graph_Renderable<T>);
/* /*
* Renderable Scene * Renderable Scene
* Private Prepare/Paint 使 Impl<T> * Private Prepare/Paint 使 Impl<T>
*/ */
struct Renderable : Def<Renderable, Root, State_Type<Renderable_State_Tag>> { struct Renderable : Def<Renderable, Root> {
/* Renderable 当前没有额外发布属性;派生定义可在自己的 Prop 中继续追加字段。 */ /* Renderable 当前没有额外发布属性;派生定义可在自己的 Prop 中继续追加字段。 */
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {};
/* Renderable 每次 Scene 执行后发布的阶段状态与统计。 */ /* Renderable 每次 Scene 执行后发布的阶段状态与统计。 */
struct State : Prev_State<Renderable_State_Tag> { struct State : Prev_State {
bool prepare_dirty{}; /* Prepare 条件判断时观察到的 Prepare_Data_Tag dirty 状态。 */ bool prepare_dirty{}; /* Prepare 条件判断时观察到的 Prepare_Data_Tag dirty 状态。 */
bool paint_dirty{}; /* Paint 条件判断时观察到的 Paint_Tag dirty 状态。 */ bool paint_dirty{}; /* Paint 条件判断时观察到的 Paint_Tag dirty 状态。 */
bool prepare_executed{}; /* 本次 Scene 执行是否运行了 Prepare 数据函数或子图。 */ bool prepare_executed{}; /* 本次 Scene 执行是否运行了 Prepare 数据函数或子图。 */
+9 -9
View File
@@ -40,9 +40,9 @@ struct Renderable::Private : Prev_Private {
bool prepare_graph_built{}; /* Prepare 子图是否至少成功构建过一次。 */ bool prepare_graph_built{}; /* Prepare 子图是否至少成功构建过一次。 */
bool paint_graph_built{}; /* Paint 子图是否至少成功构建过一次。 */ bool paint_graph_built{}; /* Paint 子图是否至少成功构建过一次。 */
/* CRTP 可覆盖:决定已选中子图模式的 Prepare 子图是否重建;object 为最终对象,state 为当前发布状态;默认返回 false。 */ /* CRTP 可覆盖:决定已选中子图模式的 Prepare 子图是否重建;object 为最终对象,state 为当前发布状态;默认返回 false。 */
bool should_rebuild_prepare_graph(Attached auto* object, const State& state); bool should_rebuild_prepare_graph(Attached auto* object, const Prop& prop);
/* CRTP 可覆盖:决定已选中子图模式的 Paint 子图是否重建;object 为最终对象,state 为当前发布状态;默认返回 false。 */ /* CRTP 可覆盖:决定已选中子图模式的 Paint 子图是否重建;object 为最终对象,state 为当前发布状态;默认返回 false。 */
bool should_rebuild_paint_graph(Attached auto* object, const State& state); bool should_rebuild_paint_graph(Attached auto* object, const Prop& prop);
/* CRTP 可覆盖:决定本次是否执行 Prepare;object 为最终对象,state 为当前发布状态,dirty 为 Prepare dirty;默认返回 dirty。 */ /* CRTP 可覆盖:决定本次是否执行 Prepare;object 为最终对象,state 为当前发布状态,dirty 为 Prepare dirty;默认返回 dirty。 */
bool should_prepare(Attached auto* object, const State& state, bool dirty); bool should_prepare(Attached auto* object, const State& state, bool dirty);
/* CRTP 可覆盖:决定本次是否执行 Paint;object 为最终对象,state 为当前发布状态,dirty 为 Paint dirty;默认返回 dirty。 */ /* CRTP 可覆盖:决定本次是否执行 Paint;object 为最终对象,state 为当前发布状态,dirty 为 Paint dirty;默认返回 dirty。 */
@@ -63,10 +63,10 @@ void Renderable::run_prepare_data(Object* object) {
}; };
walk_private<true, typename Object::Attached_Object>(object, callback); walk_private<true, typename Object::Attached_Object>(object, callback);
} }
inline bool Renderable::Private::should_rebuild_prepare_graph(Attached auto*, const State&) { inline bool Renderable::Private::should_rebuild_prepare_graph(Attached auto*, const Prop&) {
return false; return false;
} }
inline bool Renderable::Private::should_rebuild_paint_graph(Attached auto*, const State&) { inline bool Renderable::Private::should_rebuild_paint_graph(Attached auto*, const Prop&) {
return false; return false;
} }
inline bool Renderable::Private::should_prepare(Attached auto*, const State&, bool dirty) { inline bool Renderable::Private::should_prepare(Attached auto*, const State&, bool dirty) {
@@ -117,7 +117,7 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) {
[](Root* root) { [](Root* root) {
auto* value = static_cast<Object*>(root); auto* value = static_cast<Object*>(root);
auto& private_data = static_cast<typename Object::Private&>(*value->d); auto& private_data = static_cast<typename Object::Private&>(*value->d);
return private_data.should_rebuild_prepare_graph(value, *private_data.state.current); return private_data.should_rebuild_prepare_graph(value, *private_data.current);
}, },
[]() -> Private::Stage_Run { []() -> Private::Stage_Run {
if constexpr (Prepare_Data_Renderable<Object> && !No_Prepare_Renderable<Object>) { if constexpr (Prepare_Data_Renderable<Object> && !No_Prepare_Renderable<Object>) {
@@ -135,7 +135,7 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) {
return [](Root* root) { return [](Root* root) {
auto* value = static_cast<Object*>(root); auto* value = static_cast<Object*>(root);
auto& private_data = static_cast<typename Object::Private&>(*value->d); auto& private_data = static_cast<typename Object::Private&>(*value->d);
return private_data.build_prepare_graph(value, *private_data.state.current); return private_data.build_prepare_graph(value, *private_data.current);
}; };
} }
else { else {
@@ -152,7 +152,7 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) {
[](Root* root) { [](Root* root) {
auto* value = static_cast<Object*>(root); auto* value = static_cast<Object*>(root);
auto& private_data = static_cast<typename Object::Private&>(*value->d); auto& private_data = static_cast<typename Object::Private&>(*value->d);
return private_data.should_rebuild_paint_graph(value, *private_data.state.current); return private_data.should_rebuild_paint_graph(value, *private_data.current);
}, },
[]() -> Private::Stage_Run { []() -> Private::Stage_Run {
if constexpr (Paint_Data_Renderable<Object>) { if constexpr (Paint_Data_Renderable<Object>) {
@@ -171,7 +171,7 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) {
return [](Root* root) { return [](Root* root) {
auto* value = static_cast<Object*>(root); auto* value = static_cast<Object*>(root);
auto& private_data = static_cast<typename Object::Private&>(*value->d); auto& private_data = static_cast<typename Object::Private&>(*value->d);
return private_data.build_paint_graph(value, *private_data.state.current); return private_data.build_paint_graph(value, *private_data.current);
}; };
} }
else { else {
@@ -187,7 +187,7 @@ inline void Renderable::bind_dependency_graph_object(Attached auto* object) {
}, },
[](Root* root) { [](Root* root) {
auto* value = static_cast<Object*>(root); auto* value = static_cast<Object*>(root);
value->template notify_state<Renderable_State_Tag>(); value->template notify_state<Renderable::Base_Tag>();
} }
} }
}; };
+2 -3
View File
@@ -2,16 +2,15 @@
#include "renderable.hpp" #include "renderable.hpp"
namespace aethera { namespace aethera {
/* Scene 状态标签,用于访问和订阅 Scene::State。 */ /* Scene 状态标签,用于访问和订阅 Scene::State。 */
struct Scene_State_Tag {};
/* /*
* Scene Prepare/Paint Dependency_Graph Taskflow * Scene Prepare/Paint Dependency_Graph Taskflow
* Impl<Scene> 使 Dependency_Graph advance() process(...) * Impl<Scene> 使 Dependency_Graph advance() process(...)
*/ */
struct Scene : Def<Scene, Root, State_Type<Scene_State_Tag>, Dependency_Graph_Type<Prepare_Data_Tag, Renderable>, Dependency_Graph_Type<Paint_Tag, Renderable>> { struct Scene : Def<Scene, Root, Dependency_Graph_Type<Prepare_Data_Tag, Renderable>, Dependency_Graph_Type<Paint_Tag, Renderable>> {
/* Scene 当前没有额外发布属性;派生定义可在自己的 Prop 中继续追加字段。 */ /* Scene 当前没有额外发布属性;派生定义可在自己的 Prop 中继续追加字段。 */
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {};
/* Scene 每次 process(...) 后发布的总图结构与执行统计。 */ /* Scene 每次 process(...) 后发布的总图结构与执行统计。 */
struct State : Prev_State<Scene_State_Tag> { struct State : Prev_State {
bool taskflow_rebuilt{}; /* 本次 process(...) 前的 advance 是否重新构建了总 Taskflow。 */ bool taskflow_rebuilt{}; /* 本次 process(...) 前的 advance 是否重新构建了总 Taskflow。 */
std::size_t renderable_count{}; /* Prepare/Paint 两张依赖图中去重后的 Renderable 数量。 */ std::size_t renderable_count{}; /* Prepare/Paint 两张依赖图中去重后的 Renderable 数量。 */
std::size_t taskflow_task_count{}; /* 当前总 Taskflow 中的任务节点数量。 */ std::size_t taskflow_task_count{}; /* 当前总 Taskflow 中的任务节点数量。 */
+2 -2
View File
@@ -31,7 +31,7 @@ void Scene::Private::process(Object* object, Callback&& callback) requires std::
auto& state = static_cast<State&>(*private_data.state.current); auto& state = static_cast<State&>(*private_data.state.current);
state.taskflow_execution_time_ns = 0; state.taskflow_execution_time_ns = 0;
if (runtime->taskflow && !runtime->taskflow->empty()) state.taskflow_execution_time_ns = detail::run_taskflow(*runtime->taskflow); if (runtime->taskflow && !runtime->taskflow->empty()) state.taskflow_execution_time_ns = detail::run_taskflow(*runtime->taskflow);
object->template notify_state<Scene_State_Tag>(); object->template notify_state<Scene::Base_Tag>();
Result result; Result result;
std::invoke(std::forward<Callback>(callback), std::as_const(result)); std::invoke(std::forward<Callback>(callback), std::as_const(result));
} }
@@ -42,7 +42,7 @@ void Scene::Private::after_advance(Object* object,
const Prop* current_prop, const Prop* current_prop,
State_Access<State> current_states) { State_Access<State> current_states) {
auto* resource = detail::task_memory_resource(); auto* resource = detail::task_memory_resource();
auto& scene_state = current_states.get<Scene_State_Tag>(); auto& scene_state = current_states.get<Scene::Base_Tag>();
scene_state.taskflow_rebuilt = false; scene_state.taskflow_rebuilt = false;
std::pmr::unordered_set<Root*> advanced_objects{resource}; std::pmr::unordered_set<Root*> advanced_objects{resource};
advanced_objects.insert(object); advanced_objects.insert(object);
+30 -9
View File
@@ -1,12 +1,10 @@
#include "double_buffer/model.hpp" #include "double_buffer/model.hpp"
#include <gtest/gtest.h> #include <gtest/gtest.h>
namespace { namespace {
struct Node_State_Tag {};
struct Graph_State_Tag {};
struct Graph_Tag {}; struct Graph_Tag {};
struct Node_Object : double_buffer::Def<Node_Object, double_buffer::Root, double_buffer::State_Type<Node_State_Tag>> { struct Node_Object : double_buffer::Def<Node_Object, double_buffer::Root> {
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop { int value{}; int other{}; };
struct State : Prev_State<Node_State_Tag> { struct State : Prev_State {
int value{}; /* 字段级依赖测试使用的状态值。 */ int value{}; /* 字段级依赖测试使用的状态值。 */
int other{}; /* 状态层级依赖测试使用的同层其他值。 */ int other{}; /* 状态层级依赖测试使用的同层其他值。 */
bool operator==(const State&) const = default; bool operator==(const State&) const = default;
@@ -14,9 +12,9 @@ struct Node_Object : double_buffer::Def<Node_Object, double_buffer::Root, double
struct Private : Prev_Private {}; struct Private : Prev_Private {};
}; };
using Node = double_buffer::Impl<Node_Object>; using Node = double_buffer::Impl<Node_Object>;
struct Graph_Object : double_buffer::Def<Graph_Object, double_buffer::Root, double_buffer::State_Type<Graph_State_Tag>, double_buffer::Dependency_Graph_Type<Graph_Tag, Node_Object>> { struct Graph_Object : double_buffer::Def<Graph_Object, double_buffer::Root, double_buffer::Dependency_Graph_Type<Graph_Tag, Node_Object>> {
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {};
struct State : Prev_State<Graph_State_Tag> { struct State : Prev_State {
bool operator==(const State&) const = default; bool operator==(const State&) const = default;
}; };
struct Private : Prev_Private {}; struct Private : Prev_Private {};
@@ -35,7 +33,8 @@ std::unique_ptr<Object_Type> build_object() {
static_assert(!double_buffer::detail::Dependency_Object<Node_Object>); static_assert(!double_buffer::detail::Dependency_Object<Node_Object>);
static_assert(double_buffer::detail::Dependency_Object<Node>); static_assert(double_buffer::detail::Dependency_Object<Node>);
static_assert(!double_buffer::detail::State_Dependency_Source<Node_Object, &Node_Object::State::value>); static_assert(!double_buffer::detail::State_Dependency_Source<Node_Object, &Node_Object::State::value>);
static_assert(double_buffer::detail::State_Layer_Dependency_Source<Node, Node_State_Tag>); static_assert(double_buffer::detail::State_Layer_Dependency_Source<Node, Node_Object::Base_Tag>);
static_assert(double_buffer::detail::Prop_Layer_Dependency_Source<Node, Node_Object::Base_Tag>);
} }
TEST(dependency_graph_storage, edited_graph_commits_and_stays_synchronized) { TEST(dependency_graph_storage, edited_graph_commits_and_stays_synchronized) {
auto first = build_object<Node>(); auto first = build_object<Node>();
@@ -111,7 +110,7 @@ TEST(dependency_graph, state_dependency_granularity_is_selectable) {
editor.add(layer_source.get()); editor.add(layer_source.get());
editor.add(layer_target.get()); editor.add(layer_target.get());
editor.template add_dependency<&Node_Object::State::value>(member_target.get(), member_source.get()); editor.template add_dependency<&Node_Object::State::value>(member_target.get(), member_source.get());
editor.template add_state_dependency<Node_State_Tag>(layer_target.get(), layer_source.get()); editor.template add_state_dependency<Node_Object::Base_Tag>(layer_target.get(), layer_source.get());
} }
).has_value()); ).has_value());
graph->advance(); graph->advance();
@@ -122,6 +121,28 @@ TEST(dependency_graph, state_dependency_granularity_is_selectable) {
layer_source->update_state<&Node_Object::State::other>(3); layer_source->update_state<&Node_Object::State::other>(3);
EXPECT_TRUE(layer_target->dirty<Graph_Tag>()); EXPECT_TRUE(layer_target->dirty<Graph_Tag>());
} }
TEST(dependency_graph, prop_dependency_granularity_is_selectable) {
auto member_source = build_object<Node>();
auto member_target = build_object<Node>();
auto layer_source = build_object<Node>();
auto layer_target = build_object<Node>();
auto graph = build_object<Graph>();
ASSERT_TRUE(graph->edit_dependency_graph<Graph_Tag>([&](auto& editor) {
editor.add(member_source.get());
editor.add(member_target.get());
editor.add(layer_source.get());
editor.add(layer_target.get());
editor.template add_prop_dependency<&Node_Object::Prop::value>(member_target.get(), member_source.get());
editor.template add_prop_dependency<Node_Object::Base_Tag>(layer_target.get(), layer_source.get());
}).has_value());
graph->advance();
member_source->set<&Node_Object::Prop::other>(1);
EXPECT_FALSE(member_target->dirty<Graph_Tag>());
member_source->set<&Node_Object::Prop::value>(2);
EXPECT_TRUE(member_target->dirty<Graph_Tag>());
layer_source->set<&Node_Object::Prop::other>(3);
EXPECT_TRUE(layer_target->dirty<Graph_Tag>());
}
TEST(dependency_graph, cycle_is_rejected_before_pending_graph_commit) { TEST(dependency_graph, cycle_is_rejected_before_pending_graph_commit) {
auto first = build_object<Node>(); auto first = build_object<Node>();
auto second = build_object<Node>(); auto second = build_object<Node>();
+46 -31
View File
@@ -1,14 +1,13 @@
#include "double_buffer/model.hpp" #include "double_buffer/model.hpp"
#include <gtest/gtest.h> #include <gtest/gtest.h>
namespace { namespace {
struct Object_State_Tag {};
struct Object_Buffer_Tag {}; struct Object_Buffer_Tag {};
struct Test_Object : double_buffer::Def<Test_Object, double_buffer::Root, double_buffer::State_Type<Object_State_Tag>, double_buffer::Tagged_Buffer<Object_Buffer_Tag, int>> { struct Test_Object : double_buffer::Def<Test_Object, double_buffer::Root, double_buffer::Tagged_Buffer<Object_Buffer_Tag, int>> {
struct Prop : Prev_Prop { struct Prop : Prev_Prop {
int first{}; int first{};
int second{}; int second{};
}; };
struct State : Prev_State<Object_State_Tag> { struct State : Prev_State {
int first{}; int first{};
int second{}; int second{};
bool operator==(const State&) const = default; bool operator==(const State&) const = default;
@@ -20,20 +19,18 @@ using Object = double_buffer::Impl<Test_Object>;
inline auto& Test_Object::data_for_test() { inline auto& Test_Object::data_for_test() {
return static_cast<Object::Private&>(*d); return static_cast<Object::Private&>(*d);
} }
struct Derived_State_Tag {}; struct Derived_Object : double_buffer::Def<Derived_Object, Test_Object> {
struct Derived_Object : double_buffer::Def<Derived_Object, Test_Object, double_buffer::State_Type<Derived_State_Tag>> {
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {};
struct State : Prev_State<Derived_State_Tag> { struct State : Prev_State {
int derived{}; int derived{};
bool operator==(const State&) const = default; bool operator==(const State&) const = default;
}; };
struct Private : Prev_Private {}; struct Private : Prev_Private {};
}; };
using Derived = double_buffer::Impl<Derived_Object>; using Derived = double_buffer::Impl<Derived_Object>;
struct Lifetime_State_Tag {}; struct Lifetime_Object : double_buffer::Def<Lifetime_Object, double_buffer::Root> {
struct Lifetime_Object : double_buffer::Def<Lifetime_Object, double_buffer::Root, double_buffer::State_Type<Lifetime_State_Tag>> {
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {};
struct State : Prev_State<Lifetime_State_Tag> { struct State : Prev_State {
bool operator==(const State&) const = default; bool operator==(const State&) const = default;
}; };
struct Private : Prev_Private { struct Private : Prev_Private {
@@ -84,13 +81,13 @@ TEST(object_buffer, state_commits_and_keeps_incremental_baseline) {
} }
TEST(object_buffer, prop_publishes_and_keeps_incremental_baseline) { TEST(object_buffer, prop_publishes_and_keeps_incremental_baseline) {
auto object = build_object<Object>(); auto object = build_object<Object>();
object->set(&Test_Object::Prop::first, 17); object->set<&Test_Object::Prop::first>(17);
EXPECT_EQ(object->data_for_test().current->first, 17); EXPECT_EQ(object->data_for_test().current->first, 17);
EXPECT_EQ(object->data_for_test().pending->first, 0); EXPECT_EQ(object->data_for_test().pending->first, 0);
object->advance(); object->advance();
EXPECT_EQ(object->data_for_test().pending->first, 17); EXPECT_EQ(object->data_for_test().pending->first, 17);
EXPECT_EQ(object->data_for_test().current->first, 17); EXPECT_EQ(object->data_for_test().current->first, 17);
object->set(&Test_Object::Prop::second, 19); object->set<&Test_Object::Prop::second>(19);
object->advance(); object->advance();
EXPECT_EQ(object->data_for_test().pending->first, 17); EXPECT_EQ(object->data_for_test().pending->first, 17);
EXPECT_EQ(object->data_for_test().pending->second, 19); EXPECT_EQ(object->data_for_test().pending->second, 19);
@@ -98,7 +95,7 @@ TEST(object_buffer, prop_publishes_and_keeps_incremental_baseline) {
TEST(state_tag, callback_publishes_only_requested_layer) { TEST(state_tag, callback_publishes_only_requested_layer) {
auto object = build_object<Object>(); auto object = build_object<Object>();
int calls = 0; int calls = 0;
object->set_state_callback<Object_State_Tag>( object->set_state_callback<Test_Object::Base_Tag>(
[&](const auto& state) { [&](const auto& state) {
++calls; ++calls;
EXPECT_EQ(state.first, 23); EXPECT_EQ(state.first, 23);
@@ -107,14 +104,14 @@ TEST(state_tag, callback_publishes_only_requested_layer) {
object->update_state<&Test_Object::State::first>(23); object->update_state<&Test_Object::State::first>(23);
object->advance(); object->advance();
EXPECT_EQ(calls, 0); EXPECT_EQ(calls, 0);
object->notify_state<Object_State_Tag>(); object->notify_state<Test_Object::Base_Tag>();
EXPECT_EQ(calls, 1); EXPECT_EQ(calls, 1);
} }
static_assert(requires(Object& object) { static_assert(requires(Object& object) {
object.template access_state<Object_State_Tag>([](const auto&) {}); object.template access_state<Test_Object::Base_Tag>([](const auto&) {});
}); });
static_assert(std::same_as< static_assert(std::same_as<
decltype(std::declval<const Object&>().template read_state<Object_State_Tag>()), decltype(std::declval<const Object&>().template read_state<Test_Object::Base_Tag>()),
const Test_Object::State& const Test_Object::State&
>); >);
struct Missing_State_Tag {}; struct Missing_State_Tag {};
@@ -123,12 +120,12 @@ TEST(state_tag, inherited_tags_remain_independently_addressable) {
auto object = build_object<Derived>(); auto object = build_object<Derived>();
int base_calls = 0; int base_calls = 0;
int derived_calls = 0; int derived_calls = 0;
object->set_state_callback<Object_State_Tag>([&](const auto&) { ++base_calls; }); object->set_state_callback<Test_Object::Base_Tag>([&](const auto&) { ++base_calls; });
object->set_state_callback<Derived_State_Tag>([&](const auto&) { ++derived_calls; }); object->set_state_callback<Derived_Object::Base_Tag>([&](const auto&) { ++derived_calls; });
object->notify_state<Object_State_Tag>(); object->notify_state<Test_Object::Base_Tag>();
EXPECT_EQ(base_calls, 1); EXPECT_EQ(base_calls, 1);
EXPECT_EQ(derived_calls, 0); EXPECT_EQ(derived_calls, 0);
object->notify_state<Derived_State_Tag>(); object->notify_state<Derived_Object::Base_Tag>();
EXPECT_EQ(base_calls, 1); EXPECT_EQ(base_calls, 1);
EXPECT_EQ(derived_calls, 1); EXPECT_EQ(derived_calls, 1);
} }
@@ -137,31 +134,31 @@ TEST(state_tag, committed_layer_is_directly_readable_by_tag) {
object->update_state<&Test_Object::State::first>(41); object->update_state<&Test_Object::State::first>(41);
object->update_state<&Derived_Object::State::derived>(43); object->update_state<&Derived_Object::State::derived>(43);
object->advance(); object->advance();
EXPECT_EQ(object->read_state<Object_State_Tag>().first, 41); EXPECT_EQ(object->read_state<Test_Object::Base_Tag>().first, 41);
EXPECT_EQ(object->read_state<Derived_State_Tag>().derived, 43); EXPECT_EQ(object->read_state<Derived_Object::Base_Tag>().derived, 43);
} }
static_assert(double_buffer::detail::State_Tag_In<double_buffer::Root_State_Tag, Derived::States>); static_assert(double_buffer::detail::State_Tag_In<double_buffer::Root::Base_Tag, Derived::States>);
static_assert(double_buffer::detail::State_Tag_In<Object_State_Tag, Derived::States>); static_assert(double_buffer::detail::State_Tag_In<Test_Object::Base_Tag, Derived::States>);
static_assert(double_buffer::detail::State_Tag_In<Derived_State_Tag, Derived::States>); static_assert(double_buffer::detail::State_Tag_In<Derived_Object::Base_Tag, Derived::States>);
static_assert(std::same_as< static_assert(std::same_as<
decltype(std::declval<double_buffer::State_Access<Derived::State>>().template get<Object_State_Tag>()), decltype(std::declval<double_buffer::State_Access<Derived::State>>().template get<Test_Object::Base_Tag>()),
Test_Object::State& Test_Object::State&
>); >);
static_assert(std::same_as< static_assert(std::same_as<
decltype(std::declval<double_buffer::State_Access<const Derived::State>>().template get<Derived_State_Tag>()), decltype(std::declval<double_buffer::State_Access<const Derived::State>>().template get<Derived_Object::Base_Tag>()),
const Derived_Object::State& const Derived_Object::State&
>); >);
TEST(state_tag, state_access_selects_mutable_and_const_layers_by_tag) { TEST(state_tag, state_access_selects_mutable_and_const_layers_by_tag) {
Derived::State state; Derived::State state;
double_buffer::State_Access states{state}; double_buffer::State_Access states{state};
states.get<Object_State_Tag>().first = 31; states.get<Test_Object::Base_Tag>().first = 31;
states.get<Derived_State_Tag>().derived = 47; states.get<Derived_Object::Base_Tag>().derived = 47;
double_buffer::State_Access<Test_Object::State> base_states = states; double_buffer::State_Access<Test_Object::State> base_states = states;
EXPECT_EQ(base_states.get<Object_State_Tag>().first, 31); EXPECT_EQ(base_states.get<Test_Object::Base_Tag>().first, 31);
const auto& const_state = state; const auto& const_state = state;
double_buffer::State_Access current_states{const_state}; double_buffer::State_Access current_states{const_state};
EXPECT_EQ(current_states.get<Object_State_Tag>().first, 31); EXPECT_EQ(current_states.get<Test_Object::Base_Tag>().first, 31);
EXPECT_EQ(current_states.get<Derived_State_Tag>().derived, 47); EXPECT_EQ(current_states.get<Derived_Object::Base_Tag>().derived, 47);
} }
TEST(state_tag, state_chain_keeps_default_equality_usable) { TEST(state_tag, state_chain_keeps_default_equality_usable) {
Test_Object::State first; Test_Object::State first;
@@ -170,3 +167,21 @@ TEST(state_tag, state_chain_keeps_default_equality_usable) {
second.first = 1; second.first = 1;
EXPECT_FALSE(first == second); EXPECT_FALSE(first == second);
} }
static_assert(std::same_as<
decltype(std::declval<double_buffer::Prop_Access<Derived::Prop>>().template get<Test_Object::Base_Tag>()),
Test_Object::Prop&
>);
static_assert(std::same_as<
decltype(std::declval<double_buffer::Private_Access<Derived_Object::Private>>().template get<Test_Object::Base_Tag>()),
Test_Object::Private&
>);
TEST(base_tag, prop_state_and_private_domains_are_independently_addressable) {
Derived::Prop prop;
double_buffer::Prop_Access props{prop};
props.get<Test_Object::Base_Tag>().first = 53;
EXPECT_EQ(props.get<Test_Object::Base_Tag>().first, 53);
Derived_Object::Private private_data;
double_buffer::Private_Access private_layers{private_data};
EXPECT_EQ(&private_layers.get<Test_Object::Base_Tag>(), static_cast<Test_Object::Private*>(&private_data));
EXPECT_EQ(&private_layers.get<Derived_Object::Base_Tag>(), static_cast<Derived_Object::Private*>(&private_data));
}
+11 -14
View File
@@ -1,11 +1,9 @@
#include "scene.hpp" #include "scene.hpp"
#include <gtest/gtest.h> #include <gtest/gtest.h>
namespace { namespace {
struct Direct_State_Tag {}; struct Direct_Renderable : double_buffer::Def<Direct_Renderable, aethera::Renderable> {
struct Graph_State_Tag {};
struct Direct_Renderable : double_buffer::Def<Direct_Renderable, aethera::Renderable, double_buffer::State_Type<Direct_State_Tag>> {
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {};
struct State : Prev_State<Direct_State_Tag> { struct State : Prev_State {
bool operator==(const State&) const = default; bool operator==(const State&) const = default;
}; };
struct Private : Prev_Private { struct Private : Prev_Private {
@@ -20,9 +18,9 @@ struct Direct_Renderable : double_buffer::Def<Direct_Renderable, aethera::Render
}; };
Private& data_for_test(); Private& data_for_test();
}; };
struct Graph_Renderable : double_buffer::Def<Graph_Renderable, aethera::Renderable, double_buffer::State_Type<Graph_State_Tag>> { struct Graph_Renderable : double_buffer::Def<Graph_Renderable, aethera::Renderable> {
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {};
struct State : Prev_State<Graph_State_Tag> { struct State : Prev_State {
bool operator==(const State&) const = default; bool operator==(const State&) const = default;
}; };
struct Private : Prev_Private { struct Private : Prev_Private {
@@ -30,13 +28,13 @@ struct Graph_Renderable : double_buffer::Def<Graph_Renderable, aethera::Renderab
int paint_calls{}; int paint_calls{};
int prepare_builds{}; int prepare_builds{};
bool rebuild_prepare{}; bool rebuild_prepare{};
tf::Taskflow build_prepare_graph(double_buffer::Attached auto*, const State&) { tf::Taskflow build_prepare_graph(double_buffer::Attached auto*, const Prop&) {
++prepare_builds; ++prepare_builds;
tf::Taskflow graph; tf::Taskflow graph;
graph.emplace([this] { ++prepare_calls; }).name("test.prepare.graph.task"); graph.emplace([this] { ++prepare_calls; }).name("test.prepare.graph.task");
return graph; return graph;
} }
bool should_rebuild_prepare_graph(double_buffer::Attached auto*, const State&) { bool should_rebuild_prepare_graph(double_buffer::Attached auto*, const Prop&) {
return std::exchange(rebuild_prepare, false); return std::exchange(rebuild_prepare, false);
} }
void paint(double_buffer::Attached auto*) { void paint(double_buffer::Attached auto*) {
@@ -54,10 +52,9 @@ inline Direct_Renderable::Private& Direct_Renderable::data_for_test() {
inline Graph_Renderable::Private& Graph_Renderable::data_for_test() { inline Graph_Renderable::Private& Graph_Renderable::data_for_test() {
return static_cast<Private&>(*d); return static_cast<Private&>(*d);
} }
struct Dependency_State_Tag {}; struct Dependency_Renderable : double_buffer::Def<Dependency_Renderable, aethera::Renderable> {
struct Dependency_Renderable : double_buffer::Def<Dependency_Renderable, aethera::Renderable, double_buffer::State_Type<Dependency_State_Tag>> {
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {};
struct State : Prev_State<Dependency_State_Tag> { struct State : Prev_State {
int revision{}; int revision{};
bool operator==(const State&) const = default; bool operator==(const State&) const = default;
}; };
@@ -141,12 +138,12 @@ TEST(renderable_state, scene_and_renderable_callbacks_publish_at_stage_boundarie
int renderable_updates = 0; int renderable_updates = 0;
int scene_updates = 0; int scene_updates = 0;
int runtime_updates = 0; int runtime_updates = 0;
renderable->set_state_callback<aethera::Renderable_State_Tag>([&](const auto& state) { renderable->set_state_callback<aethera::Renderable::Base_Tag>([&](const auto& state) {
++renderable_updates; ++renderable_updates;
EXPECT_TRUE(state.prepare_executed); EXPECT_TRUE(state.prepare_executed);
EXPECT_TRUE(state.paint_executed); EXPECT_TRUE(state.paint_executed);
}); });
scene->set_state_callback<aethera::Scene_State_Tag>([&](const auto& state) { scene->set_state_callback<aethera::Scene::Base_Tag>([&](const auto& state) {
++scene_updates; ++scene_updates;
EXPECT_GT(state.taskflow_task_count, 0u); EXPECT_GT(state.taskflow_task_count, 0u);
}); });
@@ -169,7 +166,7 @@ TEST(scene_state, structural_statistics_survive_a_process_without_rebuild) {
std::size_t task_count{}; std::size_t task_count{};
std::size_t dependency_count{}; std::size_t dependency_count{};
int updates{}; int updates{};
scene->set_state_callback<aethera::Scene_State_Tag>([&](const auto& state) { scene->set_state_callback<aethera::Scene::Base_Tag>([&](const auto& state) {
EXPECT_EQ(state.renderable_count, 1u); EXPECT_EQ(state.renderable_count, 1u);
EXPECT_GT(state.taskflow_task_count, 0u); EXPECT_GT(state.taskflow_task_count, 0u);
if (updates == 0) { if (updates == 0) {
+1
View File
@@ -5,6 +5,7 @@
#include <sstream> #include <sstream>
namespace aethera::render_2d { namespace aethera::render_2d {
bool Abs_Axis::State::operator==(const State&) const = default; bool Abs_Axis::State::operator==(const State&) const = default;
bool Abs_Axis::Prop::operator==(const Prop&) const = default;
Axis_Range Abs_Axis::Private::coordinate_range(const Root* object) const { Axis_Range Abs_Axis::Private::coordinate_range(const Root* object) const {
return dispatch->coordinate_range(object); return dispatch->coordinate_range(object);
+8 -9
View File
@@ -4,10 +4,8 @@
#include <renderable.hpp> #include <renderable.hpp>
#include <string> #include <string>
namespace aethera::render_2d { namespace aethera::render_2d {
/* Abs_Axis 状态标签,用于访问和订阅坐标轴布局状态。 */
struct Abs_Axis_State_Tag {};
struct Abs_Axis; struct Abs_Axis;
/* 最终轴 Private 的完整计算能力契约;所有结果直接来自当前 State。 */ /* 最终轴 Private 的完整计算能力契约;所有结果直接来自当前 Prop。 */
template <typename T> template <typename T>
concept Axis_Object = Renderable_Object<T> && std::derived_from<T, Abs_Axis> && requires( concept Axis_Object = Renderable_Object<T> && std::derived_from<T, Abs_Axis> && requires(
const typename T::Private& private_data, const typename T::Private& private_data,
@@ -21,12 +19,9 @@ concept Axis_Object = Renderable_Object<T> && std::derived_from<T, Abs_Axis> &&
{ private_data.sub_tick_count(object, tick) } -> std::same_as<Axis_Tick_Count>; { private_data.sub_tick_count(object, tick) } -> std::same_as<Axis_Tick_Count>;
}; };
/* 所有二维坐标轴共享的定义层;最终通过 Impl<Derived_Axis> 创建运行时对象。 */ /* 所有二维坐标轴共享的定义层;最终通过 Impl<Derived_Axis> 创建运行时对象。 */
struct Abs_Axis : Def<Abs_Axis, Renderable, State_Type<Abs_Axis_State_Tag>, struct Abs_Axis : Def<Abs_Axis, Renderable,
Tagged_Buffer<Color_Cache, Blend2D_Cache>> { Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
/* Abs_Axis 不发布额外属性。 */ struct Prop : Prev_Prop {
struct Prop : Prev_Prop {};
/* 双缓冲交换后供轴计算直接读取的布局权威状态。 */
struct State : Prev_State<Abs_Axis_State_Tag> {
Point_F position{}; /* 坐标轴起点在画布中的二维像素位置。 */ Point_F position{}; /* 坐标轴起点在画布中的二维像素位置。 */
Size canvas_size{}; /* 颜色缓存与裁剪区域使用的画布像素尺寸。 */ Size canvas_size{}; /* 颜色缓存与裁剪区域使用的画布像素尺寸。 */
Axis_Pixel_Length pixel_length{}; /* 从坐标起点到终点的轴向像素跨度;为 0 时反算返回坐标起点。 */ Axis_Pixel_Length pixel_length{}; /* 从坐标起点到终点的轴向像素跨度;为 0 时反算返回坐标起点。 */
@@ -39,11 +34,15 @@ struct Abs_Axis : Def<Abs_Axis, Renderable, State_Type<Abs_Axis_State_Tag>,
Pen unit_text_pen{Color::white()}; /* 刻度标签和单位文本使用的前景样式。 */ Pen unit_text_pen{Color::white()}; /* 刻度标签和单位文本使用的前景样式。 */
Brush unit_text_background_brush{}; /* 单位文本背景填充;none 表示不填充。 */ Brush unit_text_background_brush{}; /* 单位文本背景填充;none 表示不填充。 */
Axis_Label_Rotation label_rotation_degrees{}; /* 刻度标签顺时针旋转角度,单位为度。 */ Axis_Label_Rotation label_rotation_degrees{}; /* 刻度标签顺时针旋转角度,单位为度。 */
bool operator==(const Prop&) const;
};
/* 坐标轴当前没有额外发布状态。 */
struct State : Prev_State {
bool operator==(const State&) const; bool operator==(const State&) const;
}; };
/* 完整声明及派生轴 CRTP 能力契约见 Abs_Axis.ipp。 */ /* 完整声明及派生轴 CRTP 能力契约见 Abs_Axis.ipp。 */
struct Private; struct Private;
/* 返回由最终轴 State 计算得到的当前有向坐标区间。 */ /* 返回由最终轴 Prop 计算得到的当前有向坐标区间。 */
[[nodiscard]] Axis_Range coordinate_range() const; [[nodiscard]] Axis_Range coordinate_range() const;
/* 将坐标值映射到当前轴向像素位置。 */ /* 将坐标值映射到当前轴向像素位置。 */
[[nodiscard]] Axis_Pixel_Position coordinate_to_pixel(Axis_Coordinate coordinate) const; [[nodiscard]] Axis_Pixel_Position coordinate_to_pixel(Axis_Coordinate coordinate) const;
+7 -7
View File
@@ -10,7 +10,7 @@ struct Abs_Axis::Private : Prev_Private {
* double tick_step(const T* object, Axis_Range coordinate_range) const:返回主刻度步长。 * double tick_step(const T* object, Axis_Range coordinate_range) const:返回主刻度步长。
* std::string tick_label(const T* object, double tick) const:返回主刻度显示文本。 * std::string tick_label(const T* object, double tick) const:返回主刻度显示文本。
* int sub_tick_count(const T* object, double major_step) const:返回次刻度数量;默认固定返回 4。 * int sub_tick_count(const T* object, double major_step) const:返回次刻度数量;默认固定返回 4。
* 坐标映射直接读取 Abs_Axis_State_Tag 状态并调用最终 Private 的 coordinate_range(...),不保存变换快照。 * 坐标映射直接读取 Abs_Axis::Base_Tag 状态并调用最终 Private 的 coordinate_range(...),不保存变换快照。
*/ */
using Coordinate_Range_Call = Axis_Range (*)(const Root*); using Coordinate_Range_Call = Axis_Range (*)(const Root*);
using Scalar_Call = double (*)(const Root*, double); using Scalar_Call = double (*)(const Root*, double);
@@ -81,7 +81,7 @@ const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() {
[](const Root* root, double coordinate) { [](const Root* root, double coordinate) {
auto* object = static_cast<const Object*>(root); auto* object = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*object->d); const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
const auto& axis_state = static_cast<const State&>(*private_data.state.current); const auto& axis_state = static_cast<const Prop&>(*private_data.current);
const Axis_Range range = private_data.coordinate_range(object); const Axis_Range range = private_data.coordinate_range(object);
const double coordinate_length = range.length(); const double coordinate_length = range.length();
const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal
@@ -92,7 +92,7 @@ const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() {
[](const Root* root, double pixel) { [](const Root* root, double pixel) {
auto* object = static_cast<const Object*>(root); auto* object = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*object->d); const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
const auto& axis_state = static_cast<const State&>(*private_data.state.current); const auto& axis_state = static_cast<const Prop&>(*private_data.current);
const Axis_Range range = private_data.coordinate_range(object); const Axis_Range range = private_data.coordinate_range(object);
if (axis_state.pixel_length == 0.0) return range.origin; if (axis_state.pixel_length == 0.0) return range.origin;
const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal
@@ -102,7 +102,7 @@ const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() {
[](const Root* root, Point_F point) { [](const Root* root, Point_F point) {
auto* object = static_cast<const Object*>(root); auto* object = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*object->d); const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
const auto& axis_state = static_cast<const State&>(*private_data.state.current); const auto& axis_state = static_cast<const Prop&>(*private_data.current);
const double pixel = axis_state.orientation == Axis_Orientation::horizontal ? point.x : point.y; const double pixel = axis_state.orientation == Axis_Orientation::horizontal ? point.x : point.y;
const Axis_Range range = private_data.coordinate_range(object); const Axis_Range range = private_data.coordinate_range(object);
if (axis_state.pixel_length == 0.0) return range.origin; if (axis_state.pixel_length == 0.0) return range.origin;
@@ -113,7 +113,7 @@ const Abs_Axis::Private::Dispatch& Abs_Axis::Private::dispatch_for() {
[](const Root* root, Axis_Range coordinate_range) { [](const Root* root, Axis_Range coordinate_range) {
auto* object = static_cast<const Object*>(root); auto* object = static_cast<const Object*>(root);
const auto& private_data = static_cast<const typename Object::Private&>(*object->d); const auto& private_data = static_cast<const typename Object::Private&>(*object->d);
const auto& axis_state = static_cast<const State&>(*private_data.state.current); const auto& axis_state = static_cast<const Prop&>(*private_data.current);
const Axis_Range range = private_data.coordinate_range(object); const Axis_Range range = private_data.coordinate_range(object);
const auto map = [&](double coordinate) { const auto map = [&](double coordinate) {
const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal const double pixel_origin = axis_state.orientation == Axis_Orientation::horizontal
@@ -151,7 +151,7 @@ void Abs_Axis::Private::bind_private_crtp(Object* object) {
inline void Abs_Axis::Private::prepare_data(Attached auto* object) { inline void Abs_Axis::Private::prepare_data(Attached auto* object) {
using Object = std::remove_pointer_t<decltype(object)>; using Object = std::remove_pointer_t<decltype(object)>;
auto& private_data = static_cast<typename Object::Private&>(*this); auto& private_data = static_cast<typename Object::Private&>(*this);
const auto& state = static_cast<const State&>(*private_data.state.current); const auto& state = static_cast<const Prop&>(*private_data.current);
auto& output = prepared; auto& output = prepared;
output = {}; output = {};
if (state.pixel_length == 0.0 || state.canvas_size.empty()) return; if (state.pixel_length == 0.0 || state.canvas_size.empty()) return;
@@ -206,7 +206,7 @@ inline void Abs_Axis::Private::prepare_data(Attached auto* object) {
inline void Abs_Axis::Private::paint(Attached auto* object) { inline void Abs_Axis::Private::paint(Attached auto* object) {
using Object = std::remove_pointer_t<decltype(object)>; using Object = std::remove_pointer_t<decltype(object)>;
auto& private_data = static_cast<typename Object::Private&>(*this); auto& private_data = static_cast<typename Object::Private&>(*this);
const auto& state = static_cast<const State&>(*private_data.state.current); const auto& state = static_cast<const Prop&>(*private_data.current);
auto& cache = object->template pending_buffer<Color_Cache>(); auto& cache = object->template pending_buffer<Color_Cache>();
cache.ensure_size(state.canvas_size); cache.ensure_size(state.canvas_size);
cache.clear(); cache.clear();
+2 -4
View File
@@ -1,14 +1,12 @@
#pragma once #pragma once
#include "Numeric_Axis.hpp" #include "Numeric_Axis.hpp"
namespace aethera::render_2d { namespace aethera::render_2d {
/* Frequency_Axis 状态标签,用于独立订阅频率轴层。 */
struct Frequency_Axis_State_Tag {};
/* 根据数值量级自动选择 Hz、kHz 或 MHz 标签的数值轴。 */ /* 根据数值量级自动选择 Hz、kHz 或 MHz 标签的数值轴。 */
struct Frequency_Axis : Def<Frequency_Axis, Numeric_Axis, State_Type<Frequency_Axis_State_Tag>> { struct Frequency_Axis : Def<Frequency_Axis, Numeric_Axis> {
/* Frequency_Axis 不发布额外属性。 */ /* Frequency_Axis 不发布额外属性。 */
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {};
/* Frequency_Axis 没有重复保存数值轴状态,仅保留独立状态层。 */ /* Frequency_Axis 没有重复保存数值轴状态,仅保留独立状态层。 */
struct State : Prev_State<Frequency_Axis_State_Tag> { struct State : Prev_State {
bool operator==(const State&) const; bool operator==(const State&) const;
}; };
/* 完整声明及频率标签 CRTP 覆盖见 Frequency_Axis.ipp。 */ /* 完整声明及频率标签 CRTP 覆盖见 Frequency_Axis.ipp。 */
+1 -1
View File
@@ -7,7 +7,7 @@ struct Frequency_Axis::Private : Prev_Private {
inline std::string Frequency_Axis::Private::tick_label(const Attached auto* object, double tick) const { inline std::string Frequency_Axis::Private::tick_label(const Attached auto* object, double tick) const {
using Object = std::remove_cv_t<std::remove_pointer_t<decltype(object)>>; using Object = std::remove_cv_t<std::remove_pointer_t<decltype(object)>>;
const auto& final_private = static_cast<const typename Object::Private&>(*this); const auto& final_private = static_cast<const typename Object::Private&>(*this);
const auto& state = static_cast<const Numeric_Axis::State&>(*final_private.state.current); const auto& state = static_cast<const Numeric_Axis::Prop&>(*final_private.current);
const double absolute = std::abs(tick); const double absolute = std::abs(tick);
if (absolute >= 1'000'000.0) return localized_number(tick / 1'000'000.0, state.precision, state.locale) + " MHz"; if (absolute >= 1'000'000.0) return localized_number(tick / 1'000'000.0, state.precision, state.locale) + " MHz";
if (absolute >= 1'000.0) return localized_number(tick / 1'000.0, state.precision, state.locale) + " kHz"; if (absolute >= 1'000.0) return localized_number(tick / 1'000.0, state.precision, state.locale) + " kHz";
@@ -2,4 +2,5 @@
namespace aethera::render_2d { namespace aethera::render_2d {
bool Numeric_Axis::State::operator==(const State&) const = default; bool Numeric_Axis::State::operator==(const State&) const = default;
bool Numeric_Axis::Prop::operator==(const Prop&) const = default;
} }
+6 -7
View File
@@ -1,19 +1,18 @@
#pragma once #pragma once
#include "Abs_Axis.hpp" #include "Abs_Axis.hpp"
namespace aethera::render_2d { namespace aethera::render_2d {
/* Numeric_Axis 状态标签,用于访问和订阅数值轴状态。 */
struct Numeric_Axis_State_Tag {};
/* 具有显式数值范围和十进制标签的坐标轴定义层。 */ /* 具有显式数值范围和十进制标签的坐标轴定义层。 */
struct Numeric_Axis : Def<Numeric_Axis, Abs_Axis, State_Type<Numeric_Axis_State_Tag>> { struct Numeric_Axis : Def<Numeric_Axis, Abs_Axis> {
/* Numeric_Axis 不发布额外属性。 */ struct Prop : Prev_Prop {
struct Prop : Prev_Prop {};
/* 数值轴的权威状态;范围和标签格式都由该层直接提供。 */
struct State : Prev_State<Numeric_Axis_State_Tag> {
Axis_Range coordinate_range{0.0, 20.0}; /* 当前有向数值区间;必须有限且非零。 */ Axis_Range coordinate_range{0.0, 20.0}; /* 当前有向数值区间;必须有限且非零。 */
Axis_Label_Precision precision{2}; /* 标签最大小数位数;格式化时限制到 0..12。 */ Axis_Label_Precision precision{2}; /* 标签最大小数位数;格式化时限制到 0..12。 */
Number_Locale locale{}; /* 数值标签的小数点规则。 */ Number_Locale locale{}; /* 数值标签的小数点规则。 */
bool wheel_enabled{true}; /* 是否允许滚轮以指针位置为锚点缩放坐标范围。 */ bool wheel_enabled{true}; /* 是否允许滚轮以指针位置为锚点缩放坐标范围。 */
bool drag_enabled{true}; /* 是否允许按住鼠标左键拖动坐标范围。 */ bool drag_enabled{true}; /* 是否允许按住鼠标左键拖动坐标范围。 */
bool operator==(const Prop&) const;
};
/* 数值轴当前没有额外发布状态。 */
struct State : Prev_State {
bool operator==(const State&) const; bool operator==(const State&) const;
}; };
/* 完整声明及数值轴 CRTP 能力见 Numeric_Axis.ipp。 */ /* 完整声明及数值轴 CRTP 能力见 Numeric_Axis.ipp。 */
+17 -17
View File
@@ -7,7 +7,7 @@ struct Numeric_Axis::Private : Prev_Private {
std::mutex interaction_mutex{}; /* 保护跨事件保留的拖动手势状态。 */ std::mutex interaction_mutex{}; /* 保护跨事件保留的拖动手势状态。 */
bool dragging{}; /* 左键拖动手势是否已经开始且尚未释放。 */ bool dragging{}; /* 左键拖动手势是否已经开始且尚未释放。 */
Point_F last_pointer{}; /* 上一个拖动事件的位置,单位为画布局部像素。 */ Point_F last_pointer{}; /* 上一个拖动事件的位置,单位为画布局部像素。 */
/* CRTP 实现:直接返回 Numeric_Axis_State_Tag 中唯一保存的数值范围。 */ /* CRTP 实现:直接返回 Numeric_Axis::Base_Tag 中唯一保存的数值范围。 */
[[nodiscard]] Axis_Range coordinate_range(const Attached auto* object) const; [[nodiscard]] Axis_Range coordinate_range(const Attached auto* object) const;
/* CRTP 实现:使用 Abs_Axis::Private 的 1/2/5 十进制算法计算主刻度。 */ /* CRTP 实现:使用 Abs_Axis::Private 的 1/2/5 十进制算法计算主刻度。 */
[[nodiscard]] double tick_step(const Attached auto* object, Axis_Range coordinate_range) const; [[nodiscard]] double tick_step(const Attached auto* object, Axis_Range coordinate_range) const;
@@ -16,13 +16,13 @@ struct Numeric_Axis::Private : Prev_Private {
/* CRTP 实现:处理滚轮缩放和左键拖动,并在消费事件后标记 Prepare dirty。 */ /* CRTP 实现:处理滚轮缩放和左键拖动,并在消费事件后标记 Prepare dirty。 */
void handle_event(Attached auto* object, const Event& event); void handle_event(Attached auto* object, const Event& event);
/* CRTP State 钩子:拒绝非有限、零长度范围以及 0..12 之外的精度,并恢复本次无效写入。 */ /* CRTP State 钩子:拒绝非有限、零长度范围以及 0..12 之外的精度,并恢复本次无效写入。 */
template <typename Object, typename Owner, typename Member, typename State_Type> template <typename Object, typename Owner, typename Member, typename Prop_Type>
void after_state_set(Object* object, Member Owner::* member, State_Access<State_Type> pending_states); void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> props);
}; };
inline Axis_Range Numeric_Axis::Private::coordinate_range(const Attached auto* object) const { inline Axis_Range Numeric_Axis::Private::coordinate_range(const Attached auto* object) const {
using Object = std::remove_cv_t<std::remove_pointer_t<decltype(object)>>; using Object = std::remove_cv_t<std::remove_pointer_t<decltype(object)>>;
const auto& private_data = static_cast<const typename Object::Private&>(*this); const auto& private_data = static_cast<const typename Object::Private&>(*this);
return static_cast<const State&>(*private_data.state.current).coordinate_range; return static_cast<const Prop&>(*private_data.current).coordinate_range;
} }
inline double Numeric_Axis::Private::tick_step(const Attached auto*, Axis_Range coordinate_range) const { inline double Numeric_Axis::Private::tick_step(const Attached auto*, Axis_Range coordinate_range) const {
return nice_tick_step(coordinate_range); return nice_tick_step(coordinate_range);
@@ -30,14 +30,14 @@ inline double Numeric_Axis::Private::tick_step(const Attached auto*, Axis_Range
inline std::string Numeric_Axis::Private::tick_label(const Attached auto* object, double tick) const { inline std::string Numeric_Axis::Private::tick_label(const Attached auto* object, double tick) const {
using Object = std::remove_cv_t<std::remove_pointer_t<decltype(object)>>; using Object = std::remove_cv_t<std::remove_pointer_t<decltype(object)>>;
const auto& private_data = static_cast<const typename Object::Private&>(*this); const auto& private_data = static_cast<const typename Object::Private&>(*this);
const auto& state = static_cast<const State&>(*private_data.state.current); const auto& state = static_cast<const Prop&>(*private_data.current);
return localized_number(tick, state.precision, state.locale); return localized_number(tick, state.precision, state.locale);
} }
inline void Numeric_Axis::Private::handle_event(Attached auto* object, const Event& event) { inline void Numeric_Axis::Private::handle_event(Attached auto* object, const Event& event) {
using Object = std::remove_pointer_t<decltype(object)>; using Object = std::remove_pointer_t<decltype(object)>;
auto& private_data = static_cast<typename Object::Private&>(*this); auto& private_data = static_cast<typename Object::Private&>(*this);
const auto& numeric_state = static_cast<const State&>(*private_data.state.current); const auto& numeric_state = static_cast<const Prop&>(*private_data.current);
const auto& axis_state = static_cast<const Abs_Axis::State&>(*private_data.state.current); const auto& axis_state = static_cast<const Abs_Axis::Prop&>(*private_data.current);
const auto* pointer = dynamic_cast<const Pointer_Event_Capability*>(&event); const auto* pointer = dynamic_cast<const Pointer_Event_Capability*>(&event);
if (event.type == Event_Type::wheel && numeric_state.wheel_enabled) { if (event.type == Event_Type::wheel && numeric_state.wheel_enabled) {
const auto* wheel = dynamic_cast<const Wheel_Event_Capability*>(&event); const auto* wheel = dynamic_cast<const Wheel_Event_Capability*>(&event);
@@ -47,7 +47,7 @@ inline void Numeric_Axis::Private::handle_event(Attached auto* object, const Eve
const double anchor = pixel_to_coordinate(object, anchor_pixel); const double anchor = pixel_to_coordinate(object, anchor_pixel);
const double factor = wheel->angle_delta_y_value() >= 0.0 ? 0.9 : 1.1; const double factor = wheel->angle_delta_y_value() >= 0.0 ? 0.9 : 1.1;
const Axis_Range range = numeric_state.coordinate_range; const Axis_Range range = numeric_state.coordinate_range;
object->template update_state<&State::coordinate_range>(Axis_Range{ object->template set<&Prop::coordinate_range>(Axis_Range{
anchor + (range.origin - anchor) * factor, anchor + (range.origin - anchor) * factor,
anchor + (range.target - anchor) * factor anchor + (range.target - anchor) * factor
}); });
@@ -76,7 +76,7 @@ inline void Numeric_Axis::Private::handle_event(Attached auto* object, const Eve
? current.x - previous.x : current.y - previous.y; ? current.x - previous.x : current.y - previous.y;
const double shift = axis_state.pixel_length == 0.0 const double shift = axis_state.pixel_length == 0.0
? 0.0 : -delta * numeric_state.coordinate_range.length() / axis_state.pixel_length; ? 0.0 : -delta * numeric_state.coordinate_range.length() / axis_state.pixel_length;
object->template update_state<&State::coordinate_range>(Axis_Range{ object->template set<&Prop::coordinate_range>(Axis_Range{
numeric_state.coordinate_range.origin + shift, numeric_state.coordinate_range.origin + shift,
numeric_state.coordinate_range.target + shift numeric_state.coordinate_range.target + shift
}); });
@@ -91,20 +91,20 @@ inline void Numeric_Axis::Private::handle_event(Attached auto* object, const Eve
if (was_dragging) event.accept(); if (was_dragging) event.accept();
} }
} }
template <typename Object, typename Owner, typename Member, typename State_Type> template <typename Object, typename Owner, typename Member, typename Prop_Type>
void Numeric_Axis::Private::after_state_set(Object*, Member Owner::* member, State_Access<State_Type> pending_states) { void Numeric_Axis::Private::after_prop_set(Object*, Member Owner::* member, Prop_Access<Prop_Type> props) {
const auto& private_data = static_cast<const typename Object::Private&>(*this); const auto& private_data = static_cast<const typename Object::Private&>(*this);
auto& pending = pending_states.template get<Numeric_Axis_State_Tag>(); auto& pending = props.template get<Numeric_Axis::Base_Tag>();
const auto& current = static_cast<const State&>(*private_data.state.current); const auto& current = static_cast<const Prop&>(*private_data.pending);
if constexpr (std::same_as<Owner, State> && std::same_as<Member, Axis_Range>) { if constexpr (std::same_as<Owner, Prop> && std::same_as<Member, Axis_Range>) {
if (member != &State::coordinate_range) return; if (member != &Prop::coordinate_range) return;
const Axis_Range range = pending.coordinate_range; const Axis_Range range = pending.coordinate_range;
if (std::isfinite(range.origin) && std::isfinite(range.target) && range.size() > 0.0) return; if (std::isfinite(range.origin) && std::isfinite(range.target) && range.size() > 0.0) return;
pending.coordinate_range = current.coordinate_range; pending.coordinate_range = current.coordinate_range;
throw std::invalid_argument("numeric axis coordinate range must be finite and non-empty"); throw std::invalid_argument("numeric axis coordinate range must be finite and non-empty");
} }
else if constexpr (std::same_as<Owner, State> && std::same_as<Member, int>) { else if constexpr (std::same_as<Owner, Prop> && std::same_as<Member, int>) {
if (member != &State::precision || (pending.precision >= 0 && pending.precision <= 12)) return; if (member != &Prop::precision || (pending.precision >= 0 && pending.precision <= 12)) return;
pending.precision = current.precision; pending.precision = current.precision;
throw std::invalid_argument("numeric axis precision must be between 0 and 12"); throw std::invalid_argument("numeric axis precision must be between 0 and 12");
} }
+1
View File
@@ -3,6 +3,7 @@
#include <sstream> #include <sstream>
namespace aethera::render_2d { namespace aethera::render_2d {
bool Time_Axis::State::operator==(const State&) const = default; bool Time_Axis::State::operator==(const State&) const = default;
bool Time_Axis::Prop::operator==(const Prop&) const = default;
std::size_t Time_Axis::Private::time_point_count(const Root* object) const { std::size_t Time_Axis::Private::time_point_count(const Root* object) const {
return time_dispatch->time_point_count(object); return time_dispatch->time_point_count(object);
+6 -7
View File
@@ -5,19 +5,18 @@
#include <string> #include <string>
#include <utility> #include <utility>
namespace aethera::render_2d { namespace aethera::render_2d {
/* Time_Axis 状态标签,用于访问和订阅时间轴状态。 */
struct Time_Axis_State_Tag {};
/* 将连续样本序号显示为一天内时间文本的坐标轴。 */ /* 将连续样本序号显示为一天内时间文本的坐标轴。 */
struct Time_Axis : Def<Time_Axis, Abs_Axis, State_Type<Time_Axis_State_Tag>> { struct Time_Axis : Def<Time_Axis, Abs_Axis> {
/* Time_Axis 不发布额外属性。 */ struct Prop : Prev_Prop {
struct Prop : Prev_Prop {};
/* 时间轴样本与显示参数的唯一权威状态。 */
struct State : Prev_State<Time_Axis_State_Tag> {
Axis_Visible_Count visible_count{100}; /* 当前坐标区间最多覆盖的样本数量;小于 2 时按 2 计算。 */ Axis_Visible_Count visible_count{100}; /* 当前坐标区间最多覆盖的样本数量;小于 2 时按 2 计算。 */
Axis_Pixel_Length tick_label_spacing_px{8.0}; /* 相邻标签之间预留的像素间距;负值按 0 计算。 */ Axis_Pixel_Length tick_label_spacing_px{8.0}; /* 相邻标签之间预留的像素间距;负值按 0 计算。 */
Axis_Pixel_Length estimated_label_width_px{48.0}; /* 当前字体下单个时间标签的估算像素宽度;小于 48 时按 48 计算。 */ Axis_Pixel_Length estimated_label_width_px{48.0}; /* 当前字体下单个时间标签的估算像素宽度;小于 48 时按 48 计算。 */
std::string format{"mm:ss.zzz"}; /* 标签格式;支持 hh、HH、mm、ss 和 zzz。 */ std::string format{"mm:ss.zzz"}; /* 标签格式;支持 hh、HH、mm、ss 和 zzz。 */
bool newest_at_start{}; /* true 时最新样本位于坐标区间起点。 */ bool newest_at_start{}; /* true 时最新样本位于坐标区间起点。 */
bool operator==(const Prop&) const;
};
/* 时间样本是由 append_time 发布给观察方的运行状态。 */
struct State : Prev_State {
Axis_Time_Tick next_tick{}; /* 下一次 append_time 分配的单调样本序号。 */ Axis_Time_Tick next_tick{}; /* 下一次 append_time 分配的单调样本序号。 */
std::deque<std::pair<Axis_Time_Tick, Time_Of_Day>> samples{}; /* tick 到时间的保留窗口;最多保留 max(512, visible_count*4) 项。 */ std::deque<std::pair<Axis_Time_Tick, Time_Of_Day>> samples{}; /* tick 到时间的保留窗口;最多保留 max(512, visible_count*4) 项。 */
bool operator==(const State&) const; bool operator==(const State&) const;
+10 -7
View File
@@ -33,16 +33,17 @@ inline Axis_Range Time_Axis::Private::coordinate_range(const Attached auto* obje
using Object = std::remove_cv_t<std::remove_pointer_t<decltype(object)>>; using Object = std::remove_cv_t<std::remove_pointer_t<decltype(object)>>;
const auto& private_data = static_cast<const typename Object::Private&>(*this); const auto& private_data = static_cast<const typename Object::Private&>(*this);
const auto& state = static_cast<const State&>(*private_data.state.current); const auto& state = static_cast<const State&>(*private_data.state.current);
const auto& prop = static_cast<const Prop&>(*private_data.current);
const int latest = std::max(1, state.next_tick - 1); const int latest = std::max(1, state.next_tick - 1);
const int earliest = std::max(0, latest - std::max(2, state.visible_count) + 1); const int earliest = std::max(0, latest - std::max(2, prop.visible_count) + 1);
if (state.newest_at_start) return {static_cast<double>(latest), static_cast<double>(earliest)}; if (prop.newest_at_start) return {static_cast<double>(latest), static_cast<double>(earliest)};
return {static_cast<double>(earliest), static_cast<double>(latest)}; return {static_cast<double>(earliest), static_cast<double>(latest)};
} }
inline double Time_Axis::Private::tick_step(const Attached auto* object, Axis_Range coordinate_range) const { inline double Time_Axis::Private::tick_step(const Attached auto* object, Axis_Range coordinate_range) const {
using Object = std::remove_cv_t<std::remove_pointer_t<decltype(object)>>; using Object = std::remove_cv_t<std::remove_pointer_t<decltype(object)>>;
const auto& private_data = static_cast<const typename Object::Private&>(*this); const auto& private_data = static_cast<const typename Object::Private&>(*this);
const auto& time_state = static_cast<const State&>(*private_data.state.current); const auto& time_state = static_cast<const Prop&>(*private_data.current);
const auto& axis_state = static_cast<const Abs_Axis::State&>(*private_data.state.current); const auto& axis_state = static_cast<const Abs_Axis::Prop&>(*private_data.current);
const double label_width = std::max(48.0, time_state.estimated_label_width_px); const double label_width = std::max(48.0, time_state.estimated_label_width_px);
const double label_count = std::max(1.0, axis_state.pixel_length / (label_width + std::max(0.0, time_state.tick_label_spacing_px))); const double label_count = std::max(1.0, axis_state.pixel_length / (label_width + std::max(0.0, time_state.tick_label_spacing_px)));
return std::max(1.0, std::ceil(coordinate_range.size() / label_count)); return std::max(1.0, std::ceil(coordinate_range.size() / label_count));
@@ -51,11 +52,12 @@ inline std::string Time_Axis::Private::tick_label(const Attached auto* object, d
using Object = std::remove_cv_t<std::remove_pointer_t<decltype(object)>>; using Object = std::remove_cv_t<std::remove_pointer_t<decltype(object)>>;
const auto& private_data = static_cast<const typename Object::Private&>(*this); const auto& private_data = static_cast<const typename Object::Private&>(*this);
const auto& state = static_cast<const State&>(*private_data.state.current); const auto& state = static_cast<const State&>(*private_data.state.current);
const auto& prop = static_cast<const Prop&>(*private_data.current);
const int target = static_cast<int>(std::llround(tick)); const int target = static_cast<int>(std::llround(tick));
const auto current = std::find_if(state.samples.begin(), state.samples.end(), [target](const auto& sample) { const auto current = std::find_if(state.samples.begin(), state.samples.end(), [target](const auto& sample) {
return sample.first == target; return sample.first == target;
}); });
return current == state.samples.end() ? std::string{} : formatted_time(current->second, state.format); return current == state.samples.end() ? std::string{} : formatted_time(current->second, prop.format);
} }
template <Axis_Object Object> template <Axis_Object Object>
const Time_Axis::Private::Time_Dispatch& Time_Axis::Private::time_dispatch_for() { const Time_Axis::Private::Time_Dispatch& Time_Axis::Private::time_dispatch_for() {
@@ -68,12 +70,13 @@ const Time_Axis::Private::Time_Dispatch& Time_Axis::Private::time_dispatch_for()
}, },
[](Root* root, Time_Of_Day time) { [](Root* root, Time_Of_Day time) {
auto* object = static_cast<Object*>(root); auto* object = static_cast<Object*>(root);
const auto visible_count = object->template read_prop<Time_Axis::Base_Tag>().visible_count;
int tick{}; int tick{};
object->template update_state<&State::next_tick, &State::samples>([&](State_Access<typename Object::State> states) { object->template update_state<&State::next_tick, &State::samples>([&](State_Access<typename Object::State> states) {
auto& state = states.template get<Time_Axis_State_Tag>(); auto& state = states.template get<Time_Axis::Base_Tag>();
tick = state.next_tick++; tick = state.next_tick++;
state.samples.emplace_back(tick, time); state.samples.emplace_back(tick, time);
const auto limit = static_cast<std::size_t>(std::max(512, std::max(2, state.visible_count) * 4)); const auto limit = static_cast<std::size_t>(std::max(512, std::max(2, visible_count) * 4));
while (state.samples.size() > limit) state.samples.pop_front(); while (state.samples.size() > limit) state.samples.pop_front();
}); });
return tick; return tick;
+2 -1
View File
@@ -1,2 +1,3 @@
#include "Afterglow.hpp" #include "Afterglow.hpp"
namespace aethera::render_2d { bool Afterglow::State::operator==(const State&) const = default; void Afterglow::append_spectrum(std::span<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append(this, values); } void Afterglow::append_spectrum(std::pmr::vector<Plot_Value>&& values) { append_spectrum(std::span<const Plot_Value>(values.data(), values.size())); } std::size_t Afterglow::history_count() const { return static_cast<const Private&>(*d).dispatch->history_count(this); } std::size_t Afterglow::latest_spectrum_point_count() const { return static_cast<const Private&>(*d).dispatch->latest_count(this); } std::size_t Afterglow::rendered_cell_count() const { return static_cast<const Private&>(*d).dispatch->rendered_count(this); } } namespace aethera::render_2d { bool Afterglow::Prop::operator==(const Prop&) const = default;
bool Afterglow::State::operator==(const State&) const = default; void Afterglow::append_spectrum(std::span<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append(this, values); } void Afterglow::append_spectrum(std::pmr::vector<Plot_Value>&& values) { append_spectrum(std::span<const Plot_Value>(values.data(), values.size())); } std::size_t Afterglow::history_count() const { return static_cast<const Private&>(*d).dispatch->history_count(this); } std::size_t Afterglow::latest_spectrum_point_count() const { return static_cast<const Private&>(*d).dispatch->latest_count(this); } std::size_t Afterglow::rendered_cell_count() const { return static_cast<const Private&>(*d).dispatch->rendered_count(this); } }
+8 -4
View File
@@ -9,11 +9,9 @@
#include <span> #include <span>
#include <vector> #include <vector>
namespace aethera::render_2d { namespace aethera::render_2d {
struct Afterglow_State_Tag {}; struct Afterglow : Def<Afterglow, Renderable, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
struct Afterglow : Def<Afterglow, Renderable, State_Type<Afterglow_State_Tag>, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
using Scene_Object = Impl<Render_Scene_2D>; using Frequency_Object = Impl<Frequency_Axis>; using Power_Object = Impl<Numeric_Axis>; using Scene_Object = Impl<Render_Scene_2D>; using Frequency_Object = Impl<Frequency_Axis>; using Power_Object = Impl<Numeric_Axis>;
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {
struct State : Prev_State<Afterglow_State_Tag> {
Axis_Range frequency_range{0.0, 10.0}; /* 输入频谱覆盖的频率范围。 */ Axis_Range frequency_range{0.0, 10.0}; /* 输入频谱覆盖的频率范围。 */
Axis_Range power_range{0.0, 10.0}; /* 色块纵向覆盖的功率范围。 */ Axis_Range power_range{0.0, 10.0}; /* 色块纵向覆盖的功率范围。 */
std::size_t frequency_point_size{}; /* 栅格频率列数;零值使用最新频谱尺寸。 */ std::size_t frequency_point_size{}; /* 栅格频率列数;零值使用最新频谱尺寸。 */
@@ -24,6 +22,12 @@ struct Afterglow : Def<Afterglow, Renderable, State_Type<Afterglow_State_Tag>, T
Plot_Ratio attenuation_rate{0.2}; /* 每增加一帧历史的强度衰减比例,限制到 0..1。 */ Plot_Ratio attenuation_rate{0.2}; /* 每增加一帧历史的强度衰减比例,限制到 0..1。 */
Color_Map color_map{}; /* 强度到颜色的映射。 */ Color_Map color_map{}; /* 强度到颜色的映射。 */
std::vector<std::vector<Plot_Value>> spectra{}; /* 从旧到新的历史频谱唯一权威集合。 */ std::vector<std::vector<Plot_Value>> spectra{}; /* 从旧到新的历史频谱唯一权威集合。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
std::size_t history_count{}; /* 当前发布的历史频谱帧数。 */
std::size_t latest_spectrum_point_count{}; /* 最新频谱包含的点数。 */
std::size_t rendered_cell_count{}; /* 最近一次 Prepare 生成的色块数。 */
bool operator==(const State&) const; bool operator==(const State&) const;
}; };
struct Private; struct Private;
+15 -13
View File
@@ -30,34 +30,36 @@ struct Afterglow::Private : Prev_Private {
void bind_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value); void bind_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value);
template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for(); template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for();
/* CRTP 覆盖:构建累加、归一化和着色三阶段 Prepare 子图。 */ /* CRTP 覆盖:构建累加、归一化和着色三阶段 Prepare 子图。 */
template <Attached Object> [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const State& state); template <Attached Object> [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state);
/* CRTP 覆盖:构建消费色块矩阵的 Paint 子图。 */ /* CRTP 覆盖:构建消费色块矩阵的 Paint 子图。 */
template <Attached Object> [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const State& state); template <Attached Object> [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state);
/* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */
template <Attached Object> [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const State& state); template <Attached Object> [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state);
template <Attached Object> void prepare_frame(Object* object); template <Attached Object> void accumulate_partition(Object* object, Plot_Partition_Count index); void normalize_frame(); template <Attached Object> void color_partition(Object* object, Plot_Partition_Count index); template <Attached Object> void paint_frame(Object* object); template <Attached Object> void prepare_frame(Object* object); template <Attached Object> void accumulate_partition(Object* object, Plot_Partition_Count index); void normalize_frame(); template <Attached Object> void color_partition(Object* object, Plot_Partition_Count index); template <Attached Object> void paint_frame(Object* object);
/* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */ /* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */
template <typename Object, typename Owner, typename Member, typename State_Type> void after_state_set(Object* object, Member Owner::* member, State_Access<State_Type> states); template <typename Object, typename Owner, typename Member, typename Prop_Type> void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> states);
template <typename Object, typename Prop_Type, typename State_Type> void before_advance(Object* object, Prop_Type* pending_prop, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type> current_states);
}; };
template <typename Object> Afterglow::Builder<Object>::Builder(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), scene(scene_value), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {} template <typename Object> Afterglow::Builder<Object>::Builder(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), scene(scene_value), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {}
template <typename Object> template <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Afterglow::Builder<Object>::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast<typename Object::Private&>(*plot->d).bind_sources(scene, frequency_axis, power_axis); auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) { prepare.add(frequency_axis); prepare.add(power_axis); prepare.add(plot.get()); prepare.template add_state_dependency<Render_Scene_2D_State_Tag>(plot.get(), scene); prepare.template add_state_dependency<Abs_Axis_State_Tag>(plot.get(), frequency_axis); prepare.template add_state_dependency<Numeric_Axis_State_Tag>(plot.get(), frequency_axis); prepare.template add_state_dependency<Abs_Axis_State_Tag>(plot.get(), power_axis); prepare.template add_state_dependency<Numeric_Axis_State_Tag>(plot.get(), power_axis); paint.add(frequency_axis); paint.add(power_axis); paint.add(plot.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); } std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Afterglow::Builder<Object>::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast<typename Object::Private&>(*plot->d).bind_sources(scene, frequency_axis, power_axis); auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) { prepare.add(frequency_axis); prepare.add(power_axis); prepare.add(plot.get()); prepare.template add_prop_dependency<Render_Scene_2D::Base_Tag>(plot.get(), scene); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(plot.get(), frequency_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(plot.get(), frequency_axis); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(plot.get(), power_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(plot.get(), power_axis); paint.add(frequency_axis); paint.add(power_axis); paint.add(plot.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); }
template <typename Values> void Afterglow::append_spectrum(const Values& values) { append_spectrum(std::span<const Plot_Value>(std::data(values), std::size(values))); } template <typename Values> void Afterglow::append_spectrum(const Values& values) { append_spectrum(std::span<const Plot_Value>(std::data(values), std::size(values))); }
template <Attached Object> bool Afterglow::Private::should_rebuild_prepare_graph(Object*, const State& state) { const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const std::size_t columns = state.frequency_point_size ? std::min(state.frequency_point_size, available) : available; return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, std::max<std::size_t>(1, columns)); } template <Attached Object> bool Afterglow::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const std::size_t columns = state.frequency_point_size ? std::min(state.frequency_point_size, available) : available; return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, std::max<std::size_t>(1, columns)); }
template <Attached Object> template <Attached Object>
tf::Taskflow Afterglow::Private::build_prepare_graph(Object* object, const State& state) { const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const std::size_t columns = state.frequency_point_size ? std::min(state.frequency_point_size, available) : available; graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, std::max<std::size_t>(1, columns)); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("afterglow.prepare.frame"); auto normalize = graph.emplace([this] { normalize_frame(); }).name("afterglow.prepare.normalize"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto accumulate = graph.emplace([this, object, index] { accumulate_partition(object, index); }).name("afterglow.prepare.accumulate"); auto color = graph.emplace([this, object, index] { color_partition(object, index); }).name("afterglow.prepare.color"); begin.precede(accumulate); accumulate.precede(normalize); normalize.precede(color); } return graph; } tf::Taskflow Afterglow::Private::build_prepare_graph(Object* object, const Prop& state) { const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const std::size_t columns = state.frequency_point_size ? std::min(state.frequency_point_size, available) : available; graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, std::max<std::size_t>(1, columns)); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("afterglow.prepare.frame"); auto normalize = graph.emplace([this] { normalize_frame(); }).name("afterglow.prepare.normalize"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto accumulate = graph.emplace([this, object, index] { accumulate_partition(object, index); }).name("afterglow.prepare.accumulate"); auto color = graph.emplace([this, object, index] { color_partition(object, index); }).name("afterglow.prepare.color"); begin.precede(accumulate); accumulate.precede(normalize); normalize.precede(color); } return graph; }
template <Attached Object> tf::Taskflow Afterglow::Private::build_paint_graph(Object* object, const State&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("afterglow.paint.frame"); return graph; } template <Attached Object> tf::Taskflow Afterglow::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("afterglow.paint.frame"); return graph; }
template <Attached Object> template <Attached Object>
void Afterglow::Private::prepare_frame(Object* object) { const auto& state = object->template read_state<Afterglow_State_Tag>(); const auto& frequency_layout = frequency_axis->template read_state<Abs_Axis_State_Tag>(); const auto& power_layout = power_axis->template read_state<Abs_Axis_State_Tag>(); const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const int columns = static_cast<int>(state.frequency_point_size ? std::min(state.frequency_point_size, available) : available); const int rows = static_cast<int>(state.power_point_size ? state.power_point_size : std::max<Axis_Pixel_Length>(1.0, std::abs(power_layout.pixel_length))); prepared = {}; prepared.canvas = scene->template read_state<Render_Scene_2D_State_Tag>().viewport; prepared.layout = detail::raster_layout(frequency_axis, state.frequency_range, columns, power_axis, state.power_range, rows, frequency_layout.orientation, power_layout.orientation); if (prepared.canvas.empty() || !prepared.layout.valid()) return; const std::size_t cells = static_cast<std::size_t>(columns) * rows; prepared.intensity.assign(cells, 0.0); prepared.pixels.assign(cells, 0); prepared.valid = true; } void Afterglow::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop<Afterglow::Base_Tag>(); const auto& frequency_layout = frequency_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& power_layout = power_axis->template read_prop<Abs_Axis::Base_Tag>(); const std::size_t available = state.spectra.empty() ? 0 : state.spectra.back().size(); const int columns = static_cast<int>(state.frequency_point_size ? std::min(state.frequency_point_size, available) : available); const int rows = static_cast<int>(state.power_point_size ? state.power_point_size : std::max<Axis_Pixel_Length>(1.0, std::abs(power_layout.pixel_length))); prepared = {}; prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport; prepared.layout = detail::raster_layout(frequency_axis, state.frequency_range, columns, power_axis, state.power_range, rows, frequency_layout.orientation, power_layout.orientation); if (prepared.canvas.empty() || !prepared.layout.valid()) return; const std::size_t cells = static_cast<std::size_t>(columns) * rows; prepared.intensity.assign(cells, 0.0); prepared.pixels.assign(cells, 0); prepared.valid = true; }
template <Attached Object> template <Attached Object>
void Afterglow::Private::accumulate_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_state<Afterglow_State_Tag>(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; const auto [first, last] = detail::raster_partition_range(static_cast<std::size_t>(columns), index, graph_partition_count); Plot_Ratio attenuation{1.0}; const Plot_Ratio decay = 1.0 - std::clamp(state.attenuation_rate, 0.0, 1.0); for (auto spectrum = state.spectra.rbegin(); spectrum != state.spectra.rend() && attenuation >= 0.01; ++spectrum, attenuation *= decay) { const std::size_t count = std::min<std::size_t>(columns, spectrum->size()); for (std::size_t column = first; column < std::min(last, count); ++column) { const int row = std::clamp(static_cast<int>(detail::normalized_plot_value((*spectrum)[column], state.power_range) * (rows - 1)), 0, rows - 1); prepared.intensity[static_cast<std::size_t>(row) * columns + column] += attenuation; if (state.interpolate && row + 1 < rows) prepared.intensity[static_cast<std::size_t>(row + 1) * columns + column] += attenuation * 0.35; } } } void Afterglow::Private::accumulate_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop<Afterglow::Base_Tag>(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; const auto [first, last] = detail::raster_partition_range(static_cast<std::size_t>(columns), index, graph_partition_count); Plot_Ratio attenuation{1.0}; const Plot_Ratio decay = 1.0 - std::clamp(state.attenuation_rate, 0.0, 1.0); for (auto spectrum = state.spectra.rbegin(); spectrum != state.spectra.rend() && attenuation >= 0.01; ++spectrum, attenuation *= decay) { const std::size_t count = std::min<std::size_t>(columns, spectrum->size()); for (std::size_t column = first; column < std::min(last, count); ++column) { const int row = std::clamp(static_cast<int>(detail::normalized_plot_value((*spectrum)[column], state.power_range) * (rows - 1)), 0, rows - 1); prepared.intensity[static_cast<std::size_t>(row) * columns + column] += attenuation; if (state.interpolate && row + 1 < rows) prepared.intensity[static_cast<std::size_t>(row + 1) * columns + column] += attenuation * 0.35; } } }
inline void Afterglow::Private::normalize_frame() { if (prepared.valid && !prepared.intensity.empty()) prepared.maximum = std::max<Plot_Ratio>(1.0, *std::max_element(prepared.intensity.begin(), prepared.intensity.end())); } inline void Afterglow::Private::normalize_frame() { if (prepared.valid && !prepared.intensity.empty()) prepared.maximum = std::max<Plot_Ratio>(1.0, *std::max_element(prepared.intensity.begin(), prepared.intensity.end())); }
template <Attached Object> template <Attached Object>
void Afterglow::Private::color_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_state<Afterglow_State_Tag>(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; const auto [first, last] = detail::raster_partition_range(static_cast<std::size_t>(columns), index, graph_partition_count); for (std::size_t column = first; column < last; ++column) for (int row = 0; row < rows; ++row) { const std::size_t cell = static_cast<std::size_t>(row) * columns + column; prepared.pixels[prepared.layout.index(static_cast<int>(column), row)] = premultiply(state.color_map.sample(prepared.intensity[cell] / prepared.maximum)); } } void Afterglow::Private::color_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop<Afterglow::Base_Tag>(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const int rows = prepared.layout.first_horizontal ? prepared.layout.height : prepared.layout.width; const auto [first, last] = detail::raster_partition_range(static_cast<std::size_t>(columns), index, graph_partition_count); for (std::size_t column = first; column < last; ++column) for (int row = 0; row < rows; ++row) { const std::size_t cell = static_cast<std::size_t>(row) * columns + column; prepared.pixels[prepared.layout.index(static_cast<int>(column), row)] = premultiply(state.color_map.sample(prepared.intensity[cell] / prepared.maximum)); } }
template <Attached Object> void Afterglow::Private::paint_frame(Object* object) { auto& cache = object->template pending_buffer<Color_Cache>(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); detail::paint_raster(painter, prepared.layout, prepared.pixels, Image_Interpolation_Mode::bilinear); } template <Attached Object> void Afterglow::Private::paint_frame(Object* object) { auto& cache = object->template pending_buffer<Color_Cache>(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); detail::paint_raster(painter, prepared.layout, prepared.pixels, Image_Interpolation_Mode::bilinear); }
template <typename Object, typename Owner, typename Member, typename State_Type> void Afterglow::Private::after_state_set(Object* object, Member Owner::*, State_Access<State_Type>) { if constexpr (std::same_as<Owner, State>) object->template mark_dirty<Prepare_Data_Tag>(); } template <typename Object, typename Owner, typename Member, typename Prop_Type> void Afterglow::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type> void Afterglow::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { auto& state = pending_states.template get<Afterglow::Base_Tag>(); const auto& prop = static_cast<const Prop&>(*current_prop); state.history_count = prop.spectra.size(); state.latest_spectrum_point_count = prop.spectra.empty() ? 0 : prop.spectra.back().size(); state.rendered_cell_count = prepared.valid ? prepared.pixels.size() : 0; }
template <Attached Object> template <Attached Object>
const Afterglow::Private::Dispatch& Afterglow::Private::dispatch_for() { static const Dispatch value{[](Root* root, std::span<const Plot_Value> values) { auto* object = static_cast<Object*>(root); object->template update_state<&State::spectra>([values](State_Access<typename Object::State> states) { auto& state = states.template get<Afterglow_State_Tag>(); state.spectra.emplace_back(values.begin(), values.end()); constexpr std::size_t history_limit = 64; while (state.spectra.size() > history_limit) state.spectra.erase(state.spectra.begin()); }); }, [](const Root* root) { return static_cast<const Object*>(root)->template read_state<Afterglow_State_Tag>().spectra.size(); }, [](const Root* root) { const auto& spectra = static_cast<const Object*>(root)->template read_state<Afterglow_State_Tag>().spectra; return spectra.empty() ? 0 : spectra.back().size(); }, [](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); return data.prepared.valid ? data.prepared.pixels.size() : 0; }}; return value; } const Afterglow::Private::Dispatch& Afterglow::Private::dispatch_for() { static const Dispatch value{[](Root* root, std::span<const Plot_Value> values) { auto* object = static_cast<Object*>(root); object->template update_prop<&Prop::spectra>([values](Prop_Access<typename Object::Prop> props) { auto& state = props.template get<Afterglow::Base_Tag>(); state.spectra.emplace_back(values.begin(), values.end()); constexpr std::size_t history_limit = 64; while (state.spectra.size() > history_limit) state.spectra.erase(state.spectra.begin()); }); }, [](const Root* root) { return static_cast<const Object*>(root)->template read_prop<Afterglow::Base_Tag>().spectra.size(); }, [](const Root* root) { const auto& spectra = static_cast<const Object*>(root)->template read_prop<Afterglow::Base_Tag>().spectra; return spectra.empty() ? 0 : spectra.back().size(); }, [](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); return data.prepared.valid ? data.prepared.pixels.size() : 0; }}; return value; }
template <Attached Object> void Afterglow::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for<Object>(); } template <Attached Object> void Afterglow::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for<Object>(); }
inline void Afterglow::Private::bind_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) { scene = scene_value; frequency_axis = frequency_axis_value; power_axis = power_axis_value; } inline void Afterglow::Private::bind_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) { scene = scene_value; frequency_axis = frequency_axis_value; power_axis = power_axis_value; }
} }
@@ -1,2 +1,3 @@
#include "Constellation_Diagram.hpp" #include "Constellation_Diagram.hpp"
namespace aethera::render_2d { bool Constellation_Point::operator==(const Constellation_Point&) const = default; bool Constellation_Diagram::State::operator==(const State&) const = default; void Constellation_Diagram::append_point(Point_F point) { static_cast<Private&>(*d).dispatch->append(this, point); } std::size_t Constellation_Diagram::point_count() const { return static_cast<const Private&>(*d).dispatch->count(this); } void Constellation_Diagram::fit_square_to_axes() { static_cast<Private&>(*d).dispatch->fit(this); } } namespace aethera::render_2d { bool Constellation_Point::operator==(const Constellation_Point&) const = default; bool Constellation_Diagram::Prop::operator==(const Prop&) const = default;
bool Constellation_Diagram::State::operator==(const State&) const = default; void Constellation_Diagram::append_point(Point_F point) { static_cast<Private&>(*d).dispatch->append(this, point); } std::size_t Constellation_Diagram::point_count() const { return static_cast<const Private&>(*d).dispatch->count(this); } void Constellation_Diagram::fit_square_to_axes() { static_cast<Private&>(*d).dispatch->fit(this); } }
@@ -8,12 +8,10 @@
#include <vector> #include <vector>
namespace aethera::render_2d { namespace aethera::render_2d {
enum class Constellation_Diagram_Type : std::uint8_t { psk4 = 4, psk8 = 8, psk16 = 16 }; enum class Constellation_Diagram_Type : std::uint8_t { psk4 = 4, psk8 = 8, psk16 = 16 };
struct Constellation_Diagram_State_Tag {};
struct Constellation_Point { Point_F point{}; Plot_Duration_Milliseconds submitted_at_ms{}; bool operator==(const Constellation_Point&) const; }; struct Constellation_Point { Point_F point{}; Plot_Duration_Milliseconds submitted_at_ms{}; bool operator==(const Constellation_Point&) const; };
struct Constellation_Diagram : Def<Constellation_Diagram, Renderable, State_Type<Constellation_Diagram_State_Tag>, Tagged_Buffer<Color_Cache, Blend2D_Cache>> { struct Constellation_Diagram : Def<Constellation_Diagram, Renderable, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
using Scene_Object = Impl<Render_Scene_2D>; using Axis_Object = Impl<Numeric_Axis>; using Scene_Object = Impl<Render_Scene_2D>; using Axis_Object = Impl<Numeric_Axis>;
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {
struct State : Prev_State<Constellation_Diagram_State_Tag> {
Axis_Range i_range{0.0, 100.0}; /* 同相分量显示范围。 */ Axis_Range i_range{0.0, 100.0}; /* 同相分量显示范围。 */
Axis_Range q_range{0.0, 100.0}; /* 正交分量显示范围。 */ Axis_Range q_range{0.0, 100.0}; /* 正交分量显示范围。 */
Color point_color{Color::red_color()}; /* 接收点颜色。 */ Color point_color{Color::red_color()}; /* 接收点颜色。 */
@@ -22,6 +20,10 @@ struct Constellation_Diagram : Def<Constellation_Diagram, Renderable, State_Type
Constellation_Diagram_Type type{Constellation_Diagram_Type::psk8}; /* 理想 PSK 锚点数量。 */ Constellation_Diagram_Type type{Constellation_Diagram_Type::psk8}; /* 理想 PSK 锚点数量。 */
Plot_Ratio phase_offset_radians{}; /* 理想锚点相位偏移,单位为弧度。 */ Plot_Ratio phase_offset_radians{}; /* 理想锚点相位偏移,单位为弧度。 */
std::vector<Constellation_Point> points{}; /* 已提交且尚未过期的点。 */ std::vector<Constellation_Point> points{}; /* 已提交且尚未过期的点。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
std::size_t point_count{}; /* 当前发布且尚未过期的点数。 */
bool operator==(const State&) const; bool operator==(const State&) const;
}; };
struct Private; struct Private;
@@ -31,19 +31,21 @@ struct Constellation_Diagram::Private : Prev_Private {
/* CRTP 覆盖:直接绘制已准备的星座点与锚点。 */ /* CRTP 覆盖:直接绘制已准备的星座点与锚点。 */
template <Attached Object> void paint(Object* object); template <Attached Object> void paint(Object* object);
/* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */ /* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */
template <typename Object, typename Owner, typename Member, typename State_Type> void after_state_set(Object* object, Member Owner::* member, State_Access<State_Type> states); template <typename Object, typename Owner, typename Member, typename Prop_Type> void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> states);
template <typename Object, typename Prop_Type, typename State_Type> void before_advance(Object* object, Prop_Type* pending_prop, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type> current_states);
[[nodiscard]] static Plot_Duration_Milliseconds now_ms(); [[nodiscard]] static Plot_Duration_Milliseconds now_ms();
}; };
template <typename Object> Constellation_Diagram::Builder<Object>::Builder(Scene_Object* scene_value, Axis_Object* i_axis_value, Axis_Object* q_axis_value) : Base(), scene(scene_value), i_axis(i_axis_value), q_axis(q_axis_value) {} template <typename Object> Constellation_Diagram::Builder<Object>::Builder(Scene_Object* scene_value, Axis_Object* i_axis_value, Axis_Object* q_axis_value) : Base(), scene(scene_value), i_axis(i_axis_value), q_axis(q_axis_value) {}
template <typename Object> template <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Constellation_Diagram::Builder<Object>::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast<typename Object::Private&>(*plot->d).bind_sources(scene, i_axis, q_axis); auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) { prepare.add(i_axis); prepare.add(q_axis); prepare.add(plot.get()); prepare.template add_state_dependency<Render_Scene_2D_State_Tag>(plot.get(), scene); prepare.template add_state_dependency<Abs_Axis_State_Tag>(plot.get(), i_axis); prepare.template add_state_dependency<Numeric_Axis_State_Tag>(plot.get(), i_axis); prepare.template add_state_dependency<Abs_Axis_State_Tag>(plot.get(), q_axis); prepare.template add_state_dependency<Numeric_Axis_State_Tag>(plot.get(), q_axis); paint.add(i_axis); paint.add(q_axis); paint.add(plot.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); } std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Constellation_Diagram::Builder<Object>::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast<typename Object::Private&>(*plot->d).bind_sources(scene, i_axis, q_axis); auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) { prepare.add(i_axis); prepare.add(q_axis); prepare.add(plot.get()); prepare.template add_prop_dependency<Render_Scene_2D::Base_Tag>(plot.get(), scene); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(plot.get(), i_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(plot.get(), i_axis); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(plot.get(), q_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(plot.get(), q_axis); paint.add(i_axis); paint.add(q_axis); paint.add(plot.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); }
inline Plot_Duration_Milliseconds Constellation_Diagram::Private::now_ms() { return static_cast<Plot_Duration_Milliseconds>(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count()); } inline Plot_Duration_Milliseconds Constellation_Diagram::Private::now_ms() { return static_cast<Plot_Duration_Milliseconds>(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count()); }
template <Attached Object> template <Attached Object>
void Constellation_Diagram::Private::prepare_data(Object* object) { const auto& state = object->template read_state<Constellation_Diagram_State_Tag>(); const auto& i_layout = i_axis->template read_state<Abs_Axis_State_Tag>(); const auto& q_layout = q_axis->template read_state<Abs_Axis_State_Tag>(); prepared = {}; prepared.canvas = scene->template read_state<Render_Scene_2D_State_Tag>().viewport; if (prepared.canvas.empty() || i_layout.orientation == q_layout.orientation) return; const auto current = now_ms(); for (const auto& value : state.points) if (current - value.submitted_at_ms <= state.point_lifetime_ms) prepared.points.push_back(detail::map_plot_point(i_axis, value.point.x, q_axis, value.point.y, i_layout.orientation)); const int count = static_cast<int>(state.type); const Plot_Coordinate center_i = state.i_range.center(); const Plot_Coordinate center_q = state.q_range.center(); const Plot_Coordinate radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; for (int index = 0; index < count; ++index) { const Plot_Ratio angle = state.phase_offset_radians + 2.0 * std::numbers::pi * index / count; prepared.anchors.push_back(detail::map_plot_point(i_axis, center_i + std::cos(angle) * radius, q_axis, center_q + std::sin(angle) * radius, i_layout.orientation)); } prepared.valid = true; object->template mark_dirty<Paint_Tag>(); } void Constellation_Diagram::Private::prepare_data(Object* object) { const auto& state = object->template read_prop<Constellation_Diagram::Base_Tag>(); const auto& i_layout = i_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& q_layout = q_axis->template read_prop<Abs_Axis::Base_Tag>(); prepared = {}; prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport; if (prepared.canvas.empty() || i_layout.orientation == q_layout.orientation) return; const auto current = now_ms(); for (const auto& value : state.points) if (current - value.submitted_at_ms <= state.point_lifetime_ms) prepared.points.push_back(detail::map_plot_point(i_axis, value.point.x, q_axis, value.point.y, i_layout.orientation)); const int count = static_cast<int>(state.type); const Plot_Coordinate center_i = state.i_range.center(); const Plot_Coordinate center_q = state.q_range.center(); const Plot_Coordinate radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; for (int index = 0; index < count; ++index) { const Plot_Ratio angle = state.phase_offset_radians + 2.0 * std::numbers::pi * index / count; prepared.anchors.push_back(detail::map_plot_point(i_axis, center_i + std::cos(angle) * radius, q_axis, center_q + std::sin(angle) * radius, i_layout.orientation)); } prepared.valid = true; object->template mark_dirty<Paint_Tag>(); }
template <Attached Object> void Constellation_Diagram::Private::paint(Object* object) { const auto& state = object->template read_state<Constellation_Diagram_State_Tag>(); auto& cache = object->template pending_buffer<Color_Cache>(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& anchor : prepared.anchors) painter.circle(anchor, 4.0, Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::solid}); for (const auto& point : prepared.points) painter.circle(point, 2.0, Pen{state.point_color}, Brush{state.point_color, Brush_Style::solid}); } template <Attached Object> void Constellation_Diagram::Private::paint(Object* object) { const auto& state = object->template read_prop<Constellation_Diagram::Base_Tag>(); auto& cache = object->template pending_buffer<Color_Cache>(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& anchor : prepared.anchors) painter.circle(anchor, 4.0, Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::solid}); for (const auto& point : prepared.points) painter.circle(point, 2.0, Pen{state.point_color}, Brush{state.point_color, Brush_Style::solid}); }
template <typename Object, typename Owner, typename Member, typename State_Type> void Constellation_Diagram::Private::after_state_set(Object* object, Member Owner::*, State_Access<State_Type>) { if constexpr (std::same_as<Owner, State>) object->template mark_dirty<Prepare_Data_Tag>(); } template <typename Object, typename Owner, typename Member, typename Prop_Type> void Constellation_Diagram::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type> void Constellation_Diagram::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { pending_states.template get<Constellation_Diagram::Base_Tag>().point_count = static_cast<const Prop&>(*current_prop).points.size(); }
template <Attached Object> template <Attached Object>
const Constellation_Diagram::Private::Dispatch& Constellation_Diagram::Private::dispatch_for() { static const Dispatch value{[](Root* root, Point_F point) { auto* object = static_cast<Object*>(root); const auto submitted = Private::now_ms(); object->template update_state<&State::points>([=](State_Access<typename Object::State> states) { auto& state = states.template get<Constellation_Diagram_State_Tag>(); state.points.erase(std::remove_if(state.points.begin(), state.points.end(), [=](const auto& value) { return submitted - value.submitted_at_ms > state.point_lifetime_ms; }), state.points.end()); state.points.push_back({point, submitted}); }); }, [](const Root* root) { return static_cast<const Object*>(root)->template read_state<Constellation_Diagram_State_Tag>().points.size(); }, [](Root* root) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); const auto& state = object->template read_state<Constellation_Diagram_State_Tag>(); const Plot_Coordinate size = std::max(state.i_range.size(), state.q_range.size()); const Plot_Coordinate i_center = state.i_range.center(); const Plot_Coordinate q_center = state.q_range.center(); data.i_axis->template update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{i_center - size * 0.5, i_center + size * 0.5}); data.q_axis->template update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{q_center - size * 0.5, q_center + size * 0.5}); }}; return value; } const Constellation_Diagram::Private::Dispatch& Constellation_Diagram::Private::dispatch_for() { static const Dispatch value{[](Root* root, Point_F point) { auto* object = static_cast<Object*>(root); const auto submitted = Private::now_ms(); object->template update_prop<&Prop::points>([=](Prop_Access<typename Object::Prop> props) { auto& state = props.template get<Constellation_Diagram::Base_Tag>(); state.points.erase(std::remove_if(state.points.begin(), state.points.end(), [=](const auto& value) { return submitted - value.submitted_at_ms > state.point_lifetime_ms; }), state.points.end()); state.points.push_back({point, submitted}); }); }, [](const Root* root) { return static_cast<const Object*>(root)->template read_prop<Constellation_Diagram::Base_Tag>().points.size(); }, [](Root* root) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); const auto& state = object->template read_prop<Constellation_Diagram::Base_Tag>(); const Plot_Coordinate size = std::max(state.i_range.size(), state.q_range.size()); const Plot_Coordinate i_center = state.i_range.center(); const Plot_Coordinate q_center = state.q_range.center(); data.i_axis->template set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{i_center - size * 0.5, i_center + size * 0.5}); data.q_axis->template set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{q_center - size * 0.5, q_center + size * 0.5}); }}; return value; }
template <Attached Object> void Constellation_Diagram::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for<Object>(); } template <Attached Object> void Constellation_Diagram::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for<Object>(); }
inline void Constellation_Diagram::Private::bind_sources(Scene_Object* scene_value, Axis_Object* i_axis_value, Axis_Object* q_axis_value) { scene = scene_value; i_axis = i_axis_value; q_axis = q_axis_value; } inline void Constellation_Diagram::Private::bind_sources(Scene_Object* scene_value, Axis_Object* i_axis_value, Axis_Object* q_axis_value) { scene = scene_value; i_axis = i_axis_value; q_axis = q_axis_value; }
} }
@@ -1,6 +1,7 @@
#include "Frequency_Trace.hpp" #include "Frequency_Trace.hpp"
namespace aethera::render_2d { namespace aethera::render_2d {
bool Frequency_Trace_Sample::operator==(const Frequency_Trace_Sample&) const = default; bool Frequency_Trace_Sample::operator==(const Frequency_Trace_Sample&) const = default;
bool Frequency_Trace::Prop::operator==(const Prop&) const = default;
bool Frequency_Trace::State::operator==(const State&) const = default; bool Frequency_Trace::State::operator==(const State&) const = default;
void Frequency_Trace::append_sample(Plot_Time_Tick tick, Plot_Value value) { static_cast<Private&>(*d).dispatch->append(this, tick, value); } void Frequency_Trace::append_sample(Plot_Time_Tick tick, Plot_Value value) { static_cast<Private&>(*d).dispatch->append(this, tick, value); }
void Frequency_Trace::append_sample(Time_Of_Day time, Plot_Value value) { static_cast<Private&>(*d).dispatch->append_time(this, time, value); } void Frequency_Trace::append_sample(Time_Of_Day time, Plot_Value value) { static_cast<Private&>(*d).dispatch->append_time(this, time, value); }
@@ -7,22 +7,25 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
namespace aethera::render_2d { namespace aethera::render_2d {
struct Frequency_Trace_State_Tag {};
struct Frequency_Trace_Sample { struct Frequency_Trace_Sample {
Plot_Time_Tick tick{}; /* 时间轴上的单调样本序号。 */ Plot_Time_Tick tick{}; /* 时间轴上的单调样本序号。 */
Plot_Value value{}; /* 该时间点对应的频率值。 */ Plot_Value value{}; /* 该时间点对应的频率值。 */
bool operator==(const Frequency_Trace_Sample&) const; bool operator==(const Frequency_Trace_Sample&) const;
}; };
struct Frequency_Trace : Def<Frequency_Trace, Renderable, State_Type<Frequency_Trace_State_Tag>, Tagged_Buffer<Color_Cache, Blend2D_Cache>> { struct Frequency_Trace : Def<Frequency_Trace, Renderable, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
using Scene_Object = Impl<Render_Scene_2D>; using Scene_Object = Impl<Render_Scene_2D>;
using Time_Object = Impl<Time_Axis>; using Time_Object = Impl<Time_Axis>;
using Value_Object = Impl<Numeric_Axis>; using Value_Object = Impl<Numeric_Axis>;
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {
struct State : Prev_State<Frequency_Trace_State_Tag> {
Pen pen{Color::yellow()}; /* 频率轨迹折线样式。 */ Pen pen{Color::yellow()}; /* 频率轨迹折线样式。 */
Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */
Plot_Partition_Count partition_count{1}; /* fixed 模式的分块数量。 */ Plot_Partition_Count partition_count{1}; /* fixed 模式的分块数量。 */
std::vector<Frequency_Trace_Sample> samples{}; /* 已提交轨迹样本的唯一权威集合。 */ std::vector<Frequency_Trace_Sample> samples{}; /* 已提交轨迹样本的唯一权威集合。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
std::size_t sample_count{}; /* 当前发布的轨迹样本数。 */
std::size_t rendered_point_count{}; /* 最近一次 Prepare 生成的折线点数。 */
bool operator==(const State&) const; bool operator==(const State&) const;
}; };
struct Private; struct Private;
@@ -29,16 +29,17 @@ struct Frequency_Trace::Private : Prev_Private {
void bind_sources(Scene_Object* scene_value, Time_Object* time_axis_value, Value_Object* value_axis_value); void bind_sources(Scene_Object* scene_value, Time_Object* time_axis_value, Value_Object* value_axis_value);
template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for(); template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for();
/* CRTP 覆盖:按当前样本规模构建分块 Prepare 子图。 */ /* CRTP 覆盖:按当前样本规模构建分块 Prepare 子图。 */
template <Attached Object> [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const State& state); template <Attached Object> [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state);
/* CRTP 覆盖:构建消费已准备曲线的 Paint 子图。 */ /* CRTP 覆盖:构建消费已准备曲线的 Paint 子图。 */
template <Attached Object> [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const State& state); template <Attached Object> [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state);
/* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */
template <Attached Object> [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const State& state); template <Attached Object> [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state);
template <Attached Object> void prepare_frame(Object* object, Plot_Partition_Count partition_count); template <Attached Object> void prepare_frame(Object* object, Plot_Partition_Count partition_count);
template <Attached Object> void prepare_partition(Object* object, Plot_Partition_Count partition_index); template <Attached Object> void prepare_partition(Object* object, Plot_Partition_Count partition_index);
template <Attached Object> void paint_frame(Object* object); template <Attached Object> void paint_frame(Object* object);
/* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */ /* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */
template <typename Object, typename Owner, typename Member, typename State_Type> void after_state_set(Object* object, Member Owner::* member, State_Access<State_Type> pending_states); template <typename Object, typename Owner, typename Member, typename Prop_Type> void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> pending_states);
template <typename Object, typename Prop_Type, typename State_Type> void before_advance(Object* object, Prop_Type* pending_prop, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type> current_states);
}; };
template <typename Object> template <typename Object>
Frequency_Trace::Builder<Object>::Builder(Scene_Object* scene_value, Time_Object* time_axis_value, Value_Object* value_axis_value) : Base(), scene(scene_value), time_axis(time_axis_value), value_axis(value_axis_value) {} Frequency_Trace::Builder<Object>::Builder(Scene_Object* scene_value, Time_Object* time_axis_value, Value_Object* value_axis_value) : Base(), scene(scene_value), time_axis(time_axis_value), value_axis(value_axis_value) {}
@@ -48,50 +49,52 @@ std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Frequency_Trace::
static_cast<typename Object::Private&>(*trace->d).bind_sources(scene, time_axis, value_axis); static_cast<typename Object::Private&>(*trace->d).bind_sources(scene, time_axis, value_axis);
auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) { auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) {
prepare.add(time_axis); prepare.add(value_axis); prepare.add(trace.get()); prepare.add(time_axis); prepare.add(value_axis); prepare.add(trace.get());
prepare.template add_state_dependency<Render_Scene_2D_State_Tag>(trace.get(), scene); prepare.template add_prop_dependency<Render_Scene_2D::Base_Tag>(trace.get(), scene);
prepare.template add_state_dependency<Abs_Axis_State_Tag>(trace.get(), time_axis); prepare.template add_state_dependency<Time_Axis_State_Tag>(trace.get(), time_axis); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(trace.get(), time_axis); prepare.template add_prop_dependency<Time_Axis::Base_Tag>(trace.get(), time_axis);
prepare.template add_state_dependency<Abs_Axis_State_Tag>(trace.get(), value_axis); prepare.template add_state_dependency<Numeric_Axis_State_Tag>(trace.get(), value_axis); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(trace.get(), value_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(trace.get(), value_axis);
paint.add(time_axis); paint.add(value_axis); paint.add(trace.get()); paint.add(time_axis); paint.add(value_axis); paint.add(trace.get());
}); });
if (!graph_result) return std::unexpected(graph_result.error()); return std::move(trace); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(trace);
} }
template <Attached Object> template <Attached Object>
bool Frequency_Trace::Private::should_rebuild_prepare_graph(Object* object, const State& state) { return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, state.samples.size()); } bool Frequency_Trace::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) { return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, state.samples.size()); }
template <Attached Object> template <Attached Object>
tf::Taskflow Frequency_Trace::Private::build_prepare_graph(Object* object, const State& state) { tf::Taskflow Frequency_Trace::Private::build_prepare_graph(Object* object, const Prop& state) {
graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, state.samples.size()); tf::Taskflow graph; graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, state.samples.size()); tf::Taskflow graph;
auto begin = graph.emplace([this, object] { prepare_frame(object, graph_partition_count); }).name("frequency_trace.prepare.frame"); auto begin = graph.emplace([this, object] { prepare_frame(object, graph_partition_count); }).name("frequency_trace.prepare.frame");
for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("frequency_trace.prepare.partition"); begin.precede(task); } for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("frequency_trace.prepare.partition"); begin.precede(task); }
return graph; return graph;
} }
template <Attached Object> template <Attached Object>
tf::Taskflow Frequency_Trace::Private::build_paint_graph(Object* object, const State&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("frequency_trace.paint.frame"); return graph; } tf::Taskflow Frequency_Trace::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("frequency_trace.paint.frame"); return graph; }
template <Attached Object> template <Attached Object>
void Frequency_Trace::Private::prepare_frame(Object* object, Plot_Partition_Count partition_count) { void Frequency_Trace::Private::prepare_frame(Object* object, Plot_Partition_Count partition_count) {
const auto& state = object->template read_state<Frequency_Trace_State_Tag>(); const auto& time_layout = time_axis->template read_state<Abs_Axis_State_Tag>(); const auto& value_layout = value_axis->template read_state<Abs_Axis_State_Tag>(); const auto& state = object->template read_prop<Frequency_Trace::Base_Tag>(); const auto& time_layout = time_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& value_layout = value_axis->template read_prop<Abs_Axis::Base_Tag>();
prepared = {}; prepared.partitions.resize(partition_count); prepared.canvas = scene->template read_state<Render_Scene_2D_State_Tag>().viewport; prepared = {}; prepared.partitions.resize(partition_count); prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport;
if (prepared.canvas.empty() || time_layout.orientation == value_layout.orientation || state.samples.empty()) return; if (prepared.canvas.empty() || time_layout.orientation == value_layout.orientation || state.samples.empty()) return;
prepared.values.reserve(state.samples.size()); for (const auto& sample : state.samples) prepared.values.push_back(sample.value); prepared.values.reserve(state.samples.size()); for (const auto& sample : state.samples) prepared.values.push_back(sample.value);
prepared.domain = {static_cast<Plot_Coordinate>(state.samples.front().tick), static_cast<Plot_Coordinate>(state.samples.back().tick)}; prepared.valid = true; prepared.domain = {static_cast<Plot_Coordinate>(state.samples.front().tick), static_cast<Plot_Coordinate>(state.samples.back().tick)}; prepared.valid = true;
} }
template <Attached Object> template <Attached Object>
void Frequency_Trace::Private::prepare_partition(Object* object, Plot_Partition_Count partition_index) { void Frequency_Trace::Private::prepare_partition(Object* object, Plot_Partition_Count partition_index) {
if (!prepared.valid) return; const auto& state = object->template read_state<Frequency_Trace_State_Tag>(); const auto& time_layout = time_axis->template read_state<Abs_Axis_State_Tag>(); const auto& value_layout = value_axis->template read_state<Abs_Axis_State_Tag>(); const auto& value_state = value_axis->template read_state<Numeric_Axis_State_Tag>(); if (!prepared.valid) return; const auto& state = object->template read_prop<Frequency_Trace::Base_Tag>(); const auto& time_layout = time_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& value_layout = value_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& value_state = value_axis->template read_prop<Numeric_Axis::Base_Tag>();
const auto range = detail::curve_partition_range(prepared.values.size(), partition_index, prepared.partitions.size(), prepared.domain); const auto range = detail::curve_partition_range(prepared.values.size(), partition_index, prepared.partitions.size(), prepared.domain);
prepared.partitions[partition_index] = detail::prepare_curve(std::span<const Plot_Value>(prepared.values).subspan(range.first_sample, range.sample_count), range.domain, Line_Interpolation_Mode::linear_value, true, time_axis->coordinate_range(), value_state.coordinate_range, time_axis, value_axis, time_layout.orientation, value_layout.orientation); prepared.partitions[partition_index] = detail::prepare_curve(std::span<const Plot_Value>(prepared.values).subspan(range.first_sample, range.sample_count), range.domain, Line_Interpolation_Mode::linear_value, true, time_axis->coordinate_range(), value_state.coordinate_range, time_axis, value_axis, time_layout.orientation, value_layout.orientation);
} }
template <Attached Object> template <Attached Object>
void Frequency_Trace::Private::paint_frame(Object* object) { void Frequency_Trace::Private::paint_frame(Object* object) {
const auto& state = object->template read_state<Frequency_Trace_State_Tag>(); auto& cache = object->template pending_buffer<Color_Cache>(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& curve : prepared.partitions) detail::paint_curve(painter, curve, state.pen); const auto& state = object->template read_prop<Frequency_Trace::Base_Tag>(); auto& cache = object->template pending_buffer<Color_Cache>(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& curve : prepared.partitions) detail::paint_curve(painter, curve, state.pen);
} }
template <typename Object, typename Owner, typename Member, typename State_Type> template <typename Object, typename Owner, typename Member, typename Prop_Type>
void Frequency_Trace::Private::after_state_set(Object* object, Member Owner::*, State_Access<State_Type>) { if constexpr (std::same_as<Owner, State>) object->template mark_dirty<Prepare_Data_Tag>(); } void Frequency_Trace::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type>
void Frequency_Trace::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { auto& state = pending_states.template get<Frequency_Trace::Base_Tag>(); const auto& prop = static_cast<const Prop&>(*current_prop); state.sample_count = prop.samples.size(); state.rendered_point_count = 0; for (const auto& partition : prepared.partitions) state.rendered_point_count += partition.points.size(); }
template <Attached Object> template <Attached Object>
const Frequency_Trace::Private::Dispatch& Frequency_Trace::Private::dispatch_for() { const Frequency_Trace::Private::Dispatch& Frequency_Trace::Private::dispatch_for() {
static const Dispatch value{ static const Dispatch value{
[](Root* root, Plot_Time_Tick tick, Plot_Value sample_value) { auto* object = static_cast<Object*>(root); object->template update_state<&State::samples>([=](State_Access<typename Object::State> states) { states.template get<Frequency_Trace_State_Tag>().samples.push_back({tick, sample_value}); }); }, [](Root* root, Plot_Time_Tick tick, Plot_Value sample_value) { auto* object = static_cast<Object*>(root); object->template update_prop<&Prop::samples>([=](Prop_Access<typename Object::Prop> props) { props.template get<Frequency_Trace::Base_Tag>().samples.push_back({tick, sample_value}); }); },
[](Root* root, Time_Of_Day time, Plot_Value sample_value) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); const Plot_Time_Tick tick = data.time_axis->append_time(time); object->template update_state<&State::samples>([=](State_Access<typename Object::State> states) { states.template get<Frequency_Trace_State_Tag>().samples.push_back({tick, sample_value}); }); }, [](Root* root, Time_Of_Day time, Plot_Value sample_value) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); const Plot_Time_Tick tick = data.time_axis->append_time(time); object->template update_prop<&Prop::samples>([=](Prop_Access<typename Object::Prop> props) { props.template get<Frequency_Trace::Base_Tag>().samples.push_back({tick, sample_value}); }); },
[](const Root* root) { return static_cast<const Object*>(root)->template read_state<Frequency_Trace_State_Tag>().samples.size(); }, [](const Root* root) { return static_cast<const Object*>(root)->template read_prop<Frequency_Trace::Base_Tag>().samples.size(); },
[](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); std::size_t result{}; for (const auto& curve : data.prepared.partitions) result += curve.points.size(); return result; } [](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); std::size_t result{}; for (const auto& curve : data.prepared.partitions) result += curve.points.size(); return result; }
}; return value; }; return value;
} }
@@ -1,5 +1,6 @@
#include "Selection_Rectangle_Overlay.hpp" #include "Selection_Rectangle_Overlay.hpp"
namespace aethera::render_2d { namespace aethera::render_2d {
bool Selection_Rectangle_Overlay::Prop::operator==(const Prop&) const = default;
bool Selection_Rectangle_Overlay::State::operator==(const State&) const = default; bool Selection_Rectangle_Overlay::State::operator==(const State&) const = default;
std::vector<Rect_F> Selection_Rectangle_Overlay::selected_regions() const { return static_cast<const Private&>(*d).dispatch->selected_regions(this); } std::vector<Rect_F> Selection_Rectangle_Overlay::selected_regions() const { return static_cast<const Private&>(*d).dispatch->selected_regions(this); }
void Selection_Rectangle_Overlay::clear_selected_regions() { static_cast<Private&>(*d).dispatch->clear_selected_regions(this); } void Selection_Rectangle_Overlay::clear_selected_regions() { static_cast<Private&>(*d).dispatch->clear_selected_regions(this); }
@@ -7,17 +7,19 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
namespace aethera::render_2d { namespace aethera::render_2d {
struct Selection_Rectangle_Overlay_State_Tag {}; struct Selection_Rectangle_Overlay : Def<Selection_Rectangle_Overlay, Renderable, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
struct Selection_Rectangle_Overlay : Def<Selection_Rectangle_Overlay, Renderable, State_Type<Selection_Rectangle_Overlay_State_Tag>, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
using Scene_Object = Impl<Render_Scene_2D>; using Scene_Object = Impl<Render_Scene_2D>;
using Axis_Object = Impl<Numeric_Axis>; using Axis_Object = Impl<Numeric_Axis>;
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {
struct State : Prev_State<Selection_Rectangle_Overlay_State_Tag> {
Font label_font{}; /* 选择范围标签使用的字体。 */ Font label_font{}; /* 选择范围标签使用的字体。 */
Pen label_pen{Color::white()}; /* 选择范围标签的文字样式。 */ Pen label_pen{Color::white()}; /* 选择范围标签的文字样式。 */
Brush selection_brush{Color{0, 0, 255, 50}, Brush_Style::solid}; /* 选择矩形内部填充。 */ Brush selection_brush{Color{0, 0, 255, 50}, Brush_Style::solid}; /* 选择矩形内部填充。 */
Pen selection_border_pen{Color::white(), 1.0, Line_Style::dash}; /* 选择矩形边框样式。 */ Pen selection_border_pen{Color::white(), 1.0, Line_Style::dash}; /* 选择矩形边框样式。 */
std::vector<Rect_F> selected_regions{}; /* 已完成选择的轴坐标矩形。 */ std::vector<Rect_F> selected_regions{}; /* 已完成选择的轴坐标矩形。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
std::size_t selected_region_count{}; /* 当前已完成选择的矩形数量。 */
bool operator==(const State&) const; bool operator==(const State&) const;
}; };
struct Private; struct Private;
@@ -26,7 +26,8 @@ struct Selection_Rectangle_Overlay::Private : Prev_Private {
/* CRTP 覆盖:处理拖拽并更新权威选择区域状态。 */ /* CRTP 覆盖:处理拖拽并更新权威选择区域状态。 */
template <Attached Object> void handle_event(Object* object, const Event& event); template <Attached Object> void handle_event(Object* object, const Event& event);
/* CRTP 覆盖:本类状态写入后只标记 Paint 数据失效。 */ /* CRTP 覆盖:本类状态写入后只标记 Paint 数据失效。 */
template <typename Object, typename Owner, typename Member, typename State_Type> void after_state_set(Object* object, Member Owner::* member, State_Access<State_Type> pending_states); template <typename Object, typename Owner, typename Member, typename Prop_Type> void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> pending_states);
template <typename Object, typename Prop_Type, typename State_Type> void before_advance(Object* object, Prop_Type* pending_prop, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type> current_states);
}; };
template <typename Object> template <typename Object>
Selection_Rectangle_Overlay::Builder<Object>::Builder(Scene_Object* scene_value, Axis_Object* horizontal_axis_value, Axis_Object* vertical_axis_value) : Base(), scene(scene_value), horizontal_axis(horizontal_axis_value), vertical_axis(vertical_axis_value) {} Selection_Rectangle_Overlay::Builder<Object>::Builder(Scene_Object* scene_value, Axis_Object* horizontal_axis_value, Axis_Object* vertical_axis_value) : Base(), scene(scene_value), horizontal_axis(horizontal_axis_value), vertical_axis(vertical_axis_value) {}
@@ -38,11 +39,11 @@ std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Selection_Rectang
static_cast<typename Object::Private&>(*overlay->d).bind_sources(scene, horizontal_axis, vertical_axis); static_cast<typename Object::Private&>(*overlay->d).bind_sources(scene, horizontal_axis, vertical_axis);
auto graph_result = scene->template edit_dependency_graph<Paint_Tag>([&](auto& paint) { auto graph_result = scene->template edit_dependency_graph<Paint_Tag>([&](auto& paint) {
paint.add(overlay.get()); paint.add(overlay.get());
paint.template add_state_dependency<Render_Scene_2D_State_Tag>(overlay.get(), scene); paint.template add_prop_dependency<Render_Scene_2D::Base_Tag>(overlay.get(), scene);
paint.template add_state_dependency<Abs_Axis_State_Tag>(overlay.get(), horizontal_axis); paint.template add_prop_dependency<Abs_Axis::Base_Tag>(overlay.get(), horizontal_axis);
paint.template add_state_dependency<Numeric_Axis_State_Tag>(overlay.get(), horizontal_axis); paint.template add_prop_dependency<Numeric_Axis::Base_Tag>(overlay.get(), horizontal_axis);
paint.template add_state_dependency<Abs_Axis_State_Tag>(overlay.get(), vertical_axis); paint.template add_prop_dependency<Abs_Axis::Base_Tag>(overlay.get(), vertical_axis);
paint.template add_state_dependency<Numeric_Axis_State_Tag>(overlay.get(), vertical_axis); paint.template add_prop_dependency<Numeric_Axis::Base_Tag>(overlay.get(), vertical_axis);
}); });
if (!graph_result) return std::unexpected(graph_result.error()); if (!graph_result) return std::unexpected(graph_result.error());
return std::move(overlay); return std::move(overlay);
@@ -50,8 +51,8 @@ std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Selection_Rectang
template <Attached Object> template <Attached Object>
void Selection_Rectangle_Overlay::Private::paint(Object* object) { void Selection_Rectangle_Overlay::Private::paint(Object* object) {
auto& data = static_cast<typename Object::Private&>(*this); auto& data = static_cast<typename Object::Private&>(*this);
const auto& state = static_cast<const State&>(*data.state.current); const auto& state = static_cast<const Prop&>(*data.current);
const Size canvas = scene->template read_state<Render_Scene_2D_State_Tag>().viewport; const Size canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport;
auto& cache = object->template pending_buffer<Color_Cache>(); auto& cache = object->template pending_buffer<Color_Cache>();
cache.ensure_size(canvas); cache.clear(); cache.ensure_size(canvas); cache.clear();
if (canvas.empty()) return; if (canvas.empty()) return;
@@ -76,16 +77,18 @@ void Selection_Rectangle_Overlay::Private::handle_event(Object* object, const Ev
const Axis_Coordinate second_x = horizontal_axis->point_to_coordinate(drag_current); const Axis_Coordinate second_x = horizontal_axis->point_to_coordinate(drag_current);
const Axis_Coordinate first_y = vertical_axis->point_to_coordinate(drag_origin); const Axis_Coordinate first_y = vertical_axis->point_to_coordinate(drag_origin);
const Axis_Coordinate second_y = vertical_axis->point_to_coordinate(drag_current); const Axis_Coordinate second_y = vertical_axis->point_to_coordinate(drag_current);
object->template update_state<&State::selected_regions>([=](State_Access<typename Object::State> states) { states.template get<Selection_Rectangle_Overlay_State_Tag>().selected_regions.push_back(Rect_F{first_x, first_y, second_x - first_x, second_y - first_y}.normalized()); }); object->template update_prop<&Prop::selected_regions>([=](Prop_Access<typename Object::Prop> props) { props.template get<Selection_Rectangle_Overlay::Base_Tag>().selected_regions.push_back(Rect_F{first_x, first_y, second_x - first_x, second_y - first_y}.normalized()); });
event.accept(); event.accept();
} }
template <typename Object, typename Owner, typename Member, typename State_Type> template <typename Object, typename Owner, typename Member, typename Prop_Type>
void Selection_Rectangle_Overlay::Private::after_state_set(Object* object, Member Owner::*, State_Access<State_Type>) { if constexpr (std::same_as<Owner, State>) object->template mark_dirty<Paint_Tag>(); } void Selection_Rectangle_Overlay::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Paint_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type>
void Selection_Rectangle_Overlay::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { pending_states.template get<Selection_Rectangle_Overlay::Base_Tag>().selected_region_count = static_cast<const Prop&>(*current_prop).selected_regions.size(); }
template <Attached Object> template <Attached Object>
const Selection_Rectangle_Overlay::Private::Dispatch& Selection_Rectangle_Overlay::Private::dispatch_for() { const Selection_Rectangle_Overlay::Private::Dispatch& Selection_Rectangle_Overlay::Private::dispatch_for() {
static const Dispatch value{ static const Dispatch value{
[](const Root* root) { return static_cast<const Object*>(root)->template read_state<Selection_Rectangle_Overlay_State_Tag>().selected_regions; }, [](const Root* root) { return static_cast<const Object*>(root)->template read_prop<Selection_Rectangle_Overlay::Base_Tag>().selected_regions; },
[](Root* root) { auto* object = static_cast<Object*>(root); object->template update_state<&State::selected_regions>(std::vector<Rect_F>{}); } [](Root* root) { static_cast<Object*>(root)->template set<&Prop::selected_regions>(std::vector<Rect_F>{}); }
}; };
return value; return value;
} }
@@ -1,6 +1,7 @@
#include "Spectrum.hpp" #include "Spectrum.hpp"
namespace aethera::render_2d { namespace aethera::render_2d {
bool Spectrum_Frame::operator==(const Spectrum_Frame&) const = default; bool Spectrum_Frame::operator==(const Spectrum_Frame&) const = default;
bool Spectrum::Prop::operator==(const Prop&) const = default;
bool Spectrum::State::operator==(const State&) const = default; bool Spectrum::State::operator==(const State&) const = default;
void Spectrum::update_samples(std::span<const Spectrum_Power> values) { void Spectrum::update_samples(std::span<const Spectrum_Power> values) {
static_cast<Private&>(*d).dispatch->update_samples(this, values); static_cast<Private&>(*d).dispatch->update_samples(this, values);
+8 -4
View File
@@ -15,7 +15,6 @@ using Spectrum_Power = Plot_Value;
using Spectrum_Interpolation_Ratio = Plot_Ratio; using Spectrum_Interpolation_Ratio = Plot_Ratio;
using Spectrum_Marker_Index = Plot_Index; using Spectrum_Marker_Index = Plot_Index;
using Spectrum_Partition_Mode = Plot_Partition_Mode; using Spectrum_Partition_Mode = Plot_Partition_Mode;
struct Spectrum_State_Tag {};
struct Spectrum_Frame_Tag {}; struct Spectrum_Frame_Tag {};
struct Spectrum_Frame { struct Spectrum_Frame {
std::vector<Spectrum_Power> samples{}; /* 最近提交的当前频谱功率样本。 */ std::vector<Spectrum_Power> samples{}; /* 最近提交的当前频谱功率样本。 */
@@ -24,12 +23,11 @@ struct Spectrum_Frame {
bool operator==(const Spectrum_Frame&) const; bool operator==(const Spectrum_Frame&) const;
}; };
/* 使用频率轴和功率轴分块准备、绘制当前值、保持曲线及频率标记。 */ /* 使用频率轴和功率轴分块准备、绘制当前值、保持曲线及频率标记。 */
struct Spectrum : Def<Spectrum, Renderable, State_Type<Spectrum_State_Tag>, Tagged_Buffer<Color_Cache, Blend2D_Cache>, Tagged_Buffer<Spectrum_Frame_Tag, Spectrum_Frame>> { struct Spectrum : Def<Spectrum, Renderable, Tagged_Buffer<Color_Cache, Blend2D_Cache>, Tagged_Buffer<Spectrum_Frame_Tag, Spectrum_Frame>> {
using Scene_Object = Impl<Render_Scene_2D>; using Scene_Object = Impl<Render_Scene_2D>;
using Frequency_Object = Impl<Frequency_Axis>; using Frequency_Object = Impl<Frequency_Axis>;
using Power_Object = Impl<Numeric_Axis>; using Power_Object = Impl<Numeric_Axis>;
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {
struct State : Prev_State<Spectrum_State_Tag> {
Axis_Range frequency_range{}; /* 输入样本首尾对应的有向频率范围,单位为 Hz。 */ Axis_Range frequency_range{}; /* 输入样本首尾对应的有向频率范围,单位为 Hz。 */
Spectrum_Frequency center_frequency{50.0}; /* 中心频率标记位置,单位为 Hz。 */ Spectrum_Frequency center_frequency{50.0}; /* 中心频率标记位置,单位为 Hz。 */
Axis_Range sweep_frequency_range{40.0, 60.0}; /* 扫频背景覆盖的频率范围,单位为 Hz。 */ Axis_Range sweep_frequency_range{40.0, 60.0}; /* 扫频背景覆盖的频率范围,单位为 Hz。 */
@@ -54,6 +52,12 @@ struct Spectrum : Def<Spectrum, Renderable, State_Type<Spectrum_State_Tag>, Tagg
Brush sweep_region_brush{Color{255, 255, 0, 100}, Brush_Style::solid}; /* 扫频区域背景样式。 */ Brush sweep_region_brush{Color{255, 255, 0, 100}, Brush_Style::solid}; /* 扫频区域背景样式。 */
std::vector<Spectrum_Frequency> custom_markers{}; /* 自定义频率标记的唯一权威集合,单位为 Hz。 */ std::vector<Spectrum_Frequency> custom_markers{}; /* 自定义频率标记的唯一权威集合,单位为 Hz。 */
Spectrum_Marker_Index selected_marker{-1}; /* 当前选中标记下标;-1 表示未选择。 */ Spectrum_Marker_Index selected_marker{-1}; /* 当前选中标记下标;-1 表示未选择。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
std::size_t sample_count{}; /* 最近一次发布帧的样本数。 */
std::size_t rendered_point_count{}; /* 最近一次 Prepare 生成的曲线点数。 */
std::size_t selectable_marker_count{}; /* 当前可选择的自定义标记数。 */
bool operator==(const State&) const; bool operator==(const State&) const;
}; };
/* 完整声明、分块子图、内部状态操作和 CRTP 分派见 Spectrum.ipp。 */ /* 完整声明、分块子图、内部状态操作和 CRTP 分派见 Spectrum.ipp。 */
+56 -46
View File
@@ -80,18 +80,19 @@ struct Spectrum::Private : Prev_Private {
template <Attached Object> [[nodiscard]] Set_Marker_Frequency_Result set_marker_frequency(Object* object, Spectrum_Marker_Index index, Spectrum_Frequency frequency); template <Attached Object> [[nodiscard]] Set_Marker_Frequency_Result set_marker_frequency(Object* object, Spectrum_Marker_Index index, Spectrum_Frequency frequency);
template <Attached Object> [[nodiscard]] Set_Current_Marker_Frequency_Result set_current_marker_frequency(Object* object, Spectrum_Frequency frequency); template <Attached Object> [[nodiscard]] Set_Current_Marker_Frequency_Result set_current_marker_frequency(Object* object, Spectrum_Frequency frequency);
/* CRTP State 钩子:Spectrum 业务状态写入后标记自身 Prepare;其他继承层状态由各自 Private 负责。 */ /* CRTP State 钩子:Spectrum 业务状态写入后标记自身 Prepare;其他继承层状态由各自 Private 负责。 */
template <typename Object, typename Owner, typename Member, typename State_Type> void after_state_set(Object* object, Member Owner::* member, State_Access<State_Type> pending_states); template <typename Object, typename Owner, typename Member, typename Prop_Type> void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> pending_states);
template <typename Object, typename Prop_Type, typename State_Type> void before_advance(Object* object, Prop_Type* pending_prop, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type> current_states);
/* CRTP 子图能力:按当前样本数和 State 分块策略构建并行 Prepare 图。 */ /* CRTP 子图能力:按当前样本数和 State 分块策略构建并行 Prepare 图。 */
template <Attached Object> [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const State& state); template <Attached Object> [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state);
/* CRTP 子图能力:构建背景、分块曲线和覆盖标记的 Paint 图。 */ /* CRTP 子图能力:构建背景、分块曲线和覆盖标记的 Paint 图。 */
template <Attached Object> [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const State& state); template <Attached Object> [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state);
/* CRTP 覆盖:样本规模或分块配置改变时重建 Prepare 子图。 */ /* CRTP 覆盖:样本规模或分块配置改变时重建 Prepare 子图。 */
template <Attached Object> [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const State& state); template <Attached Object> [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state);
template <Attached Object> [[nodiscard]] std::size_t desired_partition_count(const Object* object, const State& state) const; template <Attached Object> [[nodiscard]] std::size_t desired_partition_count(const Object* object, const Prop& state) const;
template <Attached Object> void prepare_frame(Object* object, std::size_t partition_count); template <Attached Object> void prepare_frame(Object* object, std::size_t partition_count);
template <Attached Object> void prepare_partition(Object* object, std::size_t partition_index); template <Attached Object> void prepare_partition(Object* object, std::size_t partition_index);
template <Attached Object> void paint_frame(Object* object); template <Attached Object> void paint_frame(Object* object);
[[nodiscard]] static std::expected<Spectrum_Power, Power_At_Result> power_at(const State& state, const Spectrum_Frame& frame, Spectrum_Frequency frequency); [[nodiscard]] static std::expected<Spectrum_Power, Power_At_Result> power_at(const Prop& state, const Spectrum_Frame& frame, Spectrum_Frequency frequency);
}; };
template <typename Object> template <typename Object>
Spectrum::Builder<Object>::Builder(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), scene(scene_value), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {} Spectrum::Builder<Object>::Builder(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), scene(scene_value), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {}
@@ -105,11 +106,11 @@ std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Spectrum::Builder
prepare.add(frequency_axis); prepare.add(frequency_axis);
prepare.add(power_axis); prepare.add(power_axis);
prepare.add(spectrum.get()); prepare.add(spectrum.get());
prepare.template add_state_dependency<Render_Scene_2D_State_Tag>(spectrum.get(), scene); prepare.template add_prop_dependency<Render_Scene_2D::Base_Tag>(spectrum.get(), scene);
prepare.template add_state_dependency<Abs_Axis_State_Tag>(spectrum.get(), frequency_axis); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(spectrum.get(), frequency_axis);
prepare.template add_state_dependency<Numeric_Axis_State_Tag>(spectrum.get(), frequency_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(spectrum.get(), frequency_axis);
prepare.template add_state_dependency<Abs_Axis_State_Tag>(spectrum.get(), power_axis); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(spectrum.get(), power_axis);
prepare.template add_state_dependency<Numeric_Axis_State_Tag>(spectrum.get(), power_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(spectrum.get(), power_axis);
paint.add(frequency_axis); paint.add(frequency_axis);
paint.add(power_axis); paint.add(power_axis);
paint.add(spectrum.get()); paint.add(spectrum.get());
@@ -121,7 +122,7 @@ template <typename Values>
void Spectrum::update_samples(const Values& values) { void Spectrum::update_samples(const Values& values) {
update_samples(std::span<const Spectrum_Power>(std::data(values), std::size(values))); update_samples(std::span<const Spectrum_Power>(std::data(values), std::size(values)));
} }
inline std::expected<Spectrum_Power, Spectrum::Power_At_Result> Spectrum::Private::power_at(const State& state, const Spectrum_Frame& frame, Spectrum_Frequency frequency) { inline std::expected<Spectrum_Power, Spectrum::Power_At_Result> Spectrum::Private::power_at(const Prop& state, const Spectrum_Frame& frame, Spectrum_Frequency frequency) {
if (frame.samples.empty()) return std::unexpected(Power_At_Result::no_samples); if (frame.samples.empty()) return std::unexpected(Power_At_Result::no_samples);
if (state.frequency_range.length() == 0.0) return std::unexpected(Power_At_Result::invalid_frequency_range); if (state.frequency_range.length() == 0.0) return std::unexpected(Power_At_Result::invalid_frequency_range);
if (!state.frequency_range.contains(frequency)) return std::unexpected(Power_At_Result::frequency_out_of_range); if (!state.frequency_range.contains(frequency)) return std::unexpected(Power_At_Result::frequency_out_of_range);
@@ -133,15 +134,15 @@ inline std::expected<Spectrum_Power, Spectrum::Power_At_Result> Spectrum::Privat
return frame.samples[lower] * (1.0 - fraction) + frame.samples[upper] * fraction; return frame.samples[lower] * (1.0 - fraction) + frame.samples[upper] * fraction;
} }
template <Attached Object> template <Attached Object>
std::size_t Spectrum::Private::desired_partition_count(const Object* object, const State& state) const { std::size_t Spectrum::Private::desired_partition_count(const Object* object, const Prop& state) const {
return detail::curve_partition_count(state.partition_mode, state.partition_count, object->template current_buffer<Spectrum_Frame_Tag>().samples.size()); return detail::curve_partition_count(state.partition_mode, state.partition_count, object->template current_buffer<Spectrum_Frame_Tag>().samples.size());
} }
template <Attached Object> template <Attached Object>
bool Spectrum::Private::should_rebuild_prepare_graph(Object* object, const State& state) { bool Spectrum::Private::should_rebuild_prepare_graph(Object* object, const Prop& state) {
return prepare_graph_partition_count != desired_partition_count(object, state); return prepare_graph_partition_count != desired_partition_count(object, state);
} }
template <Attached Object> template <Attached Object>
tf::Taskflow Spectrum::Private::build_prepare_graph(Object* object, const State& state) { tf::Taskflow Spectrum::Private::build_prepare_graph(Object* object, const Prop& state) {
const std::size_t partition_count = desired_partition_count(object, state); const std::size_t partition_count = desired_partition_count(object, state);
prepare_graph_partition_count = partition_count; prepare_graph_partition_count = partition_count;
tf::Taskflow graph; tf::Taskflow graph;
@@ -153,7 +154,7 @@ tf::Taskflow Spectrum::Private::build_prepare_graph(Object* object, const State&
return graph; return graph;
} }
template <Attached Object> template <Attached Object>
tf::Taskflow Spectrum::Private::build_paint_graph(Object* object, const State&) { tf::Taskflow Spectrum::Private::build_paint_graph(Object* object, const Prop&) {
tf::Taskflow graph; tf::Taskflow graph;
graph.emplace([this, object] { paint_frame(object); }).name("spectrum.paint.frame"); graph.emplace([this, object] { paint_frame(object); }).name("spectrum.paint.frame");
return graph; return graph;
@@ -161,12 +162,12 @@ tf::Taskflow Spectrum::Private::build_paint_graph(Object* object, const State&)
template <Attached Object> template <Attached Object>
void Spectrum::Private::prepare_frame(Object* object, std::size_t partition_count) { void Spectrum::Private::prepare_frame(Object* object, std::size_t partition_count) {
auto& private_data = static_cast<typename Object::Private&>(*this); auto& private_data = static_cast<typename Object::Private&>(*this);
const auto& state = static_cast<const State&>(*private_data.state.current); const auto& state = static_cast<const Prop&>(*private_data.current);
const auto& frame = object->template current_buffer<Spectrum_Frame_Tag>(); const auto& frame = object->template current_buffer<Spectrum_Frame_Tag>();
const auto& scene_state = scene->template read_state<Render_Scene_2D_State_Tag>(); const auto& scene_state = scene->template read_prop<Render_Scene_2D::Base_Tag>();
const auto& frequency_layout = frequency_axis->template read_state<Abs_Axis_State_Tag>(); const auto& frequency_layout = frequency_axis->template read_prop<Abs_Axis::Base_Tag>();
const auto& power_layout = power_axis->template read_state<Abs_Axis_State_Tag>(); const auto& power_layout = power_axis->template read_prop<Abs_Axis::Base_Tag>();
const auto& power_state = power_axis->template read_state<Numeric_Axis_State_Tag>(); const auto& power_state = power_axis->template read_prop<Numeric_Axis::Base_Tag>();
prepared = {}; prepared = {};
prepared.partitions.resize(partition_count); prepared.partitions.resize(partition_count);
if (frequency_layout.orientation == power_layout.orientation) return; if (frequency_layout.orientation == power_layout.orientation) return;
@@ -196,13 +197,13 @@ template <Attached Object>
void Spectrum::Private::prepare_partition(Object* object, std::size_t partition_index) { void Spectrum::Private::prepare_partition(Object* object, std::size_t partition_index) {
if (!prepared.valid || partition_index >= prepared.partitions.size()) return; if (!prepared.valid || partition_index >= prepared.partitions.size()) return;
auto& private_data = static_cast<typename Object::Private&>(*this); auto& private_data = static_cast<typename Object::Private&>(*this);
const auto& state = static_cast<const State&>(*private_data.state.current); const auto& state = static_cast<const Prop&>(*private_data.current);
const auto& frame = object->template current_buffer<Spectrum_Frame_Tag>(); const auto& frame = object->template current_buffer<Spectrum_Frame_Tag>();
if (frame.samples.empty()) return; if (frame.samples.empty()) return;
const auto& frequency_layout = frequency_axis->template read_state<Abs_Axis_State_Tag>(); const auto& frequency_layout = frequency_axis->template read_prop<Abs_Axis::Base_Tag>();
const auto& power_layout = power_axis->template read_state<Abs_Axis_State_Tag>(); const auto& power_layout = power_axis->template read_prop<Abs_Axis::Base_Tag>();
const auto& frequency_state = frequency_axis->template read_state<Numeric_Axis_State_Tag>(); const auto& frequency_state = frequency_axis->template read_prop<Numeric_Axis::Base_Tag>();
const auto& power_state = power_axis->template read_state<Numeric_Axis_State_Tag>(); const auto& power_state = power_axis->template read_prop<Numeric_Axis::Base_Tag>();
const auto range = detail::curve_partition_range(frame.samples.size(), partition_index, prepared.partitions.size(), state.frequency_range); const auto range = detail::curve_partition_range(frame.samples.size(), partition_index, prepared.partitions.size(), state.frequency_range);
auto& partition = prepared.partitions[partition_index]; auto& partition = prepared.partitions[partition_index];
partition.clip = detail::map_plot_rect(frequency_axis, range.domain, power_axis, power_state.coordinate_range, frequency_layout.orientation); partition.clip = detail::map_plot_rect(frequency_axis, range.domain, power_axis, power_state.coordinate_range, frequency_layout.orientation);
@@ -213,7 +214,7 @@ void Spectrum::Private::prepare_partition(Object* object, std::size_t partition_
template <Attached Object> template <Attached Object>
void Spectrum::Private::paint_frame(Object* object) { void Spectrum::Private::paint_frame(Object* object) {
auto& private_data = static_cast<typename Object::Private&>(*this); auto& private_data = static_cast<typename Object::Private&>(*this);
const auto& state = static_cast<const State&>(*private_data.state.current); const auto& state = static_cast<const Prop&>(*private_data.current);
auto& cache = object->template pending_buffer<Color_Cache>(); auto& cache = object->template pending_buffer<Color_Cache>();
cache.ensure_size(prepared.canvas_size); cache.ensure_size(prepared.canvas_size);
cache.clear(); cache.clear();
@@ -252,13 +253,13 @@ void Spectrum::Private::update_samples(Object* object, std::span<const Spectrum_
} }
template <Attached Object> template <Attached Object>
void Spectrum::Private::add_marker(Object* object, Spectrum_Frequency frequency) { void Spectrum::Private::add_marker(Object* object, Spectrum_Frequency frequency) {
object->template update_state<&State::custom_markers>([frequency](State_Access<typename Object::State> states) { states.template get<Spectrum_State_Tag>().custom_markers.push_back(frequency); }); object->template update_prop<&Prop::custom_markers>([frequency](Prop_Access<typename Object::Prop> props) { props.template get<Spectrum::Base_Tag>().custom_markers.push_back(frequency); });
object->template mark_dirty<Prepare_Data_Tag>(); object->template mark_dirty<Prepare_Data_Tag>();
} }
template <Attached Object> template <Attached Object>
void Spectrum::Private::remove_marker(Object* object, Spectrum_Frequency frequency) { void Spectrum::Private::remove_marker(Object* object, Spectrum_Frequency frequency) {
object->template update_state<&State::custom_markers, &State::selected_marker>([frequency](State_Access<typename Object::State> states) { object->template update_prop<&Prop::custom_markers, &Prop::selected_marker>([frequency](Prop_Access<typename Object::Prop> props) {
auto& state = states.template get<Spectrum_State_Tag>(); auto& state = props.template get<Spectrum::Base_Tag>();
if (state.custom_markers.empty()) return; if (state.custom_markers.empty()) return;
const auto closest = std::min_element(state.custom_markers.begin(), state.custom_markers.end(), [frequency](Spectrum_Frequency left, Spectrum_Frequency right) { return std::abs(left - frequency) < std::abs(right - frequency); }); const auto closest = std::min_element(state.custom_markers.begin(), state.custom_markers.end(), [frequency](Spectrum_Frequency left, Spectrum_Frequency right) { return std::abs(left - frequency) < std::abs(right - frequency); });
const Spectrum_Marker_Index removed = std::distance(state.custom_markers.begin(), closest); const Spectrum_Marker_Index removed = std::distance(state.custom_markers.begin(), closest);
@@ -270,8 +271,8 @@ void Spectrum::Private::remove_marker(Object* object, Spectrum_Frequency frequen
} }
template <Attached Object> template <Attached Object>
void Spectrum::Private::remove_selected_marker(Object* object) { void Spectrum::Private::remove_selected_marker(Object* object) {
object->template update_state<&State::custom_markers, &State::selected_marker>([](State_Access<typename Object::State> states) { object->template update_prop<&Prop::custom_markers, &Prop::selected_marker>([](Prop_Access<typename Object::Prop> props) {
auto& state = states.template get<Spectrum_State_Tag>(); auto& state = props.template get<Spectrum::Base_Tag>();
if (state.selected_marker < 0 || static_cast<std::size_t>(state.selected_marker) >= state.custom_markers.size()) return; if (state.selected_marker < 0 || static_cast<std::size_t>(state.selected_marker) >= state.custom_markers.size()) return;
state.custom_markers.erase(state.custom_markers.begin() + state.selected_marker); state.custom_markers.erase(state.custom_markers.begin() + state.selected_marker);
state.selected_marker = -1; state.selected_marker = -1;
@@ -280,40 +281,49 @@ void Spectrum::Private::remove_selected_marker(Object* object) {
} }
template <Attached Object> template <Attached Object>
void Spectrum::Private::clear_markers(Object* object) { void Spectrum::Private::clear_markers(Object* object) {
object->template update_state<&State::custom_markers, &State::selected_marker>([](State_Access<typename Object::State> states) { auto& state = states.template get<Spectrum_State_Tag>(); state.custom_markers.clear(); state.selected_marker = -1; }); object->template update_prop<&Prop::custom_markers, &Prop::selected_marker>([](Prop_Access<typename Object::Prop> props) { auto& state = props.template get<Spectrum::Base_Tag>(); state.custom_markers.clear(); state.selected_marker = -1; });
object->template mark_dirty<Prepare_Data_Tag>(); object->template mark_dirty<Prepare_Data_Tag>();
} }
template <Attached Object> template <Attached Object>
void Spectrum::Private::set_selected_marker(Object* object, Spectrum_Marker_Index index) { void Spectrum::Private::set_selected_marker(Object* object, Spectrum_Marker_Index index) {
object->template update_state<&State::selected_marker>([index](State_Access<typename Object::State> states) { auto& state = states.template get<Spectrum_State_Tag>(); state.selected_marker = index >= 0 && static_cast<std::size_t>(index) < state.custom_markers.size() ? index : -1; }); object->template update_prop<&Prop::selected_marker>([index](Prop_Access<typename Object::Prop> props) { auto& state = props.template get<Spectrum::Base_Tag>(); state.selected_marker = index >= 0 && static_cast<std::size_t>(index) < state.custom_markers.size() ? index : -1; });
object->template mark_dirty<Prepare_Data_Tag>(); object->template mark_dirty<Prepare_Data_Tag>();
} }
template <Attached Object> template <Attached Object>
void Spectrum::Private::select_next_marker(Object* object) { void Spectrum::Private::select_next_marker(Object* object) {
object->template update_state<&State::selected_marker>([](State_Access<typename Object::State> states) { auto& state = states.template get<Spectrum_State_Tag>(); state.selected_marker = state.custom_markers.empty() ? -1 : (state.selected_marker + 1) % static_cast<Spectrum_Marker_Index>(state.custom_markers.size()); }); object->template update_prop<&Prop::selected_marker>([](Prop_Access<typename Object::Prop> props) { auto& state = props.template get<Spectrum::Base_Tag>(); state.selected_marker = state.custom_markers.empty() ? -1 : (state.selected_marker + 1) % static_cast<Spectrum_Marker_Index>(state.custom_markers.size()); });
object->template mark_dirty<Prepare_Data_Tag>(); object->template mark_dirty<Prepare_Data_Tag>();
} }
template <Attached Object> template <Attached Object>
void Spectrum::Private::select_previous_marker(Object* object) { void Spectrum::Private::select_previous_marker(Object* object) {
object->template update_state<&State::selected_marker>([](State_Access<typename Object::State> states) { auto& state = states.template get<Spectrum_State_Tag>(); state.selected_marker = state.custom_markers.empty() ? -1 : (state.selected_marker <= 0 ? static_cast<Spectrum_Marker_Index>(state.custom_markers.size()) : state.selected_marker) - 1; }); object->template update_prop<&Prop::selected_marker>([](Prop_Access<typename Object::Prop> props) { auto& state = props.template get<Spectrum::Base_Tag>(); state.selected_marker = state.custom_markers.empty() ? -1 : (state.selected_marker <= 0 ? static_cast<Spectrum_Marker_Index>(state.custom_markers.size()) : state.selected_marker) - 1; });
object->template mark_dirty<Prepare_Data_Tag>(); object->template mark_dirty<Prepare_Data_Tag>();
} }
template <Attached Object> template <Attached Object>
Spectrum::Set_Marker_Frequency_Result Spectrum::Private::set_marker_frequency(Object* object, Spectrum_Marker_Index index, Spectrum_Frequency frequency) { Spectrum::Set_Marker_Frequency_Result Spectrum::Private::set_marker_frequency(Object* object, Spectrum_Marker_Index index, Spectrum_Frequency frequency) {
if (index < 0 || static_cast<std::size_t>(index) >= object->template read_state<Spectrum_State_Tag>().custom_markers.size()) return Set_Marker_Frequency_Result::index_out_of_range; if (index < 0 || static_cast<std::size_t>(index) >= object->template read_prop<Spectrum::Base_Tag>().custom_markers.size()) return Set_Marker_Frequency_Result::index_out_of_range;
object->template update_state<&State::custom_markers>([index, frequency](State_Access<typename Object::State> states) { states.template get<Spectrum_State_Tag>().custom_markers[static_cast<std::size_t>(index)] = frequency; }); object->template update_prop<&Prop::custom_markers>([index, frequency](Prop_Access<typename Object::Prop> props) { props.template get<Spectrum::Base_Tag>().custom_markers[static_cast<std::size_t>(index)] = frequency; });
object->template mark_dirty<Prepare_Data_Tag>(); object->template mark_dirty<Prepare_Data_Tag>();
return Set_Marker_Frequency_Result::updated; return Set_Marker_Frequency_Result::updated;
} }
template <Attached Object> template <Attached Object>
Spectrum::Set_Current_Marker_Frequency_Result Spectrum::Private::set_current_marker_frequency(Object* object, Spectrum_Frequency frequency) { Spectrum::Set_Current_Marker_Frequency_Result Spectrum::Private::set_current_marker_frequency(Object* object, Spectrum_Frequency frequency) {
const Spectrum_Marker_Index index = object->template read_state<Spectrum_State_Tag>().selected_marker; const Spectrum_Marker_Index index = object->template read_prop<Spectrum::Base_Tag>().selected_marker;
if (index < 0) return Set_Current_Marker_Frequency_Result::no_selection; if (index < 0) return Set_Current_Marker_Frequency_Result::no_selection;
return set_marker_frequency(object, index, frequency) == Set_Marker_Frequency_Result::updated ? Set_Current_Marker_Frequency_Result::updated : Set_Current_Marker_Frequency_Result::no_selection; return set_marker_frequency(object, index, frequency) == Set_Marker_Frequency_Result::updated ? Set_Current_Marker_Frequency_Result::updated : Set_Current_Marker_Frequency_Result::no_selection;
} }
template <typename Object, typename Owner, typename Member, typename State_Type> template <typename Object, typename Owner, typename Member, typename Prop_Type>
void Spectrum::Private::after_state_set(Object* object, Member Owner::*, State_Access<State_Type>) { void Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) {
if constexpr (std::same_as<Owner, State>) object->template mark_dirty<Prepare_Data_Tag>(); if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>();
}
template <typename Object, typename Prop_Type, typename State_Type>
void Spectrum::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) {
const auto& private_data = static_cast<const typename Object::Private&>(*this);
auto& state = pending_states.template get<Spectrum::Base_Tag>();
state.sample_count = private_data.buffer_storage.template get<Spectrum_Frame_Tag>().pending->samples.size();
state.rendered_point_count = 0;
for (const auto& partition : prepared.partitions) state.rendered_point_count += partition.current.points.size();
state.selectable_marker_count = static_cast<const Prop&>(*current_prop).custom_markers.size();
} }
template <Attached Object> template <Attached Object>
const Spectrum::Private::Dispatch& Spectrum::Private::dispatch_for() { const Spectrum::Private::Dispatch& Spectrum::Private::dispatch_for() {
@@ -321,17 +331,17 @@ const Spectrum::Private::Dispatch& Spectrum::Private::dispatch_for() {
[](Root* root, std::span<const Spectrum_Power> values) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).update_samples(object, values); }, [](Root* root, std::span<const Spectrum_Power> values) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).update_samples(object, values); },
[](const Root* root) { return static_cast<const Object*>(root)->template current_buffer<Spectrum_Frame_Tag>().samples.size(); }, [](const Root* root) { return static_cast<const Object*>(root)->template current_buffer<Spectrum_Frame_Tag>().samples.size(); },
[](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); std::size_t count{}; for (const auto& partition : data.prepared.partitions) count += partition.current.points.size(); return count; }, [](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); std::size_t count{}; for (const auto& partition : data.prepared.partitions) count += partition.current.points.size(); return count; },
[](const Root* root, Spectrum_Frequency frequency) { const auto* object = static_cast<const Object*>(root); const auto& data = static_cast<const typename Object::Private&>(*object->d); return Private::power_at(static_cast<const State&>(*data.state.current), object->template current_buffer<Spectrum_Frame_Tag>(), frequency); }, [](const Root* root, Spectrum_Frequency frequency) { const auto* object = static_cast<const Object*>(root); const auto& data = static_cast<const typename Object::Private&>(*object->d); return Private::power_at(static_cast<const Prop&>(*data.current), object->template current_buffer<Spectrum_Frame_Tag>(), frequency); },
[](Root* root, Spectrum_Frequency frequency) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).add_marker(object, frequency); }, [](Root* root, Spectrum_Frequency frequency) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).add_marker(object, frequency); },
[](Root* root, Spectrum_Frequency frequency) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).remove_marker(object, frequency); }, [](Root* root, Spectrum_Frequency frequency) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).remove_marker(object, frequency); },
[](Root* root) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).remove_selected_marker(object); }, [](Root* root) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).remove_selected_marker(object); },
[](Root* root) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).clear_markers(object); }, [](Root* root) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).clear_markers(object); },
[](const Root* root) { return static_cast<const Object*>(root)->template read_state<Spectrum_State_Tag>().custom_markers.size(); }, [](const Root* root) { return static_cast<const Object*>(root)->template read_prop<Spectrum::Base_Tag>().custom_markers.size(); },
[](const Root* root) { return static_cast<const Object*>(root)->template read_state<Spectrum_State_Tag>().selected_marker; }, [](const Root* root) { return static_cast<const Object*>(root)->template read_prop<Spectrum::Base_Tag>().selected_marker; },
[](Root* root, Spectrum_Marker_Index index) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).set_selected_marker(object, index); }, [](Root* root, Spectrum_Marker_Index index) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).set_selected_marker(object, index); },
[](Root* root) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).select_next_marker(object); }, [](Root* root) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).select_next_marker(object); },
[](Root* root) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).select_previous_marker(object); }, [](Root* root) { auto* object = static_cast<Object*>(root); static_cast<typename Object::Private&>(*object->d).select_previous_marker(object); },
[](const Root* root, Spectrum_Marker_Index index) -> std::expected<Spectrum_Frequency, Marker_Frequency_Result> { const auto& markers = static_cast<const Object*>(root)->template read_state<Spectrum_State_Tag>().custom_markers; if (index < 0 || static_cast<std::size_t>(index) >= markers.size()) return std::unexpected(Marker_Frequency_Result::index_out_of_range); return markers[static_cast<std::size_t>(index)]; }, [](const Root* root, Spectrum_Marker_Index index) -> std::expected<Spectrum_Frequency, Marker_Frequency_Result> { const auto& markers = static_cast<const Object*>(root)->template read_prop<Spectrum::Base_Tag>().custom_markers; if (index < 0 || static_cast<std::size_t>(index) >= markers.size()) return std::unexpected(Marker_Frequency_Result::index_out_of_range); return markers[static_cast<std::size_t>(index)]; },
[](Root* root, Spectrum_Marker_Index index, Spectrum_Frequency frequency) { auto* object = static_cast<Object*>(root); return static_cast<typename Object::Private&>(*object->d).set_marker_frequency(object, index, frequency); }, [](Root* root, Spectrum_Marker_Index index, Spectrum_Frequency frequency) { auto* object = static_cast<Object*>(root); return static_cast<typename Object::Private&>(*object->d).set_marker_frequency(object, index, frequency); },
[](Root* root, Spectrum_Frequency frequency) { auto* object = static_cast<Object*>(root); return static_cast<typename Object::Private&>(*object->d).set_current_marker_frequency(object, frequency); } [](Root* root, Spectrum_Frequency frequency) { auto* object = static_cast<Object*>(root); return static_cast<typename Object::Private&>(*object->d).set_current_marker_frequency(object, frequency); }
}; };
@@ -1,5 +1,6 @@
#include "Sweep_Spectrum.hpp" #include "Sweep_Spectrum.hpp"
namespace aethera::render_2d { namespace aethera::render_2d {
bool Sweep_Spectrum::Prop::operator==(const Prop&) const = default;
bool Sweep_Spectrum::State::operator==(const State&) const = default; bool Sweep_Spectrum::State::operator==(const State&) const = default;
void Sweep_Spectrum::append_block(std::span<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append(this, values); } void Sweep_Spectrum::append_block(std::span<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append(this, values); }
void Sweep_Spectrum::append_block(std::pmr::vector<Plot_Value>&& values) { append_block(std::span<const Plot_Value>(values.data(), values.size())); } void Sweep_Spectrum::append_block(std::pmr::vector<Plot_Value>&& values) { append_block(std::span<const Plot_Value>(values.data(), values.size())); }
@@ -9,13 +9,11 @@
#include <span> #include <span>
#include <vector> #include <vector>
namespace aethera::render_2d { namespace aethera::render_2d {
struct Sweep_Spectrum_State_Tag {}; struct Sweep_Spectrum : Def<Sweep_Spectrum, Renderable, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
struct Sweep_Spectrum : Def<Sweep_Spectrum, Renderable, State_Type<Sweep_Spectrum_State_Tag>, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
using Scene_Object = Impl<Render_Scene_2D>; using Scene_Object = Impl<Render_Scene_2D>;
using Frequency_Object = Impl<Frequency_Axis>; using Frequency_Object = Impl<Frequency_Axis>;
using Power_Object = Impl<Numeric_Axis>; using Power_Object = Impl<Numeric_Axis>;
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {
struct State : Prev_State<Sweep_Spectrum_State_Tag> {
Axis_Range frequency_range{}; /* 全部扫描块覆盖的频率范围。 */ Axis_Range frequency_range{}; /* 全部扫描块覆盖的频率范围。 */
std::size_t bins_per_block{}; /* 每个扫描块期望的功率点数;零值接受首块尺寸。 */ std::size_t bins_per_block{}; /* 每个扫描块期望的功率点数;零值接受首块尺寸。 */
std::size_t block_count{1}; /* 最多保留的扫描块数;零值按 1 处理。 */ std::size_t block_count{1}; /* 最多保留的扫描块数;零值按 1 处理。 */
@@ -26,6 +24,12 @@ struct Sweep_Spectrum : Def<Sweep_Spectrum, Renderable, State_Type<Sweep_Spectru
bool visible_range_only{true}; /* 是否裁掉频率轴可见范围外的线段。 */ bool visible_range_only{true}; /* 是否裁掉频率轴可见范围外的线段。 */
Line_Interpolation_Mode interpolation_mode{Line_Interpolation_Mode::linear_value}; /* 相邻功率点插值方式。 */ Line_Interpolation_Mode interpolation_mode{Line_Interpolation_Mode::linear_value}; /* 相邻功率点插值方式。 */
std::vector<std::vector<Plot_Value>> blocks{}; /* 已提交扫描块的唯一权威集合。 */ std::vector<std::vector<Plot_Value>> blocks{}; /* 已提交扫描块的唯一权威集合。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
std::size_t stored_block_count{}; /* 当前发布的扫描块数。 */
std::size_t stored_point_count{}; /* 当前发布的扫描点总数。 */
std::size_t rendered_point_count{}; /* 最近一次 Prepare 生成的曲线点数。 */
bool operator==(const State&) const; bool operator==(const State&) const;
}; };
struct Private; struct Private;
@@ -29,52 +29,55 @@ struct Sweep_Spectrum::Private : Prev_Private {
void bind_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value); void bind_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value);
template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for(); template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for();
/* CRTP 覆盖:按扫描点规模构建分块 Prepare 子图。 */ /* CRTP 覆盖:按扫描点规模构建分块 Prepare 子图。 */
template <Attached Object> [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const State& state); template <Attached Object> [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state);
/* CRTP 覆盖:构建消费曲线分块的 Paint 子图。 */ /* CRTP 覆盖:构建消费曲线分块的 Paint 子图。 */
template <Attached Object> [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const State& state); template <Attached Object> [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state);
/* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */
template <Attached Object> [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const State& state); template <Attached Object> [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state);
template <Attached Object> void prepare_frame(Object* object); template <Attached Object> void prepare_frame(Object* object);
template <Attached Object> void prepare_partition(Object* object, Plot_Partition_Count index); template <Attached Object> void prepare_partition(Object* object, Plot_Partition_Count index);
template <Attached Object> void paint_frame(Object* object); template <Attached Object> void paint_frame(Object* object);
template <typename Object, typename Owner, typename Member, typename State_Type> void after_state_set(Object* object, Member Owner::* member, State_Access<State_Type> states); template <typename Object, typename Owner, typename Member, typename Prop_Type> void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> states);
template <typename Object, typename Prop_Type, typename State_Type> void before_advance(Object* object, Prop_Type* pending_prop, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type> current_states);
}; };
template <typename Object> template <typename Object>
Sweep_Spectrum::Builder<Object>::Builder(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), scene(scene_value), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {} Sweep_Spectrum::Builder<Object>::Builder(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Power_Object* power_axis_value) : Base(), scene(scene_value), frequency_axis(frequency_axis_value), power_axis(power_axis_value) {}
template <typename Object> template <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Sweep_Spectrum::Builder<Object>::build() { std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Sweep_Spectrum::Builder<Object>::build() {
auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto sweep = std::move(result).value(); static_cast<typename Object::Private&>(*sweep->d).bind_sources(scene, frequency_axis, power_axis); auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto sweep = std::move(result).value(); static_cast<typename Object::Private&>(*sweep->d).bind_sources(scene, frequency_axis, power_axis);
auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) { prepare.add(frequency_axis); prepare.add(power_axis); prepare.add(sweep.get()); prepare.template add_state_dependency<Render_Scene_2D_State_Tag>(sweep.get(), scene); prepare.template add_state_dependency<Abs_Axis_State_Tag>(sweep.get(), frequency_axis); prepare.template add_state_dependency<Numeric_Axis_State_Tag>(sweep.get(), frequency_axis); prepare.template add_state_dependency<Abs_Axis_State_Tag>(sweep.get(), power_axis); prepare.template add_state_dependency<Numeric_Axis_State_Tag>(sweep.get(), power_axis); paint.add(frequency_axis); paint.add(power_axis); paint.add(sweep.get()); }); auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) { prepare.add(frequency_axis); prepare.add(power_axis); prepare.add(sweep.get()); prepare.template add_prop_dependency<Render_Scene_2D::Base_Tag>(sweep.get(), scene); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(sweep.get(), frequency_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(sweep.get(), frequency_axis); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(sweep.get(), power_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(sweep.get(), power_axis); paint.add(frequency_axis); paint.add(power_axis); paint.add(sweep.get()); });
if (!graph_result) return std::unexpected(graph_result.error()); return std::move(sweep); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(sweep);
} }
template <typename Values> void Sweep_Spectrum::append_block(const Values& values) { append_block(std::span<const Plot_Value>(std::data(values), std::size(values))); } template <typename Values> void Sweep_Spectrum::append_block(const Values& values) { append_block(std::span<const Plot_Value>(std::data(values), std::size(values))); }
template <Attached Object> template <Attached Object>
bool Sweep_Spectrum::Private::should_rebuild_prepare_graph(Object*, const State& state) { std::size_t points{}; for (const auto& block : state.blocks) points += block.size(); return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, points); } bool Sweep_Spectrum::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { std::size_t points{}; for (const auto& block : state.blocks) points += block.size(); return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, points); }
template <Attached Object> template <Attached Object>
tf::Taskflow Sweep_Spectrum::Private::build_prepare_graph(Object* object, const State& state) { std::size_t points{}; for (const auto& block : state.blocks) points += block.size(); graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, points); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("sweep_spectrum.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("sweep_spectrum.prepare.partition"); begin.precede(task); } return graph; } tf::Taskflow Sweep_Spectrum::Private::build_prepare_graph(Object* object, const Prop& state) { std::size_t points{}; for (const auto& block : state.blocks) points += block.size(); graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, points); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("sweep_spectrum.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("sweep_spectrum.prepare.partition"); begin.precede(task); } return graph; }
template <Attached Object> template <Attached Object>
tf::Taskflow Sweep_Spectrum::Private::build_paint_graph(Object* object, const State&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("sweep_spectrum.paint.frame"); return graph; } tf::Taskflow Sweep_Spectrum::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("sweep_spectrum.paint.frame"); return graph; }
template <Attached Object> template <Attached Object>
void Sweep_Spectrum::Private::prepare_frame(Object* object) { void Sweep_Spectrum::Private::prepare_frame(Object* object) {
const auto& state = object->template read_state<Sweep_Spectrum_State_Tag>(); const auto& frequency_layout = frequency_axis->template read_state<Abs_Axis_State_Tag>(); const auto& power_layout = power_axis->template read_state<Abs_Axis_State_Tag>(); const auto& power_state = power_axis->template read_state<Numeric_Axis_State_Tag>(); prepared = {}; prepared.partitions.resize(graph_partition_count); prepared.canvas = scene->template read_state<Render_Scene_2D_State_Tag>().viewport; const auto& state = object->template read_prop<Sweep_Spectrum::Base_Tag>(); const auto& frequency_layout = frequency_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& power_layout = power_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& power_state = power_axis->template read_prop<Numeric_Axis::Base_Tag>(); prepared = {}; prepared.partitions.resize(graph_partition_count); prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport;
if (prepared.canvas.empty() || frequency_layout.orientation == power_layout.orientation) return; for (const auto& block : state.blocks) prepared.values.insert(prepared.values.end(), block.begin(), block.end()); if (prepared.values.empty()) return; if (prepared.canvas.empty() || frequency_layout.orientation == power_layout.orientation) return; for (const auto& block : state.blocks) prepared.values.insert(prepared.values.end(), block.begin(), block.end()); if (prepared.values.empty()) return;
prepared.marker_first = detail::map_plot_point(frequency_axis, state.frequency_range.target, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation); prepared.marker_second = detail::map_plot_point(frequency_axis, state.frequency_range.target, power_axis, power_state.coordinate_range.target, frequency_layout.orientation); prepared.valid = true; prepared.marker_first = detail::map_plot_point(frequency_axis, state.frequency_range.target, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation); prepared.marker_second = detail::map_plot_point(frequency_axis, state.frequency_range.target, power_axis, power_state.coordinate_range.target, frequency_layout.orientation); prepared.valid = true;
} }
template <Attached Object> template <Attached Object>
void Sweep_Spectrum::Private::prepare_partition(Object* object, Plot_Partition_Count index) { void Sweep_Spectrum::Private::prepare_partition(Object* object, Plot_Partition_Count index) {
if (!prepared.valid) return; const auto& state = object->template read_state<Sweep_Spectrum_State_Tag>(); const auto& frequency_layout = frequency_axis->template read_state<Abs_Axis_State_Tag>(); const auto& power_layout = power_axis->template read_state<Abs_Axis_State_Tag>(); const auto& frequency_state = frequency_axis->template read_state<Numeric_Axis_State_Tag>(); const auto& power_state = power_axis->template read_state<Numeric_Axis_State_Tag>(); const auto range = detail::curve_partition_range(prepared.values.size(), index, prepared.partitions.size(), state.frequency_range); if (!prepared.valid) return; const auto& state = object->template read_prop<Sweep_Spectrum::Base_Tag>(); const auto& frequency_layout = frequency_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& power_layout = power_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& frequency_state = frequency_axis->template read_prop<Numeric_Axis::Base_Tag>(); const auto& power_state = power_axis->template read_prop<Numeric_Axis::Base_Tag>(); const auto range = detail::curve_partition_range(prepared.values.size(), index, prepared.partitions.size(), state.frequency_range);
prepared.partitions[index] = detail::prepare_curve(std::span<const Plot_Value>(prepared.values).subspan(range.first_sample, range.sample_count), range.domain, state.interpolation_mode, state.visible_range_only, frequency_state.coordinate_range, power_state.coordinate_range, frequency_axis, power_axis, frequency_layout.orientation, power_layout.orientation); prepared.partitions[index] = detail::prepare_curve(std::span<const Plot_Value>(prepared.values).subspan(range.first_sample, range.sample_count), range.domain, state.interpolation_mode, state.visible_range_only, frequency_state.coordinate_range, power_state.coordinate_range, frequency_axis, power_axis, frequency_layout.orientation, power_layout.orientation);
} }
template <Attached Object> template <Attached Object>
void Sweep_Spectrum::Private::paint_frame(Object* object) { const auto& state = object->template read_state<Sweep_Spectrum_State_Tag>(); auto& cache = object->template pending_buffer<Color_Cache>(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& curve : prepared.partitions) detail::paint_curve(painter, curve, state.pen); painter.line(prepared.marker_first, prepared.marker_second, state.current_frequency_pen); } void Sweep_Spectrum::Private::paint_frame(Object* object) { const auto& state = object->template read_prop<Sweep_Spectrum::Base_Tag>(); auto& cache = object->template pending_buffer<Color_Cache>(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); for (const auto& curve : prepared.partitions) detail::paint_curve(painter, curve, state.pen); painter.line(prepared.marker_first, prepared.marker_second, state.current_frequency_pen); }
template <typename Object, typename Owner, typename Member, typename State_Type> template <typename Object, typename Owner, typename Member, typename Prop_Type>
void Sweep_Spectrum::Private::after_state_set(Object* object, Member Owner::*, State_Access<State_Type>) { if constexpr (std::same_as<Owner, State>) object->template mark_dirty<Prepare_Data_Tag>(); } void Sweep_Spectrum::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type>
void Sweep_Spectrum::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { auto& state = pending_states.template get<Sweep_Spectrum::Base_Tag>(); const auto& prop = static_cast<const Prop&>(*current_prop); state.stored_block_count = prop.blocks.size(); state.stored_point_count = 0; for (const auto& block : prop.blocks) state.stored_point_count += block.size(); state.rendered_point_count = 0; for (const auto& partition : prepared.partitions) state.rendered_point_count += partition.points.size(); }
template <Attached Object> template <Attached Object>
const Sweep_Spectrum::Private::Dispatch& Sweep_Spectrum::Private::dispatch_for() { const Sweep_Spectrum::Private::Dispatch& Sweep_Spectrum::Private::dispatch_for() {
static const Dispatch value{ static const Dispatch value{
[](Root* root, std::span<const Plot_Value> values) { auto* object = static_cast<Object*>(root); object->template update_state<&State::blocks>([values](State_Access<typename Object::State> states) { auto& state = states.template get<Sweep_Spectrum_State_Tag>(); state.blocks.emplace_back(values.begin(), values.end()); const std::size_t limit = std::max<std::size_t>(1, state.block_count); while (state.blocks.size() > limit) state.blocks.erase(state.blocks.begin()); }); }, [](Root* root, std::span<const Plot_Value> values) { auto* object = static_cast<Object*>(root); object->template update_prop<&Prop::blocks>([values](Prop_Access<typename Object::Prop> props) { auto& state = props.template get<Sweep_Spectrum::Base_Tag>(); state.blocks.emplace_back(values.begin(), values.end()); const std::size_t limit = std::max<std::size_t>(1, state.block_count); while (state.blocks.size() > limit) state.blocks.erase(state.blocks.begin()); }); },
[](const Root* root) { return static_cast<const Object*>(root)->template read_state<Sweep_Spectrum_State_Tag>().blocks.size(); }, [](const Root* root) { return static_cast<const Object*>(root)->template read_prop<Sweep_Spectrum::Base_Tag>().blocks.size(); },
[](const Root* root) { const auto& blocks = static_cast<const Object*>(root)->template read_state<Sweep_Spectrum_State_Tag>().blocks; std::size_t count{}; for (const auto& block : blocks) count += block.size(); return count; }, [](const Root* root) { const auto& blocks = static_cast<const Object*>(root)->template read_prop<Sweep_Spectrum::Base_Tag>().blocks; std::size_t count{}; for (const auto& block : blocks) count += block.size(); return count; },
[](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); std::size_t count{}; for (const auto& curve : data.prepared.partitions) count += curve.points.size(); return count; } [](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); std::size_t count{}; for (const auto& curve : data.prepared.partitions) count += curve.points.size(); return count; }
}; return value; }; return value;
} }
+2 -1
View File
@@ -1,2 +1,3 @@
#include "Waterfall.hpp" #include "Waterfall.hpp"
namespace aethera::render_2d { bool Waterfall_Row::operator==(const Waterfall_Row&) const = default; bool Waterfall::State::operator==(const State&) const = default; void Waterfall::append_row(Plot_Time_Tick tick, std::span<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append(this, tick, values); } void Waterfall::append_row(Plot_Time_Tick tick, std::pmr::vector<Plot_Value>&& values) { append_row(tick, std::span<const Plot_Value>(values.data(), values.size())); } void Waterfall::append_row(Time_Of_Day time, std::span<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append_time(this, time, values); } void Waterfall::append_row(Time_Of_Day time, std::pmr::vector<Plot_Value>&& values) { append_row(time, std::span<const Plot_Value>(values.data(), values.size())); } std::size_t Waterfall::row_count() const { return static_cast<const Private&>(*d).dispatch->row_count(this); } std::size_t Waterfall::stored_point_count() const { return static_cast<const Private&>(*d).dispatch->point_count(this); } std::size_t Waterfall::rendered_cell_count() const { return static_cast<const Private&>(*d).dispatch->rendered_count(this); } } namespace aethera::render_2d { bool Waterfall_Row::operator==(const Waterfall_Row&) const = default; bool Waterfall::Prop::operator==(const Prop&) const = default;
bool Waterfall::State::operator==(const State&) const = default; void Waterfall::append_row(Plot_Time_Tick tick, std::span<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append(this, tick, values); } void Waterfall::append_row(Plot_Time_Tick tick, std::pmr::vector<Plot_Value>&& values) { append_row(tick, std::span<const Plot_Value>(values.data(), values.size())); } void Waterfall::append_row(Time_Of_Day time, std::span<const Plot_Value> values) { static_cast<Private&>(*d).dispatch->append_time(this, time, values); } void Waterfall::append_row(Time_Of_Day time, std::pmr::vector<Plot_Value>&& values) { append_row(time, std::span<const Plot_Value>(values.data(), values.size())); } std::size_t Waterfall::row_count() const { return static_cast<const Private&>(*d).dispatch->row_count(this); } std::size_t Waterfall::stored_point_count() const { return static_cast<const Private&>(*d).dispatch->point_count(this); } std::size_t Waterfall::rendered_cell_count() const { return static_cast<const Private&>(*d).dispatch->rendered_count(this); } }
+8 -4
View File
@@ -10,12 +10,10 @@
#include <span> #include <span>
#include <vector> #include <vector>
namespace aethera::render_2d { namespace aethera::render_2d {
struct Waterfall_State_Tag {};
struct Waterfall_Row { Plot_Time_Tick tick{}; std::vector<Plot_Value> values{}; bool operator==(const Waterfall_Row&) const; }; struct Waterfall_Row { Plot_Time_Tick tick{}; std::vector<Plot_Value> values{}; bool operator==(const Waterfall_Row&) const; };
struct Waterfall : Def<Waterfall, Renderable, State_Type<Waterfall_State_Tag>, Tagged_Buffer<Color_Cache, Blend2D_Cache>> { struct Waterfall : Def<Waterfall, Renderable, Tagged_Buffer<Color_Cache, Blend2D_Cache>> {
using Scene_Object = Impl<Render_Scene_2D>; using Frequency_Object = Impl<Frequency_Axis>; using Time_Object = Impl<Time_Axis>; using Scene_Object = Impl<Render_Scene_2D>; using Frequency_Object = Impl<Frequency_Axis>; using Time_Object = Impl<Time_Axis>;
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop, Hover_Tooltip_Properties {
struct State : Prev_State<Waterfall_State_Tag>, Hover_Tooltip_Properties {
Axis_Range frequency_range{0.0, 10.0}; /* 每行频谱覆盖的频率范围。 */ Axis_Range frequency_range{0.0, 10.0}; /* 每行频谱覆盖的频率范围。 */
Axis_Range power_range{0.0, 10.0}; /* 颜色映射使用的功率范围。 */ Axis_Range power_range{0.0, 10.0}; /* 颜色映射使用的功率范围。 */
std::size_t frequency_bin_count{}; /* 目标频率列数;零值使用最新行尺寸。 */ std::size_t frequency_bin_count{}; /* 目标频率列数;零值使用最新行尺寸。 */
@@ -25,6 +23,12 @@ struct Waterfall : Def<Waterfall, Renderable, State_Type<Waterfall_State_Tag>, T
Image_Interpolation_Mode interpolation_mode{Image_Interpolation_Mode::nearest}; /* 栅格放大时的图像插值方式。 */ Image_Interpolation_Mode interpolation_mode{Image_Interpolation_Mode::nearest}; /* 栅格放大时的图像插值方式。 */
Color_Map color_map{}; /* 功率到颜色的映射。 */ Color_Map color_map{}; /* 功率到颜色的映射。 */
std::vector<Waterfall_Row> rows{}; /* 从旧到新的瀑布行唯一权威集合。 */ std::vector<Waterfall_Row> rows{}; /* 从旧到新的瀑布行唯一权威集合。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
std::size_t row_count{}; /* 当前发布的瀑布行数。 */
std::size_t stored_point_count{}; /* 当前发布的功率点总数。 */
std::size_t rendered_cell_count{}; /* 最近一次 Prepare 生成的色块数。 */
bool operator==(const State&) const; bool operator==(const State&) const;
}; };
struct Private; struct Private;
+15 -13
View File
@@ -36,34 +36,36 @@ struct Waterfall::Private : Prev_Private {
void bind_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Time_Object* time_axis_value); void bind_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Time_Object* time_axis_value);
template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for(); template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for();
/* CRTP 覆盖:按当前色块工作量构建分块 Prepare 子图。 */ /* CRTP 覆盖:按当前色块工作量构建分块 Prepare 子图。 */
template <Attached Object> [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const State& state); template <Attached Object> [[nodiscard]] tf::Taskflow build_prepare_graph(Object* object, const Prop& state);
/* CRTP 覆盖:构建消费色块矩阵和提示信息的 Paint 子图。 */ /* CRTP 覆盖:构建消费色块矩阵和提示信息的 Paint 子图。 */
template <Attached Object> [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const State& state); template <Attached Object> [[nodiscard]] tf::Taskflow build_paint_graph(Object* object, const Prop& state);
/* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */ /* CRTP 覆盖:分块数量改变时请求重建 Prepare 子图。 */
template <Attached Object> [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const State& state); template <Attached Object> [[nodiscard]] bool should_rebuild_prepare_graph(Object* object, const Prop& state);
template <Attached Object> void prepare_frame(Object* object); template <Attached Object> void prepare_partition(Object* object, Plot_Partition_Count index); template <Attached Object> void paint_frame(Object* object); template <Attached Object> void prepare_frame(Object* object); template <Attached Object> void prepare_partition(Object* object, Plot_Partition_Count index); template <Attached Object> void paint_frame(Object* object);
/* CRTP 覆盖:更新 hover 位置并请求重绘。 */ /* CRTP 覆盖:更新 hover 位置并请求重绘。 */
template <Attached Object> void handle_event(Object* object, const Event& event); template <Attached Object> void handle_event(Object* object, const Event& event);
/* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */ /* CRTP 覆盖:本类状态写入后标记 Prepare 数据失效。 */
template <typename Object, typename Owner, typename Member, typename State_Type> void after_state_set(Object* object, Member Owner::* member, State_Access<State_Type> states); template <typename Object, typename Owner, typename Member, typename Prop_Type> void after_prop_set(Object* object, Member Owner::* member, Prop_Access<Prop_Type> states);
template <typename Object, typename Prop_Type, typename State_Type> void before_advance(Object* object, Prop_Type* pending_prop, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type> current_states);
}; };
template <typename Object> Waterfall::Builder<Object>::Builder(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) : Base(), scene(scene_value), frequency_axis(frequency_axis_value), time_axis(time_axis_value) {} template <typename Object> Waterfall::Builder<Object>::Builder(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) : Base(), scene(scene_value), frequency_axis(frequency_axis_value), time_axis(time_axis_value) {}
template <typename Object> template <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Waterfall::Builder<Object>::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast<typename Object::Private&>(*plot->d).bind_sources(scene, frequency_axis, time_axis); auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) { prepare.add(frequency_axis); prepare.add(time_axis); prepare.add(plot.get()); prepare.template add_state_dependency<Render_Scene_2D_State_Tag>(plot.get(), scene); prepare.template add_state_dependency<Abs_Axis_State_Tag>(plot.get(), frequency_axis); prepare.template add_state_dependency<Numeric_Axis_State_Tag>(plot.get(), frequency_axis); prepare.template add_state_dependency<Abs_Axis_State_Tag>(plot.get(), time_axis); prepare.template add_state_dependency<Time_Axis_State_Tag>(plot.get(), time_axis); paint.add(frequency_axis); paint.add(time_axis); paint.add(plot.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); } std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Waterfall::Builder<Object>::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto plot = std::move(result).value(); static_cast<typename Object::Private&>(*plot->d).bind_sources(scene, frequency_axis, time_axis); auto graph_result = scene->template edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) { prepare.add(frequency_axis); prepare.add(time_axis); prepare.add(plot.get()); prepare.template add_prop_dependency<Render_Scene_2D::Base_Tag>(plot.get(), scene); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(plot.get(), frequency_axis); prepare.template add_prop_dependency<Numeric_Axis::Base_Tag>(plot.get(), frequency_axis); prepare.template add_prop_dependency<Abs_Axis::Base_Tag>(plot.get(), time_axis); prepare.template add_prop_dependency<Time_Axis::Base_Tag>(plot.get(), time_axis); paint.add(frequency_axis); paint.add(time_axis); paint.add(plot.get()); }); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(plot); }
template <typename Values> void Waterfall::append_row(Plot_Time_Tick tick, const Values& values) { append_row(tick, std::span<const Plot_Value>(std::data(values), std::size(values))); } template <typename Values> void Waterfall::append_row(Plot_Time_Tick tick, const Values& values) { append_row(tick, std::span<const Plot_Value>(std::data(values), std::size(values))); }
template <typename Values> void Waterfall::append_row(Time_Of_Day time, const Values& values) { append_row(time, std::span<const Plot_Value>(std::data(values), std::size(values))); } template <typename Values> void Waterfall::append_row(Time_Of_Day time, const Values& values) { append_row(time, std::span<const Plot_Value>(std::data(values), std::size(values))); }
template <Attached Object> bool Waterfall::Private::should_rebuild_prepare_graph(Object*, const State& state) { const std::size_t cells = state.rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : state.rows.empty() ? 1 : state.rows.back().values.size()); return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, cells); } template <Attached Object> bool Waterfall::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { const std::size_t cells = state.rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : state.rows.empty() ? 1 : state.rows.back().values.size()); return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, cells); }
template <Attached Object> tf::Taskflow Waterfall::Private::build_prepare_graph(Object* object, const State& state) { const std::size_t cells = state.rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : state.rows.empty() ? 1 : state.rows.back().values.size()); graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, cells); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("waterfall.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("waterfall.prepare.partition"); begin.precede(task); } return graph; } template <Attached Object> tf::Taskflow Waterfall::Private::build_prepare_graph(Object* object, const Prop& state) { const std::size_t cells = state.rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : state.rows.empty() ? 1 : state.rows.back().values.size()); graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, cells); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("waterfall.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("waterfall.prepare.partition"); begin.precede(task); } return graph; }
template <Attached Object> tf::Taskflow Waterfall::Private::build_paint_graph(Object* object, const State&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("waterfall.paint.frame"); return graph; } template <Attached Object> tf::Taskflow Waterfall::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("waterfall.paint.frame"); return graph; }
template <Attached Object> template <Attached Object>
void Waterfall::Private::prepare_frame(Object* object) { const auto& state = object->template read_state<Waterfall_State_Tag>(); const auto& frequency_layout = frequency_axis->template read_state<Abs_Axis_State_Tag>(); const auto& time_layout = time_axis->template read_state<Abs_Axis_State_Tag>(); prepared = {}; prepared.canvas = scene->template read_state<Render_Scene_2D_State_Tag>().viewport; if (state.rows.empty()) return; const auto shortest = std::min_element(state.rows.begin(), state.rows.end(), [](const Waterfall_Row& left, const Waterfall_Row& right) { return left.values.size() < right.values.size(); }); const std::size_t available = shortest->values.size(); const int source_columns = static_cast<int>(state.frequency_bin_count ? std::min(state.frequency_bin_count, available) : available); const auto selection = detail::raster_axis_selection(state.frequency_range, frequency_axis->coordinate_range(), source_columns, state.visible_range_only); if (!selection) return; const int rows = static_cast<int>(state.rows.size()); const Axis_Range time_range = rows == 1 ? time_axis->coordinate_range() : Axis_Range{static_cast<Axis_Coordinate>(state.rows.front().tick), static_cast<Axis_Coordinate>(state.rows.back().tick)}; prepared.layout = detail::raster_layout(frequency_axis, selection->range, selection->count(), time_axis, time_range, rows, frequency_layout.orientation, time_layout.orientation); if (prepared.canvas.empty() || !prepared.layout.valid()) return; prepared.source_first = selection->first; prepared.pixels.assign(static_cast<std::size_t>(prepared.layout.width) * prepared.layout.height, 0); if (state.tooltip_enabled && tooltip.active && prepared.layout.target.contains(tooltip.position)) { std::ostringstream text; text << std::fixed << std::setprecision(2) << frequency_axis->point_to_coordinate(tooltip.position) << " Hz"; prepared.tooltip_text = text.str(); prepared.tooltip_box = {tooltip.position.x + 8.0, tooltip.position.y + 8.0, 110.0, 24.0}; } prepared.valid = true; } void Waterfall::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop<Waterfall::Base_Tag>(); const auto& frequency_layout = frequency_axis->template read_prop<Abs_Axis::Base_Tag>(); const auto& time_layout = time_axis->template read_prop<Abs_Axis::Base_Tag>(); prepared = {}; prepared.canvas = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport; if (state.rows.empty()) return; const auto shortest = std::min_element(state.rows.begin(), state.rows.end(), [](const Waterfall_Row& left, const Waterfall_Row& right) { return left.values.size() < right.values.size(); }); const std::size_t available = shortest->values.size(); const int source_columns = static_cast<int>(state.frequency_bin_count ? std::min(state.frequency_bin_count, available) : available); const auto selection = detail::raster_axis_selection(state.frequency_range, frequency_axis->coordinate_range(), source_columns, state.visible_range_only); if (!selection) return; const int rows = static_cast<int>(state.rows.size()); const Axis_Range time_range = rows == 1 ? time_axis->coordinate_range() : Axis_Range{static_cast<Axis_Coordinate>(state.rows.front().tick), static_cast<Axis_Coordinate>(state.rows.back().tick)}; prepared.layout = detail::raster_layout(frequency_axis, selection->range, selection->count(), time_axis, time_range, rows, frequency_layout.orientation, time_layout.orientation); if (prepared.canvas.empty() || !prepared.layout.valid()) return; prepared.source_first = selection->first; prepared.pixels.assign(static_cast<std::size_t>(prepared.layout.width) * prepared.layout.height, 0); if (state.tooltip_enabled && tooltip.active && prepared.layout.target.contains(tooltip.position)) { std::ostringstream text; text << std::fixed << std::setprecision(2) << frequency_axis->point_to_coordinate(tooltip.position) << " Hz"; prepared.tooltip_text = text.str(); prepared.tooltip_box = {tooltip.position.x + 8.0, tooltip.position.y + 8.0, 110.0, 24.0}; } prepared.valid = true; }
template <Attached Object> template <Attached Object>
void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_state<Waterfall_State_Tag>(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const std::size_t cells = static_cast<std::size_t>(columns) * state.rows.size(); const auto [first, last] = detail::raster_partition_range(cells, index, graph_partition_count); for (std::size_t cell = first; cell < last; ++cell) { const std::size_t row_index = cell / static_cast<std::size_t>(columns); const int column = static_cast<int>(cell % static_cast<std::size_t>(columns)); const auto& values = state.rows[row_index].values; const std::size_t source = static_cast<std::size_t>(prepared.source_first + column); prepared.pixels[prepared.layout.index(column, static_cast<int>(row_index))] = premultiply(state.color_map.sample(detail::normalized_plot_value(values[source], state.power_range))); } } void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop<Waterfall::Base_Tag>(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const std::size_t cells = static_cast<std::size_t>(columns) * state.rows.size(); const auto [first, last] = detail::raster_partition_range(cells, index, graph_partition_count); for (std::size_t cell = first; cell < last; ++cell) { const std::size_t row_index = cell / static_cast<std::size_t>(columns); const int column = static_cast<int>(cell % static_cast<std::size_t>(columns)); const auto& values = state.rows[row_index].values; const std::size_t source = static_cast<std::size_t>(prepared.source_first + column); prepared.pixels[prepared.layout.index(column, static_cast<int>(row_index))] = premultiply(state.color_map.sample(detail::normalized_plot_value(values[source], state.power_range))); } }
template <Attached Object> void Waterfall::Private::paint_frame(Object* object) { const auto& state = object->template read_state<Waterfall_State_Tag>(); auto& cache = object->template pending_buffer<Color_Cache>(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); detail::paint_raster(painter, prepared.layout, prepared.pixels, state.interpolation_mode); if (!prepared.tooltip_text.empty()) { painter.rect(prepared.tooltip_box, Pen{state.tooltip_text_pen.color}, state.tooltip_background_brush); painter.text({prepared.tooltip_box.x + 4.0, prepared.tooltip_box.y + 3.0}, prepared.tooltip_text, state.tooltip_font, state.tooltip_text_pen); } } template <Attached Object> void Waterfall::Private::paint_frame(Object* object) { const auto& state = object->template read_prop<Waterfall::Base_Tag>(); auto& cache = object->template pending_buffer<Color_Cache>(); cache.ensure_size(prepared.canvas); cache.clear(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); detail::paint_raster(painter, prepared.layout, prepared.pixels, state.interpolation_mode); if (!prepared.tooltip_text.empty()) { painter.rect(prepared.tooltip_box, Pen{state.tooltip_text_pen.color}, state.tooltip_background_brush); painter.text({prepared.tooltip_box.x + 4.0, prepared.tooltip_box.y + 3.0}, prepared.tooltip_text, state.tooltip_font, state.tooltip_text_pen); } }
template <Attached Object> void Waterfall::Private::handle_event(Object* object, const Event& event) { if (detail::update_hover_tooltip(tooltip, event)) object->template mark_dirty<Paint_Tag>(); } template <Attached Object> void Waterfall::Private::handle_event(Object* object, const Event& event) { if (detail::update_hover_tooltip(tooltip, event)) object->template mark_dirty<Paint_Tag>(); }
template <typename Object, typename Owner, typename Member, typename State_Type> void Waterfall::Private::after_state_set(Object* object, Member Owner::*, State_Access<State_Type>) { if constexpr (std::same_as<Owner, State>) object->template mark_dirty<Prepare_Data_Tag>(); } template <typename Object, typename Owner, typename Member, typename Prop_Type> void Waterfall::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) { if constexpr (std::same_as<Owner, Prop>) object->template mark_dirty<Prepare_Data_Tag>(); }
template <typename Object, typename Prop_Type, typename State_Type> void Waterfall::Private::before_advance(Object*, Prop_Type*, State_Access<State_Type> pending_states, const Prop_Type* current_prop, State_Access<const State_Type>) { auto& state = pending_states.template get<Waterfall::Base_Tag>(); const auto& prop = static_cast<const Prop&>(*current_prop); state.row_count = prop.rows.size(); state.stored_point_count = 0; for (const auto& row : prop.rows) state.stored_point_count += row.values.size(); state.rendered_cell_count = prepared.valid ? prepared.pixels.size() : 0; }
template <Attached Object> template <Attached Object>
const Waterfall::Private::Dispatch& Waterfall::Private::dispatch_for() { static const Dispatch value{[](Root* root, Plot_Time_Tick tick, std::span<const Plot_Value> values) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); const std::size_t row_limit = static_cast<std::size_t>(std::max<Axis_Visible_Count>(2, data.time_axis->template read_state<Time_Axis_State_Tag>().visible_count)); object->template update_state<&State::rows>([=](State_Access<typename Object::State> states) { auto& state = states.template get<Waterfall_State_Tag>(); state.rows.push_back({tick, {values.begin(), values.end()}}); while (state.rows.size() > row_limit) state.rows.erase(state.rows.begin()); }); }, [](Root* root, Time_Of_Day time, std::span<const Plot_Value> values) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); data.dispatch->append(root, data.time_axis->append_time(time), values); }, [](const Root* root) { return static_cast<const Object*>(root)->template read_state<Waterfall_State_Tag>().rows.size(); }, [](const Root* root) { const auto& rows = static_cast<const Object*>(root)->template read_state<Waterfall_State_Tag>().rows; std::size_t count{}; for (const auto& row : rows) count += row.values.size(); return count; }, [](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); return data.prepared.valid ? data.prepared.pixels.size() : 0; }}; return value; } const Waterfall::Private::Dispatch& Waterfall::Private::dispatch_for() { static const Dispatch value{[](Root* root, Plot_Time_Tick tick, std::span<const Plot_Value> values) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); const std::size_t row_limit = static_cast<std::size_t>(std::max<Axis_Visible_Count>(2, data.time_axis->template read_prop<Time_Axis::Base_Tag>().visible_count)); object->template update_prop<&Prop::rows>([=](Prop_Access<typename Object::Prop> props) { auto& state = props.template get<Waterfall::Base_Tag>(); state.rows.push_back({tick, {values.begin(), values.end()}}); while (state.rows.size() > row_limit) state.rows.erase(state.rows.begin()); }); }, [](Root* root, Time_Of_Day time, std::span<const Plot_Value> values) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); data.dispatch->append(root, data.time_axis->append_time(time), values); }, [](const Root* root) { return static_cast<const Object*>(root)->template read_prop<Waterfall::Base_Tag>().rows.size(); }, [](const Root* root) { const auto& rows = static_cast<const Object*>(root)->template read_prop<Waterfall::Base_Tag>().rows; std::size_t count{}; for (const auto& row : rows) count += row.values.size(); return count; }, [](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); return data.prepared.valid ? data.prepared.pixels.size() : 0; }}; return value; }
template <Attached Object> void Waterfall::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for<Object>(); } template <Attached Object> void Waterfall::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for<Object>(); }
inline void Waterfall::Private::bind_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) { scene = scene_value; frequency_axis = frequency_axis_value; time_axis = time_axis_value; } inline void Waterfall::Private::bind_sources(Scene_Object* scene_value, Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) { scene = scene_value; frequency_axis = frequency_axis_value; time_axis = time_axis_value; }
} }
@@ -1,6 +1,7 @@
#include "Render_Scene_2D.hpp" #include "Render_Scene_2D.hpp"
namespace aethera::render_2d { namespace aethera::render_2d {
bool Render_Scene_2D::State::operator==(const State&) const = default; bool Render_Scene_2D::State::operator==(const State&) const = default;
bool Render_Scene_2D::Prop::operator==(const Prop&) const = default;
Render_Frame_Status Render_Scene_2D::render_frame() { Render_Frame_Status Render_Scene_2D::render_frame() {
return static_cast<Private&>(*d).dispatch->render_frame(this); return static_cast<Private&>(*d).dispatch->render_frame(this);
@@ -3,7 +3,6 @@
#include "../render/Blend2D_Cache.hpp" #include "../render/Blend2D_Cache.hpp"
#include <scene.hpp> #include <scene.hpp>
namespace aethera::render_2d { namespace aethera::render_2d {
struct Render_Scene_2D_State_Tag {};
struct Scene_Color_Cache_Tag {}; struct Scene_Color_Cache_Tag {};
enum class Render_Frame_Status : std::uint8_t { enum class Render_Frame_Status : std::uint8_t {
rendered, rendered,
@@ -11,13 +10,15 @@ enum class Render_Frame_Status : std::uint8_t {
empty_viewport empty_viewport
}; };
/* 执行二维 Renderable 图、合成颜色层并发布最终像素帧。 */ /* 执行二维 Renderable 图、合成颜色层并发布最终像素帧。 */
struct Render_Scene_2D : Def<Render_Scene_2D, Scene, State_Type<Render_Scene_2D_State_Tag>, struct Render_Scene_2D : Def<Render_Scene_2D, Scene,
Tagged_Buffer<Scene_Color_Cache_Tag, Blend2D_Cache>> { Tagged_Buffer<Scene_Color_Cache_Tag, Blend2D_Cache>> {
struct Prop : Prev_Prop {}; struct Prop : Prev_Prop {
struct State : Prev_State<Render_Scene_2D_State_Tag> {
Size viewport{}; /* 最终帧的像素尺寸;空尺寸不执行渲染。 */ Size viewport{}; /* 最终帧的像素尺寸;空尺寸不执行渲染。 */
Color background{Color::black()}; /* 每帧合成前写入的背景颜色。 */ Color background{Color::black()}; /* 每帧合成前写入的背景颜色。 */
bool view_active{}; /* 视图是否接受 render_frame 请求。 */ bool view_active{}; /* 视图是否接受 render_frame 请求。 */
bool operator==(const Prop&) const;
};
struct State : Prev_State {
bool operator==(const State&) const; bool operator==(const State&) const;
}; };
/* 完整声明、合成顺序和 CRTP 分派见 Render_Scene_2D.ipp。 */ /* 完整声明、合成顺序和 CRTP 分派见 Render_Scene_2D.ipp。 */
@@ -30,7 +30,7 @@ struct Render_Scene_2D::Private : Prev_Private {
template <Attached Object, typename Callback> template <Attached Object, typename Callback>
void Render_Scene_2D::Private::process(Object* object, Callback&& callback) void Render_Scene_2D::Private::process(Object* object, Callback&& callback)
requires std::invocable<Callback, const Render_Frame_Status&> { requires std::invocable<Callback, const Render_Frame_Status&> {
const auto& state = static_cast<const State&>(*static_cast<typename Object::Private&>(*this).state.current); const auto& state = static_cast<const Prop&>(*static_cast<typename Object::Private&>(*this).current);
if (!state.view_active) { if (!state.view_active) {
const Render_Frame_Status status = Render_Frame_Status::view_inactive; const Render_Frame_Status status = Render_Frame_Status::view_inactive;
std::invoke(std::forward<Callback>(callback), status); std::invoke(std::forward<Callback>(callback), status);
@@ -92,7 +92,7 @@ const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for
static_cast<typename Object::Private&>(*object->d).dispatch_event(object, event); static_cast<typename Object::Private&>(*object->d).dispatch_event(object, event);
}, },
[](Root* root, bool active) { [](Root* root, bool active) {
static_cast<Object*>(root)->template update_state<&State::view_active>(active); static_cast<Object*>(root)->template set<&Prop::view_active>(active);
}, },
[](const Root* root) { [](const Root* root) {
const auto* object = static_cast<const Object*>(root); const auto* object = static_cast<const Object*>(root);
+14 -14
View File
@@ -15,9 +15,9 @@ std::unique_ptr<Object> build_axis() {
TEST(axis_dispatch, numeric_axis_public_shell_reads_final_private_state) { TEST(axis_dispatch, numeric_axis_public_shell_reads_final_private_state) {
using Object = Impl<Numeric_Axis>; using Object = Impl<Numeric_Axis>;
auto axis = build_axis<Object>(); auto axis = build_axis<Object>();
axis->update_state<&Abs_Axis::State::position>(Point_F{10.0, 0.0}); axis->set<&Abs_Axis::Prop::position>(Point_F{10.0, 0.0});
axis->update_state<&Abs_Axis::State::pixel_length>(200.0); axis->set<&Abs_Axis::Prop::pixel_length>(200.0);
axis->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{0.0, 20.0}); axis->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 20.0});
axis->advance(); axis->advance();
EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.0, 20.0})); EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.0, 20.0}));
EXPECT_DOUBLE_EQ(axis->coordinate_to_pixel(5.0), 60.0); EXPECT_DOUBLE_EQ(axis->coordinate_to_pixel(5.0), 60.0);
@@ -30,10 +30,10 @@ TEST(axis_dispatch, numeric_axis_public_shell_reads_final_private_state) {
TEST(axis_dispatch, point_conversion_uses_current_orientation) { TEST(axis_dispatch, point_conversion_uses_current_orientation) {
using Object = Impl<Numeric_Axis>; using Object = Impl<Numeric_Axis>;
auto axis = build_axis<Object>(); auto axis = build_axis<Object>();
axis->update_state<&Abs_Axis::State::position>(Point_F{0.0, 20.0}); axis->set<&Abs_Axis::Prop::position>(Point_F{0.0, 20.0});
axis->update_state<&Abs_Axis::State::pixel_length>(100.0); axis->set<&Abs_Axis::Prop::pixel_length>(100.0);
axis->update_state<&Abs_Axis::State::orientation>(Axis_Orientation::vertical); axis->set<&Abs_Axis::Prop::orientation>(Axis_Orientation::vertical);
axis->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{0.0, 10.0}); axis->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 10.0});
axis->advance(); axis->advance();
EXPECT_DOUBLE_EQ(axis->point_to_coordinate({90.0, 70.0}), 5.0); EXPECT_DOUBLE_EQ(axis->point_to_coordinate({90.0, 70.0}), 5.0);
} }
@@ -60,9 +60,9 @@ TEST(axis_render, scene_prepares_and_paints_axis_cache) {
initialize_runtime(2); initialize_runtime(2);
auto axis = build_axis<Object>(); auto axis = build_axis<Object>();
auto scene = build_axis<Scene_Object>(); auto scene = build_axis<Scene_Object>();
axis->update_state<&Abs_Axis::State::position>(Point_F{8.0, 8.0}); axis->set<&Abs_Axis::Prop::position>(Point_F{8.0, 8.0});
axis->update_state<&Abs_Axis::State::canvas_size>(Size{160, 64}); axis->set<&Abs_Axis::Prop::canvas_size>(Size{160, 64});
axis->update_state<&Abs_Axis::State::pixel_length>(120.0); axis->set<&Abs_Axis::Prop::pixel_length>(120.0);
ASSERT_TRUE((scene->edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) { ASSERT_TRUE((scene->edit_dependency_graph<Prepare_Data_Tag, Paint_Tag>([&](auto& prepare, auto& paint) {
prepare.add(axis.get()); prepare.add(axis.get());
paint.add(axis.get()); paint.add(axis.get());
@@ -80,9 +80,9 @@ TEST(axis_render, scene_prepares_and_paints_axis_cache) {
TEST(axis_event, numeric_axis_wheel_zoom_and_drag_update_authoritative_range) { TEST(axis_event, numeric_axis_wheel_zoom_and_drag_update_authoritative_range) {
using Object = Impl<Numeric_Axis>; using Object = Impl<Numeric_Axis>;
auto axis = build_axis<Object>(); auto axis = build_axis<Object>();
axis->update_state<&Abs_Axis::State::position>(Point_F{0.0, 0.0}); axis->set<&Abs_Axis::Prop::position>(Point_F{0.0, 0.0});
axis->update_state<&Abs_Axis::State::pixel_length>(100.0); axis->set<&Abs_Axis::Prop::pixel_length>(100.0);
axis->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{0.0, 10.0}); axis->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 10.0});
axis->advance(); axis->advance();
Wheel_Event wheel; Wheel_Event wheel;
wheel.position = {50.0, 0.0}; wheel.position = {50.0, 0.0};
@@ -105,7 +105,7 @@ TEST(axis_event, numeric_axis_wheel_zoom_and_drag_update_authoritative_range) {
TEST(axis_state, invalid_numeric_range_is_rejected_without_poisoning_pending_state) { TEST(axis_state, invalid_numeric_range_is_rejected_without_poisoning_pending_state) {
using Object = Impl<Numeric_Axis>; using Object = Impl<Numeric_Axis>;
auto axis = build_axis<Object>(); auto axis = build_axis<Object>();
EXPECT_THROW((axis->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{1.0, 1.0})), std::invalid_argument); EXPECT_THROW((axis->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{1.0, 1.0})), std::invalid_argument);
axis->advance(); axis->advance();
EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.0, 20.0})); EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.0, 20.0}));
} }
+28 -28
View File
@@ -17,10 +17,10 @@ std::unique_ptr<Object> build_object(Args&&... args) {
template <typename Axis> template <typename Axis>
void configure_axis(Axis* axis, Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length, Size canvas) { void configure_axis(Axis* axis, Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length, Size canvas) {
axis->template update_state<&Abs_Axis::State::orientation>(orientation); axis->template set<&Abs_Axis::Prop::orientation>(orientation);
axis->template update_state<&Abs_Axis::State::position>(position); axis->template set<&Abs_Axis::Prop::position>(position);
axis->template update_state<&Abs_Axis::State::pixel_length>(length); axis->template set<&Abs_Axis::Prop::pixel_length>(length);
axis->template update_state<&Abs_Axis::State::canvas_size>(canvas); axis->template set<&Abs_Axis::Prop::canvas_size>(canvas);
} }
} }
@@ -42,22 +42,22 @@ TEST(plottable_migration, curve_plots_share_partitioned_rendering) {
configure_axis(power.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas); configure_axis(power.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas);
configure_axis(time.get(), Axis_Orientation::horizontal, {20.0, 100.0}, 120.0, canvas); configure_axis(time.get(), Axis_Orientation::horizontal, {20.0, 100.0}, 120.0, canvas);
configure_axis(value.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas); configure_axis(value.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas);
frequency->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{0.0, 100.0}); frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0});
power->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{-100.0, 0.0}); power->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-100.0, 0.0});
value->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{0.0, 100.0}); value->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0});
auto trace = build_object<Trace>(scene.get(), time.get(), value.get()); auto trace = build_object<Trace>(scene.get(), time.get(), value.get());
auto sweep = build_object<Sweep>(scene.get(), frequency.get(), power.get()); auto sweep = build_object<Sweep>(scene.get(), frequency.get(), power.get());
trace->update_state<&Frequency_Trace::State::partition_mode>(Plot_Partition_Mode::fixed); trace->set<&Frequency_Trace::Prop::partition_mode>(Plot_Partition_Mode::fixed);
trace->update_state<&Frequency_Trace::State::partition_count>(2u); trace->set<&Frequency_Trace::Prop::partition_count>(2u);
trace->append_sample(0, 10.0); trace->append_sample(0, 10.0);
trace->append_sample(1, 50.0); trace->append_sample(1, 50.0);
trace->append_sample(2, 90.0); trace->append_sample(2, 90.0);
sweep->update_state<&Sweep_Spectrum::State::frequency_range>(Axis_Range{0.0, 100.0}); sweep->set<&Sweep_Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0});
sweep->update_state<&Sweep_Spectrum::State::partition_mode>(Plot_Partition_Mode::fixed); sweep->set<&Sweep_Spectrum::Prop::partition_mode>(Plot_Partition_Mode::fixed);
sweep->update_state<&Sweep_Spectrum::State::partition_count>(2u); sweep->set<&Sweep_Spectrum::Prop::partition_count>(2u);
const std::array<Plot_Value, 4> block{-90.0, -60.0, -30.0, -10.0}; const std::array<Plot_Value, 4> block{-90.0, -60.0, -30.0, -10.0};
sweep->append_block(block); sweep->append_block(block);
scene->update_state<&Render_Scene_2D::State::viewport>(canvas); scene->set<&Render_Scene_2D::Prop::viewport>(canvas);
scene->activate_view(); scene->activate_view();
EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered);
EXPECT_EQ(trace->sample_count(), 3u); EXPECT_EQ(trace->sample_count(), 3u);
@@ -82,20 +82,20 @@ TEST(plottable_migration, raster_plots_share_partitioned_color_blocks) {
configure_axis(frequency.get(), Axis_Orientation::horizontal, {20.0, 100.0}, 120.0, canvas); configure_axis(frequency.get(), Axis_Orientation::horizontal, {20.0, 100.0}, 120.0, canvas);
configure_axis(power.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas); configure_axis(power.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas);
configure_axis(time.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas); configure_axis(time.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas);
frequency->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{0.0, 100.0}); frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0});
power->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{-100.0, 0.0}); power->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-100.0, 0.0});
auto glow = build_object<Glow>(scene.get(), frequency.get(), power.get()); auto glow = build_object<Glow>(scene.get(), frequency.get(), power.get());
auto waterfall = build_object<Fall>(scene.get(), frequency.get(), time.get()); auto waterfall = build_object<Fall>(scene.get(), frequency.get(), time.get());
glow->update_state<&Afterglow::State::frequency_range>(Axis_Range{0.0, 100.0}); glow->set<&Afterglow::Prop::frequency_range>(Axis_Range{0.0, 100.0});
glow->update_state<&Afterglow::State::power_range>(Axis_Range{-100.0, 0.0}); glow->set<&Afterglow::Prop::power_range>(Axis_Range{-100.0, 0.0});
glow->update_state<&Afterglow::State::power_point_size>(16u); glow->set<&Afterglow::Prop::power_point_size>(16u);
waterfall->update_state<&Waterfall::State::frequency_range>(Axis_Range{0.0, 100.0}); waterfall->set<&Waterfall::Prop::frequency_range>(Axis_Range{0.0, 100.0});
waterfall->update_state<&Waterfall::State::power_range>(Axis_Range{-100.0, 0.0}); waterfall->set<&Waterfall::Prop::power_range>(Axis_Range{-100.0, 0.0});
const std::array<Plot_Value, 4> row{-90.0, -60.0, -30.0, -10.0}; const std::array<Plot_Value, 4> row{-90.0, -60.0, -30.0, -10.0};
glow->append_spectrum(row); glow->append_spectrum(row);
waterfall->append_row(0, row); waterfall->append_row(0, row);
waterfall->append_row(1, row); waterfall->append_row(1, row);
scene->update_state<&Render_Scene_2D::State::viewport>(canvas); scene->set<&Render_Scene_2D::Prop::viewport>(canvas);
scene->activate_view(); scene->activate_view();
EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered);
EXPECT_EQ(glow->history_count(), 1u); EXPECT_EQ(glow->history_count(), 1u);
@@ -116,17 +116,17 @@ TEST(plottable_migration, direct_overlay_and_constellation_build_and_render) {
auto vertical = build_object<Numeric>(); auto vertical = build_object<Numeric>();
configure_axis(horizontal.get(), Axis_Orientation::horizontal, {20.0, 100.0}, 120.0, canvas); configure_axis(horizontal.get(), Axis_Orientation::horizontal, {20.0, 100.0}, 120.0, canvas);
configure_axis(vertical.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas); configure_axis(vertical.get(), Axis_Orientation::vertical, {20.0, 100.0}, -80.0, canvas);
horizontal->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{-1.0, 1.0}); horizontal->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-1.0, 1.0});
vertical->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{-1.0, 1.0}); vertical->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-1.0, 1.0});
auto diagram = build_object<Diagram>(scene.get(), horizontal.get(), vertical.get()); auto diagram = build_object<Diagram>(scene.get(), horizontal.get(), vertical.get());
auto overlay = build_object<Overlay>(scene.get(), horizontal.get(), vertical.get()); auto overlay = build_object<Overlay>(scene.get(), horizontal.get(), vertical.get());
diagram->update_state<&Constellation_Diagram::State::i_range>(Axis_Range{-1.0, 1.0}); diagram->set<&Constellation_Diagram::Prop::i_range>(Axis_Range{-1.0, 1.0});
diagram->update_state<&Constellation_Diagram::State::q_range>(Axis_Range{-1.0, 1.0}); diagram->set<&Constellation_Diagram::Prop::q_range>(Axis_Range{-1.0, 1.0});
diagram->append_point({0.25, -0.25}); diagram->append_point({0.25, -0.25});
scene->update_state<&Render_Scene_2D::State::viewport>(canvas); scene->set<&Render_Scene_2D::Prop::viewport>(canvas);
scene->activate_view(); scene->activate_view();
EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered);
EXPECT_EQ(diagram->point_count(), 1u); EXPECT_EQ(diagram->point_count(), 1u);
EXPECT_EQ(overlay->read_state<Renderable_State_Tag>().prepare_task_count, 0u); EXPECT_EQ(overlay->read_state<Renderable::Base_Tag>().prepare_task_count, 0u);
EXPECT_EQ(overlay->read_state<Renderable_State_Tag>().paint_task_count, 1u); EXPECT_EQ(overlay->read_state<Renderable::Base_Tag>().paint_task_count, 1u);
} }
+23 -23
View File
@@ -32,7 +32,7 @@ TEST(spectrum_data, publishes_samples_and_interpolates_power) {
const auto missing = spectrum->power_at(25.0); const auto missing = spectrum->power_at(25.0);
ASSERT_FALSE(missing.has_value()); ASSERT_FALSE(missing.has_value());
EXPECT_EQ(missing.error(), Spectrum::Power_At_Result::no_samples); EXPECT_EQ(missing.error(), Spectrum::Power_At_Result::no_samples);
spectrum->update_state<&Spectrum::State::frequency_range>(Axis_Range{0.0, 100.0}); spectrum->set<&Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0});
const double samples[]{-100.0, -50.0, 0.0}; const double samples[]{-100.0, -50.0, 0.0};
spectrum->update_samples(samples); spectrum->update_samples(samples);
spectrum->advance(); spectrum->advance();
@@ -84,24 +84,24 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) {
auto power = build_object<Power>(); auto power = build_object<Power>();
auto spectrum = build_object<Spectrum_Object>(scene.get(), frequency.get(), power.get()); auto spectrum = build_object<Spectrum_Object>(scene.get(), frequency.get(), power.get());
const Size canvas{160, 120}; const Size canvas{160, 120};
frequency->update_state<&Abs_Axis::State::position>(Point_F{20.0, 100.0}); frequency->set<&Abs_Axis::Prop::position>(Point_F{20.0, 100.0});
frequency->update_state<&Abs_Axis::State::canvas_size>(canvas); frequency->set<&Abs_Axis::Prop::canvas_size>(canvas);
frequency->update_state<&Abs_Axis::State::pixel_length>(120.0); frequency->set<&Abs_Axis::Prop::pixel_length>(120.0);
frequency->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{0.0, 100.0}); frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0});
power->update_state<&Abs_Axis::State::position>(Point_F{20.0, 100.0}); power->set<&Abs_Axis::Prop::position>(Point_F{20.0, 100.0});
power->update_state<&Abs_Axis::State::canvas_size>(canvas); power->set<&Abs_Axis::Prop::canvas_size>(canvas);
power->update_state<&Abs_Axis::State::pixel_length>(-80.0); power->set<&Abs_Axis::Prop::pixel_length>(-80.0);
power->update_state<&Abs_Axis::State::orientation>(Axis_Orientation::vertical); power->set<&Abs_Axis::Prop::orientation>(Axis_Orientation::vertical);
power->update_state<&Numeric_Axis::State::coordinate_range>(Axis_Range{-100.0, 0.0}); power->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-100.0, 0.0});
spectrum->update_state<&Spectrum::State::frequency_range>(Axis_Range{0.0, 100.0}); spectrum->set<&Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0});
spectrum->update_state<&Spectrum::State::partition_mode>(Spectrum_Partition_Mode::fixed); spectrum->set<&Spectrum::Prop::partition_mode>(Spectrum_Partition_Mode::fixed);
spectrum->update_state<&Spectrum::State::partition_count>(3u); spectrum->set<&Spectrum::Prop::partition_count>(3u);
spectrum->update_state<&Spectrum::State::sweep_region_visible>(true); spectrum->set<&Spectrum::Prop::sweep_region_visible>(true);
spectrum->update_state<&Spectrum::State::max_hold_visible>(true); spectrum->set<&Spectrum::Prop::max_hold_visible>(true);
const double samples[]{-90.0, -65.0, -20.0, -45.0, -75.0}; const double samples[]{-90.0, -65.0, -20.0, -45.0, -75.0};
spectrum->update_samples(samples); spectrum->update_samples(samples);
scene->update_state<&Render_Scene_2D::State::viewport>(canvas); scene->set<&Render_Scene_2D::Prop::viewport>(canvas);
scene->update_state<&Render_Scene_2D::State::background>(Color::transparent()); scene->set<&Render_Scene_2D::Prop::background>(Color::transparent());
scene->activate_view(); scene->activate_view();
const auto prepare_graph = scene->pending_dependency_graph<Prepare_Data_Tag>(); const auto prepare_graph = scene->pending_dependency_graph<Prepare_Data_Tag>();
EXPECT_TRUE(prepare_graph.depends_on(spectrum.get(), scene.get())); EXPECT_TRUE(prepare_graph.depends_on(spectrum.get(), scene.get()));
@@ -109,23 +109,23 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) {
EXPECT_TRUE(prepare_graph.depends_on(spectrum.get(), power.get())); EXPECT_TRUE(prepare_graph.depends_on(spectrum.get(), power.get()));
EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered);
EXPECT_GT(spectrum->rendered_point_count(), 0u); EXPECT_GT(spectrum->rendered_point_count(), 0u);
const auto& render_state = spectrum->read_state<Renderable_State_Tag>(); const auto& render_state = spectrum->read_state<Renderable::Base_Tag>();
EXPECT_EQ(render_state.prepare_task_count, 4u); EXPECT_EQ(render_state.prepare_task_count, 4u);
EXPECT_EQ(render_state.paint_task_count, 1u); EXPECT_EQ(render_state.paint_task_count, 1u);
const Image_View frame = scene->frame_view(); const Image_View frame = scene->frame_view();
ASSERT_FALSE(frame.empty()); ASSERT_FALSE(frame.empty());
EXPECT_TRUE(contains_color(frame)); EXPECT_TRUE(contains_color(frame));
spectrum->update_state<&Spectrum::State::partition_count>(2u); spectrum->set<&Spectrum::Prop::partition_count>(2u);
spectrum->update_samples(samples); spectrum->update_samples(samples);
EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered);
const auto& rebuilt_state = spectrum->read_state<Renderable_State_Tag>(); const auto& rebuilt_state = spectrum->read_state<Renderable::Base_Tag>();
EXPECT_TRUE(rebuilt_state.prepare_graph_rebuilt); EXPECT_TRUE(rebuilt_state.prepare_graph_rebuilt);
EXPECT_EQ(rebuilt_state.prepare_task_count, 3u); EXPECT_EQ(rebuilt_state.prepare_task_count, 3u);
EXPECT_TRUE(contains_color(scene->frame_view())); EXPECT_TRUE(contains_color(scene->frame_view()));
const Size resized_canvas{200, 140}; const Size resized_canvas{200, 140};
scene->update_state<&Render_Scene_2D::State::viewport>(resized_canvas); scene->set<&Render_Scene_2D::Prop::viewport>(resized_canvas);
frequency->update_state<&Abs_Axis::State::canvas_size>(resized_canvas); frequency->set<&Abs_Axis::Prop::canvas_size>(resized_canvas);
power->update_state<&Abs_Axis::State::canvas_size>(resized_canvas); power->set<&Abs_Axis::Prop::canvas_size>(resized_canvas);
EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered); EXPECT_EQ(scene->render_frame(), Render_Frame_Status::rendered);
const Image_View resized_frame = scene->frame_view(); const Image_View resized_frame = scene->frame_view();
EXPECT_EQ(resized_frame.width, resized_canvas.width); EXPECT_EQ(resized_frame.width, resized_canvas.width);