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
|
||||
|
||||
@@ -36,7 +36,13 @@ const SETTINGS_PATH: String = "user://prop_editor_settings.json"
|
||||
const RECENT_MAX: int = 5
|
||||
const OFF_ORIGIN_WARNING_MIN_DISTANCE: float = 1024.0
|
||||
const OFF_ORIGIN_WARNING_BOUNDS_MULTIPLIER: float = 2.0
|
||||
const NORMALIZED_MAX_EXTENT: float = 96.0
|
||||
const STAGE_REFERENCE_HEIGHT: float = 939.5 # WholeStickmanPreview head top (-463.5 - 100) to ankle line (376).
|
||||
const STAGE_SIZE_STEP: float = 1.1
|
||||
const ART_TAG_IDS: Array[String] = ["nature", "buildings", "walls", "roads", "objects"]
|
||||
const ART_TAG_LABELS: Array[String] = ["Nature", "Buildings", "Walls", "Roads", "Objects"]
|
||||
|
||||
signal artwork_saved(path: String)
|
||||
signal return_requested
|
||||
|
||||
const MENU_NEW := 0
|
||||
const MENU_LOAD := 2
|
||||
@@ -113,6 +119,7 @@ var _current_path: String = ""
|
||||
var _dirty: bool = false
|
||||
var _clipboard: Dictionary = {}
|
||||
var _anchors: Dictionary = PropAnchorDataScript.default_anchors()
|
||||
var _tags: Array[String] = []
|
||||
var _selected_anchor_id: String = ""
|
||||
var _thumb: PropThumbnail
|
||||
var _thumb_cache: ThumbnailCache
|
||||
@@ -138,15 +145,26 @@ var _params_box: VBoxContainer
|
||||
var _param_spins: Dictionary = {}
|
||||
var _add_button: Button
|
||||
var _updating_inspector: bool = false
|
||||
var _tag_checks: Dictionary = {}
|
||||
var _tag_section: VBoxContainer
|
||||
var _stage_authoring: bool = false
|
||||
var _stage_return_pending: bool = false
|
||||
var _stage_place_button: Button
|
||||
|
||||
# Grid settings dialog (removed Round 4 - replaced by top-right controls)
|
||||
var _grid_spin: SpinBox
|
||||
var _snap_check: CheckBox
|
||||
var _center_prop_button: Button
|
||||
var _normalize_size_button: Button
|
||||
var _size_for_stage_button: Button
|
||||
var _stage_size_controls: FlowContainer
|
||||
var _stage_size_status: Label
|
||||
var _show_stickman_guide_check: CheckBox
|
||||
var _show_stickman_guide: bool = false
|
||||
var _whole_prop_resize_button: Button
|
||||
var _stage_size_preview_active: bool = false
|
||||
var _stage_size_preview_factor: float = 1.0
|
||||
var _stage_size_entry_extent: float = 0.0
|
||||
var _pending_save_after_stage_size: int = -1
|
||||
|
||||
# Edit-mode toolbar (Round 2/3)
|
||||
var _mode_group: ButtonGroup
|
||||
@@ -192,7 +210,8 @@ var _recent_props: Array[String] = []
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ready() -> void:
|
||||
get_tree().auto_accept_quit = false
|
||||
if not _stage_authoring:
|
||||
get_tree().auto_accept_quit = false
|
||||
_theme = PropThemeScript.new()
|
||||
_theme.load_from(PropThemeScript.DEFAULT_PATH)
|
||||
_thumb_cache = ThumbnailCacheScript.new()
|
||||
@@ -225,6 +244,7 @@ func _ready() -> void:
|
||||
_canvas.whole_prop_resize_requested.connect(_on_whole_prop_resize_requested)
|
||||
_canvas.whole_prop_move_requested.connect(_on_whole_prop_move_requested)
|
||||
_canvas.whole_prop_rotate_requested.connect(_on_whole_prop_rotate_requested)
|
||||
_canvas.stage_size_cancel_requested.connect(func() -> void: _finish_stage_size_preview(false))
|
||||
_canvas.set_grid_size(_theme.grid_size())
|
||||
_canvas.set_snap_enabled(false)
|
||||
_canvas.set_has_clipboard(false)
|
||||
@@ -250,12 +270,24 @@ func _ready() -> void:
|
||||
_canvas.call_deferred("reset_view")
|
||||
call_deferred("_initialize_inspector_split")
|
||||
_refresh_center_prop_button()
|
||||
_check_autosave_recovery()
|
||||
if _stage_authoring:
|
||||
if _canvas.get_edit_mode() == PropCanvasScript.EditMode.ATTACH:
|
||||
_canvas.set_edit_mode(PropCanvasScript.EditMode.TRANSFORM)
|
||||
_do_new()
|
||||
_build_stage_authoring_controls()
|
||||
_apply_authoring_context()
|
||||
else:
|
||||
_check_autosave_recovery()
|
||||
|
||||
|
||||
func configure_stage_authoring() -> void:
|
||||
_stage_authoring = true
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_update_cursor_coords()
|
||||
if _autosave_remaining > 0.0:
|
||||
if not _stage_authoring and _autosave_remaining > 0.0:
|
||||
_autosave_remaining -= delta
|
||||
if _autosave_remaining <= 0.0:
|
||||
_autosave_remaining = 0.0
|
||||
@@ -266,6 +298,17 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey:
|
||||
var key := event as InputEventKey
|
||||
if key.pressed and not key.echo and key.keycode == KEY_ESCAPE:
|
||||
if _stage_size_preview_active:
|
||||
_finish_stage_size_preview(false)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if _stage_authoring:
|
||||
var stage_focus: Control = get_viewport().gui_get_focus_owner() if get_viewport() != null else null
|
||||
if stage_focus is LineEdit or stage_focus is TextEdit:
|
||||
return
|
||||
_request_stage_return()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner() if get_viewport() != null else null
|
||||
if not (focus_owner is LineEdit or focus_owner is TextEdit):
|
||||
if _canvas.is_whole_prop_resize_dragging():
|
||||
@@ -349,6 +392,11 @@ func _on_attach_mode_pressed() -> void:
|
||||
|
||||
|
||||
func _on_canvas_mode_changed(mode: int) -> void:
|
||||
if _stage_size_preview_active:
|
||||
_cancel_stage_size_preview()
|
||||
if _stage_authoring and mode == PropCanvasScript.EditMode.ATTACH:
|
||||
_canvas.set_edit_mode(PropCanvasScript.EditMode.TRANSFORM)
|
||||
return
|
||||
if mode != PropCanvasScript.EditMode.TRANSFORM:
|
||||
_set_whole_prop_resize_enabled(false)
|
||||
var entering_attach: bool = mode == PropCanvasScript.EditMode.ATTACH and _last_canvas_mode != PropCanvasScript.EditMode.ATTACH
|
||||
@@ -373,11 +421,11 @@ func _sync_mode_buttons(mode: int) -> void:
|
||||
|
||||
func _sync_mode_controls(mode: int) -> void:
|
||||
if _whole_prop_resize_button != null:
|
||||
_whole_prop_resize_button.visible = mode == PropCanvasScript.EditMode.TRANSFORM
|
||||
if _normalize_size_button != null:
|
||||
_normalize_size_button.visible = mode == PropCanvasScript.EditMode.TRANSFORM
|
||||
_whole_prop_resize_button.visible = mode == PropCanvasScript.EditMode.TRANSFORM and not _stage_size_preview_active
|
||||
if _size_for_stage_button != null:
|
||||
_size_for_stage_button.visible = mode == PropCanvasScript.EditMode.TRANSFORM and not _stage_size_preview_active
|
||||
if _attach_tools != null:
|
||||
_attach_tools.visible = mode == PropCanvasScript.EditMode.ATTACH
|
||||
_attach_tools.visible = mode == PropCanvasScript.EditMode.ATTACH and not _stage_authoring
|
||||
if _add_section != null:
|
||||
_add_section.visible = mode == PropCanvasScript.EditMode.TRANSFORM
|
||||
if _delete_button != null:
|
||||
@@ -385,9 +433,9 @@ func _sync_mode_controls(mode: int) -> void:
|
||||
if _shape_section != null:
|
||||
_shape_section.visible = mode != PropCanvasScript.EditMode.ATTACH and not _canvas.is_whole_prop_resize_enabled() and _canvas.get_selected_index() >= 0
|
||||
if _material_section != null:
|
||||
_material_section.visible = mode != PropCanvasScript.EditMode.ATTACH
|
||||
_material_section.visible = mode != PropCanvasScript.EditMode.ATTACH and not _stage_authoring
|
||||
if _anchor_section != null:
|
||||
_anchor_section.visible = mode == PropCanvasScript.EditMode.ATTACH
|
||||
_anchor_section.visible = mode == PropCanvasScript.EditMode.ATTACH and not _stage_authoring
|
||||
if _attach_preview != null:
|
||||
_attach_preview.visible = mode == PropCanvasScript.EditMode.ATTACH and (_show_preview_check == null or _show_preview_check.button_pressed)
|
||||
if _attach_preview.has_method("set_document"):
|
||||
@@ -666,11 +714,12 @@ func _build_size_tools() -> void:
|
||||
_whole_prop_resize_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
_whole_prop_resize_button.toggled.connect(_set_whole_prop_resize_enabled)
|
||||
_size_tools.add_child(_whole_prop_resize_button)
|
||||
_normalize_size_button = Button.new()
|
||||
_normalize_size_button.text = "📏 Normalize Size"
|
||||
_normalize_size_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
_normalize_size_button.pressed.connect(_on_normalize_size_pressed)
|
||||
_size_tools.add_child(_normalize_size_button)
|
||||
_size_for_stage_button = Button.new()
|
||||
_size_for_stage_button.text = "📏 Size for Stage"
|
||||
_size_for_stage_button.tooltip_text = "Compare this drawing with a standing stickman."
|
||||
_size_for_stage_button.custom_minimum_size = Vector2(148.0, maxf(44.0, float(_theme.button_min_height())))
|
||||
_size_for_stage_button.pressed.connect(_on_size_for_stage_pressed)
|
||||
_size_tools.add_child(_size_for_stage_button)
|
||||
_center_prop_button = Button.new()
|
||||
_center_prop_button.text = "🎯 Center Prop"
|
||||
_center_prop_button.tooltip_text = "Move the whole prop and its anchors to (0,0)."
|
||||
@@ -683,9 +732,57 @@ func _build_size_tools() -> void:
|
||||
_show_stickman_guide_check.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
_show_stickman_guide_check.toggled.connect(_on_stickman_guide_toggled)
|
||||
_size_tools.add_child(_show_stickman_guide_check)
|
||||
_stage_size_controls = FlowContainer.new()
|
||||
_stage_size_controls.name = "StageSizeControls"
|
||||
_stage_size_controls.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_stage_size_controls.add_theme_constant_override("h_separation", 6)
|
||||
_stage_size_controls.add_theme_constant_override("v_separation", 6)
|
||||
_stage_size_controls.hide()
|
||||
_size_tools.add_child(_stage_size_controls)
|
||||
for preset: Dictionary in [
|
||||
{"label": "Small", "factor": 0.35},
|
||||
{"label": "Room", "factor": 1.0},
|
||||
{"label": "Big", "factor": 2.0},
|
||||
]:
|
||||
var preset_button: Button = _make_stage_size_button(str(preset["label"]))
|
||||
var preset_factor: float = float(preset["factor"])
|
||||
preset_button.pressed.connect(func() -> void: _choose_stage_size(preset_factor))
|
||||
_stage_size_controls.add_child(preset_button)
|
||||
var smaller_button: Button = _make_stage_size_button("Make smaller")
|
||||
smaller_button.pressed.connect(func() -> void: _adjust_stage_size(1.0 / STAGE_SIZE_STEP))
|
||||
_stage_size_controls.add_child(smaller_button)
|
||||
var bigger_button: Button = _make_stage_size_button("Make bigger")
|
||||
bigger_button.pressed.connect(func() -> void: _adjust_stage_size(STAGE_SIZE_STEP))
|
||||
_stage_size_controls.add_child(bigger_button)
|
||||
var keep_button: Button = _make_stage_size_button("Keep size", true)
|
||||
keep_button.pressed.connect(func() -> void: _finish_stage_size_preview(true))
|
||||
_stage_size_controls.add_child(keep_button)
|
||||
var cancel_button: Button = _make_stage_size_button("✕ Cancel")
|
||||
cancel_button.pressed.connect(func() -> void: _finish_stage_size_preview(false))
|
||||
_stage_size_controls.add_child(cancel_button)
|
||||
_stage_size_status = Label.new()
|
||||
_stage_size_status.name = "StageSizeStatus"
|
||||
# This label lives in the size-tools FlowContainer. With an auto-wrapping
|
||||
# label and no minimum width, FlowContainer can collapse it to a sliver and
|
||||
# grow the toolbar to thousands of pixels tall when the preview opens.
|
||||
_stage_size_status.custom_minimum_size.x = 360.0
|
||||
_stage_size_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_stage_size_status.hide()
|
||||
_size_tools.add_child(_stage_size_status)
|
||||
_refresh_scale_buttons()
|
||||
|
||||
|
||||
func _make_stage_size_button(label: String, prominent: bool = false) -> Button:
|
||||
var button: Button = Button.new()
|
||||
button.text = label
|
||||
button.custom_minimum_size.y = maxf(44.0, float(_theme.button_min_height()))
|
||||
button.focus_mode = Control.FOCUS_ALL
|
||||
button.theme = _ui_theme
|
||||
if prominent:
|
||||
button.add_theme_color_override("font_color", _theme.accent_color())
|
||||
return button
|
||||
|
||||
|
||||
func _on_top_grid_changed(value: float) -> void:
|
||||
_canvas.set_grid_size(int(value))
|
||||
|
||||
@@ -693,12 +790,110 @@ func _on_top_grid_changed(value: float) -> void:
|
||||
func _on_top_snap_toggled(pressed: bool) -> void:
|
||||
_canvas.set_snap_enabled(pressed)
|
||||
|
||||
func _on_normalize_size_pressed() -> void:
|
||||
var bounds: Rect2 = _canvas.get_shapes_bounds()
|
||||
var extent: float = maxf(bounds.size.x, bounds.size.y)
|
||||
if is_finite(extent) and extent > 0.0:
|
||||
_scale_document(NORMALIZED_MAX_EXTENT / extent)
|
||||
func _on_size_for_stage_pressed() -> void:
|
||||
if _stage_size_preview_active or _canvas.get_edit_mode() != PropCanvasScript.EditMode.TRANSFORM:
|
||||
return
|
||||
_set_whole_prop_resize_enabled(false)
|
||||
var bounds: Rect2 = _canvas.get_visible_shapes_bounds()
|
||||
_stage_size_entry_extent = maxf(bounds.size.x, bounds.size.y)
|
||||
if not is_finite(_stage_size_entry_extent) or _stage_size_entry_extent <= 0.0:
|
||||
return
|
||||
if not _canvas.begin_stage_size_preview(_anchors):
|
||||
return
|
||||
_stage_size_preview_factor = 1.0
|
||||
_stage_size_preview_active = true
|
||||
_stage_size_status.text = "Compare the drawing with the guide, then keep it or cancel."
|
||||
_update_stage_size_controls_visibility()
|
||||
# Hiding the inspector and mode bar changes the canvas allocation. Wait for
|
||||
# that layout pass before centering/fitting, otherwise the guide can be
|
||||
# pushed outside the newly-sized viewport until a preset is clicked.
|
||||
await get_tree().process_frame
|
||||
if not _stage_size_preview_active:
|
||||
return
|
||||
_canvas.fit_stage_size_view(_canvas.get_stage_size_comparison_bounds(), 48.0, true)
|
||||
|
||||
|
||||
func _choose_stage_size(relative_height: float) -> void:
|
||||
if not _stage_size_preview_active or not is_finite(relative_height) or relative_height <= 0.0:
|
||||
return
|
||||
var factor: float = STAGE_REFERENCE_HEIGHT * relative_height / _stage_size_entry_extent
|
||||
if _canvas.preview_stage_size(factor):
|
||||
_stage_size_preview_factor = factor
|
||||
_stage_size_status.text = ""
|
||||
_canvas.fit_stage_size_view(_canvas.get_stage_size_comparison_bounds(), 48.0, true)
|
||||
else:
|
||||
_stage_size_status.text = "That size is too large to display. Try a smaller choice."
|
||||
|
||||
|
||||
func _adjust_stage_size(multiplier: float) -> void:
|
||||
if not _stage_size_preview_active:
|
||||
return
|
||||
var factor: float = _stage_size_preview_factor * multiplier
|
||||
if _canvas.preview_stage_size(factor):
|
||||
_stage_size_preview_factor = factor
|
||||
_stage_size_status.text = ""
|
||||
_canvas.fit_stage_size_view(_canvas.get_stage_size_comparison_bounds(), 48.0, false)
|
||||
else:
|
||||
_stage_size_status.text = "That size is outside the drawing area. Try a smaller change."
|
||||
|
||||
|
||||
func _finish_stage_size_preview(keep: bool) -> void:
|
||||
if not _stage_size_preview_active:
|
||||
return
|
||||
var pending_save: int = _pending_save_after_stage_size if keep else -1
|
||||
_pending_save_after_stage_size = -1
|
||||
if keep:
|
||||
var result: Dictionary = _canvas.commit_stage_size_preview()
|
||||
_anchors = (result.get("anchors", _anchors) as Dictionary).duplicate(true)
|
||||
_selected_anchor_id = PropAnchorDataScript.preferred_editable_id(_anchors, _selected_anchor_id)
|
||||
_canvas.set_anchor_data(_anchors)
|
||||
_canvas.select_anchor(_selected_anchor_id)
|
||||
_refresh_anchor_inspector()
|
||||
_sync_attach_preview()
|
||||
else:
|
||||
_canvas.cancel_stage_size_preview()
|
||||
_stage_size_preview_active = false
|
||||
_stage_size_preview_factor = 1.0
|
||||
_stage_size_entry_extent = 0.0
|
||||
_stage_size_status.text = ""
|
||||
_update_stage_size_controls_visibility()
|
||||
_refresh_scale_buttons()
|
||||
if pending_save >= 0:
|
||||
_continue_save_after_stage_size(pending_save)
|
||||
|
||||
|
||||
func _cancel_stage_size_preview() -> void:
|
||||
if _stage_size_preview_active:
|
||||
_pending_save_after_stage_size = -1
|
||||
_finish_stage_size_preview(false)
|
||||
|
||||
|
||||
func _request_save_after_stage_size(menu_id: int) -> void:
|
||||
_pending_save_after_stage_size = menu_id
|
||||
_stage_size_status.text = "Keep size before saving, or cancel to leave the drawing unchanged."
|
||||
|
||||
|
||||
func _continue_save_after_stage_size(menu_id: int) -> void:
|
||||
if menu_id == MENU_SAVE:
|
||||
if _current_path.is_empty():
|
||||
_save_dialog.popup_centered()
|
||||
else:
|
||||
_begin_save_request(_current_path, true)
|
||||
elif menu_id == MENU_SAVE_AS:
|
||||
_save_dialog.popup_centered()
|
||||
|
||||
|
||||
func _update_stage_size_controls_visibility() -> void:
|
||||
if _stage_size_controls == null:
|
||||
return
|
||||
_stage_size_controls.visible = _stage_size_preview_active
|
||||
_stage_size_status.visible = _stage_size_preview_active
|
||||
_size_for_stage_button.visible = not _stage_size_preview_active and _canvas.get_edit_mode() == PropCanvasScript.EditMode.TRANSFORM
|
||||
_whole_prop_resize_button.visible = not _stage_size_preview_active and _canvas.get_edit_mode() == PropCanvasScript.EditMode.TRANSFORM
|
||||
_center_prop_button.visible = not _stage_size_preview_active
|
||||
_show_stickman_guide_check.visible = not _stage_size_preview_active and not _stage_authoring
|
||||
_mode_bar.visible = not _stage_size_preview_active
|
||||
_inspector_column.visible = not _stage_size_preview_active
|
||||
|
||||
func _on_stickman_guide_toggled(enabled: bool) -> void:
|
||||
_show_stickman_guide = enabled
|
||||
@@ -755,6 +950,9 @@ func _notification(what: int) -> void:
|
||||
|
||||
|
||||
func _request_close() -> void:
|
||||
if _stage_authoring:
|
||||
_request_stage_return()
|
||||
return
|
||||
if not _dirty:
|
||||
get_tree().quit()
|
||||
return
|
||||
@@ -765,6 +963,7 @@ func _request_close() -> void:
|
||||
|
||||
|
||||
func _request_new() -> void:
|
||||
_cancel_stage_size_preview()
|
||||
if not _dirty:
|
||||
_do_new()
|
||||
return
|
||||
@@ -774,6 +973,22 @@ func _request_new() -> void:
|
||||
_exit_dialog.popup_centered()
|
||||
|
||||
|
||||
func _request_stage_return() -> void:
|
||||
if not _stage_authoring:
|
||||
return
|
||||
_cancel_stage_size_preview()
|
||||
if not _dirty:
|
||||
if not _current_path.is_empty():
|
||||
artwork_saved.emit(_current_path)
|
||||
return_requested.emit()
|
||||
return
|
||||
_stage_return_pending = true
|
||||
_pending_new = false
|
||||
_pending_quit = false
|
||||
_exit_dialog.dialog_text = "Save your artwork before returning? Saving places it on the Stage."
|
||||
_exit_dialog.popup_centered()
|
||||
|
||||
|
||||
func _on_exit_save() -> void:
|
||||
_exit_dialog.hide()
|
||||
if _current_path.is_empty():
|
||||
@@ -785,8 +1000,12 @@ func _on_exit_save() -> void:
|
||||
func _on_exit_custom_action(action: StringName) -> void:
|
||||
if action == &"discard":
|
||||
_exit_dialog.hide()
|
||||
_clear_autosave()
|
||||
if _pending_quit:
|
||||
if not _stage_authoring:
|
||||
_clear_autosave()
|
||||
if _stage_return_pending:
|
||||
_stage_return_pending = false
|
||||
return_requested.emit()
|
||||
elif _pending_quit:
|
||||
_pending_quit = false
|
||||
get_tree().quit()
|
||||
elif _pending_new:
|
||||
@@ -797,6 +1016,7 @@ func _on_exit_custom_action(action: StringName) -> void:
|
||||
func _on_exit_cancel() -> void:
|
||||
_pending_quit = false
|
||||
_pending_new = false
|
||||
_stage_return_pending = false
|
||||
_cancel_pending_save_request()
|
||||
|
||||
|
||||
@@ -805,6 +1025,8 @@ func _do_new() -> void:
|
||||
_set_whole_prop_resize_enabled(false)
|
||||
_prop_name_edit.text = ""
|
||||
_material_preset = PropBlockScript.MaterialPreset.NONE
|
||||
_tags.clear()
|
||||
_refresh_tag_controls()
|
||||
_preset_option.selected = _material_preset
|
||||
_update_physics_labels()
|
||||
_canvas.clear()
|
||||
@@ -820,21 +1042,29 @@ func _do_new() -> void:
|
||||
_refresh_anchor_inspector()
|
||||
_refresh_center_prop_button()
|
||||
_update_save_status()
|
||||
_refresh_tag_controls()
|
||||
|
||||
|
||||
func _on_menu_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
MENU_NEW:
|
||||
_request_new()
|
||||
MENU_LOAD:
|
||||
_load_dialog.popup_centered()
|
||||
MENU_SAVE:
|
||||
if _stage_size_preview_active:
|
||||
_request_save_after_stage_size(MENU_SAVE)
|
||||
return
|
||||
if _current_path.is_empty():
|
||||
_save_dialog.popup_centered()
|
||||
else:
|
||||
_begin_save_request(_current_path, true)
|
||||
MENU_SAVE_AS:
|
||||
if _stage_size_preview_active:
|
||||
_request_save_after_stage_size(MENU_SAVE_AS)
|
||||
return
|
||||
_save_dialog.popup_centered()
|
||||
MENU_LOAD:
|
||||
_cancel_stage_size_preview()
|
||||
_load_dialog.popup_centered()
|
||||
MENU_RESET_VIEWS:
|
||||
_canvas.reset_view()
|
||||
|
||||
@@ -983,7 +1213,95 @@ func _build_inspector() -> void:
|
||||
|
||||
_apply_control_theme(_inspector_list)
|
||||
_rebuild_primitive_params()
|
||||
_build_tag_inspector()
|
||||
_build_anchor_inspector()
|
||||
_refresh_tag_controls()
|
||||
|
||||
|
||||
func _build_stage_authoring_controls() -> void:
|
||||
var top_bar: HBoxContainer = get_node_or_null(NodePath("TopBar")) as HBoxContainer
|
||||
if top_bar == null:
|
||||
return
|
||||
var name_label: Label = top_bar.get_node_or_null(NodePath("NameLabel")) as Label
|
||||
if name_label != null:
|
||||
name_label.text = "Artwork name:"
|
||||
_prop_name_edit.placeholder_text = "Name this artwork"
|
||||
_stage_place_button = Button.new()
|
||||
_stage_place_button.text = "✨ Save & Place as Scenery"
|
||||
_stage_place_button.tooltip_text = "Save this drawing and put a picture of it on your Stage."
|
||||
_stage_place_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
_stage_place_button.size_flags_horizontal = Control.SIZE_SHRINK_END
|
||||
_stage_place_button.theme = _ui_theme
|
||||
_stage_place_button.pressed.connect(_request_stage_return)
|
||||
top_bar.add_child(_stage_place_button)
|
||||
|
||||
|
||||
func _apply_authoring_context() -> void:
|
||||
if not _stage_authoring:
|
||||
return
|
||||
if _material_section != null:
|
||||
_material_section.hide()
|
||||
if _anchor_section != null:
|
||||
_anchor_section.hide()
|
||||
if _attach_tools != null:
|
||||
_attach_tools.hide()
|
||||
if _attach_button != null:
|
||||
_attach_button.hide()
|
||||
if _show_stickman_guide_check != null:
|
||||
_show_stickman_guide_check.hide()
|
||||
if _tag_section != null:
|
||||
_tag_section.show()
|
||||
_sync_mode_buttons(_canvas.get_edit_mode())
|
||||
_sync_mode_controls(_canvas.get_edit_mode())
|
||||
_save_dialog.title = "Save Scenery Artwork"
|
||||
|
||||
|
||||
func _build_tag_inspector() -> void:
|
||||
_tag_section = VBoxContainer.new()
|
||||
_tag_section.add_theme_constant_override("separation", 4)
|
||||
_inspector_list.add_child(_tag_section)
|
||||
var title := Label.new()
|
||||
title.text = "Artwork Tags"
|
||||
title.add_theme_font_size_override("font_size", _theme.font_header())
|
||||
_tag_section.add_child(title)
|
||||
var hint := Label.new()
|
||||
hint.text = "Choose a few so this art is easy to find."
|
||||
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_tag_section.add_child(hint)
|
||||
var rows := FlowContainer.new()
|
||||
rows.add_theme_constant_override("h_separation", 6)
|
||||
rows.add_theme_constant_override("v_separation", 4)
|
||||
_tag_section.add_child(rows)
|
||||
for index: int in ART_TAG_IDS.size():
|
||||
var tag_id: String = ART_TAG_IDS[index]
|
||||
var check := CheckBox.new()
|
||||
check.text = ART_TAG_LABELS[index]
|
||||
check.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
check.toggled.connect(_on_art_tag_toggled.bind(tag_id))
|
||||
rows.add_child(check)
|
||||
_tag_checks[tag_id] = check
|
||||
|
||||
|
||||
func _on_art_tag_toggled(enabled: bool, tag_id: String) -> void:
|
||||
if _updating_inspector:
|
||||
return
|
||||
if enabled and not _tags.has(tag_id):
|
||||
_tags.append(tag_id)
|
||||
elif not enabled:
|
||||
_tags.erase(tag_id)
|
||||
_tags = PropFileScript.normalize_tags(_tags)
|
||||
_mark_dirty()
|
||||
|
||||
|
||||
func _refresh_tag_controls() -> void:
|
||||
if _tag_checks.is_empty():
|
||||
return
|
||||
_updating_inspector = true
|
||||
for tag_id: String in ART_TAG_IDS:
|
||||
var check: CheckBox = _tag_checks.get(tag_id) as CheckBox
|
||||
if check != null:
|
||||
check.set_pressed_no_signal(_tags.has(tag_id))
|
||||
_updating_inspector = false
|
||||
|
||||
func _build_anchor_inspector() -> void:
|
||||
_anchor_section = VBoxContainer.new()
|
||||
@@ -1313,11 +1631,12 @@ func _refresh_center_prop_button() -> void:
|
||||
_refresh_scale_buttons()
|
||||
|
||||
func _refresh_scale_buttons() -> void:
|
||||
if _normalize_size_button == null:
|
||||
if _size_for_stage_button == null:
|
||||
return
|
||||
var bounds: Rect2 = _canvas.get_shapes_bounds()
|
||||
var bounds: Rect2 = _canvas.get_visible_shapes_bounds()
|
||||
var extent: float = maxf(bounds.size.x, bounds.size.y)
|
||||
_normalize_size_button.disabled = not _canvas.has_shape_geometry() or not is_finite(extent) or extent <= 0.0
|
||||
_size_for_stage_button.disabled = not _canvas.has_shape_geometry() or not is_finite(extent) or extent <= 0.0
|
||||
_size_for_stage_button.tooltip_text = "Add visible artwork to use Size for Stage." if _size_for_stage_button.disabled else "Compare this drawing with a standing stickman."
|
||||
if _whole_prop_resize_button != null:
|
||||
_whole_prop_resize_button.disabled = _canvas.get_edit_mode() != PropCanvasScript.EditMode.TRANSFORM or not _canvas.has_shape_geometry()
|
||||
|
||||
@@ -1422,6 +1741,8 @@ func _write_autosave() -> void:
|
||||
|
||||
func _clear_autosave() -> void:
|
||||
_autosave_remaining = 0.0
|
||||
if _stage_authoring:
|
||||
return
|
||||
if FileAccess.file_exists(AUTOSAVE_PATH):
|
||||
DirAccess.remove_absolute(AUTOSAVE_PATH)
|
||||
|
||||
@@ -1469,7 +1790,7 @@ func _check_autosave_recovery() -> void:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _build_json_data() -> Dictionary:
|
||||
return PropFileScript.to_file_dict(_prop_name_edit.text, _material_preset, _canvas.get_shape_data(), _anchors)
|
||||
return PropFileScript.to_file_dict(_prop_name_edit.text, _material_preset, _canvas.get_shape_data(), _anchors, _tags)
|
||||
|
||||
|
||||
func _apply_json_data(json: Variant, is_recovery: bool = false) -> String:
|
||||
@@ -1479,6 +1800,8 @@ func _apply_json_data(json: Variant, is_recovery: bool = false) -> String:
|
||||
if model.is_empty():
|
||||
return "The file could not be read: unsupported version or malformed data."
|
||||
_prop_name_edit.text = str(model.get("prop_name", ""))
|
||||
_tags = model.get("tags", []) as Array[String]
|
||||
_refresh_tag_controls()
|
||||
_material_preset = int(model.get("material_preset", PropBlockScript.MaterialPreset.NONE))
|
||||
if _preset_option != null:
|
||||
_preset_option.selected = _material_preset
|
||||
@@ -1500,6 +1823,9 @@ func _apply_json_data(json: Variant, is_recovery: bool = false) -> String:
|
||||
|
||||
|
||||
func _save_to(path: String, with_thumbnail: bool = true) -> bool:
|
||||
if _stage_size_preview_active:
|
||||
_request_save_after_stage_size(MENU_SAVE)
|
||||
return false
|
||||
path = ContentPathsScript.canonicalize(path)
|
||||
var ok: bool = PropFileScript.save(path, _build_json_data())
|
||||
if not ok:
|
||||
@@ -1507,7 +1833,8 @@ func _save_to(path: String, with_thumbnail: bool = true) -> bool:
|
||||
return false
|
||||
_current_path = path
|
||||
_dirty = false
|
||||
_clear_autosave()
|
||||
if not _stage_authoring:
|
||||
_clear_autosave()
|
||||
_push_recent(path)
|
||||
_update_save_status()
|
||||
if with_thumbnail:
|
||||
@@ -1554,21 +1881,27 @@ func _execute_pending_save(center_first: bool) -> void:
|
||||
elif _pending_new:
|
||||
_pending_new = false
|
||||
_do_new()
|
||||
elif _stage_return_pending:
|
||||
_stage_return_pending = false
|
||||
artwork_saved.emit(_current_path)
|
||||
return_requested.emit()
|
||||
|
||||
func _cancel_pending_save_request() -> void:
|
||||
_pending_save_path = ""
|
||||
_pending_save_with_thumbnail = true
|
||||
_pending_quit = false
|
||||
_pending_new = false
|
||||
_stage_return_pending = false
|
||||
|
||||
func _on_save_file_selected(path: String) -> void:
|
||||
path = ContentPathsScript.canonicalize(path)
|
||||
if path.get_extension().to_lower() != "prop":
|
||||
path = path.get_basename() + ".prop"
|
||||
_begin_save_request(path, not (_pending_quit or _pending_new))
|
||||
_begin_save_request(path, not (_pending_quit or _pending_new or _stage_return_pending))
|
||||
|
||||
|
||||
func _load_from(path: String) -> void:
|
||||
_cancel_stage_size_preview()
|
||||
path = ContentPathsScript.canonicalize(path)
|
||||
var raw: Dictionary = PropFileScript.load(path)
|
||||
if raw.is_empty():
|
||||
|
||||
@@ -2,7 +2,7 @@ class_name PropFile
|
||||
extends RefCounted
|
||||
## PropFile - Static schema + converters for .prop vector prop assets.
|
||||
##
|
||||
## The file stores the editor's full multi-shape stack (version "1.0") plus
|
||||
## The file stores the editor's full multi-shape stack (version "1.1") plus
|
||||
## enough provenance for later sandbox placement. flatten_to_payload() converts
|
||||
## the stack into the single-shape payload PropUtils.spawn_prop() already
|
||||
## consumes: frontmost closed polygon -> else circle -> else first shape.
|
||||
@@ -12,10 +12,11 @@ const TerrainUtilsScript := preload("res://scripts/terrain/terrain_utils.gd")
|
||||
const PropAnchorDataScript := preload("res://scripts/props/prop_anchor_data.gd")
|
||||
const ContentPathsScript := preload("res://scripts/shared/content_paths.gd")
|
||||
|
||||
const FILE_VERSION: String = "1.0"
|
||||
const FILE_VERSION: String = "1.1"
|
||||
const FILE_EXT: String = ".prop"
|
||||
const DEFAULT_DIR: String = "res://content/props"
|
||||
const SUPPORTED_VERSIONS: Array[String] = ["1.0"]
|
||||
const SUPPORTED_VERSIONS: Array[String] = ["1.0", "1.1"]
|
||||
const KNOWN_TAGS: Array[String] = ["nature", "buildings", "walls", "roads", "objects"]
|
||||
# index == PropBlock.MaterialPreset
|
||||
const PRESET_STRINGS: PackedStringArray = ["none", "wood", "rubber", "cardboard", "metal"]
|
||||
const SHAPE_STRINGS: PackedStringArray = ["polygon", "circle", "line"]
|
||||
@@ -63,7 +64,7 @@ static func sanitize_shape(shape: Dictionary, grid_size: float = TerrainUtilsScr
|
||||
return result
|
||||
|
||||
|
||||
static func to_file_dict(prop_name: String, material_preset: int, shapes: Array[Dictionary], anchors: Dictionary = {}) -> Dictionary:
|
||||
static func to_file_dict(prop_name: String, material_preset: int, shapes: Array[Dictionary], anchors: Dictionary = {}, tags: Array[String] = []) -> Dictionary:
|
||||
var serialized: Array = []
|
||||
for shape: Dictionary in shapes:
|
||||
serialized.append(_serialize_shape(shape))
|
||||
@@ -74,6 +75,7 @@ static func to_file_dict(prop_name: String, material_preset: int, shapes: Array[
|
||||
"material_preset": preset_to_string(material_preset),
|
||||
"shapes": serialized,
|
||||
"anchors": PropAnchorDataScript.serialize_anchors(anchors),
|
||||
"tags": normalize_tags(tags),
|
||||
"metadata": {"created_at": now, "modified_at": now},
|
||||
}
|
||||
|
||||
@@ -98,6 +100,7 @@ static func from_file_dict(data: Variant) -> Dictionary:
|
||||
return {}
|
||||
|
||||
var preset: int = preset_from_string(str(dict.get("material_preset", "none")))
|
||||
var tags: Array[String] = normalize_tags(dict.get("tags", []))
|
||||
var shapes: Array[Dictionary] = []
|
||||
var raw_shapes: Variant = dict.get("shapes", [])
|
||||
if raw_shapes is Array:
|
||||
@@ -109,9 +112,21 @@ static func from_file_dict(data: Variant) -> Dictionary:
|
||||
"material_preset": preset,
|
||||
"shapes": shapes,
|
||||
"anchors": PropAnchorDataScript.normalize_anchors(dict.get("anchors", {})),
|
||||
"tags": tags,
|
||||
}
|
||||
|
||||
|
||||
static func normalize_tags(value: Variant) -> Array[String]:
|
||||
var tags: Array[String] = []
|
||||
if not value is Array:
|
||||
return tags
|
||||
for raw_tag: Variant in value as Array:
|
||||
var tag: String = str(raw_tag).strip_edges().to_lower()
|
||||
if KNOWN_TAGS.has(tag) and not tags.has(tag):
|
||||
tags.append(tag)
|
||||
return tags
|
||||
|
||||
|
||||
## Deterministic conversion of the multi-shape stack into a single
|
||||
## PropUtils.spawn_prop payload. Rule: frontmost closed polygon -> else circle
|
||||
## -> else first shape. Returns {} when shapes is empty.
|
||||
|
||||
Reference in New Issue
Block a user