Add StageBackgroundView and related tests for plain background functionality
- Implemented StageBackgroundView class for managing sky and plain backgrounds in the stage. - Added functionality to set background type and color, bind camera, and apply themes. - Created test_phase19_plain_background.gd to validate plain background behavior and migration. - Developed test_phase19_starter_chooser_layout.gd to ensure proper layout and functionality of the starter chooser. - Included visual capture tests for UI elements and interactions.
This commit is contained in:
@@ -61,6 +61,7 @@ var _warned: Dictionary = {}
|
||||
|
||||
func _init() -> void:
|
||||
_apply_confirmation_defaults()
|
||||
_apply_phase19_defaults()
|
||||
|
||||
func load_from(path: String = DEFAULT_PATH) -> bool:
|
||||
invalidate_caches()
|
||||
@@ -80,8 +81,59 @@ func load_from(path: String = DEFAULT_PATH) -> bool:
|
||||
return false
|
||||
_apply_legacy_aliases(parsed as Dictionary)
|
||||
_config = _deep_merge(_config, parsed as Dictionary)
|
||||
_apply_phase19_defaults()
|
||||
return true
|
||||
|
||||
|
||||
func _apply_phase19_defaults() -> void:
|
||||
var grid: Dictionary = _config.get("grid", {}).duplicate(true)
|
||||
for key: String in ["plain_light_minor", "plain_light_major", "plain_dark_minor", "plain_dark_major"]:
|
||||
if not grid.has(key):
|
||||
grid[key] = {
|
||||
"plain_light_minor": "#11182120",
|
||||
"plain_light_major": "#11182148",
|
||||
"plain_dark_minor": "#FFFFFF25",
|
||||
"plain_dark_major": "#FFFFFF58",
|
||||
}[key]
|
||||
_config["grid"] = grid
|
||||
var popups: Dictionary = _config.get("popups", {}).duplicate(true)
|
||||
var asset_selector: Dictionary = popups.get("asset_selector", {}).duplicate(true)
|
||||
asset_selector["compact_swatch_size"] = maxi(44, int(asset_selector.get("compact_swatch_size", 44)))
|
||||
var starter_defaults: Dictionary = {
|
||||
"starter_dialog_width": 1160.0,
|
||||
"starter_viewport_height_ratio": 0.96,
|
||||
"starter_card_min_width": 340.0,
|
||||
"starter_preview_aspect": 2.0,
|
||||
"starter_preview_corner_radius": 12,
|
||||
"starter_preview_horizontal_inset": 10.0,
|
||||
"starter_preview_top_inset": 6,
|
||||
"starter_card_text_height": 108.0,
|
||||
"starter_title_size": 30,
|
||||
"starter_label_size": 22,
|
||||
"starter_label_height": 34,
|
||||
"starter_description_size": 20,
|
||||
"starter_description_height": 48,
|
||||
}
|
||||
for key: String in starter_defaults:
|
||||
if not asset_selector.has(key):
|
||||
asset_selector[key] = starter_defaults[key]
|
||||
popups["asset_selector"] = asset_selector
|
||||
_config["popups"] = popups
|
||||
var overlays: Dictionary = _config.get("overlays", {}).duplicate(true)
|
||||
var gizmos: Dictionary = overlays.get("gizmos", {}).duplicate(true)
|
||||
var defaults: Dictionary = {
|
||||
"backing_color": "#111821E8",
|
||||
"selection_color": "#F5F7FAE6",
|
||||
"hover_color": "#FFE66DCC",
|
||||
"rotate_color": "#4DB9FFE6",
|
||||
"backing_extra_width": 3.0,
|
||||
}
|
||||
for key: String in defaults:
|
||||
if not gizmos.has(key):
|
||||
gizmos[key] = defaults[key]
|
||||
overlays["gizmos"] = gizmos
|
||||
_config["overlays"] = overlays
|
||||
|
||||
func _apply_confirmation_defaults() -> void:
|
||||
var popups: Dictionary = _config.get("popups", {}).duplicate(true)
|
||||
popups["confirmation"] = CONFIRMATION_DEFAULTS.duplicate(true)
|
||||
|
||||
@@ -3,23 +3,29 @@ extends Control
|
||||
## Reuses the Stage's Sky and wall-finish drawing rules in gallery cards.
|
||||
|
||||
const SKY_VIEW: Script = preload("res://scripts/stage/scenery/stage_sky_view.gd")
|
||||
const BACKGROUND_VIEW: Script = preload("res://scripts/stage/scenery/stage_background_view.gd")
|
||||
const SURFACE_RENDERER: Script = preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
|
||||
var _background_kind: String = ""
|
||||
var _preview_id: StringName = &""
|
||||
var _sky_view: StageSkyView
|
||||
var _background_view: StageBackgroundView
|
||||
var _plain_color: Color = Color("#F5EFE0")
|
||||
|
||||
|
||||
func configure(entry: Dictionary) -> void:
|
||||
_background_kind = String(entry.get("background_kind", ""))
|
||||
_preview_id = StringName(String(entry.get("id", ""))) if _background_kind == "sky" else StringName(String(entry.get("finish_id", "")))
|
||||
var raw_plain_color: Variant = entry.get("plain_color", Color("#F5EFE0"))
|
||||
_plain_color = raw_plain_color if raw_plain_color is Color else Color.from_string(String(raw_plain_color), Color("#F5EFE0"))
|
||||
clip_contents = true
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
if _background_kind == "sky":
|
||||
_sky_view = SKY_VIEW.new() as StageSkyView
|
||||
_sky_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_sky_view.set_sky(_preview_id)
|
||||
add_child(_sky_view)
|
||||
if _background_kind in ["sky", "plain"]:
|
||||
_background_view = BACKGROUND_VIEW.new() as StageBackgroundView
|
||||
_background_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_background_view.set_background(StringName(_background_kind), _preview_id, _plain_color)
|
||||
add_child(_background_view)
|
||||
_sky_view = _background_view.get_sky_view()
|
||||
var frame: Panel = Panel.new()
|
||||
frame.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
frame.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
@@ -35,6 +41,8 @@ func configure(entry: Dictionary) -> void:
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if _background_kind == "plain":
|
||||
return
|
||||
if _background_kind == "wall" and SURFACE_RENDERER.is_wall_finish(_preview_id):
|
||||
var wall_tint: Color = Color("#7FA36C") if _preview_id == &"finish_wall_paint" else Color.WHITE
|
||||
# The preview rect is already small and clipped, so draw its full pattern directly.
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
class_name StageBackgroundView
|
||||
extends Control
|
||||
## Shared full-rect backdrop for the live Stage and still card previews.
|
||||
|
||||
const SKY_VIEW_SCRIPT := preload("res://scripts/stage/scenery/stage_sky_view.gd")
|
||||
|
||||
var _sky_view: StageSkyView
|
||||
var _plain_fill: ColorRect
|
||||
var _bound_camera: Camera2D
|
||||
var _background_kind: StringName = &"sky"
|
||||
var _sky_id: StringName = &"sky_neutral"
|
||||
var _plain_color: Color = Color("#F5EFE0")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
if _sky_view == null:
|
||||
_sky_view = SKY_VIEW_SCRIPT.new() as StageSkyView
|
||||
_sky_view.name = "StageSkyView"
|
||||
_sky_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_sky_view.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(_sky_view)
|
||||
if _plain_fill == null:
|
||||
_plain_fill = ColorRect.new()
|
||||
_plain_fill.name = "PlainFill"
|
||||
_plain_fill.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_plain_fill.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_plain_fill.color = _opaque(_plain_color)
|
||||
add_child(_plain_fill)
|
||||
_refresh_children()
|
||||
|
||||
|
||||
func set_background(kind: StringName, sky_id: StringName, plain_color: Color) -> void:
|
||||
_background_kind = &"plain" if kind == &"plain" else &"sky"
|
||||
_sky_id = sky_id
|
||||
_plain_color = _opaque(plain_color)
|
||||
_refresh_children()
|
||||
|
||||
|
||||
func bind_camera(camera: Camera2D) -> void:
|
||||
_bound_camera = camera
|
||||
if _sky_view != null:
|
||||
_sky_view.bind_camera(_bound_camera if _background_kind == &"sky" else null)
|
||||
|
||||
|
||||
func apply_theme(stage_theme: SandboxTheme) -> void:
|
||||
if _sky_view != null:
|
||||
_sky_view.apply_theme(stage_theme)
|
||||
|
||||
|
||||
func get_sky_view() -> StageSkyView:
|
||||
return _sky_view
|
||||
|
||||
|
||||
func get_background_kind() -> StringName:
|
||||
return _background_kind
|
||||
|
||||
|
||||
func get_plain_color() -> Color:
|
||||
return _plain_color
|
||||
|
||||
|
||||
func _refresh_children() -> void:
|
||||
if _sky_view == null or _plain_fill == null:
|
||||
return
|
||||
_sky_view.set_sky(_sky_id)
|
||||
_sky_view.visible = _background_kind == &"sky"
|
||||
_sky_view.bind_camera(_bound_camera if _background_kind == &"sky" else null)
|
||||
_plain_fill.color = _plain_color
|
||||
_plain_fill.visible = _background_kind == &"plain"
|
||||
|
||||
|
||||
func _opaque(color: Color) -> Color:
|
||||
return Color(color.r, color.g, color.b, 1.0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dc72udajou30y
|
||||
@@ -6,6 +6,7 @@ const STAGE_SCENE_IO := preload("res://scripts/stage/stage_scene_io.gd")
|
||||
const STAGE_SCENE_ADAPTER := preload("res://scripts/editors/stage/stage_scene_adapter.gd")
|
||||
const STAGE_SPAWNER := preload("res://scripts/stage/stage_spawner.gd")
|
||||
const STAGE_SKY_VIEW := preload("res://scripts/stage/scenery/stage_sky_view.gd")
|
||||
const STAGE_BACKGROUND_VIEW := preload("res://scripts/stage/scenery/stage_background_view.gd")
|
||||
const PROP_BLOCK := preload("res://scripts/props/prop_block.gd")
|
||||
|
||||
const PREVIEW_SIZE := Vector2i(256, 256)
|
||||
@@ -14,14 +15,16 @@ const REFERENCE_VIEWPORT_SIZE := Vector2(1280.0, 720.0)
|
||||
|
||||
var _viewport: SubViewport
|
||||
var _sky_view: StageSkyView
|
||||
var _background_view: StageBackgroundView
|
||||
var _world: Node2D
|
||||
var _loaded_nodes: Array[Node2D] = []
|
||||
var _scene_valid: bool = false
|
||||
|
||||
|
||||
func configure(entry: Dictionary) -> void:
|
||||
func configure(entry: Dictionary, requested_preview_size: Vector2 = Vector2.ZERO) -> void:
|
||||
name = "StarterScenePreview"
|
||||
custom_minimum_size = Vector2(128.0, 128.0)
|
||||
var resolved_preview_size: Vector2 = requested_preview_size if requested_preview_size.x > 0.0 and requested_preview_size.y > 0.0 else Vector2(PREVIEW_SIZE)
|
||||
custom_minimum_size = resolved_preview_size
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
stretch = true
|
||||
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||||
@@ -31,36 +34,40 @@ func configure(entry: Dictionary) -> void:
|
||||
return
|
||||
var data: Dictionary = result.get("data", {}) as Dictionary
|
||||
_scene_valid = true
|
||||
_build_preview_viewport(data)
|
||||
_build_preview_viewport(data, resolved_preview_size)
|
||||
|
||||
|
||||
func has_preview_scene() -> bool:
|
||||
return _scene_valid and _viewport != null and _sky_view != null and _world != null
|
||||
return _scene_valid and _viewport != null and _background_view != null and _world != null
|
||||
|
||||
|
||||
func get_loaded_object_count() -> int:
|
||||
return _loaded_nodes.size()
|
||||
|
||||
|
||||
func _build_preview_viewport(data: Dictionary) -> void:
|
||||
func _build_preview_viewport(data: Dictionary, requested_preview_size: Vector2) -> void:
|
||||
_viewport = SubViewport.new()
|
||||
_viewport.name = "PreviewViewport"
|
||||
_viewport.size = PREVIEW_SIZE
|
||||
_viewport.size = Vector2i(maxi(1, roundi(requested_preview_size.x)), maxi(1, roundi(requested_preview_size.y)))
|
||||
_viewport.world_2d = World2D.new()
|
||||
_viewport.transparent_bg = false
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
|
||||
add_child(_viewport)
|
||||
|
||||
var sky_layer := CanvasLayer.new()
|
||||
sky_layer.name = "PreviewSky"
|
||||
sky_layer.layer = -1
|
||||
_viewport.add_child(sky_layer)
|
||||
_sky_view = STAGE_SKY_VIEW.new() as StageSkyView
|
||||
_sky_view.name = "SkyView"
|
||||
_sky_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_sky_view.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_sky_view.set_sky(StringName(String(data.get("sky_id", "sky_neutral"))))
|
||||
sky_layer.add_child(_sky_view)
|
||||
var background_layer := CanvasLayer.new()
|
||||
background_layer.name = "PreviewBackgroundLayer"
|
||||
background_layer.layer = -1
|
||||
_viewport.add_child(background_layer)
|
||||
_background_view = STAGE_BACKGROUND_VIEW.new() as StageBackgroundView
|
||||
_background_view.name = "StageBackgroundView"
|
||||
_background_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_background_view.set_background(
|
||||
StringName(String(data.get("background_kind", "sky"))),
|
||||
StringName(String(data.get("sky_id", "sky_neutral"))),
|
||||
STAGE_SCENE_IO.color_from_data(data.get("plain_color", STAGE_SCENE_IO.DEFAULT_PLAIN_COLOR), Color("#F5EFE0"))
|
||||
)
|
||||
background_layer.add_child(_background_view)
|
||||
_sky_view = _background_view.get_sky_view()
|
||||
|
||||
_world = Node2D.new()
|
||||
_world.name = "PreviewWorld"
|
||||
@@ -85,27 +92,30 @@ func _build_preview_viewport(data: Dictionary) -> void:
|
||||
func _fit_scene_to_preview(camera_state: Dictionary) -> void:
|
||||
if _world == null or not is_instance_valid(_world):
|
||||
return
|
||||
var preview_frame: Dictionary = _preview_transform_for_camera(camera_state)
|
||||
var preview_frame: Dictionary = _preview_transform_for_camera(camera_state, Vector2(_viewport.size))
|
||||
var preview_scale: float = float(preview_frame["scale"])
|
||||
_world.scale = Vector2.ONE * preview_scale
|
||||
_world.position = preview_frame["position"] as Vector2
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
|
||||
|
||||
|
||||
static func _preview_transform_for_camera(camera_state: Dictionary) -> Dictionary:
|
||||
static func _preview_transform_for_camera(camera_state: Dictionary, preview_size: Vector2 = Vector2(PREVIEW_SIZE)) -> Dictionary:
|
||||
var camera_position: Vector2 = STAGE_SCENE_IO.data_to_vector(camera_state.get("position"), Vector2.ZERO)
|
||||
var camera_zoom: float = float(camera_state.get("zoom", 0.5))
|
||||
if not is_finite(camera_zoom) or camera_zoom <= 0.0:
|
||||
camera_zoom = 0.5
|
||||
var safe_preview_size: Vector2 = preview_size if preview_size.x > 0.0 and preview_size.y > 0.0 else Vector2(PREVIEW_SIZE)
|
||||
var preview_aspect: float = safe_preview_size.x / safe_preview_size.y
|
||||
var square_world_extent: float = REFERENCE_VIEWPORT_SIZE.y / camera_zoom
|
||||
var focus_size := Vector2(square_world_extent * preview_aspect, square_world_extent)
|
||||
var focus_rect := Rect2(
|
||||
camera_position - Vector2.ONE * square_world_extent * 0.5,
|
||||
Vector2.ONE * square_world_extent
|
||||
camera_position - focus_size * 0.5,
|
||||
focus_size
|
||||
)
|
||||
var usable_size: Vector2 = Vector2(PREVIEW_SIZE) - Vector2.ONE * PREVIEW_MARGIN * 2.0
|
||||
var usable_size: Vector2 = safe_preview_size - Vector2(PREVIEW_MARGIN * 2.0, PREVIEW_MARGIN * 2.0)
|
||||
var preview_scale: float = minf(usable_size.x / focus_rect.size.x, usable_size.y / focus_rect.size.y)
|
||||
return {
|
||||
"focus_rect": focus_rect,
|
||||
"scale": preview_scale,
|
||||
"position": Vector2(PREVIEW_SIZE) * 0.5 - camera_position * preview_scale,
|
||||
"position": safe_preview_size * 0.5 - camera_position * preview_scale,
|
||||
}
|
||||
|
||||
@@ -29,14 +29,16 @@ func _draw() -> void:
|
||||
var rect: Rect2 = Rect2(Vector2(3.0, 3.0), size - Vector2(6.0, 6.0))
|
||||
if rect.size.x <= 0.0 or rect.size.y <= 0.0:
|
||||
return
|
||||
if surface_kind == &"background_ground":
|
||||
if surface_kind == &"plain_background":
|
||||
draw_rect(rect, Color(tint.r, tint.g, tint.b, 1.0), true)
|
||||
elif surface_kind == &"background_ground":
|
||||
SURFACE_RENDERER.draw_background(self, rect, finish_id, tint, Vector2.ZERO, Vector2.ONE, false)
|
||||
else:
|
||||
SURFACE_RENDERER.draw_wall(self, rect, finish_id, tint, Vector2.ZERO, Vector2.ONE, false)
|
||||
var radius: float = minf(corner_radius, minf(rect.size.x, rect.size.y) * 0.5)
|
||||
if radius > 0.0:
|
||||
var wall: bool = surface_kind != &"background_ground"
|
||||
var base_color: Color = SURFACE_RENDERER.surface_base_color(finish_id, tint, wall)
|
||||
var base_color: Color = Color(tint.r, tint.g, tint.b, 1.0) if surface_kind == &"plain_background" else SURFACE_RENDERER.surface_base_color(finish_id, tint, wall)
|
||||
_draw_rounded_corner(rect, radius, base_color, outside_color, 0)
|
||||
_draw_rounded_corner(rect, radius, base_color, outside_color, 1)
|
||||
_draw_rounded_corner(rect, radius, base_color, outside_color, 2)
|
||||
|
||||
@@ -54,6 +54,13 @@ const MAX_SCALE: float = 20.0
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var camera: Camera2D = null
|
||||
var _selection_backing_color: Color = Color("#111821E8")
|
||||
var _selection_core_color: Color = Color(1.0, 1.0, 1.0, 0.9)
|
||||
var _hover_backing_color: Color = Color("#111821E8")
|
||||
var _hover_core_color: Color = Color(1.0, 0.9, 0.2, 0.5)
|
||||
var _rotate_backing_color: Color = Color("#111821E8")
|
||||
var _rotate_core_color: Color = Color(0.3, 0.6, 1.0, 0.9)
|
||||
var _outline_extra_width: float = 3.0
|
||||
|
||||
## When > 0, translation drags snap to this grid size (0 = off).
|
||||
var snap_size: float = 0.0
|
||||
@@ -105,6 +112,19 @@ func set_snap_size(size: float) -> void:
|
||||
snap_size = maxf(size, 0.0)
|
||||
|
||||
|
||||
func apply_theme(stage_theme: SandboxTheme) -> void:
|
||||
if stage_theme == null:
|
||||
return
|
||||
_selection_backing_color = stage_theme.color("overlays.gizmos.backing_color", Color("#111821E8"))
|
||||
_selection_core_color = stage_theme.color("overlays.gizmos.selection_color", SELECTION_COLOR)
|
||||
_hover_backing_color = stage_theme.color("overlays.gizmos.backing_color", Color("#111821E8"))
|
||||
_hover_core_color = stage_theme.color("overlays.gizmos.hover_color", HOVER_COLOR)
|
||||
_rotate_backing_color = stage_theme.color("overlays.gizmos.backing_color", Color("#111821E8"))
|
||||
_rotate_core_color = stage_theme.color("overlays.gizmos.rotate_color", ROTATE_COLOR)
|
||||
_outline_extra_width = stage_theme.number("overlays.gizmos.backing_extra_width", 3.0, 1.0, 8.0)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func set_box_rect(rect: Rect2) -> void:
|
||||
_box_rect = rect
|
||||
queue_redraw()
|
||||
@@ -253,16 +273,18 @@ func _draw() -> void:
|
||||
return
|
||||
var zoom := _zoom()
|
||||
if _hovered != null and is_instance_valid(_hovered) and not _hovered.is_queued_for_deletion() and not _targets.has(_hovered):
|
||||
draw_rect(_target_bounds(_hovered), HOVER_COLOR, false, HOVER_WIDTH / zoom)
|
||||
_draw_halo_rect(_target_bounds(_hovered), _hover_core_color, HOVER_WIDTH / zoom, _hover_backing_color)
|
||||
# A selection outline for every selected object.
|
||||
for node: Node2D in _targets:
|
||||
if node != null and is_instance_valid(node):
|
||||
draw_rect(_target_bounds(node), SELECTION_COLOR, false, SELECTION_WIDTH / zoom)
|
||||
_draw_halo_rect(_target_bounds(node), _selection_core_color, SELECTION_WIDTH / zoom, _selection_backing_color)
|
||||
# Rotate ring only for a single selection.
|
||||
if _transform_handles_visible and _targets.size() == 1 and is_instance_valid(_targets[0]):
|
||||
var rect: Rect2 = _target_bounds(_targets[0])
|
||||
if not _is_scenery_target(_targets[0]):
|
||||
draw_arc(rect.get_center(), _rotate_ring_radius(rect), 0.0, TAU, 64, ROTATE_COLOR, ROTATE_WIDTH / zoom, true)
|
||||
var ring_radius: float = _rotate_ring_radius(rect)
|
||||
draw_arc(rect.get_center(), ring_radius, 0.0, TAU, 64, _rotate_backing_color, ROTATE_WIDTH / zoom + _outline_extra_width / zoom, true)
|
||||
draw_arc(rect.get_center(), ring_radius, 0.0, TAU, 64, _rotate_core_color, ROTATE_WIDTH / zoom, true)
|
||||
if _is_scenery_target(_targets[0]):
|
||||
for corner: Vector2 in _corners(rect):
|
||||
draw_circle(corner, 11.0 / zoom, Color("#111821E8"))
|
||||
@@ -275,7 +297,7 @@ func _draw() -> void:
|
||||
draw_line(corner + Vector2(-half, 0.0), corner + Vector2(half, 0.0), SELECTION_COLOR, 1.5 / zoom)
|
||||
draw_line(corner + Vector2(0.0, -half), corner + Vector2(0.0, half), SELECTION_COLOR, 1.5 / zoom)
|
||||
if _box_rect.size != Vector2.ZERO:
|
||||
draw_rect(_box_rect.abs(), HOVER_COLOR, false, HOVER_WIDTH / zoom)
|
||||
_draw_halo_rect(_box_rect.abs(), _hover_core_color, HOVER_WIDTH / zoom, _hover_backing_color)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
@@ -287,6 +309,11 @@ func _zoom() -> float:
|
||||
return 1.0
|
||||
|
||||
|
||||
func _draw_halo_rect(rect: Rect2, core_color: Color, core_width: float, backing_color: Color) -> void:
|
||||
draw_rect(rect, backing_color, false, core_width + _outline_extra_width / _zoom())
|
||||
draw_rect(rect, core_color, false, core_width)
|
||||
|
||||
|
||||
func _rotate_ring_radius(rect: Rect2) -> float:
|
||||
return rect.size.length() * 0.5 + ROTATE_HANDLE_MARGIN_PX / _zoom()
|
||||
|
||||
|
||||
@@ -9,6 +9,12 @@ extends Node2D
|
||||
var camera: Camera2D = null
|
||||
var grid_size: float = 15.0
|
||||
var enabled: bool = true
|
||||
var _plain_background_active: bool = false
|
||||
var _plain_background_color: Color = Color("#F5EFE0")
|
||||
var _plain_light_minor_color: Color = Color("#11182120")
|
||||
var _plain_light_major_color: Color = Color("#11182148")
|
||||
var _plain_dark_minor_color: Color = Color("#FFFFFF25")
|
||||
var _plain_dark_major_color: Color = Color("#FFFFFF58")
|
||||
|
||||
const GRID_COLOR := Color(1.0, 1.0, 1.0, 0.08)
|
||||
const MAJOR_COLOR := Color(1.0, 1.0, 1.0, 0.16)
|
||||
@@ -19,6 +25,23 @@ func _process(_delta: float) -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func set_background_treatment(
|
||||
plain_active: bool,
|
||||
plain_color: Color,
|
||||
light_minor: Color,
|
||||
light_major: Color,
|
||||
dark_minor: Color,
|
||||
dark_major: Color
|
||||
) -> void:
|
||||
_plain_background_active = plain_active
|
||||
_plain_background_color = Color(plain_color.r, plain_color.g, plain_color.b, 1.0)
|
||||
_plain_light_minor_color = light_minor
|
||||
_plain_light_major_color = light_major
|
||||
_plain_dark_minor_color = dark_minor
|
||||
_plain_dark_major_color = dark_major
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if not enabled or grid_size <= 0.0:
|
||||
return
|
||||
@@ -34,13 +57,22 @@ func _draw() -> void:
|
||||
var x := floorf(rect.position.x / grid_size) * grid_size
|
||||
while x <= rect.end.x:
|
||||
var grid_index := int(round(x / grid_size))
|
||||
var color := MAJOR_COLOR if grid_index % MAJOR_EVERY == 0 else GRID_COLOR
|
||||
var color: Color = _grid_color(grid_index % MAJOR_EVERY == 0)
|
||||
draw_line(Vector2(x, rect.position.y), Vector2(x, rect.end.y), color, line_width)
|
||||
x += grid_size
|
||||
|
||||
var y := floorf(rect.position.y / grid_size) * grid_size
|
||||
while y <= rect.end.y:
|
||||
var grid_index := int(round(y / grid_size))
|
||||
var color := MAJOR_COLOR if grid_index % MAJOR_EVERY == 0 else GRID_COLOR
|
||||
var color: Color = _grid_color(grid_index % MAJOR_EVERY == 0)
|
||||
draw_line(Vector2(rect.position.x, y), Vector2(rect.end.x, y), color, line_width)
|
||||
y += grid_size
|
||||
|
||||
|
||||
func _grid_color(is_major: bool) -> Color:
|
||||
if not _plain_background_active:
|
||||
return MAJOR_COLOR if is_major else GRID_COLOR
|
||||
var light_fill: bool = _plain_background_color.get_luminance() >= 0.55
|
||||
if light_fill:
|
||||
return _plain_light_major_color if is_major else _plain_light_minor_color
|
||||
return _plain_dark_major_color if is_major else _plain_dark_minor_color
|
||||
|
||||
@@ -3,7 +3,8 @@ extends RefCounted
|
||||
## JSON boundary for Sandbox Stage documents. This class deliberately has no
|
||||
## scene-tree dependencies, which makes rejected documents non-destructive.
|
||||
|
||||
const FILE_VERSION: String = "1.6"
|
||||
const FILE_VERSION: String = "1.7"
|
||||
const DEFAULT_PLAIN_COLOR: String = "#F5EFE0"
|
||||
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
const CONTENT_PATHS := preload("res://scripts/shared/content_paths.gd")
|
||||
const ACTION_REGISTRY := preload("res://scripts/stage/action_registry.gd")
|
||||
@@ -185,11 +186,13 @@ static func _migrate_to_current(data: Dictionary) -> Dictionary:
|
||||
prop_entry["wobble_on_impact"] = false
|
||||
_assign_legacy_depths(current.get("objects", []))
|
||||
return current
|
||||
if version not in ["1.0", "1.1", "1.2", "1.3", "1.4", "1.5"]:
|
||||
if version not in ["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6"]:
|
||||
return {}
|
||||
var migrated: Dictionary = data.duplicate(true)
|
||||
if version != "1.5":
|
||||
if version not in ["1.5", "1.6"]:
|
||||
migrated["sky_id"] = "sky_neutral"
|
||||
migrated["background_kind"] = "sky"
|
||||
migrated["plain_color"] = DEFAULT_PLAIN_COLOR
|
||||
for raw: Variant in migrated.get("objects", []):
|
||||
if not raw is Dictionary:
|
||||
continue
|
||||
@@ -203,9 +206,9 @@ static func _migrate_to_current(data: Dictionary) -> Dictionary:
|
||||
object["next_use_spot_number"] = object.get("next_use_spot_number", 1)
|
||||
if not object.has("wobble_on_impact"):
|
||||
object["wobble_on_impact"] = false
|
||||
if version != "1.5" and String(object.get("type", "")) == "terrain" and String(object.get("asset_id", "")) == "ground":
|
||||
if version not in ["1.5", "1.6"] and String(object.get("type", "")) == "terrain" and String(object.get("asset_id", "")) == "ground":
|
||||
object["finish_id"] = "finish_ground_legacy"
|
||||
if String(object.get("type", "")) == "wall_panel":
|
||||
if version != "1.6" and String(object.get("type", "")) == "wall_panel":
|
||||
_migrate_legacy_wall_tint(object)
|
||||
migrated["version"] = FILE_VERSION
|
||||
_assign_legacy_depths(migrated.get("objects", []))
|
||||
@@ -254,9 +257,15 @@ static func _assign_legacy_depths(raw_objects: Variant) -> void:
|
||||
roles["depth"] = PROP_STAGE_ROLES.DEPTH_FRONT if characters_before > characters_after else PROP_STAGE_ROLES.DEPTH_BEHIND
|
||||
|
||||
static func _validate_root(data: Dictionary) -> Dictionary:
|
||||
var sky_id: Variant = data.get("sky_id", "sky_neutral")
|
||||
var background_kind: Variant = data.get("background_kind", null)
|
||||
if not background_kind is String or not ["sky", "plain"].has(String(background_kind)):
|
||||
return {"ok": false, "error": "Scene background kind is invalid."}
|
||||
var sky_id: Variant = data.get("sky_id", null)
|
||||
if not sky_id is String or not SKY_IDS.has(String(sky_id)):
|
||||
return {"ok": false, "error": "Scene Sky is invalid."}
|
||||
var plain_color: Variant = data.get("plain_color", null)
|
||||
if not _valid_opaque_hex_color(plain_color):
|
||||
return {"ok": false, "error": "Scene Plain Background color is invalid."}
|
||||
var seen: Dictionary = {}
|
||||
var seen_stage_ids: Dictionary = {}
|
||||
for raw: Variant in data["objects"]:
|
||||
@@ -346,6 +355,19 @@ static func _valid_color(value: Variant) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
static func _valid_opaque_hex_color(value: Variant) -> bool:
|
||||
if not value is String:
|
||||
return false
|
||||
var raw: String = String(value).strip_edges()
|
||||
if raw.length() != 7 or not raw.begins_with("#"):
|
||||
return false
|
||||
const HEX_DIGITS: String = "0123456789abcdefABCDEF"
|
||||
for index: int in range(1, raw.length()):
|
||||
if not HEX_DIGITS.contains(raw.substr(index, 1)):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _valid_rect_data(value: Variant) -> bool:
|
||||
if not value is Dictionary:
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user