From 35f426dbb6d69cbc4e08ad575dda5d6e70c54305 Mon Sep 17 00:00:00 2001 From: norle Date: Thu, 14 May 2026 15:07:20 +0200 Subject: [PATCH 01/27] geometry --- pylabrobot/resources/__init__.py | 1 + pylabrobot/resources/geometry.py | 139 +++++++++++++++++++++++++ pylabrobot/resources/geometry_tests.py | 40 +++++++ 3 files changed, 180 insertions(+) create mode 100644 pylabrobot/resources/geometry.py create mode 100644 pylabrobot/resources/geometry_tests.py diff --git a/pylabrobot/resources/__init__.py b/pylabrobot/resources/__init__.py index 4652e9a859b..e25136c3d06 100644 --- a/pylabrobot/resources/__init__.py +++ b/pylabrobot/resources/__init__.py @@ -25,6 +25,7 @@ from .diy import * from .eppendorf import * from .errors import ResourceNotFoundError +from .geometry import generate_geometry_catalog, save_geometry_catalog from .greiner import * from .hamilton import * from .itemized_resource import ItemizedResource diff --git a/pylabrobot/resources/geometry.py b/pylabrobot/resources/geometry.py new file mode 100644 index 00000000000..741dddea0ed --- /dev/null +++ b/pylabrobot/resources/geometry.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +from typing import Any, Dict, List, Optional, Union + +from pylabrobot.resources.container import Container +from pylabrobot.resources.coordinate import Coordinate +from pylabrobot.resources.resource import Resource +from pylabrobot.resources.rotation import Rotation +from pylabrobot.resources.tip_rack import TipSpot +from pylabrobot.resources.well import Well + + +def generate_geometry_catalog(root: Resource) -> Dict[str, Any]: + """Generate a renderer-oriented geometry catalog for a resource tree. + + The catalog is intentionally separate from the package data: call this on a deck + or labware resource after building a layout, then write the returned dict to JSON + if a simulator needs it. + """ + + prototypes: Dict[str, Dict[str, Any]] = {} + instances: Dict[str, Dict[str, Any]] = {} + + for resource in _walk_resources(root): + prototype = _resource_geometry_prototype(resource) + prototype_id = _geometry_prototype_id(prototype) + prototypes.setdefault(prototype_id, prototype) + + instance: Dict[str, Any] = { + "prototype": prototype_id, + "parent": resource.parent.name if resource.parent is not None else None, + "pose": _coordinate_or_none(resource, x="l", y="f", z="b"), + "rotation": _rotation_values(resource.get_absolute_rotation()), + } + if len(resource.children) > 0: + instance["children"] = [child.name for child in resource.children] + instances[resource.name] = instance + + return { + "root": root.name, + "prototypes": prototypes, + "instances": instances, + } + + +def save_geometry_catalog( + root: Resource, + path: Union[str, Path], + indent: Optional[int] = 2, +) -> None: + """Generate a geometry catalog and write it to a JSON file.""" + + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(generate_geometry_catalog(root), indent=indent), encoding="utf-8") + + +def _walk_resources(root: Resource) -> List[Resource]: + resources = [root] + for child in root.children: + resources.extend(_walk_resources(child)) + return resources + + +def _resource_geometry_prototype(resource: Resource) -> Dict[str, Any]: + prototype: Dict[str, Any] = { + "type": resource.__class__.__name__, + "category": resource.category, + "size": _coordinate_values( + Coordinate(resource.get_size_x(), resource.get_size_y(), resource.get_size_z()) + ), + "geometry": _resource_geometry_hints(resource), + } + if resource.model is not None: + prototype["model"] = resource.model + return prototype + + +def _resource_geometry_hints(resource: Resource) -> Dict[str, Any]: + if isinstance(resource, Well): + geometry: Dict[str, Any] = { + "shape": "well", + "cross_section": resource.cross_section_type.value, + "bottom": resource.bottom_type.value, + } + if resource._material_z_thickness is not None: + geometry["material_z_thickness"] = resource._material_z_thickness + if len(resource.no_go_zones) > 0: + geometry["no_go_zones"] = _no_go_zones_to_values(resource.no_go_zones) + return geometry + + if isinstance(resource, TipSpot): + return {"shape": "tip_spot"} + + if isinstance(resource, Container): + geometry = {"shape": "container"} + if resource._material_z_thickness is not None: + geometry["material_z_thickness"] = resource._material_z_thickness + if len(resource.no_go_zones) > 0: + geometry["no_go_zones"] = _no_go_zones_to_values(resource.no_go_zones) + return geometry + + if resource.category == "deck": + return {"shape": "deck"} + + return {"shape": "box"} + + +def _geometry_prototype_id(prototype: Dict[str, Any]) -> str: + payload = json.dumps(prototype, sort_keys=True, separators=(",", ":")) + digest = hashlib.sha1(payload.encode("utf-8")).hexdigest()[:10] + return f"{prototype['type']}_{digest}" + + +def _coordinate_or_none(resource: Resource, x: str, y: str, z: str) -> Optional[List[float]]: + try: + return _coordinate_values(resource.get_absolute_location(x=x, y=y, z=z)) + except Exception: + return None + + +def _coordinate_values(coordinate: Coordinate) -> List[float]: + return [coordinate.x, coordinate.y, coordinate.z] + + +def _rotation_values(rotation: Rotation) -> List[float]: + return [rotation.x, rotation.y, rotation.z] + + +def _no_go_zones_to_values( + no_go_zones: List[tuple[Coordinate, Coordinate]], +) -> List[List[List[float]]]: + return [ + [_coordinate_values(front_left_bottom), _coordinate_values(back_right_top)] + for front_left_bottom, back_right_top in no_go_zones + ] diff --git a/pylabrobot/resources/geometry_tests.py b/pylabrobot/resources/geometry_tests.py new file mode 100644 index 00000000000..c5d59917bfc --- /dev/null +++ b/pylabrobot/resources/geometry_tests.py @@ -0,0 +1,40 @@ +import json +import unittest + +from pylabrobot.resources import ( + Cor_96_wellplate_360ul_Fb, + generate_geometry_catalog, + hamilton_96_tiprack_1000uL_filter, +) +from pylabrobot.resources.hamilton import STARLetDeck + + +class GeometryCatalogTests(unittest.TestCase): + def setUp(self): + self.deck = STARLetDeck() + self.tip_rack = hamilton_96_tiprack_1000uL_filter(name="tip_rack") + self.plate = Cor_96_wellplate_360ul_Fb(name="plate") + self.deck.assign_child_resource(self.tip_rack, rails=3) + self.deck.assign_child_resource(self.plate, rails=9) + + def test_generate_geometry_catalog_for_deck(self): + catalog = generate_geometry_catalog(self.deck) + json.dumps(catalog) + + self.assertEqual(catalog["root"], "deck") + self.assertIn("plate", catalog["instances"]) + self.assertIn("plate_well_A1", catalog["instances"]) + + well_instance = catalog["instances"]["plate_well_A1"] + well_prototype = catalog["prototypes"][well_instance["prototype"]] + self.assertEqual(well_prototype["geometry"]["shape"], "well") + self.assertEqual(well_prototype["geometry"]["cross_section"], "circle") + self.assertEqual(len(well_instance["pose"]), 3) + + def test_generate_geometry_catalog_for_single_labware(self): + catalog = generate_geometry_catalog(self.plate) + + self.assertEqual(catalog["root"], "plate") + self.assertIn("plate", catalog["instances"]) + self.assertIn("plate_well_A1", catalog["instances"]) + self.assertLess(len(catalog["prototypes"]), len(catalog["instances"])) From ea1972e71e27d97b6ba83b284f0d2f3b107db401 Mon Sep 17 00:00:00 2001 From: norle Date: Fri, 15 May 2026 11:19:55 +0200 Subject: [PATCH 02/27] remade resource catalog with 3d models --- .github/workflows/docs.yml | 4 +- .gitignore | 1 + Makefile | 11 +- docs/_exts/pylabrobot_labware_catalog.py | 278 +++++++++ docs/_static/plr_geometry_viewer.js | 677 ++++++++++++++++++++++ docs/_static/plr_labware_catalog.css | 268 +++++++++ docs/_static/plr_labware_catalog.js | 274 +++++++++ docs/_templates/autosummary/attribute.rst | 7 + docs/_templates/autosummary/data.rst | 7 + docs/_templates/autosummary/exception.rst | 7 + docs/_templates/autosummary/function.rst | 7 + docs/_templates/autosummary/method.rst | 7 + docs/_templates/autosummary/property.rst | 7 + docs/api/pylabrobot.resources.rst | 2 + docs/conf.py | 8 + docs/resources/catalog.md | 8 + docs/resources/index.md | 3 + pylabrobot/resources/geometry.py | 9 +- pylabrobot/resources/geometry_tests.py | 10 +- 19 files changed, 1584 insertions(+), 11 deletions(-) create mode 100644 docs/_exts/pylabrobot_labware_catalog.py create mode 100644 docs/_static/plr_geometry_viewer.js create mode 100644 docs/_static/plr_labware_catalog.css create mode 100644 docs/_static/plr_labware_catalog.js create mode 100644 docs/_templates/autosummary/attribute.rst create mode 100644 docs/_templates/autosummary/data.rst create mode 100644 docs/_templates/autosummary/exception.rst create mode 100644 docs/_templates/autosummary/function.rst create mode 100644 docs/_templates/autosummary/method.rst create mode 100644 docs/_templates/autosummary/property.rst create mode 100644 docs/resources/catalog.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 90c862f8e22..78bd42f8146 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -35,7 +35,7 @@ jobs: - name: Check documentation run: | - rm -rf docs/build docs/_autosummary + make clean-docs make docs-check deploy_docs: @@ -71,7 +71,7 @@ jobs: env: DOCS_VERSION: ${{ steps.version.outputs.slug }} run: | - rm -rf docs/build docs/_autosummary + make clean-docs make docs - name: Clean build artifacts diff --git a/.gitignore b/.gitignore index 01db94c3ec0..2e6fc786294 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ pyhamilton/LAY-BACKUP .ipynb_checkpoints *.egg-info *.log +test_logs/ build/lib myenv diff --git a/Makefile b/Makefile index 2e23b332765..852a91e7186 100644 --- a/Makefile +++ b/Makefile @@ -5,25 +5,28 @@ endif .PHONY: docs docs-fast docs-check docs-linkcheck clean-docs lint test +DOCS_GENERATED_DIRS = docs/_autosummary docs/api/_autosummary docs/jupyter_execute docs/user_guide/jupyter_execute + docs: + rm -rf $(DOCS_GENERATED_DIRS) sphinx-build -b html docs docs/build/ -j 16 -W docs-fast: echo "building docs without api for speed" + rm -rf $(DOCS_GENERATED_DIRS) sphinx-build -t no-api -b html docs docs/build/ -j 16 -W docs-check: + rm -rf $(DOCS_GENERATED_DIRS) sphinx-build -b dummy docs docs/build/ -j 16 -W docs-linkcheck: + rm -rf $(DOCS_GENERATED_DIRS) sphinx-build -b linkcheck docs docs/build/linkcheck -j 16 -W clean-docs: rm -rf docs/build - rm -rf docs/_autosummary - rm -rf docs/api/_autosummary - rm -rf docs/jupyter_execute - rm -rf docs/user_guide/jupyter_execute + rm -rf $(DOCS_GENERATED_DIRS) TRACKED_PY = $(shell git ls-files 'pylabrobot/*.py' 'pylabrobot/*.ipynb') diff --git a/docs/_exts/pylabrobot_labware_catalog.py b/docs/_exts/pylabrobot_labware_catalog.py new file mode 100644 index 00000000000..9864ee885a1 --- /dev/null +++ b/docs/_exts/pylabrobot_labware_catalog.py @@ -0,0 +1,278 @@ +from __future__ import annotations + +import importlib +import inspect +import json +import re +from html import escape +from pathlib import Path +from typing import Any, Callable, Dict, Iterable, List, Optional, Set + +from pylabrobot.resources import generate_geometry_catalog + + +LIBRARY_RELATIVE_ROOT = Path("resources") / "library" +GEOMETRY_INDEX_FILENAME = "labware_geometry_index.json" +RESOURCE_ALIASES = { + "Azenta4titudeFrameStar_96_wellplate_skirted": "Azenta4titudeFrameStar_96_wellplate_200ul_Vb", + "BioRad_384_DWP_50uL_Vb": "BioRad_384_wellplate_50uL_Vb", + "CellTreat_6_DWP_16300ul_Fb": "CellTreat_6_wellplate_16300ul_Fb", + "Cor_12_wellplate_6900ul_Fb": "Cor_Cos_12_wellplate_6900ul_Fb", + "Cor_24_wellplate_3470ul_Fb": "Cor_Cos_24_wellplate_3470ul_Fb", + "Cor_48_wellplate_1620ul_Fb": "Cor_Cos_48_wellplate_1620ul_Fb", + "Cos_96_wellplate_2mL_Vb": "Cor_96_wellplate_2mL_Vb", + "Cos_6_wellplate_16800ul_Fb": "Cor_Cos_6_wellplate_16800ul_Fb", + "Hamilton_mfx_plateholder_DWP_metal_tapped": "hamilton_mfx_plateholder_DWP_metal_tapped", + "PLT_CAR_P3AC": "PLT_CAR_P3AC_A00", +} + + +def _library_doc_paths(srcdir: str) -> List[Path]: + library_root = Path(srcdir) / LIBRARY_RELATIVE_ROOT + return sorted(path for path in library_root.rglob("*.md") if path.is_file()) + + +def _markdown_links_to_html(text: str) -> str: + escaped = escape(text, quote=False) + return re.sub( + r"\[([^\]]+)\]\(([^)]+)\)", + lambda match: f'{match.group(1)}', + escaped, + ) + + +def _description_to_html(description: str, definition_name: str) -> str: + parts = [part.strip() for part in re.split(r"", description) if part.strip()] + if parts and parts[0].strip("'` ") == definition_name: + parts = parts[1:] + return "
".join(_markdown_links_to_html(part) for part in parts) + + +def _page_title(markdown: str, fallback: str) -> str: + for line in markdown.splitlines(): + match = re.match(r"^#\s+(.+?)\s*$", line) + if match: + return match.group(1) + return fallback + + +def _image_path_from_cell(cell: str, doc_relative_path: Path) -> Optional[str]: + match = re.search(r"!\[[^\]]*]\(([^)]+)\)", cell) + if match is None: + return None + image_path = match.group(1).strip() + if image_path.startswith(("http://", "https://", "/")): + return image_path + resolved = doc_relative_path.parent / image_path + if "img" in resolved.parts: + img_index = resolved.parts.index("img") + return ("_static/" + "/".join(resolved.parts[img_index + 1:])).replace("//", "/") + return resolved.as_posix() + + +def _extract_labware_entries_from_markdown( + markdown: str, + doc_relative_path: Path, +) -> List[Dict[str, Any]]: + entries: List[Dict[str, Any]] = [] + vendor = _page_title(markdown, doc_relative_path.stem.replace("_", " ").title()) + current_section = "" + + for line in markdown.splitlines(): + stripped = line.strip() + heading_match = re.match(r"^(#{2,4})\s+(.+?)\s*$", stripped) + if heading_match: + current_section = heading_match.group(2) + continue + + if not stripped.startswith("|"): + continue + + cells = [cell.strip() for cell in stripped.split("|")[1:-1]] + if len(cells) < 3: + continue + + matches = re.findall(r"`([A-Za-z][A-Za-z0-9_]*)`", cells[-1]) + if len(matches) == 0: + continue + + image_path = _image_path_from_cell(cells[1], doc_relative_path) + for definition_name in matches: + entries.append({ + "definition": definition_name, + "vendor": vendor, + "section": current_section, + "description_html": _description_to_html(cells[0], definition_name), + "image": image_path, + "page": doc_relative_path.with_suffix(".html").as_posix(), + }) + + return entries + + +def _iter_unique_resource_names(srcdir: str) -> Iterable[str]: + seen: Set[str] = set() + + for doc_path in _library_doc_paths(srcdir): + doc_relative_path = doc_path.relative_to(srcdir) + entries = _extract_labware_entries_from_markdown( + doc_path.read_text(encoding="utf-8"), + doc_relative_path, + ) + for entry in entries: + name = entry["definition"] + if name not in seen: + seen.add(name) + yield name + + +def _catalog_entries(srcdir: str) -> List[Dict[str, Any]]: + entries: List[Dict[str, Any]] = [] + seen: Set[str] = set() + + for doc_path in _library_doc_paths(srcdir): + doc_relative_path = doc_path.relative_to(srcdir) + for entry in _extract_labware_entries_from_markdown( + doc_path.read_text(encoding="utf-8"), + doc_relative_path, + ): + definition_name = entry["definition"] + if definition_name in seen: + continue + seen.add(definition_name) + entries.append(entry) + + return entries + + +def _resource_factory_registry() -> Dict[str, Callable[..., Any]]: + resources_module = importlib.import_module("pylabrobot.resources") + registry: Dict[str, Callable[..., Any]] = {} + + for name in dir(resources_module): + if name.startswith("_"): + continue + value = getattr(resources_module, name) + if callable(value): + registry[name] = value + + resources_root = Path(resources_module.__file__).resolve().parent + for module_path in resources_root.rglob("*.py"): + if module_path.name == "__init__.py" or module_path.name.endswith("_tests.py"): + continue + relative_path = module_path.relative_to(resources_root).with_suffix("") + if "falcon" in relative_path.parts: + continue + module_name = "pylabrobot.resources." + ".".join(relative_path.parts) + try: + module = importlib.import_module(module_name) + except Exception: + continue + for name, value in vars(module).items(): + if name.startswith("_") or not callable(value): + continue + registry.setdefault(name, value) + + return registry + + +def _resolve_definition_callable( + definition_name: str, + registry: Dict[str, Callable[..., Any]], +) -> tuple[Optional[str], Optional[Callable[..., Any]]]: + candidate_name = RESOURCE_ALIASES.get(definition_name, definition_name) + if candidate_name in registry: + return candidate_name, registry[candidate_name] + + lowered = candidate_name.lower() + matches = [name for name in registry if name.lower() == lowered] + if len(matches) == 1: + match = matches[0] + return match, registry[match] + + return None, None + + +def _build_resource_definition( + definition_name: str, + registry: Dict[str, Callable[..., Any]], +): + resolved_name, definition = _resolve_definition_callable(definition_name, registry) + if definition is None or resolved_name is None: + return None + + signature = inspect.signature(definition) + kwargs: Dict[str, Any] = {} + args: List[Any] = [] + + parameters = list(signature.parameters.values()) + if any(parameter.name == "name" for parameter in parameters): + kwargs["name"] = definition_name + if any(parameter.name == "modules" for parameter in parameters): + kwargs["modules"] = {} + elif len(parameters) > 0 and "name" not in signature.parameters: + first = parameters[0] + if first.kind in ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + ): + args.append(definition_name) + + try: + return definition(*args, **kwargs) + except Exception: + return None + + +def build_labware_geometry_index(srcdir: str) -> Dict[str, Any]: + resources: Dict[str, Any] = {} + entries = _catalog_entries(srcdir) + registry = _resource_factory_registry() + + for definition_name in [entry["definition"] for entry in entries]: + resource = _build_resource_definition(definition_name, registry) + if resource is None: + continue + + try: + resources[definition_name] = generate_geometry_catalog(resource) + except Exception: + continue + + for entry in entries: + entry["has_geometry"] = entry["definition"] in resources + + return { + "items": entries, + "resources": resources, + } + + +def _write_geometry_index(app) -> None: + if app.builder.format != "html": + return + + geometry_index = build_labware_geometry_index(app.srcdir) + target_dir = Path(app.outdir) / "_static" + target_dir.mkdir(parents=True, exist_ok=True) + target_path = target_dir / GEOMETRY_INDEX_FILENAME + target_path.write_text( + json.dumps(geometry_index, separators=(",", ":"), ensure_ascii=True), + encoding="utf-8", + ) + + +def _build_finished(app, exception: Optional[Exception]) -> None: + if exception is not None: + return + _write_geometry_index(app) + + +def setup(app): + app.connect("build-finished", _build_finished) + + return { + "version": "1.0", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/_static/plr_geometry_viewer.js b/docs/_static/plr_geometry_viewer.js new file mode 100644 index 00000000000..acd180a7f2f --- /dev/null +++ b/docs/_static/plr_geometry_viewer.js @@ -0,0 +1,677 @@ +(function () { + "use strict"; + + function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); + } + + function colorForPrototype(prototype) { + const geometry = prototype.geometry || {}; + const type = prototype.type || ""; + const category = prototype.category || ""; + + if (geometry.shape === "deck") return "#dbe7f1"; + if (type.includes("Carrier")) return "#9eb3c4"; + if (type.includes("Plate")) return "#4a5f73"; + if (type.includes("TipRack")) return "#8f6e34"; + if (type.includes("TubeRack")) return "#5f7c55"; + if (type.includes("Well")) return "#66b8c4"; + if (type.includes("TipSpot")) return "#d8c48a"; + if (category === "trash") return "#9f7c7c"; + return "#8ea3b6"; + } + + function hexToRgb(hex) { + const normalized = hex.replace("#", ""); + const value = parseInt(normalized, 16); + return { + r: (value >> 16) & 255, + g: (value >> 8) & 255, + b: value & 255, + }; + } + + function shadeColor(hex, factor) { + const rgb = hexToRgb(hex); + const scale = clamp(factor, 0.45, 1.2); + return `rgb(${Math.round(rgb.r * scale)}, ${Math.round(rgb.g * scale)}, ${Math.round(rgb.b * scale)})`; + } + + function multiplyMatrix(a, b) { + return [ + [ + a[0][0] * b[0][0] + a[0][1] * b[1][0] + a[0][2] * b[2][0], + a[0][0] * b[0][1] + a[0][1] * b[1][1] + a[0][2] * b[2][1], + a[0][0] * b[0][2] + a[0][1] * b[1][2] + a[0][2] * b[2][2], + ], + [ + a[1][0] * b[0][0] + a[1][1] * b[1][0] + a[1][2] * b[2][0], + a[1][0] * b[0][1] + a[1][1] * b[1][1] + a[1][2] * b[2][1], + a[1][0] * b[0][2] + a[1][1] * b[1][2] + a[1][2] * b[2][2], + ], + [ + a[2][0] * b[0][0] + a[2][1] * b[1][0] + a[2][2] * b[2][0], + a[2][0] * b[0][1] + a[2][1] * b[1][1] + a[2][2] * b[2][1], + a[2][0] * b[0][2] + a[2][1] * b[1][2] + a[2][2] * b[2][2], + ], + ]; + } + + function rotationMatrix(rotation) { + const [rx, ry, rz] = (rotation || [0, 0, 0]).map((degrees) => (degrees * Math.PI) / 180); + + const cx = Math.cos(rx); + const sx = Math.sin(rx); + const cy = Math.cos(ry); + const sy = Math.sin(ry); + const cz = Math.cos(rz); + const sz = Math.sin(rz); + + const mx = [ + [1, 0, 0], + [0, cx, -sx], + [0, sx, cx], + ]; + const my = [ + [cy, 0, sy], + [0, 1, 0], + [-sy, 0, cy], + ]; + const mz = [ + [cz, -sz, 0], + [sz, cz, 0], + [0, 0, 1], + ]; + + return multiplyMatrix(mz, multiplyMatrix(my, mx)); + } + + function applyMatrix(point, matrix) { + return { + x: point.x * matrix[0][0] + point.y * matrix[0][1] + point.z * matrix[0][2], + y: point.x * matrix[1][0] + point.y * matrix[1][1] + point.z * matrix[1][2], + z: point.x * matrix[2][0] + point.y * matrix[2][1] + point.z * matrix[2][2], + }; + } + + function translatePoint(point, offset) { + return { + x: point.x + offset[0], + y: point.y + offset[1], + z: point.z + offset[2], + }; + } + + function createBoxGeometry(size) { + const sx = size[0]; + const sy = size[1]; + const sz = size[2]; + + const vertices = [ + { x: 0, y: 0, z: 0 }, + { x: sx, y: 0, z: 0 }, + { x: sx, y: sy, z: 0 }, + { x: 0, y: sy, z: 0 }, + { x: 0, y: 0, z: sz }, + { x: sx, y: 0, z: sz }, + { x: sx, y: sy, z: sz }, + { x: 0, y: sy, z: sz }, + ]; + + const faces = [ + [0, 1, 2, 3], + [4, 5, 6, 7], + [0, 1, 5, 4], + [1, 2, 6, 5], + [2, 3, 7, 6], + [3, 0, 4, 7], + ]; + + return { vertices, faces }; + } + + function createOffsetBoxGeometry(box) { + const geometry = createBoxGeometry([box.sx, box.sy, box.sz]); + geometry.vertices = geometry.vertices.map((vertex) => ({ + x: vertex.x + box.x, + y: vertex.y + box.y, + z: vertex.z + box.z, + })); + return geometry; + } + + function mergeGeometries(geometries) { + const merged = { vertices: [], faces: [] }; + geometries.forEach((geometry) => { + const offset = merged.vertices.length; + geometry.vertices.forEach((vertex) => merged.vertices.push(vertex)); + geometry.faces.forEach((face) => { + merged.faces.push(face.map((index) => index + offset)); + }); + }); + return merged; + } + + function createTrayGeometry(size) { + const sx = size[0]; + const sy = size[1]; + const sz = size[2]; + const wallThickness = clamp(Math.min(sx, sy) * 0.055, 2.2, 6.5); + const baseThickness = clamp(sz * 0.18, 1.8, Math.max(2.2, sz * 0.28)); + + return mergeGeometries([ + createOffsetBoxGeometry({ x: 0, y: 0, z: 0, sx, sy, sz: baseThickness }), + createOffsetBoxGeometry({ x: 0, y: 0, z: baseThickness, sx, sy: wallThickness, sz: sz - baseThickness }), + createOffsetBoxGeometry({ + x: 0, + y: sy - wallThickness, + z: baseThickness, + sx, + sy: wallThickness, + sz: sz - baseThickness, + }), + createOffsetBoxGeometry({ + x: 0, + y: wallThickness, + z: baseThickness, + sx: wallThickness, + sy: sy - wallThickness * 2, + sz: sz - baseThickness, + }), + createOffsetBoxGeometry({ + x: sx - wallThickness, + y: wallThickness, + z: baseThickness, + sx: wallThickness, + sy: sy - wallThickness * 2, + sz: sz - baseThickness, + }), + ]); + } + + function createCylinderGeometry(size, segments) { + const sx = size[0]; + const sy = size[1]; + const sz = size[2]; + const radiusX = sx / 2; + const radiusY = sy / 2; + const centerX = sx / 2; + const centerY = sy / 2; + const vertices = []; + const top = []; + const bottom = []; + const faces = []; + + for (let index = 0; index < segments; index += 1) { + const theta = (Math.PI * 2 * index) / segments; + const x = centerX + Math.cos(theta) * radiusX; + const y = centerY + Math.sin(theta) * radiusY; + bottom.push(vertices.length); + vertices.push({ x, y, z: 0 }); + top.push(vertices.length); + vertices.push({ x, y, z: sz }); + } + + faces.push(bottom.slice().reverse()); + faces.push(top.slice()); + for (let index = 0; index < segments; index += 1) { + const next = (index + 1) % segments; + faces.push([ + bottom[index], + bottom[next], + top[next], + top[index], + ]); + } + + return { vertices, faces }; + } + + function createShapeGeometry(prototype) { + const geometry = prototype.geometry || {}; + const size = displaySizeForPrototype(prototype); + const type = prototype.type || ""; + + if (geometry.shape === "well" && geometry.cross_section === "circle") { + return createCylinderGeometry(size, 18); + } + + if (geometry.shape === "tip_spot") { + return createCylinderGeometry(size, 12); + } + + if ( + type.includes("Plate") || + type.includes("TipRack") || + type.includes("TubeRack") + ) { + return createTrayGeometry(size); + } + + return createBoxGeometry(size); + } + + function displaySizeForPrototype(prototype) { + const size = (prototype.size || [10, 10, 10]).slice(); + if (size[2] > 0) { + return size; + } + + const type = prototype.type || ""; + const minPlanar = Math.max(1, Math.min(size[0], size[1])); + let fallbackHeight = Math.max(2, minPlanar * 0.1); + + if (type.includes("TipSpot")) { + fallbackHeight = Math.max(2, minPlanar * 0.3); + } else if (type.includes("Well")) { + fallbackHeight = Math.max(2, minPlanar * 0.35); + } else if (type.includes("Holder")) { + fallbackHeight = Math.max(3, minPlanar * 0.18); + } + + size[2] = fallbackHeight; + return size; + } + + function normalizeCatalog(catalog) { + if (!catalog || !catalog.prototypes || !catalog.instances) { + return []; + } + + const drawables = []; + Object.entries(catalog.instances).forEach(([name, instance]) => { + const prototype = catalog.prototypes[instance.prototype]; + if (!prototype || !instance.pose) { + return; + } + + const baseGeometry = createShapeGeometry(prototype); + const matrix = rotationMatrix(instance.rotation || [0, 0, 0]); + const translatedVertices = baseGeometry.vertices.map((vertex) => + translatePoint(applyMatrix(vertex, matrix), instance.pose), + ); + const outline = createOutlinePoints(prototype, matrix, instance.pose); + + drawables.push({ + name, + prototype, + color: colorForPrototype(prototype), + layer: drawableLayer(prototype), + alpha: drawableAlpha(prototype), + vertices: translatedVertices, + faces: baseGeometry.faces, + outline, + }); + }); + + return drawables; + } + + function createOutlinePoints(prototype, matrix, pose) { + const type = prototype.type || ""; + if ( + !type.includes("Plate") && + !type.includes("TipRack") && + !type.includes("TubeRack") + ) { + return null; + } + + const size = displaySizeForPrototype(prototype); + const z = size[2]; + const top = [ + { x: 0, y: 0, z }, + { x: size[0], y: 0, z }, + { x: size[0], y: size[1], z }, + { x: 0, y: size[1], z }, + ].map((corner) => translatePoint(applyMatrix(corner, matrix), pose)); + const bottom = [ + { x: 0, y: 0, z: 0 }, + { x: size[0], y: 0, z: 0 }, + { x: size[0], y: size[1], z: 0 }, + { x: 0, y: size[1], z: 0 }, + ].map((corner) => translatePoint(applyMatrix(corner, matrix), pose)); + + return { + top, + bottom, + verticals: [ + [bottom[0], top[0]], + [bottom[1], top[1]], + [bottom[2], top[2]], + [bottom[3], top[3]], + ], + }; + } + + function drawableLayer(prototype) { + const geometry = prototype.geometry || {}; + const type = prototype.type || ""; + + if (geometry.shape === "deck") return 0; + if (type.includes("Carrier") || type.includes("Holder")) return 1; + if (type.includes("Plate") || type.includes("TipRack") || type.includes("TubeRack")) return 2; + if (type.includes("Well") || type.includes("TipSpot")) return 4; + return 3; + } + + function drawableAlpha(prototype) { + const type = prototype.type || ""; + const geometry = prototype.geometry || {}; + + if (geometry.shape === "deck") return 0.38; + if (type.includes("Plate") || type.includes("TipRack") || type.includes("TubeRack")) return 0.84; + if (type.includes("Well") || type.includes("TipSpot")) return 0.68; + return 0.78; + } + + function computeBounds(drawables) { + const points = []; + drawables.forEach((drawable) => { + drawable.vertices.forEach((vertex) => points.push(vertex)); + }); + + if (points.length === 0) { + return { + min: { x: 0, y: 0, z: 0 }, + max: { x: 1, y: 1, z: 1 }, + center: { x: 0.5, y: 0.5, z: 0.5 }, + span: 1, + }; + } + + const min = { x: Infinity, y: Infinity, z: Infinity }; + const max = { x: -Infinity, y: -Infinity, z: -Infinity }; + points.forEach((point) => { + min.x = Math.min(min.x, point.x); + min.y = Math.min(min.y, point.y); + min.z = Math.min(min.z, point.z); + max.x = Math.max(max.x, point.x); + max.y = Math.max(max.y, point.y); + max.z = Math.max(max.z, point.z); + }); + + const center = { + x: (min.x + max.x) / 2, + y: (min.y + max.y) / 2, + z: (min.z + max.z) / 2, + }; + + const span = Math.max(max.x - min.x, max.y - min.y, max.z - min.z, 1); + return { min, max, center, span }; + } + + class CanvasCatalogViewer { + constructor(root) { + this.root = root; + this.canvas = document.createElement("canvas"); + this.canvas.className = "plr-geometry-canvas"; + this.root.innerHTML = ""; + this.root.appendChild(this.canvas); + this.context = this.canvas.getContext("2d"); + this.drawables = []; + this.bounds = computeBounds([]); + this.rotation = { yaw: -0.75, pitch: 0.55 }; + this.zoom = 1; + this.isDragging = false; + this.lastPointer = { x: 0, y: 0 }; + + this.handlePointerDown = this.handlePointerDown.bind(this); + this.handlePointerMove = this.handlePointerMove.bind(this); + this.handlePointerUp = this.handlePointerUp.bind(this); + this.handleWheel = this.handleWheel.bind(this); + this.handleResize = this.handleResize.bind(this); + this.resize = this.resize.bind(this); + + this.canvas.addEventListener("pointerdown", this.handlePointerDown); + window.addEventListener("pointermove", this.handlePointerMove); + window.addEventListener("pointerup", this.handlePointerUp); + this.canvas.addEventListener("wheel", this.handleWheel, { passive: false }); + window.addEventListener("resize", this.handleResize); + if (typeof ResizeObserver !== "undefined") { + this.resizeObserver = new ResizeObserver(this.handleResize); + this.resizeObserver.observe(this.root); + } + + this.handleResize(); + } + + destroy() { + this.canvas.removeEventListener("pointerdown", this.handlePointerDown); + window.removeEventListener("pointermove", this.handlePointerMove); + window.removeEventListener("pointerup", this.handlePointerUp); + this.canvas.removeEventListener("wheel", this.handleWheel); + window.removeEventListener("resize", this.handleResize); + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + } + } + + setCatalog(catalog) { + this.drawables = normalizeCatalog(catalog); + this.bounds = computeBounds(this.drawables); + this.zoom = 1; + this.render(); + } + + handlePointerDown(event) { + this.isDragging = true; + this.lastPointer = { x: event.clientX, y: event.clientY }; + this.canvas.setPointerCapture(event.pointerId); + } + + handlePointerMove(event) { + if (!this.isDragging) { + return; + } + + const deltaX = event.clientX - this.lastPointer.x; + const deltaY = event.clientY - this.lastPointer.y; + this.lastPointer = { x: event.clientX, y: event.clientY }; + this.rotation.yaw += deltaX * 0.01; + this.rotation.pitch = clamp(this.rotation.pitch + deltaY * 0.01, -1.4, 1.4); + this.render(); + } + + handlePointerUp(event) { + this.isDragging = false; + if (event.pointerId != null && this.canvas.hasPointerCapture(event.pointerId)) { + this.canvas.releasePointerCapture(event.pointerId); + } + } + + handleWheel(event) { + event.preventDefault(); + const zoomDelta = event.deltaY > 0 ? 0.92 : 1.08; + this.zoom = clamp(this.zoom * zoomDelta, 0.25, 6); + this.render(); + } + + handleResize() { + const width = Math.max(this.root.clientWidth, 200); + const height = Math.max(this.root.clientHeight, 200); + this.canvas.width = Math.floor(width * Math.min(window.devicePixelRatio || 1, 2)); + this.canvas.height = Math.floor(height * Math.min(window.devicePixelRatio || 1, 2)); + this.canvas.style.width = `${width}px`; + this.canvas.style.height = `${height}px`; + this.render(); + } + + resize() { + this.handleResize(); + } + + project(point) { + const centered = { + x: point.x - this.bounds.center.x, + y: point.y - this.bounds.center.y, + z: point.z - this.bounds.center.z, + }; + + const cosYaw = Math.cos(this.rotation.yaw); + const sinYaw = Math.sin(this.rotation.yaw); + const cosPitch = Math.cos(this.rotation.pitch); + const sinPitch = Math.sin(this.rotation.pitch); + + const yawed = { + x: centered.x * cosYaw - centered.y * sinYaw, + y: centered.x * sinYaw + centered.y * cosYaw, + z: centered.z, + }; + + const pitched = { + x: yawed.x, + y: yawed.y * cosPitch - yawed.z * sinPitch, + z: yawed.y * sinPitch + yawed.z * cosPitch, + }; + + const scaleBase = Math.min(this.canvas.width, this.canvas.height) / (this.bounds.span * 1.8); + const scale = scaleBase * this.zoom; + return { + x: this.canvas.width / 2 + pitched.x * scale, + y: this.canvas.height / 2 - pitched.y * scale, + depth: pitched.z, + }; + } + + drawBackground() { + const ctx = this.context; + const gradient = ctx.createLinearGradient(0, 0, 0, this.canvas.height); + gradient.addColorStop(0, "#f5f8fb"); + gradient.addColorStop(1, "#e6edf3"); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } + + drawGround() { + const ctx = this.context; + const span = this.bounds.span * 0.8; + const groundZ = this.bounds.min.z; + const gridSize = Math.max(4, Math.min(12, Math.round(this.bounds.span / 20))); + + ctx.lineWidth = 1; + ctx.strokeStyle = "rgba(126, 152, 176, 0.22)"; + + for (let index = -gridSize; index <= gridSize; index += 1) { + const ratio = index / gridSize; + const x = this.bounds.center.x + ratio * span; + const y = this.bounds.center.y + ratio * span; + + const xLineStart = this.project({ x, y: this.bounds.center.y - span, z: groundZ }); + const xLineEnd = this.project({ x, y: this.bounds.center.y + span, z: groundZ }); + ctx.beginPath(); + ctx.moveTo(xLineStart.x, xLineStart.y); + ctx.lineTo(xLineEnd.x, xLineEnd.y); + ctx.stroke(); + + const yLineStart = this.project({ x: this.bounds.center.x - span, y, z: groundZ }); + const yLineEnd = this.project({ x: this.bounds.center.x + span, y, z: groundZ }); + ctx.beginPath(); + ctx.moveTo(yLineStart.x, yLineStart.y); + ctx.lineTo(yLineEnd.x, yLineEnd.y); + ctx.stroke(); + } + } + + drawDrawables() { + const faces = []; + const outlines = []; + + this.drawables.forEach((drawable) => { + drawable.faces.forEach((face) => { + const projected = face.map((vertexIndex) => this.project(drawable.vertices[vertexIndex])); + const depth = + projected.reduce((sum, point) => sum + point.depth, 0) / Math.max(projected.length, 1); + faces.push({ + points: projected, + depth, + color: drawable.color, + layer: drawable.layer, + alpha: drawable.alpha, + }); + }); + + if (drawable.outline) { + outlines.push({ + top: drawable.outline.top.map((point) => this.project(point)), + bottom: drawable.outline.bottom.map((point) => this.project(point)), + verticals: drawable.outline.verticals.map((pair) => pair.map((point) => this.project(point))), + color: shadeColor(drawable.color, 0.42), + }); + } + }); + + faces.sort((left, right) => { + if (left.layer !== right.layer) { + return left.layer - right.layer; + } + return left.depth - right.depth; + }); + + const ctx = this.context; + faces.forEach((face) => { + const shade = + face.points.length >= 4 + ? shadeColor(face.color, 0.9 + (face.depth / (this.bounds.span || 1)) * 0.15) + : face.color; + + ctx.beginPath(); + ctx.moveTo(face.points[0].x, face.points[0].y); + for (let index = 1; index < face.points.length; index += 1) { + ctx.lineTo(face.points[index].x, face.points[index].y); + } + ctx.closePath(); + ctx.fillStyle = shade; + ctx.globalAlpha = face.alpha; + ctx.fill(); + ctx.globalAlpha = 1; + ctx.strokeStyle = "rgba(30, 53, 77, 0.28)"; + ctx.lineWidth = 1; + ctx.stroke(); + }); + + outlines.forEach((outline) => { + ctx.strokeStyle = outline.color; + ctx.lineWidth = 3; + + ctx.beginPath(); + ctx.moveTo(outline.top[0].x, outline.top[0].y); + for (let index = 1; index < outline.top.length; index += 1) { + ctx.lineTo(outline.top[index].x, outline.top[index].y); + } + ctx.closePath(); + ctx.stroke(); + + ctx.lineWidth = 1.5; + ctx.beginPath(); + ctx.moveTo(outline.bottom[0].x, outline.bottom[0].y); + for (let index = 1; index < outline.bottom.length; index += 1) { + ctx.lineTo(outline.bottom[index].x, outline.bottom[index].y); + } + ctx.closePath(); + ctx.stroke(); + + outline.verticals.forEach((pair) => { + ctx.beginPath(); + ctx.moveTo(pair[0].x, pair[0].y); + ctx.lineTo(pair[1].x, pair[1].y); + ctx.stroke(); + }); + }); + } + + render() { + if (!this.context) { + return; + } + + this.drawBackground(); + this.drawGround(); + this.drawDrawables(); + } + } + + window.PLRGeometryViewer = { + CanvasCatalogViewer, + }; +})(); diff --git a/docs/_static/plr_labware_catalog.css b/docs/_static/plr_labware_catalog.css new file mode 100644 index 00000000000..8419b65a537 --- /dev/null +++ b/docs/_static/plr_labware_catalog.css @@ -0,0 +1,268 @@ +.plr-catalog-toolbar { + display: grid; + grid-template-columns: minmax(240px, 1fr) minmax(160px, 220px) minmax(160px, 220px) auto; + gap: 0.75rem; + align-items: end; + margin: 1rem 0 1.25rem; + padding: 0.85rem; + border: 1px solid #d6dee7; + border-radius: 8px; + background: #f7fafc; +} + +.plr-catalog-search, +.plr-catalog-filter { + display: grid; + gap: 0.35rem; +} + +.plr-catalog-search label, +.plr-catalog-filter label { + color: #53687b; + font-size: 0.78rem; + font-weight: 700; + text-transform: uppercase; +} + +.plr-catalog-search input, +.plr-catalog-filter select { + width: 100%; + min-height: 2.4rem; + border: 1px solid #bdc9d4; + border-radius: 6px; + background: #ffffff; + color: #17334a; + font: inherit; + padding: 0.45rem 0.65rem; +} + +.plr-catalog-count { + color: #53687b; + font-size: 0.9rem; + white-space: nowrap; +} + +.plr-catalog-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fill, minmax(285px, 1fr)); + margin: 1rem 0 2rem; +} + +.plr-catalog-loading, +.plr-catalog-empty { + padding: 1rem; + border: 1px solid #d6dee7; + border-radius: 8px; + background: #f7fafc; + color: #53687b; +} + +.plr-library-card { + display: grid; + grid-template-rows: 170px 1fr; + overflow: hidden; + border: 1px solid #d3dce6; + border-radius: 8px; + background: #ffffff; + box-shadow: 0 10px 22px rgba(22, 38, 55, 0.07); +} + +.plr-library-card__media { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + border: 0; + border-bottom: 1px solid #d3dce6; + padding: 0.75rem; + background: #edf3f7; + cursor: pointer; +} + +.plr-library-card__media:hover { + background: #e3ecf3; +} + +.plr-library-card__media img { + max-width: 100%; + max-height: 100%; + object-fit: contain; + mix-blend-mode: multiply; +} + +.plr-library-card__placeholder { + color: #6b7f91; + font-size: 0.95rem; +} + +.plr-library-card__body { + display: flex; + flex-direction: column; + gap: 0.55rem; + padding: 0.95rem; +} + +.plr-library-card__vendor { + color: #607487; + font-size: 0.76rem; + font-weight: 700; + text-transform: uppercase; +} + +.plr-library-card__title { + margin: 0; + color: #17334a; + font-size: 0.98rem; + line-height: 1.3; + overflow-wrap: anywhere; +} + +.plr-library-card__section { + width: fit-content; + max-width: 100%; + padding: 0.22rem 0.45rem; + border-radius: 6px; + background: #eaf1f6; + color: #3d5367; + font-size: 0.78rem; + overflow-wrap: anywhere; +} + +.plr-library-card__description { + flex: 1; + color: #344a5f; + font-size: 0.86rem; + line-height: 1.45; +} + +.plr-library-card__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + margin-top: auto; + padding-top: 0.2rem; +} + +.plr-library-card__source { + color: #315b7c; + font-size: 0.86rem; +} + +.plr-library-card__action { + border: 0; + border-radius: 6px; + padding: 0.5rem 0.75rem; + background: #17334a; + color: #ffffff; + font-weight: 600; + cursor: pointer; + white-space: nowrap; +} + +.plr-library-card__action:hover { + background: #254963; +} + +.plr-library-card__action:disabled { + background: #9baaba; + cursor: not-allowed; +} + +.plr-library-modal[hidden] { + display: none; +} + +.plr-library-modal { + position: fixed; + inset: 0; + z-index: 1200; +} + +.plr-library-modal__backdrop { + position: absolute; + inset: 0; + background: rgba(15, 27, 38, 0.58); +} + +.plr-library-modal__dialog { + position: relative; + display: grid; + grid-template-rows: auto 1fr; + width: min(1080px, calc(100vw - 2rem)); + height: min(760px, calc(100vh - 2rem)); + margin: 1rem auto; + overflow: hidden; + border-radius: 8px; + background: #f4f8fb; + box-shadow: 0 32px 80px rgba(8, 20, 31, 0.28); +} + +.plr-library-modal__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.9rem 1.1rem; + border-bottom: 1px solid #d5e0e9; + background: #ffffff; +} + +.plr-library-modal__eyebrow { + margin: 0 0 0.2rem; + color: #6e8598; + font-size: 0.76rem; + font-weight: 700; + text-transform: uppercase; +} + +.plr-library-modal__title { + margin: 0; + color: #17334a; + font-size: 1.05rem; + overflow-wrap: anywhere; +} + +.plr-library-modal__close { + border: 0; + border-radius: 6px; + padding: 0.55rem 0.8rem; + background: #17334a; + color: #ffffff; + cursor: pointer; +} + +.plr-library-modal__stage { + min-height: 420px; +} + +.plr-geometry-canvas { + display: block; + width: 100%; + height: 100%; +} + +.plr-library-modal-open { + overflow: hidden; +} + +@media (max-width: 900px) { + .plr-catalog-toolbar { + grid-template-columns: 1fr; + } + + .plr-catalog-count { + white-space: normal; + } + + .plr-library-modal__dialog { + width: calc(100vw - 1rem); + height: calc(100vh - 1rem); + margin: 0.5rem auto; + } + + .plr-library-card { + grid-template-rows: 145px 1fr; + } +} diff --git a/docs/_static/plr_labware_catalog.js b/docs/_static/plr_labware_catalog.js new file mode 100644 index 00000000000..072ef2792ee --- /dev/null +++ b/docs/_static/plr_labware_catalog.js @@ -0,0 +1,274 @@ +(function () { + "use strict"; + + const state = { + index: null, + query: "", + vendor: "All", + section: "All", + selectedDefinition: null, + viewer: null, + }; + + function getUrlRoot() { + if (window.DOCUMENTATION_OPTIONS && window.DOCUMENTATION_OPTIONS.URL_ROOT) { + return window.DOCUMENTATION_OPTIONS.URL_ROOT; + } + if (document.documentElement && document.documentElement.dataset.content_root) { + return document.documentElement.dataset.content_root; + } + return ""; + } + + function staticUrl(path) { + return `${getUrlRoot()}${path}`; + } + + function catalogIndexUrl() { + const currentScript = + document.currentScript || + document.querySelector('script[src*="plr_labware_catalog.js"]'); + if (currentScript && currentScript.src) { + return new URL("labware_geometry_index.json", currentScript.src).toString(); + } + return staticUrl("_static/labware_geometry_index.json"); + } + + function element(tagName, className, text) { + const node = document.createElement(tagName); + if (className) node.className = className; + if (text) node.textContent = text; + return node; + } + + function unique(values) { + return Array.from(new Set(values.filter(Boolean))).sort((left, right) => + left.localeCompare(right), + ); + } + + function itemMatchesFilters(item) { + const haystack = [ + item.definition, + item.vendor, + item.section, + item.description_html, + ] + .join(" ") + .toLowerCase(); + + if (state.query && haystack.indexOf(state.query.toLowerCase()) === -1) { + return false; + } + if (state.vendor !== "All" && item.vendor !== state.vendor) { + return false; + } + if (state.section !== "All" && item.section !== state.section) { + return false; + } + return true; + } + + function setSelectOptions(select, values, selectedValue) { + select.innerHTML = ""; + ["All"].concat(values).forEach((value) => { + const option = document.createElement("option"); + option.value = value; + option.textContent = value; + option.selected = value === selectedValue; + select.appendChild(option); + }); + } + + function createModal() { + const overlay = document.createElement("div"); + overlay.className = "plr-library-modal"; + overlay.setAttribute("hidden", "hidden"); + overlay.innerHTML = ` +
+ + `; + document.body.appendChild(overlay); + return overlay; + } + + function ensureModal() { + if (state.modal) { + return state.modal; + } + + const viewerApi = window.PLRGeometryViewer; + if (!viewerApi || !viewerApi.CanvasCatalogViewer) { + return null; + } + + const modal = createModal(); + const stage = modal.querySelector(".plr-library-modal__stage"); + state.viewer = new viewerApi.CanvasCatalogViewer(stage); + state.modal = modal; + + function closeModal() { + modal.setAttribute("hidden", "hidden"); + document.body.classList.remove("plr-library-modal-open"); + } + + modal.querySelector(".plr-library-modal__close").addEventListener("click", closeModal); + modal.querySelector(".plr-library-modal__backdrop").addEventListener("click", closeModal); + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") closeModal(); + }); + + return modal; + } + + function openModel(definitionName) { + const modal = ensureModal(); + const catalog = state.index.resources[definitionName]; + if (!modal || !catalog || !state.viewer) { + return; + } + + modal.querySelector(".plr-library-modal__title").textContent = definitionName; + modal.removeAttribute("hidden"); + document.body.classList.add("plr-library-modal-open"); + state.viewer.setCatalog(catalog); + window.requestAnimationFrame(() => state.viewer.resize()); + } + + function createCard(item) { + const card = element("article", "plr-library-card"); + + const media = element("button", "plr-library-card__media"); + media.type = "button"; + media.setAttribute("aria-label", `Open 3D preview for ${item.definition}`); + if (item.image) { + const image = document.createElement("img"); + image.src = item.image.startsWith("http") || item.image.startsWith("/") + ? item.image + : staticUrl(item.image); + image.alt = item.definition; + image.loading = "lazy"; + media.appendChild(image); + } else { + media.appendChild(element("div", "plr-library-card__placeholder", "No image")); + } + media.addEventListener("click", () => openModel(item.definition)); + + const body = element("div", "plr-library-card__body"); + const vendor = element("div", "plr-library-card__vendor", item.vendor); + const title = element("h3", "plr-library-card__title", item.definition); + const section = element("div", "plr-library-card__section", item.section || "Resource"); + const description = element("div", "plr-library-card__description"); + description.innerHTML = item.description_html || ""; + + const footer = element("div", "plr-library-card__footer"); + const sourceLink = element("a", "plr-library-card__source", "Source page"); + sourceLink.href = staticUrl(item.page); + const modelButton = element("button", "plr-library-card__action", "View 3D"); + modelButton.type = "button"; + modelButton.disabled = !item.has_geometry; + modelButton.addEventListener("click", () => openModel(item.definition)); + + footer.appendChild(sourceLink); + footer.appendChild(modelButton); + body.appendChild(vendor); + body.appendChild(title); + body.appendChild(section); + if (item.description_html) body.appendChild(description); + body.appendChild(footer); + card.appendChild(media); + card.appendChild(body); + return card; + } + + function renderCatalog(root) { + const items = (state.index.items || []).filter(itemMatchesFilters); + const grid = root.querySelector(".plr-catalog-grid"); + const count = root.querySelector(".plr-catalog-count"); + grid.innerHTML = ""; + count.textContent = `${items.length} resources`; + + if (items.length === 0) { + grid.appendChild(element("div", "plr-catalog-empty", "No labware matches these filters.")); + return; + } + + items.forEach((item) => { + grid.appendChild(createCard(item)); + }); + } + + function renderCatalogShell(root) { + root.innerHTML = ` +
+ +
+ + +
+
+ + +
+
+
+
+ `; + + const search = root.querySelector("#plr-catalog-search-input"); + const vendor = root.querySelector("#plr-catalog-vendor"); + const section = root.querySelector("#plr-catalog-section"); + setSelectOptions(vendor, unique(state.index.items.map((item) => item.vendor)), state.vendor); + setSelectOptions(section, unique(state.index.items.map((item) => item.section)), state.section); + + search.addEventListener("input", () => { + state.query = search.value; + renderCatalog(root); + }); + vendor.addEventListener("change", () => { + state.vendor = vendor.value; + renderCatalog(root); + }); + section.addEventListener("change", () => { + state.section = section.value; + renderCatalog(root); + }); + + renderCatalog(root); + } + + function initializeCatalogPage() { + const root = document.getElementById("plr-labware-catalog"); + if (!root) { + return; + } + + root.innerHTML = `
Loading catalog...
`; + fetch(catalogIndexUrl()) + .then((response) => { + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); + }) + .then((index) => { + state.index = index; + renderCatalogShell(root); + }) + .catch((error) => { + root.innerHTML = `
Could not load the generated catalog index: ${error.message}
`; + }); + } + + document.addEventListener("DOMContentLoaded", initializeCatalogPage); +})(); diff --git a/docs/_templates/autosummary/attribute.rst b/docs/_templates/autosummary/attribute.rst new file mode 100644 index 00000000000..4ad94123a2c --- /dev/null +++ b/docs/_templates/autosummary/attribute.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. autoattribute:: {{ objname }} diff --git a/docs/_templates/autosummary/data.rst b/docs/_templates/autosummary/data.rst new file mode 100644 index 00000000000..ad6fafdd46a --- /dev/null +++ b/docs/_templates/autosummary/data.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. autodata:: {{ objname }} diff --git a/docs/_templates/autosummary/exception.rst b/docs/_templates/autosummary/exception.rst new file mode 100644 index 00000000000..752bc57414b --- /dev/null +++ b/docs/_templates/autosummary/exception.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. autoexception:: {{ objname }} diff --git a/docs/_templates/autosummary/function.rst b/docs/_templates/autosummary/function.rst new file mode 100644 index 00000000000..eade6e8536e --- /dev/null +++ b/docs/_templates/autosummary/function.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. autofunction:: {{ objname }} diff --git a/docs/_templates/autosummary/method.rst b/docs/_templates/autosummary/method.rst new file mode 100644 index 00000000000..b8eaf012e69 --- /dev/null +++ b/docs/_templates/autosummary/method.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. automethod:: {{ objname }} diff --git a/docs/_templates/autosummary/property.rst b/docs/_templates/autosummary/property.rst new file mode 100644 index 00000000000..12c1dee318e --- /dev/null +++ b/docs/_templates/autosummary/property.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. autoproperty:: {{ objname }} diff --git a/docs/api/pylabrobot.resources.rst b/docs/api/pylabrobot.resources.rst index 5cbc3ceee2e..acfb0f859d6 100644 --- a/docs/api/pylabrobot.resources.rst +++ b/docs/api/pylabrobot.resources.rst @@ -14,6 +14,7 @@ Resources represent on-deck liquid handling equipment, including tip racks, plat Container Coordinate Deck + generate_geometry_catalog ItemizedResource utils.create_equally_spaced_2d Lid @@ -26,6 +27,7 @@ Resources represent on-deck liquid handling equipment, including tip racks, plat ResourceHolder ResourceStack Rotation + save_geometry_catalog tip.Tip TipCarrier TipRack diff --git a/docs/conf.py b/docs/conf.py index a6e9359e41b..5f45f709ac3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,6 +38,7 @@ "sphinx.ext.napoleon", "sphinx.ext.autodoc", "pylabrobot_cards", # NEW: PLR cards (plrcard/plrcardgrid + compat) + "pylabrobot_labware_catalog", "sphinx.ext.autosummary", "sphinx.ext.autosectionlabel", "sphinx.ext.intersphinx", @@ -98,10 +99,16 @@ html_css_files = list(globals().get("html_css_files", [])) if "plr_cards.css" not in html_css_files: html_css_files.append("plr_cards.css") # served from _static/plr_cards.css +if "plr_labware_catalog.css" not in html_css_files: + html_css_files.append("plr_labware_catalog.css") html_js_files = list(globals().get("html_js_files", [])) if "plr_cards.js" not in html_js_files: html_js_files.append("plr_cards.js") # served from _static/plr_cards.js +if "plr_geometry_viewer.js" not in html_js_files: + html_js_files.append("plr_geometry_viewer.js") +if "plr_labware_catalog.js" not in html_js_files: + html_js_files.append("plr_labware_catalog.js") # NOTE: templates_path already includes "_templates", which is where # plr_card_grid.html should live. @@ -193,6 +200,7 @@ "tilting.html": "user_guide/tilting.html", "heating-shaking.html": "user_guide/heating_shaking.html", "fans.html": "user_guide/fans.html", + "resources/geometry-catalog.html": "resources/catalog.html", } html_sidebars = { diff --git a/docs/resources/catalog.md b/docs/resources/catalog.md new file mode 100644 index 00000000000..97bde8f6cb4 --- /dev/null +++ b/docs/resources/catalog.md @@ -0,0 +1,8 @@ +# Resource Catalog + +The PyLabRobot Resource Library is rendered from the resource definitions used by PLR itself. +The catalog below is generated during the docs build: each item links back to its source page and opens a 3D preview from the same geometry metadata returned by `generate_geometry_catalog`. + +```{raw} html +
+``` diff --git a/docs/resources/index.md b/docs/resources/index.md index 4a86bf7ca1c..af6716d4c41 100644 --- a/docs/resources/index.md +++ b/docs/resources/index.md @@ -141,6 +141,8 @@ Laboratories across the world use an almost infinite number of different resourc We believe the way to most efficiently capture the largest portion of this resource superset is via crowd-sourcing and iteratively peer-reviewing definitions. If you cannot find something, please contribute what you are looking for! +Open the {doc}`catalog page ` to search the library across manufacturers and inspect generated 3D previews for labware definitions. +
@@ -167,6 +169,7 @@ If you cannot find something, please contribute what you are looking for! ```{toctree} :caption: Resource Library +catalog library/agenbio library/agilent library/alpaqua diff --git a/pylabrobot/resources/geometry.py b/pylabrobot/resources/geometry.py index 741dddea0ed..d1abe893286 100644 --- a/pylabrobot/resources/geometry.py +++ b/pylabrobot/resources/geometry.py @@ -32,7 +32,7 @@ def generate_geometry_catalog(root: Resource) -> Dict[str, Any]: instance: Dict[str, Any] = { "prototype": prototype_id, "parent": resource.parent.name if resource.parent is not None else None, - "pose": _coordinate_or_none(resource, x="l", y="f", z="b"), + "pose": _resource_pose(resource), "rotation": _rotation_values(resource.get_absolute_rotation()), } if len(resource.children) > 0: @@ -122,6 +122,13 @@ def _coordinate_or_none(resource: Resource, x: str, y: str, z: str) -> Optional[ return None +def _resource_pose(resource: Resource) -> Optional[List[float]]: + pose = _coordinate_or_none(resource, x="l", y="f", z="b") + if pose is None and resource.parent is None: + return [0, 0, 0] + return pose + + def _coordinate_values(coordinate: Coordinate) -> List[float]: return [coordinate.x, coordinate.y, coordinate.z] diff --git a/pylabrobot/resources/geometry_tests.py b/pylabrobot/resources/geometry_tests.py index c5d59917bfc..b57eb162303 100644 --- a/pylabrobot/resources/geometry_tests.py +++ b/pylabrobot/resources/geometry_tests.py @@ -32,9 +32,11 @@ def test_generate_geometry_catalog_for_deck(self): self.assertEqual(len(well_instance["pose"]), 3) def test_generate_geometry_catalog_for_single_labware(self): - catalog = generate_geometry_catalog(self.plate) + plate = Cor_96_wellplate_360ul_Fb(name="standalone_plate") + catalog = generate_geometry_catalog(plate) - self.assertEqual(catalog["root"], "plate") - self.assertIn("plate", catalog["instances"]) - self.assertIn("plate_well_A1", catalog["instances"]) + self.assertEqual(catalog["root"], "standalone_plate") + self.assertIn("standalone_plate", catalog["instances"]) + self.assertIn("standalone_plate_well_A1", catalog["instances"]) + self.assertEqual(catalog["instances"]["standalone_plate"]["pose"], [0, 0, 0]) self.assertLess(len(catalog["prototypes"]), len(catalog["instances"])) From 712eff09f9625aadd5f8882e258c31d12ad81b60 Mon Sep 17 00:00:00 2001 From: norle Date: Fri, 15 May 2026 15:34:37 +0200 Subject: [PATCH 03/27] remove old resource libarry --- docs/resources/index.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/resources/index.md b/docs/resources/index.md index af6716d4c41..d03094c7912 100644 --- a/docs/resources/index.md +++ b/docs/resources/index.md @@ -170,6 +170,11 @@ Open the {doc}`catalog page ` to search the library across manufacturer :caption: Resource Library catalog +``` + +```{toctree} +:hidden: + library/agenbio library/agilent library/alpaqua From 2c214a793dc8d62d70e758de24f6ff86988484ab Mon Sep 17 00:00:00 2001 From: norle Date: Fri, 15 May 2026 15:48:29 +0200 Subject: [PATCH 04/27] replace resource libary with resource catalog --- docs/_static/plr_labware_catalog.js | 3 -- docs/conf.py | 29 ++++++++++++++ .../carrier/plate-carrier/plate_carrier.ipynb | 2 +- docs/resources/catalog.md | 4 +- docs/resources/index.md | 39 +++---------------- .../heating_shaking/hamilton.ipynb | 2 +- 6 files changed, 38 insertions(+), 41 deletions(-) diff --git a/docs/_static/plr_labware_catalog.js b/docs/_static/plr_labware_catalog.js index 072ef2792ee..1c249c839c6 100644 --- a/docs/_static/plr_labware_catalog.js +++ b/docs/_static/plr_labware_catalog.js @@ -171,14 +171,11 @@ description.innerHTML = item.description_html || ""; const footer = element("div", "plr-library-card__footer"); - const sourceLink = element("a", "plr-library-card__source", "Source page"); - sourceLink.href = staticUrl(item.page); const modelButton = element("button", "plr-library-card__action", "View 3D"); modelButton.type = "button"; modelButton.disabled = !item.has_geometry; modelButton.addEventListener("click", () => openModel(item.definition)); - footer.appendChild(sourceLink); footer.appendChild(modelButton); body.appendChild(vendor); body.appendChild(title); diff --git a/docs/conf.py b/docs/conf.py index 5f45f709ac3..64b5043010a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,6 +13,7 @@ import os import shutil import sys +from pathlib import Path sys.path.insert(0, os.path.abspath("..")) # Allow importing local Sphinx extensions (e.g., pylabrobot_cards) @@ -66,6 +67,8 @@ "Thumbs.db", ".DS_Store", "jupyter_execute", + "resources/library/*.md", + "resources/library/**/*.md", ] autodoc_default_options = { @@ -203,6 +206,32 @@ "resources/geometry-catalog.html": "resources/catalog.html", } + +def _catalog_redirect_target(source_html_path: str) -> str: + target = "resources/catalog.html" + source_dir = os.path.dirname(source_html_path) + if not source_dir: + return target + return os.path.relpath(target, start=source_dir).replace("\\", "/") + + +def _library_doc_redirects() -> dict[str, str]: + redirects_map: dict[str, str] = {} + library_root = Path(__file__).parent / "resources" / "library" + for markdown_path in library_root.rglob("*.md"): + relative_markdown = markdown_path.relative_to(Path(__file__).parent) + source_html = relative_markdown.with_suffix(".html").as_posix() + redirects_map[source_html] = _catalog_redirect_target(source_html) + + # Also redirect extension-less style links such as /resources/library/hamilton. + if markdown_path.stem != "index": + folder_style_source = (relative_markdown.with_suffix("") / "index.html").as_posix() + redirects_map[folder_style_source] = _catalog_redirect_target(folder_style_source) + return redirects_map + + +redirects.update(_library_doc_redirects()) + html_sidebars = { "api/**": ["search-field"], } diff --git a/docs/resources/carrier/plate-carrier/plate_carrier.ipynb b/docs/resources/carrier/plate-carrier/plate_carrier.ipynb index cfb5a304848..cf945523eb2 100644 --- a/docs/resources/carrier/plate-carrier/plate_carrier.ipynb +++ b/docs/resources/carrier/plate-carrier/plate_carrier.ipynb @@ -15,7 +15,7 @@ "source": [ "## Using a plate carrier\n", "\n", - "The PyLabRobot Resource Library (PLR-RL) has a big number of predefined carriers. You can find these in the [PLR-RL docs](../../index.md). [Hamilton Plate Carriers](../../library/hamilton.md#plate-carriers) may be of particular interest." + "The PyLabRobot Resource Catalog has a big number of predefined carriers. You can find these in the [resource catalog](../../catalog.md)." ] }, { diff --git a/docs/resources/catalog.md b/docs/resources/catalog.md index 97bde8f6cb4..6b0ff24c99e 100644 --- a/docs/resources/catalog.md +++ b/docs/resources/catalog.md @@ -1,7 +1,7 @@ # Resource Catalog -The PyLabRobot Resource Library is rendered from the resource definitions used by PLR itself. -The catalog below is generated during the docs build: each item links back to its source page and opens a 3D preview from the same geometry metadata returned by `generate_geometry_catalog`. +The PyLabRobot Resource Catalog is rendered from the resource definitions used by PLR itself. +The catalog below is generated during the docs build and includes manufacturer, type, definition details, images, and 3D previews from the same geometry metadata returned by `generate_geometry_catalog`. ```{raw} html
diff --git a/docs/resources/index.md b/docs/resources/index.md index d03094c7912..1d5c67bc8a6 100644 --- a/docs/resources/index.md +++ b/docs/resources/index.md @@ -11,7 +11,7 @@ The PLR Resource Management System consists of two key components, each serving 1. **Resource Ontology System** - The ***'blueprint'*** of PLR's physical definition framework, responsible for defining physical resources, modeling their distinct behaviors, and dynamically managing their relationships (i.e. tracking their *state*). -2. **Resource Library** +2. **Resource Catalog** - The ***'catalog'*** of premade resource definitions. This provides reusable, standardized definitions that enhance consistency and interoperability across automation workflows. This ensures smooth integration, scalability, and efficient resource utilization. @@ -134,14 +134,14 @@ plate-adapter/plate-adapter resource-stack/resource-stack ``` -## Resource Library +## Resource Catalog -The PyLabRobot Resource Library (PLR-RL) is PyLabRobot's open-source, crowd-sourced collection of pre-made resource definitions. +The PyLabRobot Resource Catalog is PyLabRobot's open-source, crowd-sourced collection of pre-made resource definitions. Laboratories across the world use an almost infinite number of different resources (e.g. plates, tubes, liquid handlers, microscopes, arms, ...). We believe the way to most efficiently capture the largest portion of this resource superset is via crowd-sourcing and iteratively peer-reviewing definitions. If you cannot find something, please contribute what you are looking for! -Open the {doc}`catalog page ` to search the library across manufacturers and inspect generated 3D previews for labware definitions. +Open the {doc}`catalog page ` to search the catalog across manufacturers and inspect generated 3D previews for labware definitions.
@@ -167,36 +167,7 @@ Open the {doc}`catalog page ` to search the library across manufacturer ```{toctree} -:caption: Resource Library +:caption: Resource Catalog catalog ``` - -```{toctree} -:hidden: - -library/agenbio -library/agilent -library/alpaqua -library/azenta -library/bioer -library/biorad -library/boekel -library/celltreat -library/cellvis -library/corning -library/eppendorf -library/falcon -library/greiner -library/hamilton -library/imcs -library/nest -library/opentrons -library/perkin_elmer -library/porvair -library/revvity -library/sergi -library/thermo_fisher -library/vwr -library/diy/index -``` diff --git a/docs/user_guide/01_material-handling/heating_shaking/hamilton.ipynb b/docs/user_guide/01_material-handling/heating_shaking/hamilton.ipynb index 8ba4ebe5348..c6a06db0f0e 100644 --- a/docs/user_guide/01_material-handling/heating_shaking/hamilton.ipynb +++ b/docs/user_guide/01_material-handling/heating_shaking/hamilton.ipynb @@ -212,7 +212,7 @@ "\n", "Before you can use the Hamilton Heater Shaker in combination with a Hamilton STAR liquid handler, you need to assign it to the deck. This is needed when, for example, you want to use the iSWAP or CoRe grippers to move a plate to or from the heater shaker. This is also required to get the heater shaker to show up in the Visualizer.\n", "\n", - "Here's one example of assigning a Hamilton Heater Shaker to the deck using a `MFX_CAR_P3_SHAKER`. Note that you can use any carrier, or even directly place heater shakers on the deck if you like. See the [Hamilton STAR resources page](/resources/library/hamilton) for carriers." + "Here's one example of assigning a Hamilton Heater Shaker to the deck using a `MFX_CAR_P3_SHAKER`. Note that you can use any carrier, or even directly place heater shakers on the deck if you like. See the [resource catalog](../../../resources/catalog.md) for carriers." ] }, { From ddea892c229a35b4302a2434d19bfa166d68d130 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Fri, 15 May 2026 15:44:50 -0700 Subject: [PATCH 05/27] docs: make resource catalog respect light/dark theme Replace hardcoded colors in plr_labware_catalog.css with pydata-sphinx-theme CSS variables (--pst-color-background/surface/border/text-base/text-muted/ primary) so the catalog grid and modal pick up the active theme. The labware image tile stays white because the photos use mix-blend-mode: multiply. In plr_geometry_viewer.js read the same vars at render time and use them for the background gradient, ground grid and face edges. Alpha-blended strokes use ctx.globalAlpha so the canvas parses the CSS color itself instead of hand-parsing rgb() strings. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/_static/plr_geometry_viewer.js | 25 ++++++++-- docs/_static/plr_labware_catalog.css | 71 ++++++++++++++-------------- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/docs/_static/plr_geometry_viewer.js b/docs/_static/plr_geometry_viewer.js index acd180a7f2f..c1f81aed1f5 100644 --- a/docs/_static/plr_geometry_viewer.js +++ b/docs/_static/plr_geometry_viewer.js @@ -37,6 +37,15 @@ return `rgb(${Math.round(rgb.r * scale)}, ${Math.round(rgb.g * scale)}, ${Math.round(rgb.b * scale)})`; } + function readThemeColors(root) { + const probe = root || document.documentElement; + const style = getComputedStyle(probe); + const bg = style.getPropertyValue("--pst-color-background").trim() || "#f7fafc"; + const surface = style.getPropertyValue("--pst-color-surface").trim() || bg; + const text = style.getPropertyValue("--pst-color-text-base").trim() || "#17334a"; + return { bg, surface, text }; + } + function multiplyMatrix(a, b) { return [ [ @@ -536,9 +545,10 @@ drawBackground() { const ctx = this.context; + const theme = readThemeColors(this.root); const gradient = ctx.createLinearGradient(0, 0, 0, this.canvas.height); - gradient.addColorStop(0, "#f5f8fb"); - gradient.addColorStop(1, "#e6edf3"); + gradient.addColorStop(0, theme.bg); + gradient.addColorStop(1, theme.surface); ctx.fillStyle = gradient; ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); } @@ -550,7 +560,9 @@ const gridSize = Math.max(4, Math.min(12, Math.round(this.bounds.span / 20))); ctx.lineWidth = 1; - ctx.strokeStyle = "rgba(126, 152, 176, 0.22)"; + ctx.save(); + ctx.strokeStyle = readThemeColors(this.root).text; + ctx.globalAlpha = 0.22; for (let index = -gridSize; index <= gridSize; index += 1) { const ratio = index / gridSize; @@ -571,6 +583,7 @@ ctx.lineTo(yLineEnd.x, yLineEnd.y); ctx.stroke(); } + ctx.restore(); } drawDrawables() { @@ -609,6 +622,7 @@ }); const ctx = this.context; + const edgeColor = readThemeColors(this.root).text; faces.forEach((face) => { const shade = face.points.length >= 4 @@ -624,10 +638,11 @@ ctx.fillStyle = shade; ctx.globalAlpha = face.alpha; ctx.fill(); - ctx.globalAlpha = 1; - ctx.strokeStyle = "rgba(30, 53, 77, 0.28)"; + ctx.globalAlpha = 0.28; + ctx.strokeStyle = edgeColor; ctx.lineWidth = 1; ctx.stroke(); + ctx.globalAlpha = 1; }); outlines.forEach((outline) => { diff --git a/docs/_static/plr_labware_catalog.css b/docs/_static/plr_labware_catalog.css index 8419b65a537..fba15dad8b9 100644 --- a/docs/_static/plr_labware_catalog.css +++ b/docs/_static/plr_labware_catalog.css @@ -5,9 +5,9 @@ align-items: end; margin: 1rem 0 1.25rem; padding: 0.85rem; - border: 1px solid #d6dee7; + border: 1px solid var(--pst-color-border); border-radius: 8px; - background: #f7fafc; + background: var(--pst-color-surface); } .plr-catalog-search, @@ -18,7 +18,7 @@ .plr-catalog-search label, .plr-catalog-filter label { - color: #53687b; + color: var(--pst-color-text-muted); font-size: 0.78rem; font-weight: 700; text-transform: uppercase; @@ -28,16 +28,16 @@ .plr-catalog-filter select { width: 100%; min-height: 2.4rem; - border: 1px solid #bdc9d4; + border: 1px solid var(--pst-color-border); border-radius: 6px; - background: #ffffff; - color: #17334a; + background: var(--pst-color-on-background); + color: var(--pst-color-text-base); font: inherit; padding: 0.45rem 0.65rem; } .plr-catalog-count { - color: #53687b; + color: var(--pst-color-text-muted); font-size: 0.9rem; white-space: nowrap; } @@ -52,20 +52,20 @@ .plr-catalog-loading, .plr-catalog-empty { padding: 1rem; - border: 1px solid #d6dee7; + border: 1px solid var(--pst-color-border); border-radius: 8px; - background: #f7fafc; - color: #53687b; + background: var(--pst-color-surface); + color: var(--pst-color-text-muted); } .plr-library-card { display: grid; grid-template-rows: 170px 1fr; overflow: hidden; - border: 1px solid #d3dce6; + border: 1px solid var(--pst-color-border); border-radius: 8px; - background: #ffffff; - box-shadow: 0 10px 22px rgba(22, 38, 55, 0.07); + background: var(--pst-color-on-background); + box-shadow: 0 10px 22px rgba(0, 0, 0, 0.08); } .plr-library-card__media { @@ -74,14 +74,14 @@ justify-content: center; width: 100%; border: 0; - border-bottom: 1px solid #d3dce6; + border-bottom: 1px solid var(--pst-color-border); padding: 0.75rem; - background: #edf3f7; + background: #ffffff; cursor: pointer; } .plr-library-card__media:hover { - background: #e3ecf3; + filter: brightness(0.97); } .plr-library-card__media img { @@ -104,7 +104,7 @@ } .plr-library-card__vendor { - color: #607487; + color: var(--pst-color-text-muted); font-size: 0.76rem; font-weight: 700; text-transform: uppercase; @@ -112,7 +112,7 @@ .plr-library-card__title { margin: 0; - color: #17334a; + color: var(--pst-color-text-base); font-size: 0.98rem; line-height: 1.3; overflow-wrap: anywhere; @@ -123,15 +123,15 @@ max-width: 100%; padding: 0.22rem 0.45rem; border-radius: 6px; - background: #eaf1f6; - color: #3d5367; + background: var(--pst-color-surface); + color: var(--pst-color-text-base); font-size: 0.78rem; overflow-wrap: anywhere; } .plr-library-card__description { flex: 1; - color: #344a5f; + color: var(--pst-color-text-base); font-size: 0.86rem; line-height: 1.45; } @@ -146,7 +146,7 @@ } .plr-library-card__source { - color: #315b7c; + color: var(--pst-color-link); font-size: 0.86rem; } @@ -154,20 +154,21 @@ border: 0; border-radius: 6px; padding: 0.5rem 0.75rem; - background: #17334a; - color: #ffffff; + background: var(--pst-color-primary); + color: var(--pst-color-background); font-weight: 600; cursor: pointer; white-space: nowrap; } .plr-library-card__action:hover { - background: #254963; + filter: brightness(0.92); } .plr-library-card__action:disabled { - background: #9baaba; + background: var(--pst-color-text-muted); cursor: not-allowed; + opacity: 0.6; } .plr-library-modal[hidden] { @@ -183,7 +184,7 @@ .plr-library-modal__backdrop { position: absolute; inset: 0; - background: rgba(15, 27, 38, 0.58); + background: rgba(0, 0, 0, 0.58); } .plr-library-modal__dialog { @@ -195,8 +196,8 @@ margin: 1rem auto; overflow: hidden; border-radius: 8px; - background: #f4f8fb; - box-shadow: 0 32px 80px rgba(8, 20, 31, 0.28); + background: var(--pst-color-background); + box-shadow: 0 32px 80px rgba(0, 0, 0, 0.4); } .plr-library-modal__header { @@ -205,13 +206,13 @@ justify-content: space-between; gap: 1rem; padding: 0.9rem 1.1rem; - border-bottom: 1px solid #d5e0e9; - background: #ffffff; + border-bottom: 1px solid var(--pst-color-border); + background: var(--pst-color-on-background); } .plr-library-modal__eyebrow { margin: 0 0 0.2rem; - color: #6e8598; + color: var(--pst-color-text-muted); font-size: 0.76rem; font-weight: 700; text-transform: uppercase; @@ -219,7 +220,7 @@ .plr-library-modal__title { margin: 0; - color: #17334a; + color: var(--pst-color-text-base); font-size: 1.05rem; overflow-wrap: anywhere; } @@ -228,8 +229,8 @@ border: 0; border-radius: 6px; padding: 0.55rem 0.8rem; - background: #17334a; - color: #ffffff; + background: var(--pst-color-primary); + color: var(--pst-color-background); cursor: pointer; } From 9b5e1363a930e50bcf2280ca437fa47052789325 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Fri, 15 May 2026 15:56:54 -0700 Subject: [PATCH 06/27] docs: persist catalog filters in URL and code-format definition names readUrlState/writeUrlState read and update q/vendor/section query params via URLSearchParams + history.replaceState, so refreshing or sharing the URL preserves the active filters. Wrap the definition name in in the card title and modal title so pydata-sphinx-theme renders it monospaced. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/_static/plr_labware_catalog.js | 36 +++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/_static/plr_labware_catalog.js b/docs/_static/plr_labware_catalog.js index 1c249c839c6..9cc1b37baf4 100644 --- a/docs/_static/plr_labware_catalog.js +++ b/docs/_static/plr_labware_catalog.js @@ -137,7 +137,11 @@ return; } - modal.querySelector(".plr-library-modal__title").textContent = definitionName; + const modalTitle = modal.querySelector(".plr-library-modal__title"); + modalTitle.textContent = ""; + const modalCode = document.createElement("code"); + modalCode.textContent = definitionName; + modalTitle.appendChild(modalCode); modal.removeAttribute("hidden"); document.body.classList.add("plr-library-modal-open"); state.viewer.setCatalog(catalog); @@ -165,7 +169,10 @@ const body = element("div", "plr-library-card__body"); const vendor = element("div", "plr-library-card__vendor", item.vendor); - const title = element("h3", "plr-library-card__title", item.definition); + const title = element("h3", "plr-library-card__title"); + const titleCode = document.createElement("code"); + titleCode.textContent = item.definition; + title.appendChild(titleCode); const section = element("div", "plr-library-card__section", item.section || "Resource"); const description = element("div", "plr-library-card__description"); description.innerHTML = item.description_html || ""; @@ -229,29 +236,54 @@ const section = root.querySelector("#plr-catalog-section"); setSelectOptions(vendor, unique(state.index.items.map((item) => item.vendor)), state.vendor); setSelectOptions(section, unique(state.index.items.map((item) => item.section)), state.section); + search.value = state.query; search.addEventListener("input", () => { state.query = search.value; + writeUrlState(); renderCatalog(root); }); vendor.addEventListener("change", () => { state.vendor = vendor.value; + writeUrlState(); renderCatalog(root); }); section.addEventListener("change", () => { state.section = section.value; + writeUrlState(); renderCatalog(root); }); renderCatalog(root); } + function readUrlState() { + const params = new URLSearchParams(window.location.search); + state.query = params.get("q") || ""; + state.vendor = params.get("vendor") || "All"; + state.section = params.get("section") || "All"; + } + + function writeUrlState() { + const params = new URLSearchParams(window.location.search); + if (state.query) params.set("q", state.query); + else params.delete("q"); + if (state.vendor && state.vendor !== "All") params.set("vendor", state.vendor); + else params.delete("vendor"); + if (state.section && state.section !== "All") params.set("section", state.section); + else params.delete("section"); + const queryString = params.toString(); + const newUrl = `${window.location.pathname}${queryString ? "?" + queryString : ""}${window.location.hash}`; + window.history.replaceState(null, "", newUrl); + } + function initializeCatalogPage() { const root = document.getElementById("plr-labware-catalog"); if (!root) { return; } + readUrlState(); root.innerHTML = `
Loading catalog...
`; fetch(catalogIndexUrl()) .then((response) => { From 081a4b7c04815f9a67272514ebf5ed133f867b85 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Fri, 15 May 2026 15:59:09 -0700 Subject: [PATCH 07/27] docs: group catalog cards by section when Type filter is "All" When no Type is selected the cards are grouped under their section headers (e.g. "96-well plates", "Plate carriers") so a single result page does not mix unrelated labware. Picking a Type collapses back to a flat grid. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/_static/plr_labware_catalog.css | 10 ++++++++++ docs/_static/plr_labware_catalog.js | 29 +++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/docs/_static/plr_labware_catalog.css b/docs/_static/plr_labware_catalog.css index fba15dad8b9..569f6e8cbb0 100644 --- a/docs/_static/plr_labware_catalog.css +++ b/docs/_static/plr_labware_catalog.css @@ -49,6 +49,16 @@ margin: 1rem 0 2rem; } +.plr-catalog-group + .plr-catalog-group { + margin-top: 1rem; +} + +.plr-catalog-group__title { + margin: 0.5rem 0 0.25rem; + color: var(--pst-color-text-base); + font-size: 1.1rem; +} + .plr-catalog-loading, .plr-catalog-empty { padding: 1rem; diff --git a/docs/_static/plr_labware_catalog.js b/docs/_static/plr_labware_catalog.js index 9cc1b37baf4..37ff0e80c36 100644 --- a/docs/_static/plr_labware_catalog.js +++ b/docs/_static/plr_labware_catalog.js @@ -196,18 +196,37 @@ function renderCatalog(root) { const items = (state.index.items || []).filter(itemMatchesFilters); - const grid = root.querySelector(".plr-catalog-grid"); + const results = root.querySelector(".plr-catalog-results"); const count = root.querySelector(".plr-catalog-count"); - grid.innerHTML = ""; + results.innerHTML = ""; count.textContent = `${items.length} resources`; if (items.length === 0) { - grid.appendChild(element("div", "plr-catalog-empty", "No labware matches these filters.")); + results.appendChild(element("div", "plr-catalog-empty", "No labware matches these filters.")); return; } + if (state.section !== "All") { + const grid = element("div", "plr-catalog-grid"); + items.forEach((item) => grid.appendChild(createCard(item))); + results.appendChild(grid); + return; + } + + const groups = new Map(); items.forEach((item) => { - grid.appendChild(createCard(item)); + const key = item.section || "Other"; + if (!groups.has(key)) groups.set(key, []); + groups.get(key).push(item); + }); + + groups.forEach((groupItems, sectionName) => { + const group = element("section", "plr-catalog-group"); + group.appendChild(element("h2", "plr-catalog-group__title", sectionName)); + const grid = element("div", "plr-catalog-grid"); + groupItems.forEach((item) => grid.appendChild(createCard(item))); + group.appendChild(grid); + results.appendChild(group); }); } @@ -228,7 +247,7 @@
-
+
`; const search = root.querySelector("#plr-catalog-search-input"); From fda6ed3ec13a60464212c7cde358e18548184fac Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sat, 16 May 2026 12:42:09 +0100 Subject: [PATCH 08/27] docs: add mm rulers, Blender-colored XYZ axes, and tuned camera to 3D viewer Replace the arbitrary floor grid with a true mm grid (nice 1-2-5 step), add labeled X/Y/Z axes in Blender colors (X red, Y green, Z blue) with a camera-facing Z axis sharing the X/Y origin corner, thicker axis lines, bold tick labels, a tuned default camera, and a small -Z drop so Y tick labels stay clear of the body. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/_static/plr_geometry_viewer.js | 186 ++++++++++++++++++++++++---- 1 file changed, 165 insertions(+), 21 deletions(-) diff --git a/docs/_static/plr_geometry_viewer.js b/docs/_static/plr_geometry_viewer.js index c1f81aed1f5..2135028ce0a 100644 --- a/docs/_static/plr_geometry_viewer.js +++ b/docs/_static/plr_geometry_viewer.js @@ -5,6 +5,25 @@ return Math.min(Math.max(value, min), max); } + function niceStep(span, targetCount) { + const raw = Math.max(span, 1) / Math.max(targetCount, 1); + const magnitude = Math.pow(10, Math.floor(Math.log10(raw))); + const normalized = raw / magnitude; + let factor; + if (normalized <= 1) { + factor = 1; + } else if (normalized <= 2) { + factor = 2; + } else if (normalized <= 5) { + factor = 5; + } else { + factor = 10; + } + return factor * magnitude; + } + + const AXIS_COLORS = { x: "#e84a4a", y: "#4caf3e", z: "#3b7dd8" }; + function colorForPrototype(prototype) { const geometry = prototype.geometry || {}; const type = prototype.type || ""; @@ -420,7 +439,7 @@ this.context = this.canvas.getContext("2d"); this.drawables = []; this.bounds = computeBounds([]); - this.rotation = { yaw: -0.75, pitch: 0.55 }; + this.rotation = { yaw: -0.95 + Math.PI / 2, pitch: -0.9 }; this.zoom = 1; this.isDragging = false; this.lastPointer = { x: 0, y: 0 }; @@ -553,36 +572,160 @@ ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); } - drawGround() { + drawRuler() { const ctx = this.context; - const span = this.bounds.span * 0.8; + const theme = readThemeColors(this.root); const groundZ = this.bounds.min.z; - const gridSize = Math.max(4, Math.min(12, Math.round(this.bounds.span / 20))); - ctx.lineWidth = 1; - ctx.save(); - ctx.strokeStyle = readThemeColors(this.root).text; - ctx.globalAlpha = 0.22; + const spanX = this.bounds.max.x - this.bounds.min.x; + const spanY = this.bounds.max.y - this.bounds.min.y; + const step = Math.max(niceStep(spanX, 8), niceStep(spanY, 8)); - for (let index = -gridSize; index <= gridSize; index += 1) { - const ratio = index / gridSize; - const x = this.bounds.center.x + ratio * span; - const y = this.bounds.center.y + ratio * span; + const x0 = Math.floor(this.bounds.min.x / step) * step; + const x1 = Math.ceil(this.bounds.max.x / step) * step; + const y0 = Math.floor(this.bounds.min.y / step) * step; + const y1 = Math.ceil(this.bounds.max.y / step) * step; + const epsilon = step * 1e-6; + const labelGap = step * 0.2; + + ctx.save(); + ctx.strokeStyle = theme.text; - const xLineStart = this.project({ x, y: this.bounds.center.y - span, z: groundZ }); - const xLineEnd = this.project({ x, y: this.bounds.center.y + span, z: groundZ }); + // Minor mm grid. + ctx.lineWidth = 1; + ctx.globalAlpha = 0.18; + for (let x = x0; x <= x1 + epsilon; x += step) { + const start = this.project({ x, y: y0, z: groundZ }); + const end = this.project({ x, y: y1, z: groundZ }); + ctx.beginPath(); + ctx.moveTo(start.x, start.y); + ctx.lineTo(end.x, end.y); + ctx.stroke(); + } + for (let y = y0; y <= y1 + epsilon; y += step) { + const start = this.project({ x: x0, y, z: groundZ }); + const end = this.project({ x: x1, y, z: groundZ }); ctx.beginPath(); - ctx.moveTo(xLineStart.x, xLineStart.y); - ctx.lineTo(xLineEnd.x, xLineEnd.y); + ctx.moveTo(start.x, start.y); + ctx.lineTo(end.x, end.y); ctx.stroke(); + } + + // Origin axes (Blender-style colors: X red, Y green). + ctx.globalAlpha = 0.9; + ctx.lineWidth = 3; + const xAxisStart = this.project({ x: x0, y: y0, z: groundZ }); + const xAxisEnd = this.project({ x: x1, y: y0, z: groundZ }); + ctx.strokeStyle = AXIS_COLORS.x; + ctx.beginPath(); + ctx.moveTo(xAxisStart.x, xAxisStart.y); + ctx.lineTo(xAxisEnd.x, xAxisEnd.y); + ctx.stroke(); + const yAxisEnd = this.project({ x: x0, y: y1, z: groundZ }); + ctx.strokeStyle = AXIS_COLORS.y; + ctx.beginPath(); + ctx.moveTo(xAxisStart.x, xAxisStart.y); + ctx.lineTo(yAxisEnd.x, yAxisEnd.y); + ctx.stroke(); + + // Tick labels in mm (matched to axis colors). + ctx.globalAlpha = 0.9; + ctx.font = "bold 12px system-ui, -apple-system, sans-serif"; + ctx.fillStyle = AXIS_COLORS.x; + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + for (let x = x0; x <= x1 + epsilon; x += step) { + const point = this.project({ x, y: y0 - labelGap, z: groundZ }); + ctx.fillText(String(Math.round(x)), point.x, point.y + 5); + } + ctx.fillStyle = AXIS_COLORS.y; + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + for (let y = y0; y <= y1 + epsilon; y += step) { + const point = this.project({ x: x0 - labelGap, y, z: groundZ - step * 0.2 }); + ctx.fillText(String(Math.round(y)), point.x - 7, point.y); + } - const yLineStart = this.project({ x: this.bounds.center.x - span, y, z: groundZ }); - const yLineEnd = this.project({ x: this.bounds.center.x + span, y, z: groundZ }); + // Axis unit captions. + ctx.globalAlpha = 1; + ctx.font = "bold 11px system-ui, -apple-system, sans-serif"; + const xCaption = this.project({ x: x1, y: y0, z: groundZ }); + ctx.fillStyle = AXIS_COLORS.x; + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillText("X (mm)", xCaption.x + 25, xCaption.y); + const yCaption = this.project({ x: x0, y: y1, z: groundZ }); + ctx.fillStyle = AXIS_COLORS.y; + ctx.textAlign = "right"; + ctx.textBaseline = "bottom"; + ctx.fillText("Y (mm)", yCaption.x - 7, yCaption.y - 22); + + ctx.restore(); + } + + drawZAxis() { + const z0 = this.bounds.min.z; + const z1 = this.bounds.max.z; + if (z1 - z0 < 1e-6) { + return; + } + + const step = niceStep(z1 - z0, 4); + const zStart = Math.floor(z0 / step) * step; + const zEnd = Math.ceil(z1 / step) * step; + const epsilon = step * 1e-6; + + // Share the origin corner with the X/Y axes (matplotlib/Blender convention) + // so all three meet at one point and Z rides the front silhouette edge + // rather than piercing the translucent body. + const gridStep = Math.max( + niceStep(this.bounds.max.x - this.bounds.min.x, 8), + niceStep(this.bounds.max.y - this.bounds.min.y, 8), + ); + const corner = { + x: Math.floor(this.bounds.min.x / gridStep) * gridStep, + y: Math.floor(this.bounds.min.y / gridStep) * gridStep, + }; + + const centerScreen = this.project({ + x: this.bounds.center.x, + y: this.bounds.center.y, + z: z0, + }); + const baseScreen = this.project({ x: corner.x, y: corner.y, z: z0 }); + const outwardSign = baseScreen.x >= centerScreen.x ? 1 : -1; + + const ctx = this.context; + ctx.save(); + ctx.strokeStyle = AXIS_COLORS.z; + ctx.fillStyle = AXIS_COLORS.z; + + ctx.globalAlpha = 0.9; + ctx.lineWidth = 3; + const axisBottom = this.project({ x: corner.x, y: corner.y, z: zStart }); + const axisTop = this.project({ x: corner.x, y: corner.y, z: zEnd }); + ctx.beginPath(); + ctx.moveTo(axisBottom.x, axisBottom.y); + ctx.lineTo(axisTop.x, axisTop.y); + ctx.stroke(); + + ctx.font = "bold 12px system-ui, -apple-system, sans-serif"; + ctx.textAlign = outwardSign > 0 ? "left" : "right"; + ctx.textBaseline = "middle"; + for (let z = zStart; z <= zEnd + epsilon; z += step) { + const point = this.project({ x: corner.x, y: corner.y, z }); ctx.beginPath(); - ctx.moveTo(yLineStart.x, yLineStart.y); - ctx.lineTo(yLineEnd.x, yLineEnd.y); + ctx.moveTo(point.x, point.y); + ctx.lineTo(point.x + outwardSign * 6, point.y); ctx.stroke(); + ctx.fillText(String(Math.round(z)), point.x + outwardSign * 10, point.y); } + + ctx.globalAlpha = 1; + ctx.font = "bold 11px system-ui, -apple-system, sans-serif"; + ctx.textBaseline = "bottom"; + ctx.fillText("Z (mm)", axisTop.x + outwardSign * 10, axisTop.y - 8); + ctx.restore(); } @@ -681,8 +824,9 @@ } this.drawBackground(); - this.drawGround(); + this.drawRuler(); this.drawDrawables(); + this.drawZAxis(); } } From 5863d60bacf7afbadcf3c1a166b72fb9f25ea14f Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sat, 16 May 2026 13:09:30 +0100 Subject: [PATCH 09/27] docs: add size readout, pan, reset, full pitch range, and tick decimation to 3D viewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a corner size readout (X×Y×Z mm + well/tip count), middle-drag / shift+left-drag panning, double-click view reset, open the interactive pitch clamp to the full ±90°, and skip overlapping tick labels when an axis goes near edge-on. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/_static/plr_geometry_viewer.js | 118 ++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 7 deletions(-) diff --git a/docs/_static/plr_geometry_viewer.js b/docs/_static/plr_geometry_viewer.js index 2135028ce0a..797273c80b0 100644 --- a/docs/_static/plr_geometry_viewer.js +++ b/docs/_static/plr_geometry_viewer.js @@ -24,6 +24,10 @@ const AXIS_COLORS = { x: "#e84a4a", y: "#4caf3e", z: "#3b7dd8" }; + // Minimum on-screen spacing (canvas px) between drawn tick labels; closer + // ones are skipped so they don't pile up when an axis goes near edge-on. + const TICK_MIN_GAP = 30; + function colorForPrototype(prototype) { const geometry = prototype.geometry || {}; const type = prototype.type || ""; @@ -439,15 +443,20 @@ this.context = this.canvas.getContext("2d"); this.drawables = []; this.bounds = computeBounds([]); - this.rotation = { yaw: -0.95 + Math.PI / 2, pitch: -0.9 }; + this.defaultRotation = { yaw: -0.95 + Math.PI / 2, pitch: -0.9 }; + this.rotation = { ...this.defaultRotation }; this.zoom = 1; + this.pan = { x: 0, y: 0 }; + this.pixelRatio = 1; this.isDragging = false; + this.dragMode = "orbit"; this.lastPointer = { x: 0, y: 0 }; this.handlePointerDown = this.handlePointerDown.bind(this); this.handlePointerMove = this.handlePointerMove.bind(this); this.handlePointerUp = this.handlePointerUp.bind(this); this.handleWheel = this.handleWheel.bind(this); + this.handleDoubleClick = this.handleDoubleClick.bind(this); this.handleResize = this.handleResize.bind(this); this.resize = this.resize.bind(this); @@ -455,6 +464,7 @@ window.addEventListener("pointermove", this.handlePointerMove); window.addEventListener("pointerup", this.handlePointerUp); this.canvas.addEventListener("wheel", this.handleWheel, { passive: false }); + this.canvas.addEventListener("dblclick", this.handleDoubleClick); window.addEventListener("resize", this.handleResize); if (typeof ResizeObserver !== "undefined") { this.resizeObserver = new ResizeObserver(this.handleResize); @@ -469,20 +479,38 @@ window.removeEventListener("pointermove", this.handlePointerMove); window.removeEventListener("pointerup", this.handlePointerUp); this.canvas.removeEventListener("wheel", this.handleWheel); + this.canvas.removeEventListener("dblclick", this.handleDoubleClick); window.removeEventListener("resize", this.handleResize); if (this.resizeObserver) { this.resizeObserver.disconnect(); } } + resetView() { + this.rotation = { ...this.defaultRotation }; + this.zoom = 1; + this.pan = { x: 0, y: 0 }; + this.render(); + } + + handleDoubleClick(event) { + event.preventDefault(); + this.resetView(); + } + setCatalog(catalog) { this.drawables = normalizeCatalog(catalog); this.bounds = computeBounds(this.drawables); this.zoom = 1; + this.pan = { x: 0, y: 0 }; this.render(); } handlePointerDown(event) { + if (event.button === 1) { + event.preventDefault(); // suppress middle-click autoscroll + } + this.dragMode = event.button === 1 || event.shiftKey ? "pan" : "orbit"; this.isDragging = true; this.lastPointer = { x: event.clientX, y: event.clientY }; this.canvas.setPointerCapture(event.pointerId); @@ -496,8 +524,18 @@ const deltaX = event.clientX - this.lastPointer.x; const deltaY = event.clientY - this.lastPointer.y; this.lastPointer = { x: event.clientX, y: event.clientY }; - this.rotation.yaw += deltaX * 0.01; - this.rotation.pitch = clamp(this.rotation.pitch + deltaY * 0.01, -1.4, 1.4); + if (this.dragMode === "pan") { + // pointer deltas are CSS px; project() outputs device px. + this.pan.x += deltaX * this.pixelRatio; + this.pan.y += deltaY * this.pixelRatio; + } else { + this.rotation.yaw += deltaX * 0.01; + this.rotation.pitch = clamp( + this.rotation.pitch + deltaY * 0.01, + -Math.PI / 2, + Math.PI / 2, + ); + } this.render(); } @@ -518,8 +556,9 @@ handleResize() { const width = Math.max(this.root.clientWidth, 200); const height = Math.max(this.root.clientHeight, 200); - this.canvas.width = Math.floor(width * Math.min(window.devicePixelRatio || 1, 2)); - this.canvas.height = Math.floor(height * Math.min(window.devicePixelRatio || 1, 2)); + this.pixelRatio = Math.min(window.devicePixelRatio || 1, 2); + this.canvas.width = Math.floor(width * this.pixelRatio); + this.canvas.height = Math.floor(height * this.pixelRatio); this.canvas.style.width = `${width}px`; this.canvas.style.height = `${height}px`; this.render(); @@ -556,8 +595,8 @@ const scaleBase = Math.min(this.canvas.width, this.canvas.height) / (this.bounds.span * 1.8); const scale = scaleBase * this.zoom; return { - x: this.canvas.width / 2 + pitched.x * scale, - y: this.canvas.height / 2 - pitched.y * scale, + x: this.canvas.width / 2 + pitched.x * scale + this.pan.x, + y: this.canvas.height / 2 - pitched.y * scale + this.pan.y, depth: pitched.z, }; } @@ -634,15 +673,31 @@ ctx.fillStyle = AXIS_COLORS.x; ctx.textAlign = "center"; ctx.textBaseline = "top"; + let lastXLabel = null; for (let x = x0; x <= x1 + epsilon; x += step) { const point = this.project({ x, y: y0 - labelGap, z: groundZ }); + if ( + lastXLabel !== null && + Math.hypot(point.x - lastXLabel.x, point.y - lastXLabel.y) < TICK_MIN_GAP + ) { + continue; + } + lastXLabel = point; ctx.fillText(String(Math.round(x)), point.x, point.y + 5); } ctx.fillStyle = AXIS_COLORS.y; ctx.textAlign = "right"; ctx.textBaseline = "middle"; + let lastYLabel = null; for (let y = y0; y <= y1 + epsilon; y += step) { const point = this.project({ x: x0 - labelGap, y, z: groundZ - step * 0.2 }); + if ( + lastYLabel !== null && + Math.hypot(point.x - lastYLabel.x, point.y - lastYLabel.y) < TICK_MIN_GAP + ) { + continue; + } + lastYLabel = point; ctx.fillText(String(Math.round(y)), point.x - 7, point.y); } @@ -712,8 +767,16 @@ ctx.font = "bold 12px system-ui, -apple-system, sans-serif"; ctx.textAlign = outwardSign > 0 ? "left" : "right"; ctx.textBaseline = "middle"; + let lastZLabel = null; for (let z = zStart; z <= zEnd + epsilon; z += step) { const point = this.project({ x: corner.x, y: corner.y, z }); + if ( + lastZLabel !== null && + Math.hypot(point.x - lastZLabel.x, point.y - lastZLabel.y) < TICK_MIN_GAP + ) { + continue; + } + lastZLabel = point; ctx.beginPath(); ctx.moveTo(point.x, point.y); ctx.lineTo(point.x + outwardSign * 6, point.y); @@ -827,6 +890,47 @@ this.drawRuler(); this.drawDrawables(); this.drawZAxis(); + this.drawSizeReadout(); + } + + drawSizeReadout() { + const ctx = this.context; + const sizeX = this.bounds.max.x - this.bounds.min.x; + const sizeY = this.bounds.max.y - this.bounds.min.y; + const sizeZ = this.bounds.max.z - this.bounds.min.z; + if (sizeX <= 0 && sizeY <= 0 && sizeZ <= 0) { + return; + } + + const fmt = (value) => (Math.round(value * 10) / 10).toFixed(1); + const lines = [`${fmt(sizeX)} × ${fmt(sizeY)} × ${fmt(sizeZ)} mm`]; + + const wells = this.drawables.filter( + (d) => d.prototype.geometry && d.prototype.geometry.shape === "well", + ).length; + const tips = this.drawables.filter( + (d) => d.prototype.geometry && d.prototype.geometry.shape === "tip_spot", + ).length; + if (wells > 0) { + lines.push(`${wells} well${wells === 1 ? "" : "s"}`); + } else if (tips > 0) { + lines.push(`${tips} tip${tips === 1 ? "" : "s"} spot${tips === 1 ? "" : "s"}`); + } + + ctx.save(); + ctx.textAlign = "left"; + ctx.textBaseline = "top"; + let y = 12; + lines.forEach((line, index) => { + ctx.font = index === 0 + ? "bold 13px system-ui, -apple-system, sans-serif" + : "12px system-ui, -apple-system, sans-serif"; + ctx.fillStyle = readThemeColors(this.root).text; + ctx.globalAlpha = index === 0 ? 0.95 : 0.7; + ctx.fillText(line, 14, y); + y += index === 0 ? 19 : 16; + }); + ctx.restore(); } } From 8de6911d1f2e3988c9219ea6de262f4a352561d8 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sat, 16 May 2026 14:42:44 +0100 Subject: [PATCH 10/27] docs: add 3D viewer home button, axis tick marks, closer default zoom Add a Visualizer-style reset/home button overlaid top-right of the 3D stage (same action as double-click), short tick marks on the X/Y axes, a tighter default framing (model fills more of the canvas), and bump the size readout to 2 decimal places. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/_static/plr_geometry_viewer.js | 47 ++++++++++++++++++++++++++-- docs/_static/plr_labware_catalog.css | 35 +++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/docs/_static/plr_geometry_viewer.js b/docs/_static/plr_geometry_viewer.js index 797273c80b0..84e5d120d5d 100644 --- a/docs/_static/plr_geometry_viewer.js +++ b/docs/_static/plr_geometry_viewer.js @@ -441,6 +441,16 @@ this.root.innerHTML = ""; this.root.appendChild(this.canvas); this.context = this.canvas.getContext("2d"); + + this.homeButton = document.createElement("button"); + this.homeButton.type = "button"; + this.homeButton.className = "plr-geometry-home"; + this.homeButton.title = "Reset view"; + this.homeButton.setAttribute("aria-label", "Reset view"); + this.homeButton.innerHTML = + ''; + this.root.appendChild(this.homeButton); this.drawables = []; this.bounds = computeBounds([]); this.defaultRotation = { yaw: -0.95 + Math.PI / 2, pitch: -0.9 }; @@ -457,6 +467,7 @@ this.handlePointerUp = this.handlePointerUp.bind(this); this.handleWheel = this.handleWheel.bind(this); this.handleDoubleClick = this.handleDoubleClick.bind(this); + this.handleHomeClick = this.handleHomeClick.bind(this); this.handleResize = this.handleResize.bind(this); this.resize = this.resize.bind(this); @@ -465,6 +476,7 @@ window.addEventListener("pointerup", this.handlePointerUp); this.canvas.addEventListener("wheel", this.handleWheel, { passive: false }); this.canvas.addEventListener("dblclick", this.handleDoubleClick); + this.homeButton.addEventListener("click", this.handleHomeClick); window.addEventListener("resize", this.handleResize); if (typeof ResizeObserver !== "undefined") { this.resizeObserver = new ResizeObserver(this.handleResize); @@ -480,6 +492,7 @@ window.removeEventListener("pointerup", this.handlePointerUp); this.canvas.removeEventListener("wheel", this.handleWheel); this.canvas.removeEventListener("dblclick", this.handleDoubleClick); + this.homeButton.removeEventListener("click", this.handleHomeClick); window.removeEventListener("resize", this.handleResize); if (this.resizeObserver) { this.resizeObserver.disconnect(); @@ -498,6 +511,14 @@ this.resetView(); } + handleHomeClick() { + this.resetView(); + this.homeButton.classList.add("plr-geometry-home--active"); + window.setTimeout(() => { + this.homeButton.classList.remove("plr-geometry-home--active"); + }, 220); + } + setCatalog(catalog) { this.drawables = normalizeCatalog(catalog); this.bounds = computeBounds(this.drawables); @@ -592,7 +613,7 @@ z: yawed.y * sinPitch + yawed.z * cosPitch, }; - const scaleBase = Math.min(this.canvas.width, this.canvas.height) / (this.bounds.span * 1.8); + const scaleBase = Math.min(this.canvas.width, this.canvas.height) / (this.bounds.span * 1.4); const scale = scaleBase * this.zoom; return { x: this.canvas.width / 2 + pitched.x * scale + this.pan.x, @@ -667,6 +688,28 @@ ctx.lineTo(yAxisEnd.x, yAxisEnd.y); ctx.stroke(); + // Tick marks: short stubs pointing outward toward each axis's labels. + ctx.lineWidth = 2; + const tickLen = step * 0.12; + ctx.strokeStyle = AXIS_COLORS.x; + for (let x = x0; x <= x1 + epsilon; x += step) { + const tickStart = this.project({ x, y: y0, z: groundZ }); + const tickEnd = this.project({ x, y: y0 - tickLen, z: groundZ }); + ctx.beginPath(); + ctx.moveTo(tickStart.x, tickStart.y); + ctx.lineTo(tickEnd.x, tickEnd.y); + ctx.stroke(); + } + ctx.strokeStyle = AXIS_COLORS.y; + for (let y = y0; y <= y1 + epsilon; y += step) { + const tickStart = this.project({ x: x0, y, z: groundZ }); + const tickEnd = this.project({ x: x0 - tickLen, y, z: groundZ }); + ctx.beginPath(); + ctx.moveTo(tickStart.x, tickStart.y); + ctx.lineTo(tickEnd.x, tickEnd.y); + ctx.stroke(); + } + // Tick labels in mm (matched to axis colors). ctx.globalAlpha = 0.9; ctx.font = "bold 12px system-ui, -apple-system, sans-serif"; @@ -902,7 +945,7 @@ return; } - const fmt = (value) => (Math.round(value * 10) / 10).toFixed(1); + const fmt = (value) => (Math.round(value * 100) / 100).toFixed(2); const lines = [`${fmt(sizeX)} × ${fmt(sizeY)} × ${fmt(sizeZ)} mm`]; const wells = this.drawables.filter( diff --git a/docs/_static/plr_labware_catalog.css b/docs/_static/plr_labware_catalog.css index 569f6e8cbb0..09428ac6050 100644 --- a/docs/_static/plr_labware_catalog.css +++ b/docs/_static/plr_labware_catalog.css @@ -246,6 +246,41 @@ .plr-library-modal__stage { min-height: 420px; + position: relative; +} + +.plr-geometry-home { + position: absolute; + top: 14px; + right: 14px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px; + border: 0; + border-radius: 6px; + cursor: pointer; + color: var(--pst-color-text-base); + opacity: 0.55; + background: rgba(255, 255, 255, 0.75); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px); + line-height: 0; + transition: opacity 0.15s, transform 0.15s, background 0.15s, box-shadow 0.15s; + z-index: 5; +} + +.plr-geometry-home:hover { + opacity: 1; + transform: scale(1.12); + background: rgba(0, 255, 255, 0.4); + box-shadow: 0 0 10px rgba(0, 255, 255, 0.8); +} + +.plr-geometry-home--active { + opacity: 1; + background: rgba(255, 230, 0, 0.65); + box-shadow: 0 0 12px rgba(255, 230, 0, 0.9); } .plr-geometry-canvas { From 38d68904e44cae7600a33e0841034965597a5417 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sat, 16 May 2026 15:35:52 +0100 Subject: [PATCH 11/27] docs: add chamfered orientation cube with on-face labels to 3D viewer Add a non-interactive Fusion-style orientation cube (top-right, beside the home button) that mirrors the camera: chamfered corners, octagon faces with labels painted flat onto each surface, and the "Tip Spots" readout wording fixed. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/_static/plr_geometry_viewer.js | 182 +++++++++++++++++++++++++++- 1 file changed, 181 insertions(+), 1 deletion(-) diff --git a/docs/_static/plr_geometry_viewer.js b/docs/_static/plr_geometry_viewer.js index 84e5d120d5d..d3dcd5ee241 100644 --- a/docs/_static/plr_geometry_viewer.js +++ b/docs/_static/plr_geometry_viewer.js @@ -934,6 +934,7 @@ this.drawDrawables(); this.drawZAxis(); this.drawSizeReadout(); + this.drawOrientationCube(); } drawSizeReadout() { @@ -957,7 +958,7 @@ if (wells > 0) { lines.push(`${wells} well${wells === 1 ? "" : "s"}`); } else if (tips > 0) { - lines.push(`${tips} tip${tips === 1 ? "" : "s"} spot${tips === 1 ? "" : "s"}`); + lines.push(`${tips} Tip Spot${tips === 1 ? "" : "s"}`); } ctx.save(); @@ -975,6 +976,185 @@ }); ctx.restore(); } + + drawOrientationCube() { + const ctx = this.context; + const dpr = this.pixelRatio; + const halfCss = 30; // half widget size in CSS px + const marginCss = 16; + const topCss = 14; // top-right corner, right of the home button + const cx = this.canvas.width - (marginCss + halfCss) * dpr; + const cy = (topCss + halfCss) * dpr; + const s = halfCss * dpr * 0.6; // projected half-extent of the cube + + const cosYaw = Math.cos(this.rotation.yaw); + const sinYaw = Math.sin(this.rotation.yaw); + const cosPitch = Math.cos(this.rotation.pitch); + const sinPitch = Math.sin(this.rotation.pitch); + + const projectCubePoint = (p) => { + const yx = p.x * cosYaw - p.y * sinYaw; + const yy = p.x * sinYaw + p.y * cosYaw; + const py = yy * cosPitch - p.z * sinPitch; + const pz = yy * sinPitch + p.z * cosPitch; + return { x: cx + yx * s, y: cy - py * s, depth: pz }; + }; + + const add = (a, b, t) => ({ + x: a.x + b.x * t, + y: a.y + b.y * t, + z: a.z + b.z * t, + }); + const depthAt = (p) => projectCubePoint(p).depth; + + // Chamfered cube: each corner cut by a plane -> 6 octagon faces + 8 + // corner triangles. `chamfer` is the fraction of each edge removed. + const chamfer = 0.504; + const k = 1 - chamfer; + const ring = [ + [1, k], [k, 1], [-k, 1], [-1, k], + [-1, -k], [-k, -1], [k, -1], [1, -k], + ]; + // (b, d) in-plane coords -> 3D, per axis (0=x,1=y,2=z), at axis=sg. + const place = { + 0: (sg, b, d) => ({ x: sg, y: b, z: d }), + 1: (sg, b, d) => ({ x: d, y: sg, z: b }), + 2: (sg, b, d) => ({ x: b, y: d, z: sg }), + }; + const inPlaneU = { 0: { x: 0, y: 1, z: 0 }, 1: { x: 1, y: 0, z: 0 }, 2: { x: 1, y: 0, z: 0 } }; + const inPlaneW = { 0: { x: 0, y: 0, z: 1 }, 1: { x: 0, y: 0, z: 1 }, 2: { x: 0, y: 1, z: 0 } }; + const axisNormal = (axis, sg) => ({ + x: axis === 0 ? sg : 0, + y: axis === 1 ? sg : 0, + z: axis === 2 ? sg : 0, + }); + + const faces = []; + + [ + { axis: 2, sg: 1, label: "TOP", color: AXIS_COLORS.z }, + { axis: 2, sg: -1, label: "BOTTOM", color: AXIS_COLORS.z }, + { axis: 0, sg: 1, label: "RIGHT", color: AXIS_COLORS.x }, + { axis: 0, sg: -1, label: "LEFT", color: AXIS_COLORS.x }, + { axis: 1, sg: 1, label: "BACK", color: AXIS_COLORS.y }, + { axis: 1, sg: -1, label: "FRONT", color: AXIS_COLORS.y }, + ].forEach((f) => { + const pts3 = ring.map(([b, d]) => place[f.axis](f.sg, b, d)); + const center3 = axisNormal(f.axis, f.sg); + const normal = center3; + const visible = depthAt(add(center3, normal, 0.02)) > depthAt(center3); + const pts = pts3.map(projectCubePoint); + const depth = pts.reduce((sum, p) => sum + p.depth, 0) / pts.length; + faces.push({ + kind: "oct", + label: f.label, + color: f.color, + pts, + depth, + visible, + center3, + uW: inPlaneU[f.axis], + wW: inPlaneW[f.axis], + }); + }); + + [-1, 1].forEach((sx) => { + [-1, 1].forEach((sy) => { + [-1, 1].forEach((sz) => { + const pts3 = [ + { x: sx * k, y: sy, z: sz }, + { x: sx, y: sy * k, z: sz }, + { x: sx, y: sy, z: sz * k }, + ]; + const centroid = { + x: (pts3[0].x + pts3[1].x + pts3[2].x) / 3, + y: (pts3[0].y + pts3[1].y + pts3[2].y) / 3, + z: (pts3[0].z + pts3[1].z + pts3[2].z) / 3, + }; + const inv = 1 / Math.sqrt(3); + const normal = { x: sx * inv, y: sy * inv, z: sz * inv }; + const visible = depthAt(add(centroid, normal, 0.02)) > depthAt(centroid); + const pts = pts3.map(projectCubePoint); + const depth = pts.reduce((sum, p) => sum + p.depth, 0) / 3; + faces.push({ kind: "chamfer", pts, depth, visible }); + }); + }); + }); + + faces.sort((a, b) => a.depth - b.depth); + + const theme = readThemeColors(this.root); + ctx.save(); + ctx.lineJoin = "round"; + faces.forEach((f) => { + ctx.beginPath(); + ctx.moveTo(f.pts[0].x, f.pts[0].y); + for (let i = 1; i < f.pts.length; i += 1) { + ctx.lineTo(f.pts[i].x, f.pts[i].y); + } + ctx.closePath(); + + if (f.kind === "chamfer") { + ctx.globalAlpha = f.visible ? 0.5 : 0.12; + ctx.fillStyle = "rgba(150, 165, 180, 0.9)"; + ctx.fill(); + ctx.globalAlpha = f.visible ? 0.45 : 0.12; + ctx.lineWidth = 1; + ctx.strokeStyle = "rgba(120, 135, 150, 0.9)"; + ctx.stroke(); + return; + } + + ctx.globalAlpha = f.visible ? 0.82 : 0.18; + ctx.fillStyle = "rgba(255, 255, 255, 0.85)"; + ctx.fill(); + ctx.globalAlpha = f.visible ? 0.9 : 0.25; + ctx.lineWidth = Math.max(1, dpr); + ctx.strokeStyle = f.color; + ctx.stroke(); + + if (!f.visible) { + return; + } + const fc = projectCubePoint(f.center3); + const pU = projectCubePoint(add(f.center3, f.uW, 1)); + const pW = projectCubePoint(add(f.center3, f.wW, 1)); + let ux = pU.x - fc.x; + let uy = pU.y - fc.y; + let vx = pW.x - fc.x; + let vy = pW.y - fc.y; + const exLen = Math.hypot(ux, uy); + const eyLen = Math.hypot(vx, vy); + if (exLen > 4 && eyLen > 4) { + // Unit in-plane basis keeps the face's shear/foreshorten so the + // label reads as painted on the surface; sign-normalize so it + // stays roughly upright rather than mirrored/upside-down. + ux /= exLen; + uy /= exLen; + vx /= eyLen; + vy /= eyLen; + if (ux < 0) { + ux = -ux; + uy = -uy; + } + if (vy < 0) { + vx = -vx; + vy = -vy; + } + const fontPx = Math.min(exLen, eyLen) * 0.486; + ctx.save(); + ctx.setTransform(ux, uy, vx, vy, fc.x, fc.y); + ctx.globalAlpha = 0.95; + ctx.fillStyle = theme.text; + ctx.font = `bold ${fontPx}px system-ui, -apple-system, sans-serif`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(f.label, 0, 0); + ctx.restore(); + } + }); + ctx.restore(); + } } window.PLRGeometryViewer = { From a5ca148285cc62fea91d00488af625907df6b408 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sat, 16 May 2026 15:35:52 +0100 Subject: [PATCH 12/27] docs: rename catalog "Vendor" to "Manufacturer" Rename the field end-to-end (extension JSON key, JS state/URL param, filter label, card class, CSS) since the value is sourced from each library page's manufacturer title. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/_exts/pylabrobot_labware_catalog.py | 4 ++-- docs/_static/plr_labware_catalog.css | 4 ++-- docs/_static/plr_labware_catalog.js | 28 ++++++++++++------------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/_exts/pylabrobot_labware_catalog.py b/docs/_exts/pylabrobot_labware_catalog.py index 9864ee885a1..eb129247a46 100644 --- a/docs/_exts/pylabrobot_labware_catalog.py +++ b/docs/_exts/pylabrobot_labware_catalog.py @@ -75,7 +75,7 @@ def _extract_labware_entries_from_markdown( doc_relative_path: Path, ) -> List[Dict[str, Any]]: entries: List[Dict[str, Any]] = [] - vendor = _page_title(markdown, doc_relative_path.stem.replace("_", " ").title()) + manufacturer = _page_title(markdown, doc_relative_path.stem.replace("_", " ").title()) current_section = "" for line in markdown.splitlines(): @@ -100,7 +100,7 @@ def _extract_labware_entries_from_markdown( for definition_name in matches: entries.append({ "definition": definition_name, - "vendor": vendor, + "manufacturer": manufacturer, "section": current_section, "description_html": _description_to_html(cells[0], definition_name), "image": image_path, diff --git a/docs/_static/plr_labware_catalog.css b/docs/_static/plr_labware_catalog.css index 09428ac6050..415ace32d8b 100644 --- a/docs/_static/plr_labware_catalog.css +++ b/docs/_static/plr_labware_catalog.css @@ -113,7 +113,7 @@ padding: 0.95rem; } -.plr-library-card__vendor { +.plr-library-card__manufacturer { color: var(--pst-color-text-muted); font-size: 0.76rem; font-weight: 700; @@ -252,7 +252,7 @@ .plr-geometry-home { position: absolute; top: 14px; - right: 14px; + right: 90px; display: inline-flex; align-items: center; justify-content: center; diff --git a/docs/_static/plr_labware_catalog.js b/docs/_static/plr_labware_catalog.js index 37ff0e80c36..a7eb0441c7e 100644 --- a/docs/_static/plr_labware_catalog.js +++ b/docs/_static/plr_labware_catalog.js @@ -4,7 +4,7 @@ const state = { index: null, query: "", - vendor: "All", + manufacturer: "All", section: "All", selectedDefinition: null, viewer: null, @@ -50,7 +50,7 @@ function itemMatchesFilters(item) { const haystack = [ item.definition, - item.vendor, + item.manufacturer, item.section, item.description_html, ] @@ -60,7 +60,7 @@ if (state.query && haystack.indexOf(state.query.toLowerCase()) === -1) { return false; } - if (state.vendor !== "All" && item.vendor !== state.vendor) { + if (state.manufacturer !== "All" && item.manufacturer !== state.manufacturer) { return false; } if (state.section !== "All" && item.section !== state.section) { @@ -168,7 +168,7 @@ media.addEventListener("click", () => openModel(item.definition)); const body = element("div", "plr-library-card__body"); - const vendor = element("div", "plr-library-card__vendor", item.vendor); + const manufacturer = element("div", "plr-library-card__manufacturer", item.manufacturer); const title = element("h3", "plr-library-card__title"); const titleCode = document.createElement("code"); titleCode.textContent = item.definition; @@ -184,7 +184,7 @@ modelButton.addEventListener("click", () => openModel(item.definition)); footer.appendChild(modelButton); - body.appendChild(vendor); + body.appendChild(manufacturer); body.appendChild(title); body.appendChild(section); if (item.description_html) body.appendChild(description); @@ -238,8 +238,8 @@
- - + +
@@ -251,9 +251,9 @@ `; const search = root.querySelector("#plr-catalog-search-input"); - const vendor = root.querySelector("#plr-catalog-vendor"); + const manufacturer = root.querySelector("#plr-catalog-manufacturer"); const section = root.querySelector("#plr-catalog-section"); - setSelectOptions(vendor, unique(state.index.items.map((item) => item.vendor)), state.vendor); + setSelectOptions(manufacturer, unique(state.index.items.map((item) => item.manufacturer)), state.manufacturer); setSelectOptions(section, unique(state.index.items.map((item) => item.section)), state.section); search.value = state.query; @@ -262,8 +262,8 @@ writeUrlState(); renderCatalog(root); }); - vendor.addEventListener("change", () => { - state.vendor = vendor.value; + manufacturer.addEventListener("change", () => { + state.manufacturer = manufacturer.value; writeUrlState(); renderCatalog(root); }); @@ -279,7 +279,7 @@ function readUrlState() { const params = new URLSearchParams(window.location.search); state.query = params.get("q") || ""; - state.vendor = params.get("vendor") || "All"; + state.manufacturer = params.get("manufacturer") || "All"; state.section = params.get("section") || "All"; } @@ -287,8 +287,8 @@ const params = new URLSearchParams(window.location.search); if (state.query) params.set("q", state.query); else params.delete("q"); - if (state.vendor && state.vendor !== "All") params.set("vendor", state.vendor); - else params.delete("vendor"); + if (state.manufacturer && state.manufacturer !== "All") params.set("manufacturer", state.manufacturer); + else params.delete("manufacturer"); if (state.section && state.section !== "All") params.set("section", state.section); else params.delete("section"); const queryString = params.toString(); From ff8083daebaa27938486349b94cfbdc16ef3de3e Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sat, 16 May 2026 15:54:51 +0100 Subject: [PATCH 13/27] docs: depth-sort plate outline against wells in 3D viewer Decompose the plate wireframe into subdivided edge segments merged into the depth-sorted draw list (instead of a flat final overlay), so wells correctly occlude the tray's back/bottom edges. Also enlarge the orientation cube slightly. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/_static/plr_geometry_viewer.js | 111 ++++++++++++++++------------ 1 file changed, 63 insertions(+), 48 deletions(-) diff --git a/docs/_static/plr_geometry_viewer.js b/docs/_static/plr_geometry_viewer.js index d3dcd5ee241..f4c5e6f516d 100644 --- a/docs/_static/plr_geometry_viewer.js +++ b/docs/_static/plr_geometry_viewer.js @@ -836,15 +836,19 @@ } drawDrawables() { - const faces = []; - const outlines = []; + const items = []; + // Outline segments share this layer so they depth-interleave with the + // wells/tip-spots (Well/TipSpot get layer 4 in drawableLayer()) instead + // of being painted on top as a flat overlay. + const outlineLayer = 4; this.drawables.forEach((drawable) => { drawable.faces.forEach((face) => { const projected = face.map((vertexIndex) => this.project(drawable.vertices[vertexIndex])); const depth = projected.reduce((sum, point) => sum + point.depth, 0) / Math.max(projected.length, 1); - faces.push({ + items.push({ + kind: "poly", points: projected, depth, color: drawable.color, @@ -854,16 +858,45 @@ }); if (drawable.outline) { - outlines.push({ - top: drawable.outline.top.map((point) => this.project(point)), - bottom: drawable.outline.bottom.map((point) => this.project(point)), - verticals: drawable.outline.verticals.map((pair) => pair.map((point) => this.project(point))), - color: shadeColor(drawable.color, 0.42), - }); + const color = shadeColor(drawable.color, 0.42); + // Subdivide each edge so it depth-sorts against the well field in + // many short pieces (one average depth per whole edge can't be + // partly-behind / partly-in-front of 96 wells). Still an + // approximation -- exact occlusion would need a depth buffer. + const SUBDIV = 18; + const addSegment = (a, b, width) => { + let prev = this.project(a); + for (let k = 1; k <= SUBDIV; k += 1) { + const t = k / SUBDIV; + const next = this.project({ + x: a.x + (b.x - a.x) * t, + y: a.y + (b.y - a.y) * t, + z: a.z + (b.z - a.z) * t, + }); + items.push({ + kind: "stroke", + a: prev, + b: next, + depth: (prev.depth + next.depth) / 2, + color, + width, + layer: outlineLayer, + }); + prev = next; + } + }; + const ring = (pts, width) => { + for (let i = 0; i < pts.length; i += 1) { + addSegment(pts[i], pts[(i + 1) % pts.length], width); + } + }; + ring(drawable.outline.top, 3); + ring(drawable.outline.bottom, 1.5); + drawable.outline.verticals.forEach((pair) => addSegment(pair[0], pair[1], 1.5)); } }); - faces.sort((left, right) => { + items.sort((left, right) => { if (left.layer !== right.layer) { return left.layer - right.layer; } @@ -872,20 +905,31 @@ const ctx = this.context; const edgeColor = readThemeColors(this.root).text; - faces.forEach((face) => { + items.forEach((item) => { + if (item.kind === "stroke") { + ctx.globalAlpha = 1; + ctx.strokeStyle = item.color; + ctx.lineWidth = item.width; + ctx.beginPath(); + ctx.moveTo(item.a.x, item.a.y); + ctx.lineTo(item.b.x, item.b.y); + ctx.stroke(); + return; + } + const shade = - face.points.length >= 4 - ? shadeColor(face.color, 0.9 + (face.depth / (this.bounds.span || 1)) * 0.15) - : face.color; + item.points.length >= 4 + ? shadeColor(item.color, 0.9 + (item.depth / (this.bounds.span || 1)) * 0.15) + : item.color; ctx.beginPath(); - ctx.moveTo(face.points[0].x, face.points[0].y); - for (let index = 1; index < face.points.length; index += 1) { - ctx.lineTo(face.points[index].x, face.points[index].y); + ctx.moveTo(item.points[0].x, item.points[0].y); + for (let index = 1; index < item.points.length; index += 1) { + ctx.lineTo(item.points[index].x, item.points[index].y); } ctx.closePath(); ctx.fillStyle = shade; - ctx.globalAlpha = face.alpha; + ctx.globalAlpha = item.alpha; ctx.fill(); ctx.globalAlpha = 0.28; ctx.strokeStyle = edgeColor; @@ -893,35 +937,6 @@ ctx.stroke(); ctx.globalAlpha = 1; }); - - outlines.forEach((outline) => { - ctx.strokeStyle = outline.color; - ctx.lineWidth = 3; - - ctx.beginPath(); - ctx.moveTo(outline.top[0].x, outline.top[0].y); - for (let index = 1; index < outline.top.length; index += 1) { - ctx.lineTo(outline.top[index].x, outline.top[index].y); - } - ctx.closePath(); - ctx.stroke(); - - ctx.lineWidth = 1.5; - ctx.beginPath(); - ctx.moveTo(outline.bottom[0].x, outline.bottom[0].y); - for (let index = 1; index < outline.bottom.length; index += 1) { - ctx.lineTo(outline.bottom[index].x, outline.bottom[index].y); - } - ctx.closePath(); - ctx.stroke(); - - outline.verticals.forEach((pair) => { - ctx.beginPath(); - ctx.moveTo(pair[0].x, pair[0].y); - ctx.lineTo(pair[1].x, pair[1].y); - ctx.stroke(); - }); - }); } render() { @@ -980,7 +995,7 @@ drawOrientationCube() { const ctx = this.context; const dpr = this.pixelRatio; - const halfCss = 30; // half widget size in CSS px + const halfCss = 31.5; // half widget size in CSS px const marginCss = 16; const topCss = 14; // top-right corner, right of the home button const cx = this.canvas.width - (marginCss + halfCss) * dpr; From 6821f7f5eeac14591a0976f6159d98b8320ed20c Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sat, 16 May 2026 16:07:55 +0100 Subject: [PATCH 14/27] docs: cache per-frame projection and coalesce 3D viewer renders project() recomputed yaw/pitch sin-cos and allocated three temp objects on every projected vertex. Hoist the camera transform into a once-per-frame cache and reuse it; the math and output are identical. On the heaviest catalog resources (384-well plates, ~41.8k project() calls/frame) this removes ~167k trig calls and ~125k allocations per frame, mainly reducing GC pressure / jank potential during a drag. Also route the high-frequency handlers (pointermove, wheel, resize) through requestAnimationFrame so a burst of events coalesces to at most one render per animation frame instead of one synchronous full-scene render per event. This is low-risk hygiene, not a headline speedup: the dominant frame cost is canvas rasterization of thousands of translucent polygons, which is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/_static/plr_geometry_viewer.js | 81 ++++++++++++++++++----------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/docs/_static/plr_geometry_viewer.js b/docs/_static/plr_geometry_viewer.js index f4c5e6f516d..0e0ef7b5f2c 100644 --- a/docs/_static/plr_geometry_viewer.js +++ b/docs/_static/plr_geometry_viewer.js @@ -458,6 +458,8 @@ this.zoom = 1; this.pan = { x: 0, y: 0 }; this.pixelRatio = 1; + this._proj = null; // per-frame projection cache (see _updateProjection) + this._rafId = null; // pending requestAnimationFrame, for render coalescing this.isDragging = false; this.dragMode = "orbit"; this.lastPointer = { x: 0, y: 0 }; @@ -497,6 +499,10 @@ if (this.resizeObserver) { this.resizeObserver.disconnect(); } + if (this._rafId !== null) { + window.cancelAnimationFrame(this._rafId); + this._rafId = null; + } } resetView() { @@ -557,7 +563,7 @@ Math.PI / 2, ); } - this.render(); + this.requestRender(); } handlePointerUp(event) { @@ -571,7 +577,7 @@ event.preventDefault(); const zoomDelta = event.deltaY > 0 ? 0.92 : 1.08; this.zoom = clamp(this.zoom * zoomDelta, 0.25, 6); - this.render(); + this.requestRender(); } handleResize() { @@ -582,43 +588,47 @@ this.canvas.height = Math.floor(height * this.pixelRatio); this.canvas.style.width = `${width}px`; this.canvas.style.height = `${height}px`; - this.render(); + this.requestRender(); } resize() { this.handleResize(); } - project(point) { - const centered = { - x: point.x - this.bounds.center.x, - y: point.y - this.bounds.center.y, - z: point.z - this.bounds.center.z, - }; - - const cosYaw = Math.cos(this.rotation.yaw); - const sinYaw = Math.sin(this.rotation.yaw); - const cosPitch = Math.cos(this.rotation.pitch); - const sinPitch = Math.sin(this.rotation.pitch); - - const yawed = { - x: centered.x * cosYaw - centered.y * sinYaw, - y: centered.x * sinYaw + centered.y * cosYaw, - z: centered.z, - }; - - const pitched = { - x: yawed.x, - y: yawed.y * cosPitch - yawed.z * sinPitch, - z: yawed.y * sinPitch + yawed.z * cosPitch, + // Per-frame transform constants. The camera is fixed for a whole render, + // so the trig/scale are computed once here instead of per projected vertex. + _updateProjection() { + const c = this.bounds.center; + this._proj = { + cx: c.x, + cy: c.y, + cz: c.z, + cosYaw: Math.cos(this.rotation.yaw), + sinYaw: Math.sin(this.rotation.yaw), + cosPitch: Math.cos(this.rotation.pitch), + sinPitch: Math.sin(this.rotation.pitch), + scale: + (Math.min(this.canvas.width, this.canvas.height) / (this.bounds.span * 1.4)) * this.zoom, + halfW: this.canvas.width / 2, + halfH: this.canvas.height / 2, + panX: this.pan.x, + panY: this.pan.y, }; + } - const scaleBase = Math.min(this.canvas.width, this.canvas.height) / (this.bounds.span * 1.4); - const scale = scaleBase * this.zoom; + project(point) { + const p = this._proj || (this._updateProjection(), this._proj); + const cx = point.x - p.cx; + const cy = point.y - p.cy; + const cz = point.z - p.cz; + const yawedX = cx * p.cosYaw - cy * p.sinYaw; + const yawedY = cx * p.sinYaw + cy * p.cosYaw; + const pitchedY = yawedY * p.cosPitch - cz * p.sinPitch; + const pitchedZ = yawedY * p.sinPitch + cz * p.cosPitch; return { - x: this.canvas.width / 2 + pitched.x * scale + this.pan.x, - y: this.canvas.height / 2 - pitched.y * scale + this.pan.y, - depth: pitched.z, + x: p.halfW + yawedX * p.scale + p.panX, + y: p.halfH - pitchedY * p.scale + p.panY, + depth: pitchedZ, }; } @@ -939,11 +949,22 @@ }); } + requestRender() { + if (this._rafId !== null) { + return; + } + this._rafId = window.requestAnimationFrame(() => { + this._rafId = null; + this.render(); + }); + } + render() { if (!this.context) { return; } + this._updateProjection(); this.drawBackground(); this.drawRuler(); this.drawDrawables(); From c6ac9c56a06a83e93065b06727c7c34daeddc7de Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sat, 16 May 2026 16:29:34 +0100 Subject: [PATCH 15/27] docs: render well bottom shapes and recolor orientation cube Circular wells now render their bottom type from the catalog data (flat = flat cylinder, V = shallow cone over the lower 20%, U = rounded cap) instead of always flat. Also color the orientation-cube faces by the axis their long edge spans (FRONT/BACK red = X, LEFT/RIGHT green = Y, TOP/BOTTOM blue = Z). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/_static/plr_geometry_viewer.js | 115 +++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 4 deletions(-) diff --git a/docs/_static/plr_geometry_viewer.js b/docs/_static/plr_geometry_viewer.js index 0e0ef7b5f2c..32a367cac71 100644 --- a/docs/_static/plr_geometry_viewer.js +++ b/docs/_static/plr_geometry_viewer.js @@ -259,12 +259,117 @@ return { vertices, faces }; } + // V-bottom: straight upper cylinder + cone tapering to a centre apex. + function createVBottomGeometry(size, segments) { + const sx = size[0]; + const sy = size[1]; + const sz = size[2]; + const radiusX = sx / 2; + const radiusY = sy / 2; + const centerX = sx / 2; + const centerY = sy / 2; + const coneTopZ = sz * 0.2; // lower 20% of the depth is the V taper + const vertices = []; + const shoulder = []; + const top = []; + + for (let index = 0; index < segments; index += 1) { + const theta = (Math.PI * 2 * index) / segments; + const x = centerX + Math.cos(theta) * radiusX; + const y = centerY + Math.sin(theta) * radiusY; + shoulder.push(vertices.length); + vertices.push({ x, y, z: coneTopZ }); + top.push(vertices.length); + vertices.push({ x, y, z: sz }); + } + const apex = vertices.length; + vertices.push({ x: centerX, y: centerY, z: 0 }); + + const faces = [top.slice()]; + for (let index = 0; index < segments; index += 1) { + const next = (index + 1) % segments; + faces.push([shoulder[index], shoulder[next], top[next], top[index]]); + faces.push([apex, shoulder[next], shoulder[index]]); + } + return { vertices, faces }; + } + + // U-bottom: straight upper cylinder + a rounded (quarter-ellipse) cap. + function createUBottomGeometry(size, segments, lat) { + const sx = size[0]; + const sy = size[1]; + const sz = size[2]; + const radiusX = sx / 2; + const radiusY = sy / 2; + const centerX = sx / 2; + const centerY = sy / 2; + const capH = Math.min((radiusX + radiusY) / 2, sz * 0.6); + const vertices = []; + const rings = []; + + for (let r = 1; r <= lat; r += 1) { + const tt = r / lat; // fraction up the cap, 1 == equator + const z = capH * tt; + const factor = Math.sin((tt * Math.PI) / 2); // 0 -> full radius + const ring = []; + for (let index = 0; index < segments; index += 1) { + const theta = (Math.PI * 2 * index) / segments; + ring.push(vertices.length); + vertices.push({ + x: centerX + Math.cos(theta) * radiusX * factor, + y: centerY + Math.sin(theta) * radiusY * factor, + z, + }); + } + rings.push(ring); + } + const top = []; + for (let index = 0; index < segments; index += 1) { + const theta = (Math.PI * 2 * index) / segments; + top.push(vertices.length); + vertices.push({ + x: centerX + Math.cos(theta) * radiusX, + y: centerY + Math.sin(theta) * radiusY, + z: sz, + }); + } + const apex = vertices.length; + vertices.push({ x: centerX, y: centerY, z: 0 }); + + const faces = [top.slice()]; + const first = rings[0]; + for (let index = 0; index < segments; index += 1) { + const next = (index + 1) % segments; + faces.push([apex, first[next], first[index]]); + } + for (let r = 0; r < lat - 1; r += 1) { + const a = rings[r]; + const b = rings[r + 1]; + for (let index = 0; index < segments; index += 1) { + const next = (index + 1) % segments; + faces.push([a[index], a[next], b[next], b[index]]); + } + } + const equator = rings[lat - 1]; + for (let index = 0; index < segments; index += 1) { + const next = (index + 1) % segments; + faces.push([equator[index], equator[next], top[next], top[index]]); + } + return { vertices, faces }; + } + function createShapeGeometry(prototype) { const geometry = prototype.geometry || {}; const size = displaySizeForPrototype(prototype); const type = prototype.type || ""; if (geometry.shape === "well" && geometry.cross_section === "circle") { + if (geometry.bottom === "V") { + return createVBottomGeometry(size, 18); + } + if (geometry.bottom === "U") { + return createUBottomGeometry(size, 18, 3); + } return createCylinderGeometry(size, 18); } @@ -1068,12 +1173,14 @@ const faces = []; [ + // Faces colored by the axis their long edge spans: FRONT/BACK run + // along X (red), LEFT/RIGHT along Y (green), TOP/BOTTOM up Z (blue). { axis: 2, sg: 1, label: "TOP", color: AXIS_COLORS.z }, { axis: 2, sg: -1, label: "BOTTOM", color: AXIS_COLORS.z }, - { axis: 0, sg: 1, label: "RIGHT", color: AXIS_COLORS.x }, - { axis: 0, sg: -1, label: "LEFT", color: AXIS_COLORS.x }, - { axis: 1, sg: 1, label: "BACK", color: AXIS_COLORS.y }, - { axis: 1, sg: -1, label: "FRONT", color: AXIS_COLORS.y }, + { axis: 0, sg: 1, label: "RIGHT", color: AXIS_COLORS.y }, + { axis: 0, sg: -1, label: "LEFT", color: AXIS_COLORS.y }, + { axis: 1, sg: 1, label: "BACK", color: AXIS_COLORS.x }, + { axis: 1, sg: -1, label: "FRONT", color: AXIS_COLORS.x }, ].forEach((f) => { const pts3 = ring.map(([b, d]) => place[f.axis](f.sg, b, d)); const center3 = axisNormal(f.axis, f.sg); From 8b254d46b67869e466ee1512c3c199b7f1993a52 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sat, 16 May 2026 19:23:36 +0100 Subject: [PATCH 16/27] docs: add collapsible manufacturer panel to resource catalog When a specific manufacturer is selected, show a collapsed details panel above the cards: reference link (Website/Wikipedia by host), an extracted "about" blurb, the curated brand-structure tree (verbatim code block where present), and a sub-type count breakdown in the summary line. Extension now emits section_path per entry and a manufacturers map (company_url/blurb/brand_tree); JSON changes are additive so existing keys/filters are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/_exts/pylabrobot_labware_catalog.py | 59 +++++++++++++++- docs/_static/plr_labware_catalog.css | 86 +++++++++++++++++++++++- docs/_static/plr_labware_catalog.js | 69 +++++++++++++++++++ 3 files changed, 211 insertions(+), 3 deletions(-) diff --git a/docs/_exts/pylabrobot_labware_catalog.py b/docs/_exts/pylabrobot_labware_catalog.py index eb129247a46..9efef0f8583 100644 --- a/docs/_exts/pylabrobot_labware_catalog.py +++ b/docs/_exts/pylabrobot_labware_catalog.py @@ -56,6 +56,37 @@ def _page_title(markdown: str, fallback: str) -> str: return fallback +def _company_url(markdown: str) -> Optional[str]: + # Manufacturer pages lead with a reference link whose label varies + # ("Company Page", "Company page:", "Wikipedia page:", ...), so take the + # first markdown link in the preamble (before the first ## heading). + preamble = re.split(r"^#{2,4}\s", markdown, maxsplit=1, flags=re.MULTILINE)[0] + match = re.search(r"\[[^\]]+\]\((https?://[^)]+)\)", preamble) + return match.group(1).strip() if match else None + + +def _blurb(markdown: str) -> Optional[str]: + # The "about" text is the first blockquote in the preamble. + preamble = re.split(r"^#{2,4}\s", markdown, maxsplit=1, flags=re.MULTILINE)[0] + lines: List[str] = [] + for line in preamble.splitlines(): + quoted = re.match(r"^>\s?(.*)$", line) + if quoted is not None: + lines.append(quoted.group(1).strip()) + elif lines: + break + text = " ".join(part for part in lines if part).strip() + return text or None + + +def _brand_tree(markdown: str) -> Optional[str]: + # Some manufacturer pages (e.g. Thermo Fisher) hand-curate the brand + # hierarchy as an ASCII tree in a fenced code block in the preamble. + preamble = re.split(r"^#{2,4}\s", markdown, maxsplit=1, flags=re.MULTILINE)[0] + match = re.search(r"```[^\n]*\n(.*?)\n```", preamble, flags=re.DOTALL) + return match.group(1).rstrip("\n") if match else None + + def _image_path_from_cell(cell: str, doc_relative_path: Path) -> Optional[str]: match = re.search(r"!\[[^\]]*]\(([^)]+)\)", cell) if match is None: @@ -76,13 +107,16 @@ def _extract_labware_entries_from_markdown( ) -> List[Dict[str, Any]]: entries: List[Dict[str, Any]] = [] manufacturer = _page_title(markdown, doc_relative_path.stem.replace("_", " ").title()) - current_section = "" + heading_stack: List[Any] = [] # of (level, title), level in 2..4 for line in markdown.splitlines(): stripped = line.strip() heading_match = re.match(r"^(#{2,4})\s+(.+?)\s*$", stripped) if heading_match: - current_section = heading_match.group(2) + level = len(heading_match.group(1)) + while heading_stack and heading_stack[-1][0] >= level: + heading_stack.pop() + heading_stack.append((level, heading_match.group(2))) continue if not stripped.startswith("|"): @@ -96,12 +130,15 @@ def _extract_labware_entries_from_markdown( if len(matches) == 0: continue + section_path = [title for _level, title in heading_stack] + current_section = section_path[-1] if section_path else "" image_path = _image_path_from_cell(cells[1], doc_relative_path) for definition_name in matches: entries.append({ "definition": definition_name, "manufacturer": manufacturer, "section": current_section, + "section_path": section_path, "description_html": _description_to_html(cells[0], definition_name), "image": image_path, "page": doc_relative_path.with_suffix(".html").as_posix(), @@ -145,6 +182,23 @@ def _catalog_entries(srcdir: str) -> List[Dict[str, Any]]: return entries +def _manufacturers_index(srcdir: str) -> Dict[str, Dict[str, Any]]: + manufacturers: Dict[str, Dict[str, Any]] = {} + for doc_path in _library_doc_paths(srcdir): + markdown = doc_path.read_text(encoding="utf-8") + fallback = doc_path.relative_to(srcdir).stem.replace("_", " ").title() + name = _page_title(markdown, fallback) + manufacturers.setdefault( + name, + { + "company_url": _company_url(markdown), + "blurb": _blurb(markdown), + "brand_tree": _brand_tree(markdown), + }, + ) + return manufacturers + + def _resource_factory_registry() -> Dict[str, Callable[..., Any]]: resources_module = importlib.import_module("pylabrobot.resources") registry: Dict[str, Callable[..., Any]] = {} @@ -245,6 +299,7 @@ def build_labware_geometry_index(srcdir: str) -> Dict[str, Any]: return { "items": entries, "resources": resources, + "manufacturers": _manufacturers_index(srcdir), } diff --git a/docs/_static/plr_labware_catalog.css b/docs/_static/plr_labware_catalog.css index 415ace32d8b..f0c887732f3 100644 --- a/docs/_static/plr_labware_catalog.css +++ b/docs/_static/plr_labware_catalog.css @@ -163,7 +163,8 @@ .plr-library-card__action { border: 0; border-radius: 6px; - padding: 0.5rem 0.75rem; + padding: 0.45rem 0.675rem; + font-size: 0.9em; background: var(--pst-color-primary); color: var(--pst-color-background); font-weight: 600; @@ -312,3 +313,86 @@ grid-template-rows: 145px 1fr; } } + +.plr-catalog-manufacturer { + margin-bottom: 1.25rem; + padding: 0.75rem 1rem; + border: 1px solid var(--pst-color-border, rgba(128, 128, 128, 0.3)); + border-radius: 8px; + background: var(--pst-color-surface, rgba(128, 128, 128, 0.05)); +} + +.plr-catalog-manufacturer > summary { + cursor: pointer; + font-weight: 600; + display: flex; + align-items: baseline; + gap: 0.6rem; + list-style: none; + user-select: none; +} + +.plr-catalog-manufacturer > summary::-webkit-details-marker { + display: none; +} + +.plr-catalog-manufacturer > summary::before { + content: "▸"; + display: inline-block; + margin-right: 0.1rem; + opacity: 0.65; + transition: transform 0.15s ease; +} + +.plr-catalog-manufacturer[open] > summary::before { + transform: rotate(90deg); +} + +.plr-catalog-manufacturer > summary:hover { + color: var(--pst-color-primary, inherit); +} + +.plr-catalog-manufacturer__name { + font-size: 1.05rem; +} + +.plr-catalog-manufacturer__meta { + font-size: 0.85rem; + opacity: 0.7; +} + +.plr-catalog-manufacturer__company { + display: inline-block; + margin: 0.5rem 0 0.25rem; + font-size: 0.9rem; +} + +.plr-catalog-manufacturer__subhead { + margin: 0.75rem 0 0.25rem; + font-size: 0.78rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + opacity: 0.6; +} + +.plr-catalog-manufacturer__blurb { + margin: 0.5rem 0 0; + font-size: 0.9rem; + line-height: 1.55; + opacity: 0.85; +} + +.plr-catalog-manufacturer__tree { + margin: 0; + padding: 0.6rem 0.8rem; + overflow-x: auto; + font-size: 0.82rem; + line-height: 1.4; + border: 1px solid var(--pst-color-border, rgba(128, 128, 128, 0.3)); + border-radius: 6px; + background: rgba(128, 128, 128, 0.12); + color: var(--pst-color-text-base, currentColor); + white-space: pre; +} + diff --git a/docs/_static/plr_labware_catalog.js b/docs/_static/plr_labware_catalog.js index a7eb0441c7e..dc0e2e58456 100644 --- a/docs/_static/plr_labware_catalog.js +++ b/docs/_static/plr_labware_catalog.js @@ -194,6 +194,71 @@ return card; } + function buildSectionTree(items) { + const tree = { children: new Map(), count: 0 }; + items.forEach((item) => { + const path = + Array.isArray(item.section_path) && item.section_path.length + ? item.section_path + : [item.section || "Other"]; + let node = tree; + tree.count += 1; + path.forEach((seg) => { + if (!node.children.has(seg)) { + node.children.set(seg, { children: new Map(), count: 0 }); + } + node = node.children.get(seg); + node.count += 1; + }); + }); + return tree; + } + + function createManufacturerPanel(name, items) { + const meta = (state.index.manufacturers || {})[name] || {}; + const details = document.createElement("details"); + details.className = "plr-catalog-manufacturer"; + details.open = false; + + const breakdown = Array.from(buildSectionTree(items).children.entries()) + .map(([title, node]) => `${title} (${node.count})`) + .join(" · "); + const summary = document.createElement("summary"); + summary.appendChild(element("span", "plr-catalog-manufacturer__name", name)); + summary.appendChild( + element("span", "plr-catalog-manufacturer__meta", breakdown), + ); + details.appendChild(summary); + + if (meta.company_url) { + const link = document.createElement("a"); + link.className = "plr-catalog-manufacturer__company"; + link.href = meta.company_url; + link.target = "_blank"; + link.rel = "noopener noreferrer"; + link.textContent = /wikipedia\.org/i.test(meta.company_url) + ? "Wikipedia ↗" + : "Website ↗"; + details.appendChild(link); + } + + if (meta.blurb) { + details.appendChild( + element("p", "plr-catalog-manufacturer__blurb", meta.blurb), + ); + } + + if (meta.brand_tree) { + details.appendChild( + element("div", "plr-catalog-manufacturer__subhead", "Brand structure"), + ); + const pre = element("pre", "plr-catalog-manufacturer__tree", meta.brand_tree); + details.appendChild(pre); + } + + return details; + } + function renderCatalog(root) { const items = (state.index.items || []).filter(itemMatchesFilters); const results = root.querySelector(".plr-catalog-results"); @@ -206,6 +271,10 @@ return; } + if (state.manufacturer !== "All") { + results.appendChild(createManufacturerPanel(state.manufacturer, items)); + } + if (state.section !== "All") { const grid = element("div", "plr-catalog-grid"); items.forEach((item) => grid.appendChild(createCard(item))); From 043ab0d14d880966190a697708d4b242c318871c Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sat, 16 May 2026 19:23:36 +0100 Subject: [PATCH 17/27] docs: add resource-library file convention proposal + example Proposal (not yet enforced/wired) for a deterministic vendor-file structure: one H1, labelled OEM metadata, reserved About/Brand structure sections, heading-derived organisation, and a fixed definition-table shape. Placed outside resources/library/ so the catalog extension does not scrape them. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/resources/library_convention.md | 171 +++++++++++++++++++ docs/resources/library_convention_example.md | 72 ++++++++ 2 files changed, 243 insertions(+) create mode 100644 docs/resources/library_convention.md create mode 100644 docs/resources/library_convention_example.md diff --git a/docs/resources/library_convention.md b/docs/resources/library_convention.md new file mode 100644 index 00000000000..71afbb10069 --- /dev/null +++ b/docs/resources/library_convention.md @@ -0,0 +1,171 @@ +# Resource Library file convention (PROPOSAL — for review) + +Status: **proposal, not yet enforced or wired.** This describes the target +structure for `docs/resources/library/.md` so that (a) the full +manufacturer / OEM information is captured deterministically and (b) the +resources are organised into a consistent, machine-readable hierarchy that the +Resource Catalog can render and CI can validate. + +It deliberately replaces heuristics in today's catalog extension ("first link +in the preamble", "first blockquote", "first fenced code block") with explicit, +reserved structure. + +--- + +## 1. One file per manufacturer + +- Exactly **one H1**, the first heading in the file: the canonical manufacturer + name. This is both the displayed name and the de-duplication key. + - `# Corning Inc.` +- The filename is the slug (`corning.md`); it is *not* the source of the name. + +## 2. OEM metadata (explicit, labelled — never inferred) + +Immediately after the H1, a bullet list of labelled links. Keys are fixed and +case-insensitive; each value is a single absolute URL. All keys are optional +but recognised keys must use exactly these labels: + +```markdown +- **Website:** https://www.corning.com +- **Wikipedia:** https://en.wikipedia.org/wiki/Corning_Inc. +``` + +Rationale: today the catalog grabs "whichever link appears first", which is why +some manufacturers surface a Wikipedia link and others a homepage. With labelled +keys both can be captured and shown distinctly, with no ambiguity. + +## 3. Reserved info sections + +These headings are **reserved**: they hold manufacturer information and are +**excluded from the resource organisation tree**. Names are exact. + +### `## About` + +A single descriptive paragraph (plain prose or one blockquote). Replaces the +"first blockquote in the preamble" heuristic. + +```markdown +## About + +Corning Incorporated is an American multinational technology company that +specialises in specialty glass, ceramics, and related materials. +``` + +### `## Brand structure` (optional) + +A human-curated overview of the manufacturer's brand hierarchy, as a fenced +code block (ASCII tree) and/or prose. This is narrative context for humans; it +is **not** the source of truth for how resources are organised (see §4). + +```markdown +## Brand structure + +​``` +Thermo Fisher Scientific Inc. +├── Applied Biosystems +│ └── MicroAmp +└── Thermo Scientific + ├── Nalgene + └── Nunc +​``` +``` + +## 4. Resource organisation = heading nesting + +The organisational tree the catalog renders is derived **only** from the +`##` / `###` / `####` heading nesting of the non-reserved sections — not from +the `## Brand structure` art (which only a few files have today). + +- `##` = top-level grouping. A manufacturer chooses one consistent axis: + **brand-first** (`## Costar`, `## Axygen`) **or** **category-first** + (`## Plates`, `## Tip Racks`) — not a mix. +- `###` / `####` = finer grouping under that (e.g. `## Costar` → `### Plates`). +- A section that directly contains resources must end in a definition table. + +## 5. Definition table (fixed shape) + +Every leaf section that lists resources contains exactly one table with this +exact header: + +```markdown +| Description | Image | PLR definition | +|-|-|-| +|
Part no.:
[manufacturer website]() | ![](img//) | `Exact_PLR_Definition` | +``` + +Rules per row: + +- **Description**: free text. Optional `Part no.: …` and a + `[manufacturer website]()` link, `
`-separated. +- **Image**: a markdown image whose path resolves to a real file under + `docs/resources/library/img//…`, or an absolute URL. +- **PLR definition**: exactly **one** backticked identifier matching + `^[A-Za-z][A-Za-z0-9_]*$`, and it must resolve to a callable in + `pylabrobot.resources`. One definition per row. + +## 6. Worked example + +```markdown +# Corning Inc. + +- **Website:** https://www.corning.com +- **Wikipedia:** https://en.wikipedia.org/wiki/Corning_Inc. + +## About + +> Corning Incorporated is an American multinational technology company +> specialising in specialty glass and ceramics. + +## Costar + +### Plates + +| Description | Image | PLR definition | +|-|-|-| +| 96-well, 2 mL, V-bottom
Part no.: 3960
[manufacturer website](https://ecatalog.corning.com/3960) | ![](img/corning/Cor_96_wellplate_2mL_Vb.jpg) | `Cor_96_wellplate_2mL_Vb` | + +## Axygen + +### Plates + +| Description | Image | PLR definition | +|-|-|-| +| 384-well PCR, V-bottom
Part no.: PCR-384 | ![](img/corning/Axy_384.jpg) | `Axy_384_wellplate_50uL_Vb` | +``` + +--- + +## What CI would enforce (objective) + +These are mechanically checkable and proposed to **fail** CI on violation: + +1. Exactly one H1. +2. OEM metadata list, if present, uses only recognised labels with valid + absolute URLs. +3. Reserved section names spelled exactly (`About`, `Brand structure`). +4. No prose/tables placed where the parser expects structure (e.g. a resource + table outside any non-reserved section). +5. Every definition table matches the fixed header. +6. Each PLR definition: single identifier, regex-valid, resolves to a real + `pylabrobot.resources` factory. +7. Every image path resolves to an existing file. +8. No duplicate PLR definition names across the whole library. + +## What is NOT in scope for CI (author / project decision) + +These are real consistency issues but are **naming/structure policy**, not +mechanics, and tie into the open "Resource Library vs Catalog" / +plural-vs-singular discussion. The spec *recommends* a rule but the project +owner decides; CI should not impose it: + +- Heading **naming**: Title Case, singular vs plural ("Tip Rack" vs + "TipRacks" vs "Tip racks"), and the typo class ("Plate Adapterrs"). + *Recommendation:* Title Case, singular category nouns. +- Whether organisation is **brand-first or category-first** per manufacturer. + +## Migration note + +Adopting this fully requires: updating the catalog extension to parse the +explicit metadata + reserved sections + heading-derived tree, migrating the +~23 existing vendor files, then enabling the CI. That is a separate, +author-coordinated effort; this document is the spec to agree on first. diff --git a/docs/resources/library_convention_example.md b/docs/resources/library_convention_example.md new file mode 100644 index 00000000000..6c899657b53 --- /dev/null +++ b/docs/resources/library_convention_example.md @@ -0,0 +1,72 @@ +# Example vendor file (reference for library_convention.md) + +This is a complete, conformant reference for the proposed +`docs/resources/library/.md` convention. It is **not** a real vendor +page and is intentionally placed outside `resources/library/` so the catalog +extension does not scrape it. Copy this skeleton when adding a manufacturer. + +--- + +# Acme Labware Inc. + +- **Website:** https://www.acme-labware.example +- **Wikipedia:** https://en.wikipedia.org/wiki/Example + +## About + +> Acme Labware Inc. is a fictional manufacturer of plates, tip racks, and +> reservoirs, used here purely to demonstrate the file convention. + +## Brand structure + +``` +Acme Labware Inc. +├── AcmePure (consumables brand) +│ ├── Plates +│ └── Reservoirs +└── AcmeTips (tips brand) + └── Tip Racks +``` + +## AcmePure + +### Plates + +| Description | Image | PLR definition | +|-|-|-| +| 96-well, 2 mL, V-bottom
Part no.: AP-9620
[manufacturer website](https://www.acme-labware.example/p/AP-9620) | ![](img/acme/Acme_96_wellplate_2mL_Vb.jpg) | `Acme_96_wellplate_2mL_Vb` | +| 384-well, 120 uL, flat bottom
Part no.: AP-3841
[manufacturer website](https://www.acme-labware.example/p/AP-3841) | ![](img/acme/Acme_384_wellplate_120uL_Fb.jpg) | `Acme_384_wellplate_120uL_Fb` | + +### Reservoirs + +| Description | Image | PLR definition | +|-|-|-| +| Single-channel reservoir, 290 mL
Part no.: AP-RES290 | ![](img/acme/Acme_1_troughplate_290000uL_Vb.jpg) | `Acme_1_troughplate_290000uL_Vb` | + +## AcmeTips + +### Tip Racks + +| Description | Image | PLR definition | +|-|-|-| +| 96 tips, 1000 uL, filtered
Part no.: AT-1000F
[manufacturer website](https://www.acme-labware.example/p/AT-1000F) | ![](img/acme/Acme_96_tiprack_1000uL_filtered.jpg) | `Acme_96_tiprack_1000uL_filtered` | + +--- + +## Why this is conformant + +- **One H1** (`# Acme Labware Inc.`) — the canonical name + dedup key. +- **OEM metadata** as a labelled list (`Website:`, `Wikipedia:`) — both + captured unambiguously, no "first link" guessing. +- **`## About`** reserved section holds the description (not a scraped + blockquote heuristic). +- **`## Brand structure`** reserved section holds the human-curated overview; + it is *not* used to build the catalog tree. +- **Organisation = heading nesting**: brand-first here + (`## AcmePure` → `### Plates`/`### Reservoirs`, `## AcmeTips` → + `### Tip Racks`), one consistent axis, reserved sections excluded. +- **Definition tables**: exact `| Description | Image | PLR definition |` + header; one backticked, regex-valid PLR definition per row; image paths + under `img//…`; optional `Part no.` and `[manufacturer website]`. +- Headings use Title Case, singular category nouns ("Tip Rack", "Plate") — + the *recommended* (not CI-enforced) naming policy. From 69e02c258ed9207e3755a5a1e93245d90eb53721 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sun, 17 May 2026 09:42:11 +0100 Subject: [PATCH 18/27] docs: render safe inline HTML in catalog descriptions (fix PR regression) Pre-PR, Sphinx/MyST rendered inline HTML (e.g.
  • part-number lists) in library table cells; routing through the catalog extension html.escape()'d it, so tags showed as literal text. Escape everything then re-enable only an attribute-less allowlist (br/p/ul/ol/li/b/ strong/i/em/sub/sup) plus scheme-checked -- safe by construction (scripts/handlers/js: hrefs stay inert). Clamp the card description (max-height + scroll) so a pathological cell can't dominate the layout. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/_exts/pylabrobot_labware_catalog.py | 50 ++++++++++++++++++++---- docs/_static/plr_labware_catalog.css | 12 ++++++ 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/docs/_exts/pylabrobot_labware_catalog.py b/docs/_exts/pylabrobot_labware_catalog.py index 9efef0f8583..a56024497d0 100644 --- a/docs/_exts/pylabrobot_labware_catalog.py +++ b/docs/_exts/pylabrobot_labware_catalog.py @@ -32,20 +32,54 @@ def _library_doc_paths(srcdir: str) -> List[Path]: return sorted(path for path in library_root.rglob("*.md") if path.is_file()) -def _markdown_links_to_html(text: str) -> str: - escaped = escape(text, quote=False) - return re.sub( - r"\[([^\]]+)\]\(([^)]+)\)", - lambda match: f'{match.group(1)}', - escaped, +# Structural inline tags that pre-PR Sphinx/MyST rendered from these cells. +# Re-enabled here so the catalog doesn't regress files that used inline HTML. +_ALLOWED_TAGS = ("br", "p", "ul", "ol", "li", "b", "strong", "i", "em", "sub", "sup") +_SAFE_HREF = re.compile(r"(?i)^(https?:|mailto:)") + + +def _render_cell_html(text: str) -> str: + """Safe-by-construction: escape everything, then re-enable only a fixed + allowlist of attribute-less structural tags plus sanitised . Anything + else (scripts, on* handlers, unsafe schemes) stays escaped as inert text.""" + out = escape(text, quote=True) + + def _md_link(match: "re.Match") -> str: + label, href = match.group(1), match.group(2) + if not re.match(r"(?i)^(https?:|mailto:|/|#)", href): + return label + return f'{label}' + + out = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", _md_link, out) + + tags = "|".join(_ALLOWED_TAGS) + out = re.sub( + rf"<(/?)({tags})\s*/?>", + lambda m: f"<{m.group(1)}{m.group(2).lower()}>", + out, + flags=re.IGNORECASE, ) + def _anchor(match: "re.Match") -> str: + href = match.group(1) + if _SAFE_HREF.match(href): + return f'' + return "" + + out = re.sub(r"<a\s+href="(.*?)"\s*>", _anchor, out, flags=re.IGNORECASE) + out = re.sub(r"</a>", "", out, flags=re.IGNORECASE) + return out + def _description_to_html(description: str, definition_name: str) -> str: - parts = [part.strip() for part in re.split(r"", description) if part.strip()] + parts = [ + part.strip() + for part in re.split(r"", description, flags=re.IGNORECASE) + if part.strip() + ] if parts and parts[0].strip("'` ") == definition_name: parts = parts[1:] - return "
    ".join(_markdown_links_to_html(part) for part in parts) + return "
    ".join(_render_cell_html(part) for part in parts) def _page_title(markdown: str, fallback: str) -> str: diff --git a/docs/_static/plr_labware_catalog.css b/docs/_static/plr_labware_catalog.css index f0c887732f3..547e40f66e0 100644 --- a/docs/_static/plr_labware_catalog.css +++ b/docs/_static/plr_labware_catalog.css @@ -144,6 +144,18 @@ color: var(--pst-color-text-base); font-size: 0.86rem; line-height: 1.45; + max-height: 10rem; + overflow-y: auto; +} + +.plr-library-card__description ul, +.plr-library-card__description ol { + margin: 0.25rem 0; + padding-left: 1.1rem; +} + +.plr-library-card__description li { + margin: 0.05rem 0; } .plr-library-card__footer { From c91cb4562fcd765f6f5e267cd9203bca76daae3f Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sun, 17 May 2026 10:04:28 +0100 Subject: [PATCH 19/27] docs: make resource-library convention docs build-clean Mark both proposal docs `orphan` (intentional standalone references, not in the nav) and present the example vendor file as a fenced code block so its placeholder images/headings are not resolved. Fixes the image.not_readable + toc.not_included warnings that failed the -W docs build. Also folds in the proposal revisions: About/Brand-structure sections made explicitly optional with fallbacks, and Wikipedia de-emphasised (Website is the only "primary" key; all metadata optional). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/resources/library_convention.md | 81 +++++++++++++++----- docs/resources/library_convention_example.md | 27 ++++--- 2 files changed, 75 insertions(+), 33 deletions(-) diff --git a/docs/resources/library_convention.md b/docs/resources/library_convention.md index 71afbb10069..70abfdc8e83 100644 --- a/docs/resources/library_convention.md +++ b/docs/resources/library_convention.md @@ -1,3 +1,7 @@ +--- +orphan: true +--- + # Resource Library file convention (PROPOSAL — for review) Status: **proposal, not yet enforced or wired.** This describes the target @@ -21,28 +25,50 @@ reserved structure. ## 2. OEM metadata (explicit, labelled — never inferred) -Immediately after the H1, a bullet list of labelled links. Keys are fixed and -case-insensitive; each value is a single absolute URL. All keys are optional -but recognised keys must use exactly these labels: +Immediately after the H1, an **optional** bullet list of labelled links. The +list, any individual key, and the whole block are all optional — many +manufacturers (smaller OEMs, regional suppliers, the DIY entries) have no +external page at all, and that is normal, not a violation. + +Recognised keys (case-insensitive, value = one absolute URL): + +- **`Website:`** — the manufacturer's own page. This is the only key worth + treating as the "primary" reference when present. +- Any number of *additional, optional* labelled links, e.g. + **`Wikipedia:`**, **`Catalog:`**, **`Datasheet:`**. **None of these is + expected** — `Wikipedia:` in particular must not be assumed, since most + manufacturers have no Wikipedia page. ```markdown - **Website:** https://www.corning.com -- **Wikipedia:** https://en.wikipedia.org/wiki/Corning_Inc. +- **Wikipedia:** https://en.wikipedia.org/wiki/Corning_Inc. (optional extra) ``` -Rationale: today the catalog grabs "whichever link appears first", which is why -some manufacturers surface a Wikipedia link and others a homepage. With labelled -keys both can be captured and shown distinctly, with no ambiguity. +A file with no links is fully conformant. **Fallback:** no metadata → no +reference link in the panel; only `Website:` → a "Website ↗" link; a +`Wikipedia:` present → an additional "Wikipedia ↗" link. The panel never +implies a missing key should exist. + +Rationale: today the catalog grabs "whichever link appears first", so some +manufacturers surface a Wikipedia link and others a homepage purely by +ordering accident. Explicit, optional labels remove the ambiguity without +making any particular source mandatory. -## 3. Reserved info sections +## 3. Reserved info sections (all optional) -These headings are **reserved**: they hold manufacturer information and are -**excluded from the resource organisation tree**. Names are exact. +These headings are **reserved** and **optional**. When present they hold +manufacturer information and are **excluded from the resource organisation +tree**; when absent the panel simply omits that part — never an error. The +names, *if used*, must be spelled exactly, but **no file is required to have +them**. The only mandatory content is §1 (one H1) and §5 (at least one +resource table whose definitions resolve); everything in this section is +optional enrichment. -### `## About` +### `## About` (optional) A single descriptive paragraph (plain prose or one blockquote). Replaces the "first blockquote in the preamble" heuristic. +**Fallback:** if absent, no description paragraph is shown — not an error. ```markdown ## About @@ -56,6 +82,8 @@ specialises in specialty glass, ceramics, and related materials. A human-curated overview of the manufacturer's brand hierarchy, as a fenced code block (ASCII tree) and/or prose. This is narrative context for humans; it is **not** the source of truth for how resources are organised (see §4). +**Fallback:** if absent, the panel shows only the heading-derived breakdown +from §4 (which every file has) — not an error. ```markdown ## Brand structure @@ -137,19 +165,30 @@ Rules per row: ## What CI would enforce (objective) -These are mechanically checkable and proposed to **fail** CI on violation: +Mechanically checkable, proposed to **fail** CI on violation. Split into the +mandatory core and conditional checks that only apply to *optional* content +when it is present. + +**Required (mandatory core — every file):** 1. Exactly one H1. -2. OEM metadata list, if present, uses only recognised labels with valid +2. At least one resource table, and every PLR definition in it is a single + regex-valid identifier that resolves to a real `pylabrobot.resources` + factory. +3. Every definition table matches the fixed header. +4. Every image path resolves to an existing file. +5. No duplicate PLR definition names across the whole library. + +**Conditional (optional content — checked only if present):** + +6. OEM metadata list, *if present*, uses only recognised labels with valid absolute URLs. -3. Reserved section names spelled exactly (`About`, `Brand structure`). -4. No prose/tables placed where the parser expects structure (e.g. a resource - table outside any non-reserved section). -5. Every definition table matches the fixed header. -6. Each PLR definition: single identifier, regex-valid, resolves to a real - `pylabrobot.resources` factory. -7. Every image path resolves to an existing file. -8. No duplicate PLR definition names across the whole library. +7. Reserved sections are **not required**. A section titled exactly + `## About` or `## Brand structure` is treated as reserved (excluded from + the resource tree); near-miss spellings are flagged so they are not + silently parsed as resource categories. +8. No resource table placed where the parser expects an info section, or + vice-versa. ## What is NOT in scope for CI (author / project decision) diff --git a/docs/resources/library_convention_example.md b/docs/resources/library_convention_example.md index 6c899657b53..81fc21d39a3 100644 --- a/docs/resources/library_convention_example.md +++ b/docs/resources/library_convention_example.md @@ -1,12 +1,16 @@ +--- +orphan: true +--- + # Example vendor file (reference for library_convention.md) This is a complete, conformant reference for the proposed `docs/resources/library/.md` convention. It is **not** a real vendor -page and is intentionally placed outside `resources/library/` so the catalog -extension does not scrape it. Copy this skeleton when adding a manufacturer. - ---- +page; it is shown below as a literal code block so it is not built as a page +and its placeholder images are not resolved. Copy this skeleton when adding a +manufacturer. +````markdown # Acme Labware Inc. - **Website:** https://www.acme-labware.example @@ -50,18 +54,17 @@ Acme Labware Inc. | Description | Image | PLR definition | |-|-|-| | 96 tips, 1000 uL, filtered
    Part no.: AT-1000F
    [manufacturer website](https://www.acme-labware.example/p/AT-1000F) | ![](img/acme/Acme_96_tiprack_1000uL_filtered.jpg) | `Acme_96_tiprack_1000uL_filtered` | - ---- +```` ## Why this is conformant - **One H1** (`# Acme Labware Inc.`) — the canonical name + dedup key. -- **OEM metadata** as a labelled list (`Website:`, `Wikipedia:`) — both - captured unambiguously, no "first link" guessing. -- **`## About`** reserved section holds the description (not a scraped - blockquote heuristic). -- **`## Brand structure`** reserved section holds the human-curated overview; - it is *not* used to build the catalog tree. +- **OEM metadata** as a labelled list (`Website:`, optional `Wikipedia:`) — + captured unambiguously, no "first link" guessing; all keys optional. +- **`## About`** (optional) reserved section holds the description (not a + scraped blockquote heuristic). +- **`## Brand structure`** (optional) reserved section holds the human-curated + overview; it is *not* used to build the catalog tree. - **Organisation = heading nesting**: brand-first here (`## AcmePure` → `### Plates`/`### Reservoirs`, `## AcmeTips` → `### Tip Racks`), one consistent axis, reserved sections excluded. From e38f0d62904864ebc1f3c1dda733ddda37c8b8d6 Mon Sep 17 00:00:00 2001 From: norle Date: Sun, 17 May 2026 11:56:39 +0200 Subject: [PATCH 20/27] docs: change naming back to resource library from resource catalog --- .../carrier/plate-carrier/plate_carrier.ipynb | 2 +- docs/resources/catalog.md | 6 +++--- docs/resources/index.md | 12 ++++++------ docs/resources/library_convention.md | 2 +- .../heating_shaking/hamilton.ipynb | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/resources/carrier/plate-carrier/plate_carrier.ipynb b/docs/resources/carrier/plate-carrier/plate_carrier.ipynb index cf945523eb2..8e493d0638f 100644 --- a/docs/resources/carrier/plate-carrier/plate_carrier.ipynb +++ b/docs/resources/carrier/plate-carrier/plate_carrier.ipynb @@ -15,7 +15,7 @@ "source": [ "## Using a plate carrier\n", "\n", - "The PyLabRobot Resource Catalog has a big number of predefined carriers. You can find these in the [resource catalog](../../catalog.md)." + "The PyLabRobot Resource Library has a big number of predefined carriers. You can find these in the [resource library](../../catalog.md)." ] }, { diff --git a/docs/resources/catalog.md b/docs/resources/catalog.md index 6b0ff24c99e..76dc30cdbd3 100644 --- a/docs/resources/catalog.md +++ b/docs/resources/catalog.md @@ -1,7 +1,7 @@ -# Resource Catalog +# Resource Library -The PyLabRobot Resource Catalog is rendered from the resource definitions used by PLR itself. -The catalog below is generated during the docs build and includes manufacturer, type, definition details, images, and 3D previews from the same geometry metadata returned by `generate_geometry_catalog`. +The PyLabRobot Resource Library is rendered from the resource definitions used by PLR itself. +The library below is generated during the docs build and includes manufacturer, type, definition details, images, and 3D previews from the same geometry metadata returned by `generate_geometry_catalog`. ```{raw} html
    diff --git a/docs/resources/index.md b/docs/resources/index.md index 1d5c67bc8a6..4ce1f3f7fb2 100644 --- a/docs/resources/index.md +++ b/docs/resources/index.md @@ -11,8 +11,8 @@ The PLR Resource Management System consists of two key components, each serving 1. **Resource Ontology System** - The ***'blueprint'*** of PLR's physical definition framework, responsible for defining physical resources, modeling their distinct behaviors, and dynamically managing their relationships (i.e. tracking their *state*). -2. **Resource Catalog** - - The ***'catalog'*** of premade resource definitions. +2. **Resource Library** + - The ***'library'*** of premade resource definitions. This provides reusable, standardized definitions that enhance consistency and interoperability across automation workflows. This ensures smooth integration, scalability, and efficient resource utilization. @@ -134,14 +134,14 @@ plate-adapter/plate-adapter resource-stack/resource-stack ``` -## Resource Catalog +## Resource Library -The PyLabRobot Resource Catalog is PyLabRobot's open-source, crowd-sourced collection of pre-made resource definitions. +The PyLabRobot Resource Library is PyLabRobot's open-source, crowd-sourced collection of pre-made resource definitions. Laboratories across the world use an almost infinite number of different resources (e.g. plates, tubes, liquid handlers, microscopes, arms, ...). We believe the way to most efficiently capture the largest portion of this resource superset is via crowd-sourcing and iteratively peer-reviewing definitions. If you cannot find something, please contribute what you are looking for! -Open the {doc}`catalog page ` to search the catalog across manufacturers and inspect generated 3D previews for labware definitions. +Open the {doc}`library page ` to search the resource library across manufacturers and inspect generated 3D previews for labware definitions.
    @@ -167,7 +167,7 @@ Open the {doc}`catalog page ` to search the catalog across manufacturer ```{toctree} -:caption: Resource Catalog +:caption: Resource Library catalog ``` diff --git a/docs/resources/library_convention.md b/docs/resources/library_convention.md index 70abfdc8e83..319fa3ce763 100644 --- a/docs/resources/library_convention.md +++ b/docs/resources/library_convention.md @@ -8,7 +8,7 @@ Status: **proposal, not yet enforced or wired.** This describes the target structure for `docs/resources/library/.md` so that (a) the full manufacturer / OEM information is captured deterministically and (b) the resources are organised into a consistent, machine-readable hierarchy that the -Resource Catalog can render and CI can validate. +Resource Library can render and CI can validate. It deliberately replaces heuristics in today's catalog extension ("first link in the preamble", "first blockquote", "first fenced code block") with explicit, diff --git a/docs/user_guide/01_material-handling/heating_shaking/hamilton.ipynb b/docs/user_guide/01_material-handling/heating_shaking/hamilton.ipynb index c6a06db0f0e..9957a6c7993 100644 --- a/docs/user_guide/01_material-handling/heating_shaking/hamilton.ipynb +++ b/docs/user_guide/01_material-handling/heating_shaking/hamilton.ipynb @@ -212,7 +212,7 @@ "\n", "Before you can use the Hamilton Heater Shaker in combination with a Hamilton STAR liquid handler, you need to assign it to the deck. This is needed when, for example, you want to use the iSWAP or CoRe grippers to move a plate to or from the heater shaker. This is also required to get the heater shaker to show up in the Visualizer.\n", "\n", - "Here's one example of assigning a Hamilton Heater Shaker to the deck using a `MFX_CAR_P3_SHAKER`. Note that you can use any carrier, or even directly place heater shakers on the deck if you like. See the [resource catalog](../../../resources/catalog.md) for carriers." + "Here's one example of assigning a Hamilton Heater Shaker to the deck using a `MFX_CAR_P3_SHAKER`. Note that you can use any carrier, or even directly place heater shakers on the deck if you like. See the [resource library](../../../resources/catalog.md) for carriers." ] }, { From 1db34c7b028717ecaf5b12f390ecf03dd456ffc2 Mon Sep 17 00:00:00 2001 From: norle Date: Sun, 17 May 2026 12:16:25 +0200 Subject: [PATCH 21/27] docs: tune 3d ruler scaling --- docs/_static/plr_geometry_viewer.js | 46 ++++++++++++++++++----------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/docs/_static/plr_geometry_viewer.js b/docs/_static/plr_geometry_viewer.js index 32a367cac71..cb953d8b806 100644 --- a/docs/_static/plr_geometry_viewer.js +++ b/docs/_static/plr_geometry_viewer.js @@ -737,6 +737,12 @@ }; } + rulerUiScale() { + // Keep rulers a bit larger at baseline and grow with zoom so labels and + // axis strokes stay legible while inspecting details. + return clamp(1.1 + (this.zoom - 1) * 0.28, 1.0, 2.0); + } + drawBackground() { const ctx = this.context; const theme = readThemeColors(this.root); @@ -762,12 +768,14 @@ const y1 = Math.ceil(this.bounds.max.y / step) * step; const epsilon = step * 1e-6; const labelGap = step * 0.2; + const uiScale = this.rulerUiScale(); + const tickMinGap = TICK_MIN_GAP * uiScale; ctx.save(); ctx.strokeStyle = theme.text; // Minor mm grid. - ctx.lineWidth = 1; + ctx.lineWidth = Math.max(1, 0.9 * uiScale); ctx.globalAlpha = 0.18; for (let x = x0; x <= x1 + epsilon; x += step) { const start = this.project({ x, y: y0, z: groundZ }); @@ -788,7 +796,7 @@ // Origin axes (Blender-style colors: X red, Y green). ctx.globalAlpha = 0.9; - ctx.lineWidth = 3; + ctx.lineWidth = 3 * uiScale; const xAxisStart = this.project({ x: x0, y: y0, z: groundZ }); const xAxisEnd = this.project({ x: x1, y: y0, z: groundZ }); ctx.strokeStyle = AXIS_COLORS.x; @@ -804,7 +812,7 @@ ctx.stroke(); // Tick marks: short stubs pointing outward toward each axis's labels. - ctx.lineWidth = 2; + ctx.lineWidth = 2 * uiScale; const tickLen = step * 0.12; ctx.strokeStyle = AXIS_COLORS.x; for (let x = x0; x <= x1 + epsilon; x += step) { @@ -827,7 +835,7 @@ // Tick labels in mm (matched to axis colors). ctx.globalAlpha = 0.9; - ctx.font = "bold 12px system-ui, -apple-system, sans-serif"; + ctx.font = `bold ${Math.round(12 * uiScale)}px system-ui, -apple-system, sans-serif`; ctx.fillStyle = AXIS_COLORS.x; ctx.textAlign = "center"; ctx.textBaseline = "top"; @@ -836,12 +844,12 @@ const point = this.project({ x, y: y0 - labelGap, z: groundZ }); if ( lastXLabel !== null && - Math.hypot(point.x - lastXLabel.x, point.y - lastXLabel.y) < TICK_MIN_GAP + Math.hypot(point.x - lastXLabel.x, point.y - lastXLabel.y) < tickMinGap ) { continue; } lastXLabel = point; - ctx.fillText(String(Math.round(x)), point.x, point.y + 5); + ctx.fillText(String(Math.round(x)), point.x, point.y + 5 * uiScale); } ctx.fillStyle = AXIS_COLORS.y; ctx.textAlign = "right"; @@ -851,27 +859,27 @@ const point = this.project({ x: x0 - labelGap, y, z: groundZ - step * 0.2 }); if ( lastYLabel !== null && - Math.hypot(point.x - lastYLabel.x, point.y - lastYLabel.y) < TICK_MIN_GAP + Math.hypot(point.x - lastYLabel.x, point.y - lastYLabel.y) < tickMinGap ) { continue; } lastYLabel = point; - ctx.fillText(String(Math.round(y)), point.x - 7, point.y); + ctx.fillText(String(Math.round(y)), point.x - 7 * uiScale, point.y); } // Axis unit captions. ctx.globalAlpha = 1; - ctx.font = "bold 11px system-ui, -apple-system, sans-serif"; + ctx.font = `bold ${Math.round(11 * uiScale)}px system-ui, -apple-system, sans-serif`; const xCaption = this.project({ x: x1, y: y0, z: groundZ }); ctx.fillStyle = AXIS_COLORS.x; ctx.textAlign = "left"; ctx.textBaseline = "middle"; - ctx.fillText("X (mm)", xCaption.x + 25, xCaption.y); + ctx.fillText("X (mm)", xCaption.x + 25 * uiScale, xCaption.y); const yCaption = this.project({ x: x0, y: y1, z: groundZ }); ctx.fillStyle = AXIS_COLORS.y; ctx.textAlign = "right"; ctx.textBaseline = "bottom"; - ctx.fillText("Y (mm)", yCaption.x - 7, yCaption.y - 22); + ctx.fillText("Y (mm)", yCaption.x - 7 * uiScale, yCaption.y - 22 * uiScale); ctx.restore(); } @@ -907,6 +915,8 @@ }); const baseScreen = this.project({ x: corner.x, y: corner.y, z: z0 }); const outwardSign = baseScreen.x >= centerScreen.x ? 1 : -1; + const uiScale = this.rulerUiScale(); + const tickMinGap = TICK_MIN_GAP * uiScale; const ctx = this.context; ctx.save(); @@ -914,7 +924,7 @@ ctx.fillStyle = AXIS_COLORS.z; ctx.globalAlpha = 0.9; - ctx.lineWidth = 3; + ctx.lineWidth = 3 * uiScale; const axisBottom = this.project({ x: corner.x, y: corner.y, z: zStart }); const axisTop = this.project({ x: corner.x, y: corner.y, z: zEnd }); ctx.beginPath(); @@ -922,7 +932,7 @@ ctx.lineTo(axisTop.x, axisTop.y); ctx.stroke(); - ctx.font = "bold 12px system-ui, -apple-system, sans-serif"; + ctx.font = `bold ${Math.round(12 * uiScale)}px system-ui, -apple-system, sans-serif`; ctx.textAlign = outwardSign > 0 ? "left" : "right"; ctx.textBaseline = "middle"; let lastZLabel = null; @@ -930,22 +940,22 @@ const point = this.project({ x: corner.x, y: corner.y, z }); if ( lastZLabel !== null && - Math.hypot(point.x - lastZLabel.x, point.y - lastZLabel.y) < TICK_MIN_GAP + Math.hypot(point.x - lastZLabel.x, point.y - lastZLabel.y) < tickMinGap ) { continue; } lastZLabel = point; ctx.beginPath(); ctx.moveTo(point.x, point.y); - ctx.lineTo(point.x + outwardSign * 6, point.y); + ctx.lineTo(point.x + outwardSign * 6 * uiScale, point.y); ctx.stroke(); - ctx.fillText(String(Math.round(z)), point.x + outwardSign * 10, point.y); + ctx.fillText(String(Math.round(z)), point.x + outwardSign * 10 * uiScale, point.y); } ctx.globalAlpha = 1; - ctx.font = "bold 11px system-ui, -apple-system, sans-serif"; + ctx.font = `bold ${Math.round(11 * uiScale)}px system-ui, -apple-system, sans-serif`; ctx.textBaseline = "bottom"; - ctx.fillText("Z (mm)", axisTop.x + outwardSign * 10, axisTop.y - 8); + ctx.fillText("Z (mm)", axisTop.x + outwardSign * 10 * uiScale, axisTop.y - 8 * uiScale); ctx.restore(); } From 11937b8dec8c7a0660fd32ae1a0c42474e700bc1 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sun, 17 May 2026 13:05:17 +0100 Subject: [PATCH 22/27] docs: fix vendor library content, normalise tables, consolidate Falcon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Content fixes: imcs.md (malformed
      /truncated entry/empty image cell), falcon→`## Plates` H1, eppendorf multi-definition row split, sergi "Plate Adapterrs" typo, vwr internal-host + nest 404 link rot. - Deterministic GFM table formatting across all vendor files (content-safe + idempotent; incidentally recovers 15 definitions that ragged rows were silently dropping from the catalog). - Consolidate Falcon (a Corning brand) from standalone falcon.md into corning.md `## Falcon`; verified lossless via the catalog extractor. Falcon no longer appears as its own manufacturer. - Rename stale `Falcon_96_wellplate_Rb` (no backing factory) to the current `Cor_Falcon_96_wellplate_250ul_Rb`. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/resources/library/agenbio.md | 12 +-- docs/resources/library/agilent.md | 8 +- docs/resources/library/alpaqua.md | 4 +- docs/resources/library/azenta.md | 6 +- docs/resources/library/bioer.md | 4 +- docs/resources/library/biorad.md | 4 +- docs/resources/library/boekel.md | 10 +-- docs/resources/library/celltreat.md | 18 ++-- docs/resources/library/cellvis.md | 6 +- docs/resources/library/corning.md | 43 +++++---- docs/resources/library/diy/davidnedrud.md | 6 +- docs/resources/library/diy/grindbio.md | 6 +- docs/resources/library/eppendorf.md | 21 ++--- docs/resources/library/falcon.md | 17 ---- docs/resources/library/greiner.md | 8 +- docs/resources/library/hamilton.md | 102 +++++++++++----------- docs/resources/library/imcs.md | 6 +- docs/resources/library/nest.md | 14 +-- docs/resources/library/opentrons.md | 4 +- docs/resources/library/perkin_elmer.md | 4 +- docs/resources/library/porvair.md | 10 +-- docs/resources/library/revvity.md | 6 +- docs/resources/library/sergi.md | 6 +- docs/resources/library/thermo_fisher.md | 30 +++---- docs/resources/library/vwr.md | 10 +-- 25 files changed, 177 insertions(+), 188 deletions(-) delete mode 100644 docs/resources/library/falcon.md diff --git a/docs/resources/library/agenbio.md b/docs/resources/library/agenbio.md index e735ad94bf8..f6533d08e94 100644 --- a/docs/resources/library/agenbio.md +++ b/docs/resources/library/agenbio.md @@ -4,9 +4,9 @@ ## Plates -| Description | Image | PLR definition | -|-|-|-| -| 'AGenBio_4_troughplate_75000uL_Vb'
      Part no.: RES-75-4MW
      [manufacturer website](https://agenbio.en.made-in-china.com/product/ZTqYVMiCkpcF/China-Medical-Consumable-Plastic-Reagent-Reservoir-Disposable-4-Channel-Troughs-Reagent-Reservoir.html?) | ![](img/agenbio/AGenBio_4_troughplate_75000uL_Vb.webp) | `AGenBio_4_troughplate_75000uL_Vb` | -| 'AGenBio_1_troughplate_190000uL_Fl'
      Part no.: RES-190-F
      [manufacturer website](https://agenbio.en.made-in-china.com/product/pZWaBIPvZMkm/China-Res-190-F-Lad-Consumables-of-Flat-Reservoir.html) | ![](img/agenbio/AGenBio_1_troughplate_190000uL_Fl.webp) | `AGenBio_1_troughplate_190000uL_Fl` | -| 'AGenBio_1_troughplate_100000uL_Fl'
      Part no.: RES-100-F
      [manufacturer website](https://agenbio.en.made-in-china.com/product/rxgRnesJIjcQ/China-100ml-Flat-Bottom-Single-Well-Low-Profile-Design-Reagent-Reservoir.html) | ![](img/agenbio/AGenBio_1_troughplate_100000uL_Fl.jpg) | `AGenBio_1_troughplate_100000uL_Fl` | -| `AGenBio_96_wellplate_Ub_2200ul`
      Part no.: P-2.2-SQG-96
      [manufacturer website](https://agenbio.en.made-in-china.com/product/GfoUzYcARahV/China-2-2ml-Square-Well-96-Square-Deep-Well-Plate-Profile-Concave-U-Bottom-96-Deep-Well-Plate.html) | ![](img/agenbio/AGenBio_96_wellplate_Ub_2200ul.avif) | `AGenBio_96_wellplate_Ub_2200ul` | +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------------------- | +| 'AGenBio_4_troughplate_75000uL_Vb'
      Part no.: RES-75-4MW
      [manufacturer website](https://agenbio.en.made-in-china.com/product/ZTqYVMiCkpcF/China-Medical-Consumable-Plastic-Reagent-Reservoir-Disposable-4-Channel-Troughs-Reagent-Reservoir.html?) | ![](img/agenbio/AGenBio_4_troughplate_75000uL_Vb.webp) | `AGenBio_4_troughplate_75000uL_Vb` | +| 'AGenBio_1_troughplate_190000uL_Fl'
      Part no.: RES-190-F
      [manufacturer website](https://agenbio.en.made-in-china.com/product/pZWaBIPvZMkm/China-Res-190-F-Lad-Consumables-of-Flat-Reservoir.html) | ![](img/agenbio/AGenBio_1_troughplate_190000uL_Fl.webp) | `AGenBio_1_troughplate_190000uL_Fl` | +| 'AGenBio_1_troughplate_100000uL_Fl'
      Part no.: RES-100-F
      [manufacturer website](https://agenbio.en.made-in-china.com/product/rxgRnesJIjcQ/China-100ml-Flat-Bottom-Single-Well-Low-Profile-Design-Reagent-Reservoir.html) | ![](img/agenbio/AGenBio_1_troughplate_100000uL_Fl.jpg) | `AGenBio_1_troughplate_100000uL_Fl` | +| `AGenBio_96_wellplate_Ub_2200ul`
      Part no.: P-2.2-SQG-96
      [manufacturer website](https://agenbio.en.made-in-china.com/product/GfoUzYcARahV/China-2-2ml-Square-Well-96-Square-Deep-Well-Plate-Profile-Concave-U-Bottom-96-Deep-Well-Plate.html) | ![](img/agenbio/AGenBio_96_wellplate_Ub_2200ul.avif) | `AGenBio_96_wellplate_Ub_2200ul` | diff --git a/docs/resources/library/agilent.md b/docs/resources/library/agilent.md index 4a1fbdf6075..89d0844944c 100644 --- a/docs/resources/library/agilent.md +++ b/docs/resources/library/agilent.md @@ -4,7 +4,7 @@ ## Plates -| Description | Image | PLR definition | -|-|-|-| -| 'agilent_96_wellplate_150uL_Vb'
      Part no.: 5042-8502
      [manufacturer website](https://www.agilent.com/store/en_US/Prod-5042-8502/5042-8502) | ![](img/agilent/agilent_96_wellplate_150uL_Vb.jpg) | `agilent_96_wellplate_150uL_Vb` | -| 'Agilent_2_reservoir_144mL_Vb'
      Part no.: 203852-100
      [manufacturer website](https://www.agilent.com/store/en_US/Prod-203852-100/203852-100?srsltid=AfmBOorcg-D2jAf9VwaEDFvo7ZCQ-_G14cndKReIcpSBsFA0YjxNgDKm) | ![](img/agilent/Agilent_2_reservoir_144mL_Vb.jpg) | `Agilent_2_reservoir_144mL_Vb` | \ No newline at end of file +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------- | +| 'agilent_96_wellplate_150uL_Vb'
      Part no.: 5042-8502
      [manufacturer website](https://www.agilent.com/store/en_US/Prod-5042-8502/5042-8502) | ![](img/agilent/agilent_96_wellplate_150uL_Vb.jpg) | `agilent_96_wellplate_150uL_Vb` | +| 'Agilent_2_reservoir_144mL_Vb'
      Part no.: 203852-100
      [manufacturer website](https://www.agilent.com/store/en_US/Prod-203852-100/203852-100?srsltid=AfmBOorcg-D2jAf9VwaEDFvo7ZCQ-_G14cndKReIcpSBsFA0YjxNgDKm) | ![](img/agilent/Agilent_2_reservoir_144mL_Vb.jpg) | `Agilent_2_reservoir_144mL_Vb` | \ No newline at end of file diff --git a/docs/resources/library/alpaqua.md b/docs/resources/library/alpaqua.md index 7ca7f097f9a..a183b2caa61 100644 --- a/docs/resources/library/alpaqua.md +++ b/docs/resources/library/alpaqua.md @@ -7,6 +7,6 @@ Our products include a line of innovative, high performance magnet plates built ## Labware -| Description | Image | PLR definition | -|-|-|-| +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ----------------------- | | 'Alpaqua_96_magnum_flx'
      Part no.: A000400
      [manufacturer website](https://www.alpaqua.com/product/magnum-flx/) | ![](img/alpaqua/Alpaqua_96_magnum_flx.jpg) | `Alpaqua_96_magnum_flx` | diff --git a/docs/resources/library/azenta.md b/docs/resources/library/azenta.md index 34ed3ea1a28..8924b7ce1d8 100644 --- a/docs/resources/library/azenta.md +++ b/docs/resources/library/azenta.md @@ -8,6 +8,6 @@ Company wikipedia: [Azenta](https://en.wikipedia.org/wiki/Azenta) ## Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'Azenta4titudeFrameStar_96_wellplate_skirted'

      - Man. part no.: 4ti-0960
      - Supplier part no.: PCR1232
      - [manufacturer website](https://www.azenta.com/products/framestar-96-well-skirted-pcr-plate)
      - [supplier website](https://www.scientificlabs.co.uk/product/pcr-plates/PCR1232)
      - working volume: <100µl
      - total well capacity: 200µl| ![](img/azenta/azenta_4titude_96PCR_4ti-0960.jpg) | `Azenta4titudeFrameStar_96_wellplate_skirted` | +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | --------------------------------------------- | +| 'Azenta4titudeFrameStar_96_wellplate_skirted'

      - Man. part no.: 4ti-0960
      - Supplier part no.: PCR1232
      - [manufacturer website](https://www.azenta.com/products/framestar-96-well-skirted-pcr-plate)
      - [supplier website](https://www.scientificlabs.co.uk/product/pcr-plates/PCR1232)
      - working volume: <100µl
      - total well capacity: 200µl | ![](img/azenta/azenta_4titude_96PCR_4ti-0960.jpg) | `Azenta4titudeFrameStar_96_wellplate_skirted` | diff --git a/docs/resources/library/bioer.md b/docs/resources/library/bioer.md index a05a5493c31..6df79aef430 100644 --- a/docs/resources/library/bioer.md +++ b/docs/resources/library/bioer.md @@ -4,6 +4,6 @@ ## Plates -| Description | Image | PLR definition | -|-|-|-| +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------ | | 'BioER_96_wellplate_Vb_2200uL'
      Part no.: BSH06M1T-A
      [manufacturer website](https://en.bioer.com/en/ConsumablesCenter/info_itemid_3261_lcid_134.html) | ![](img/bioer/BioER_96_wellplate_Vb_2200uL.jpg) | `BioER_96_wellplate_Vb_2200uL` | diff --git a/docs/resources/library/biorad.md b/docs/resources/library/biorad.md index 183aabed8d8..2d04fb8d6b8 100644 --- a/docs/resources/library/biorad.md +++ b/docs/resources/library/biorad.md @@ -4,6 +4,6 @@ ## Plates -| Description | Image | PLR definition | -|-|-|-| +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------- | ------------------------ | | 'BioRad_384_DWP_50uL_Vb'
      Part no.: HSP3805
      [manufacturer website](https://www.bio-rad.com/en-us/sku/HSP3805-hard-shell-384-well-pcr-plates-thin-wall-skirted-clear-white?ID=HSP3805) | ![](img/biorad/BioRad_384_DWP_50uL_Vb.webp) | `BioRad_384_DWP_50uL_Vb` | diff --git a/docs/resources/library/boekel.md b/docs/resources/library/boekel.md index 5e23166b935..19d643b8ab8 100644 --- a/docs/resources/library/boekel.md +++ b/docs/resources/library/boekel.md @@ -9,9 +9,9 @@ The following rack exists in 4 orientations: - 1.5/2ml microcentrifuge tubes = `boekel_1_5mL_microcentrifuge_carrier` - ?ml microcentrifuge tubes = `boekel_mini_microcentrifuge_carrier` -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| Multi Tube Rack For 50ml Conical, 15ml Conical, And Microcentrifuge Tubes, PN:120008 [manufacturer website](https://www.boekelsci.com/multi-tube-rack-for-50ml-conical-15ml-conical-and-microcentrifuge-tubes-pn-120008.html) | ![](img/boekel/boekel_carrier50mL.jpg) | `boekel_50mL_falcon_carrier` | -| Multi Tube Rack For 50ml Conical, 15ml Conical, And Microcentrifuge Tubes, PN:120008 [manufacturer website](https://www.boekelsci.com/multi-tube-rack-for-50ml-conical-15ml-conical-and-microcentrifuge-tubes-pn-120008.html) | ![](img/boekel/boekel_carrier15mL.jpg) | `boekel_15mL_falcon_carrier` | +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | -------------------------------------- | +| Multi Tube Rack For 50ml Conical, 15ml Conical, And Microcentrifuge Tubes, PN:120008 [manufacturer website](https://www.boekelsci.com/multi-tube-rack-for-50ml-conical-15ml-conical-and-microcentrifuge-tubes-pn-120008.html) | ![](img/boekel/boekel_carrier50mL.jpg) | `boekel_50mL_falcon_carrier` | +| Multi Tube Rack For 50ml Conical, 15ml Conical, And Microcentrifuge Tubes, PN:120008 [manufacturer website](https://www.boekelsci.com/multi-tube-rack-for-50ml-conical-15ml-conical-and-microcentrifuge-tubes-pn-120008.html) | ![](img/boekel/boekel_carrier15mL.jpg) | `boekel_15mL_falcon_carrier` | | Multi Tube Rack For 50ml Conical, 15ml Conical, And Microcentrifuge Tubes, PN:120008 [manufacturer website](https://www.boekelsci.com/multi-tube-rack-for-50ml-conical-15ml-conical-and-microcentrifuge-tubes-pn-120008.html) | ![](img/boekel/boekel_carrier1_5mL.jpg) | `boekel_1_5mL_microcentrifuge_carrier` | -| Multi Tube Rack For 50ml Conical, 15ml Conical, And Microcentrifuge Tubes, PN:120008 [manufacturer website](https://www.boekelsci.com/multi-tube-rack-for-50ml-conical-15ml-conical-and-microcentrifuge-tubes-pn-120008.html) | ![](img/boekel/boekel_carrier_mini.jpg) | `boekel_mini_microcentrifuge_carrier` | +| Multi Tube Rack For 50ml Conical, 15ml Conical, And Microcentrifuge Tubes, PN:120008 [manufacturer website](https://www.boekelsci.com/multi-tube-rack-for-50ml-conical-15ml-conical-and-microcentrifuge-tubes-pn-120008.html) | ![](img/boekel/boekel_carrier_mini.jpg) | `boekel_mini_microcentrifuge_carrier` | diff --git a/docs/resources/library/celltreat.md b/docs/resources/library/celltreat.md index 5753bf26c7e..ab1fb074310 100644 --- a/docs/resources/library/celltreat.md +++ b/docs/resources/library/celltreat.md @@ -2,17 +2,17 @@ ## Plates -| Description | Image | PLR definition | -|-|-|-| -| 'CellTreat_6_DWP_16300ul_Fb'
      Part no.: 229105
      [manufacturer website](https://www.celltreat.com/product/229105/) | ![](img/celltreat/CellTreat_6_DWP_16300ul_Fb.jpg) | `CellTreat_6_DWP_16300ul_Fb` | -| Same as 229590 (229590 is sold with lids) 'CellTreat_96_wellplate_350ul_Ub'
      Part no.: 229591
      [manufacturer website](https://www.celltreat.com/product/229591/) | ![](img/celltreat/CellTreat_96_wellplate_350ul_Ub.jpg) | `CellTreat_96_wellplate_350ul_Ub` | -| 229195 and 229196 'CellTreat_96_wellplate_350ul_Fb'
      Part no.: 229195
      [manufacturer website](https://www.celltreat.com/product/229195/)
      are treated | ![](img/celltreat/CellTreat_96_wellplate_350ul_Fb.jpg) | `CellTreat_96_wellplate_350ul_Fb` | -| 229562 (not sterile), 229566 (sterile) 'CellTreat_12_troughplate_15000ul_Vb'
      [manufacturer website](https://www.celltreat.com/product/229562)
      are treated | ![](img/celltreat/CellTreat_12_troughplate_15000ul_Vb.jpg) | `CellTreat_12_troughplate_15000ul_Vb` | -| 229123, 229124
      [manufacturer website](https://www.celltreat.com/product/229123) | ![](img/celltreat/CellTreat_24_wellplate_3300ul_Fb.jpg) | `CellTreat_24_wellplate_3300ul_Fb` | +| Description | Image | PLR definition | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------- | +| 'CellTreat_6_DWP_16300ul_Fb'
      Part no.: 229105
      [manufacturer website](https://www.celltreat.com/product/229105/) | ![](img/celltreat/CellTreat_6_DWP_16300ul_Fb.jpg) | `CellTreat_6_DWP_16300ul_Fb` | +| Same as 229590 (229590 is sold with lids) 'CellTreat_96_wellplate_350ul_Ub'
      Part no.: 229591
      [manufacturer website](https://www.celltreat.com/product/229591/) | ![](img/celltreat/CellTreat_96_wellplate_350ul_Ub.jpg) | `CellTreat_96_wellplate_350ul_Ub` | +| 229195 and 229196 'CellTreat_96_wellplate_350ul_Fb'
      Part no.: 229195
      [manufacturer website](https://www.celltreat.com/product/229195/)
      are treated | ![](img/celltreat/CellTreat_96_wellplate_350ul_Fb.jpg) | `CellTreat_96_wellplate_350ul_Fb` | +| 229562 (not sterile), 229566 (sterile) 'CellTreat_12_troughplate_15000ul_Vb'
      [manufacturer website](https://www.celltreat.com/product/229562)
      are treated | ![](img/celltreat/CellTreat_12_troughplate_15000ul_Vb.jpg) | `CellTreat_12_troughplate_15000ul_Vb` | +| 229123, 229124
      [manufacturer website](https://www.celltreat.com/product/229123) | ![](img/celltreat/CellTreat_24_wellplate_3300ul_Fb.jpg) | `CellTreat_24_wellplate_3300ul_Fb` | ## Tubes -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------- | | 229414 (sterile)
      [manufacturer website](https://www.celltreat.com/product/229414/)
      - bottom_type=TubeBottomType.V | ![](img/celltreat/celltreat_15000ul_centrifuge_tube_Vb.webp) | `celltreat_15000ul_centrifuge_tube_Vb` | diff --git a/docs/resources/library/cellvis.md b/docs/resources/library/cellvis.md index 4dff0e88973..37198a54a3b 100644 --- a/docs/resources/library/cellvis.md +++ b/docs/resources/library/cellvis.md @@ -4,7 +4,7 @@ ## Plates -| Description | Image | PLR definition | -|-|-|-| +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------------------------------- | | 'CellVis_24_wellplate_3600uL_Fb'
      Part no.: P24-1.5P
      [manufacturer website](https://www.cellvis.com/_24-well-plate-with--number-1.5-glass-like-polymer-coverslip-bottom-tissue-culture-treated-for-better-cell-attachment-than-cover-glass_/product_detail.php?product_id=65) | ![](img/cellvis/CellVis_24_wellplate_3600uL_Fb.jpg) | `CellVis_24_wellplate_3600uL_Fb` | -| 'CellVis_96_wellplate_350uL_Fb'
      Part no.: P96-1.5H-N
      [manufacturer website](https://www.cellvis.com/_96-well-glass-bottom-plate-with-high-performance-number-1.5-cover-glass_/product_detail.php?product_id=50) | ![](img/cellvis/CellVis_96_wellplate_350uL_Fb.jpg) | `CellVis_96_wellplate_350uL_Fb` | +| 'CellVis_96_wellplate_350uL_Fb'
      Part no.: P96-1.5H-N
      [manufacturer website](https://www.cellvis.com/_96-well-glass-bottom-plate-with-high-performance-number-1.5-cover-glass_/product_detail.php?product_id=50) | ![](img/cellvis/CellVis_96_wellplate_350uL_Fb.jpg) | `CellVis_96_wellplate_350uL_Fb` | diff --git a/docs/resources/library/corning.md b/docs/resources/library/corning.md index 1248f83c2ef..8f0ddd2d786 100644 --- a/docs/resources/library/corning.md +++ b/docs/resources/library/corning.md @@ -41,9 +41,9 @@ Company page: [Corning - Axygen® Brand Products](https://www.corning.com/emea/e (axygen-plates)= ### Plates -| Description | Image | PLR definition | -|-|-|-| -| 'Cor_Axy_24_wellplate_10mL_Vb'
      Part no.: P-DW-10ML-24-C-S
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Genomics-&-Molecular-Biology/Automation-Consumables/Deep-Well-Plate/Axygen%C2%AE-Deep-Well-and-Assay-Plates/p/P-DW-10ML-24-C-S) | ![](img/corning_axygen/Cor_Axy_24_wellplate_10mL_Vb.jpg) | `Cor_Axy_24_wellplate_10mL_Vb` | +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------- | +| 'Cor_Axy_24_wellplate_10mL_Vb'
      Part no.: P-DW-10ML-24-C-S
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Genomics-&-Molecular-Biology/Automation-Consumables/Deep-Well-Plate/Axygen%C2%AE-Deep-Well-and-Assay-Plates/p/P-DW-10ML-24-C-S) | ![](img/corning_axygen/Cor_Axy_24_wellplate_10mL_Vb.jpg) | `Cor_Axy_24_wellplate_10mL_Vb` | | 'Cor_Axy_96_wellplate_500uL_Ub'
      Part no.: P-96-450V-C-S ("-S" indicates sterile labware)
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2c/US/en/Genomics-&-Molecular-Biology/Automation-Consumables/Deep-Well-Plate/Axygen%C2%AE-Deep-Well-and-Assay-Plates/p/P-96-450V-C-S) | ![](img/corning_axygen/Cor_Axy_96_wellplate_500uL_Ub.png) | `Cor_Axy_96_wellplate_500uL_Ub` | ## Corning - Costar @@ -51,13 +51,13 @@ Company page: [Corning - Axygen® Brand Products](https://www.corning.com/emea/e (costar-plates)= ### Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------- | | 'Cos_6_wellplate_16800ul_Fb'
      Part no.s:
      • [3335 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3335)
      • [3506 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3506)
      • [3516 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3516)
      • [3471 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3471)

      - Material: ?
      - Cleanliness: 3516: sterilized by gamma irradiation
      - Nonreversible lids with condensation rings to reduce contamination
      - Treated for optimal cell attachment
      - Cell growth area: 9.5 cm² (approx.)
      - Total volume: 16.8 mL | ![](img/corning_costar/Cos_6_wellplate_16800ul_Fb.jpg) | `Cos_6_wellplate_16800ul_Fb` | -| 'Cor_12_wellplate_6900ul_Fb'
      Part no.s:
      • [3336 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3336)
      • [3512 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3512)
      • [3513 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3513)

      - Total volume: 6.9 mL | ![](img/corning_costar/Cor_12_wellplate_6900ul_Fb.jpg) | `Cor_12_wellplate_6900ul_Fb` | -| 'Cor_24_wellplate_3470ul_Fb'
      Part no.s:
      • [3337 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3337)
      • [3524 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3524)
      • [3526 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3526)
      • [3527 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3527)
      • [3473 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3473)

      - Total volume: 3.47 mL | ![](img/corning_costar/Cor_24_wellplate_3470ul_Fb.jpg) | `Cor_24_wellplate_3470ul_Fb` | -| 'Cor_48_wellplate_1620ul_Fb'
      Part no.: 3548
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3548)

      - Material: TC-treated polystyrene
      - Cleanliness: sterile
      - Total volume: 1.62 mL | ![](img/corning_costar/Cor_48_wellplate_1620ul_Fb.jpg) | `Cor_48_wellplate_1620ul_Fb` | -| 'Cos_96_wellplate_2mL_Vb'
      Part no.: 3516
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3516)

      - Material: Polypropylene
      - Resistant to many common organic solvents (e.g., DMSO, ethanol, methanol)
      - 3960: Sterile and DNase- and RNase-free
      - Total volume: 2 mL
      - Features uniform skirt heights for greater robotic gripping surface| ![](img/corning_costar/Cos_96_wellplate_2mL_Vb.jpg) | `Cos_96_wellplate_2mL_Vb` | +| 'Cor_12_wellplate_6900ul_Fb'
      Part no.s:
      • [3336 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3336)
      • [3512 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3512)
      • [3513 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3513)

      - Total volume: 6.9 mL | ![](img/corning_costar/Cor_12_wellplate_6900ul_Fb.jpg) | `Cor_12_wellplate_6900ul_Fb` | +| 'Cor_24_wellplate_3470ul_Fb'
      Part no.s:
      • [3337 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3337)
      • [3524 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3524)
      • [3526 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3526)
      • [3527 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3527)
      • [3473 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3473)

      - Total volume: 3.47 mL | ![](img/corning_costar/Cor_24_wellplate_3470ul_Fb.jpg) | `Cor_24_wellplate_3470ul_Fb` | +| 'Cor_48_wellplate_1620ul_Fb'
      Part no.: 3548
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3548)

      - Material: TC-treated polystyrene
      - Cleanliness: sterile
      - Total volume: 1.62 mL | ![](img/corning_costar/Cor_48_wellplate_1620ul_Fb.jpg) | `Cor_48_wellplate_1620ul_Fb` | +| 'Cos_96_wellplate_2mL_Vb'
      Part no.: 3516
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3516)

      - Material: Polypropylene
      - Resistant to many common organic solvents (e.g., DMSO, ethanol, methanol)
      - 3960: Sterile and DNase- and RNase-free
      - Total volume: 2 mL
      - Features uniform skirt heights for greater robotic gripping surface | ![](img/corning_costar/Cos_96_wellplate_2mL_Vb.jpg) | `Cos_96_wellplate_2mL_Vb` | 'Cor_96_wellplate_360ul_Fb'
      Part no.: 353376
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/NL/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon®-96-well-Polystyrene-Microplates/p/353376)

      - Material: TC-treated polystyrene
      - Cleanliness: sterile
      - Total volume: 392 uL
      - Working volume: 25-340 uL | ![](img/corning_costar/Cor_96_wellplate_360ul_Fb.jpg) | `Cor_96_wellplate_360ul_Fb` | ## Falcon @@ -65,16 +65,21 @@ Company page: [Corning - Axygen® Brand Products](https://www.corning.com/emea/e (falcon-plates)= ### Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| Falcon_96_wellplate_Fl [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-cell-culture-treated-flat-bottom-microplate/087722C) | ![](img/falcon/Falcon_96_wellplate_Fl.webp) | `Falcon_96_wellplate_Fl` -| Falcon_96_wellplate_Rb [manufacturer website](https://ecatalog.corning.com/life-sciences/b2c/US/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon®-96-well-Polystyrene-Microplates/p/353077) | ![](img/falcon/Falcon_96_wellplate_Rb.jpg) | `Falcon_96_wellplate_Rb` -| Falcon_96_wellplate_Fl_Black [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-imaging-plate-lid/08772225) | ![](img/falcon/Falcon_96_wellplate_Fl_Black.jpg.webp) | `Falcon_96_wellplate_Fl_Black` +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------- | +| Falcon_96_wellplate_Fl [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-cell-culture-treated-flat-bottom-microplate/087722C) | ![](img/falcon/Falcon_96_wellplate_Fl.webp) | `Falcon_96_wellplate_Fl` | +| Cor_Falcon_96_wellplate_250ul_Rb [manufacturer website](https://ecatalog.corning.com/life-sciences/b2c/US/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon®-96-well-Polystyrene-Microplates/p/353077) | ![](img/falcon/Falcon_96_wellplate_Rb.jpg) | `Cor_Falcon_96_wellplate_250ul_Rb` | +| Falcon_96_wellplate_Fl_Black [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-imaging-plate-lid/08772225) | ![](img/falcon/Falcon_96_wellplate_Fl_Black.jpg.webp) | `Falcon_96_wellplate_Fl_Black` | +| Part number: 353072 [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-cell-culture-treated-flat-bottom-microplate/087722C) | ![](img/falcon/Falcon_96_wellplate_Fl.webp) | `Cor_Falcon_96_wellplate_275ul_Fb` | +| Part number: 353219 [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-imaging-plate-lid/08772225) | ![](img/falcon/Falcon_96_wellplate_Fl_Black.jpg.webp) | `Cor_Falcon_96_wellplate_340ul_Fb_Black` | ### Tubes -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 50mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-50ml-conical-centrifuge-tubes-2/1495949A) | ![](img/falcon/falcon-tube-50mL.webp) | `falcon_tube_50mL` -| 15mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-15ml-conical-centrifuge-tubes-5/p-193301) | ![](img/falcon/falcon-tube-15mL.webp) | `falcon_tube_15mL` -| Falcon_tube_14mL_Rb
      Corning cat. no.: 352059
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/General-Labware/Tubes/Tubes,-Round-Bottom/Falcon%C2%AE-Round-Bottom-High-clarity-Polypropylene-Tube/p/352059) | ![](img/falcon/Falcon_tube_14mL_Rb.jpg) | `Falcon_tube_14mL_Rb` +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ------------------------- | +| 50mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-50ml-conical-centrifuge-tubes-2/1495949A) | ![](img/falcon/falcon-tube-50mL.webp) | `falcon_tube_50mL` | +| 15mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-15ml-conical-centrifuge-tubes-5/p-193301) | ![](img/falcon/falcon-tube-15mL.webp) | `falcon_tube_15mL` | +| Falcon_tube_14mL_Rb
      Corning cat. no.: 352059
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/General-Labware/Tubes/Tubes,-Round-Bottom/Falcon%C2%AE-Round-Bottom-High-clarity-Polypropylene-Tube/p/352059) | ![](img/falcon/Falcon_tube_14mL_Rb.jpg) | `Falcon_tube_14mL_Rb` | +| 50mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-50ml-conical-centrifuge-tubes-2/1495949A) | ![](img/falcon/falcon-tube-50mL.webp) | `Cor_Falcon_tube_50mL_Vb` | +| 15mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-15ml-conical-centrifuge-tubes-5/p-193301) | ![](img/falcon/falcon-tube-15mL.webp) | `Cor_Falcon_tube_15mL_Vb` | +| Falcon_tube_14mL_Rb
      Corning cat. no.: 352059
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/General-Labware/Tubes/Tubes,-Round-Bottom/Falcon%C2%AE-Round-Bottom-High-clarity-Polypropylene-Tube/p/352059) | ![](img/falcon/Falcon_tube_14mL_Rb.jpg) | `Cor_Falcon_tube_14mL_Rb` | diff --git a/docs/resources/library/diy/davidnedrud.md b/docs/resources/library/diy/davidnedrud.md index f1d5923327e..78f8c81ffec 100644 --- a/docs/resources/library/diy/davidnedrud.md +++ b/docs/resources/library/diy/davidnedrud.md @@ -3,6 +3,6 @@ I created a 3D printed part for the Hamilton MFX carrier that can hold opentrons modules. ## MFX modules -| Description | Image | PLR definition | -| - | - | - | -| 'hamilton_mfx_opentrons_module'
      This 3D printed module accepts Opentrons hardware. I tested with Opentrons temperature module
      [OnShape link to part](https://cad.onshape.com/documents/71f70c40910fd15876f75d76/w/81912f5001c1f8dcb28dfd3b/e/da8c964d83d158897c596d21) | ![](../img/davidnedrud/hamilton_mfx_opentrons_module_1.jpeg) ![](../img/davidnedrud/hamilton_mfx_opentrons_module_2.jpeg)| `hamilton_mfx_opentrons_module` | +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | +| 'hamilton_mfx_opentrons_module'
      This 3D printed module accepts Opentrons hardware. I tested with Opentrons temperature module
      [OnShape link to part](https://cad.onshape.com/documents/71f70c40910fd15876f75d76/w/81912f5001c1f8dcb28dfd3b/e/da8c964d83d158897c596d21) | ![](../img/davidnedrud/hamilton_mfx_opentrons_module_1.jpeg) ![](../img/davidnedrud/hamilton_mfx_opentrons_module_2.jpeg) | `hamilton_mfx_opentrons_module` | diff --git a/docs/resources/library/diy/grindbio.md b/docs/resources/library/diy/grindbio.md index 2ab0a5bea0e..814f4a3682c 100644 --- a/docs/resources/library/diy/grindbio.md +++ b/docs/resources/library/diy/grindbio.md @@ -3,6 +3,6 @@ GrindBio created a 3D printed part for one of the Hamilton modules (cat.-no. 188042). The purpose of the part is to lower the module to better accommodate deepwell plates on top of magnet blocks. The lower height also accommodates taller labware. ## MFX modules -| Description | Image | PLR definition | -| - | - | - | -| 'Hamilton_MFX_plateholder_DWP_metal_tapped_10mm_3dprint'
      3D printed supports accept Hamilton MFX DWP Module (cat.-no. 188042 / 188042-00)
      [OnShape link to part](https://cad.onshape.com/documents/87b79aea22945656e1849b61/w/1d28384d184c23a6551facf8/e/3313021cc0b2fe3c5e005547)
      Read more about assembly [here](https://labautomation.io/t/adapters-for-hamilton-carrier-188039/6561)| ![](../img/grindbio/3d-supports-for-Hamilton-module.jpeg) | `Hamilton_MFX_plateholder_DWP_metal_tapped_10mm_3dprint` | +| Description | Image | PLR definition | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------- | +| 'Hamilton_MFX_plateholder_DWP_metal_tapped_10mm_3dprint'
      3D printed supports accept Hamilton MFX DWP Module (cat.-no. 188042 / 188042-00)
      [OnShape link to part](https://cad.onshape.com/documents/87b79aea22945656e1849b61/w/1d28384d184c23a6551facf8/e/3313021cc0b2fe3c5e005547)
      Read more about assembly [here](https://labautomation.io/t/adapters-for-hamilton-carrier-188039/6561) | ![](../img/grindbio/3d-supports-for-Hamilton-module.jpeg) | `Hamilton_MFX_plateholder_DWP_metal_tapped_10mm_3dprint` | diff --git a/docs/resources/library/eppendorf.md b/docs/resources/library/eppendorf.md index 5094690a55c..8c2c19b0984 100644 --- a/docs/resources/library/eppendorf.md +++ b/docs/resources/library/eppendorf.md @@ -10,16 +10,17 @@ Company page: [Eppendorf Wikipedia](https://en.wikipedia.org/wiki/Eppendorf_(com ## Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'Eppendorf_96_wellplate_250ul_Vb'
      Part no.: 0030133374
      [manufacturer website](https://www.eppendorf.com/gb-en/Products/Laboratory-Consumables/Plates/Eppendorf-twintec-PCR-Plates-p-0030133374)

      - Material: polycarbonate (frame), polypropylene (wells)
      - part of the twin.tec(R) product line
      - WARNING: not ANSI/SLAS 1-2004 footprint dimensions (123x81 mm^2!) ==> requires `PlateAdapter`
      - 'Can be divided into 4 segments of 24 wells each to prevent waste and save money'. | ![](img/eppendorf/Eppendorf_96_wellplate_250ul_Vb_COMPLETE.png) ![](img/eppendorf/Eppendorf_96_wellplate_250ul_Vb_DIVIDED.png) | `Eppendorf_96_wellplate_250ul_Vb` | -| 'eppendorf_96_wellplate_500ul_Vb.avif'
      Part no.: 951032107
      [manufacturer website](https://www.eppendorf.com/us-en/Products/Lab-Consumables/Plates/Protein-LoBind-Plates-p-951032107) | ![](img/eppendorf/eppendorf_96_wellplate_500ul_Vb.avif) | `'eppendorf_96_wellplate_500ul_Vb` | -| 'eppendorf_96_wellplate_1000ul_Vb.avif'
      Part no.: 951032921
      [manufacturer website](https://www.eppendorf.com/ca-en/Products/Lab-Consumables/Plates/Protein-LoBind-Plates-p-951032921) | ![](img/eppendorf/eppendorf_96_wellplate_1000ul_Vb.avif) | `'eppendorf_96_wellplate_1000ul_Vb` | -| 'eppendorf_96_wellplate_2000ul_Vb.avif'
      Part no.: 951033502
      [manufacturer website](https://www.eppendorf.com/ca-en/Products/Lab-Consumables/Plates/Protein-LoBind-Plates-p-951033502) | ![](img/eppendorf/eppendorf_96_wellplate_2000ul_Vb.avif) | `'eppendorf_96_wellplate_2000ul_Vb` | +| Description | Image | PLR definition | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------- | +| 'Eppendorf_96_wellplate_250ul_Vb'
      Part no.: 0030133374
      [manufacturer website](https://www.eppendorf.com/gb-en/Products/Laboratory-Consumables/Plates/Eppendorf-twintec-PCR-Plates-p-0030133374)

      - Material: polycarbonate (frame), polypropylene (wells)
      - part of the twin.tec(R) product line
      - WARNING: not ANSI/SLAS 1-2004 footprint dimensions (123x81 mm^2!) ==> requires `PlateAdapter`
      - 'Can be divided into 4 segments of 24 wells each to prevent waste and save money'. | ![](img/eppendorf/Eppendorf_96_wellplate_250ul_Vb_COMPLETE.png) ![](img/eppendorf/Eppendorf_96_wellplate_250ul_Vb_DIVIDED.png) | `Eppendorf_96_wellplate_250ul_Vb` | +| 'eppendorf_96_wellplate_500ul_Vb.avif'
      Part no.: 951032107
      [manufacturer website](https://www.eppendorf.com/us-en/Products/Lab-Consumables/Plates/Protein-LoBind-Plates-p-951032107) | ![](img/eppendorf/eppendorf_96_wellplate_500ul_Vb.avif) | `'eppendorf_96_wellplate_500ul_Vb` | +| 'eppendorf_96_wellplate_1000ul_Vb.avif'
      Part no.: 951032921
      [manufacturer website](https://www.eppendorf.com/ca-en/Products/Lab-Consumables/Plates/Protein-LoBind-Plates-p-951032921) | ![](img/eppendorf/eppendorf_96_wellplate_1000ul_Vb.avif) | `'eppendorf_96_wellplate_1000ul_Vb` | +| 'eppendorf_96_wellplate_2000ul_Vb.avif'
      Part no.: 951033502
      [manufacturer website](https://www.eppendorf.com/ca-en/Products/Lab-Consumables/Plates/Protein-LoBind-Plates-p-951033502) | ![](img/eppendorf/eppendorf_96_wellplate_2000ul_Vb.avif) | `'eppendorf_96_wellplate_2000ul_Vb` | ## Tubes -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| "Common eppendorf tube"
      Part no.: 022431021 (DNA), 022431081 (protein)
      [manufacturer website](https://www.fishersci.com/shop/products/dna-lobind-microcentrifuge-tubes/13698791) | ![](img/eppendorf/Eppendorf_DNA_LoBind_1_5ml_Vb.webp) | `Eppendorf_DNA_LoBind_1_5ml_Vb` `Eppendorf_Protein_LoBind_1_5ml_Vb` | -| "Common eppendorf tube"
      Part no.: 022431048
      [manufacturer website](https://www.fishersci.com/shop/products/dna-lobind-microcentrifuge-tubes/13698792) | ![](img/eppendorf/Eppendorf_DNA_LoBind_2ml_Ub.jpg) | `Eppendorf_DNA_LoBind_2ml_Ub` | +| Description | Image | PLR definition | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ----------------------------------- | +| Eppendorf LoBind tube, 1.5 mL (DNA)
      Part no.: 022431021
      [manufacturer website](https://www.fishersci.com/shop/products/dna-lobind-microcentrifuge-tubes/13698791) | ![](img/eppendorf/Eppendorf_DNA_LoBind_1_5ml_Vb.webp) | `Eppendorf_DNA_LoBind_1_5ml_Vb` | +| Eppendorf LoBind tube, 1.5 mL (protein)
      Part no.: 022431081
      [manufacturer website](https://www.fishersci.com/shop/products/dna-lobind-microcentrifuge-tubes/13698791) | ![](img/eppendorf/Eppendorf_DNA_LoBind_1_5ml_Vb.webp) | `Eppendorf_Protein_LoBind_1_5ml_Vb` | +| "Common eppendorf tube"
      Part no.: 022431048
      [manufacturer website](https://www.fishersci.com/shop/products/dna-lobind-microcentrifuge-tubes/13698792) | ![](img/eppendorf/Eppendorf_DNA_LoBind_2ml_Ub.jpg) | `Eppendorf_DNA_LoBind_2ml_Ub` | diff --git a/docs/resources/library/falcon.md b/docs/resources/library/falcon.md deleted file mode 100644 index b85a4e929ae..00000000000 --- a/docs/resources/library/falcon.md +++ /dev/null @@ -1,17 +0,0 @@ -# Falcon - -# Plates - -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| Part number: 353072 [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-cell-culture-treated-flat-bottom-microplate/087722C) | ![](img/falcon/Falcon_96_wellplate_Fl.webp) | `Cor_Falcon_96_wellplate_275ul_Fb` -| Part number: 353077 [manufacturer website](https://ecatalog.corning.com/life-sciences/b2c/US/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon®-96-well-Polystyrene-Microplates/p/353077) | ![](img/falcon/Falcon_96_wellplate_Rb.jpg) | `Cor_Falcon_96_wellplate_250ul_Rb` -| Part number: 353219 [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-imaging-plate-lid/08772225) | ![](img/falcon/Falcon_96_wellplate_Fl_Black.jpg.webp) | `Cor_Falcon_96_wellplate_340ul_Fb_Black` - -## Tubes - -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 50mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-50ml-conical-centrifuge-tubes-2/1495949A) | ![](img/falcon/falcon-tube-50mL.webp) | `Cor_Falcon_tube_50mL_Vb` -| 15mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-15ml-conical-centrifuge-tubes-5/p-193301) | ![](img/falcon/falcon-tube-15mL.webp) | `Cor_Falcon_tube_15mL_Vb` -| Falcon_tube_14mL_Rb
      Corning cat. no.: 352059
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/General-Labware/Tubes/Tubes,-Round-Bottom/Falcon%C2%AE-Round-Bottom-High-clarity-Polypropylene-Tube/p/352059) | ![](img/falcon/Falcon_tube_14mL_Rb.jpg) | `Cor_Falcon_tube_14mL_Rb` diff --git a/docs/resources/library/greiner.md b/docs/resources/library/greiner.md index a33411a1717..652151eb934 100644 --- a/docs/resources/library/greiner.md +++ b/docs/resources/library/greiner.md @@ -7,7 +7,7 @@ Company page: [Greiner Bio-One](https://www.gbo.com/en-gb/company) ## Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'Greiner_384_wellplate_28ul_Fb'
      Part no.: 784075 (white), 784076 (black), 784101 (transparent)
      [manufacturer website](https://shop.gbo.com/en/england/products/bioscience/microplates/384-well-microplates/384-well-small-volume-hibase-microplates/784075.html) | ![](img/greiner/Greiner_384_wellplate_28ul_Fb.png) | `Greiner_384_wellplate_28ul_Fb` -| 'greiner_96_wellplate_200uL_Vb'
      Part no.: 652260
      [manufacturer website](https://shop.gbo.com/en/usa/products/bioscience/molecular-biology/pcr-microplates/652260.html)
      NOTE: This is a half-skirt plate and requires a plate adapter to use| ![](img/greiner/greiner_96_wellplate_200uL_Vb.jpg) | `greiner_96_wellplate_200uL_Vb` +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------- | +| 'Greiner_384_wellplate_28ul_Fb'
      Part no.: 784075 (white), 784076 (black), 784101 (transparent)
      [manufacturer website](https://shop.gbo.com/en/england/products/bioscience/microplates/384-well-microplates/384-well-small-volume-hibase-microplates/784075.html) | ![](img/greiner/Greiner_384_wellplate_28ul_Fb.png) | `Greiner_384_wellplate_28ul_Fb` | +| 'greiner_96_wellplate_200uL_Vb'
      Part no.: 652260
      [manufacturer website](https://shop.gbo.com/en/usa/products/bioscience/molecular-biology/pcr-microplates/652260.html)
      NOTE: This is a half-skirt plate and requires a plate adapter to use | ![](img/greiner/greiner_96_wellplate_200uL_Vb.jpg) | `greiner_96_wellplate_200uL_Vb` | diff --git a/docs/resources/library/hamilton.md b/docs/resources/library/hamilton.md index 82a9d43e59f..ff505ce0e35 100644 --- a/docs/resources/library/hamilton.md +++ b/docs/resources/library/hamilton.md @@ -8,92 +8,92 @@ Company history: [Hamilton Robotics history](https://www.hamiltoncompany.com/his ### Tip carriers -| Description | Image | PLR definition | -| - | - | - | +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- | ----------------- | | 'TIP_CAR_480_A00'
      Part no.: 182085
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182085)
      Carrier for 5x 96 tip (10μl, 50μl, 300μl, 1000μl) racks or 5x 24 tip (5ml) racks (6T) | ![](img/hamilton/TIP_CAR_480_A00_182085.jpg) | `TIP_CAR_480_A00` | -| 'TIP_CAR_288_C00'
      Part no.: 182060
      [manufacturer website](https://www.hamiltoncompany.com/other-robotics/182060)
      Carrier for 3x 96 tip (10μl, 50μl, 300μl, 1000μl) racks or 3x 24 tip (5ml) racks (4T) | ![](img/hamilton/TIP_CAR_288_C00.jpg.avif) | `TIP_CAR_288_C00` | +| 'TIP_CAR_288_C00'
      Part no.: 182060
      [manufacturer website](https://www.hamiltoncompany.com/other-robotics/182060)
      Carrier for 3x 96 tip (10μl, 50μl, 300μl, 1000μl) racks or 3x 24 tip (5ml) racks (4T) | ![](img/hamilton/TIP_CAR_288_C00.jpg.avif) | `TIP_CAR_288_C00` | ### Plate carriers -| Description | Image | PLR definition | -| - | - | - | +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | ------------------ | | 'PLT_CAR_L5AC_A00'
      Part no.: 182090
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182090)
      Carrier for 5x 96 Deep Well Plates or for 5x 384 tip racks (e.g.384HEAD_384TIPS_50μl) (6T) | ![](img/hamilton/PLT_CAR_L5AC_A00_182090.jpg) | `PLT_CAR_L5AC_A00` | -| 'PLT_CAR_L5MD_A00'
      Part no.: 182365/02
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182365)
      Carries five ANSI/SLAS footprint MTPs in landscape orientation. Occupies six tracks.| ![](img/hamilton/182365-Plate-Carrier.webp) | `PLT_CAR_L5MD_A00` | -| 'PLT_CAR_P3AC'
      Part no.: 182365/03
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182365)
      Hamilton Deepwell Plate Carrier for 3 Plates (Portrait, 6 tracks wide)| ![](img/hamilton/PLT_CAR_P3AC.jpg) | `PLT_CAR_P3AC` | -| 'PLT_CAR_L5_DWP'
      Part no.: 93522-01/03
      manufacturer website?
      Hamilton Plate Carrier for 5 Plates (Landscape, 6 tracks wide). Plastic tabs. | ![](img/hamilton/PLT_CAR_L5_DWP.jpg) | `PLT_CAR_L5_DWP` | +| 'PLT_CAR_L5MD_A00'
      Part no.: 182365/02
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182365)
      Carries five ANSI/SLAS footprint MTPs in landscape orientation. Occupies six tracks. | ![](img/hamilton/182365-Plate-Carrier.webp) | `PLT_CAR_L5MD_A00` | +| 'PLT_CAR_P3AC'
      Part no.: 182365/03
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182365)
      Hamilton Deepwell Plate Carrier for 3 Plates (Portrait, 6 tracks wide) | ![](img/hamilton/PLT_CAR_P3AC.jpg) | `PLT_CAR_P3AC` | +| 'PLT_CAR_L5_DWP'
      Part no.: 93522-01/03
      manufacturer website?
      Hamilton Plate Carrier for 5 Plates (Landscape, 6 tracks wide). Plastic tabs. | ![](img/hamilton/PLT_CAR_L5_DWP.jpg) | `PLT_CAR_L5_DWP` | ### MFX carriers See [MFX Carrier documentation](/resources/carrier/mfx-carrier/mfx_carrier). -| Description | Image | PLR definition | -| - | - | - | -| 'hamilton_mfx_carrier_L5_base'
      Part no.: 188039
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188039)
      Labware carrier base for up to 5 Multiflex Modules
      Occupies 6 tracks (6T). | ![](img/hamilton/hamilton_mfx_carrier_L5_base.jpg) | `hamilton_mfx_carrier_L5_base` | -| 'MFX_CAR_L4_SHAKER'
      Part no.: 187001
      [secondary supplier website](https://www.testmart.com/estore/unit.cfm/PIPPET/HAMROB/187001/automated_pippetting_devices_and_systems/8.html) (cannot find information on Hamilton website)
      Sometimes referred to as "PLT_CAR_L4_SHAKER" by Hamilton.
      Template carrier with 4 positions for Hamilton Heater Shaker.
      Occupies 7 tracks (7T). Can be screwed onto the deck. | ![](img/hamilton/MFX_CAR_L4_SHAKER_187001.png) | `MFX_CAR_L4_SHAKER` | +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------ | +| 'hamilton_mfx_carrier_L5_base'
      Part no.: 188039
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188039)
      Labware carrier base for up to 5 Multiflex Modules
      Occupies 6 tracks (6T). | ![](img/hamilton/hamilton_mfx_carrier_L5_base.jpg) | `hamilton_mfx_carrier_L5_base` | +| 'MFX_CAR_L4_SHAKER'
      Part no.: 187001
      [secondary supplier website](https://www.testmart.com/estore/unit.cfm/PIPPET/HAMROB/187001/automated_pippetting_devices_and_systems/8.html) (cannot find information on Hamilton website)
      Sometimes referred to as "PLT_CAR_L4_SHAKER" by Hamilton.
      Template carrier with 4 positions for Hamilton Heater Shaker.
      Occupies 7 tracks (7T). Can be screwed onto the deck. | ![](img/hamilton/MFX_CAR_L4_SHAKER_187001.png) | `MFX_CAR_L4_SHAKER` | ### MFX modules See [MFX Carrier documentation](/resources/carrier/mfx-carrier/mfx_carrier). -| Description | Image | PLR definition | -| - | - | - | -| 'MFX_TIP_module'
      Part no.: 188160 or 188040
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188040)
      Module to position a high-, standard-, low volume or 5ml tip rack (but not a 384 tip rack) | ![](img/hamilton/MFX_TIP_module_188040.jpg) | `MFX_TIP_module` | -| 'hamilton_mfx_plateholder_DWP_flat'
      Part no.: 188229
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188229) (<-non-functional link?)
      MFX DWP module rack-based | ![](img/hamilton/MFX_DWP_RB_module_188229_.jpg) | `hamilton_mfx_plateholder_DWP_flat` | -| 'MFX_DWP_module_flat'
      Part no.: 6601988-01
      manufacturer website unknown | ![](img/hamilton/MFX_DWP_module_flat.jpg) | `MFX_DWP_module_flat` | -| 'Hamilton_mfx_plateholder_DWP_metal_tapped'
      Part no.: 188042
      [manufacturer website](https://www.hamiltoncompany.com/other-robotics/188042)
      Carries a single ANSI/SLAS footprint DWP, a Matrix or Micronics tube rack, or a Nunc reagent reservoir. Occupies one MFX site.| ![](img/hamilton/hamilton_MFX_plateholder_DWP_metal_tapped.png) | `Hamilton_mfx_plateholder_DWP_metal_tapped` | +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------- | +| 'MFX_TIP_module'
      Part no.: 188160 or 188040
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188040)
      Module to position a high-, standard-, low volume or 5ml tip rack (but not a 384 tip rack) | ![](img/hamilton/MFX_TIP_module_188040.jpg) | `MFX_TIP_module` | +| 'hamilton_mfx_plateholder_DWP_flat'
      Part no.: 188229
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188229) (<-non-functional link?)
      MFX DWP module rack-based | ![](img/hamilton/MFX_DWP_RB_module_188229_.jpg) | `hamilton_mfx_plateholder_DWP_flat` | +| 'MFX_DWP_module_flat'
      Part no.: 6601988-01
      manufacturer website unknown | ![](img/hamilton/MFX_DWP_module_flat.jpg) | `MFX_DWP_module_flat` | +| 'Hamilton_mfx_plateholder_DWP_metal_tapped'
      Part no.: 188042
      [manufacturer website](https://www.hamiltoncompany.com/other-robotics/188042)
      Carries a single ANSI/SLAS footprint DWP, a Matrix or Micronics tube rack, or a Nunc reagent reservoir. Occupies one MFX site. | ![](img/hamilton/hamilton_MFX_plateholder_DWP_metal_tapped.png) | `Hamilton_mfx_plateholder_DWP_metal_tapped` | ### Tube carriers Sometimes called "sample carriers" in Hamilton jargon. -| Description | Image | PLR definition | -| - | - | - | -| 'Tube_CAR_24_A00'
      Part no.: 173400
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/173400)
      Carries 24 "sample" tubes with 14.5–18 mm outer diameter, 60–120 mm high. Occupies one track. | ![](img/hamilton/Tube_CAR_24_A00.png) | `Tube_CAR_24_A00` | -| 'hamilton_tube_carrier_32_a00_insert_eppendorf_1_5mL'
      Part no.: 173410
      [manufacturer website](https://www.hamiltoncompany.com/other-robotics/173410)
      Carries 32 `Eppendorf_DNA_LoBind_1_5ml_Vb` or `Eppendorf_Protein_LoBind_1_5ml_Vb` tubes. Occupies one track. | ![](img/hamilton/Tube_CAR_32_A00.png.avif) | `hamilton_tube_carrier_32_a00_insert_eppendorf_1_5mL` | -| 'hamilton_tube_carrier_12_b00'
      Part no.: 182045
      [manufacturer website](https://www.hamiltoncompany.com/other-robotics/50-ml-falcon-tube-carrier)
      Carries 12 "sample" tubes with 30 mm outer diameter, 115 mm high. Occupies two tracks. | ![](img/hamilton/hamilton_tube_carrier_12_b00.jpg) | `hamilton_tube_carrier_12_b00` | +| Description | Image | PLR definition | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------- | +| 'Tube_CAR_24_A00'
      Part no.: 173400
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/173400)
      Carries 24 "sample" tubes with 14.5–18 mm outer diameter, 60–120 mm high. Occupies one track. | ![](img/hamilton/Tube_CAR_24_A00.png) | `Tube_CAR_24_A00` | +| 'hamilton_tube_carrier_32_a00_insert_eppendorf_1_5mL'
      Part no.: 173410
      [manufacturer website](https://www.hamiltoncompany.com/other-robotics/173410)
      Carries 32 `Eppendorf_DNA_LoBind_1_5ml_Vb` or `Eppendorf_Protein_LoBind_1_5ml_Vb` tubes. Occupies one track. | ![](img/hamilton/Tube_CAR_32_A00.png.avif) | `hamilton_tube_carrier_32_a00_insert_eppendorf_1_5mL` | +| 'hamilton_tube_carrier_12_b00'
      Part no.: 182045
      [manufacturer website](https://www.hamiltoncompany.com/other-robotics/50-ml-falcon-tube-carrier)
      Carries 12 "sample" tubes with 30 mm outer diameter, 115 mm high. Occupies two tracks. | ![](img/hamilton/hamilton_tube_carrier_12_b00.jpg) | `hamilton_tube_carrier_12_b00` | ### Trough carriers Sometimes called "reagent carriers" in Hamilton jargon. -| Description | Image | PLR definition | -| - | - | - | -| 'Trough_CAR_4R200_A00'
      Part no.: 185436 (same as 96890-01?)
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/96890-01)
      Trough carrier for 4x 200ml troughs. 2 tracks(T) wide. | ![](img/hamilton/Trough_CAR_4R200_A00.png) | `Trough_CAR_4R200_A00` | -| 'Trough_CAR_5R60_A00'
      Part no.: 53646-01
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/reagent-reservoir-carrier-5-x-60-ml-2)
      Trough carrier for 5x 60ml troughs. 1 track(T) wide. | ![](img/hamilton/Trough_CAR_5R60_A00.jpg) | `Trough_CAR_5R60_A00` | +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ---------------------- | +| 'Trough_CAR_4R200_A00'
      Part no.: 185436 (same as 96890-01?)
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/96890-01)
      Trough carrier for 4x 200ml troughs. 2 tracks(T) wide. | ![](img/hamilton/Trough_CAR_4R200_A00.png) | `Trough_CAR_4R200_A00` | +| 'Trough_CAR_5R60_A00'
      Part no.: 53646-01
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/reagent-reservoir-carrier-5-x-60-ml-2)
      Trough carrier for 5x 60ml troughs. 1 track(T) wide. | ![](img/hamilton/Trough_CAR_5R60_A00.jpg) | `Trough_CAR_5R60_A00` | ## Plate Adapters -| Description | Image | PLR definition | -| - | - | - | +| Description | Image | PLR definition | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ---------------------------- | | 'Hamilton_96_adapter_188182'
      Part no.: 188182
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188182) (<-non-functional link?)
      Adapter for 96 well PCR plate, plunged. Does not have an ANSI/SLAS footprint -> requires assignment with specified location. | ![](img/hamilton/hamilton_96_adapter_188182.png) | `Hamilton_96_adapter_188182` | ## Consumables ### TipRacks -| Description | Image | PLR definition | -| - | - | - | -| Hamilton 96 tip rack 10uL.
      Hamilton name: `LT`.
      Part number 235900 (non-sterile), 235935 (sterile).
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/10-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_10uL.jpg.avif) | `hamilton_96_tiprack_10uL` | -| Hamilton 96 tip rack 10uL filter.
      Hamilton name: `LTF`.
      Part number: 235936 (sterile), 235901 (non-sterile).
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/10-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_10uL_filter.jpg.avif) | `hamilton_96_tiprack_10uL_filter` | -| Hamilton 96 tip rack 50uL.
      Hamilton name: `TIP_50ul`
      Part number: 235966 (non-sterile), 235978 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/50-μl-co-re-ii-tips)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_50uL.jpg.avif) | `hamilton_96_tiprack_50uL` | -| Hamilton 96 tip rack 50uL filter.
      Hamilton name: `TIP_50ul_w_filter`
      Part number: 235948 (non-sterile), 235979 (sterile), 235829 (clear, non-sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/50-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_50uL_filter.jpg.avif) | `hamilton_96_tiprack_50uL_filter` | -| Hamilton 96 nested tip rack (NTR) 50uL.
      Hamilton name: ?
      Part number: 235947 (non-sterile), 235964 (clear, non-sterile), 235987 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/50-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_50uL_NTR.jpg.avif) | `hamilton_96_tiprack_50uL_NTR` | -| Hamilton 96 tip rack 300uL.
      Hamilton name: `ST`
      Part number: 235834 (clear, non-sterile), 235902 (non-sterile), 235937 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/300-μl-co-re-ii-tips)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL.jpg.avif) | `hamilton_96_tiprack_300uL` | -| Hamilton 96 tip rack 300uL filter.
      Hamilton name: `STF`
      Part number: 235830 (clear), 235903 (non-sterile), 235938 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/300-μl-co-re-ii-tips)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL.jpg.avif) | `hamilton_96_tiprack_300uL_filter` | -| Hamilton 96 tip rack 300uL filter slim.
      Hamilton name: `STF_Slim`
      Part number: 235646
      [Hamilton website](https://www.hamiltoncompany.com/consumables/300-µl-slim-conductive-tips?part-number=235646)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL_filter_slim.jpg.avif) | `hamilton_96_tiprack_300uL_filter_slim` | -| Hamilton 96 tip rack 300uL filter ultra wide bore.
      Hamilton name: `STF_ULTRAWIDE`
      Part number: 235449
      [Hamilton website](https://www.hamiltoncompany.com/consumables/300-µl-co-re-ii-wide-bore-conductive-tips?part-number=235449)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL_filter_wide.avif) | `hamilton_96_tiprack_300uL_filter_ultrawide` | -| Hamilton 96 tip rack 1000uL.
      Hamilton name: `HT`
      Part number: 235822 (clear, non-sterile), 235904 (non-sterile), 235939 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/1000-µl-co-re-ii-disposable-tips)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL.jpg.avif) | `hamilton_96_tiprack_1000uL` | -| Hamilton 96 tip rack 1000uL filter.
      Hamilton name: `HTF`
      Part number: 235820 (clear), 235905 (non-sterile), 235940 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/1000-µl-co-re-ii-disposable-tips)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL.jpg.avif) | `hamilton_96_tiprack_1000uL_filter` | -| Hamilton 96 tip rack 1000uL filter wide (1.2mm orifice Size).
      Hamilton name: `HTF_WIDE`
      Part number: 235678 (non-sterile), 235677 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables/1000-µl-co-re-ii-wide-bore-conductive-tips?part-number=235678)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL_filter_wide.jpg.avif) | `hamilton_96_tiprack_1000uL_filter_wide` | -| Hamilton 96 tip rack 1000uL filter ultra wide (3.2mm orifice Size).
      Hamilton name: `HTF_ULTRAWIDE`
      Part number: 235541 (non-sterile), 235842 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables/1000-µl-co-re-ii-wide-bore-conductive-tips?part-number=235541)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL_filter_ultrawide.jpg.avif) | `hamilton_96_tiprack_1000uL_filter_ultrawide` | -| Hamilton 24 tip rack 4000uL filter.
      Hamilton name: `FourmlTF`
      Part number: 184021 (non-sterile), 184023 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/4000-μl-5000-μl-co-re-tips)| ![](img/hamilton/tip_racks/hamilton_24_tiprack_4000uL_filter.jpg.avif) | `hamilton_24_tiprack_4000uL_filter` | -| Hamilton 24 tip rack 5000uL.
      Hamilton name: `FivemlT`
      Part number: 184020 (non-sterile), 184022 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/4000-μl-5000-μl-co-re-tips)| ![](img/hamilton/tip_racks/hamilton_24_tiprack_5000uL.jpg.avif) | `hamilton_24_tiprack_5000uL` | +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | --------------------------------------------- | +| Hamilton 96 tip rack 10uL.
      Hamilton name: `LT`.
      Part number 235900 (non-sterile), 235935 (sterile).
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/10-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_10uL.jpg.avif) | `hamilton_96_tiprack_10uL` | +| Hamilton 96 tip rack 10uL filter.
      Hamilton name: `LTF`.
      Part number: 235936 (sterile), 235901 (non-sterile).
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/10-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_10uL_filter.jpg.avif) | `hamilton_96_tiprack_10uL_filter` | +| Hamilton 96 tip rack 50uL.
      Hamilton name: `TIP_50ul`
      Part number: 235966 (non-sterile), 235978 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/50-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_50uL.jpg.avif) | `hamilton_96_tiprack_50uL` | +| Hamilton 96 tip rack 50uL filter.
      Hamilton name: `TIP_50ul_w_filter`
      Part number: 235948 (non-sterile), 235979 (sterile), 235829 (clear, non-sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/50-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_50uL_filter.jpg.avif) | `hamilton_96_tiprack_50uL_filter` | +| Hamilton 96 nested tip rack (NTR) 50uL.
      Hamilton name: ?
      Part number: 235947 (non-sterile), 235964 (clear, non-sterile), 235987 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/50-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_50uL_NTR.jpg.avif) | `hamilton_96_tiprack_50uL_NTR` | +| Hamilton 96 tip rack 300uL.
      Hamilton name: `ST`
      Part number: 235834 (clear, non-sterile), 235902 (non-sterile), 235937 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/300-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL.jpg.avif) | `hamilton_96_tiprack_300uL` | +| Hamilton 96 tip rack 300uL filter.
      Hamilton name: `STF`
      Part number: 235830 (clear), 235903 (non-sterile), 235938 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/300-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL.jpg.avif) | `hamilton_96_tiprack_300uL_filter` | +| Hamilton 96 tip rack 300uL filter slim.
      Hamilton name: `STF_Slim`
      Part number: 235646
      [Hamilton website](https://www.hamiltoncompany.com/consumables/300-µl-slim-conductive-tips?part-number=235646) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL_filter_slim.jpg.avif) | `hamilton_96_tiprack_300uL_filter_slim` | +| Hamilton 96 tip rack 300uL filter ultra wide bore.
      Hamilton name: `STF_ULTRAWIDE`
      Part number: 235449
      [Hamilton website](https://www.hamiltoncompany.com/consumables/300-µl-co-re-ii-wide-bore-conductive-tips?part-number=235449) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL_filter_wide.avif) | `hamilton_96_tiprack_300uL_filter_ultrawide` | +| Hamilton 96 tip rack 1000uL.
      Hamilton name: `HT`
      Part number: 235822 (clear, non-sterile), 235904 (non-sterile), 235939 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/1000-µl-co-re-ii-disposable-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL.jpg.avif) | `hamilton_96_tiprack_1000uL` | +| Hamilton 96 tip rack 1000uL filter.
      Hamilton name: `HTF`
      Part number: 235820 (clear), 235905 (non-sterile), 235940 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/1000-µl-co-re-ii-disposable-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL.jpg.avif) | `hamilton_96_tiprack_1000uL_filter` | +| Hamilton 96 tip rack 1000uL filter wide (1.2mm orifice Size).
      Hamilton name: `HTF_WIDE`
      Part number: 235678 (non-sterile), 235677 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables/1000-µl-co-re-ii-wide-bore-conductive-tips?part-number=235678) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL_filter_wide.jpg.avif) | `hamilton_96_tiprack_1000uL_filter_wide` | +| Hamilton 96 tip rack 1000uL filter ultra wide (3.2mm orifice Size).
      Hamilton name: `HTF_ULTRAWIDE`
      Part number: 235541 (non-sterile), 235842 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables/1000-µl-co-re-ii-wide-bore-conductive-tips?part-number=235541) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL_filter_ultrawide.jpg.avif) | `hamilton_96_tiprack_1000uL_filter_ultrawide` | +| Hamilton 24 tip rack 4000uL filter.
      Hamilton name: `FourmlTF`
      Part number: 184021 (non-sterile), 184023 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/4000-μl-5000-μl-co-re-tips) | ![](img/hamilton/tip_racks/hamilton_24_tiprack_4000uL_filter.jpg.avif) | `hamilton_24_tiprack_4000uL_filter` | +| Hamilton 24 tip rack 5000uL.
      Hamilton name: `FivemlT`
      Part number: 184020 (non-sterile), 184022 (sterile)
      [Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/4000-μl-5000-μl-co-re-tips) | ![](img/hamilton/tip_racks/hamilton_24_tiprack_5000uL.jpg.avif) | `hamilton_24_tiprack_5000uL` | ### Troughs -| Description | Image | PLR definition | -| - | - | - | -| 'Hamilton_1_trough_60mL_Vb'
      Part no.: 56694-01 (natural/white), 56694-02 (black), 56694-03 (black, bulk)
      [manufacturer website](https://www.hamiltoncompany.com/consumables/60-ml-reagent-reservoirs)
      Trough 60ml, w lid, self-standing. Barcode-compliant.
      Compatible with Trough_CAR_5R60_A00 (53646-01). | ![](img/hamilton/hamilton_1_trough_60mL_Vb_56694-01.png) | `hamilton_1_trough_60mL_Vb` | -| 'hamilton_1_trough_120ml_Vb'
      Part no.: 194052 (white/translucent)
      [manufacturer website](https://www.hamiltoncompany.com/consumables/120-ml-reagent-reservoir-self-standing-with-barcode-label?srsltid=AfmBOoobNQhgF9KPbwURilJLIDO_pJhS3AnUXMRQ5QATOYMemUnU-aUU)
      Trough 120mL, without lid, self-standing, transparent.
      Compatible with Trough_CAR_3R120 (194058). | ![](img/hamilton/hamilton_1_trough_120mL_Vb.png) | `hamilton_1_trough_120mL_Vb` | -| 'Hamilton_1_trough_200mL_Vb'
      Part no.: 56695-01 (white/translucent), 56695-02 (black/conductive)
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/56695-02)
      Trough 200ml, w lid, self-standing, Black.
      Compatible with Trough_CAR_4R200_A00 (185436). | ![](img/hamilton/hamilton_1_trough_200ml_Vb.jpg) | `hamilton_1_trough_200mL_Vb` | +| Description | Image | PLR definition | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------- | +| 'Hamilton_1_trough_60mL_Vb'
      Part no.: 56694-01 (natural/white), 56694-02 (black), 56694-03 (black, bulk)
      [manufacturer website](https://www.hamiltoncompany.com/consumables/60-ml-reagent-reservoirs)
      Trough 60ml, w lid, self-standing. Barcode-compliant.
      Compatible with Trough_CAR_5R60_A00 (53646-01). | ![](img/hamilton/hamilton_1_trough_60mL_Vb_56694-01.png) | `hamilton_1_trough_60mL_Vb` | +| 'hamilton_1_trough_120ml_Vb'
      Part no.: 194052 (white/translucent)
      [manufacturer website](https://www.hamiltoncompany.com/consumables/120-ml-reagent-reservoir-self-standing-with-barcode-label?srsltid=AfmBOoobNQhgF9KPbwURilJLIDO_pJhS3AnUXMRQ5QATOYMemUnU-aUU)
      Trough 120mL, without lid, self-standing, transparent.
      Compatible with Trough_CAR_3R120 (194058). | ![](img/hamilton/hamilton_1_trough_120mL_Vb.png) | `hamilton_1_trough_120mL_Vb` | +| 'Hamilton_1_trough_200mL_Vb'
      Part no.: 56695-01 (white/translucent), 56695-02 (black/conductive)
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/56695-02)
      Trough 200ml, w lid, self-standing, Black.
      Compatible with Trough_CAR_4R200_A00 (185436). | ![](img/hamilton/hamilton_1_trough_200ml_Vb.jpg) | `hamilton_1_trough_200mL_Vb` | diff --git a/docs/resources/library/imcs.md b/docs/resources/library/imcs.md index 91df7cbaaec..1506a49114c 100644 --- a/docs/resources/library/imcs.md +++ b/docs/resources/library/imcs.md @@ -8,6 +8,6 @@ Automated high-throughput protein purification ## TipRacks -| Description | Image | PLR definition | -| - | - | - | -|IMCS tips for automated protein purification. 300uL. Part numbers:
      • 04T-H8R80A-1-5-96
      • 04T-H8R80A-1-10-96
      • 04T-H8R80P-1-5-96
      • 04T-H8R80P-1-10-96
      • 04T-H8R72-1-2-96
      • 04T-H8R72-1-5-96
      • 04T-H8R72-1-10-96
      • 04T-H8R72Q-1-10-96
      • 04T-H8R85P-1-5-96
      • 04T-H8R85P-1-10-96
      • 04T-H8R88F-1-2-96
      • 04T-H8R88F-1-5-96
      • 04T-H8R88F-1-10-96
      • 04T-H8R89-1-10-96
      • 04T-H8D20F-1A-3-96
      • 04T-H8CD20F-1A-3-96
      • 04T-H8R68-1-5-96
      • 04T-H8R73-1-10-96
      • 04T-H8R05-1-2-96
      • 04T-H8R05-1-5-96
      • 04T-H8R41-1-2-96
      • 04T-H8R53-1-2-96
      • 04T-H8R52-1-2-96
      • 04T-H8R52-1-5-96
      • 04T-H8R30-1-2-96
      • 04T-H8R30-1-5-96
      • 04T-H8R03R-1-5-96
      • 04T-H8R02R-1-2-96
      • 04T-H8R02R-1-5-96
      • 04T-I3R73-1-10-

      [OEM website](https://imcstips.com/) | picture not available | `imcs_96_tiprack_300uL_filter` | +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------------------------------ | +| IMCS tips for automated protein purification, 300 uL.
      Part no.: 04T-H8R80A-1-5-96, 04T-H8R80A-1-10-96, 04T-H8R80P-1-5-96, 04T-H8R80P-1-10-96, 04T-H8R72-1-2-96, 04T-H8R72-1-5-96, 04T-H8R72-1-10-96, 04T-H8R72Q-1-10-96, 04T-H8R85P-1-5-96, 04T-H8R85P-1-10-96, 04T-H8R88F-1-2-96, 04T-H8R88F-1-5-96, 04T-H8R88F-1-10-96, 04T-H8R89-1-10-96, 04T-H8D20F-1A-3-96, 04T-H8CD20F-1A-3-96, 04T-H8R68-1-5-96, 04T-H8R73-1-10-96, 04T-H8R05-1-2-96, 04T-H8R05-1-5-96, 04T-H8R41-1-2-96, 04T-H8R53-1-2-96, 04T-H8R52-1-2-96, 04T-H8R52-1-5-96, 04T-H8R30-1-2-96, 04T-H8R30-1-5-96, 04T-H8R03R-1-5-96, 04T-H8R02R-1-2-96, 04T-H8R02R-1-5-96
      [OEM website](https://imcstips.com/) | | `imcs_96_tiprack_300uL_filter` | diff --git a/docs/resources/library/nest.md b/docs/resources/library/nest.md index 19ee44fdf53..3cb6c0546dc 100644 --- a/docs/resources/library/nest.md +++ b/docs/resources/library/nest.md @@ -4,10 +4,10 @@ Wuxi NEST Biotechnology Co., Ltd. a leading life science plastic consumables man ## Plates -| Description | Image | PLR definition | -|-|-|-| -| 'nest_1_troughplate_195000uL_Vb'
      Part no.: 360101
      [manufacturer website](https://www.nest-biotech.com/reagent-reserviors/59178416.html)
      - Material: polypropylene | ![](img/nest/nest_1_troughplate_195000uL_Vb.webp) | `nest_1_troughplate_195000uL_Vb` | -| 'nest_1_troughplate_185000uL_Vb'
      Part no.: 360101
      [manufacturer website](https://www.nest-biotech.com/reagent-reserviors/59178415.html)
      - Material: polypropylene | ![](img/nest/nest_1_troughplate_185000uL_Vb.webp) | `nest_1_troughplate_185000uL_Vb` | -| 'nest_8_troughplate_22000uL_Vb'
      Part no.: 360101
      [manufacturer website](https://www.nestscientificusa.com/product/detail/513006470820794368)
      - Material: polypropylene | ![](img/nest/nest_8_troughplate_22000uL_Vb.jpg) | `nest_8_troughplate_22000uL_Vb` | -| 'nest_12_troughplate_15000uL_Vb'
      Part no.: 360102
      [manufacturer website](https://www.nestscientificusa.com/product/detail/513006470820794368)
      - Material: polypropylene | ![](img/nest/nest_12_troughplate_15000uL_Vb.jpg) | `nest_12_troughplate_15000uL_Vb` | -| 'NEST_96_wellplate_2200uL_Ub'
      Part no.: 503062
      [manufacturer website](https://www.nest-biotech.com/deep-well-plates/59253727.html)
      - Material: polypropylene | ![](img/nest/NEST_96_wellplate_2200uL_Ub.png) | `NEST_96_wellplate_2200uL_Ub` | \ No newline at end of file +| Description | Image | PLR definition | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | -------------------------------- | +| 'nest_1_troughplate_195000uL_Vb'
      Part no.: 360101
      [manufacturer website](https://www.nest-biotech.com/reagent-reserviors/59178416.html)
      - Material: polypropylene | ![](img/nest/nest_1_troughplate_195000uL_Vb.webp) | `nest_1_troughplate_195000uL_Vb` | +| 'nest_1_troughplate_185000uL_Vb'
      Part no.: 360101
      [manufacturer website](https://www.nest-biotech.com/reagent-reserviors/59178415.html)
      - Material: polypropylene | ![](img/nest/nest_1_troughplate_185000uL_Vb.webp) | `nest_1_troughplate_185000uL_Vb` | +| 'nest_8_troughplate_22000uL_Vb'
      Part no.: 360101
      [manufacturer website](https://web.archive.org/web/20240909185532/https://www.nestscientificusa.com/product/detail/513006470820794368)
      - Material: polypropylene | ![](img/nest/nest_8_troughplate_22000uL_Vb.jpg) | `nest_8_troughplate_22000uL_Vb` | +| 'nest_12_troughplate_15000uL_Vb'
      Part no.: 360102
      [manufacturer website](https://web.archive.org/web/20240909185532/https://www.nestscientificusa.com/product/detail/513006470820794368)
      - Material: polypropylene | ![](img/nest/nest_12_troughplate_15000uL_Vb.jpg) | `nest_12_troughplate_15000uL_Vb` | +| 'NEST_96_wellplate_2200uL_Ub'
      Part no.: 503062
      [manufacturer website](https://www.nest-biotech.com/deep-well-plates/59253727.html)
      - Material: polypropylene | ![](img/nest/NEST_96_wellplate_2200uL_Ub.png) | `NEST_96_wellplate_2200uL_Ub` | \ No newline at end of file diff --git a/docs/resources/library/opentrons.md b/docs/resources/library/opentrons.md index fa1f2a39187..992768f311d 100644 --- a/docs/resources/library/opentrons.md +++ b/docs/resources/library/opentrons.md @@ -40,8 +40,8 @@ Unfortunately, most of the other labware (plates) is missing information that is - `opentrons_24_tuberack_nest_1_5ml_screwcap` - `opentrons_24_tuberack_nest_1_5ml_snapcap` -| Description | Image | PLR definition | -|-|-|-| +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | | 'opentrons_24_tuberack_generic_1point5ml_snapcap_short'
      Part no.: 3405002
      [manufacturer website](https://www.thingiverse.com/thing:3405002) | ![](img/opentrons/ot2-1.5mL-tube-rack-24w.png) | `opentrons_24_tuberack_generic_1point5ml_snapcap_short` | - `opentrons_24_tuberack_nest_2ml_screwcap` diff --git a/docs/resources/library/perkin_elmer.md b/docs/resources/library/perkin_elmer.md index 20d8278351b..2303621dbbc 100644 --- a/docs/resources/library/perkin_elmer.md +++ b/docs/resources/library/perkin_elmer.md @@ -2,6 +2,6 @@ ## Plates -| Description | Image | PLR definition | -|-|-|-| +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------- | | 'PerkinElmer_96_wellplate_400ul_Fb'
      Part no.: 6005680, 6005688 and 6005689
      [manufacturer website](https://www.revvity.com/product/culturplate-96-lid-50w-6055680) | ![](img/perkin_elmer/PerkinElmer_96_wellplate_400ul_Fb.webp) | `PerkinElmer_96_wellplate_400ul_Fb` | diff --git a/docs/resources/library/porvair.md b/docs/resources/library/porvair.md index cb3b8d531ab..951b596a93a 100644 --- a/docs/resources/library/porvair.md +++ b/docs/resources/library/porvair.md @@ -6,12 +6,12 @@ Company history: [Porvair Filtration Group](https://www.porvairfiltration.com/ab ## Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| +| Description | Image | PLR definition | +| --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------- | | 'Porvair_24_wellplate_Vb'
      Part no.: 390108
      [manufacturer website](https://www.microplates.com/product/78-ml-reservoir-plate-2-rows-of-12-v-bottom/) | ![](img/porvair/Porvair_24_wellplate_Vb.jpg) | `Porvair_24_wellplate_Vb` | ## Reservoirs -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'Porvair_6_reservoir_47ml_Vb'
      Part no.: 6008280
      [manufacturer website](https://www.microplates.com/product/282-ml-reservoir-plate-6-columns-v-bottom/)
      - Material: Polypropylene
      - Sterilization compatibility: Autoclaving (15 minutes at 121°C) or Gamma Irradiation
      - Chemical resistance: "High chemical resistance"
      - Temperature resistance: high: -196°C to + 121°C
      - Cleanliness: 390015: Free of detectable DNase, RNase
      - ANSI/SLAS-format for compatibility with automated systems
      - Tolerances: "Uniform external dimensions and tolerances"| ![](img/porvair/porvair_6x47_reservoir_390015.jpg) | `Porvair_6_reservoir_47ml_Vb` | +| Description | Image | PLR definition | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ----------------------------- | +| 'Porvair_6_reservoir_47ml_Vb'
      Part no.: 6008280
      [manufacturer website](https://www.microplates.com/product/282-ml-reservoir-plate-6-columns-v-bottom/)
      - Material: Polypropylene
      - Sterilization compatibility: Autoclaving (15 minutes at 121°C) or Gamma Irradiation
      - Chemical resistance: "High chemical resistance"
      - Temperature resistance: high: -196°C to + 121°C
      - Cleanliness: 390015: Free of detectable DNase, RNase
      - ANSI/SLAS-format for compatibility with automated systems
      - Tolerances: "Uniform external dimensions and tolerances" | ![](img/porvair/porvair_6x47_reservoir_390015.jpg) | `Porvair_6_reservoir_47ml_Vb` | diff --git a/docs/resources/library/revvity.md b/docs/resources/library/revvity.md index fcdacfd40da..a0189140a0d 100644 --- a/docs/resources/library/revvity.md +++ b/docs/resources/library/revvity.md @@ -6,6 +6,6 @@ Company wikipedia: [Revvity, Inc. (formerly PerkinElmer, Inc.)](https://en.wikip ## Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'Revvity_384_wellplate_28ul_Ub'
      Part no.: 6008280
      [manufacturer website](https://www.revvity.com/product/proxiplate-384-plus-50w-6008280) | ![](img/revvity/Revvity_384_wellplate_28ul_Ub.jpg) | `Revvity_384_wellplate_28ul_Ub` +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------- | +| 'Revvity_384_wellplate_28ul_Ub'
      Part no.: 6008280
      [manufacturer website](https://www.revvity.com/product/proxiplate-384-plus-50w-6008280) | ![](img/revvity/Revvity_384_wellplate_28ul_Ub.jpg) | `Revvity_384_wellplate_28ul_Ub` | diff --git a/docs/resources/library/sergi.md b/docs/resources/library/sergi.md index 0b05975b281..48a53085ce9 100644 --- a/docs/resources/library/sergi.md +++ b/docs/resources/library/sergi.md @@ -2,8 +2,8 @@ Company page: [Sergi Lab Supplies](https://sergilabsupplies.com/?srsltid=AfmBOoqk2e3QkpWxvWEtSXS4ySJVoly7hvdiji_ehH5-s6tM3gi67SMu) -## Plate Adapterrs +## Plate Adapters -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------- | | 'SergiLabSupplies_96_MagneticRack_250ul_Vb'
      Part no.: 1047
      [manufacturer website](https://sergilabsupplies.com/collections/magnetic-racks/products/96-wells-magnetic-rack-for-dna-rna-and-other-molecules-purification)
      A separator for purifying DNA, RNA or other biomolecules with magnetic beads | ![](img/sergi/SergiLabSupplies_96_MagneticRack_250ul_Vb.jpg) | `SergiLabSupplies_96_MagneticRack_250ul_Vb` | diff --git a/docs/resources/library/thermo_fisher.md b/docs/resources/library/thermo_fisher.md index 7cb7482834b..52752b1aa79 100644 --- a/docs/resources/library/thermo_fisher.md +++ b/docs/resources/library/thermo_fisher.md @@ -26,23 +26,23 @@ Thermo Fisher Scientific Inc. (TFS, aka "Thermo") ## Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'Thermo_TS_96_wellplate_1200ul_Rb'
      Part no.: AB-1127 or 10243223
      [manufacturer website](https://www.fishersci.co.uk/shop/products/product/10243223)

      - Material: Polypropylene (AB-1068, polystyrene)
      | ![](img/thermo_fisher/Thermo_TS_96_wellplate_1200ul_Rb.webp) | `Thermo_TS_96_wellplate_1200ul_Rb` | -| 'Thermo_TS_Nunc_96_wellplate_300uL_Fb'
      Part no.: 165305
      [manufacturer website](https://www.fishersci.com/shop/products/nunc-microwell-96-well-cell-culture-treated-flat-bottom-microplate/1256670#)
      | ![](img/thermo_fisher/Thermo_TS_Nunc_96_wellplate_300uL_Fb.webp) | `Thermo_TS_Nunc_96_wellplate_300uL_Fb` | +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | --------------------------------------------- | +| 'Thermo_TS_96_wellplate_1200ul_Rb'
      Part no.: AB-1127 or 10243223
      [manufacturer website](https://www.fishersci.co.uk/shop/products/product/10243223)

      - Material: Polypropylene (AB-1068, polystyrene)
      | ![](img/thermo_fisher/Thermo_TS_96_wellplate_1200ul_Rb.webp) | `Thermo_TS_96_wellplate_1200ul_Rb` | +| 'Thermo_TS_Nunc_96_wellplate_300uL_Fb'
      Part no.: 165305
      [manufacturer website](https://www.fishersci.com/shop/products/nunc-microwell-96-well-cell-culture-treated-flat-bottom-microplate/1256670#)
      | ![](img/thermo_fisher/Thermo_TS_Nunc_96_wellplate_300uL_Fb.webp) | `Thermo_TS_Nunc_96_wellplate_300uL_Fb` | | 'Thermo_AB_96_wellplate_300ul_Vb_EnduraPlate'
      Part no.: 4483354 (TFS) or 15273005 (FS) (= with barcode)
      Part no.: 16698853 (FS) (= **without** barcode)
      [manufacturer website](https://www.thermofisher.com/order/catalog/product/4483354)

      - Material: Polycarbonate, Polypropylene
      - plate_type: semi-skirted
      - product line: "MicroAmp"
      - (sub)product line: "EnduraPlate" | ![](img/thermo_fisher/Thermo_AB_96_wellplate_300ul_Vb_EnduraPlate.png) | `Thermo_AB_96_wellplate_300ul_Vb_EnduraPlate` | -| 'Thermo_Nunc_96_well_plate_1300uL_Rb'
      Part no.: 26025X | ![](img/thermo_fisher/Thermo_Nunc_96_well_plate_1300uL_Rb.jpg) | `Thermo_Nunc_96_well_plate_1300uL_Rb` | -| 'thermo_AB_96_wellplate_300ul_Vb_MicroAmp'
      Part no.: N8010560/4316813 (w/o barcode)
      Part no.: 4306737/4326659 (with barcode) | ![](img/thermo_fisher/thermo_AB_96_wellplate_300ul_Vb_MicroAmp.webp) | `thermo_AB_96_wellplate_300ul_Vb_MicroAmp` | -| 'thermo_AB_384_wellplate_40uL_Vb_MicroAmp'
      Part no.: 4309849, 4326270, 4343814 (with barcode), 4343370 (w/o barcode). | ![](img/thermo_fisher/thermo_AB_384_wellplate_40uL_Vb_MicroAmp.jpg) | `thermo_AB_384_wellplate_40uL_Vb_MicroAmp` | +| 'Thermo_Nunc_96_well_plate_1300uL_Rb'
      Part no.: 26025X | ![](img/thermo_fisher/Thermo_Nunc_96_well_plate_1300uL_Rb.jpg) | `Thermo_Nunc_96_well_plate_1300uL_Rb` | +| 'thermo_AB_96_wellplate_300ul_Vb_MicroAmp'
      Part no.: N8010560/4316813 (w/o barcode)
      Part no.: 4306737/4326659 (with barcode) | ![](img/thermo_fisher/thermo_AB_96_wellplate_300ul_Vb_MicroAmp.webp) | `thermo_AB_96_wellplate_300ul_Vb_MicroAmp` | +| 'thermo_AB_384_wellplate_40uL_Vb_MicroAmp'
      Part no.: 4309849, 4326270, 4343814 (with barcode), 4343370 (w/o barcode). | ![](img/thermo_fisher/thermo_AB_384_wellplate_40uL_Vb_MicroAmp.jpg) | `thermo_AB_384_wellplate_40uL_Vb_MicroAmp` | ## Troughs -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'thermo_nunc_1_troughplate_90000uL_Fb_omnitray'
      Part no.: 165218, 140156, 242811, 264728 | ![](img/thermo_fisher/thermo_nunc_1_troughplate_90000uL_Fb_omnitray.jpg.avif) | `thermo_nunc_1_troughplate_90000uL_Fb_omnitray` | -| 'ThermoFisherMatrixTrough8094'
      Part no.: 8094
      [manufacturer website](https://www.thermofisher.com/order/catalog/product/8094) | ![](img/thermo_fisher/ThermoFisherMatrixTrough8094.jpg.avif) | `ThermoFisherMatrixTrough8094` | -| 'thermo_TS_nalgene_1_troughplate_300mL_Fb'
      Part no.: 12001300 (non-sterile), 12001301 (sterile)
      [manufacturer website](https://www.fishersci.com/shop/products/nalgene-disposable-polypropylene-robotic-reservoirs/12565571)| ![](img/thermo_fisher/thermo_TS_nalgene_1_troughplate_300mL_Fb.jpeg) | `thermo_TS_nalgene_1_troughplate_300mL_Fb` | +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | ----------------------------------------------- | +| 'thermo_nunc_1_troughplate_90000uL_Fb_omnitray'
      Part no.: 165218, 140156, 242811, 264728 | ![](img/thermo_fisher/thermo_nunc_1_troughplate_90000uL_Fb_omnitray.jpg.avif) | `thermo_nunc_1_troughplate_90000uL_Fb_omnitray` | +| 'ThermoFisherMatrixTrough8094'
      Part no.: 8094
      [manufacturer website](https://www.thermofisher.com/order/catalog/product/8094) | ![](img/thermo_fisher/ThermoFisherMatrixTrough8094.jpg.avif) | `ThermoFisherMatrixTrough8094` | +| 'thermo_TS_nalgene_1_troughplate_300mL_Fb'
      Part no.: 12001300 (non-sterile), 12001301 (sterile)
      [manufacturer website](https://www.fishersci.com/shop/products/nalgene-disposable-polypropylene-robotic-reservoirs/12565571) | ![](img/thermo_fisher/thermo_TS_nalgene_1_troughplate_300mL_Fb.jpeg) | `thermo_TS_nalgene_1_troughplate_300mL_Fb` | ## Plate Adapters -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'thermo_AB_96_plateadapter_MicroAmp'
      Part no.: 4312063
      [manufacturer website](https://www.thermofisher.com/order/catalog/product/4312063)| ![](img/thermo_fisher/thermo_AB_96_plateadapter_MicroAmp.jpg) | `thermo_AB_96_plateadapter_MicroAmp` | +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------ | +| 'thermo_AB_96_plateadapter_MicroAmp'
      Part no.: 4312063
      [manufacturer website](https://www.thermofisher.com/order/catalog/product/4312063) | ![](img/thermo_fisher/thermo_AB_96_plateadapter_MicroAmp.jpg) | `thermo_AB_96_plateadapter_MicroAmp` | diff --git a/docs/resources/library/vwr.md b/docs/resources/library/vwr.md index 62cae2f87ce..4b4445edb3d 100644 --- a/docs/resources/library/vwr.md +++ b/docs/resources/library/vwr.md @@ -4,13 +4,13 @@ Company page: [Wikipedia](https://en.wikipedia.org/wiki/VWR_International) ## Troughs -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | -------------------------- | | 'VWRReagentReservoirs25mL'
      Part no.: 89094
      [manufacturer website](https://us.vwr.com/store/product/4694822/vwr-disposable-pipetting-reservoirs)
      Polystyrene Reservoirs | ![](img/vwr/VWRReagentReservoirs25mL.jpg) | `VWRReagentReservoirs25mL` | ## Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------- | | 'VWR_1_troughplate_195000uL_Ub'
      Part no.: 77575-302
      [manufacturer website](https://www.avantorsciences.com/us/en/product/47763965/vwr-multi-channel-polypropylene-reagent-reservoirs?isCatNumSearch=true&searchedCatalogNumber=77575-302)
      Polypropylene multi-channel reagent reservoirs | ![](img/vwr/VWR_1_troughplate_195000uL_Ub.jpg) | `VWR_1_troughplate_195000uL_Ub` | -| 'VWR_96_wellplate_2mL_Vb'
      Part no.: 76329-998
      [manufacturer website](https://us-prod2.vwr.com/store/product/26915641/vwr-96-well-deep-well-plates-with-automation-notches)
      Polypropylene multi-channel reagent reservoirs | ![](img/vwr/VWR_96_wellplate_2mL_Vb.jpg) | `VWR_96_wellplate_2mL_Vb` | +| 'VWR_96_wellplate_2mL_Vb'
      Part no.: 76329-998
      [manufacturer website](https://us.vwr.com/store/product/26915641/vwr-96-well-deep-well-plates-with-automation-notches)
      Polypropylene multi-channel reagent reservoirs | ![](img/vwr/VWR_96_wellplate_2mL_Vb.jpg) | `VWR_96_wellplate_2mL_Vb` | From a133389e07b1b46f342dbae9288860bf73b4d938 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sun, 17 May 2026 17:00:39 +0100 Subject: [PATCH 23/27] docs: point resource library at canonical defs; fix falcon_tube_50mL deprecation target Remove duplicate/deprecated-stub catalog rows and re-point them at canonical definitions: Alpaqua magnum, Azenta FrameStar, and Costar 2 mL now reference their non-deprecated names; drop the redundant Falcon stub rows (Falcon_96_wellplate_Fl/_Black, Falcon_tube_14mL_Rb, falcon_tube_15mL/50mL) whose canonical Cor_Falcon_* entries are already listed. Fix the Hamilton manufacturer heading (was 'Hamilton STAR "ML_STAR"' -> 'Hamilton'). Correct the falcon_tube_50mL deprecation message to its real target Cor_Falcon_tube_50mL_Vb (the prior 'Cor_Falcon_tube_50mL' name does not exist). No code or backward-compatibility impact: the deprecated aliases remain callable; only the rendered docs catalog changes. Docs build is clean under -W. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/resources/library/alpaqua.md | 2 +- docs/resources/library/azenta.md | 2 +- docs/resources/library/corning.md | 7 +------ docs/resources/library/hamilton.md | 2 +- pylabrobot/resources/corning/falcon/tubes.py | 2 +- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/docs/resources/library/alpaqua.md b/docs/resources/library/alpaqua.md index a183b2caa61..7ae69befb69 100644 --- a/docs/resources/library/alpaqua.md +++ b/docs/resources/library/alpaqua.md @@ -9,4 +9,4 @@ Our products include a line of innovative, high performance magnet plates built | Description | Image | PLR definition | | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ----------------------- | -| 'Alpaqua_96_magnum_flx'
      Part no.: A000400
      [manufacturer website](https://www.alpaqua.com/product/magnum-flx/) | ![](img/alpaqua/Alpaqua_96_magnum_flx.jpg) | `Alpaqua_96_magnum_flx` | +| 'alpaqua_96_plateadapter_magnum_flx'
      Part no.: A000400
      [manufacturer website](https://www.alpaqua.com/product/magnum-flx/) | ![](img/alpaqua/Alpaqua_96_magnum_flx.jpg) | `alpaqua_96_plateadapter_magnum_flx` | diff --git a/docs/resources/library/azenta.md b/docs/resources/library/azenta.md index 8924b7ce1d8..4193ea5e340 100644 --- a/docs/resources/library/azenta.md +++ b/docs/resources/library/azenta.md @@ -10,4 +10,4 @@ Company wikipedia: [Azenta](https://en.wikipedia.org/wiki/Azenta) | Description | Image | PLR definition | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | --------------------------------------------- | -| 'Azenta4titudeFrameStar_96_wellplate_skirted'

      - Man. part no.: 4ti-0960
      - Supplier part no.: PCR1232
      - [manufacturer website](https://www.azenta.com/products/framestar-96-well-skirted-pcr-plate)
      - [supplier website](https://www.scientificlabs.co.uk/product/pcr-plates/PCR1232)
      - working volume: <100µl
      - total well capacity: 200µl | ![](img/azenta/azenta_4titude_96PCR_4ti-0960.jpg) | `Azenta4titudeFrameStar_96_wellplate_skirted` | +| 'Azenta4titudeFrameStar_96_wellplate_200ul_Vb'

      - Man. part no.: 4ti-0960
      - Supplier part no.: PCR1232
      - [manufacturer website](https://www.azenta.com/products/framestar-96-well-skirted-pcr-plate)
      - [supplier website](https://www.scientificlabs.co.uk/product/pcr-plates/PCR1232)
      - working volume: <100µl
      - total well capacity: 200µl | ![](img/azenta/azenta_4titude_96PCR_4ti-0960.jpg) | `Azenta4titudeFrameStar_96_wellplate_200ul_Vb` | diff --git a/docs/resources/library/corning.md b/docs/resources/library/corning.md index 8f0ddd2d786..1ee426adb9d 100644 --- a/docs/resources/library/corning.md +++ b/docs/resources/library/corning.md @@ -57,7 +57,7 @@ Company page: [Corning - Axygen® Brand Products](https://www.corning.com/emea/e | 'Cor_12_wellplate_6900ul_Fb'
      Part no.s:
      • [3336 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3336)
      • [3512 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3512)
      • [3513 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3513)

      - Total volume: 6.9 mL | ![](img/corning_costar/Cor_12_wellplate_6900ul_Fb.jpg) | `Cor_12_wellplate_6900ul_Fb` | | 'Cor_24_wellplate_3470ul_Fb'
      Part no.s:
      • [3337 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3337)
      • [3524 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3524)
      • [3526 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3526)
      • [3527 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3527)
      • [3473 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3473)

      - Total volume: 3.47 mL | ![](img/corning_costar/Cor_24_wellplate_3470ul_Fb.jpg) | `Cor_24_wellplate_3470ul_Fb` | | 'Cor_48_wellplate_1620ul_Fb'
      Part no.: 3548
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3548)

      - Material: TC-treated polystyrene
      - Cleanliness: sterile
      - Total volume: 1.62 mL | ![](img/corning_costar/Cor_48_wellplate_1620ul_Fb.jpg) | `Cor_48_wellplate_1620ul_Fb` | -| 'Cos_96_wellplate_2mL_Vb'
      Part no.: 3516
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3516)

      - Material: Polypropylene
      - Resistant to many common organic solvents (e.g., DMSO, ethanol, methanol)
      - 3960: Sterile and DNase- and RNase-free
      - Total volume: 2 mL
      - Features uniform skirt heights for greater robotic gripping surface | ![](img/corning_costar/Cos_96_wellplate_2mL_Vb.jpg) | `Cos_96_wellplate_2mL_Vb` | +| 'Cor_96_wellplate_2mL_Vb'
      Part no.: 3516
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3516)

      - Material: Polypropylene
      - Resistant to many common organic solvents (e.g., DMSO, ethanol, methanol)
      - 3960: Sterile and DNase- and RNase-free
      - Total volume: 2 mL
      - Features uniform skirt heights for greater robotic gripping surface | ![](img/corning_costar/Cos_96_wellplate_2mL_Vb.jpg) | `Cor_96_wellplate_2mL_Vb` | 'Cor_96_wellplate_360ul_Fb'
      Part no.: 353376
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/NL/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon®-96-well-Polystyrene-Microplates/p/353376)

      - Material: TC-treated polystyrene
      - Cleanliness: sterile
      - Total volume: 392 uL
      - Working volume: 25-340 uL | ![](img/corning_costar/Cor_96_wellplate_360ul_Fb.jpg) | `Cor_96_wellplate_360ul_Fb` | ## Falcon @@ -67,9 +67,7 @@ Company page: [Corning - Axygen® Brand Products](https://www.corning.com/emea/e | Description | Image | PLR definition | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------- | -| Falcon_96_wellplate_Fl [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-cell-culture-treated-flat-bottom-microplate/087722C) | ![](img/falcon/Falcon_96_wellplate_Fl.webp) | `Falcon_96_wellplate_Fl` | | Cor_Falcon_96_wellplate_250ul_Rb [manufacturer website](https://ecatalog.corning.com/life-sciences/b2c/US/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon®-96-well-Polystyrene-Microplates/p/353077) | ![](img/falcon/Falcon_96_wellplate_Rb.jpg) | `Cor_Falcon_96_wellplate_250ul_Rb` | -| Falcon_96_wellplate_Fl_Black [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-imaging-plate-lid/08772225) | ![](img/falcon/Falcon_96_wellplate_Fl_Black.jpg.webp) | `Falcon_96_wellplate_Fl_Black` | | Part number: 353072 [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-cell-culture-treated-flat-bottom-microplate/087722C) | ![](img/falcon/Falcon_96_wellplate_Fl.webp) | `Cor_Falcon_96_wellplate_275ul_Fb` | | Part number: 353219 [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-imaging-plate-lid/08772225) | ![](img/falcon/Falcon_96_wellplate_Fl_Black.jpg.webp) | `Cor_Falcon_96_wellplate_340ul_Fb_Black` | @@ -77,9 +75,6 @@ Company page: [Corning - Axygen® Brand Products](https://www.corning.com/emea/e | Description | Image | PLR definition | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ------------------------- | -| 50mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-50ml-conical-centrifuge-tubes-2/1495949A) | ![](img/falcon/falcon-tube-50mL.webp) | `falcon_tube_50mL` | -| 15mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-15ml-conical-centrifuge-tubes-5/p-193301) | ![](img/falcon/falcon-tube-15mL.webp) | `falcon_tube_15mL` | -| Falcon_tube_14mL_Rb
      Corning cat. no.: 352059
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/General-Labware/Tubes/Tubes,-Round-Bottom/Falcon%C2%AE-Round-Bottom-High-clarity-Polypropylene-Tube/p/352059) | ![](img/falcon/Falcon_tube_14mL_Rb.jpg) | `Falcon_tube_14mL_Rb` | | 50mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-50ml-conical-centrifuge-tubes-2/1495949A) | ![](img/falcon/falcon-tube-50mL.webp) | `Cor_Falcon_tube_50mL_Vb` | | 15mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-15ml-conical-centrifuge-tubes-5/p-193301) | ![](img/falcon/falcon-tube-15mL.webp) | `Cor_Falcon_tube_15mL_Vb` | | Falcon_tube_14mL_Rb
      Corning cat. no.: 352059
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/General-Labware/Tubes/Tubes,-Round-Bottom/Falcon%C2%AE-Round-Bottom-High-clarity-Polypropylene-Tube/p/352059) | ![](img/falcon/Falcon_tube_14mL_Rb.jpg) | `Cor_Falcon_tube_14mL_Rb` | diff --git a/docs/resources/library/hamilton.md b/docs/resources/library/hamilton.md index ff505ce0e35..adc552b098c 100644 --- a/docs/resources/library/hamilton.md +++ b/docs/resources/library/hamilton.md @@ -1,4 +1,4 @@ -# Hamilton STAR "ML_STAR" +# Hamilton Company history: [Hamilton Robotics history](https://www.hamiltoncompany.com/history) diff --git a/pylabrobot/resources/corning/falcon/tubes.py b/pylabrobot/resources/corning/falcon/tubes.py index 8929b0d93ff..a637123bcfe 100644 --- a/pylabrobot/resources/corning/falcon/tubes.py +++ b/pylabrobot/resources/corning/falcon/tubes.py @@ -84,7 +84,7 @@ def Cor_Falcon_tube_14mL_Rb(name: str) -> Tube: def falcon_tube_50mL(name: str) -> Tube: raise NotImplementedError( - "falcon_tube_50mL definition is deprecated. Use Cor_Falcon_tube_50mL instead." + "falcon_tube_50mL definition is deprecated. Use Cor_Falcon_tube_50mL_Vb instead." ) From 77d705608f4c303346930f708656b7d04d304fa7 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sun, 17 May 2026 17:50:35 +0100 Subject: [PATCH 24/27] docs: use canonical factory names instead of legacy alias names The resource library catalogued 8 definitions under stale names that only resolved via the catalog extension's RESOURCE_ALIASES shim (BioRad_384_DWP, CellTreat_6_DWP, Cor_12/24/48_wellplate, Cos_6_wellplate, PLT_CAR_P3AC, Hamilton_mfx_plateholder_DWP_metal_tapped). Point each docs row at the real factory name (BioRad_384_wellplate_50uL_Vb, CellTreat_6_wellplate_16300ul_Fb, Cor_Cos_*, PLT_CAR_P3AC_A00, hamilton_mfx_plateholder_DWP_metal_tapped), closing the docs<->code naming drift. Image asset paths left unchanged. No code/back-compat impact: RESOURCE_ALIASES remains as a shim for user code; the docs simply no longer depend on it. Docs build clean under -W. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/resources/library/biorad.md | 2 +- docs/resources/library/celltreat.md | 2 +- docs/resources/library/corning.md | 8 ++++---- docs/resources/library/hamilton.md | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/resources/library/biorad.md b/docs/resources/library/biorad.md index 2d04fb8d6b8..98f19b567d4 100644 --- a/docs/resources/library/biorad.md +++ b/docs/resources/library/biorad.md @@ -6,4 +6,4 @@ | Description | Image | PLR definition | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------- | ------------------------ | -| 'BioRad_384_DWP_50uL_Vb'
      Part no.: HSP3805
      [manufacturer website](https://www.bio-rad.com/en-us/sku/HSP3805-hard-shell-384-well-pcr-plates-thin-wall-skirted-clear-white?ID=HSP3805) | ![](img/biorad/BioRad_384_DWP_50uL_Vb.webp) | `BioRad_384_DWP_50uL_Vb` | +| 'BioRad_384_wellplate_50uL_Vb'
      Part no.: HSP3805
      [manufacturer website](https://www.bio-rad.com/en-us/sku/HSP3805-hard-shell-384-well-pcr-plates-thin-wall-skirted-clear-white?ID=HSP3805) | ![](img/biorad/BioRad_384_DWP_50uL_Vb.webp) | `BioRad_384_wellplate_50uL_Vb` | diff --git a/docs/resources/library/celltreat.md b/docs/resources/library/celltreat.md index ab1fb074310..c50b70434bd 100644 --- a/docs/resources/library/celltreat.md +++ b/docs/resources/library/celltreat.md @@ -4,7 +4,7 @@ | Description | Image | PLR definition | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------- | -| 'CellTreat_6_DWP_16300ul_Fb'
      Part no.: 229105
      [manufacturer website](https://www.celltreat.com/product/229105/) | ![](img/celltreat/CellTreat_6_DWP_16300ul_Fb.jpg) | `CellTreat_6_DWP_16300ul_Fb` | +| 'CellTreat_6_wellplate_16300ul_Fb'
      Part no.: 229105
      [manufacturer website](https://www.celltreat.com/product/229105/) | ![](img/celltreat/CellTreat_6_DWP_16300ul_Fb.jpg) | `CellTreat_6_wellplate_16300ul_Fb` | | Same as 229590 (229590 is sold with lids) 'CellTreat_96_wellplate_350ul_Ub'
      Part no.: 229591
      [manufacturer website](https://www.celltreat.com/product/229591/) | ![](img/celltreat/CellTreat_96_wellplate_350ul_Ub.jpg) | `CellTreat_96_wellplate_350ul_Ub` | | 229195 and 229196 'CellTreat_96_wellplate_350ul_Fb'
      Part no.: 229195
      [manufacturer website](https://www.celltreat.com/product/229195/)
      are treated | ![](img/celltreat/CellTreat_96_wellplate_350ul_Fb.jpg) | `CellTreat_96_wellplate_350ul_Fb` | | 229562 (not sterile), 229566 (sterile) 'CellTreat_12_troughplate_15000ul_Vb'
      [manufacturer website](https://www.celltreat.com/product/229562)
      are treated | ![](img/celltreat/CellTreat_12_troughplate_15000ul_Vb.jpg) | `CellTreat_12_troughplate_15000ul_Vb` | diff --git a/docs/resources/library/corning.md b/docs/resources/library/corning.md index 1ee426adb9d..b2cbe8773fb 100644 --- a/docs/resources/library/corning.md +++ b/docs/resources/library/corning.md @@ -53,10 +53,10 @@ Company page: [Corning - Axygen® Brand Products](https://www.corning.com/emea/e | Description | Image | PLR definition | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------- | -| 'Cos_6_wellplate_16800ul_Fb'
      Part no.s:
      • [3335 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3335)
      • [3506 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3506)
      • [3516 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3516)
      • [3471 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3471)

      - Material: ?
      - Cleanliness: 3516: sterilized by gamma irradiation
      - Nonreversible lids with condensation rings to reduce contamination
      - Treated for optimal cell attachment
      - Cell growth area: 9.5 cm² (approx.)
      - Total volume: 16.8 mL | ![](img/corning_costar/Cos_6_wellplate_16800ul_Fb.jpg) | `Cos_6_wellplate_16800ul_Fb` | -| 'Cor_12_wellplate_6900ul_Fb'
      Part no.s:
      • [3336 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3336)
      • [3512 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3512)
      • [3513 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3513)

      - Total volume: 6.9 mL | ![](img/corning_costar/Cor_12_wellplate_6900ul_Fb.jpg) | `Cor_12_wellplate_6900ul_Fb` | -| 'Cor_24_wellplate_3470ul_Fb'
      Part no.s:
      • [3337 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3337)
      • [3524 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3524)
      • [3526 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3526)
      • [3527 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3527)
      • [3473 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3473)

      - Total volume: 3.47 mL | ![](img/corning_costar/Cor_24_wellplate_3470ul_Fb.jpg) | `Cor_24_wellplate_3470ul_Fb` | -| 'Cor_48_wellplate_1620ul_Fb'
      Part no.: 3548
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3548)

      - Material: TC-treated polystyrene
      - Cleanliness: sterile
      - Total volume: 1.62 mL | ![](img/corning_costar/Cor_48_wellplate_1620ul_Fb.jpg) | `Cor_48_wellplate_1620ul_Fb` | +| 'Cor_Cos_6_wellplate_16800ul_Fb'
      Part no.s:
      • [3335 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3335)
      • [3506 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3506)
      • [3516 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3516)
      • [3471 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3471)

      - Material: ?
      - Cleanliness: 3516: sterilized by gamma irradiation
      - Nonreversible lids with condensation rings to reduce contamination
      - Treated for optimal cell attachment
      - Cell growth area: 9.5 cm² (approx.)
      - Total volume: 16.8 mL | ![](img/corning_costar/Cos_6_wellplate_16800ul_Fb.jpg) | `Cor_Cos_6_wellplate_16800ul_Fb` | +| 'Cor_Cos_12_wellplate_6900ul_Fb'
      Part no.s:
      • [3336 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3336)
      • [3512 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3512)
      • [3513 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3513)

      - Total volume: 6.9 mL | ![](img/corning_costar/Cor_12_wellplate_6900ul_Fb.jpg) | `Cor_Cos_12_wellplate_6900ul_Fb` | +| 'Cor_Cos_24_wellplate_3470ul_Fb'
      Part no.s:
      • [3337 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3337)
      • [3524 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3524)
      • [3526 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3526)
      • [3527 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3527)
      • [3473 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3473)

      - Total volume: 3.47 mL | ![](img/corning_costar/Cor_24_wellplate_3470ul_Fb.jpg) | `Cor_Cos_24_wellplate_3470ul_Fb` | +| 'Cor_Cos_48_wellplate_1620ul_Fb'
      Part no.: 3548
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3548)

      - Material: TC-treated polystyrene
      - Cleanliness: sterile
      - Total volume: 1.62 mL | ![](img/corning_costar/Cor_48_wellplate_1620ul_Fb.jpg) | `Cor_Cos_48_wellplate_1620ul_Fb` | | 'Cor_96_wellplate_2mL_Vb'
      Part no.: 3516
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3516)

      - Material: Polypropylene
      - Resistant to many common organic solvents (e.g., DMSO, ethanol, methanol)
      - 3960: Sterile and DNase- and RNase-free
      - Total volume: 2 mL
      - Features uniform skirt heights for greater robotic gripping surface | ![](img/corning_costar/Cos_96_wellplate_2mL_Vb.jpg) | `Cor_96_wellplate_2mL_Vb` | 'Cor_96_wellplate_360ul_Fb'
      Part no.: 353376
      [manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/NL/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon®-96-well-Polystyrene-Microplates/p/353376)

      - Material: TC-treated polystyrene
      - Cleanliness: sterile
      - Total volume: 392 uL
      - Working volume: 25-340 uL | ![](img/corning_costar/Cor_96_wellplate_360ul_Fb.jpg) | `Cor_96_wellplate_360ul_Fb` | diff --git a/docs/resources/library/hamilton.md b/docs/resources/library/hamilton.md index adc552b098c..d66c27b095e 100644 --- a/docs/resources/library/hamilton.md +++ b/docs/resources/library/hamilton.md @@ -19,7 +19,7 @@ Company history: [Hamilton Robotics history](https://www.hamiltoncompany.com/his | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | ------------------ | | 'PLT_CAR_L5AC_A00'
      Part no.: 182090
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182090)
      Carrier for 5x 96 Deep Well Plates or for 5x 384 tip racks (e.g.384HEAD_384TIPS_50μl) (6T) | ![](img/hamilton/PLT_CAR_L5AC_A00_182090.jpg) | `PLT_CAR_L5AC_A00` | | 'PLT_CAR_L5MD_A00'
      Part no.: 182365/02
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182365)
      Carries five ANSI/SLAS footprint MTPs in landscape orientation. Occupies six tracks. | ![](img/hamilton/182365-Plate-Carrier.webp) | `PLT_CAR_L5MD_A00` | -| 'PLT_CAR_P3AC'
      Part no.: 182365/03
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182365)
      Hamilton Deepwell Plate Carrier for 3 Plates (Portrait, 6 tracks wide) | ![](img/hamilton/PLT_CAR_P3AC.jpg) | `PLT_CAR_P3AC` | +| 'PLT_CAR_P3AC_A00'
      Part no.: 182365/03
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182365)
      Hamilton Deepwell Plate Carrier for 3 Plates (Portrait, 6 tracks wide) | ![](img/hamilton/PLT_CAR_P3AC.jpg) | `PLT_CAR_P3AC_A00` | | 'PLT_CAR_L5_DWP'
      Part no.: 93522-01/03
      manufacturer website?
      Hamilton Plate Carrier for 5 Plates (Landscape, 6 tracks wide). Plastic tabs. | ![](img/hamilton/PLT_CAR_L5_DWP.jpg) | `PLT_CAR_L5_DWP` | ### MFX carriers @@ -41,7 +41,7 @@ See [MFX Carrier documentation](/resources/carrier/mfx-carrier/mfx_carrier). | 'MFX_TIP_module'
      Part no.: 188160 or 188040
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188040)
      Module to position a high-, standard-, low volume or 5ml tip rack (but not a 384 tip rack) | ![](img/hamilton/MFX_TIP_module_188040.jpg) | `MFX_TIP_module` | | 'hamilton_mfx_plateholder_DWP_flat'
      Part no.: 188229
      [manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188229) (<-non-functional link?)
      MFX DWP module rack-based | ![](img/hamilton/MFX_DWP_RB_module_188229_.jpg) | `hamilton_mfx_plateholder_DWP_flat` | | 'MFX_DWP_module_flat'
      Part no.: 6601988-01
      manufacturer website unknown | ![](img/hamilton/MFX_DWP_module_flat.jpg) | `MFX_DWP_module_flat` | -| 'Hamilton_mfx_plateholder_DWP_metal_tapped'
      Part no.: 188042
      [manufacturer website](https://www.hamiltoncompany.com/other-robotics/188042)
      Carries a single ANSI/SLAS footprint DWP, a Matrix or Micronics tube rack, or a Nunc reagent reservoir. Occupies one MFX site. | ![](img/hamilton/hamilton_MFX_plateholder_DWP_metal_tapped.png) | `Hamilton_mfx_plateholder_DWP_metal_tapped` | +| 'hamilton_mfx_plateholder_DWP_metal_tapped'
      Part no.: 188042
      [manufacturer website](https://www.hamiltoncompany.com/other-robotics/188042)
      Carries a single ANSI/SLAS footprint DWP, a Matrix or Micronics tube rack, or a Nunc reagent reservoir. Occupies one MFX site. | ![](img/hamilton/hamilton_MFX_plateholder_DWP_metal_tapped.png) | `hamilton_mfx_plateholder_DWP_metal_tapped` | ### Tube carriers From 7a147dd28ee01b5777c4d6178427eab7f6c123c0 Mon Sep 17 00:00:00 2001 From: norle Date: Mon, 18 May 2026 16:21:36 +0200 Subject: [PATCH 25/27] docs: rename catalog to library everywhere and make plr-lr docs python primary, md secondary --- ...talog.py => pylabrobot_labware_library.py} | 292 +++++++++++++++--- .../_exts/pylabrobot_labware_library_tests.py | 49 +++ ...re_catalog.css => plr_labware_library.css} | 54 ++-- ...ware_catalog.js => plr_labware_library.js} | 98 +++--- docs/api/pylabrobot.resources.rst | 4 +- docs/conf.py | 21 +- .../carrier/plate-carrier/plate_carrier.ipynb | 2 +- docs/resources/index.md | 4 +- docs/resources/{catalog.md => library.md} | 4 +- docs/resources/library_convention.md | 10 +- docs/resources/library_convention_example.md | 2 +- .../heating_shaking/hamilton.ipynb | 2 +- pylabrobot/resources/__init__.py | 7 +- pylabrobot/resources/geometry.py | 28 +- pylabrobot/resources/geometry_tests.py | 34 +- 15 files changed, 440 insertions(+), 171 deletions(-) rename docs/_exts/{pylabrobot_labware_catalog.py => pylabrobot_labware_library.py} (59%) create mode 100644 docs/_exts/pylabrobot_labware_library_tests.py rename docs/_static/{plr_labware_catalog.css => plr_labware_library.css} (89%) rename docs/_static/{plr_labware_catalog.js => plr_labware_library.js} (79%) rename docs/resources/{catalog.md => library.md} (87%) diff --git a/docs/_exts/pylabrobot_labware_catalog.py b/docs/_exts/pylabrobot_labware_library.py similarity index 59% rename from docs/_exts/pylabrobot_labware_catalog.py rename to docs/_exts/pylabrobot_labware_library.py index a56024497d0..8971d248e78 100644 --- a/docs/_exts/pylabrobot_labware_catalog.py +++ b/docs/_exts/pylabrobot_labware_library.py @@ -6,13 +6,14 @@ import re from html import escape from pathlib import Path -from typing import Any, Callable, Dict, Iterable, List, Optional, Set +from typing import Any, Callable, Dict, List, Optional, get_type_hints -from pylabrobot.resources import generate_geometry_catalog +from pylabrobot.resources import generate_geometry_library +from pylabrobot.resources.resource import Resource LIBRARY_RELATIVE_ROOT = Path("resources") / "library" -GEOMETRY_INDEX_FILENAME = "labware_geometry_index.json" +GEOMETRY_INDEX_FILENAME = "labware_library_index.json" RESOURCE_ALIASES = { "Azenta4titudeFrameStar_96_wellplate_skirted": "Azenta4titudeFrameStar_96_wellplate_200ul_Vb", "BioRad_384_DWP_50uL_Vb": "BioRad_384_wellplate_50uL_Vb", @@ -26,6 +27,56 @@ "PLT_CAR_P3AC": "PLT_CAR_P3AC_A00", } +MANUFACTURER_NAMES = { + "agenbio": "AgenBio", + "agilent": "Agilent", + "alpaqua": "Alpaqua", + "azenta": "Azenta", + "bioer": "Bioer", + "biorad": "Bio-Rad", + "boekel": "Boekel", + "btx": "BTX", + "celltreat": "CellTreat", + "cellvis": "CellVis", + "corning": "Corning Inc.", + "diy": "DIY", + "eppendorf": "Eppendorf", + "greiner": "Greiner Bio-One", + "hamilton": "Hamilton", + "imcs": "IMCS", + "nest": "NEST", + "opentrons": "Opentrons", + "perkin_elmer": "Perkin Elmer", + "porvair": "Porvair", + "revvity": "Revvity", + "sergi": "Sergi", + "tecan": "Tecan", + "thermo_fisher": "Thermo Fisher Scientific", + "vwr": "VWR", +} + +SECTION_NAMES = { + "adapters": "Adapters", + "carrier": "Carriers", + "deck": "Decks", + "hamilton_decks": "Decks", + "mfx_carriers": "MFX Carriers", + "mfx_modules": "MFX Modules", + "nimbus_decks": "Decks", + "plate_adapters": "Plate Adapters", + "plate_carriers": "Plate Carriers", + "plates": "Plates", + "tip_carriers": "Tip Carriers", + "tip_racks": "Tip Racks", + "trash": "Trash", + "trough_carriers": "Trough Carriers", + "troughs": "Troughs", + "tube_carriers": "Tube Carriers", + "tube_racks": "Tube Racks", + "vantage_decks": "Decks", + "wash": "Wash Stations", +} + def _library_doc_paths(srcdir: str) -> List[Path]: library_root = Path(srcdir) / LIBRARY_RELATIVE_ROOT @@ -33,7 +84,7 @@ def _library_doc_paths(srcdir: str) -> List[Path]: # Structural inline tags that pre-PR Sphinx/MyST rendered from these cells. -# Re-enabled here so the catalog doesn't regress files that used inline HTML. +# Re-enabled here so the library doesn't regress files that used inline HTML. _ALLOWED_TAGS = ("br", "p", "ul", "ol", "li", "b", "strong", "i", "em", "sub", "sup") _SAFE_HREF = re.compile(r"(?i)^(https?:|mailto:)") @@ -181,37 +232,15 @@ def _extract_labware_entries_from_markdown( return entries -def _iter_unique_resource_names(srcdir: str) -> Iterable[str]: - seen: Set[str] = set() - - for doc_path in _library_doc_paths(srcdir): - doc_relative_path = doc_path.relative_to(srcdir) - entries = _extract_labware_entries_from_markdown( - doc_path.read_text(encoding="utf-8"), - doc_relative_path, - ) - for entry in entries: - name = entry["definition"] - if name not in seen: - seen.add(name) - yield name - - -def _catalog_entries(srcdir: str) -> List[Dict[str, Any]]: - entries: List[Dict[str, Any]] = [] - seen: Set[str] = set() - +def _markdown_library_entries(srcdir: str) -> Dict[str, Dict[str, Any]]: + entries: Dict[str, Dict[str, Any]] = {} for doc_path in _library_doc_paths(srcdir): doc_relative_path = doc_path.relative_to(srcdir) for entry in _extract_labware_entries_from_markdown( doc_path.read_text(encoding="utf-8"), doc_relative_path, ): - definition_name = entry["definition"] - if definition_name in seen: - continue - seen.add(definition_name) - entries.append(entry) + entries.setdefault(entry["definition"], entry) return entries @@ -241,7 +270,7 @@ def _resource_factory_registry() -> Dict[str, Callable[..., Any]]: if name.startswith("_"): continue value = getattr(resources_module, name) - if callable(value): + if inspect.isfunction(value): registry[name] = value resources_root = Path(resources_module.__file__).resolve().parent @@ -249,15 +278,13 @@ def _resource_factory_registry() -> Dict[str, Callable[..., Any]]: if module_path.name == "__init__.py" or module_path.name.endswith("_tests.py"): continue relative_path = module_path.relative_to(resources_root).with_suffix("") - if "falcon" in relative_path.parts: - continue module_name = "pylabrobot.resources." + ".".join(relative_path.parts) try: module = importlib.import_module(module_name) except Exception: continue for name, value in vars(module).items(): - if name.startswith("_") or not callable(value): + if name.startswith("_") or not inspect.isfunction(value): continue registry.setdefault(name, value) @@ -303,7 +330,7 @@ def _build_resource_definition( if first.kind in ( inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD, - ): + ) and first.default is inspect.Parameter.empty: args.append(definition_name) try: @@ -312,23 +339,194 @@ def _build_resource_definition( return None -def build_labware_geometry_index(srcdir: str) -> Dict[str, Any]: - resources: Dict[str, Any] = {} - entries = _catalog_entries(srcdir) - registry = _resource_factory_registry() +def _title_from_slug(slug: str) -> str: + return slug.replace("_", " ").replace("-", " ").title() - for definition_name in [entry["definition"] for entry in entries]: - resource = _build_resource_definition(definition_name, registry) - if resource is None: - continue +def _manufacturer_from_definition(definition: Callable[..., Any]) -> str: + parts = definition.__module__.split(".") + try: + resources_index = parts.index("resources") + except ValueError: + return "Other" + + if len(parts) <= resources_index + 1: + return "Other" + + slug = parts[resources_index + 1] + return MANUFACTURER_NAMES.get(slug, _title_from_slug(slug)) + + +def _section_from_definition( + definition: Callable[..., Any], + resource: Optional[Resource] = None, +) -> str: + module_slug = definition.__module__.split(".")[-1] + if module_slug in SECTION_NAMES: + return SECTION_NAMES[module_slug] + + if resource is not None: + category = getattr(resource, "category", None) + if isinstance(category, str) and category: + return _title_from_slug(category) + + return resource.__class__.__name__ + + return _title_from_slug(module_slug) + + +def _fallback_description_html( + definition: Callable[..., Any], + resource: Optional[Resource] = None, +) -> str: + if resource is None: + docstring = inspect.getdoc(definition) + if docstring: + first_paragraph = docstring.split("\n\n", maxsplit=1)[0].strip() + if first_paragraph: + return _render_cell_html(first_paragraph) + return "" + + details = [ + f"Type: {escape(resource.__class__.__name__)}", + ( + "Size: " + f"{resource.get_size_x():g} x {resource.get_size_y():g} x {resource.get_size_z():g} mm" + ), + ] + if resource.model is not None: + details.append(f"Model: {escape(resource.model)}") + return "
      ".join(details) + + +def _enrichment_for_definition( + definition_name: str, + markdown_entries: Dict[str, Dict[str, Any]], +) -> Optional[Dict[str, Any]]: + if definition_name in markdown_entries: + return markdown_entries[definition_name] + + aliases = [ + alias + for alias, target in RESOURCE_ALIASES.items() + if target == definition_name and alias in markdown_entries + ] + if len(aliases) > 0: + return markdown_entries[aliases[0]] + + return None + + +def _library_entry_from_resource( + definition_name: str, + definition: Callable[..., Any], + resource: Optional[Resource], + markdown_entries: Dict[str, Dict[str, Any]], +) -> Dict[str, Any]: + enrichment = _enrichment_for_definition(definition_name, markdown_entries) + if enrichment is not None: + entry = dict(enrichment) + entry["definition"] = definition_name + entry["section"] = entry.get("section") or _section_from_definition(definition, resource) + entry["section_path"] = entry.get("section_path") or [entry["section"]] + if not entry.get("description_html"): + entry["description_html"] = _fallback_description_html(definition, resource) + return entry + + section = _section_from_definition(definition, resource) + return { + "definition": definition_name, + "manufacturer": _manufacturer_from_definition(definition), + "section": section, + "section_path": [section], + "description_html": _fallback_description_html(definition, resource), + "image": None, + "page": "resources/library.html", + } + + +def _is_resource_factory(definition: Callable[..., Any]) -> bool: + try: + hints = get_type_hints(definition) + except Exception: + hints = {} + + return_type = hints.get("return", inspect.signature(definition).return_annotation) + if return_type is inspect.Signature.empty: + return False + + if inspect.isclass(return_type): try: - resources[definition_name] = generate_geometry_catalog(resource) - except Exception: + return issubclass(return_type, Resource) + except TypeError: + return False + + return_annotation = str(return_type) + return any( + resource_type in return_annotation + for resource_type in ( + "Resource", + "Plate", + "Lid", + "TipRack", + "TubeRack", + "Carrier", + "Trough", + "Trash", + "Deck", + "PlateAdapter", + "ResourceHolder", + "PetriDish", + "Tecan", + ) + ) + + +def _should_instantiate_factory(definition: Callable[..., Any]) -> bool: + # These Opentrons factories download JSON labware definitions as part of + # construction. Discovery should remain deterministic and offline-friendly. + return definition.__module__ not in { + "pylabrobot.resources.opentrons.adapters", + "pylabrobot.resources.opentrons.tip_racks", + "pylabrobot.resources.opentrons.tube_racks", + } + + +def build_labware_library_index(srcdir: str) -> Dict[str, Any]: + resources: Dict[str, Any] = {} + entries: List[Dict[str, Any]] = [] + markdown_entries = _markdown_library_entries(srcdir) + registry = _resource_factory_registry() + + for definition_name in sorted(registry): + definition = registry[definition_name] + resource = ( + _build_resource_definition(definition_name, registry) + if _should_instantiate_factory(definition) + else None + ) + if not isinstance(resource, Resource) and not _is_resource_factory(definition): continue - for entry in entries: - entry["has_geometry"] = entry["definition"] in resources + entry = _library_entry_from_resource( + definition_name, + definition, + resource if isinstance(resource, Resource) else None, + markdown_entries, + ) + if isinstance(resource, Resource): + try: + resources[definition_name] = generate_geometry_library(resource) + except Exception: + pass + entry["has_geometry"] = definition_name in resources + entries.append(entry) + + entries.sort(key=lambda entry: ( + entry["manufacturer"].lower(), + entry["section"].lower(), + entry["definition"].lower(), + )) return { "items": entries, @@ -341,7 +539,7 @@ def _write_geometry_index(app) -> None: if app.builder.format != "html": return - geometry_index = build_labware_geometry_index(app.srcdir) + geometry_index = build_labware_library_index(app.srcdir) target_dir = Path(app.outdir) / "_static" target_dir.mkdir(parents=True, exist_ok=True) target_path = target_dir / GEOMETRY_INDEX_FILENAME diff --git a/docs/_exts/pylabrobot_labware_library_tests.py b/docs/_exts/pylabrobot_labware_library_tests.py new file mode 100644 index 00000000000..6af50341a86 --- /dev/null +++ b/docs/_exts/pylabrobot_labware_library_tests.py @@ -0,0 +1,49 @@ +import sys +import unittest +from pathlib import Path + +sys.path.append(str(Path(__file__).resolve().parent)) + +from pylabrobot_labware_library import ( + _markdown_library_entries, + _resource_factory_registry, + _should_instantiate_factory, + build_labware_library_index, +) + + +class LabwareLibraryTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.index = build_labware_library_index("docs") + cls.items_by_definition = { + item["definition"]: item + for item in cls.index["items"] + } + + def test_library_includes_python_only_resource_definitions(self): + markdown_entries = _markdown_library_entries("docs") + + self.assertNotIn("Microplate_96_Well", markdown_entries) + self.assertIn("Microplate_96_Well", self.items_by_definition) + self.assertTrue(self.items_by_definition["Microplate_96_Well"]["has_geometry"]) + + def test_library_keeps_markdown_enrichment_when_available(self): + item = self.items_by_definition["hamilton_96_tiprack_1000uL_filter"] + + self.assertEqual(item["manufacturer"], "Hamilton") + self.assertEqual(item["section_path"], ["Consumables", "TipRacks"]) + self.assertIsNotNone(item["image"]) + self.assertTrue(item["has_geometry"]) + + def test_offline_opentrons_factories_are_listed_without_instantiation(self): + registry = _resource_factory_registry() + definition = registry["opentrons_96_tiprack_300ul"] + + self.assertFalse(_should_instantiate_factory(definition)) + self.assertIn("opentrons_96_tiprack_300ul", self.items_by_definition) + self.assertFalse(self.items_by_definition["opentrons_96_tiprack_300ul"]["has_geometry"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/_static/plr_labware_catalog.css b/docs/_static/plr_labware_library.css similarity index 89% rename from docs/_static/plr_labware_catalog.css rename to docs/_static/plr_labware_library.css index 547e40f66e0..b4be13215d1 100644 --- a/docs/_static/plr_labware_catalog.css +++ b/docs/_static/plr_labware_library.css @@ -1,4 +1,4 @@ -.plr-catalog-toolbar { +.plr-library-toolbar { display: grid; grid-template-columns: minmax(240px, 1fr) minmax(160px, 220px) minmax(160px, 220px) auto; gap: 0.75rem; @@ -10,22 +10,22 @@ background: var(--pst-color-surface); } -.plr-catalog-search, -.plr-catalog-filter { +.plr-library-search, +.plr-library-filter { display: grid; gap: 0.35rem; } -.plr-catalog-search label, -.plr-catalog-filter label { +.plr-library-search label, +.plr-library-filter label { color: var(--pst-color-text-muted); font-size: 0.78rem; font-weight: 700; text-transform: uppercase; } -.plr-catalog-search input, -.plr-catalog-filter select { +.plr-library-search input, +.plr-library-filter select { width: 100%; min-height: 2.4rem; border: 1px solid var(--pst-color-border); @@ -36,31 +36,31 @@ padding: 0.45rem 0.65rem; } -.plr-catalog-count { +.plr-library-count { color: var(--pst-color-text-muted); font-size: 0.9rem; white-space: nowrap; } -.plr-catalog-grid { +.plr-library-grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(285px, 1fr)); margin: 1rem 0 2rem; } -.plr-catalog-group + .plr-catalog-group { +.plr-library-group + .plr-library-group { margin-top: 1rem; } -.plr-catalog-group__title { +.plr-library-group__title { margin: 0.5rem 0 0.25rem; color: var(--pst-color-text-base); font-size: 1.1rem; } -.plr-catalog-loading, -.plr-catalog-empty { +.plr-library-loading, +.plr-library-empty { padding: 1rem; border: 1px solid var(--pst-color-border); border-radius: 8px; @@ -307,11 +307,11 @@ } @media (max-width: 900px) { - .plr-catalog-toolbar { + .plr-library-toolbar { grid-template-columns: 1fr; } - .plr-catalog-count { + .plr-library-count { white-space: normal; } @@ -326,7 +326,7 @@ } } -.plr-catalog-manufacturer { +.plr-library-manufacturer { margin-bottom: 1.25rem; padding: 0.75rem 1rem; border: 1px solid var(--pst-color-border, rgba(128, 128, 128, 0.3)); @@ -334,7 +334,7 @@ background: var(--pst-color-surface, rgba(128, 128, 128, 0.05)); } -.plr-catalog-manufacturer > summary { +.plr-library-manufacturer > summary { cursor: pointer; font-weight: 600; display: flex; @@ -344,11 +344,11 @@ user-select: none; } -.plr-catalog-manufacturer > summary::-webkit-details-marker { +.plr-library-manufacturer > summary::-webkit-details-marker { display: none; } -.plr-catalog-manufacturer > summary::before { +.plr-library-manufacturer > summary::before { content: "▸"; display: inline-block; margin-right: 0.1rem; @@ -356,30 +356,30 @@ transition: transform 0.15s ease; } -.plr-catalog-manufacturer[open] > summary::before { +.plr-library-manufacturer[open] > summary::before { transform: rotate(90deg); } -.plr-catalog-manufacturer > summary:hover { +.plr-library-manufacturer > summary:hover { color: var(--pst-color-primary, inherit); } -.plr-catalog-manufacturer__name { +.plr-library-manufacturer__name { font-size: 1.05rem; } -.plr-catalog-manufacturer__meta { +.plr-library-manufacturer__meta { font-size: 0.85rem; opacity: 0.7; } -.plr-catalog-manufacturer__company { +.plr-library-manufacturer__company { display: inline-block; margin: 0.5rem 0 0.25rem; font-size: 0.9rem; } -.plr-catalog-manufacturer__subhead { +.plr-library-manufacturer__subhead { margin: 0.75rem 0 0.25rem; font-size: 0.78rem; font-weight: 700; @@ -388,14 +388,14 @@ opacity: 0.6; } -.plr-catalog-manufacturer__blurb { +.plr-library-manufacturer__blurb { margin: 0.5rem 0 0; font-size: 0.9rem; line-height: 1.55; opacity: 0.85; } -.plr-catalog-manufacturer__tree { +.plr-library-manufacturer__tree { margin: 0; padding: 0.6rem 0.8rem; overflow-x: auto; diff --git a/docs/_static/plr_labware_catalog.js b/docs/_static/plr_labware_library.js similarity index 79% rename from docs/_static/plr_labware_catalog.js rename to docs/_static/plr_labware_library.js index dc0e2e58456..2b98d7aaa7e 100644 --- a/docs/_static/plr_labware_catalog.js +++ b/docs/_static/plr_labware_library.js @@ -24,14 +24,14 @@ return `${getUrlRoot()}${path}`; } - function catalogIndexUrl() { + function libraryIndexUrl() { const currentScript = document.currentScript || - document.querySelector('script[src*="plr_labware_catalog.js"]'); + document.querySelector('script[src*="plr_labware_library.js"]'); if (currentScript && currentScript.src) { - return new URL("labware_geometry_index.json", currentScript.src).toString(); + return new URL("labware_library_index.json", currentScript.src).toString(); } - return staticUrl("_static/labware_geometry_index.json"); + return staticUrl("_static/labware_library_index.json"); } function element(tagName, className, text) { @@ -132,8 +132,8 @@ function openModel(definitionName) { const modal = ensureModal(); - const catalog = state.index.resources[definitionName]; - if (!modal || !catalog || !state.viewer) { + const library = state.index.resources[definitionName]; + if (!modal || !library || !state.viewer) { return; } @@ -144,7 +144,7 @@ modalTitle.appendChild(modalCode); modal.removeAttribute("hidden"); document.body.classList.add("plr-library-modal-open"); - state.viewer.setCatalog(catalog); + state.viewer.setCatalog(library); window.requestAnimationFrame(() => state.viewer.resize()); } @@ -217,22 +217,22 @@ function createManufacturerPanel(name, items) { const meta = (state.index.manufacturers || {})[name] || {}; const details = document.createElement("details"); - details.className = "plr-catalog-manufacturer"; + details.className = "plr-library-manufacturer"; details.open = false; const breakdown = Array.from(buildSectionTree(items).children.entries()) .map(([title, node]) => `${title} (${node.count})`) .join(" · "); const summary = document.createElement("summary"); - summary.appendChild(element("span", "plr-catalog-manufacturer__name", name)); + summary.appendChild(element("span", "plr-library-manufacturer__name", name)); summary.appendChild( - element("span", "plr-catalog-manufacturer__meta", breakdown), + element("span", "plr-library-manufacturer__meta", breakdown), ); details.appendChild(summary); if (meta.company_url) { const link = document.createElement("a"); - link.className = "plr-catalog-manufacturer__company"; + link.className = "plr-library-manufacturer__company"; link.href = meta.company_url; link.target = "_blank"; link.rel = "noopener noreferrer"; @@ -244,30 +244,30 @@ if (meta.blurb) { details.appendChild( - element("p", "plr-catalog-manufacturer__blurb", meta.blurb), + element("p", "plr-library-manufacturer__blurb", meta.blurb), ); } if (meta.brand_tree) { details.appendChild( - element("div", "plr-catalog-manufacturer__subhead", "Brand structure"), + element("div", "plr-library-manufacturer__subhead", "Brand structure"), ); - const pre = element("pre", "plr-catalog-manufacturer__tree", meta.brand_tree); + const pre = element("pre", "plr-library-manufacturer__tree", meta.brand_tree); details.appendChild(pre); } return details; } - function renderCatalog(root) { + function renderLibrary(root) { const items = (state.index.items || []).filter(itemMatchesFilters); - const results = root.querySelector(".plr-catalog-results"); - const count = root.querySelector(".plr-catalog-count"); + const results = root.querySelector(".plr-library-results"); + const count = root.querySelector(".plr-library-count"); results.innerHTML = ""; count.textContent = `${items.length} resources`; if (items.length === 0) { - results.appendChild(element("div", "plr-catalog-empty", "No labware matches these filters.")); + results.appendChild(element("div", "plr-library-empty", "No labware matches these filters.")); return; } @@ -276,7 +276,7 @@ } if (state.section !== "All") { - const grid = element("div", "plr-catalog-grid"); + const grid = element("div", "plr-library-grid"); items.forEach((item) => grid.appendChild(createCard(item))); results.appendChild(grid); return; @@ -290,38 +290,38 @@ }); groups.forEach((groupItems, sectionName) => { - const group = element("section", "plr-catalog-group"); - group.appendChild(element("h2", "plr-catalog-group__title", sectionName)); - const grid = element("div", "plr-catalog-grid"); + const group = element("section", "plr-library-group"); + group.appendChild(element("h2", "plr-library-group__title", sectionName)); + const grid = element("div", "plr-library-grid"); groupItems.forEach((item) => grid.appendChild(createCard(item))); group.appendChild(grid); results.appendChild(group); }); } - function renderCatalogShell(root) { + function renderLibraryShell(root) { root.innerHTML = ` -
      -