## 卷轴视口承载件(level-map.md 布局决策 5——关卡画面选路屏):画布宿主与裁切、平移状态 ## 与 clamp、锚点跟随与平滑滚动、指引件(屏缘渐隐带+平移提示 chip)。 ## 纯呈现态:平移位置零核心写、不进存档(重进本屏按锚点规则重新定位);跟随只随停点切换/ ## 兑付回图发生(零自动推进)。拖拽/滚轮判定留画布 _gui_input(事件流同源——悬停/选路同源), ## 本件只管裁切/offset/clamp/跟随/指引;画布包络=画布实际尺寸(open_choices 拟合后同帧收敛)。 class_name LevelMapViewport extends Control const SCROLL_DUR := 0.3 # 平滑滚动时长(布局决策 5 provisional 0.3s 单段缓动) const FADE_BAND_W := 70.0 # 屏缘渐隐带宽(布局决策 5/像素级判据②) const CHIP_SIZE := Vector2(240, 30) # 平移提示 chip(像素级判据③——图区底部居中) const CHIP_CORNER := 6 # chip 圆角(布局决策 5) const CHIP_BOTTOM_GAP := 24.0 # chip 距视口底缘(效果图档同族取值) const CHIP_TEXT := "◄ 拖拽画布 / 滚轮平移 ►" # 14px(FS_HINT 档——布局决策 5) const EDGE_EPS := 0.5 # 包络边界判定容差(浮点平移累积) var _canvas: Control var _anchor_provider: Callable # () -> Vector2 锚点画布坐标(screen 提供——当前节点/入口组) var _viewport_size := Vector2.ZERO # 窗口尺寸兜底(setup 注入设计基座——实况窗口以控件 size 为准,布局晚于构造) var _offset := Vector2.ZERO: # 画布左上角在视口件坐标系的位置(纯呈现态——直驱画布 position) set(value): _offset = value if _canvas != null: _canvas.position = _offset var _view_tween: Tween var _guided_visible := false # 指引件可见态(画布超窗才暗示——浅图无平移余量即隐藏) ## 一次交齐注入(code-conventions §1.6 拆分交接:画布宿主、锚点来源、窗口兜底尺寸) func setup(canvas: Control, anchor_provider: Callable, viewport_size: Vector2) -> void: _canvas = canvas _anchor_provider = anchor_provider _viewport_size = viewport_size clip_contents = true add_child(canvas) _build_guidance() func _notification(what: int) -> void: # 窗口实况尺寸落定/变化即重锚(首开布局晚于 open_choices 的锚定——隐藏子树容器排序 # 首个可见帧才传播,4.7 行为;实况窗口以本件 size 为准,落定即收敛锚点) if what == NOTIFICATION_RESIZED and _canvas != null and _anchor_provider.is_valid(): reanchor(false) ## 锚点跟随(布局决策 5:锚点居中;smooth=停点切换/兑付回图平滑滚动,减动效开=跳终态; ## 重入先杀在飞 tween 再建新——快速连续停点切换防叠加) func reanchor(smooth: bool) -> void: if _canvas == null or not _anchor_provider.is_valid(): return var target := _clamped_center_offset(_anchor_provider.call()) _kill_scroll_tween() if not smooth or ArmedTimer.motion_still(): _offset = target _sync_guidance() return _view_tween = create_tween() _view_tween.tween_property(self, "_offset", target, UiKit.motion(SCROLL_DUR)) \ .set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT) _sync_guidance() ## 手动平移(画布 _gui_input 判定后调用——拖拽沿逐帧/滚轮步进;纯呈现零核心写) func pan_by(delta: Vector2) -> void: if _canvas == null: return _kill_scroll_tween() var win := _window_size() _offset = Vector2( _axis_pan_offset(_offset.x, delta.x, _canvas.size.x, win.x), _axis_pan_offset(_offset.y, delta.y, _canvas.size.y, win.y)) _sync_guidance() ## 视口中线的画布坐标(锚点居中判据投影面——浅图=画布中心,深图=夹紧域内当前注视点) func viewport_center_canvas_pos() -> Vector2: return _window_size() / 2.0 - _offset ## 视口是否钉在画布包络边界(任一超窗轴 offset 顶边——clamp 判据面) func is_clamped_at_edge() -> bool: var win := _window_size() if _canvas == null: return false return _axis_at_edge(_offset.x, _canvas.size.x, win.x) \ or _axis_at_edge(_offset.y, _canvas.size.y, win.y) func _kill_scroll_tween() -> void: if _view_tween != null and _view_tween.is_valid(): _view_tween.kill() _view_tween = null ## 单轴锚定 offset:画布不超窗=居中分支(夹紧域退化——布局决策 5); ## 超窗=锚点夹紧在画布包络内,锚点落视口中线(offset = 半窗 − 锚点) func _axis_center_offset(anchor: float, canvas_len: float, half_win: float) -> float: if canvas_len <= half_win * 2.0: return half_win - canvas_len / 2.0 return half_win - clampf(anchor, half_win, canvas_len - half_win) func _clamped_center_offset(anchor: Vector2) -> Vector2: var half := _window_size() / 2.0 return Vector2( _axis_center_offset(anchor.x, _canvas.size.x, half.x), _axis_center_offset(anchor.y, _canvas.size.y, half.y)) ## 单轴平移收敛:居中轴零平移余量不动;超窗轴 offset 收敛在 [半窗−画布, 0](不出露屏底空档) func _axis_pan_offset(cur: float, delta: float, canvas_len: float, win_len: float) -> float: if canvas_len <= win_len: return cur return clampf(cur + delta, win_len - canvas_len, 0.0) func _axis_at_edge(offset: float, canvas_len: float, win_len: float) -> bool: if canvas_len <= win_len: return false return offset >= -EDGE_EPS or offset <= win_len - canvas_len + EDGE_EPS ## 实况窗口优先(容器布局晚于构造——注入值只兜构造帧数学) func _window_size() -> Vector2: if size.x > 0.0 and size.y > 0.0: return size return _viewport_size ## 指引件(布局决策 5/像素级判据②③):屏缘渐隐带盖在画布之上+图区底部居中提示 chip; ## 可见性=画布超窗(_sync_guidance 随锚定/平移刷新) func _build_guidance() -> void: var band_l := TextureRect.new() band_l.name = "FadeL" band_l.texture = _fade_gradient(false) band_l.set_anchors_preset(Control.PRESET_LEFT_WIDE) band_l.offset_right = FADE_BAND_W band_l.expand_mode = TextureRect.EXPAND_IGNORE_SIZE band_l.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(band_l) var band_r := TextureRect.new() band_r.name = "FadeR" band_r.texture = _fade_gradient(true) band_r.set_anchors_preset(Control.PRESET_RIGHT_WIDE) band_r.offset_left = -FADE_BAND_W band_r.expand_mode = TextureRect.EXPAND_IGNORE_SIZE band_r.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(band_r) var chip := PanelContainer.new() chip.name = "PanHint" chip.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.HP_BAR_BACK, UiKit.STONE, CHIP_CORNER, 1, Vector4(12, 5, 12, 5))) chip.set_anchors_preset(Control.PRESET_CENTER_BOTTOM) chip.offset_left = -CHIP_SIZE.x / 2.0 chip.offset_right = CHIP_SIZE.x / 2.0 chip.offset_top = -CHIP_SIZE.y - CHIP_BOTTOM_GAP chip.offset_bottom = -CHIP_BOTTOM_GAP chip.custom_minimum_size = CHIP_SIZE chip.mouse_filter = Control.MOUSE_FILTER_IGNORE var chip_label := UiKit.label(CHIP_TEXT, UiKit.FS_HINT, false, UiKit.WARM) chip_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER chip_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER chip.add_child(chip_label) add_child(chip) _sync_guidance() ## 屏缘渐隐渐变(色取屏底同族 BG_ROOT——代拍板:不透明端 0.9 单段衰减) func _fade_gradient(transparent_at_left: bool) -> GradientTexture2D: var grad := Gradient.new() grad.offsets = PackedFloat32Array([0.0, 1.0]) grad.colors = PackedColorArray([Color(UiKit.BG_ROOT, 0.9), Color(UiKit.BG_ROOT, 0.0)]) var tex := GradientTexture2D.new() tex.gradient = grad tex.fill_from = Vector2(1, 0.5) if transparent_at_left else Vector2(0, 0.5) tex.fill_to = Vector2(0, 0.5) if transparent_at_left else Vector2(1, 0.5) tex.width = int(FADE_BAND_W) return tex func _sync_guidance() -> void: var win := _window_size() var over := _canvas != null and (_canvas.size.x > win.x or _canvas.size.y > win.y) if over == _guided_visible: return _guided_visible = over for nm in ["FadeL", "FadeR", "PanHint"]: var node := get_node_or_null(NodePath(nm)) as Control if node != null: node.visible = over