Fix prop renders

* Incorporate prop scaling
* Props now render to a canvas
This commit is contained in:
Evan Carroll 2026-01-23 16:00:47 -06:00
parent af89394df1
commit a2841c413d
21 changed files with 942 additions and 353 deletions

View file

@ -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.