extends RefCounted ## M59 test-only ink checker. The shipped runtime never loads this script; the ## Market visual probe preloads it so the M58-C defect class (label wider than ## its slot, label ink reaching into a sibling icon, label leaving its owning ## module) fails the capture gate instead of reaching the owner. ## ## Width rules use `Font.get_string_size` at the label's theme size, which is ## the same authority the renderer uses. The checker reports; the probe decides ## how to fail. Nothing here mutates the scanned view. const TOLERANCE := 1.5 static func scan(root: Control) -> Array: var violations: Array = [] for node in _descendants(root): var label := node as Label if label == null or not label.is_visible_in_tree(): continue _check_text_fit(label, violations) _check_containment(label, violations) _check_icon_overlap(label, violations) return violations static func self_test(host: Node) -> Array: var container := Control.new() container.name = "ABGInkSelfTest" container.size = Vector2(200.0, 60.0) host.add_child(container) var wide := Label.new() wide.name = "SelfTestWideLabel" wide.text = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" wide.add_theme_font_override("font", load("res://assets/fonts/NotoSansSC.ttf")) wide.add_theme_font_size_override("font_size", 20) wide.clip_text = true wide.position = Vector2(0.0, 0.0) wide.size = Vector2(40.0, 20.0) container.add_child(wide) var icon := Control.new() icon.name = "SelfTestIcon" icon.position = Vector2(20.0, 0.0) icon.size = Vector2(30.0, 30.0) container.add_child(icon) var outside := Label.new() outside.name = "SelfTestOutsideLabel" outside.text = "ok" outside.add_theme_font_override("font", load("res://assets/fonts/NotoSansSC.ttf")) outside.add_theme_font_size_override("font_size", 20) outside.position = Vector2(190.0, 0.0) outside.size = Vector2(60.0, 20.0) container.add_child(outside) var violations := scan(container) container.queue_free() return violations static func _descendants(root: Control) -> Array: var out: Array = [] var stack: Array = [root] while not stack.is_empty(): var current: Node = stack.pop_back() for child in current.get_children(): out.append(child) stack.append(child) return out static func _check_text_fit(label: Label, violations: Array) -> void: if label.text.is_empty(): return var font := label.get_theme_font("font") if font == null: return var font_size := label.get_theme_font_size("font_size") if label.autowrap_mode == TextServer.AUTOWRAP_OFF: var wanted := font.get_string_size(label.text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x if wanted > label.size.x + TOLERANCE: violations.append({ "kind": "slot_overflow", "node": String(label.name), "path": String(label.get_path()), "detail": "text=%.1f slot=%.1f font=%d" % [wanted, label.size.x, font_size], }) else: var wrapped := font.get_multiline_string_size( label.text, HORIZONTAL_ALIGNMENT_LEFT, maxf(label.size.x, 1.0), font_size ) # The box is the authority: fully wrapped text taller than the label is # truncation, and a max-lines cap that still fits the box is not. var allowed := label.size.y if wrapped.y > allowed + TOLERANCE: violations.append({ "kind": "vertical_overflow", "node": String(label.name), "path": String(label.get_path()), "detail": "wrapped=%.1f allowed=%.1f font=%d" % [wrapped.y, allowed, font_size], }) static func _check_containment(label: Label, violations: Array) -> void: var parent := label.get_parent() as Control if parent == null: return var rect := label.get_global_rect() var owner := parent.get_global_rect() if ( rect.position.x < owner.position.x - TOLERANCE or rect.position.y < owner.position.y - TOLERANCE or rect.end.x > owner.end.x + TOLERANCE or rect.end.y > owner.end.y + TOLERANCE ): violations.append({ "kind": "outside_owner", "node": String(label.name), "path": String(label.get_path()), "detail": "label=%s owner=%s" % [rect, owner], }) static func ink_rect(label: Label) -> Rect2: # The rect of a centered label spans the whole slot, so comparing rects # against a sibling icon produces false positives on accepted command # plates. Measure the box the glyphs actually occupy instead. var rect := label.get_global_rect() var font := label.get_theme_font("font") if font == null or label.text.is_empty(): return rect var font_size := label.get_theme_font_size("font_size") var text_size: Vector2 if label.autowrap_mode == TextServer.AUTOWRAP_OFF: text_size = font.get_string_size(label.text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size) else: text_size = font.get_multiline_string_size(label.text, HORIZONTAL_ALIGNMENT_LEFT, rect.size.x, font_size) var x := rect.position.x match label.horizontal_alignment: HORIZONTAL_ALIGNMENT_CENTER: x = rect.position.x + (rect.size.x - text_size.x) * 0.5 HORIZONTAL_ALIGNMENT_RIGHT: x = rect.end.x - text_size.x var y := rect.position.y match label.vertical_alignment: VERTICAL_ALIGNMENT_CENTER: y = rect.position.y + (rect.size.y - text_size.y) * 0.5 VERTICAL_ALIGNMENT_BOTTOM: y = rect.end.y - text_size.y return Rect2( Vector2(x, y), Vector2(minf(text_size.x, rect.size.x), minf(text_size.y, rect.size.y)) ) static func _check_icon_overlap(label: Label, violations: Array) -> void: var parent := label.get_parent() if parent == null: return var rect := ink_rect(label) for sibling in parent.get_children(): if sibling == label: continue var control := sibling as Control if control == null or not control.is_visible_in_tree(): continue if "Icon" not in String(control.name): continue # A stacked plate (icon above its caption) shares horizontal extent with # its label by design; the defect class is a glyph drawn over the icon, # which is when the icon's centre falls inside the label's ink box. var icon_rect := control.get_global_rect() if rect.has_point(icon_rect.get_center()): violations.append({ "kind": "icon_overlap", "node": String(label.name), "path": String(label.get_path()), "detail": "label_ink=%s icon=%s(%s)" % [rect, control.name, icon_rect], })