rework web server so multiple clients can be server at a time

This commit is contained in:
matei jordache
2026-03-27 16:18:22 -07:00
parent 1eda4030d1
commit 558046e7aa
33 changed files with 1042 additions and 551 deletions

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_warning
from backend.data_types import (
COLORMAPS,
DataField,
@@ -120,7 +121,4 @@ class Annotations:
return (ImageData(annotated, metadata={"annotation_context": context}),)
def _send_warning(self, message: str):
fn = Annotations._broadcast_warning_fn
nid = Annotations._current_node_id
if fn and nid:
fn(nid, message)
emit_warning(message)

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_overlay
from backend.data_types import DataField, datafield_to_uint8, encode_preview
@@ -61,20 +62,16 @@ class CropResizeField:
x2 = float(np.clip(x2, 0.0, 1.0))
y2 = float(np.clip(y2, 0.0, 1.0))
if CropResizeField._broadcast_overlay_fn is not None:
CropResizeField._broadcast_overlay_fn(
CropResizeField._current_node_id,
{
"kind": "crop_box",
"image": encode_preview(datafield_to_uint8(field, field.colormap)),
"x1": x1,
"y1": y1,
"x2": x2,
"y2": y2,
"a_locked": corner_a is not None,
"b_locked": corner_b is not None,
},
)
emit_overlay({
"kind": "crop_box",
"image": encode_preview(datafield_to_uint8(field, field.colormap)),
"x1": x1,
"y1": y1,
"x2": x2,
"y2": y2,
"a_locked": corner_a is not None,
"b_locked": corner_b is not None,
})
left = min(x1, x2)
right = max(x1, x2)

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_overlay
from backend.data_types import DataField, LineData, datafield_to_uint8, encode_preview
from backend.nodes.helpers import _extend_to_edges
@@ -73,19 +74,14 @@ class CrossSection:
profile = map_coordinates(field.data, [coords_y, coords_x], order=3, mode="nearest")
if CrossSection._broadcast_overlay_fn is not None:
image_uri = encode_preview(datafield_to_uint8(field, field.colormap))
CrossSection._broadcast_overlay_fn(
CrossSection._current_node_id,
{
"image": image_uri,
"x1": marker_x1, "y1": marker_y1,
"x2": marker_x2, "y2": marker_y2,
"a_locked": marker_pair is not None,
"b_locked": marker_pair is not None,
},
)
image_uri = encode_preview(datafield_to_uint8(field, field.colormap))
emit_overlay({
"image": image_uri,
"x1": marker_x1, "y1": marker_y1,
"x2": marker_x2, "y2": marker_y2,
"a_locked": marker_pair is not None,
"b_locked": marker_pair is not None,
})
dx_real = (x2 - x1) * field.xreal
dy_real = (y2 - y1) * field.yreal

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_overlay
from backend.data_types import DataField, LineData, MeasureTable, encode_preview, render_datafield_preview
@@ -87,22 +88,18 @@ class Cursors:
xa, ya = float(x[idx_a]), float(y[idx_a])
xb, yb = float(x[idx_b]), float(y[idx_b])
if Cursors._broadcast_overlay_fn is not None:
Cursors._broadcast_overlay_fn(
Cursors._current_node_id,
{
"kind": "line_plot",
"section_title": "Cursors",
"line": y.tolist(),
"x_axis": x.tolist(),
"x1": x1,
"x2": x2,
"y1": float(y1),
"y2": float(y2),
"a_locked": locked,
"b_locked": locked,
},
)
emit_overlay({
"kind": "line_plot",
"section_title": "Cursors",
"line": y.tolist(),
"x_axis": x.tolist(),
"x1": x1,
"x2": x2,
"y1": float(y1),
"y2": float(y2),
"a_locked": locked,
"b_locked": locked,
})
table = MeasureTable([
{"quantity": "A x", "value": xa, "unit": x_unit},
@@ -143,21 +140,17 @@ class Cursors:
bx = float(field.xoff + x2 * field.xreal)
by = float(field.yoff + y2 * field.yreal)
if Cursors._broadcast_overlay_fn is not None:
Cursors._broadcast_overlay_fn(
Cursors._current_node_id,
{
"kind": "cursor_points",
"section_title": "Cursors",
"image": encode_preview(render_datafield_preview(field, field.colormap)),
"x1": x1,
"y1": y1,
"x2": x2,
"y2": y2,
"a_locked": locked,
"b_locked": locked,
},
)
emit_overlay({
"kind": "cursor_points",
"section_title": "Cursors",
"image": encode_preview(render_datafield_preview(field, field.colormap)),
"x1": x1,
"y1": y1,
"x2": x2,
"y2": y2,
"a_locked": locked,
"b_locked": locked,
})
table = MeasureTable([
{"quantity": "A x", "value": ax, "unit": field.si_unit_xy},

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_overlay
from backend.data_types import DataField, datafield_to_uint8, encode_preview
from backend.nodes.helpers import _parse_mask_strokes, _rasterize_mask
@@ -40,17 +41,13 @@ class DrawMask:
if invert:
mask = np.where(mask > 127, np.uint8(0), np.uint8(255))
if DrawMask._broadcast_overlay_fn is not None:
DrawMask._broadcast_overlay_fn(
DrawMask._current_node_id,
{
"kind": "mask_paint",
"section_title": "Mask",
"image": encode_preview(datafield_to_uint8(field, "gray")),
"image_width": field.xres,
"image_height": field.yres,
"invert": bool(invert),
},
)
emit_overlay({
"kind": "mask_paint",
"section_title": "Mask",
"image": encode_preview(datafield_to_uint8(field, "gray")),
"image_width": field.xres,
"image_height": field.yres,
"invert": bool(invert),
})
return (mask,)

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_overlay
from backend.data_types import DataField, MeasureTable
@@ -72,22 +73,18 @@ class Histogram:
yb = float(counts[idx_b]) if len(counts) else 0.0
count_unit = "count" if y_scale == "linear" else "log10(1+count)"
if Histogram._broadcast_overlay_fn is not None:
Histogram._broadcast_overlay_fn(
Histogram._current_node_id,
{
"kind": "line_plot",
"section_title": "Histogram",
"line": counts.tolist(),
"x_axis": bin_centers.astype(np.float64).tolist(),
"x1": float(np.clip(x1, 0.0, 1.0)),
"x2": float(np.clip(x2, 0.0, 1.0)),
"y1": float(y1),
"y2": float(y2),
"a_locked": False,
"b_locked": False,
},
)
emit_overlay({
"kind": "line_plot",
"section_title": "Histogram",
"line": counts.tolist(),
"x_axis": bin_centers.astype(np.float64).tolist(),
"x1": float(np.clip(x1, 0.0, 1.0)),
"x2": float(np.clip(x2, 0.0, 1.0)),
"y1": float(y1),
"y2": float(y2),
"a_locked": False,
"b_locked": False,
})
table = MeasureTable([
{"quantity": "A position", "value": xa, "unit": field.si_unit_z},

View File

@@ -4,6 +4,7 @@ import numpy as np
from pathlib import Path
from backend.node_registry import register_node
from backend.execution_context import emit_warning
from backend.data_types import COLORMAPS, DataField, resolve_colormap_input
from backend.nodes.helpers import _resolve_path, _SPM_EXTENSIONS, _import_ibw_loader
@@ -66,10 +67,7 @@ class Image:
return fields
def _send_warning(self, message: str):
fn = Image._broadcast_warning_fn
nid = Image._current_node_id
if fn and nid:
fn(nid, message)
emit_warning(message)
@staticmethod
@lru_cache(maxsize=32)

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from backend.node_registry import register_node
from backend.execution_context import emit_overlay
from backend.data_types import (
DataField,
ImageData,
@@ -70,17 +71,13 @@ class Markup:
metadata=image_metadata(input),
)
if Markup._broadcast_overlay_fn is not None:
Markup._broadcast_overlay_fn(
Markup._current_node_id,
{
"kind": "markup",
"section_title": "Markup",
"image": encode_preview(preview_base),
"shape": str(shape),
"stroke_color": _normalize_markup_color(stroke_color),
"stroke_width": max(1, int(stroke_width)),
},
)
emit_overlay({
"kind": "markup",
"section_title": "Markup",
"image": encode_preview(preview_base),
"shape": str(shape),
"stroke_color": _normalize_markup_color(stroke_color),
"stroke_width": max(1, int(stroke_width)),
})
return (out,)

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_preview
from backend.data_types import DataField, encode_preview
from backend.nodes.helpers import _mask_overlay
@@ -53,10 +54,8 @@ class MaskCombine:
out = result.astype(np.uint8) * 255
if field is not None and MaskCombine._broadcast_fn is not None:
if field is not None:
overlay = _mask_overlay(field, out)
MaskCombine._broadcast_fn(
MaskCombine._current_node_id, encode_preview(overlay),
)
emit_preview(encode_preview(overlay))
return (out,)

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_preview
from backend.data_types import DataField, encode_preview
from backend.nodes.helpers import _mask_overlay
@@ -32,10 +33,8 @@ class MaskInvert:
def process(self, mask: np.ndarray, field: DataField | None = None) -> tuple:
out = np.where(mask > 127, np.uint8(0), np.uint8(255))
if field is not None and MaskInvert._broadcast_fn is not None:
if field is not None:
overlay = _mask_overlay(field, out)
MaskInvert._broadcast_fn(
MaskInvert._current_node_id, encode_preview(overlay),
)
emit_preview(encode_preview(overlay))
return (out,)

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_preview
from backend.data_types import DataField, encode_preview
from backend.nodes.helpers import _mask_overlay, _mask_structure
@@ -62,10 +63,8 @@ class MaskMorphology:
out = result.astype(np.uint8) * 255
if field is not None and MaskMorphology._broadcast_fn is not None:
if field is not None:
overlay = _mask_overlay(field, out)
MaskMorphology._broadcast_fn(
MaskMorphology._current_node_id, encode_preview(overlay),
)
emit_preview(encode_preview(overlay))
return (out,)

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_preview
from backend.data_types import (
COLORMAPS,
colormap_to_uint8,
@@ -68,7 +69,6 @@ class PreviewImage:
data_uri = encode_preview(arr_u8)
if PreviewImage._broadcast_fn is not None:
PreviewImage._broadcast_fn(PreviewImage._current_node_id, data_uri)
emit_preview(data_uri)
return ()

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from backend.node_registry import register_node
from backend.execution_context import emit_table
@register_node(display_name="Print Table")
@@ -22,6 +23,5 @@ class PrintTable:
_current_node_id: str = ""
def print_table(self, table: list) -> tuple:
if PrintTable._broadcast_table_fn is not None:
PrintTable._broadcast_table_fn(PrintTable._current_node_id, table)
emit_table(table)
return ()

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_warning
from backend.data_types import DataField
@@ -84,10 +85,7 @@ class RotateField:
return (result,)
def _send_warning(self, message: str):
fn = RotateField._broadcast_warning_fn
nid = RotateField._current_node_id
if fn and nid:
fn(nid, message)
emit_warning(message)
@staticmethod
def _rotated_extents(field: DataField, angle: float, expand_canvas: bool) -> tuple[float, float]:

View File

@@ -7,6 +7,7 @@ from pathlib import Path
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_warning
from backend.data_types import DataField, LineData, MeshModel, datafield_to_uint8, image_to_uint8
@@ -255,7 +256,4 @@ class Save:
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def _send_warning(self, message: str):
fn = Save._broadcast_warning_fn
nid = Save._current_node_id
if fn and nid:
fn(nid, message)
emit_warning(message)

View File

@@ -4,6 +4,7 @@ import numpy as np
from pathlib import Path
from backend.node_registry import register_node
from backend.execution_context import emit_warning
from backend.data_types import DataField, image_to_uint8
from backend.nodes.helpers import _MAX_SAVE_FIELDS
@@ -174,9 +175,6 @@ class SaveImage:
raise ValueError(f"Unsupported save layer type: {type(layer).__name__}")
def _send_warning(self, message: str):
fn = SaveImage._broadcast_warning_fn
nid = SaveImage._current_node_id
if fn and nid:
fn(nid, message)
emit_warning(message)
return ()

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_value
from backend.data_types import DataField, LineData, MeasureTable
from backend.nodes.helpers import (
LINE_OPS,
@@ -71,11 +72,9 @@ class Stats:
op_entry = ops[operation]
fn = op_entry[0] if isinstance(op_entry, tuple) else op_entry
result = fn(values)
if Stats._broadcast_value_fn is not None:
Stats._broadcast_value_fn(
Stats._current_node_id,
_scalar_payload(result, self._resolve_output_unit(input, source_type, resolved_column, operation)),
)
emit_value(
_scalar_payload(result, self._resolve_output_unit(input, source_type, resolved_column, operation)),
)
return (result,)
def _resolve_output_unit(self, input_value, source_type: str, column: str | None, operation: str) -> str:

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_preview
from backend.data_types import DataField, encode_preview
from backend.nodes.helpers import _mask_overlay
@@ -52,10 +53,7 @@ class ThresholdMask:
else:
mask = (data < t).astype(np.uint8) * 255
if ThresholdMask._broadcast_fn is not None:
overlay = _mask_overlay(field, mask)
ThresholdMask._broadcast_fn(
ThresholdMask._current_node_id, encode_preview(overlay),
)
overlay = _mask_overlay(field, mask)
emit_preview(encode_preview(overlay))
return (mask,)

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from backend.node_registry import register_node
from backend.execution_context import emit_value
from backend.data_types import MeasureTable
from backend.nodes.helpers import _measurement_entry, _measurement_value, _scalar_payload
@@ -38,6 +39,5 @@ class ValueDisplay:
unit = row.get("unit", "") if isinstance(row.get("unit"), str) else ""
else:
numeric = float(value)
if ValueDisplay._broadcast_value_fn is not None:
ValueDisplay._broadcast_value_fn(ValueDisplay._current_node_id, _scalar_payload(numeric, unit))
emit_value(_scalar_payload(numeric, unit))
return (numeric,)

View File

@@ -3,6 +3,7 @@ import base64
import io
import numpy as np
from backend.node_registry import register_node
from backend.execution_context import emit_mesh
from backend.data_types import (
COLORMAPS,
DataField,
@@ -211,8 +212,7 @@ class View3D:
"y_range": [float(field.yoff), float(field.yoff + field.yreal)],
}
if View3D._broadcast_mesh_fn is not None:
View3D._broadcast_mesh_fn(View3D._current_node_id, mesh_data)
emit_mesh(mesh_data)
annotation_context = _annotation_context_from_field(color_field, resolved_colormap)
annotation_context["xreal"] = float(field.xreal)