Updates post-M2
This commit is contained in:
1 parent
0ef9952ae4
commit
7a3088e847
8 files changed
+363
-49
No files matched your search
@@ -2,3 +2,4 @@
|
||||
/examples/models/nasa/
|
||||
/data/
|
||||
__pycache__/
|
||||
/gallery/
|
||||
@@ -231,11 +231,20 @@ fn missing_kernels_say_where_they_looked() {
|
||||
|
||||
#[test]
|
||||
fn lighting_l1_says_what_it_is_missing() {
|
||||
// boxwing.toml has no epoch and no earth_month: L1 cannot pick a month.
|
||||
// Without an epoch or an earth_month, L1 cannot pick a month: boxwing.toml with its
|
||||
// [appearance] removed.
|
||||
let scene = out_dir().join("no-month.toml");
|
||||
let models = repo().join("examples/models").canonicalize().unwrap();
|
||||
let text = std::fs::read_to_string(repo().join("examples/scenes/boxwing.toml"))
|
||||
.unwrap()
|
||||
.replace("../models", models.to_str().unwrap())
|
||||
.replace("[appearance]\nearth_month = 7\n", "");
|
||||
assert!(!text.contains("earth_month"));
|
||||
std::fs::write(&scene, text).unwrap();
|
||||
let no_month = resolved(&[
|
||||
"render",
|
||||
"--scene",
|
||||
"examples/scenes/boxwing.toml",
|
||||
scene.to_str().unwrap(),
|
||||
"--lighting",
|
||||
"L1",
|
||||
"--preview",
|
||||
@@ -246,17 +255,10 @@ fn lighting_l1_says_what_it_is_missing() {
|
||||
assert!(stderr.contains("earth_month"), "{stderr}");
|
||||
|
||||
// With a month but no data: the error names the file and the fetch script.
|
||||
let scene = out_dir().join("month.toml");
|
||||
let models = repo().join("examples/models").canonicalize().unwrap();
|
||||
let text = std::fs::read_to_string(repo().join("examples/scenes/boxwing.toml"))
|
||||
.unwrap()
|
||||
.replace("../models", models.to_str().unwrap())
|
||||
+ "\n[appearance]\nearth_month = 7\n";
|
||||
std::fs::write(&scene, text).unwrap();
|
||||
let no_data = resolved(&[
|
||||
"render",
|
||||
"--scene",
|
||||
scene.to_str().unwrap(),
|
||||
"examples/scenes/boxwing.toml",
|
||||
"--lighting",
|
||||
"L1",
|
||||
"--data",
|
||||
|
||||
@@ -5,17 +5,23 @@
|
||||
//! from low orbit with Earth's limb 2 degrees above the image center. Models are the
|
||||
//! box-wing from the repository and, if `scripts/fetch-models.sh` has been run,
|
||||
//! Landsat 8 (85k triangles) and ICESat-2 (752k triangles). Each runs on the GPU and
|
||||
//! on lavapipe at 1024 x 1024 and 1920 x 1080.
|
||||
//! on lavapipe at 1024 x 1024 and 1920 x 1080, at lighting L0; and at L1 at 1920 x 1080
|
||||
//! if the Earth and Moon maps are in `data/` (`scripts/fetch-earth.sh`,
|
||||
//! `scripts/fetch-moon.py`), Earth showing July.
|
||||
//!
|
||||
//! Per-pass GPU times are not here; `resolved render --timings N` reports them.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use resolved::gpu::AdapterChoice;
|
||||
use resolved::nalgebra::{Matrix3, Rotation3, UnitQuaternion, Vector3};
|
||||
use resolved::radiometry::AU;
|
||||
use resolved::{CameraConfig, LoadOptions, Mesh, Pose, Renderer, RendererOptions, Scene, Target};
|
||||
use resolved::{
|
||||
CameraConfig, EarthTexture, Fidelity, LightingTier, LoadOptions, Mesh, MoonTexture, Pose,
|
||||
Renderer, RendererOptions, Scene, Target,
|
||||
};
|
||||
|
||||
const EARTH_RADIUS: f64 = 6_378_137.0;
|
||||
const ORBIT: f64 = 7.0e6;
|
||||
@@ -24,6 +30,28 @@ fn examples() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/models")
|
||||
}
|
||||
|
||||
/// Options for lighting L1 with the maps from `data/`, or None if they are missing.
|
||||
fn l1_options() -> Option<RendererOptions> {
|
||||
let data = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data");
|
||||
let (earth, moon) = (
|
||||
data.join("earth/bmng-2004-07.jpg"),
|
||||
data.join("moon/wac-hapke-643.png"),
|
||||
);
|
||||
if !earth.is_file() || !moon.is_file() {
|
||||
eprintln!("skipping lighting L1: run scripts/fetch-earth.sh and scripts/fetch-moon.py");
|
||||
return None;
|
||||
}
|
||||
Some(RendererOptions {
|
||||
fidelity: Fidelity {
|
||||
lighting: LightingTier::L1,
|
||||
..Default::default()
|
||||
},
|
||||
earth_texture: Some(Arc::new(EarthTexture::load(&earth).unwrap())),
|
||||
moon_texture: Some(Arc::new(MoonTexture::load(&moon).unwrap())),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// The models to bench, skipping NASA models that have not been fetched.
|
||||
fn models() -> Vec<(&'static str, Mesh)> {
|
||||
let mut models = vec![(
|
||||
@@ -89,36 +117,47 @@ fn camera(width: u32, height: u32) -> CameraConfig {
|
||||
|
||||
fn frames(c: &mut Criterion) {
|
||||
let models = models();
|
||||
for (device, adapter) in [("gpu", AdapterChoice::Gpu), ("cpu", AdapterChoice::Cpu)] {
|
||||
let mut group = c.benchmark_group(format!("frame/{device}"));
|
||||
if device == "cpu" {
|
||||
group.sample_size(20);
|
||||
}
|
||||
for (width, height) in [(1024, 1024), (1920, 1080)] {
|
||||
let options = RendererOptions {
|
||||
adapter: adapter.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut renderer = Renderer::new(camera(width, height), options).unwrap();
|
||||
for (name, mesh) in &models {
|
||||
let model = renderer.load_model(mesh).unwrap();
|
||||
let (scene, observer) = limb_view(mesh, model);
|
||||
let id = BenchmarkId::new(format!("{width}x{height}"), name);
|
||||
group.bench_function(id, |b| {
|
||||
b.iter(|| renderer.submit(&scene, &observer).unwrap().wait().unwrap())
|
||||
});
|
||||
if width == 1920 {
|
||||
let id = BenchmarkId::new(format!("{width}x{height}/view"), name);
|
||||
// L0 keeps the group names it had before L1 was benched.
|
||||
let mut tiers = vec![(
|
||||
"frame",
|
||||
RendererOptions::default(),
|
||||
vec![(1024, 1024), (1920, 1080)],
|
||||
)];
|
||||
if let Some(l1) = l1_options() {
|
||||
tiers.push(("frame-l1", l1, vec![(1920, 1080)]));
|
||||
}
|
||||
for (tier, base, sizes) in &tiers {
|
||||
for (device, adapter) in [("gpu", AdapterChoice::Gpu), ("cpu", AdapterChoice::Cpu)] {
|
||||
let mut group = c.benchmark_group(format!("{tier}/{device}"));
|
||||
if device == "cpu" {
|
||||
group.sample_size(20);
|
||||
}
|
||||
for &(width, height) in sizes {
|
||||
let options = RendererOptions {
|
||||
adapter: adapter.clone(),
|
||||
..base.clone()
|
||||
};
|
||||
let mut renderer = Renderer::new(camera(width, height), options).unwrap();
|
||||
for (name, mesh) in &models {
|
||||
let model = renderer.load_model(mesh).unwrap();
|
||||
let (scene, observer) = limb_view(mesh, model);
|
||||
let id = BenchmarkId::new(format!("{width}x{height}"), name);
|
||||
group.bench_function(id, |b| {
|
||||
b.iter(|| {
|
||||
let view = renderer.submit(&scene, &observer).unwrap();
|
||||
view.wait_view().unwrap().dn()[0]
|
||||
})
|
||||
b.iter(|| renderer.submit(&scene, &observer).unwrap().wait().unwrap())
|
||||
});
|
||||
if width == 1920 {
|
||||
let id = BenchmarkId::new(format!("{width}x{height}/view"), name);
|
||||
group.bench_function(id, |b| {
|
||||
b.iter(|| {
|
||||
let view = renderer.submit(&scene, &observer).unwrap();
|
||||
view.wait_view().unwrap().dn()[0]
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 316 KiB |
@@ -10,6 +10,7 @@
|
||||
# The Sun is placed to light those three faces and the Earth near the limb.
|
||||
#
|
||||
# resolved render --scene examples/scenes/boxwing.toml --preview boxwing.png
|
||||
# resolved render --scene examples/scenes/boxwing.toml --lighting L1 --preview boxwing.png
|
||||
|
||||
sun = [111155951345, -100085268138, 2610842842]
|
||||
moon = [0, 0, -3.844e8]
|
||||
@@ -23,6 +24,10 @@ position = [7000026.62, -53.77, 0.0]
|
||||
look_at = [7000000.0, 0.0, 0.0]
|
||||
up = [1, 0, 0]
|
||||
|
||||
# At lighting L1, Earth's surface shows July (from scripts/fetch-earth.sh).
|
||||
[appearance]
|
||||
earth_month = 7
|
||||
|
||||
[[target]]
|
||||
model = "../models/boxwing.gltf"
|
||||
position = [7000000.0, 0.0, 0.0]
|
||||
|
||||
@@ -30,23 +30,25 @@ step up is visible at a glance.
|
||||
|
||||
| Tier | Lighting | Camera | RTX 3060 | lavapipe (CPU) | Sample |
|
||||
|---|---|---|---|---|---|
|
||||
| L0 / C0 | Lambertian surfaces, Earth's and the Moon's limbs exact, eclipse by Earth and the Moon, no shadows on the target, no light from Earth | Pinhole, absolute radiometry to 12-bit DN, no noise or blur | 0.9–1.3 ms | 6–69 ms | [](docs/images/l0-c0.png) |
|
||||
| L0 / C0 | Lambertian surfaces, Earth's and the Moon's limbs exact, eclipse by Earth and the Moon, no shadows on the target, no light from Earth | Pinhole, absolute radiometry to 12-bit DN, no noise or blur | 0.9–1.3 ms | 5–68 ms | [](docs/images/l0-c0.png) |
|
||||
| L1 / C0 | Adds Earth's surface from Blue Marble and the Moon's from the LRO Camera's map, the Moon's measured phase curve, shadows on the target, Earthshine and Moonshine, eclipses seen on Earth and the Moon, the Sun's limb darkening | Pinhole, absolute radiometry to 12-bit DN, no noise or blur | 1.3–2.3 ms | 12–114 ms | [](docs/images/l1-c0.png) |
|
||||
|
||||
Times are one 1920 x 1080 RGB frame end to end (`submit` to `wait_view`, with
|
||||
the DN image ready to read), median of criterion runs, over models from 12 to
|
||||
752,000 triangles: the box-wing, Landsat 8 and ICESat-2. The GPU's own work is
|
||||
0.3 to 0.6 ms of that. `wait`, which copies the 12 MB image into a new `Vec`,
|
||||
takes 1.6 to 2.0 ms on the RTX 3060 and 7.7 to 82 ms on lavapipe. lavapipe ran
|
||||
on the Intel Core Ultra 9 285K's 24 cores. Run to run, the RTX 3060's times
|
||||
vary by about 5 %, and lavapipe's by up to 15 % between sessions. The sample is the Landsat 8 example scene
|
||||
(`examples/scenes/landsat8.toml`). `cargo bench -p resolved` reproduces the
|
||||
numbers, and `resolved render --timings N` shows the per-pass split.
|
||||
0.7 to 0.9 ms of that at L0 and 1.1 to 1.5 ms at L1, of which 0.48 ms is the
|
||||
sensor pass writing the image into memory the CPU reads. `wait`, which copies
|
||||
the 12 MB image into a new `Vec`, takes 1.8 to 2.0 ms on the RTX 3060 at L0
|
||||
and 2.3 to 3.1 ms at L1, and on lavapipe 7 to 77 ms and 14 to 144 ms. L1's
|
||||
setup also loads the Earth and Moon maps, about 1 s, once. lavapipe ran on the
|
||||
Intel Core Ultra 9 285K's 24 cores. Run to run, the RTX 3060's times vary by
|
||||
about 5 %, and lavapipe's by up to 15 % between sessions. The samples are the
|
||||
Landsat 8 example scene (`examples/scenes/landsat8.toml`). `cargo bench -p
|
||||
resolved` reproduces the numbers (L1 when the maps are in `data/`), and
|
||||
`resolved render --timings N` shows the per-pass split.
|
||||
|
||||
L1 adds Earth's surface, shadows on the target, Earthshine and Moonshine,
|
||||
the Moon's albedo map and phase curve, the Sun's limb darkening and eclipses
|
||||
seen on Earth and the Moon; its row comes with its measurements. Planned: C1
|
||||
and C2 add pixel-area sampling, lens distortion, blur and detector noise; L2
|
||||
adds measured materials and the atmosphere; L3 is path traced.
|
||||
Planned: C1 and C2 add pixel-area sampling, lens distortion, blur and detector
|
||||
noise; L2 adds measured materials and the atmosphere; L3 is path traced.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -123,6 +125,11 @@ cargo run --release -p resolved-cli -- render \
|
||||
--out frame.png --preview preview.png --timings 50
|
||||
```
|
||||
|
||||
`scripts/make-gallery.sh` renders every example scene at L0 and L1 into
|
||||
`gallery/`, skipping any whose model or data is missing, and
|
||||
`scripts/serve-gallery.py` serves the result as a web page on port 8000 of
|
||||
every interface, each scene captioned from its file.
|
||||
|
||||
- `--out` writes raw DN as a 16-bit PNG, shifted up to fill 16 bits as
|
||||
cameras usually store them: a 12-bit DN is the stored value divided by 16.
|
||||
It is linear, so it looks dark next to the preview.
|
||||
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
# Renders every example scene at lighting L0 and L1 into gallery/ (gitignored), as
|
||||
# <scene>-L0.png and <scene>-L1.png previews. A scene whose model, maps or kernels are
|
||||
# missing is skipped with the reason. View the result with scripts/serve-gallery.py.
|
||||
#
|
||||
# Needs: scripts/fetch-models.sh for the NASA models, scripts/fetch-earth.sh and
|
||||
# scripts/fetch-moon.py for L1, and astrokit's kernels for scenes given by epoch.
|
||||
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
cargo build --quiet --release -p resolved-cli
|
||||
mkdir -p gallery
|
||||
|
||||
made=0
|
||||
skipped=0
|
||||
for scene in examples/scenes/*.toml; do
|
||||
name=$(basename "$scene" .toml)
|
||||
for tier in L0 L1; do
|
||||
out="gallery/$name-$tier.png"
|
||||
if error=$(./target/release/resolved render --scene "$scene" --lighting "$tier" \
|
||||
--preview "$out" 2>&1); then
|
||||
echo "$out"
|
||||
made=$((made + 1))
|
||||
else
|
||||
echo "skipped $name at $tier: ${error#error: }"
|
||||
rm -f "$out"
|
||||
skipped=$((skipped + 1))
|
||||
fi
|
||||
done
|
||||
done
|
||||
echo "$made images in gallery/, $skipped skipped"
|
||||
Executable
+228
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Serves gallery/ (from scripts/make-gallery.sh) as a web page on every interface, for
|
||||
looking at the example renders from another machine on the network.
|
||||
|
||||
python3 scripts/serve-gallery.py [--port 8000]
|
||||
|
||||
Each example scene gets a card with its L0 and L1 renders side by side, captioned
|
||||
from the comment at the top of its file in examples/scenes/. Click an image to see it
|
||||
full size; arrow keys step through them and Escape closes. The page is built on each
|
||||
request, so rerunning make-gallery.sh and reloading shows the new images. Only the
|
||||
PNG files in gallery/ are served.
|
||||
|
||||
Needs only the Python standard library.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import html
|
||||
import socket
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
GALLERY = ROOT / "gallery"
|
||||
SCENES = ROOT / "examples" / "scenes"
|
||||
TIERS = ["L0", "L1"]
|
||||
|
||||
STYLE = """
|
||||
:root {
|
||||
--bg: #0b0d12; --card: #141821; --line: #232938; --text: #e6e8ee;
|
||||
--muted: #9aa3b5; --accent: #e8b04a;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; background: var(--bg); color: var(--text);
|
||||
font: 15px/1.55 system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
}
|
||||
header { padding: 2.5rem 1.5rem 1rem; max-width: 1400px; margin: 0 auto; }
|
||||
h1 { margin: 0; font-size: 1.9rem; font-weight: 650; letter-spacing: -0.01em; }
|
||||
h1 span { color: var(--accent); }
|
||||
header p { margin: 0.4rem 0 0; color: var(--muted); }
|
||||
main {
|
||||
max-width: 1400px; margin: 0 auto; padding: 1rem 1.5rem 3rem;
|
||||
display: grid; gap: 1.5rem;
|
||||
}
|
||||
section {
|
||||
background: var(--card); border: 1px solid var(--line); border-radius: 14px;
|
||||
padding: 1.25rem 1.25rem 1.1rem;
|
||||
}
|
||||
h2 { margin: 0 0 0.35rem; font-size: 1.15rem; font-weight: 600; }
|
||||
.caption { margin: 0 0 1rem; color: var(--muted); max-width: 75ch; }
|
||||
details { margin: -0.6rem 0 1rem; color: var(--muted); font-size: 0.9rem; }
|
||||
details summary { cursor: pointer; color: var(--accent); }
|
||||
details p { margin: 0.5rem 0 0; max-width: 75ch; }
|
||||
.pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1rem; }
|
||||
figure { margin: 0; }
|
||||
figure img {
|
||||
width: 100%; aspect-ratio: 16 / 9; object-fit: contain; display: block;
|
||||
background: #000; border-radius: 8px; border: 1px solid var(--line); cursor: zoom-in;
|
||||
transition: transform 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
figure img:hover { border-color: var(--accent); transform: translateY(-2px); }
|
||||
figcaption { margin-top: 0.4rem; font-size: 0.85rem; color: var(--muted); }
|
||||
figcaption b { color: var(--text); font-weight: 600; }
|
||||
.missing {
|
||||
aspect-ratio: 16 / 9; display: grid; place-items: center; border-radius: 8px;
|
||||
border: 1px dashed var(--line); color: var(--muted); font-size: 0.9rem;
|
||||
}
|
||||
#view {
|
||||
position: fixed; inset: 0; background: rgba(0, 0, 0, 0.93); display: none;
|
||||
align-items: center; justify-content: center; flex-direction: column; gap: 0.75rem;
|
||||
cursor: zoom-out; padding: 1rem;
|
||||
}
|
||||
#view.open { display: flex; }
|
||||
#view img { max-width: 100%; max-height: calc(100vh - 4rem); }
|
||||
#view div { color: var(--muted); font-size: 0.9rem; }
|
||||
"""
|
||||
|
||||
SCRIPT = """
|
||||
const images = [...document.querySelectorAll('figure img')];
|
||||
const view = document.getElementById('view');
|
||||
const big = view.querySelector('img');
|
||||
const label = view.querySelector('div');
|
||||
let current = -1;
|
||||
function show(i) {
|
||||
current = (i + images.length) % images.length;
|
||||
big.src = images[current].src;
|
||||
label.textContent = images[current].alt;
|
||||
view.classList.add('open');
|
||||
}
|
||||
images.forEach((img, i) => img.addEventListener('click', () => show(i)));
|
||||
view.addEventListener('click', () => view.classList.remove('open'));
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!view.classList.contains('open')) return;
|
||||
if (e.key === 'Escape') view.classList.remove('open');
|
||||
if (e.key === 'ArrowRight') show(current + 1);
|
||||
if (e.key === 'ArrowLeft') show(current - 1);
|
||||
});
|
||||
"""
|
||||
|
||||
|
||||
def caption(scene):
|
||||
"""The paragraphs of the comment at the top of `scene`, leaving out the example
|
||||
commands."""
|
||||
paragraphs, lines = [], []
|
||||
for line in scene.read_text().splitlines():
|
||||
if not line.startswith("#"):
|
||||
break
|
||||
text = line[1:]
|
||||
if text.strip().startswith("resolved render"):
|
||||
continue
|
||||
if text.strip():
|
||||
lines.append(text.strip())
|
||||
elif lines:
|
||||
paragraphs.append(" ".join(lines))
|
||||
lines = []
|
||||
if lines:
|
||||
paragraphs.append(" ".join(lines))
|
||||
return paragraphs
|
||||
|
||||
|
||||
def page():
|
||||
names = sorted({p.stem.rsplit("-", 1)[0] for p in GALLERY.glob("*.png")})
|
||||
cards = []
|
||||
count = 0
|
||||
for name in names:
|
||||
scene = SCENES / f"{name}.toml"
|
||||
paragraphs = caption(scene) if scene.is_file() else []
|
||||
figures = []
|
||||
for tier in TIERS:
|
||||
image = GALLERY / f"{name}-{tier}.png"
|
||||
if image.is_file():
|
||||
count += 1
|
||||
stamp = int(image.stat().st_mtime)
|
||||
alt = f"{name}, lighting {tier}"
|
||||
figures.append(
|
||||
f'<figure><img src="/img/{name}-{tier}.png?{stamp}" '
|
||||
f'alt="{html.escape(alt)}" loading="lazy">'
|
||||
f"<figcaption><b>{tier}</b> lighting</figcaption></figure>"
|
||||
)
|
||||
else:
|
||||
figures.append(
|
||||
f'<figure><div class="missing">no {tier} render</div>'
|
||||
f"<figcaption><b>{tier}</b> lighting</figcaption></figure>"
|
||||
)
|
||||
first = html.escape(paragraphs[0]) if paragraphs else ""
|
||||
more = ""
|
||||
if len(paragraphs) > 1:
|
||||
body = "".join(f"<p>{html.escape(p)}</p>" for p in paragraphs[1:])
|
||||
more = f"<details><summary>More</summary>{body}</details>"
|
||||
cards.append(
|
||||
f"<section><h2>{html.escape(name)}</h2>"
|
||||
f'<p class="caption">{first}</p>{more}'
|
||||
f'<div class="pair">{"".join(figures)}</div></section>'
|
||||
)
|
||||
if not cards:
|
||||
cards.append(
|
||||
"<section><h2>No images yet</h2><p class=\"caption\">"
|
||||
"Run scripts/make-gallery.sh, then reload.</p></section>"
|
||||
)
|
||||
newest = max((p.stat().st_mtime for p in GALLERY.glob("*.png")), default=None)
|
||||
when = time.strftime("%Y-%m-%d %H:%M", time.localtime(newest)) if newest else "never"
|
||||
return f"""<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>resolved gallery</title><style>{STYLE}</style></head>
|
||||
<body><header><h1><span>resolved</span> gallery</h1>
|
||||
<p>{count} renders of {len(names)} example scenes, last rendered {when}.
|
||||
Click an image to see it full size.</p></header>
|
||||
<main>{"".join(cards)}</main>
|
||||
<div id="view"><img alt=""><div></div></div>
|
||||
<script>{SCRIPT}</script></body></html>"""
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
path = unquote(self.path.split("?", 1)[0])
|
||||
if path in ("/", "/index.html"):
|
||||
self.send(200, "text/html; charset=utf-8", page().encode())
|
||||
elif path.startswith("/img/"):
|
||||
name = path[len("/img/"):]
|
||||
# Only a PNG directly in gallery/, by its exact name.
|
||||
image = GALLERY / name
|
||||
if "/" in name or not name.endswith(".png") or not image.is_file():
|
||||
self.send(404, "text/plain", b"not found")
|
||||
else:
|
||||
self.send(200, "image/png", image.read_bytes())
|
||||
else:
|
||||
self.send(404, "text/plain", b"not found")
|
||||
|
||||
def send(self, status, kind, body):
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", kind)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Cache-Control", "no-cache")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
|
||||
def lan_address():
|
||||
"""This machine's address on the network: the source of a route outward. A UDP
|
||||
connect sends nothing."""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
try:
|
||||
s.connect(("10.255.255.255", 1))
|
||||
return s.getsockname()[0]
|
||||
except OSError:
|
||||
return "127.0.0.1"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
||||
parser.add_argument("--port", type=int, default=8000)
|
||||
args = parser.parse_args()
|
||||
server = ThreadingHTTPServer(("0.0.0.0", args.port), Handler)
|
||||
print(f"serving {GALLERY} at http://{lan_address()}:{args.port}/ (Ctrl+C to stop)", flush=True)
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user