调整图片画布路由和画布数据模型

将图片画布入口改为 /editor/canvas

新增 editor_canvas 表并关联 editor_project 默认画布

更新 project API 响应中的 canvas 快照兼容层

统一图片画布侧栏列表项和图标按钮组件

同步前端测试、SpacetimeDB bindings、技术文档和 TRACKING 记录
This commit is contained in:
2026-06-13 22:09:45 +08:00
parent a1b9ac8544
commit 242860e2d3
21 changed files with 1649 additions and 295 deletions

View File

@@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use shared_kernel::build_prefixed_uuid_id;
use spacetime_client::{
EditorCanvasViewportRecord, EditorProjectCreateRecordInput, EditorProjectGetRecordInput,
EditorProjectLayoutSaveRecordInput, EditorProjectRecord,
EditorCanvasRecord, EditorCanvasViewportRecord, EditorProjectCreateRecordInput,
EditorProjectGetRecordInput, EditorProjectLayoutSaveRecordInput, EditorProjectRecord,
EditorProjectResourceCreateRecordInput, EditorProjectResourceRecord, SpacetimeClientError,
};
@@ -120,12 +120,25 @@ pub struct EditorImageGenerationResponse {
pub struct EditorProjectPayload {
project_id: String,
title: String,
canvas: EditorCanvasPayload,
viewport: EditorCanvasViewportPayload,
layers: Value,
resources: Vec<EditorProjectResourcePayload>,
updated_at: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorCanvasPayload {
canvas_id: String,
project_id: String,
title: String,
viewport: EditorCanvasViewportPayload,
layers: Value,
created_at: String,
updated_at: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorProjectResourcePayload {
@@ -417,9 +430,11 @@ pub async fn edit_editor_image(
}
fn editor_project_payload_from_record(record: EditorProjectRecord) -> EditorProjectPayload {
let canvas = editor_canvas_payload_from_record(record.canvas);
EditorProjectPayload {
project_id: record.project_id,
title: record.title,
canvas,
viewport: EditorCanvasViewportPayload {
x: record.viewport.x,
y: record.viewport.y,
@@ -435,6 +450,22 @@ fn editor_project_payload_from_record(record: EditorProjectRecord) -> EditorProj
}
}
fn editor_canvas_payload_from_record(record: EditorCanvasRecord) -> EditorCanvasPayload {
EditorCanvasPayload {
canvas_id: record.canvas_id,
project_id: record.project_id,
title: record.title,
viewport: EditorCanvasViewportPayload {
x: record.viewport.x,
y: record.viewport.y,
scale: record.viewport.scale,
},
layers: record.layers,
created_at: record.created_at,
updated_at: record.updated_at,
}
}
fn editor_project_resource_payload_from_record(
record: EditorProjectResourceRecord,
) -> EditorProjectResourcePayload {

View File

@@ -30,9 +30,9 @@ pub use mapper::{
CustomWorldPublishGateRecord, CustomWorldPublishWorldRecord,
CustomWorldPublishWorldRecordInput, CustomWorldPublishedProfileCompileRecord,
CustomWorldResultPreviewBlockerRecord, CustomWorldSupportedActionRecord,
CustomWorldWorkSummaryRecord, EditorCanvasViewportRecord, EditorProjectCreateRecordInput,
EditorProjectGetRecordInput, EditorProjectLayoutSaveRecordInput, EditorProjectRecord,
EditorProjectResourceCreateRecordInput, EditorProjectResourceRecord,
CustomWorldWorkSummaryRecord, EditorCanvasRecord, EditorCanvasViewportRecord,
EditorProjectCreateRecordInput, EditorProjectGetRecordInput, EditorProjectLayoutSaveRecordInput,
EditorProjectRecord, EditorProjectResourceCreateRecordInput, EditorProjectResourceRecord,
ExternalGenerationJobClaimRecordInput,
ExternalGenerationJobCompleteRecordInput, ExternalGenerationJobEnqueueRecordInput,
ExternalGenerationJobFailRecordInput, ExternalGenerationJobGetRecordInput,

View File

@@ -43,8 +43,8 @@ pub use self::combat::{
ResolveCombatActionRecord,
};
pub use self::editor_project::{
EditorCanvasViewportRecord, EditorProjectCreateRecordInput, EditorProjectGetRecordInput,
EditorProjectLayoutSaveRecordInput, EditorProjectRecord,
EditorCanvasRecord, EditorCanvasViewportRecord, EditorProjectCreateRecordInput,
EditorProjectGetRecordInput, EditorProjectLayoutSaveRecordInput, EditorProjectRecord,
EditorProjectResourceCreateRecordInput, EditorProjectResourceRecord,
};
pub use self::common::{

View File

@@ -12,6 +12,7 @@ pub struct EditorProjectRecord {
pub project_id: String,
pub owner_user_id: String,
pub title: String,
pub canvas: EditorCanvasRecord,
pub viewport: EditorCanvasViewportRecord,
pub layers: serde_json::Value,
pub resources: Vec<EditorProjectResourceRecord>,
@@ -19,6 +20,17 @@ pub struct EditorProjectRecord {
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct EditorCanvasRecord {
pub canvas_id: String,
pub project_id: String,
pub title: String,
pub viewport: EditorCanvasViewportRecord,
pub layers: serde_json::Value,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EditorProjectResourceRecord {
pub resource_id: String,
@@ -178,19 +190,31 @@ pub(crate) fn map_editor_project_resource_procedure_result(
fn map_editor_project_snapshot(
snapshot: EditorProjectSnapshot,
) -> Result<EditorProjectRecord, SpacetimeClientError> {
let layers = serde_json::from_str(&snapshot.layers_json).map_err(|error| {
let layers: serde_json::Value =
serde_json::from_str(&snapshot.canvas.layers_json).map_err(|error| {
SpacetimeClientError::validation_failed(format!("图片画布图层布局 JSON 无法解析:{error}"))
})?;
let viewport = EditorCanvasViewportRecord {
x: snapshot.canvas.viewport.x,
y: snapshot.canvas.viewport.y,
scale: snapshot.canvas.viewport.scale,
};
let canvas = EditorCanvasRecord {
canvas_id: snapshot.canvas.canvas_id,
project_id: snapshot.canvas.project_id,
title: snapshot.canvas.title,
viewport: viewport.clone(),
layers: layers.clone(),
created_at: format_timestamp_micros(snapshot.canvas.created_at_micros),
updated_at: format_timestamp_micros(snapshot.canvas.updated_at_micros),
};
Ok(EditorProjectRecord {
project_id: snapshot.project_id,
owner_user_id: snapshot.owner_user_id,
title: snapshot.title,
viewport: EditorCanvasViewportRecord {
x: snapshot.viewport.x,
y: snapshot.viewport.y,
scale: snapshot.viewport.scale,
},
canvas,
viewport,
layers,
resources: snapshot
.resources

View File

@@ -347,6 +347,9 @@ pub mod delete_visual_novel_work_procedure;
pub mod delete_wooden_fish_work_procedure;
pub mod drag_puzzle_piece_or_group_procedure;
pub mod drop_square_hole_shape_procedure;
pub mod editor_canvas_snapshot_type;
pub mod editor_canvas_table;
pub mod editor_canvas_type;
pub mod editor_project_create_input_type;
pub mod editor_project_get_input_type;
pub mod editor_project_get_recent_input_type;
@@ -362,7 +365,6 @@ pub mod editor_project_table;
pub mod editor_project_type;
pub mod editor_project_viewport_snapshot_type;
pub mod enqueue_external_generation_job_and_return_procedure;
pub mod ensure_analytics_date_dimension_for_date_reducer;
pub mod equip_inventory_item_input_type;
pub mod execute_custom_world_agent_action_procedure;
@@ -409,7 +411,6 @@ pub mod get_custom_world_library_detail_procedure;
pub mod get_editor_project_and_return_procedure;
pub mod get_external_generation_job_and_return_procedure;
pub mod get_external_generation_queue_stats_and_return_procedure;
pub mod get_jump_hop_agent_session_procedure;
pub mod get_jump_hop_leaderboard_procedure;
pub mod get_jump_hop_run_procedure;
@@ -1504,6 +1505,9 @@ pub use delete_visual_novel_work_procedure::delete_visual_novel_work;
pub use delete_wooden_fish_work_procedure::delete_wooden_fish_work;
pub use drag_puzzle_piece_or_group_procedure::drag_puzzle_piece_or_group;
pub use drop_square_hole_shape_procedure::drop_square_hole_shape;
pub use editor_canvas_snapshot_type::EditorCanvasSnapshot;
pub use editor_canvas_table::*;
pub use editor_canvas_type::EditorCanvas;
pub use editor_project_create_input_type::EditorProjectCreateInput;
pub use editor_project_get_input_type::EditorProjectGetInput;
pub use editor_project_get_recent_input_type::EditorProjectGetRecentInput;
@@ -1519,7 +1523,6 @@ pub use editor_project_table::*;
pub use editor_project_type::EditorProject;
pub use editor_project_viewport_snapshot_type::EditorProjectViewportSnapshot;
pub use enqueue_external_generation_job_and_return_procedure::enqueue_external_generation_job_and_return;
pub use ensure_analytics_date_dimension_for_date_reducer::ensure_analytics_date_dimension_for_date;
pub use equip_inventory_item_input_type::EquipInventoryItemInput;
pub use execute_custom_world_agent_action_procedure::execute_custom_world_agent_action;
@@ -1566,7 +1569,6 @@ pub use get_custom_world_library_detail_procedure::get_custom_world_library_deta
pub use get_editor_project_and_return_procedure::get_editor_project_and_return;
pub use get_external_generation_job_and_return_procedure::get_external_generation_job_and_return;
pub use get_external_generation_queue_stats_and_return_procedure::get_external_generation_queue_stats_and_return;
pub use get_jump_hop_agent_session_procedure::get_jump_hop_agent_session;
pub use get_jump_hop_leaderboard_procedure::get_jump_hop_leaderboard;
pub use get_jump_hop_run_procedure::get_jump_hop_run;
@@ -2631,10 +2633,10 @@ pub struct DbUpdate {
custom_world_session: __sdk::TableUpdate<CustomWorldSession>,
database_migration_import_chunk: __sdk::TableUpdate<DatabaseMigrationImportChunk>,
database_migration_operator: __sdk::TableUpdate<DatabaseMigrationOperator>,
editor_canvas: __sdk::TableUpdate<EditorCanvas>,
editor_project: __sdk::TableUpdate<EditorProject>,
editor_project_resource: __sdk::TableUpdate<EditorProjectResource>,
external_generation_job: __sdk::TableUpdate<ExternalGenerationJob>,
inventory_slot: __sdk::TableUpdate<InventorySlot>,
jump_hop_agent_session: __sdk::TableUpdate<JumpHopAgentSessionRow>,
jump_hop_event: __sdk::TableUpdate<JumpHopEventRow>,
@@ -2847,6 +2849,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
"database_migration_operator" => db_update.database_migration_operator.append(
database_migration_operator_table::parse_table_update(table_update)?,
),
"editor_canvas" => db_update
.editor_canvas
.append(editor_canvas_table::parse_table_update(table_update)?),
"editor_project" => db_update
.editor_project
.append(editor_project_table::parse_table_update(table_update)?),
@@ -3316,6 +3321,9 @@ impl __sdk::DbUpdate for DbUpdate {
&self.database_migration_operator,
)
.with_updates_by_pk(|row| &row.operator_identity);
diff.editor_canvas = cache
.apply_diff_to_table::<EditorCanvas>("editor_canvas", &self.editor_canvas)
.with_updates_by_pk(|row| &row.canvas_id);
diff.editor_project = cache
.apply_diff_to_table::<EditorProject>("editor_project", &self.editor_project)
.with_updates_by_pk(|row| &row.project_id);
@@ -3331,7 +3339,6 @@ impl __sdk::DbUpdate for DbUpdate {
&self.external_generation_job,
)
.with_updates_by_pk(|row| &row.job_id);
diff.inventory_slot = cache
.apply_diff_to_table::<InventorySlot>("inventory_slot", &self.inventory_slot)
.with_updates_by_pk(|row| &row.slot_id);
@@ -3859,6 +3866,9 @@ impl __sdk::DbUpdate for DbUpdate {
"database_migration_operator" => db_update
.database_migration_operator
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"editor_canvas" => db_update
.editor_canvas
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"editor_project" => db_update
.editor_project
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
@@ -4235,6 +4245,9 @@ impl __sdk::DbUpdate for DbUpdate {
"database_migration_operator" => db_update
.database_migration_operator
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"editor_canvas" => db_update
.editor_canvas
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"editor_project" => db_update
.editor_project
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
@@ -4537,10 +4550,10 @@ pub struct AppliedDiff<'r> {
custom_world_session: __sdk::TableAppliedDiff<'r, CustomWorldSession>,
database_migration_import_chunk: __sdk::TableAppliedDiff<'r, DatabaseMigrationImportChunk>,
database_migration_operator: __sdk::TableAppliedDiff<'r, DatabaseMigrationOperator>,
editor_canvas: __sdk::TableAppliedDiff<'r, EditorCanvas>,
editor_project: __sdk::TableAppliedDiff<'r, EditorProject>,
editor_project_resource: __sdk::TableAppliedDiff<'r, EditorProjectResource>,
external_generation_job: __sdk::TableAppliedDiff<'r, ExternalGenerationJob>,
inventory_slot: __sdk::TableAppliedDiff<'r, InventorySlot>,
jump_hop_agent_session: __sdk::TableAppliedDiff<'r, JumpHopAgentSessionRow>,
jump_hop_event: __sdk::TableAppliedDiff<'r, JumpHopEventRow>,
@@ -4821,6 +4834,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
&self.database_migration_operator,
event,
);
callbacks.invoke_table_row_callbacks::<EditorCanvas>(
"editor_canvas",
&self.editor_canvas,
event,
);
callbacks.invoke_table_row_callbacks::<EditorProject>(
"editor_project",
&self.editor_project,
@@ -5918,10 +5936,10 @@ impl __sdk::SpacetimeModule for RemoteModule {
custom_world_session_table::register_table(client_cache);
database_migration_import_chunk_table::register_table(client_cache);
database_migration_operator_table::register_table(client_cache);
editor_canvas_table::register_table(client_cache);
editor_project_table::register_table(client_cache);
editor_project_resource_table::register_table(client_cache);
external_generation_job_table::register_table(client_cache);
inventory_slot_table::register_table(client_cache);
jump_hop_agent_session_table::register_table(client_cache);
jump_hop_event_table::register_table(client_cache);
@@ -6042,10 +6060,10 @@ impl __sdk::SpacetimeModule for RemoteModule {
"custom_world_session",
"database_migration_import_chunk",
"database_migration_operator",
"editor_canvas",
"editor_project",
"editor_project_resource",
"external_generation_job",
"inventory_slot",
"jump_hop_agent_session",
"jump_hop_event",

View File

@@ -0,0 +1,23 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::editor_project_viewport_snapshot_type::EditorProjectViewportSnapshot;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct EditorCanvasSnapshot {
pub canvas_id: String,
pub project_id: String,
pub title: String,
pub viewport: EditorProjectViewportSnapshot,
pub layers_json: String,
pub created_at_micros: i64,
pub updated_at_micros: i64,
}
impl __sdk::InModule for EditorCanvasSnapshot {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,159 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use super::editor_canvas_type::EditorCanvas;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `editor_canvas`.
///
/// Obtain a handle from the [`EditorCanvasTableAccess::editor_canvas`] method on [`super::RemoteTables`],
/// like `ctx.db.editor_canvas()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.editor_canvas().on_insert(...)`.
pub struct EditorCanvasTableHandle<'ctx> {
imp: __sdk::TableHandle<EditorCanvas>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `editor_canvas`.
///
/// Implemented for [`super::RemoteTables`].
pub trait EditorCanvasTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`EditorCanvasTableHandle`], which mediates access to the table `editor_canvas`.
fn editor_canvas(&self) -> EditorCanvasTableHandle<'_>;
}
impl EditorCanvasTableAccess for super::RemoteTables {
fn editor_canvas(&self) -> EditorCanvasTableHandle<'_> {
EditorCanvasTableHandle {
imp: self.imp.get_table::<EditorCanvas>("editor_canvas"),
ctx: std::marker::PhantomData,
}
}
}
pub struct EditorCanvasInsertCallbackId(__sdk::CallbackId);
pub struct EditorCanvasDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for EditorCanvasTableHandle<'ctx> {
type Row = EditorCanvas;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = EditorCanvas> + '_ {
self.imp.iter()
}
type InsertCallbackId = EditorCanvasInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> EditorCanvasInsertCallbackId {
EditorCanvasInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: EditorCanvasInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = EditorCanvasDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> EditorCanvasDeleteCallbackId {
EditorCanvasDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: EditorCanvasDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct EditorCanvasUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for EditorCanvasTableHandle<'ctx> {
type UpdateCallbackId = EditorCanvasUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> EditorCanvasUpdateCallbackId {
EditorCanvasUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: EditorCanvasUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `canvas_id` unique index on the table `editor_canvas`,
/// which allows point queries on the field of the same name
/// via the [`EditorCanvasCanvasIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.editor_canvas().canvas_id().find(...)`.
pub struct EditorCanvasCanvasIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<EditorCanvas, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> EditorCanvasTableHandle<'ctx> {
/// Get a handle on the `canvas_id` unique index on the table `editor_canvas`.
pub fn canvas_id(&self) -> EditorCanvasCanvasIdUnique<'ctx> {
EditorCanvasCanvasIdUnique {
imp: self.imp.get_unique_constraint::<String>("canvas_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> EditorCanvasCanvasIdUnique<'ctx> {
/// Find the subscribed row whose `canvas_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &String) -> Option<EditorCanvas> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table = client_cache.get_or_make_table::<EditorCanvas>("editor_canvas");
_table.add_unique_constraint::<String>("canvas_id", |row| &row.canvas_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<EditorCanvas>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse("TableUpdate<EditorCanvas>", "TableUpdate")
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `EditorCanvas`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait editor_canvasQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `EditorCanvas`.
fn editor_canvas(&self) -> __sdk::__query_builder::Table<EditorCanvas>;
}
impl editor_canvasQueryTableAccess for __sdk::QueryTableAccessor {
fn editor_canvas(&self) -> __sdk::__query_builder::Table<EditorCanvas> {
__sdk::__query_builder::Table::new("editor_canvas")
}
}

View File

@@ -0,0 +1,80 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct EditorCanvas {
pub canvas_id: String,
pub project_id: String,
pub owner_user_id: String,
pub title: String,
pub viewport_x: f64,
pub viewport_y: f64,
pub viewport_scale: f64,
pub layers_json: String,
pub created_at: __sdk::Timestamp,
pub updated_at: __sdk::Timestamp,
}
impl __sdk::InModule for EditorCanvas {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `EditorCanvas`.
///
/// Provides typed access to columns for query building.
pub struct EditorCanvasCols {
pub canvas_id: __sdk::__query_builder::Col<EditorCanvas, String>,
pub project_id: __sdk::__query_builder::Col<EditorCanvas, String>,
pub owner_user_id: __sdk::__query_builder::Col<EditorCanvas, String>,
pub title: __sdk::__query_builder::Col<EditorCanvas, String>,
pub viewport_x: __sdk::__query_builder::Col<EditorCanvas, f64>,
pub viewport_y: __sdk::__query_builder::Col<EditorCanvas, f64>,
pub viewport_scale: __sdk::__query_builder::Col<EditorCanvas, f64>,
pub layers_json: __sdk::__query_builder::Col<EditorCanvas, String>,
pub created_at: __sdk::__query_builder::Col<EditorCanvas, __sdk::Timestamp>,
pub updated_at: __sdk::__query_builder::Col<EditorCanvas, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for EditorCanvas {
type Cols = EditorCanvasCols;
fn cols(table_name: &'static str) -> Self::Cols {
EditorCanvasCols {
canvas_id: __sdk::__query_builder::Col::new(table_name, "canvas_id"),
project_id: __sdk::__query_builder::Col::new(table_name, "project_id"),
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
title: __sdk::__query_builder::Col::new(table_name, "title"),
viewport_x: __sdk::__query_builder::Col::new(table_name, "viewport_x"),
viewport_y: __sdk::__query_builder::Col::new(table_name, "viewport_y"),
viewport_scale: __sdk::__query_builder::Col::new(table_name, "viewport_scale"),
layers_json: __sdk::__query_builder::Col::new(table_name, "layers_json"),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
}
}
}
/// Indexed column accessor struct for the table `EditorCanvas`.
///
/// Provides typed access to indexed columns for query building.
pub struct EditorCanvasIxCols {
pub canvas_id: __sdk::__query_builder::IxCol<EditorCanvas, String>,
pub owner_user_id: __sdk::__query_builder::IxCol<EditorCanvas, String>,
pub project_id: __sdk::__query_builder::IxCol<EditorCanvas, String>,
}
impl __sdk::__query_builder::HasIxCols for EditorCanvas {
type IxCols = EditorCanvasIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
EditorCanvasIxCols {
canvas_id: __sdk::__query_builder::IxCol::new(table_name, "canvas_id"),
owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"),
project_id: __sdk::__query_builder::IxCol::new(table_name, "project_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for EditorCanvas {}

View File

@@ -4,8 +4,8 @@
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::editor_canvas_snapshot_type::EditorCanvasSnapshot;
use super::editor_project_resource_snapshot_type::EditorProjectResourceSnapshot;
use super::editor_project_viewport_snapshot_type::EditorProjectViewportSnapshot;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -13,8 +13,7 @@ pub struct EditorProjectSnapshot {
pub project_id: String,
pub owner_user_id: String,
pub title: String,
pub viewport: EditorProjectViewportSnapshot,
pub layers_json: String,
pub canvas: EditorCanvasSnapshot,
pub resources: Vec<EditorProjectResourceSnapshot>,
pub created_at_micros: i64,
pub updated_at_micros: i64,

View File

@@ -1,6 +1,7 @@
use crate::*;
const EDITOR_PROJECT_DEFAULT_TITLE: &str = "未命名画布";
const EDITOR_CANVAS_DEFAULT_TITLE: &str = "默认画布";
const EDITOR_PROJECT_MAX_TITLE_CHARS: usize = 80;
const EDITOR_PROJECT_MAX_LAYOUT_JSON_BYTES: usize = 256 * 1024;
const EDITOR_PROJECT_SOURCE_TYPES: [&str; 3] = ["uploaded", "generated", "mock_generated"];
@@ -22,6 +23,25 @@ pub struct EditorProject {
updated_at: Timestamp,
}
#[spacetimedb::table(
accessor = editor_canvas,
index(accessor = by_editor_canvas_project_id, btree(columns = [project_id])),
index(accessor = by_editor_canvas_owner_user_id, btree(columns = [owner_user_id]))
)]
pub struct EditorCanvas {
#[primary_key]
canvas_id: String,
project_id: String,
owner_user_id: String,
title: String,
viewport_x: f64,
viewport_y: f64,
viewport_scale: f64,
layers_json: String,
created_at: Timestamp,
updated_at: Timestamp,
}
#[spacetimedb::table(
accessor = editor_project_resource,
index(accessor = by_editor_project_resource_project_id, btree(columns = [project_id])),
@@ -123,13 +143,23 @@ pub struct EditorProjectResourceSnapshot {
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
pub struct EditorCanvasSnapshot {
pub canvas_id: String,
pub project_id: String,
pub title: String,
pub viewport: EditorProjectViewportSnapshot,
pub layers_json: String,
pub created_at_micros: i64,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
pub struct EditorProjectSnapshot {
pub project_id: String,
pub owner_user_id: String,
pub title: String,
pub viewport: EditorProjectViewportSnapshot,
pub layers_json: String,
pub canvas: EditorCanvasSnapshot,
pub resources: Vec<EditorProjectResourceSnapshot>,
pub created_at_micros: i64,
pub updated_at_micros: i64,
@@ -233,7 +263,7 @@ fn create_editor_project(
ctx.db.editor_project().insert(EditorProject {
project_id: project_id.clone(),
owner_user_id,
owner_user_id: owner_user_id.clone(),
title,
viewport_x: 0.0,
viewport_y: 0.0,
@@ -242,6 +272,13 @@ fn create_editor_project(
created_at: now,
updated_at: now,
});
ensure_default_canvas(
ctx,
project_id.as_str(),
owner_user_id.as_str(),
None,
now,
)?;
build_project_snapshot(ctx, project_id.as_str())
}
@@ -290,7 +327,28 @@ fn save_editor_project_layout(
let owner_user_id = normalize_required(&input.owner_user_id, "editor_project.owner_user_id")?;
let layers_json = normalize_layout_json(input.layers_json)?;
let project = require_owned_project(ctx, project_id.as_str(), owner_user_id.as_str())?;
let now = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
let canvas = ensure_default_canvas(
ctx,
project.project_id.as_str(),
project.owner_user_id.as_str(),
Some(&project),
now,
)?;
ctx.db.editor_canvas().canvas_id().delete(&canvas.canvas_id);
ctx.db.editor_canvas().insert(EditorCanvas {
canvas_id: canvas.canvas_id,
project_id: project.project_id.clone(),
owner_user_id: project.owner_user_id.clone(),
title: canvas.title,
viewport_x: input.viewport.x,
viewport_y: input.viewport.y,
viewport_scale: input.viewport.scale.clamp(0.01, 8.0),
layers_json: layers_json.clone(),
created_at: canvas.created_at,
updated_at: now,
});
ctx.db.editor_project().project_id().delete(&project_id);
ctx.db.editor_project().insert(EditorProject {
project_id: project.project_id.clone(),
@@ -301,7 +359,7 @@ fn save_editor_project_layout(
viewport_scale: input.viewport.scale.clamp(0.01, 8.0),
layers_json,
created_at: project.created_at,
updated_at: Timestamp::from_micros_since_unix_epoch(input.updated_at_micros),
updated_at: now,
});
build_project_snapshot(ctx, project_id.as_str())
@@ -384,6 +442,13 @@ fn build_project_snapshot(
.project_id()
.find(&project_key)
.ok_or_else(|| "图片画布工程不存在".to_string())?;
let canvas = ensure_default_canvas(
ctx,
project.project_id.as_str(),
project.owner_user_id.as_str(),
Some(&project),
project.created_at,
)?;
let mut resources = ctx
.db
.editor_project_resource()
@@ -401,18 +466,77 @@ fn build_project_snapshot(
project_id: project.project_id,
owner_user_id: project.owner_user_id,
title: project.title,
viewport: EditorProjectViewportSnapshot {
x: project.viewport_x,
y: project.viewport_y,
scale: project.viewport_scale,
},
layers_json: project.layers_json,
canvas: canvas_snapshot_from_row(canvas),
resources,
created_at_micros: project.created_at.to_micros_since_unix_epoch(),
updated_at_micros: project.updated_at.to_micros_since_unix_epoch(),
})
}
fn ensure_default_canvas(
ctx: &ReducerContext,
project_id: &str,
owner_user_id: &str,
legacy_project: Option<&EditorProject>,
now: Timestamp,
) -> Result<EditorCanvas, String> {
let canvas_id = default_canvas_id(project_id);
if let Some(canvas) = ctx.db.editor_canvas().canvas_id().find(&canvas_id) {
return Ok(canvas);
}
let legacy_view = legacy_project
.map(|project| {
(
project.viewport_x,
project.viewport_y,
project.viewport_scale,
project.layers_json.clone(),
project.created_at,
project.updated_at,
)
})
.unwrap_or((0.0, 0.0, 1.0, "[]".to_string(), now, now));
let canvas = EditorCanvas {
canvas_id: canvas_id.clone(),
project_id: project_id.to_string(),
owner_user_id: owner_user_id.to_string(),
title: EDITOR_CANVAS_DEFAULT_TITLE.to_string(),
viewport_x: legacy_view.0,
viewport_y: legacy_view.1,
viewport_scale: legacy_view.2,
layers_json: legacy_view.3,
created_at: legacy_view.4,
updated_at: legacy_view.5,
};
ctx.db.editor_canvas().insert(canvas);
ctx.db
.editor_canvas()
.canvas_id()
.find(&canvas_id)
.ok_or_else(|| "图片画布创建失败".to_string())
}
fn default_canvas_id(project_id: &str) -> String {
format!("{project_id}:canvas:default")
}
fn canvas_snapshot_from_row(row: EditorCanvas) -> EditorCanvasSnapshot {
EditorCanvasSnapshot {
canvas_id: row.canvas_id,
project_id: row.project_id,
title: row.title,
viewport: EditorProjectViewportSnapshot {
x: row.viewport_x,
y: row.viewport_y,
scale: row.viewport_scale,
},
layers_json: row.layers_json,
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
}
}
fn require_owned_project(
ctx: &ReducerContext,
project_id: &str,

View File

@@ -230,6 +230,7 @@ macro_rules! migration_tables {
asset_entity_binding,
asset_event,
editor_project,
editor_canvas,
editor_project_resource,
puzzle_agent_session,
puzzle_background_compile_task,