Add new test file for phase 19 scenery with unique identifier
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user