{% extends layout|default('base.html', true) %}
{# The viewer's tab says which project it is: the display name, else the name. #}
{% block title %}{% if data.datasource %}
{# Docked by the canvas layout above the caption (top-left), so the
two stack instead of overlapping when the sidebar is closed. #}
{# The one place the viewer's ground colour is not the app's. A
transmitted-light image IS its white background, and the dark
shell behind it reads as a shadow the scanner never recorded. #}
{# The calibration pencil. Parked here and moved into
OpenSeadragon's container by views/scaleCalibration.js, which
then keeps it beside the scale bar -- see that file. #}
{% include "_scale_calibration_float.html" %}
Channels
{% if data.active_tool_panels.tool_panel_legacy_slot %}
{% include data.active_tool_panels.tool_panel_legacy_slot %}
{% endif %}
{# What takes the centre spinner down. The spinner is in this page's own markup
and visible from first paint, so this has to be loaded before main.js builds
the viewer and calls watch() on it -- deferred scripts run in document order,
which is the whole reason it sits above rather than in base.html with the
other services. Viewer-only: no other page has the element. #}
{# What was on screen (viewport, zoom, HD mode, channels), captured and put
back -- regions remember the view they were drawn under -- and magic
select's client (click an artifact, get its outline; sets itself up once).
Core services the ROI and QC panels share; they call only /segment/v1. #}
{# Grab/grabbing on the OSD canvas. Deferred and unconditional within this
branch: it installs three document-level listeners that cost nothing until
a press lands inside a `.openseadragon-canvas`, which only this page has --
either viewer's. #}
{# Before main.js and deferred like it, so it is defined by the time init()
sees a pending mask job. Loaded here rather than in base.html because the
pending state belongs to a viewer looking at the project whose mask is
converting -- every other page would carry a listener with nothing to
listen for. #}
{# The modal-and-chip of a background job (views/jobWait.js), and its waits:
the mask pyramid here, the 3-D mask's cell surfaces below. #}
{# Brightfield display adjustments. Deferred like main.js and loaded
unconditionally: it does nothing at all when its sliders are absent, and
gating the tag on image_kind would put a second thing in this page that has
to agree with the staged markup above. It runs after main.js has built the
base image layer's card, which is where those sliders will be by then. #}
{# Here rather than base.html for the same reason: the only things that open it
are the channel list and a registered layer's channel panel, and both are the
viewer's. Only has to exist by the time the user clicks one of those upload
buttons, so deferred is ample. #}
{# What the last sample arranged, on its way into this one. BEFORE main.js
and deferred like it, so the snapshot is on hand by the time init() builds
the channel slots -- the sidebar reads it while deciding which channels to
turn on, which is the earliest any of it is wanted. Viewer-only: nothing
else has an arrangement to carry. #}
{# Previous / Next for a sample that belongs to a dataset. Deliberately
independent of main.js: a sample whose image will not open has no viewer at
all, and this is the way out of it. It mounts off its own fetch and asks
the viewer for nothing. #}
{# Why the canvas is empty, when it is. Before main.js, which calls it from
the boot path the moment a load comes back without an image. #}
{# Core's Rotate and Flip tools. Before main.js, which activates a `lazy`
definition at boot only when `?tool=` staged its panel. #}
{# Volumes: where the viewer is in a z-stack (the state), the Z strip and
badge on the canvas, and core's Depth card. Before main.js, which reads
the state beside /config and mounts the strip. All three do nothing for a
planar image. #}
{# How the 3-D view may be cut, in numbers (before the state, which repairs
its `inspect` with it), and the controller every control changes it
through. #}
{# A channel switched on comes in rather than lands, on every surface the
channel section drives (the plane, layers, section panes). Before main.js,
whose boot adds the first channels. #}
{# Surfaces (plexora/server/surface): the meshes drawn in the 3-D view, the
service that loads and styles them, their card, Surface Studio's dock, the
picked object's card and the objects table. #}
{# A 3-D mask drawn in 3-D: the Cells control's Outlines and Filled as cell
surfaces, and the wait while the mask is converted. #}
{# The 3-D view's dock (cutting the stack) and the handles that drag its cuts. #}
{# The Area tool (a square on the image, opened in 3-D), and Back (B) to the
view it was opened from. #}
{# Bookmarks: views kept as captures of the plugin that keeps them, and their
tray. The store registers itself (Plugin.viewer_scripts, in ). #}
{# An external agent driving this viewer (the live viewer control plane,
/agent/v1). Deferred and BEFORE main.js: it waits for DOMContentLoaded and
then for main.js's __plexoraReady, so being earlier costs nothing and means
it is in place before anything main.js boots can touch the revision. #}
{# The agent panel: the card that shows an agent session's phase, evidence
and controls. Its orb driver is base.html's, which the import dialog draws
with too. Before agentBridge.js, which routes show_evidence to the panel
when one is up. #}
{# Plexora AI chat (/ai/v1/conversations): a launcher chip and a non-modal
panel in the viewer's lower-left corner. #}
{# What a launch link asked this page to show beyond its channels -- a colour-by
column, cells, regions, a viewport (page_routes.launch_from_dict). After
agentBridge.js: it applies them through the bridge's own handlers. #}
{% else %}
{# The home page: `/` with no datasource named, whether or not anything is
registered yet.
One raised card that takes one image, and under it the way back to what was
opened last. The card is one question asked in reading order -- a file or a
folder, on which machine, which one -- and its button is Browse until there
is a path in the box and Load from then on, so there is never a second
control for the same act.
The one gesture -- pick an image, land in the viewer -- is what this page
is for, and it is the page's own control rather than a button that opens
the import dialog: a modal covering an otherwise empty page is a step, not
a shortcut. What it is NOT is a second importer. Load POSTs to
`/import/sample`, the same route the dialog submits to, so a Xenium run
dropped here brings its transcripts and its table with it instead of being
refused for not being one image. See views/quickViewLanding.js.
Everything that needs a decision first -- several samples in one folder, a
name, a dataset -- is the dialog, one quiet line under the card. #}
{# An instruction, not a logo: the name is already in the navbar, and
the page's one heading should say what the page is for. #}
Explore your tissue
Open an image from this computer or a connected server.
{# `is-local` / `is-remote`, `data-face` (browse | load | busy) and
`data-check` (idle | checking | ok | bad | url) are set by the
controller, and everything that changes with them -- which glyph
the machine chip leads with, which face the button shows, the mark
at the end of the box -- is CSS over those three. Every icon is in
the markup from the start because Font Awesome swaps each
for an