///|
pub struct SceneCamera {
scene_id : String
x : Int
y : Int
width : Int
height : Int
zoom_percent : Int
} derive(Debug, Eq)
///|
pub struct SceneCameraBudget {
min_zoom_percent : Int
max_zoom_percent : Int
min_viewport_width : Int
min_viewport_height : Int
} derive(Debug, Eq)
///|
pub struct SceneCameraPlan {
scene_id : String
requested : SceneCamera
camera : SceneCamera
viewport : SceneViewport
clamped : Bool
diagnostics : Array[String]
summary : String
} derive(Debug, Eq)
///|
pub fn scene_camera(
scene_id~ : String,
x~ : Int,
y~ : Int,
width~ : Int,
height~ : Int,
zoom_percent? : Int = 100,
) -> SceneCamera {
{ scene_id, x, y, width, height, zoom_percent }
}
///|
pub fn scene_camera_from_viewport(
scene_id : String,
viewport : SceneViewport,
zoom_percent? : Int = 100,
) -> SceneCamera {
scene_camera(
scene_id~,
x=viewport.x,
y=viewport.y,
width=viewport.width,
height=viewport.height,
zoom_percent~,
)
}
///|
pub fn scene_camera_budget(
min_zoom_percent? : Int = 50,
max_zoom_percent? : Int = 300,
min_viewport_width? : Int = 64,
min_viewport_height? : Int = 64,
) -> SceneCameraBudget {
{
min_zoom_percent,
max_zoom_percent,
min_viewport_width,
min_viewport_height,
}
}
///|
pub fn default_scene_camera_budget() -> SceneCameraBudget {
scene_camera_budget()
}
///|
pub fn plan_scene_camera(
input : Scene,
camera : SceneCamera,
) -> SceneCameraPlan {
plan_scene_camera_with_budget(input, camera, default_scene_camera_budget())
}
///|
pub fn plan_scene_camera_with_budget(
input : Scene,
camera : SceneCamera,
budget : SceneCameraBudget,
) -> SceneCameraPlan {
let zoom_percent = clamp_int(
camera.zoom_percent,
budget.min_zoom_percent,
budget.max_zoom_percent,
)
let visible_width = scaled_visible_size(camera.width, zoom_percent)
let visible_height = scaled_visible_size(camera.height, zoom_percent)
let width = if visible_width < budget.min_viewport_width {
budget.min_viewport_width
} else {
visible_width
}
let height = if visible_height < budget.min_viewport_height {
budget.min_viewport_height
} else {
visible_height
}
let max_x = max_int(0, input.size.width - width)
let max_y = max_int(0, input.size.height - height)
let x = clamp_int(camera.x, 0, max_x)
let y = clamp_int(camera.y, 0, max_y)
let planned = scene_camera(
scene_id=input.id,
x~,
y~,
width=camera.width,
height=camera.height,
zoom_percent~,
)
let viewport = scene_viewport(x~, y~, width~, height~)
let clamped = camera.scene_id != input.id ||
camera.x != x ||
camera.y != y ||
camera.zoom_percent != zoom_percent ||
visible_width != width ||
visible_height != height
let diagnostics = scene_camera_diagnostics(
input~,
camera,
planned,
viewport,
visible_width,
visible_height,
clamped,
budget,
)
{
scene_id: input.id,
requested: camera,
camera: planned,
viewport,
clamped,
diagnostics,
summary: "Bunnia scene camera plan: scene=\{input.id} viewport=\{viewport.x},\{viewport.y},\{viewport.width},\{viewport.height} zoom=\{zoom_percent} clamped=\{clamped} diagnostics=\{diagnostics.length()}",
}
}
///|
pub fn static_scene_view_with_camera(
input : Scene,
camera : SceneCamera,
budget? : SceneRenderBudget = default_scene_render_budget(),
camera_budget? : SceneCameraBudget = default_scene_camera_budget(),
) -> @core.Node {
let camera_plan = plan_scene_camera_with_budget(input, camera, camera_budget)
let node = static_scene_view_with_viewport(
input,
camera_plan.viewport,
budget~,
)
let attrs = node.attrs
attrs.push(@core.data("camera-x", camera_plan.camera.x.to_string()))
attrs.push(@core.data("camera-y", camera_plan.camera.y.to_string()))
attrs.push(@core.data("camera-width", camera_plan.camera.width.to_string()))
attrs.push(@core.data("camera-height", camera_plan.camera.height.to_string()))
attrs.push(
@core.data(
"camera-zoom-percent",
camera_plan.camera.zoom_percent.to_string(),
),
)
attrs.push(@core.data("camera-clamped", camera_plan.clamped.to_string()))
@core.node(
kind=node.kind,
attrs~,
events=node.events,
children=node.children,
text=node.text,
key=node.key,
list_total_count=node.list_total_count,
list_visible_count=node.list_visible_count,
)
}
///|
pub fn set_scene_camera(scene_id : String, camera : SceneCamera) -> @core.Patch {
@core.set_json("scenes.\{scene_id}.camera", scene_camera_json(camera))
}
///|
pub fn set_scene_camera_position(
scene_id : String,
x : Int,
y : Int,
) -> @core.Patch {
@core.set_json(
"scenes.\{scene_id}.camera.position",
"{\"x\":\{x},\"y\":\{y}}",
)
}
///|
pub fn set_scene_camera_zoom(
scene_id : String,
zoom_percent : Int,
) -> @core.Patch {
@core.set_json(
"scenes.\{scene_id}.camera.zoomPercent",
zoom_percent.to_string(),
)
}
///|
pub fn set_scene_camera_viewport(
scene_id : String,
viewport : SceneViewport,
) -> @core.Patch {
@core.set_json(
"scenes.\{scene_id}.camera.viewport",
"{\"x\":\{viewport.x},\"y\":\{viewport.y},\"width\":\{viewport.width},\"height\":\{viewport.height}}",
)
}
///|
fn scene_camera_diagnostics(
input~ : Scene,
requested : SceneCamera,
planned : SceneCamera,
viewport : SceneViewport,
visible_width : Int,
visible_height : Int,
clamped : Bool,
budget : SceneCameraBudget,
) -> Array[String] {
let diagnostics : Array[String] = []
if requested.scene_id != input.id {
diagnostics.push(
"scene-camera-scene-mismatch:\{requested.scene_id}/\{input.id}",
)
}
if requested.zoom_percent < budget.min_zoom_percent {
diagnostics.push(
"scene-camera-zoom-under-min:\{requested.zoom_percent}/\{budget.min_zoom_percent}",
)
}
if requested.zoom_percent > budget.max_zoom_percent {
diagnostics.push(
"scene-camera-zoom-over-max:\{requested.zoom_percent}/\{budget.max_zoom_percent}",
)
}
if visible_width < budget.min_viewport_width {
diagnostics.push(
"scene-camera-viewport-width-under-min:\{visible_width}/\{budget.min_viewport_width}",
)
}
if visible_height < budget.min_viewport_height {
diagnostics.push(
"scene-camera-viewport-height-under-min:\{visible_height}/\{budget.min_viewport_height}",
)
}
if clamped {
diagnostics.push(
"scene-camera-clamped:\{requested.x},\{requested.y}/\{planned.x},\{planned.y}",
)
}
if viewport.width > input.size.width || viewport.height > input.size.height {
diagnostics.push(
"scene-camera-viewport-over-scene:\{viewport.width}x\{viewport.height}/\{input.size.width}x\{input.size.height}",
)
}
diagnostics
}
///|
fn scaled_visible_size(size : Int, zoom_percent : Int) -> Int {
let zoom = if zoom_percent <= 0 { 1 } else { zoom_percent }
let scaled = size * 100 / zoom
if scaled < 1 {
1
} else {
scaled
}
}
///|
fn clamp_int(value : Int, min : Int, max : Int) -> Int {
if value < min {
min
} else if value > max {
max
} else {
value
}
}
///|
fn max_int(a : Int, b : Int) -> Int {
if a > b {
a
} else {
b
}
}
///|
fn scene_camera_json(input : SceneCamera) -> String {
"{" +
"\"sceneId\":\"\{scene_camera_escape_json(input.scene_id)}\"," +
"\"x\":\{input.x}," +
"\"y\":\{input.y}," +
"\"width\":\{input.width}," +
"\"height\":\{input.height}," +
"\"zoomPercent\":\{input.zoom_percent}" +
"}"
}
///|
fn scene_camera_escape_json(value : String) -> String {
value
.replace(old="\\", new="\\\\")
.replace(old="\"", new="\\\"")
.replace(old="\n", new="\\n")
.to_string()
}