From f0ddd9b5fd9646de3eedb3014f7473d24a4fdfaa Mon Sep 17 00:00:00 2001 From: by-ae <25704492+by-ae@users.noreply.github.com> Date: Mon, 2 Nov 2026 07:21:23 +1000 Subject: [PATCH] fix: round caption edges the same way as the editor Python round() uses ties-to-even, so a coordinate on a half step was one unit off from Copy JSON, which rounds halves toward +inf. --- nodes/ideogram4_nodes.py | 18 ++++++++++++++---- tests/caption_layout.test.js | 2 ++ tests/test_nodes.py | 12 ++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/nodes/ideogram4_nodes.py b/nodes/ideogram4_nodes.py index 2a4c55c..0b3c086 100644 --- a/nodes/ideogram4_nodes.py +++ b/nodes/ideogram4_nodes.py @@ -2,6 +2,7 @@ import json import logging +import math import os import re @@ -341,17 +342,26 @@ def _render_preview(boxes, width, height, bg_guid="", background_opacity=0.5): return torch.from_numpy(arr).unsqueeze(0) +def _round_half_up(value): + """Nearest integer, halves toward +inf. Matches JS Math.round. + + Python round() uses ties-to-even, so a caption edge on x.5 disagreed + with Copy JSON by one unit. + """ + return int(math.floor(value + 0.5)) + + def _scale_axis(value): """Map one finite axis onto Ideogram's integer 0-1000 range. - Clamp before rounding. round() raises OverflowError once value * 1000 - exceeds the float range, which a hand-edited region can do. + Values outside 0-1 are clamped before scaling. A huge finite coordinate + used to overflow inside round(). """ if value >= 1: return 1000 if value <= 0: return 0 - return int(round(value * 1000)) + return _round_half_up(value * 1000) def _box_coords(box): @@ -399,7 +409,7 @@ def _preview_edges(box, rw, rh): return scale if value <= 0: return 0 - return max(0, min(scale, int(round(value * scale)))) + return max(0, min(scale, _round_half_up(value * scale))) x1, y1, x2, y2 = edge(x, rw), edge(y, rh), edge(x + w, rw), edge(y + h, rh) if x2 < x1: diff --git a/tests/caption_layout.test.js b/tests/caption_layout.test.js index 69bed1d..ecfd8f8 100644 --- a/tests/caption_layout.test.js +++ b/tests/caption_layout.test.js @@ -104,6 +104,8 @@ describe("ideogram bbox import", () => { it("exports a usable bbox and drops coordinates that are not finite", () => { assert.deepEqual(rectToIdeogramBbox({ x: 0.1, y: 0.2, w: 0.3, h: 0.4 }), [200, 100, 600, 400]); + // Halves go toward +inf, the same rule the prompt node uses. + assert.deepEqual(rectToIdeogramBbox({ x: 0.0025, y: 0.0005, w: 0.001, h: 0.001 }), [1, 3, 2, 4]); assert.deepEqual(rectToIdeogramBbox({ x: "0.1", y: "0.2", w: "0.3", h: "0.4" }), [200, 100, 600, 400]); assert.deepEqual(rectToIdeogramBbox({ x: 0.5, y: 0.6, w: -0.2, h: -0.1 }), [500, 300, 600, 500]); assert.equal(rectToIdeogramBbox({ x: Number.NaN, y: 0.2, w: 0.3, h: 0.4 }), null); diff --git a/tests/test_nodes.py b/tests/test_nodes.py index 97210c5..99269ed 100644 --- a/tests/test_nodes.py +++ b/tests/test_nodes.py @@ -86,6 +86,12 @@ class TestPromptHelpers(unittest.TestCase): result = _norm_bbox({"x": 1e308, "y": 0.0, "w": 1e308, "h": 0.2}) self.assertEqual(result, [0, 1000, 200, 1000]) + def test_norm_bbox_half_steps_round_like_the_editor(self): + # 2.5 and 0.5. Copy JSON uses JS Math.round (half toward +inf). + # Python round() uses ties-to-even, which made the prompt one unit off. + box = {"x": 0.0025, "y": 0.0005, "w": 0.001, "h": 0.001} + self.assertEqual(_norm_bbox(box), [1, 3, 2, 4]) + def test_elements_from_boxes_omits_unusable_bbox(self): elements = _elements_from_boxes([ {"type": "obj", "desc": "bad", "x": "nope", "y": 0, "w": 0.2, "h": 0.2}, @@ -111,6 +117,12 @@ class TestPromptHelpers(unittest.TestCase): (10, 10, 40, 30), ) + def test_preview_edges_half_pixel_rounds_like_the_editor(self): + self.assertEqual( + _preview_edges({"x": 0.005, "y": 0, "w": 0.01, "h": 0.02}, 100, 100), + (1, 0, 2, 2), + ) + def test_elements_keep_schema_types_when_fields_are_not_strings(self): elements = _elements_from_boxes([ {