angle node working nicely
This commit is contained in:
@@ -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)))
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user