Fix prop renders
* Incorporate prop scaling * Props now render to a canvas
This commit is contained in:
parent
af89394df1
commit
a2841c413d
21 changed files with 942 additions and 353 deletions
|
|
@ -889,6 +889,8 @@ pub struct LooseProp {
|
|||
pub realm_prop_id: Option<Uuid>,
|
||||
pub position_x: f64,
|
||||
pub position_y: f64,
|
||||
/// Scale factor (0.1 - 10.0) inherited from prop definition at drop time.
|
||||
pub scale: f32,
|
||||
pub dropped_by: Option<Uuid>,
|
||||
pub expires_at: Option<DateTime<Utc>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
|
@ -915,6 +917,8 @@ pub struct ServerProp {
|
|||
pub default_emotion: Option<EmotionState>,
|
||||
/// Grid position (0-8): top row 0,1,2 / middle 3,4,5 / bottom 6,7,8
|
||||
pub default_position: Option<i16>,
|
||||
/// Default scale factor (0.1 - 10.0) applied when prop is dropped to canvas.
|
||||
pub default_scale: f32,
|
||||
pub is_unique: bool,
|
||||
pub is_transferable: bool,
|
||||
pub is_portable: bool,
|
||||
|
|
@ -966,6 +970,9 @@ pub struct CreateServerPropRequest {
|
|||
/// Whether prop appears in the public Server inventory tab.
|
||||
#[serde(default)]
|
||||
pub public: Option<bool>,
|
||||
/// Default scale factor (0.1 - 10.0) applied when prop is dropped to canvas.
|
||||
#[serde(default)]
|
||||
pub default_scale: Option<f32>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
|
|
@ -999,6 +1006,14 @@ impl CreateServerPropRequest {
|
|||
.to_string(),
|
||||
));
|
||||
}
|
||||
// Validate scale range (0.1 - 10.0)
|
||||
if let Some(scale) = self.default_scale {
|
||||
if !(0.1..=10.0).contains(&scale) {
|
||||
return Err(AppError::Validation(
|
||||
"default_scale must be between 0.1 and 10.0".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ pub async fn list_channel_loose_props<'e>(
|
|||
lp.realm_prop_id,
|
||||
ST_X(lp.position) as position_x,
|
||||
ST_Y(lp.position) as position_y,
|
||||
lp.scale,
|
||||
lp.dropped_by,
|
||||
lp.expires_at,
|
||||
lp.created_at,
|
||||
|
|
@ -81,6 +82,7 @@ pub async fn drop_prop_to_canvas<'e>(
|
|||
) -> Result<LooseProp, AppError> {
|
||||
// Single CTE that checks existence/droppability and performs the operation atomically.
|
||||
// Returns status flags plus the LooseProp data (if successful).
|
||||
// Includes scale inherited from the source prop's default_scale.
|
||||
let result: Option<(
|
||||
bool,
|
||||
bool,
|
||||
|
|
@ -91,6 +93,7 @@ pub async fn drop_prop_to_canvas<'e>(
|
|||
Option<Uuid>,
|
||||
Option<f32>,
|
||||
Option<f32>,
|
||||
Option<f32>,
|
||||
Option<Uuid>,
|
||||
Option<chrono::DateTime<chrono::Utc>>,
|
||||
Option<chrono::DateTime<chrono::Utc>>,
|
||||
|
|
@ -99,9 +102,18 @@ pub async fn drop_prop_to_canvas<'e>(
|
|||
)> = sqlx::query_as(
|
||||
r#"
|
||||
WITH item_info AS (
|
||||
SELECT id, is_droppable, server_prop_id, realm_prop_id, prop_name, prop_asset_path
|
||||
FROM auth.inventory
|
||||
WHERE id = $1 AND user_id = $2
|
||||
SELECT
|
||||
inv.id,
|
||||
inv.is_droppable,
|
||||
inv.server_prop_id,
|
||||
inv.realm_prop_id,
|
||||
inv.prop_name,
|
||||
inv.prop_asset_path,
|
||||
COALESCE(sp.default_scale, rp.default_scale, 1.0) as default_scale
|
||||
FROM auth.inventory inv
|
||||
LEFT JOIN server.props sp ON inv.server_prop_id = sp.id
|
||||
LEFT JOIN realm.props rp ON inv.realm_prop_id = rp.id
|
||||
WHERE inv.id = $1 AND inv.user_id = $2
|
||||
),
|
||||
deleted_item AS (
|
||||
DELETE FROM auth.inventory
|
||||
|
|
@ -114,6 +126,7 @@ pub async fn drop_prop_to_canvas<'e>(
|
|||
server_prop_id,
|
||||
realm_prop_id,
|
||||
position,
|
||||
scale,
|
||||
dropped_by,
|
||||
expires_at
|
||||
)
|
||||
|
|
@ -122,6 +135,7 @@ pub async fn drop_prop_to_canvas<'e>(
|
|||
di.server_prop_id,
|
||||
di.realm_prop_id,
|
||||
public.make_virtual_point($4::real, $5::real),
|
||||
(SELECT default_scale FROM item_info),
|
||||
$2,
|
||||
now() + interval '30 minutes'
|
||||
FROM deleted_item di
|
||||
|
|
@ -132,6 +146,7 @@ pub async fn drop_prop_to_canvas<'e>(
|
|||
realm_prop_id,
|
||||
ST_X(position)::real as position_x,
|
||||
ST_Y(position)::real as position_y,
|
||||
scale,
|
||||
dropped_by,
|
||||
expires_at,
|
||||
created_at
|
||||
|
|
@ -146,6 +161,7 @@ pub async fn drop_prop_to_canvas<'e>(
|
|||
ip.realm_prop_id,
|
||||
ip.position_x,
|
||||
ip.position_y,
|
||||
ip.scale,
|
||||
ip.dropped_by,
|
||||
ip.expires_at,
|
||||
ip.created_at,
|
||||
|
|
@ -171,19 +187,19 @@ pub async fn drop_prop_to_canvas<'e>(
|
|||
"Unexpected error dropping prop to canvas".to_string(),
|
||||
))
|
||||
}
|
||||
Some((false, _, _, _, _, _, _, _, _, _, _, _, _, _)) => {
|
||||
Some((false, _, _, _, _, _, _, _, _, _, _, _, _, _, _)) => {
|
||||
// Item didn't exist
|
||||
Err(AppError::NotFound(
|
||||
"Inventory item not found or not owned by user".to_string(),
|
||||
))
|
||||
}
|
||||
Some((true, false, _, _, _, _, _, _, _, _, _, _, _, _)) => {
|
||||
Some((true, false, _, _, _, _, _, _, _, _, _, _, _, _, _)) => {
|
||||
// Item existed but is not droppable
|
||||
Err(AppError::Forbidden(
|
||||
"This prop cannot be dropped - it is an essential prop".to_string(),
|
||||
))
|
||||
}
|
||||
Some((true, true, false, _, _, _, _, _, _, _, _, _, _, _)) => {
|
||||
Some((true, true, false, _, _, _, _, _, _, _, _, _, _, _, _)) => {
|
||||
// Item was droppable but delete failed (shouldn't happen)
|
||||
Err(AppError::Internal(
|
||||
"Unexpected error dropping prop to canvas".to_string(),
|
||||
|
|
@ -199,6 +215,7 @@ pub async fn drop_prop_to_canvas<'e>(
|
|||
realm_prop_id,
|
||||
Some(position_x),
|
||||
Some(position_y),
|
||||
Some(scale),
|
||||
dropped_by,
|
||||
Some(expires_at),
|
||||
Some(created_at),
|
||||
|
|
@ -213,6 +230,7 @@ pub async fn drop_prop_to_canvas<'e>(
|
|||
realm_prop_id,
|
||||
position_x: position_x.into(),
|
||||
position_y: position_y.into(),
|
||||
scale,
|
||||
dropped_by,
|
||||
expires_at: Some(expires_at),
|
||||
created_at,
|
||||
|
|
@ -313,6 +331,102 @@ pub async fn pick_up_loose_prop<'e>(
|
|||
Ok(item)
|
||||
}
|
||||
|
||||
/// Update the scale of a loose prop.
|
||||
///
|
||||
/// Server admins can update any loose prop.
|
||||
/// Realm admins can update loose props in their realm.
|
||||
pub async fn update_loose_prop_scale<'e>(
|
||||
executor: impl PgExecutor<'e>,
|
||||
loose_prop_id: Uuid,
|
||||
scale: f32,
|
||||
) -> Result<LooseProp, AppError> {
|
||||
// Validate scale range
|
||||
if !(0.1..=10.0).contains(&scale) {
|
||||
return Err(AppError::Validation(
|
||||
"Scale must be between 0.1 and 10.0".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let prop = sqlx::query_as::<_, LooseProp>(
|
||||
r#"
|
||||
WITH updated AS (
|
||||
UPDATE scene.loose_props
|
||||
SET scale = $2
|
||||
WHERE id = $1
|
||||
AND (expires_at IS NULL OR expires_at > now())
|
||||
RETURNING
|
||||
id,
|
||||
instance_id as channel_id,
|
||||
server_prop_id,
|
||||
realm_prop_id,
|
||||
ST_X(position) as position_x,
|
||||
ST_Y(position) as position_y,
|
||||
scale,
|
||||
dropped_by,
|
||||
expires_at,
|
||||
created_at
|
||||
)
|
||||
SELECT
|
||||
u.id,
|
||||
u.channel_id,
|
||||
u.server_prop_id,
|
||||
u.realm_prop_id,
|
||||
u.position_x,
|
||||
u.position_y,
|
||||
u.scale,
|
||||
u.dropped_by,
|
||||
u.expires_at,
|
||||
u.created_at,
|
||||
COALESCE(sp.name, rp.name) as prop_name,
|
||||
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path
|
||||
FROM updated u
|
||||
LEFT JOIN server.props sp ON u.server_prop_id = sp.id
|
||||
LEFT JOIN realm.props rp ON u.realm_prop_id = rp.id
|
||||
"#,
|
||||
)
|
||||
.bind(loose_prop_id)
|
||||
.bind(scale)
|
||||
.fetch_optional(executor)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound("Loose prop not found or has expired".to_string()))?;
|
||||
|
||||
Ok(prop)
|
||||
}
|
||||
|
||||
/// Get a loose prop by ID.
|
||||
pub async fn get_loose_prop_by_id<'e>(
|
||||
executor: impl PgExecutor<'e>,
|
||||
loose_prop_id: Uuid,
|
||||
) -> Result<Option<LooseProp>, AppError> {
|
||||
let prop = sqlx::query_as::<_, LooseProp>(
|
||||
r#"
|
||||
SELECT
|
||||
lp.id,
|
||||
lp.instance_id as channel_id,
|
||||
lp.server_prop_id,
|
||||
lp.realm_prop_id,
|
||||
ST_X(lp.position) as position_x,
|
||||
ST_Y(lp.position) as position_y,
|
||||
lp.scale,
|
||||
lp.dropped_by,
|
||||
lp.expires_at,
|
||||
lp.created_at,
|
||||
COALESCE(sp.name, rp.name) as prop_name,
|
||||
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path
|
||||
FROM scene.loose_props lp
|
||||
LEFT JOIN server.props sp ON lp.server_prop_id = sp.id
|
||||
LEFT JOIN realm.props rp ON lp.realm_prop_id = rp.id
|
||||
WHERE lp.id = $1
|
||||
AND (lp.expires_at IS NULL OR lp.expires_at > now())
|
||||
"#,
|
||||
)
|
||||
.bind(loose_prop_id)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(prop)
|
||||
}
|
||||
|
||||
/// Delete expired loose props.
|
||||
///
|
||||
/// Returns the number of props deleted.
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ pub async fn get_server_prop_by_id<'e>(
|
|||
default_layer,
|
||||
default_emotion,
|
||||
default_position,
|
||||
default_scale,
|
||||
is_unique,
|
||||
is_transferable,
|
||||
is_portable,
|
||||
|
|
@ -116,20 +117,23 @@ pub async fn create_server_prop<'e>(
|
|||
|
||||
let is_droppable = req.droppable.unwrap_or(true);
|
||||
let is_public = req.public.unwrap_or(false);
|
||||
let default_scale = req.default_scale.unwrap_or(1.0);
|
||||
|
||||
let prop = sqlx::query_as::<_, ServerProp>(
|
||||
r#"
|
||||
INSERT INTO server.props (
|
||||
name, slug, description, tags, asset_path,
|
||||
default_layer, default_emotion, default_position,
|
||||
default_scale,
|
||||
is_droppable, is_public,
|
||||
created_by
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6::server.avatar_layer, $7::server.emotion_state, $8,
|
||||
$9, $10,
|
||||
$11
|
||||
$9,
|
||||
$10, $11,
|
||||
$12
|
||||
)
|
||||
RETURNING
|
||||
id,
|
||||
|
|
@ -142,6 +146,7 @@ pub async fn create_server_prop<'e>(
|
|||
default_layer,
|
||||
default_emotion,
|
||||
default_position,
|
||||
default_scale,
|
||||
is_unique,
|
||||
is_transferable,
|
||||
is_portable,
|
||||
|
|
@ -163,6 +168,7 @@ pub async fn create_server_prop<'e>(
|
|||
.bind(&default_layer)
|
||||
.bind(&default_emotion)
|
||||
.bind(default_position)
|
||||
.bind(default_scale)
|
||||
.bind(is_droppable)
|
||||
.bind(is_public)
|
||||
.bind(created_by)
|
||||
|
|
@ -207,20 +213,23 @@ pub async fn upsert_server_prop<'e>(
|
|||
|
||||
let is_droppable = req.droppable.unwrap_or(true);
|
||||
let is_public = req.public.unwrap_or(false);
|
||||
let default_scale = req.default_scale.unwrap_or(1.0);
|
||||
|
||||
let prop = sqlx::query_as::<_, ServerProp>(
|
||||
r#"
|
||||
INSERT INTO server.props (
|
||||
name, slug, description, tags, asset_path,
|
||||
default_layer, default_emotion, default_position,
|
||||
default_scale,
|
||||
is_droppable, is_public,
|
||||
created_by
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6::server.avatar_layer, $7::server.emotion_state, $8,
|
||||
$9, $10,
|
||||
$11
|
||||
$9,
|
||||
$10, $11,
|
||||
$12
|
||||
)
|
||||
ON CONFLICT (slug) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
|
|
@ -230,6 +239,7 @@ pub async fn upsert_server_prop<'e>(
|
|||
default_layer = EXCLUDED.default_layer,
|
||||
default_emotion = EXCLUDED.default_emotion,
|
||||
default_position = EXCLUDED.default_position,
|
||||
default_scale = EXCLUDED.default_scale,
|
||||
is_droppable = EXCLUDED.is_droppable,
|
||||
is_public = EXCLUDED.is_public,
|
||||
updated_at = now()
|
||||
|
|
@ -244,6 +254,7 @@ pub async fn upsert_server_prop<'e>(
|
|||
default_layer,
|
||||
default_emotion,
|
||||
default_position,
|
||||
default_scale,
|
||||
is_unique,
|
||||
is_transferable,
|
||||
is_portable,
|
||||
|
|
@ -265,6 +276,7 @@ pub async fn upsert_server_prop<'e>(
|
|||
.bind(&default_layer)
|
||||
.bind(&default_emotion)
|
||||
.bind(default_position)
|
||||
.bind(default_scale)
|
||||
.bind(is_droppable)
|
||||
.bind(is_public)
|
||||
.bind(created_by)
|
||||
|
|
|
|||
|
|
@ -104,6 +104,14 @@ pub enum ClientMessage {
|
|||
/// Request to refresh identity after registration (guest → user conversion).
|
||||
/// Server will fetch updated user data and broadcast to all members.
|
||||
RefreshIdentity,
|
||||
|
||||
/// Update a loose prop's scale (moderator only).
|
||||
UpdateProp {
|
||||
/// The loose prop ID to update.
|
||||
loose_prop_id: Uuid,
|
||||
/// New scale factor (0.1 - 10.0).
|
||||
scale: f32,
|
||||
},
|
||||
}
|
||||
|
||||
/// Server-to-client WebSocket messages.
|
||||
|
|
@ -221,6 +229,12 @@ pub enum ServerMessage {
|
|||
prop_id: Uuid,
|
||||
},
|
||||
|
||||
/// A prop was updated (scale changed) - clients should update their local copy.
|
||||
PropRefresh {
|
||||
/// The updated prop with all current values.
|
||||
prop: LooseProp,
|
||||
},
|
||||
|
||||
/// A member updated their avatar appearance.
|
||||
AvatarUpdated {
|
||||
/// User ID of the member.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue