Add new test file for phase 19 scenery with unique identifier

This commit is contained in:
2026-10-17 12:53:38 -04:00
parent 36032ebb1e
commit a0510e6405
74 changed files with 40948 additions and 149 deletions
+250 -4
View File
@@ -33,6 +33,7 @@ signal anchor_selected(anchor_id: String)
signal whole_prop_resize_requested(factor: float)
signal whole_prop_move_requested(delta: Vector2)
signal whole_prop_rotate_requested(angle_rad: float)
signal stage_size_cancel_requested
# ---------------------------------------------------------------------------
# Enums / constants
@@ -141,6 +142,14 @@ var _whole_prop_delta: Vector2 = Vector2.ZERO
var _whole_prop_total_angle: float = 0.0
var _whole_prop_previous_angle: float = 0.0
var _whole_prop_preview_angle: float = 0.0
var _stage_size_preview_active: bool = false
var _stage_size_start_shapes: Array[Dictionary] = []
var _stage_size_preview_shapes: Array[Dictionary] = []
var _stage_size_start_anchors: Dictionary = {}
var _stage_size_preview_anchors: Dictionary = {}
var _stage_size_start_pan: Vector2 = Vector2.ZERO
var _stage_size_start_zoom: float = 1.0
var _stage_size_factor: float = 1.0
const SCALE_COORDINATE_LIMIT: float = 1000000.0
const WHOLE_PROP_HANDLE_HIT_RADIUS: float = 16.0
@@ -202,6 +211,8 @@ func set_edit_mode(mode: int) -> void:
var m: int = mode if mode >= EditMode.TRANSFORM and mode <= EditMode.ATTACH else EditMode.TRANSFORM
if m == _edit_mode:
return
if _stage_size_preview_active:
cancel_stage_size_preview()
_edit_mode = m
if _edit_mode != EditMode.TRANSFORM:
set_whole_prop_resize_enabled(false)
@@ -280,6 +291,188 @@ func set_show_stickman_guide(enabled: bool) -> void:
func get_show_stickman_guide() -> bool:
return _show_stickman_guide
func begin_stage_size_preview(anchors: Dictionary) -> bool:
if _stage_size_preview_active or _whole_prop_resize_enabled or _edit_mode != EditMode.TRANSFORM or not has_shape_geometry():
return false
var bounds: Rect2 = get_visible_shapes_bounds()
var extent: float = maxf(bounds.size.x, bounds.size.y)
if not _valid_rect(bounds) or not is_finite(extent) or extent <= 0.0:
return false
_stage_size_start_shapes = _duplicate_shapes(shapes)
_stage_size_preview_shapes = _duplicate_shapes(shapes)
_stage_size_start_anchors = anchors.duplicate(true)
_stage_size_preview_anchors = anchors.duplicate(true)
_stage_size_start_pan = _pan_offset
_stage_size_start_zoom = _zoom
_stage_size_factor = 1.0
_stage_size_preview_active = true
queue_redraw()
return true
func preview_stage_size(factor: float) -> bool:
if not _stage_size_preview_active or not is_finite(factor) or factor <= 0.0:
return false
var candidate: Array[Dictionary] = _scaled_stage_size_shapes(_stage_size_start_shapes, factor)
if candidate.is_empty() or not _shapes_are_representable(candidate):
return false
var anchor_result: Dictionary = PropAnchorDataScript.try_scale_non_origin(_stage_size_start_anchors, factor)
if not bool(anchor_result.get("ok", false)):
return false
var candidate_bounds: Rect2 = _visible_bounds_for_shapes(candidate)
if not _valid_rect(candidate_bounds):
return false
_stage_size_factor = factor
_stage_size_preview_shapes = candidate
_stage_size_preview_anchors = (anchor_result.get("anchors", {}) as Dictionary).duplicate(true)
queue_redraw()
return true
func commit_stage_size_preview() -> Dictionary:
if not _stage_size_preview_active:
return {"changed": false, "anchors": get_anchor_data()}
var changed: bool = not is_equal_approx(_stage_size_factor, 1.0)
var committed_anchors: Dictionary = _stage_size_preview_anchors.duplicate(true)
if changed:
shapes = _duplicate_shapes(_stage_size_preview_shapes)
set_anchor_data(committed_anchors)
_end_stage_size_preview()
if changed:
shape_changed.emit(get_shape_data())
return {"changed": changed, "anchors": committed_anchors}
func cancel_stage_size_preview() -> void:
if _stage_size_preview_active:
_end_stage_size_preview()
func is_stage_size_preview_active() -> bool:
return _stage_size_preview_active
func get_visible_shapes_bounds() -> Rect2:
return _visible_bounds_for_shapes(_stage_size_preview_shapes if _stage_size_preview_active else shapes)
func get_stage_size_comparison_bounds() -> Rect2:
var artwork_bounds: Rect2 = _visible_bounds_for_shapes(_stage_size_preview_shapes if _stage_size_preview_active else shapes)
if not _valid_rect(artwork_bounds):
return Rect2()
return artwork_bounds.merge(_stage_size_guide_bounds(artwork_bounds))
func fit_stage_size_view(bounds: Rect2, padding_px: float, force: bool = true) -> void:
if not _stage_size_preview_active or not _valid_rect(bounds):
return
if not force and _stage_size_view_contains(bounds, padding_px):
return
var available: Vector2 = size - Vector2.ONE * maxf(0.0, padding_px) * 2.0
if available.x <= 0.0 or available.y <= 0.0:
return
var fit_x: float = available.x / maxf(bounds.size.x, 1.0)
var fit_y: float = available.y / maxf(bounds.size.y, 1.0)
_zoom = clampf(minf(fit_x, fit_y), 0.02, MAX_ZOOM)
_pan_offset = size * 0.5 - bounds.get_center() * _zoom
queue_redraw()
func _end_stage_size_preview() -> void:
_stage_size_preview_active = false
_stage_size_start_shapes.clear()
_stage_size_preview_shapes.clear()
_stage_size_start_anchors.clear()
_stage_size_preview_anchors.clear()
_stage_size_factor = 1.0
_pan_offset = _stage_size_start_pan
_zoom = _stage_size_start_zoom
queue_redraw()
func _stage_size_view_contains(bounds: Rect2, padding_px: float) -> bool:
var padding: float = maxf(0.0, padding_px)
var available: Vector2 = size - Vector2.ONE * padding * 2.0
if available.x <= 0.0 or available.y <= 0.0:
return false
var world_origin: Vector2 = -_pan_offset / maxf(_zoom, 0.001)
var world_size: Vector2 = size / maxf(_zoom, 0.001)
var world_padding: float = padding / maxf(_zoom, 0.001)
var visible_bounds: Rect2 = Rect2(world_origin + Vector2.ONE * world_padding, world_size - Vector2.ONE * world_padding * 2.0)
return visible_bounds.encloses(bounds)
func _visible_bounds_for_shapes(source: Array[Dictionary]) -> Rect2:
var result: Rect2 = Rect2()
var found: bool = false
for shape: Dictionary in source:
if not _shape_has_geometry(shape):
continue
var bounds: Rect2 = _shape_bbox(shape).grow(maxf(0.0, float(shape.get("outline_width", 0.0))) * 0.5)
if not found:
result = bounds
found = true
else:
result = result.merge(bounds)
return result if found else Rect2()
func _stage_size_guide_bounds(artwork_bounds: Rect2) -> Rect2:
var head_center: Vector2 = WholeStickmanPreviewScript.GUIDE_JOINTS["Head"]
var ankle: Vector2 = WholeStickmanPreviewScript.GUIDE_JOINTS["LeftAnkle"]
var height: float = ankle.y - (head_center.y - WholeStickmanPreviewScript.GUIDE_HEAD_RADIUS)
var guide_half_width: float = 170.0
var figure_center_x: float
if artwork_bounds.size.x <= guide_half_width * 2.0 + 40.0:
figure_center_x = artwork_bounds.position.x + minf(artwork_bounds.size.x * 0.25, 40.0)
else:
figure_center_x = artwork_bounds.position.x - guide_half_width - 32.0
var floor_y: float = artwork_bounds.end.y
return Rect2(Vector2(figure_center_x - guide_half_width, floor_y - height), Vector2(guide_half_width * 2.0, height))
func _shapes_are_representable(source: Array[Dictionary]) -> bool:
if source.is_empty():
return false
for shape: Dictionary in source:
var points: PackedVector2Array = shape.get("points", PackedVector2Array())
for point: Vector2 in points:
if not is_finite(point.x) or not is_finite(point.y) or absf(point.x) > SCALE_COORDINATE_LIMIT or absf(point.y) > SCALE_COORDINATE_LIMIT:
return false
var radius: float = float(shape.get("radius", 0.0))
var outline_width: float = float(shape.get("outline_width", 0.0))
if not is_finite(radius) or radius < 0.0 or radius > SCALE_COORDINATE_LIMIT:
return false
if not is_finite(outline_width) or outline_width < 0.0 or outline_width > SCALE_COORDINATE_LIMIT:
return false
return true
func _scaled_stage_size_shapes(source: Array[Dictionary], factor: float) -> Array[Dictionary]:
var result: Array[Dictionary] = []
for source_shape: Dictionary in source:
var shape: Dictionary = source_shape.duplicate(true)
var points: PackedVector2Array = shape.get("points", PackedVector2Array())
for index: int in points.size():
points[index] *= factor
shape["points"] = points
shape["radius"] = float(shape.get("radius", 0.0)) * factor
shape["outline_width"] = float(shape.get("outline_width", 0.0)) * factor
result.append(shape)
return result
func _duplicate_shapes(source: Array[Dictionary]) -> Array[Dictionary]:
var result: Array[Dictionary] = []
for shape: Dictionary in source:
result.append(shape.duplicate(true))
return result
func _valid_rect(rect: Rect2) -> bool:
return rect.position.is_finite() and rect.size.is_finite() and rect.size.x >= 0.0 and rect.size.y >= 0.0 and (rect.size.x > 0.0 or rect.size.y > 0.0)
func get_shapes_bounds() -> Rect2:
var found: bool = false; var result: Rect2 = Rect2()
for shape: Dictionary in shapes:
@@ -378,6 +571,7 @@ func set_origin_marker_color(color: Color) -> void:
func set_shape_data(data: Variant) -> void:
## Import shapes. Accepts an Array[Dictionary] (internal or plain {x,y}
## format) or a single Dictionary.
cancel_stage_size_preview()
set_whole_prop_resize_enabled(false)
shapes.clear()
if data is Array:
@@ -403,6 +597,7 @@ func get_shape_data() -> Array[Dictionary]:
func clear() -> void:
## Remove all shapes without emitting shape_changed.
cancel_stage_size_preview()
set_whole_prop_resize_enabled(false)
shapes.clear()
_selected_shape_idx = -1
@@ -788,7 +983,6 @@ func global_to_world(global_pos: Vector2) -> Vector2:
func _on_canvas_draw() -> void:
draw_set_transform(_pan_offset, 0.0, Vector2(_zoom, _zoom))
_draw_grid()
_draw_stickman_guide()
if shapes.is_empty() and _edit_mode == EditMode.TRANSFORM:
var rect := get_rect()
@@ -797,10 +991,14 @@ func _on_canvas_draw() -> void:
if font != null:
draw_string(font, hint_pos, "Right-click to add a shape", HORIZONTAL_ALIGNMENT_CENTER, -1, 14, Color(0.5, 0.5, 0.5, 0.6))
var drawn_shapes: Array[Dictionary] = _whole_prop_preview_shapes if _whole_prop_dragging else shapes
var drawn_shapes: Array[Dictionary] = _stage_size_preview_shapes if _stage_size_preview_active else (_whole_prop_preview_shapes if _whole_prop_dragging else shapes)
for i: int in drawn_shapes.size():
var selected: bool = not _whole_prop_resize_enabled and i == _selected_shape_idx
var selected: bool = not _stage_size_preview_active and not _whole_prop_resize_enabled and i == _selected_shape_idx
_draw_shape(drawn_shapes[i], selected, _dragging_vertex if selected else -1)
if _stage_size_preview_active:
_draw_stage_size_guide(_visible_bounds_for_shapes(drawn_shapes))
else:
_draw_stickman_guide()
_draw_origin_marker()
if _whole_prop_resize_enabled:
@@ -836,6 +1034,54 @@ func _draw_stickman_guide() -> void:
draw_line(joints[first] as Vector2, joints[second] as Vector2, color, width)
if joints.has("Head"):
draw_arc(joints["Head"] as Vector2, WholeStickmanPreviewScript.GUIDE_HEAD_RADIUS, 0.0, TAU, 48, color, width)
var ankle: Vector2 = joints["LeftAnkle"]
_draw_reference_figure(0.0, ankle.y, ankle.x - 250.0, ankle.x + 250.0)
func _draw_stage_size_guide(artwork_bounds: Rect2) -> void:
var guide_bounds: Rect2 = _stage_size_guide_bounds(artwork_bounds)
var floor_y: float = artwork_bounds.end.y
_draw_reference_figure(
guide_bounds.get_center().x,
floor_y,
minf(artwork_bounds.position.x, guide_bounds.position.x - 28.0),
maxf(artwork_bounds.end.x, guide_bounds.end.x + 28.0)
)
func _draw_reference_figure(center_x: float, floor_y: float, floor_start: float, floor_end: float) -> void:
var joints: Dictionary = WholeStickmanPreviewScript.GUIDE_JOINTS
var ankle_y: float = (joints["LeftAnkle"] as Vector2).y
var offset: Vector2 = Vector2(center_x, floor_y - ankle_y)
var inv_zoom: float = 1.0 / maxf(_zoom, 0.001)
for bone: Array in WholeStickmanPreviewScript.GUIDE_BONES:
var first: String = str(bone[0])
var second: String = str(bone[1])
if not joints.has(first) or not joints.has(second):
continue
var color: Color = WholeStickmanPreviewScript.GUIDE_COLOR_CENTRAL
if first in WholeStickmanPreviewScript.GUIDE_LEFT_JOINTS or second in WholeStickmanPreviewScript.GUIDE_LEFT_JOINTS:
color = WholeStickmanPreviewScript.GUIDE_COLOR_LEFT
elif first in WholeStickmanPreviewScript.GUIDE_RIGHT_JOINTS or second in WholeStickmanPreviewScript.GUIDE_RIGHT_JOINTS:
color = WholeStickmanPreviewScript.GUIDE_COLOR_RIGHT
var start: Vector2 = (joints[first] as Vector2) + offset
var finish: Vector2 = (joints[second] as Vector2) + offset
draw_line(start, finish, Color("#111821D9"), 8.0 * inv_zoom, true)
draw_line(start, finish, color, 3.0 * inv_zoom, true)
var head: Vector2 = (joints["Head"] as Vector2) + offset
draw_arc(head, WholeStickmanPreviewScript.GUIDE_HEAD_RADIUS, 0.0, TAU, 48, Color("#111821E8"), 8.0 * inv_zoom, true)
draw_arc(head, WholeStickmanPreviewScript.GUIDE_HEAD_RADIUS, 0.0, TAU, 48, WholeStickmanPreviewScript.GUIDE_COLOR_CENTRAL, 3.0 * inv_zoom, true)
for joint_name: String in joints:
var point: Vector2 = (joints[joint_name] as Vector2) + offset
var color: Color = WholeStickmanPreviewScript.GUIDE_COLOR_CENTRAL
if joint_name in WholeStickmanPreviewScript.GUIDE_LEFT_JOINTS:
color = WholeStickmanPreviewScript.GUIDE_JOINT_COLOR_LEFT
elif joint_name in WholeStickmanPreviewScript.GUIDE_RIGHT_JOINTS:
color = WholeStickmanPreviewScript.GUIDE_JOINT_COLOR_RIGHT
draw_circle(point, 6.5 * inv_zoom, Color("#111821E8"))
draw_circle(point, 3.2 * inv_zoom, color)
draw_line(Vector2(floor_start, floor_y), Vector2(floor_end, floor_y), Color("#111821E8"), 8.0 * inv_zoom, true)
draw_line(Vector2(floor_start, floor_y), Vector2(floor_end, floor_y), Color("#F8FAFC"), 2.5 * inv_zoom, true)
func _draw_whole_prop_resize_gizmos() -> void:
var bounds: Rect2 = _whole_preview_bounds() if _whole_prop_dragging else get_shapes_bounds()
@@ -852,7 +1098,7 @@ func _draw_whole_prop_resize_gizmos() -> void:
var rotation_dot: Vector2 = Vector2(bounds.get_center().x, bounds.end.y + ROTATION_CIRCLE_OFFSET)
draw_circle(rotation_dot, (ROTATION_CIRCLE_RADIUS + 2.0) / _zoom, SELECTION_HALO_COLOR)
draw_circle(rotation_dot, ROTATION_CIRCLE_RADIUS / _zoom, SELECTION_CORE_COLOR)
var anchors: Dictionary = _whole_prop_preview_anchors if _whole_prop_dragging else get_anchor_data()
var anchors: Dictionary = _stage_size_preview_anchors if _stage_size_preview_active else (_whole_prop_preview_anchors if _whole_prop_dragging else get_anchor_data())
for id: String in anchors:
if id == "origin":
continue