diff --git a/backend/data_types.py b/backend/data_types.py index 99e2385..95ae9c4 100644 --- a/backend/data_types.py +++ b/backend/data_types.py @@ -504,7 +504,17 @@ def _render_overlay_text( @lru_cache(maxsize=1) def _overlay_font_candidates() -> tuple[str, ...]: - candidates: list[str] = [] + candidates: list[str] = [ + "/System/Library/Fonts/HelveticaNeue.ttc", + "/System/Library/Fonts/Helvetica.ttc", + "/System/Library/Fonts/Supplemental/Arial.ttf", + "/System/Library/Fonts/Supplemental/Helvetica.ttc", + "/System/Library/Fonts/Supplemental/Times New Roman.ttf", + "/Library/Fonts/Arial.ttf", + "/Library/Fonts/Helvetica.ttc", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf", + ] try: import PIL @@ -517,16 +527,6 @@ def _overlay_font_candidates() -> tuple[str, ...]: except Exception: pass - candidates.extend([ - "/System/Library/Fonts/Supplemental/Arial.ttf", - "/System/Library/Fonts/Supplemental/Helvetica.ttc", - "/System/Library/Fonts/Supplemental/Times New Roman.ttf", - "/Library/Fonts/Arial.ttf", - "/Library/Fonts/Helvetica.ttc", - "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", - "/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf", - ]) - unique: list[str] = [] for candidate in candidates: if candidate not in unique and Path(candidate).exists(): @@ -891,7 +891,7 @@ def _angle_label_base_position(spec: dict[str, Any]) -> tuple[float, float]: ) -def _sanitize_angle_overlay_thickness(value: Any, default: float = 1.35) -> float: +def _sanitize_angle_overlay_stroke_width(value: Any, default: float = 1.35) -> float: try: numeric = float(value) except (TypeError, ValueError): @@ -983,14 +983,14 @@ def _apply_angle_measure_overlay(image: np.ndarray, field: DataField | None, spe label_dy = float(spec.get("label_dy", 0.0) or 0.0) angle_deg = float(spec.get("angle_deg", 0.0) or 0.0) color_hex = _sanitize_angle_overlay_color(spec.get("color", "#ff0000")) - line_thickness = _sanitize_angle_overlay_thickness(spec.get("line_thickness", 1.35)) + stroke_width = _sanitize_angle_overlay_stroke_width(spec.get("stroke_width", spec.get("line_thickness", 1.35))) base_rgb = _hex_to_rgb(color_hex) arc_rgb = _mix_rgb(base_rgb, (255, 255, 255), 0.42) badge_text_rgb = _mix_rgb(base_rgb, (255, 255, 255), 0.72) badge_border_rgb = _mix_rgb(base_rgb, (255, 255, 255), 0.32) - line_width = max(1, int(round(longest_dim * line_thickness / 100.0))) - arc_width = max(1, int(round(longest_dim * max(0.85, line_thickness * 0.78) / 100.0))) + line_width = max(1, int(round(longest_dim * stroke_width / 100.0))) + arc_width = line_width line_color = (*base_rgb, 255) arc_color = (*arc_rgb, 242) @@ -1034,6 +1034,7 @@ def _apply_angle_measure_overlay(image: np.ndarray, field: DataField | None, spe label_text, max(10, int(round(longest_dim / 26.0))), badge_text_rgb, + font_spec={"family": "Helvetica Neue"}, ) bg_pad_x = max(5, int(round(text_image.size[0] * 0.16))) diff --git a/backend/nodes/angle_measure.py b/backend/nodes/angle_measure.py index 83e8e52..93df65a 100644 --- a/backend/nodes/angle_measure.py +++ b/backend/nodes/angle_measure.py @@ -16,12 +16,14 @@ from backend.execution_context import emit_overlay, emit_table from backend.node_registry import register_node from backend.nodes.helpers import _normalize_markup_color +ANGLE_DEFAULT_COLOR = "#ff9800" + def _clamp01(value: float) -> float: return float(np.clip(value, 0.0, 1.0)) -def _sanitize_line_thickness(value: float | None, default: float = 1.35) -> float: +def _sanitize_stroke_width(value: float | None, default: float = 1.35) -> float: try: numeric = float(value) except (TypeError, ValueError): @@ -56,7 +58,7 @@ def _angle_overlay_spec( angle_deg: float, label_dx: float, label_dy: float, - line_thickness: float, + stroke_width: float, color: str, ) -> dict[str, float | str]: return { @@ -70,7 +72,7 @@ def _angle_overlay_spec( "angle_deg": float(angle_deg), "label_dx": float(label_dx), "label_dy": float(label_dy), - "line_thickness": float(line_thickness), + "stroke_width": float(stroke_width), "color": str(color), } @@ -84,14 +86,13 @@ class AngleMeasure: return { "required": { "input": ("ANNOTATION_SOURCE", {"label": "Input"}), - "color": ("STRING", {"default": "#ff0000", "color_picker": True}), - "line_thickness": ("FLOAT", { + "color": ("STRING", {"default": ANGLE_DEFAULT_COLOR, "color_picker": True}), + "stroke_width": ("FLOAT", { "default": 1.35, "min": 0.35, "max": 6.0, "step": 0.05, - "label": "line thickness", - "hide_when_input_connected": "line_thickness_input", + "label": "stroke width", }), "x1": ("FLOAT", {"default": 0.22, "min": 0.0, "max": 1.0, "step": 0.01, "hidden": True}), "y1": ("FLOAT", {"default": 0.72, "min": 0.0, "max": 1.0, "step": 0.01, "hidden": True}), @@ -103,7 +104,8 @@ class AngleMeasure: "label_dy": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.01, "hidden": True}), }, "optional": { - "line_thickness_input": ("FLOAT", {"label": "line thickness"}), + "line_thickness": ("FLOAT", {"hidden": True}), + "line_thickness_input": ("FLOAT", {"hidden": True}), }, } @@ -114,15 +116,15 @@ class AngleMeasure: DESCRIPTION = ( "Measure the included angle between two draggable line segments over a DATA_FIELD or IMAGE. " "Drag either endpoint to change that arm, drag the middle joint to move the whole widget, " - "drag the angle label to reposition it, choose the overlay color, and adjust line thickness " - "with the widget or a FLOAT input." + "drag the angle label to reposition it, choose the overlay color, and adjust stroke width " + "with the widget or its FLOAT socket." ) def process( self, input, color: str, - line_thickness: float, + stroke_width: float, x1: float, y1: float, xm: float, @@ -131,6 +133,7 @@ class AngleMeasure: y2: float, label_dx: float, label_dy: float, + line_thickness: float | None = None, line_thickness_input: float | None = None, ) -> tuple: x1 = _clamp01(x1) @@ -141,9 +144,10 @@ class AngleMeasure: y2 = _clamp01(y2) label_dx = float(np.clip(label_dx, -1.0, 1.0)) label_dy = float(np.clip(label_dy, -1.0, 1.0)) - resolved_color = _normalize_markup_color(color, default="#ff0000") - resolved_line_thickness = _sanitize_line_thickness( - line_thickness_input if line_thickness_input is not None else line_thickness, + resolved_color = _normalize_markup_color(color, default=ANGLE_DEFAULT_COLOR) + legacy_stroke_width = line_thickness_input if line_thickness_input is not None else line_thickness + resolved_stroke_width = _sanitize_stroke_width( + legacy_stroke_width if legacy_stroke_width is not None else stroke_width, ) if isinstance(input, DataField): @@ -177,7 +181,7 @@ class AngleMeasure: angle_deg=angle_deg, label_dx=label_dx, label_dy=label_dy, - line_thickness=resolved_line_thickness, + stroke_width=resolved_stroke_width, color=resolved_color, ) table = MeasureTable([ diff --git a/frontend/src/AngleMeasureOverlay.jsx b/frontend/src/AngleMeasureOverlay.jsx index 342e149..ceca6af 100644 --- a/frontend/src/AngleMeasureOverlay.jsx +++ b/frontend/src/AngleMeasureOverlay.jsx @@ -12,7 +12,7 @@ function clamp01(value) { return Math.max(0, Math.min(1, Number(value) || 0)); } -function sanitizeHexColor(value, fallback = '#ff0000') { +function sanitizeHexColor(value, fallback = '#ff9800') { if (typeof value !== 'string') return fallback; const text = value.trim(); return /^#[0-9a-fA-F]{6}$/.test(text) ? text.toLowerCase() : fallback; @@ -73,15 +73,14 @@ export default function AngleMeasureOverlay({ labelDy, angleDeg, color, - lineThickness, + strokeWidth, nodeId, onWidgetChange, }) { const containerRef = useRef(null); const [dragging, setDragging] = useState(null); - const resolvedColor = sanitizeHexColor(color, '#ff0000'); - const resolvedLineThickness = Math.max(0.35, Math.min(6, Number(lineThickness) || 1.35)); - const resolvedArcThickness = Math.max(0.85, resolvedLineThickness * 0.78); + const resolvedColor = sanitizeHexColor(color, '#ff9800'); + const resolvedStrokeWidth = Math.max(0.35, Math.min(6, Number(strokeWidth) || 1.35)); const resolvedArcColor = mixColor(resolvedColor, '#ffffff', 0.42); const resolvedMidColor = mixColor(resolvedColor, '#ffffff', 0.72); const resolvedBadgeTextColor = mixColor(resolvedColor, '#ffffff', 0.72); @@ -166,8 +165,7 @@ export default function AngleMeasureOverlay({ '--angle-mid-handle-color': resolvedMidColor, '--angle-badge-text-color': resolvedBadgeTextColor, '--angle-badge-border-color': resolvedBadgeBorderColor, - '--angle-line-thickness': `${resolvedLineThickness}`, - '--angle-arc-thickness': `${resolvedArcThickness}`, + '--angle-stroke-width': `${resolvedStrokeWidth}`, }} onPointerMove={onPointerMove} onPointerUp={onPointerUp} diff --git a/frontend/src/CustomNode.jsx b/frontend/src/CustomNode.jsx index 20d69d5..897373a 100644 --- a/frontend/src/CustomNode.jsx +++ b/frontend/src/CustomNode.jsx @@ -1316,10 +1316,10 @@ function CustomNode({ id, data }) { labelDx={data.widgetValues.label_dx ?? data.overlay.label_dx ?? 0} labelDy={data.widgetValues.label_dy ?? data.overlay.label_dy ?? 0} angleDeg={data.overlay.angle_deg} - color={data.widgetValues.color ?? data.overlay.color ?? '#ff0000'} - lineThickness={connectedInputs?.has('line_thickness_input') - ? (data.overlay.line_thickness ?? data.widgetValues.line_thickness ?? 1.35) - : (data.widgetValues.line_thickness ?? data.overlay.line_thickness ?? 1.35)} + color={data.widgetValues.color ?? data.overlay.color ?? '#ff9800'} + strokeWidth={connectedInputs?.has('stroke_width') + ? (data.overlay.stroke_width ?? data.overlay.line_thickness ?? data.widgetValues.stroke_width ?? 1.35) + : (data.widgetValues.stroke_width ?? data.overlay.stroke_width ?? data.overlay.line_thickness ?? 1.35)} nodeId={id} onWidgetChange={ctx.onWidgetChange} /> diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 2742666..25e3e0f 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -933,14 +933,13 @@ html, body, #root { } .angle-overlay { - --angle-line-color: #ff0000; - --angle-arc-color: rgb(255, 107, 107); - --angle-end-handle-color: #ff0000; - --angle-mid-handle-color: rgb(255, 184, 184); - --angle-badge-text-color: rgb(255, 184, 184); - --angle-badge-border-color: rgb(255, 82, 82); - --angle-line-thickness: 1.35; - --angle-arc-thickness: 1.05; + --angle-line-color: #ff9800; + --angle-arc-color: rgb(255, 166, 77); + --angle-end-handle-color: #ff9800; + --angle-mid-handle-color: rgb(255, 210, 163); + --angle-badge-text-color: rgb(255, 210, 163); + --angle-badge-border-color: rgb(255, 183, 77); + --angle-stroke-width: 1.35; position: relative; overflow: hidden; user-select: none; @@ -965,14 +964,14 @@ html, body, #root { .angle-line { stroke: var(--angle-line-color); - stroke-width: var(--angle-line-thickness); + stroke-width: var(--angle-stroke-width); stroke-linecap: round; } .angle-arc { fill: none; stroke: var(--angle-arc-color); - stroke-width: var(--angle-arc-thickness); + stroke-width: var(--angle-stroke-width); stroke-dasharray: 5 3; opacity: 0.95; } diff --git a/tests/test_nodes.py b/tests/test_nodes.py index 4133460..a3c17c5 100644 --- a/tests/test_nodes.py +++ b/tests/test_nodes.py @@ -704,7 +704,11 @@ def test_angle_measure(): node = AngleMeasure() assert get_node_info("AngleMeasure")["category"] == "Overlay" required_inputs = AngleMeasure.INPUT_TYPES()["required"] - assert required_inputs["color"][1]["default"] == "#ff0000" + optional_inputs = AngleMeasure.INPUT_TYPES().get("optional", {}) + assert required_inputs["color"][1]["default"] == "#ff9800" + assert required_inputs["stroke_width"][1]["default"] == 1.35 + assert optional_inputs["line_thickness"][1]["hidden"] is True + assert optional_inputs["line_thickness_input"][1]["hidden"] is True field = make_field( data=np.zeros((32, 64), dtype=np.float64), @@ -714,7 +718,7 @@ def test_angle_measure(): output, table = node.process( field, color="#c62828", - line_thickness=1.8, + stroke_width=1.8, x1=0.2, y1=0.5, xm=0.5, @@ -730,17 +734,17 @@ def test_angle_measure(): assert len(output.overlays) == len(field.overlays) + 1 assert output.overlays[-1]["kind"] == "angle_measure" assert output.overlays[-1]["color"] == "#c62828" - assert np.isclose(output.overlays[-1]["line_thickness"], 1.8) + assert np.isclose(output.overlays[-1]["stroke_width"], 1.8) assert np.isclose(rows["Arm A length"]["value"], 1.2) assert np.isclose(rows["Arm B length"]["value"], 0.6) assert np.isclose(rows["Angle"]["value"], 90.0) assert rows["Angle"]["unit"] == "deg" assert rows["Vertex x"]["unit"] == field.si_unit_xy - overridden_output, _ = node.process( + sanitized_output, _ = node.process( field, color="not-a-color", - line_thickness=0.7, + stroke_width=-0.7, x1=0.2, y1=0.5, xm=0.5, @@ -749,16 +753,15 @@ def test_angle_measure(): y2=0.2, label_dx=0.0, label_dy=0.0, - line_thickness_input=2.4, ) - assert overridden_output.overlays[-1]["color"] == "#ff0000" - assert np.isclose(overridden_output.overlays[-1]["line_thickness"], 2.4) + assert sanitized_output.overlays[-1]["color"] == "#ff9800" + assert np.isclose(sanitized_output.overlays[-1]["stroke_width"], 0.35) image = np.zeros((50, 100, 3), dtype=np.uint8) image_output, image_table = node.process( image, - color="#ff0000", - line_thickness=1.25, + color="#ff9800", + stroke_width=1.25, x1=0.25, y1=0.5, xm=0.5,