diff --git a/app/crud/attachment_point.py b/app/crud/attachment_point.py index f58594d..b582bd5 100644 --- a/app/crud/attachment_point.py +++ b/app/crud/attachment_point.py @@ -2,6 +2,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.attachment_point import AttachmentPoint +from app.models.attachment_point_name import AttachmentPointName _RELATIONSHIPS = [ "attachment_point_name", @@ -27,6 +28,24 @@ async def list_for_augmented_part( return list(result.scalars().all()) +async def list_by_augmented_parts_and_name( + db: AsyncSession, augmented_part_ids: list[int], name: str +) -> list[AttachmentPoint]: + """AttachmentPoints named ``name`` for the given augmented parts, fetched in + one query.""" + if not augmented_part_ids: + return [] + result = await db.execute( + select(AttachmentPoint) + .join(AttachmentPoint.attachment_point_name) + .where( + AttachmentPoint.augmented_part_id.in_(augmented_part_ids), + AttachmentPointName.name == name, + ) + ) + return list(result.scalars().all()) + + async def list_all( db: AsyncSession, skip: int = 0, limit: int = 100 ) -> list[AttachmentPoint]: diff --git a/app/services/scope_guide.py b/app/services/scope_guide.py index a3f7d87..e720423 100644 --- a/app/services/scope_guide.py +++ b/app/services/scope_guide.py @@ -7,6 +7,7 @@ the parts of that type that fit the row's scope. from sqlalchemy.ext.asyncio import AsyncSession +from app.crud import attachment_point as attachment_point_crud from app.crud import augmented_part as augmented_part_crud from app.crud import fit as fit_crud from app.crud import part_type as part_type_crud @@ -14,6 +15,7 @@ from app.crud import scope_info as scope_info_crud from app.models.fit import FitLabel SCOPE_TYPE_NAME = "Scope" +OBJECTIVE_ATTACHMENT_POINT_NAME = "Objective" # "exact" outranks "possible" when the same part fits a scope via several fits. _LABEL_RANK = {FitLabel.EXACT.value: 2, FitLabel.POSSIBLE.value: 1} @@ -43,6 +45,7 @@ async def build( { "scope": AugmentedPart, "info": ScopeInfo | None, + "objective_diameter_mm": float | None, "cells": {part_type_id: [{"part": AugmentedPart, "label": str}]}, }, ..., @@ -50,12 +53,8 @@ async def build( } """ part_types = await part_type_crud.list_all(db, limit=1000) - scope_type = next( - (pt for pt in part_types if pt.name == SCOPE_TYPE_NAME), None - ) - column_types = [ - pt for pt in part_types if scope_type is None or pt.id != scope_type.id - ] + scope_type = next((pt for pt in part_types if pt.name == SCOPE_TYPE_NAME), None) + column_types = [pt for pt in part_types if scope_type is None or pt.id != scope_type.id] if scope_type is None: return {"column_types": column_types, "rows": []} @@ -73,9 +72,7 @@ async def build( if not scope_ids: return {"column_types": column_types, "rows": []} - fits = await fit_crud.list_for_parts( - db, list(scope_ids), exclude_label=FitLabel.NONE - ) + fits = await fit_crud.list_for_parts(db, list(scope_ids), exclude_label=FitLabel.NONE) # Resolve the "other" (non-scope) part on each fit in a single batched query. other_ids: set[int] = set() @@ -83,20 +80,31 @@ async def build( scope_id, other_id = _scope_and_other(fit, scope_ids) if scope_id is not None and other_id not in scope_ids: other_ids.add(other_id) - other_parts = { - p.id: p for p in await augmented_part_crud.get_many(db, list(other_ids)) - } + other_parts = {p.id: p for p in await augmented_part_crud.get_many(db, list(other_ids))} infos = { info.augmented_part_id: info for info in await scope_info_crud.list_by_augmented_parts(db, list(scope_ids)) } + # The lens outside diameter lives on the "Objective" attachment point's + # StaticConnection, not on ScopeInfo (which only carries the objective + # *thread* spec) — see app/services/scope_import.py's column-mapping doc. + objective_diameters: dict[int, float] = {} + objective_aps = await attachment_point_crud.list_by_augmented_parts_and_name( + db, list(scope_ids), OBJECTIVE_ATTACHMENT_POINT_NAME + ) + for ap in objective_aps: + if ap.static_connection is not None: + objective_diameters[ap.augmented_part_id] = ap.static_connection.od_mm + elif ap.iso_thread is not None: + objective_diameters[ap.augmented_part_id] = ap.iso_thread.diameter_mm + elif ap.unc_thread is not None: + objective_diameters[ap.augmented_part_id] = float(ap.unc_thread.diameter_fraction) + # cell_map[scope_id][type_id][other_part_id] -> {"part", "label"}, dedup'd so a # part appears once per cell, keeping its strongest fit label. - cell_map: dict[int, dict[int, dict[int, dict]]] = { - sid: {} for sid in scope_ids - } + cell_map: dict[int, dict[int, dict[int, dict]]] = {sid: {} for sid in scope_ids} for fit in fits: scope_id, other_id = _scope_and_other(fit, scope_ids) if scope_id is None: @@ -113,15 +121,18 @@ async def build( rows = [] for scope in scopes: cells = { - type_id: sorted( - entries.values(), key=lambda e: e["part"].description.lower() - ) + type_id: sorted(entries.values(), key=lambda e: e["part"].description.lower()) for type_id, entries in cell_map[scope.id].items() } if fits_part_type_id is not None and fits_part_type_id not in cells: continue # scope has no fit for the required accessory type rows.append( - {"scope": scope, "info": infos.get(scope.id), "cells": cells} + { + "scope": scope, + "info": infos.get(scope.id), + "objective_diameter_mm": objective_diameters.get(scope.id), + "cells": cells, + } ) return {"column_types": column_types, "rows": rows} diff --git a/app/templates/_scopes_table_card.html b/app/templates/_scopes_table_card.html index dc10170..8245507 100644 --- a/app/templates/_scopes_table_card.html +++ b/app/templates/_scopes_table_card.html @@ -1,11 +1,17 @@ {%- macro mag(v) -%}{{ v | int if v == v | int else v | round(1) }}{%- endmacro -%} -{%- macro magnification(info) -%} +{%- macro magnification_range(info) -%} {%- if info and info.magnification_min is not none and info.magnification_max is not none -%} - {{ mag(info.magnification_min) }}–{{ mag(info.magnification_max) }}× + {{ mag(info.magnification_min) }}-{{ mag(info.magnification_max) }} {%- elif info and info.magnification_min is not none -%} - {{ mag(info.magnification_min) }}× + {{ mag(info.magnification_min) }} {%- elif info and info.magnification_max is not none -%} - {{ mag(info.magnification_max) }}× + {{ mag(info.magnification_max) }} + {%- endif -%} +{%- endmacro -%} +{%- macro magnification(info, objective_diameter_mm) -%} + {%- set range = magnification_range(info) -%} + {%- if range -%} + {{ range }}x{% if objective_diameter_mm is not none %}{{ mag(objective_diameter_mm) }}{% endif %} {%- else -%} — {%- endif -%} @@ -37,7 +43,7 @@