Checkpoint 2: integrate native Editor, MCP, gameplay builds and standalone export
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace faset::assets {
|
||||
using Json = nlohmann::json;
|
||||
struct Vertex {
|
||||
std::array<float, 3> position{};
|
||||
std::array<float, 3> normal{0, 0, 1};
|
||||
std::array<float, 2> uv{};
|
||||
};
|
||||
struct Primitive {
|
||||
std::vector<Vertex> vertices;
|
||||
std::vector<std::uint32_t> indices;
|
||||
int material = -1;
|
||||
};
|
||||
struct Mesh {
|
||||
std::string id, name;
|
||||
std::vector<Primitive> primitives;
|
||||
};
|
||||
struct Node {
|
||||
std::string id, name, parent_id;
|
||||
int mesh = -1;
|
||||
// glTF right-handed, Y-up, metres; column-major matrix.
|
||||
std::array<float, 16> local_transform{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
|
||||
bool stable_source_id = false;
|
||||
};
|
||||
struct Material {
|
||||
std::string id, name;
|
||||
std::array<float, 4> base_color{1, 1, 1, 1};
|
||||
std::array<float, 3> emissive{};
|
||||
float metallic = 1, roughness = 1, alpha_cutoff = 0.5f;
|
||||
std::string alpha_mode = "OPAQUE";
|
||||
bool double_sided = false, unlit = false;
|
||||
int base_color_texture = -1, metallic_roughness_texture = -1;
|
||||
int normal_texture = -1, occlusion_texture = -1, emissive_texture = -1;
|
||||
};
|
||||
struct Texture {
|
||||
std::string id, name, mime_type;
|
||||
// Encoded image bytes, owned by this value. Renderer selects an image decoder.
|
||||
std::vector<std::byte> bytes;
|
||||
int wrap_s = 10497, wrap_t = 10497, min_filter = 0, mag_filter = 0;
|
||||
};
|
||||
struct CookedAsset {
|
||||
std::string asset_id, generation;
|
||||
std::vector<Mesh> meshes;
|
||||
std::vector<Node> nodes;
|
||||
std::vector<Material> materials;
|
||||
std::vector<Texture> textures;
|
||||
};
|
||||
|
||||
// Read-only cooked storage. No source parser, import jobs or publication operations.
|
||||
class AssetStore {
|
||||
public:
|
||||
explicit AssetStore(std::filesystem::path cache_root);
|
||||
Json current_manifest(const std::string& asset_id) const;
|
||||
CookedAsset load_asset(const std::string& asset_id) const;
|
||||
std::filesystem::path generation_directory(const std::string& asset_id) const;
|
||||
const std::filesystem::path& cache_root() const noexcept {
|
||||
return cache_root_;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::filesystem::path cache_root_;
|
||||
};
|
||||
} // namespace faset::assets
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include <faset/assets/asset_data.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
@@ -7,69 +8,28 @@
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace faset::assets {
|
||||
using Json = nlohmann::json;
|
||||
inline constexpr const char* importer_version = "faset-gltf-1/cgltf-1.15";
|
||||
|
||||
struct Vertex {
|
||||
std::array<float, 3> position{};
|
||||
std::array<float, 3> normal{0, 0, 1};
|
||||
std::array<float, 2> uv{};
|
||||
struct ImportProgress {
|
||||
float fraction = 0;
|
||||
std::string stage;
|
||||
};
|
||||
struct Primitive {
|
||||
std::vector<Vertex> vertices;
|
||||
std::vector<std::uint32_t> indices;
|
||||
int material = -1;
|
||||
};
|
||||
struct Mesh {
|
||||
std::string id, name;
|
||||
std::vector<Primitive> primitives;
|
||||
};
|
||||
struct Node {
|
||||
std::string id, name, parent_id;
|
||||
int mesh = -1;
|
||||
// glTF right-handed, Y-up, metres; column-major matrix.
|
||||
std::array<float, 16> local_transform{1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
|
||||
bool stable_source_id = false;
|
||||
};
|
||||
struct Material {
|
||||
std::string id, name;
|
||||
std::array<float, 4> base_color{1,1,1,1};
|
||||
std::array<float, 3> emissive{};
|
||||
float metallic = 1, roughness = 1, alpha_cutoff = 0.5f;
|
||||
std::string alpha_mode = "OPAQUE";
|
||||
bool double_sided = false, unlit = false;
|
||||
int base_color_texture = -1, metallic_roughness_texture = -1;
|
||||
int normal_texture = -1, occlusion_texture = -1, emissive_texture = -1;
|
||||
};
|
||||
struct Texture {
|
||||
std::string id, name, mime_type;
|
||||
// Encoded image bytes, owned by this value. Renderer selects an image decoder.
|
||||
std::vector<std::byte> bytes;
|
||||
int wrap_s = 10497, wrap_t = 10497, min_filter = 0, mag_filter = 0;
|
||||
};
|
||||
struct CookedAsset {
|
||||
std::string asset_id, generation;
|
||||
std::vector<Mesh> meshes;
|
||||
std::vector<Node> nodes;
|
||||
std::vector<Material> materials;
|
||||
std::vector<Texture> textures;
|
||||
};
|
||||
|
||||
struct ImportProgress { float fraction = 0; std::string stage; };
|
||||
class ImportJob {
|
||||
public:
|
||||
public:
|
||||
using Observer = std::function<void(const ImportProgress&)>;
|
||||
explicit ImportJob(Observer observer = {});
|
||||
void cancel() noexcept;
|
||||
bool cancelled() const noexcept;
|
||||
ImportProgress progress() const;
|
||||
void report(float fraction, std::string stage);
|
||||
private:
|
||||
|
||||
private:
|
||||
std::atomic<bool> cancelled_{false};
|
||||
mutable std::mutex mutex_;
|
||||
ImportProgress progress_;
|
||||
@@ -79,7 +39,7 @@ private:
|
||||
enum class ImportStatus { succeeded, failed, cancelled, conflict };
|
||||
struct ImportRequest {
|
||||
std::filesystem::path source;
|
||||
std::string asset_id{}; // Empty: restore/create source.faset-import.json identity.
|
||||
std::string asset_id{}; // Empty: restore/create source.faset-import.json identity.
|
||||
Json settings = nullptr; // Null restores the sidecar recipe; an object replaces it.
|
||||
// Explicit conflict resolution; false keeps the previous generation active.
|
||||
bool allow_removed_outputs = false;
|
||||
@@ -91,24 +51,20 @@ struct ImportResult {
|
||||
std::vector<std::string> removed_output_ids;
|
||||
Json manifest;
|
||||
bool cache_hit = false;
|
||||
bool ok() const noexcept { return status == ImportStatus::succeeded; }
|
||||
bool ok() const noexcept {
|
||||
return status == ImportStatus::succeeded;
|
||||
}
|
||||
};
|
||||
|
||||
// A pipeline is an authoring service. Player only needs read-only cooked data.
|
||||
// Writers in one process serialize publication; a cache root has one service owner.
|
||||
class AssetPipeline {
|
||||
public:
|
||||
class AssetPipeline : public AssetStore {
|
||||
public:
|
||||
explicit AssetPipeline(std::filesystem::path cache_root);
|
||||
ImportResult import_asset(const ImportRequest& request, ImportJob& job);
|
||||
ImportResult import_asset(const ImportRequest& request);
|
||||
Json current_manifest(const std::string& asset_id) const;
|
||||
CookedAsset load_asset(const std::string& asset_id) const;
|
||||
// Overrides are authoring data beside the source, never generated cache contents.
|
||||
Json overrides(const std::string& asset_id) const;
|
||||
void set_overrides(const std::string& asset_id, const Json& overrides);
|
||||
std::filesystem::path generation_directory(const std::string& asset_id) const;
|
||||
const std::filesystem::path& cache_root() const noexcept { return cache_root_; }
|
||||
private:
|
||||
std::filesystem::path cache_root_;
|
||||
};
|
||||
} // namespace faset::assets
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#pragma once
|
||||
#include <faset/core/json.hpp>
|
||||
#include <faset/core/error.hpp>
|
||||
#include <faset/core/json.hpp>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
namespace faset::authoring {
|
||||
class SchemaRegistry {
|
||||
public:
|
||||
public:
|
||||
void register_schema(const Json& schema);
|
||||
void register_schemas(const Json& schemas);
|
||||
bool contains(const std::string& type) const;
|
||||
@@ -18,29 +18,43 @@ public:
|
||||
void validate_component(const Json& component) const;
|
||||
Json migrate_component(const Json& component) const;
|
||||
void add_migration(const std::string& type, int from_version, Json field_rules);
|
||||
private:
|
||||
std::map<std::string,Json> schemas_;
|
||||
std::map<std::pair<std::string,int>,Json> migrations_;
|
||||
|
||||
private:
|
||||
std::map<std::string, Json> schemas_;
|
||||
std::map<std::pair<std::string, int>, Json> migrations_;
|
||||
};
|
||||
|
||||
template<class T> class TypeRegistration {
|
||||
public:
|
||||
TypeRegistration(SchemaRegistry& registry, std::string id, std::string name, int version=1)
|
||||
: registry_(registry),schema_{{"id",std::move(id)},{"name",std::move(name)},{"version",version},{"fields",Json::object()}} {}
|
||||
template<class Value>
|
||||
TypeRegistration& field(std::string id, std::string name, Value T::*member, Value default_value,
|
||||
std::string kind, Json constraints=Json::object()) {
|
||||
template <class T> class TypeRegistration {
|
||||
public:
|
||||
TypeRegistration(SchemaRegistry& registry, std::string id, std::string name, int version = 1)
|
||||
: registry_(registry), schema_{{"id", std::move(id)},
|
||||
{"name", std::move(name)},
|
||||
{"version", version},
|
||||
{"fields", Json::object()}} {}
|
||||
template <class Value>
|
||||
TypeRegistration& field(std::string id, std::string name, Value T::* member,
|
||||
Value default_value, std::string kind,
|
||||
Json constraints = Json::object()) {
|
||||
static_assert(std::is_member_object_pointer_v<decltype(member)>);
|
||||
require(!schema_["fields"].contains(id),"schema.duplicate_field","Duplicate stable FieldId");
|
||||
require(!schema_["fields"].contains(id), "schema.duplicate_field",
|
||||
"Duplicate stable FieldId");
|
||||
// Converting the typed default checks supported JSON serialization at compile time.
|
||||
Json descriptor={{"id",id},{"name",std::move(name)},{"type",std::move(kind)},{"default",Json(default_value)}};
|
||||
descriptor.update(constraints); schema_["fields"][id]=std::move(descriptor); return *this;
|
||||
Json descriptor = {{"id", id},
|
||||
{"name", std::move(name)},
|
||||
{"type", std::move(kind)},
|
||||
{"default", Json(default_value)}};
|
||||
descriptor.update(constraints);
|
||||
schema_["fields"][id] = std::move(descriptor);
|
||||
return *this;
|
||||
}
|
||||
void commit() { registry_.register_schema(schema_); }
|
||||
private:
|
||||
void commit() {
|
||||
registry_.register_schema(schema_);
|
||||
}
|
||||
|
||||
private:
|
||||
SchemaRegistry& registry_;
|
||||
Json schema_;
|
||||
};
|
||||
SchemaRegistry builtin_schemas();
|
||||
void validate_field(const Json& value,const Json& descriptor);
|
||||
}
|
||||
void validate_field(const Json& value, const Json& descriptor);
|
||||
} // namespace faset::authoring
|
||||
|
||||
@@ -3,47 +3,58 @@
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace faset::authoring {
|
||||
Json make_scene(std::string name,int dimension=3);
|
||||
Json make_entity(const SchemaRegistry& schemas,std::string name,const std::string& parent="");
|
||||
void validate_scene(const Json& scene,const SchemaRegistry& schemas);
|
||||
Json make_scene(std::string name, int dimension = 3);
|
||||
Json make_entity(const SchemaRegistry& schemas, std::string name, const std::string& parent = "");
|
||||
void validate_scene(const Json& scene, const SchemaRegistry& schemas);
|
||||
|
||||
class AuthoringService {
|
||||
public:
|
||||
explicit AuthoringService(std::filesystem::path project_root,SchemaRegistry schemas=builtin_schemas());
|
||||
Json create(std::string name,int dimension=3);
|
||||
Json open(const std::filesystem::path& relative,bool recover=false);
|
||||
public:
|
||||
explicit AuthoringService(std::filesystem::path project_root,
|
||||
SchemaRegistry schemas = builtin_schemas());
|
||||
Json create(std::string name, int dimension = 3);
|
||||
Json open(const std::filesystem::path& relative, bool recover = false);
|
||||
Json query(const std::string& document) const;
|
||||
Json documents() const;
|
||||
Json transact(const std::string& document,std::uint64_t expected_revision,const Json& operations,const std::string& idempotency_key="");
|
||||
Json undo(const std::string& document,std::uint64_t expected_revision);
|
||||
Json redo(const std::string& document,std::uint64_t expected_revision);
|
||||
Json save(const std::string& document,const std::filesystem::path& relative={});
|
||||
Json transact(const std::string& document, std::uint64_t expected_revision,
|
||||
const Json& operations, const std::string& idempotency_key = "");
|
||||
Json undo(const std::string& document, std::uint64_t expected_revision);
|
||||
Json redo(const std::string& document, std::uint64_t expected_revision);
|
||||
Json save(const std::string& document, const std::filesystem::path& relative = {});
|
||||
Json recovery_documents() const;
|
||||
const SchemaRegistry& schemas() const {return schemas_;}
|
||||
Json recover(const std::string& document,
|
||||
std::optional<std::uint64_t> expected_revision = std::nullopt);
|
||||
const SchemaRegistry& schemas() const {
|
||||
return schemas_;
|
||||
}
|
||||
void register_schemas(const Json& manifest);
|
||||
const std::filesystem::path& root() const {return root_;}
|
||||
private:
|
||||
void replace_external_schemas(const Json& manifest);
|
||||
const std::filesystem::path& root() const {
|
||||
return root_;
|
||||
}
|
||||
|
||||
private:
|
||||
struct State {
|
||||
Json data;
|
||||
std::uint64_t revision=0;
|
||||
std::uint64_t revision = 0;
|
||||
std::filesystem::path path;
|
||||
std::string saved_hash,disk_hash;
|
||||
std::vector<Json> undo,redo;
|
||||
std::map<std::string,std::pair<std::string,Json>> requests;
|
||||
std::string saved_hash, disk_hash;
|
||||
std::vector<Json> undo, redo;
|
||||
std::map<std::string, std::pair<std::string, Json>> requests;
|
||||
};
|
||||
Json summary(const State& state,bool include_data=true) const;
|
||||
Json summary(const State& state, bool include_data = true) const;
|
||||
State& state(const std::string& document);
|
||||
const State& state(const std::string& document) const;
|
||||
void journal(const State& state) const;
|
||||
void apply(Json& scene,const Json& operation);
|
||||
Json history(const std::string& document,std::uint64_t revision,bool redo);
|
||||
void apply(Json& scene, const Json& operation);
|
||||
Json history(const std::string& document, std::uint64_t revision, bool redo);
|
||||
std::filesystem::path root_;
|
||||
SchemaRegistry schemas_;
|
||||
std::map<std::string,State> documents_;
|
||||
std::map<std::string, State> documents_;
|
||||
mutable std::recursive_mutex mutex_;
|
||||
};
|
||||
}
|
||||
} // namespace faset::authoring
|
||||
|
||||
@@ -4,8 +4,12 @@
|
||||
#include <string>
|
||||
|
||||
namespace faset::authoring {
|
||||
struct ResolvedScene {Json scene;Json conflicts=Json::array();};
|
||||
using SceneLoader=std::function<Json(const std::string&)>;
|
||||
struct ResolvedScene {
|
||||
Json scene;
|
||||
Json conflicts = Json::array();
|
||||
};
|
||||
using SceneLoader = std::function<Json(const std::string&)>;
|
||||
// Source documents are immutable inputs. Conflicting records stay in the authoring file.
|
||||
ResolvedScene resolve_templates(const Json& scene,const SchemaRegistry& schemas,const SceneLoader& loader);
|
||||
}
|
||||
ResolvedScene resolve_templates(const Json& scene, const SchemaRegistry& schemas,
|
||||
const SceneLoader& loader);
|
||||
} // namespace faset::authoring
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
#include <faset/core/json.hpp>
|
||||
#include <string>
|
||||
namespace faset::authoring {
|
||||
// Reparents authored TRS while optionally preserving the complete world transform.
|
||||
// Non-invertible parents and transforms requiring shear are rejected atomically.
|
||||
void reparent_entity(Json& scene, const std::string& entity, const Json& parent, bool keep_world);
|
||||
} // namespace faset::authoring
|
||||
@@ -5,16 +5,23 @@
|
||||
|
||||
namespace faset {
|
||||
class Error : public std::runtime_error {
|
||||
public:
|
||||
public:
|
||||
Error(std::string code, std::string message, Json details = Json::object())
|
||||
: std::runtime_error(std::move(message)), code_(std::move(code)), details_(std::move(details)) {}
|
||||
const std::string& code() const noexcept { return code_; }
|
||||
Json json() const { return {{"code", code_}, {"message", what()}, {"details", details_}}; }
|
||||
private:
|
||||
: std::runtime_error(std::move(message)), code_(std::move(code)),
|
||||
details_(std::move(details)) {}
|
||||
const std::string& code() const noexcept {
|
||||
return code_;
|
||||
}
|
||||
Json json() const {
|
||||
return {{"code", code_}, {"message", what()}, {"details", details_}};
|
||||
}
|
||||
|
||||
private:
|
||||
std::string code_;
|
||||
Json details_;
|
||||
};
|
||||
inline void require(bool condition, const std::string& code, const std::string& message) {
|
||||
if (!condition) throw Error(code, message);
|
||||
}
|
||||
if (!condition)
|
||||
throw Error(code, message);
|
||||
}
|
||||
} // namespace faset
|
||||
|
||||
@@ -11,4 +11,4 @@ inline std::string sha256(std::string_view text) {
|
||||
return sha256(std::as_bytes(std::span(text.data(), text.size())));
|
||||
}
|
||||
std::string sha256_file(const std::filesystem::path& path);
|
||||
}
|
||||
} // namespace faset
|
||||
|
||||
@@ -11,5 +11,6 @@ Json read_json(const std::filesystem::path& path);
|
||||
void atomic_write(const std::filesystem::path& path, std::string_view bytes);
|
||||
void atomic_write_json(const std::filesystem::path& path, const Json& value);
|
||||
// Rejects traversal and symlink escapes before project-scoped file operations.
|
||||
std::filesystem::path project_path(const std::filesystem::path& root, const std::filesystem::path& relative);
|
||||
}
|
||||
std::filesystem::path project_path(const std::filesystem::path& root,
|
||||
const std::filesystem::path& relative);
|
||||
} // namespace faset
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#pragma once
|
||||
#include <nlohmann/json.hpp>
|
||||
namespace faset { using Json = nlohmann::json; }
|
||||
namespace faset {
|
||||
using Json = nlohmann::json;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace faset {
|
||||
struct ProcessOptions {
|
||||
// First element is the executable. Arguments are passed directly, never through a shell.
|
||||
std::vector<std::string> arguments;
|
||||
std::filesystem::path working_directory;
|
||||
std::map<std::string, std::string> environment;
|
||||
};
|
||||
struct ProcessPoll {
|
||||
bool running{};
|
||||
std::optional<int> exit_code;
|
||||
std::string output; // Newly available stdout and stderr, combined.
|
||||
};
|
||||
class Process {
|
||||
public:
|
||||
explicit Process(const ProcessOptions&);
|
||||
~Process();
|
||||
Process(Process&&) noexcept;
|
||||
Process& operator=(Process&&) noexcept;
|
||||
Process(const Process&) = delete;
|
||||
Process& operator=(const Process&) = delete;
|
||||
ProcessPoll poll();
|
||||
// Terminates the process group/job, including its compiler children.
|
||||
void cancel();
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
std::filesystem::path find_executable(const std::string& name);
|
||||
} // namespace faset
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
#include <faset/core/json.hpp>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace faset::editor {
|
||||
struct BuildConfig {
|
||||
std::filesystem::path project_root;
|
||||
std::filesystem::path engine_root;
|
||||
std::filesystem::path build_directory;
|
||||
std::filesystem::path cache_root;
|
||||
// Separate CMake caches prevent an export from changing the active development build.
|
||||
std::string configuration{"Debug"};
|
||||
std::string export_configuration{"Release"};
|
||||
std::string cmake{"cmake"};
|
||||
std::string generator{"Ninja"};
|
||||
std::vector<std::string> configure_arguments;
|
||||
};
|
||||
struct JobStatus {
|
||||
std::string id, kind, state{"queued"}, stage{"queued"};
|
||||
double progress{};
|
||||
std::string log, error;
|
||||
Json result = Json::object();
|
||||
bool finished() const {
|
||||
return state == "succeeded" || state == "failed" || state == "cancelled";
|
||||
}
|
||||
Json json() const;
|
||||
};
|
||||
// One serialized build/cook worker per project. GUI and MCP use the same service.
|
||||
// Input scenes must be resolved authoring snapshots, independent of live runtime state.
|
||||
class BuildService {
|
||||
public:
|
||||
explicit BuildService(BuildConfig);
|
||||
~BuildService();
|
||||
BuildService(const BuildService&) = delete;
|
||||
BuildService& operator=(const BuildService&) = delete;
|
||||
void scaffold(const std::string& name, int dimension);
|
||||
std::string start_build();
|
||||
std::string start_cook(Json resolved_scene);
|
||||
// Publishes output/generations/<id>; current.json changes only after all validation succeeds.
|
||||
std::string start_export(Json resolved_scene, const std::filesystem::path& output_directory);
|
||||
JobStatus job(const std::string& id) const;
|
||||
std::vector<JobStatus> jobs() const;
|
||||
void cancel(const std::string& id);
|
||||
JobStatus wait(const std::string& id);
|
||||
const BuildConfig& config() const;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
void write_cooked_scene(const std::filesystem::path& path, const Json& resolved_scene);
|
||||
} // namespace faset::editor
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include <faset/authoring/service.hpp>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace faset::editor {
|
||||
class Commands {
|
||||
public:
|
||||
using Handler = std::function<Json(const Json&)>;
|
||||
explicit Commands(authoring::AuthoringService& authoring);
|
||||
void add(std::string name, std::string description, Json input_schema, Handler handler,
|
||||
bool read_only = false);
|
||||
Json list() const;
|
||||
void remove(const std::string& name);
|
||||
Json call(const std::string& name, const Json& arguments);
|
||||
Json resolved_scene(const std::string& document) const;
|
||||
authoring::AuthoringService& authoring() {
|
||||
return authoring_;
|
||||
}
|
||||
static Json object_schema(Json properties, Json required = Json::array());
|
||||
|
||||
private:
|
||||
struct Command {
|
||||
Json descriptor;
|
||||
Handler handler;
|
||||
};
|
||||
authoring::AuthoringService& authoring_;
|
||||
std::map<std::string, Command> commands_;
|
||||
};
|
||||
} // namespace faset::editor
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include <faset/editor/session.hpp>
|
||||
#include <faset/ui/ui.hpp>
|
||||
#include <memory>
|
||||
|
||||
namespace faset::editor {
|
||||
// Owns authoring presentation only. Player execution and MCP transport stay in
|
||||
// the application.
|
||||
class EditorUI {
|
||||
public:
|
||||
EditorUI(Session&, render::Renderer&, const std::filesystem::path& font,
|
||||
const std::filesystem::path& styles);
|
||||
~EditorUI();
|
||||
EditorUI(const EditorUI&) = delete;
|
||||
EditorUI& operator=(const EditorUI&) = delete;
|
||||
void frame(const std::vector<render::Event>&);
|
||||
const render::Snapshot& snapshot() const;
|
||||
ui::Context& widgets();
|
||||
const std::string& current_document() const;
|
||||
void select_document(const std::string& document);
|
||||
const std::string& selected_entity() const;
|
||||
void select_entity(const std::string& entity);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
} // namespace faset::editor
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include <faset/editor/commands.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace faset::editor {
|
||||
// MCP is compiled exclusively into the Editor / headless authoring executable.
|
||||
class McpServer {
|
||||
public:
|
||||
explicit McpServer(Commands& commands) : commands_(commands) {}
|
||||
std::optional<Json> handle(const Json& message);
|
||||
Json parse_error() const;
|
||||
|
||||
private:
|
||||
Commands& commands_;
|
||||
bool initialized_ = false, ready_ = false;
|
||||
};
|
||||
class StdioTransport {
|
||||
public:
|
||||
// Nonblocking: GUI and MCP can share the same authoring session and event loop.
|
||||
std::vector<std::string> poll();
|
||||
bool closed() const noexcept {
|
||||
return closed_;
|
||||
}
|
||||
void send(const Json& message);
|
||||
|
||||
private:
|
||||
std::string buffer_;
|
||||
bool closed_ = false;
|
||||
};
|
||||
} // namespace faset::editor
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
/* Exact-build native Editor SDK. No STL types or ownership cross this ABI. */
|
||||
#include <faset/editor/sdk_build.h>
|
||||
#include <stdint.h>
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
#define FASET_EDITOR_API_VERSION 1u
|
||||
#if defined(_WIN32)
|
||||
#define FASET_PLUGIN_EXPORT __declspec(dllexport)
|
||||
#else
|
||||
#define FASET_PLUGIN_EXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
/* UTF-8 JSON is borrowed for the duration of a call. write() copies output in
|
||||
the receiving module. Return zero on success; errors use {code,message}. */
|
||||
typedef void (*FasetWrite)(void* receiver, const char* utf8, uint64_t length);
|
||||
typedef int (*FasetCommand)(void* user, const char* arguments_json, FasetWrite write,
|
||||
void* receiver);
|
||||
typedef struct FasetEditorHost {
|
||||
uint32_t api_version;
|
||||
uint32_t struct_size;
|
||||
const char* build_fingerprint;
|
||||
void* context;
|
||||
int (*register_command)(void* context, const char* descriptor_json, FasetCommand callback,
|
||||
void* user);
|
||||
int (*register_panel)(void* context, const char* panel_json);
|
||||
int (*invoke_command)(void* context, const char* name, const char* arguments_json,
|
||||
FasetWrite write, void* receiver);
|
||||
void (*log)(void* context, const char* utf8);
|
||||
} FasetEditorHost;
|
||||
typedef struct FasetEditorPlugin {
|
||||
uint32_t api_version;
|
||||
uint32_t struct_size;
|
||||
const char* build_fingerprint;
|
||||
void* user;
|
||||
void (*shutdown)(void* user);
|
||||
} FasetEditorPlugin;
|
||||
/* Required exported symbol. Called once at startup, after dependencies load. */
|
||||
typedef int (*FasetPluginEntry)(const FasetEditorHost* host, FasetEditorPlugin* plugin);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <faset/editor/commands.hpp>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
namespace faset::editor {
|
||||
class PluginManager {
|
||||
public:
|
||||
using Logger = std::function<void(std::string)>;
|
||||
explicit PluginManager(Commands&, Logger);
|
||||
~PluginManager();
|
||||
PluginManager(const PluginManager&) = delete;
|
||||
PluginManager& operator=(const PluginManager&) = delete;
|
||||
// Discover and validate the complete dependency graph before loading code.
|
||||
// Individual failures are reported; dependents are never loaded after one.
|
||||
void load(const std::filesystem::path& directory);
|
||||
Json status() const;
|
||||
Json panels() const;
|
||||
static std::string fingerprint();
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
} // namespace faset::editor
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
#include <faset/assets/asset_pipeline.hpp>
|
||||
#include <faset/core/process.hpp>
|
||||
#include <faset/editor/build_service.hpp>
|
||||
#include <faset/editor/commands.hpp>
|
||||
#include <faset/editor/plugins.hpp>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
namespace faset::editor {
|
||||
struct SessionConfig {
|
||||
std::filesystem::path project_root, engine_root, binary_directory;
|
||||
};
|
||||
class Session {
|
||||
public:
|
||||
explicit Session(SessionConfig);
|
||||
~Session();
|
||||
Session(const Session&) = delete;
|
||||
Session& operator=(const Session&) = delete;
|
||||
authoring::AuthoringService& authoring() {
|
||||
return authoring_;
|
||||
}
|
||||
Commands& commands() {
|
||||
return commands_;
|
||||
}
|
||||
void poll();
|
||||
const std::vector<std::string>& logs() const {
|
||||
return logs_;
|
||||
}
|
||||
Json project() const;
|
||||
Json plugin_panels() const {
|
||||
return plugins_ ? plugins_->panels() : Json::array();
|
||||
}
|
||||
void scaffold(const std::string& name, int dimension);
|
||||
const SessionConfig& config() const {
|
||||
return config_;
|
||||
}
|
||||
bool playing() const {
|
||||
return bool(player_);
|
||||
}
|
||||
void log(std::string message);
|
||||
|
||||
private:
|
||||
struct ImportTask;
|
||||
void register_commands();
|
||||
Json assets_list() const;
|
||||
Json jobs() const;
|
||||
Json job(const std::string& id) const;
|
||||
void load_schema(const std::filesystem::path& path);
|
||||
void launch_player(Json scene, const std::filesystem::path& executable);
|
||||
void stop_player();
|
||||
SessionConfig config_;
|
||||
authoring::AuthoringService authoring_;
|
||||
Commands commands_;
|
||||
assets::AssetPipeline assets_;
|
||||
BuildService builds_;
|
||||
std::map<std::string, std::shared_ptr<ImportTask>> imports_;
|
||||
std::vector<std::jthread> workers_;
|
||||
std::vector<std::string> logs_;
|
||||
std::map<std::string, std::string> observed_jobs_;
|
||||
std::unique_ptr<Process> player_;
|
||||
std::filesystem::path control_path_;
|
||||
std::uint64_t control_sequence_ = 0;
|
||||
std::string pending_play_job_;
|
||||
Json pending_play_scene_;
|
||||
std::unique_ptr<PluginManager> plugins_;
|
||||
};
|
||||
} // namespace faset::editor
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#include <faset/render/renderer.hpp>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace faset::player {
|
||||
struct CameraSettings {
|
||||
// Defaults frame the small demo scenes. An explicit editor camera overrides
|
||||
// scene camera components without changing the authoring document.
|
||||
bool overrideSceneCamera{false};
|
||||
render::Vec3 eye{8, 6, 10};
|
||||
render::Vec3 target{0, 0, 0};
|
||||
float verticalFovDegrees{60};
|
||||
float orthographicHeight{12};
|
||||
float nearPlane{0.1f}, farPlane{1000};
|
||||
};
|
||||
class SceneView {
|
||||
public:
|
||||
// cacheRoot contains assets/<AssetId>/current.json and generation folders.
|
||||
explicit SceneView(std::filesystem::path cacheRoot);
|
||||
~SceneView();
|
||||
SceneView(const SceneView&) = delete;
|
||||
SceneView& operator=(const SceneView&) = delete;
|
||||
render::Snapshot build(const nlohmann::json& flatSceneOrRuntimeSnapshot, float aspect,
|
||||
CameraSettings camera = {});
|
||||
void clearCache();
|
||||
const std::vector<std::string>& diagnostics() const;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
// Reads JSON development scenes or a strict FASETSCN v1 CBOR envelope.
|
||||
nlohmann::json readScene(const std::filesystem::path& path);
|
||||
} // namespace faset::player
|
||||
@@ -6,15 +6,21 @@ namespace faset::render {
|
||||
// Ordered single-queue graph. Reads must be imported or produced by an earlier pass.
|
||||
// The Vulkan executor performs barriers at each resource state transition.
|
||||
class RenderGraph {
|
||||
public:
|
||||
public:
|
||||
using Callback = std::function<void()>;
|
||||
void import(std::string resource);
|
||||
void add(std::string name, std::vector<std::string> reads, std::vector<std::string> writes, Callback execute);
|
||||
void add(std::string name, std::vector<std::string> reads, std::vector<std::string> writes,
|
||||
Callback execute);
|
||||
void execute() const;
|
||||
std::vector<std::string> pass_names() const;
|
||||
private:
|
||||
struct Pass {std::string name; std::vector<std::string> reads, writes; Callback callback;};
|
||||
|
||||
private:
|
||||
struct Pass {
|
||||
std::string name;
|
||||
std::vector<std::string> reads, writes;
|
||||
Callback callback;
|
||||
};
|
||||
std::vector<std::string> imports_;
|
||||
std::vector<Pass> passes_;
|
||||
};
|
||||
}
|
||||
} // namespace faset::render
|
||||
|
||||
@@ -11,37 +11,68 @@ using Vec2 = std::array<float, 2>;
|
||||
using Vec3 = std::array<float, 3>;
|
||||
using Color = std::array<float, 4>;
|
||||
using Mat4 = std::array<float, 16>;
|
||||
inline constexpr Mat4 identity{1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
|
||||
inline constexpr Mat4 identity{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
|
||||
// Matrices are column-major, vectors are columns; clip depth is Vulkan's [0,1].
|
||||
Mat4 multiply(const Mat4&, const Mat4&);
|
||||
Mat4 transform(Vec3 position, Vec3 rotation = {}, Vec3 scale = {1,1,1});
|
||||
Mat4 transform(Vec3 position, Vec3 rotation = {}, Vec3 scale = {1, 1, 1});
|
||||
Mat4 perspective(float vertical_fov_radians, float aspect, float near_plane, float far_plane);
|
||||
Mat4 orthographic(float left, float right, float bottom, float top, float near_plane, float far_plane);
|
||||
Mat4 look_at(Vec3 eye, Vec3 target, Vec3 up = {0,1,0});
|
||||
struct Vertex { Vec3 position{}; Vec3 normal{0,0,1}; Color color{1,1,1,1}; Vec2 uv{}; };
|
||||
struct Mesh { std::vector<Vertex> vertices; std::vector<std::uint32_t> indices; };
|
||||
Mat4 orthographic(float left, float right, float bottom, float top, float near_plane,
|
||||
float far_plane);
|
||||
Mat4 look_at(Vec3 eye, Vec3 target, Vec3 up = {0, 1, 0});
|
||||
struct Vertex {
|
||||
Vec3 position{};
|
||||
Vec3 normal{0, 0, 1};
|
||||
Color color{1, 1, 1, 1};
|
||||
Vec2 uv{};
|
||||
};
|
||||
struct Mesh {
|
||||
std::vector<Vertex> vertices;
|
||||
std::vector<std::uint32_t> indices;
|
||||
};
|
||||
std::shared_ptr<const Mesh> cube_mesh();
|
||||
struct Texture;
|
||||
struct DrawItem {
|
||||
std::shared_ptr<const Mesh> mesh;
|
||||
Mat4 model{identity};
|
||||
Color color{1,1,1,1};
|
||||
Color color{1, 1, 1, 1};
|
||||
float roughness{0.65f};
|
||||
float metallic{0.0f};
|
||||
bool cast_shadow{true};
|
||||
std::shared_ptr<const Texture> texture;
|
||||
};
|
||||
struct Sprite { Vec3 position{}; Vec2 size{1,1}; Color color{1,1,1,1}; float rotation{}; std::shared_ptr<const Texture> texture; };
|
||||
struct Texture { std::uint32_t width{}, height{}; std::vector<std::uint8_t> rgba; std::uint64_t revision{}; bool srgb{false}; };
|
||||
struct Quad { float x{}, y{}, width{}, height{}; Color color{1,1,1,1}; std::shared_ptr<const Texture> texture; std::array<float,4> uv_rect{0,0,1,1}; };
|
||||
struct Text { float x{}, y{}; std::string value; Color color{0.85f,0.87f,0.90f,1}; float size{14}; };
|
||||
struct Sprite {
|
||||
Vec3 position{};
|
||||
Vec2 size{1, 1};
|
||||
Color color{1, 1, 1, 1};
|
||||
float rotation{};
|
||||
std::shared_ptr<const Texture> texture;
|
||||
};
|
||||
struct Texture {
|
||||
std::uint32_t width{}, height{};
|
||||
std::vector<std::uint8_t> rgba;
|
||||
std::uint64_t revision{};
|
||||
bool srgb{false};
|
||||
};
|
||||
struct Quad {
|
||||
float x{}, y{}, width{}, height{};
|
||||
Color color{1, 1, 1, 1};
|
||||
std::shared_ptr<const Texture> texture;
|
||||
std::array<float, 4> uv_rect{0, 0, 1, 1};
|
||||
};
|
||||
struct Text {
|
||||
float x{}, y{};
|
||||
std::string value;
|
||||
Color color{0.85f, 0.87f, 0.90f, 1};
|
||||
float size{14};
|
||||
};
|
||||
struct Snapshot {
|
||||
// Optional scene viewport in drawable pixels (x, y, width, height); zero size uses the full target.
|
||||
std::array<float,4> scene_rect{};
|
||||
// Optional scene viewport in drawable pixels (x, y, width, height); zero size uses the full
|
||||
// target.
|
||||
std::array<float, 4> scene_rect{};
|
||||
Mat4 view_projection{identity};
|
||||
Vec3 eye{4,3,5};
|
||||
Vec3 light_direction{-0.5f,-1,-0.3f};
|
||||
Color clear_color{0.055f,0.065f,0.085f,1};
|
||||
Vec3 eye{4, 3, 5};
|
||||
Vec3 light_direction{-0.5f, -1, -0.3f};
|
||||
Color clear_color{0.055f, 0.065f, 0.085f, 1};
|
||||
std::vector<DrawItem> draws;
|
||||
std::vector<Sprite> sprites;
|
||||
// UI coordinates are drawable pixels, top-left origin. Order is preserved per list.
|
||||
@@ -55,7 +86,20 @@ struct RendererConfig {
|
||||
bool validation{true};
|
||||
};
|
||||
struct Event {
|
||||
enum class Type { Quit, Resize, FocusGained, FocusLost, MouseMove, MouseDown, MouseUp, Wheel, KeyDown, KeyUp, TextInput, TextEditing };
|
||||
enum class Type {
|
||||
Quit,
|
||||
Resize,
|
||||
FocusGained,
|
||||
FocusLost,
|
||||
MouseMove,
|
||||
MouseDown,
|
||||
MouseUp,
|
||||
Wheel,
|
||||
KeyDown,
|
||||
KeyUp,
|
||||
TextInput,
|
||||
TextEditing
|
||||
};
|
||||
Type type{};
|
||||
float x{}, y{};
|
||||
int button{};
|
||||
@@ -71,7 +115,7 @@ struct FrameStats {
|
||||
std::string device;
|
||||
};
|
||||
class Renderer {
|
||||
public:
|
||||
public:
|
||||
explicit Renderer(const RendererConfig& = {});
|
||||
~Renderer();
|
||||
Renderer(Renderer&&) noexcept;
|
||||
@@ -95,8 +139,9 @@ public:
|
||||
void set_text_input_area(float x, float y, float width, float height);
|
||||
void set_clipboard(const std::string&);
|
||||
std::string clipboard() const;
|
||||
private:
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
}
|
||||
} // namespace faset::render
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace faset::runtime {
|
||||
|
||||
@@ -26,7 +26,9 @@ struct EntityHandle {
|
||||
std::uint64_t session{};
|
||||
std::uint32_t slot{};
|
||||
std::uint64_t generation{};
|
||||
explicit operator bool() const noexcept { return session != 0; }
|
||||
explicit operator bool() const noexcept {
|
||||
return session != 0;
|
||||
}
|
||||
bool operator==(const EntityHandle&) const = default;
|
||||
};
|
||||
|
||||
@@ -37,8 +39,17 @@ struct InputState {
|
||||
bool interactPressed{};
|
||||
};
|
||||
|
||||
struct Sprite { Vec4 color{1, 1, 1, 1}; Vec2 size{1, 1}; std::string texture; int layer{}; };
|
||||
struct Mesh { std::string asset; Vec4 color{1, 1, 1, 1}; std::string primitive{"cube"}; };
|
||||
struct Sprite {
|
||||
Vec4 color{1, 1, 1, 1};
|
||||
Vec2 size{1, 1};
|
||||
std::string texture;
|
||||
int layer{};
|
||||
};
|
||||
struct Mesh {
|
||||
std::string asset;
|
||||
Vec4 color{1, 1, 1, 1};
|
||||
std::string primitive{"cube"};
|
||||
};
|
||||
struct RenderEntity {
|
||||
std::string id;
|
||||
std::string name;
|
||||
@@ -86,7 +97,7 @@ struct CollisionEvent {
|
||||
// Single-owner sequential runtime. Gameplay callbacks run on the caller's thread.
|
||||
// No Editor, MCP, renderer or platform service is linked by this API.
|
||||
class Runtime {
|
||||
public:
|
||||
public:
|
||||
explicit Runtime(RuntimeConfig config = {});
|
||||
~Runtime();
|
||||
Runtime(const Runtime&) = delete;
|
||||
@@ -111,6 +122,9 @@ public:
|
||||
// Configuration copy. Live poses and velocities have their own typed accessors.
|
||||
nlohmann::json fields(EntityHandle handle, const std::string& componentType) const;
|
||||
Vec3 velocity(EntityHandle handle) const;
|
||||
// Support from the last completed physics step. Checks actual contact normals
|
||||
// against opposite gravity (Y-up if gravity is zero), not vertical speed.
|
||||
bool grounded(EntityHandle handle) const;
|
||||
InputState input() const noexcept;
|
||||
// Valid until the next fixed tick or scene replacement. No native solver pointers.
|
||||
const std::vector<CollisionEvent>& collisions() const noexcept;
|
||||
@@ -135,7 +149,7 @@ public:
|
||||
std::uint64_t session() const noexcept;
|
||||
const std::vector<std::string>& diagnostics() const noexcept;
|
||||
|
||||
private:
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
#pragma once
|
||||
#include <faset/render/renderer.hpp>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace faset::ui {
|
||||
using Json = nlohmann::json;
|
||||
using Color = render::Color;
|
||||
struct Rect {
|
||||
float x{}, y{}, width{}, height{};
|
||||
bool contains(float px, float py) const noexcept;
|
||||
Rect intersection(const Rect&) const noexcept;
|
||||
};
|
||||
struct Theme {
|
||||
Color background{.075f, .078f, .086f, 1}, surface{.10f, .105f, .115f, 1};
|
||||
Color raised{.135f, .14f, .15f, 1}, hover{.175f, .18f, .20f, 1}, border{.23f, .235f, .255f, 1};
|
||||
Color text{.86f, .875f, .90f, 1}, muted{.55f, .575f, .62f, 1}, accent{.65f, .60f, .88f, 1};
|
||||
Color selection{.26f, .245f, .35f, 1}, danger{.92f, .39f, .38f, 1};
|
||||
float font_size = 14, row_height = 28, padding = 8, gap = 4;
|
||||
static Theme from_json(const Json&);
|
||||
static Theme load(const std::filesystem::path&);
|
||||
Json to_json() const;
|
||||
};
|
||||
|
||||
// Byte offsets always remain valid UTF-8 boundaries. Undo is local to an
|
||||
// unfinished edit.
|
||||
class TextBuffer {
|
||||
public:
|
||||
explicit TextBuffer(std::string text = {});
|
||||
const std::string& text() const noexcept {
|
||||
return text_;
|
||||
}
|
||||
std::size_t cursor() const noexcept {
|
||||
return cursor_;
|
||||
}
|
||||
std::size_t anchor() const noexcept {
|
||||
return anchor_;
|
||||
}
|
||||
bool has_selection() const noexcept {
|
||||
return cursor_ != anchor_;
|
||||
}
|
||||
std::string selected_text() const;
|
||||
void reset(std::string text);
|
||||
void set_cursor(std::size_t byte_offset, bool select = false);
|
||||
void select_all();
|
||||
void left(bool select = false, bool by_word = false);
|
||||
void right(bool select = false, bool by_word = false);
|
||||
void home(bool select = false);
|
||||
void end(bool select = false);
|
||||
bool insert(std::string_view utf8);
|
||||
bool backspace();
|
||||
bool delete_forward();
|
||||
bool undo();
|
||||
bool redo();
|
||||
static bool valid_utf8(std::string_view);
|
||||
|
||||
private:
|
||||
struct State {
|
||||
std::string text;
|
||||
std::size_t cursor, anchor;
|
||||
};
|
||||
std::string text_;
|
||||
std::size_t cursor_{}, anchor_{};
|
||||
std::vector<State> undo_, redo_;
|
||||
void remember();
|
||||
void erase_selection();
|
||||
};
|
||||
class FontAtlas {
|
||||
public:
|
||||
explicit FontAtlas(const std::filesystem::path& font);
|
||||
~FontAtlas();
|
||||
FontAtlas(const FontAtlas&) = delete;
|
||||
FontAtlas& operator=(const FontAtlas&) = delete;
|
||||
float measure(std::string_view utf8, float pixels);
|
||||
// y is the top of the line, not the baseline; clipping also adjusts glyph
|
||||
// UVs.
|
||||
void draw(render::Snapshot&, std::string_view utf8, float x, float y, float pixels, Color,
|
||||
const Rect& clip);
|
||||
std::shared_ptr<const render::Texture> texture() const;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
enum class Kind {
|
||||
Panel,
|
||||
Row,
|
||||
Column,
|
||||
Label,
|
||||
Button,
|
||||
Tab,
|
||||
TreeRow,
|
||||
TextField,
|
||||
NumberField,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Viewport
|
||||
};
|
||||
struct Layout {
|
||||
float width = -1, height = -1, flex = 0;
|
||||
float min_width = 0, min_height = 0, max_width = 100000, max_height = 100000;
|
||||
float padding = 0, gap = 4;
|
||||
bool absolute = false, scroll = false, clip = true;
|
||||
float x = 0, y = 0;
|
||||
};
|
||||
struct Widget {
|
||||
Kind kind = Kind::Panel;
|
||||
std::string id, text;
|
||||
Layout layout;
|
||||
Rect rect, clip;
|
||||
bool visible = true, enabled = true, selected = false, checked = false;
|
||||
double value = 0, step = .01;
|
||||
int precision = 3, indent = 0;
|
||||
float scroll_y = 0, content_height = 0;
|
||||
std::string error, tooltip;
|
||||
Json drag_payload;
|
||||
std::string dock_area, dock_panel;
|
||||
std::function<void(Widget&)> on_click, on_preview, on_commit, on_cancel;
|
||||
std::function<void(Widget&, const Json&)> on_drop;
|
||||
std::vector<std::unique_ptr<Widget>> children;
|
||||
Widget* parent = nullptr;
|
||||
Widget& add(Kind, const std::string& stable_id, const std::string& text = {});
|
||||
Widget* find(std::string_view stable_id);
|
||||
const Widget* find(std::string_view stable_id) const;
|
||||
void remove(std::string_view stable_id);
|
||||
};
|
||||
// Small persistent docking model: named areas, ordered tabs/panels and explicit
|
||||
// sizes.
|
||||
class DockLayout {
|
||||
public:
|
||||
void move(const std::string& panel, const std::string& area, std::size_t index);
|
||||
std::vector<std::string> panels(const std::string& area) const;
|
||||
void set_size(const std::string& panel, float size);
|
||||
float size(const std::string& panel, float fallback) const;
|
||||
Json to_json() const;
|
||||
void from_json(const Json&);
|
||||
void save(const std::filesystem::path&) const;
|
||||
void load(const std::filesystem::path&);
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::vector<std::string>> areas_;
|
||||
std::unordered_map<std::string, float> sizes_;
|
||||
};
|
||||
|
||||
class Context {
|
||||
public:
|
||||
explicit Context(const std::filesystem::path& font);
|
||||
~Context();
|
||||
Widget& root();
|
||||
Widget* find(std::string_view id);
|
||||
void set_theme(Theme);
|
||||
const Theme& theme() const;
|
||||
// Reloads declarative widget properties; matching IDs retain callbacks and
|
||||
// edit state.
|
||||
void apply_layout(const Json&);
|
||||
void layout(float drawable_width, float drawable_height, float dpi_scale = 1);
|
||||
bool handle(const render::Event&);
|
||||
void draw(render::Snapshot&);
|
||||
bool update_text(const std::string& id, const std::string& value, bool force = false);
|
||||
bool update_number(const std::string& id, double value, bool force = false);
|
||||
bool focus(const std::string& id);
|
||||
const std::string& focused_id() const;
|
||||
void clear_focus(bool commit = true);
|
||||
bool editing() const;
|
||||
FontAtlas& font();
|
||||
void set_clipboard(std::function<std::string()> read,
|
||||
std::function<void(const std::string&)> write);
|
||||
// Hook to Renderer::set_text_input and set_text_input_area.
|
||||
void set_ime(std::function<void(bool)> enabled, std::function<void(Rect)> rectangle);
|
||||
void set_docking(DockLayout*, std::function<void()> changed = {});
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
} // namespace faset::ui
|
||||
Reference in New Issue
Block a user