Commit Graph
32 Commits
Author SHA1 Message Date
9aef02cac3 macOS: menus open on press; only the title bar's free gap drags the window (#669)
Menus:
- Titles open their menu on the press and never toggle on the release (no
  blink), so one gesture can press a title, drag to an item and release on
  it to run it. A plain click still opens the menu and leaves it open;
  pressing an open title closes it.

Title bar:
- The window draws under a transparent title bar, so AppKit dragged it from
  anywhere in that strip, menus included. AppKit's own title-bar dragging is
  off (NSWindow.movable = NO via safe objc2 calls, no unsafe; objc2 crates
  pinned to the versions already in Cargo.lock), and the app's drag region
  is only the free gap between the menus and the right-hand controls.

Co-authored-by: Jason Short <3344856+jason4short@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 16:18:14 -04:00
47f9306fd0 Open HEIC/HEIF photos (#355) (#372)
* Open HEIC/HEIF photos (#355)

iPhones save HEIC by default and Photoshop opens it; PhotoCraft had no
HEIF code, so .heic was missing from File > Open and failed to decode.

Add Format::Heif to photocraft-codecs, decoded by heic-rs (pure Rust,
forbid(unsafe), MIT OR Apache-2.0, wasm-ready): single pictures and
grid-tiled photos, 8-bit to U8 and 10/12-bit to U16, auxiliary alpha,
ICC, EXIF and XMP. The container's irot/imir/clap are applied and EXIF
Orientation is rewritten to 1, so photos are never turned twice. Limits
are checked against the declared size before decoding. Read-only (no
pure-Rust HEVC encoder), listed in ASYMMETRIC_EXCEPTIONS.

File > Open (desktop, web) and the batch list accept heic/heif/hif.
Tests compare against Apple's own decode and a 16-bit source, cover
orientation, metadata, limits and every truncation; fixtures from
heic-rs and pillow-heif are attributed in ATTRIBUTION.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* HEIF: pin heic-rs exactly and add a decode_heif fuzz target

Review follow-up: heic-rs is young and parses untrusted files, so pin it
(=0.1.1) and fuzz the HEIF path. The target covers both orientation
paths and is meant to run with -timeout so a hang is reported, not
only a panic. It is added to the CI fuzz matrix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* HEIF: a heic-rs panic is a malformed-file error, not a crash

Fuzzing decode_heif found two out-of-range slices in heic-rs 0.1.1 on
malformed files:
- in its box parser (boxes.rs:130), reachable from a 72-byte file, even
  through the metadata-only probe;
- during HEVC reconstruction (hevc/decode/recon.rs:70).

The background job path catches panics, but the synchronous ones (jobs
off, the CLI) did not. The HEIF codec now runs the decoder under
catch_unwind and reports a panic as CodecError::Malformed, as
gpu/health.rs already does around wgpu.

The fuzz target also calls heic-rs directly, so its panics still reach
the fuzzer. The 72-byte input is a regression test, which fails
without the guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* HEIF: move the decoder into an optional photocraft-heif crate

The heic-rs decoding from #372 now lives in `crates/heif` (photocraft-heif, standalone, depends
only on heic-rs =0.1.1): probe, decode with a pixel limit, container transforms, alpha, 8/10-bit,
ICC/EXIF/XMP and the catch_unwind guard, behind a small API of its own (Info, Options, Decoded,
Error). photocraft-codecs uses it behind a new non-default `heif` feature; the apps and
photocraft-io forward it. HEIF detection, extensions and mime stay compiled in, so without the
feature a HEIC file is still recognised and opening it is a clear "HEIC/HEIF support isn't
included in this build of PhotoCraft" error. `cargo xtask layers` allows exactly codecs -> heif
between standalone crates.

The fixtures are no longer committed: `cargo xtask corpus --heif` fetches them from heic-rs
(tests/fixtures, v0.1.1) and pillow-heif (tests/images) at pinned commits, verified against
xtask/heif-corpus.sha256 (same bytes as the PR's copies). The fixture tests run with
`--features corpus,heif` (`cargo xtask test-corpus` enables both).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Build CI and official binaries with HEIC support (--features heif)

CI test and clippy, the FreeBSD job, and the macOS, Linux (and Flatpak), FreeBSD, Windows and
web packaging all build with the `heif` feature; CI also tests codecs/io without it on Linux so
the "not in this build" path stays covered. `cargo xtask wasm` also checks photocraft-codecs with
`--features heif`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Docs: HEIC open is an optional build feature, on in official builds

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-07 10:20:40 -04:00
b07a2e5b43 Remember desktop window and panel sizes across restarts (#587)
* Remember desktop window and panel sizes across restarts

* Window layout: discard a saved ui.ron that would crash startup

eframe restores the window geometry and egui memory before the app runs, and a
zoom factor of 0, NaN or 1e30, or a NaN/huge window size aborted every launch
(dpi/wgpu assertions). Check the file first and remove it if it holds such values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:12:44 -04:00
cc5de414dc Camera Raw: interactive histogram, clipping warnings and scopes (part of #215) (#407)
* Camera Raw: interactive histogram, clipping warnings and scopes (part of #215)

- RGB histogram of the preview proxy (algo::histogram), drawn as RGB ribbons;
  Before/After, five draggable tonal zones, shadow/highlight clipping warnings
  (U/O) and Alt-drag channel diagnostics.
- Pixel hover readout, up to nine RGB/Lab colour samplers (S), an ICC-managed
  vectorscope (algo::vectorscope) with skin-tone line, and a floating scope panel.
- Settings regrouped as Light, Color, Effects, Curve, Color Mixer, Color Grading
  and Detail; Camera Raw labels translated through the cameraRaw context.
- One point-curve editor (point_curve.rs) shared by Camera Raw, the Curves dialog
  and adjustment-layer Properties; fixes Camera Raw curve points not dragging.
- Double-clicking a Camera Raw smart filter reopens the Camera Raw dialog with its
  settings and updates that filter, instead of the generic parameter form.
- New point curves are validated (2-16 points, increasing inputs); stored Smart
  Filters keep loading.
- camera_raw_bench for P31/P36 (not enforced until a baseline includes them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Histogram tests: as_chunks for triangle indices (clippy 1.99)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Regenerate docs/scorecard.md after rebasing onto main

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n tests: close the Mixer Brush test before the Camera Raw one (rebase)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-07 07:40:55 -04:00
BFlatandClaude Opus 5.5 60224d3fb7 Release 0.3.0 (#577)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:10:08 +07:00
Brandon ThomasandClaude Opus 5.5 909efc0f6d Background jobs in the app: progress, Cancel, open-as-job (part of #210) (#373)
Re-lands #290, which was merged into its stacked base branch (jobs/engine-210) instead of main.
Same tree as the gated #290 commit 39152db.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:44:40 -04:00
Brandon Thomas 7ee4bb2858 Apply EXIF orientation on open, write Orientation = 1 on export (#285) (#318) (#318) 2026-10-06 08:20:36 -04:00
Brandon Thomas eaf92dcd9d Smart Objects and Smart Filters survive PSD save and open (#288) (#288) 2026-10-06 07:46:37 -04:00
Brandon Thomas 47f4abfed4 Stylus pressure, tilt, rotation and eraser on macOS and X11 (#255) (#255) 2026-10-05 17:47:59 -04:00
Brandon Thomas 0f955e6c82 Scorecard: cargo xtask perf + scorecard, budgets, checklists, nightly perf (#221) (#239) (#239) 2026-10-05 10:53:02 -04:00
shou 349dfa0e57 fix(clipboard): use the wayland clipboard for images (#222) (#222) 2026-10-05 09:02:14 -04:00
Brandon ThomasandClaude Opus 5.5 b22deaa944 Layers mask thumbnails + chains; Channels/Paths temporary rows and footers (#153, #154) (#191)
* Layers rows: vector-mask thumbnail, link chains, red X on disabled masks (#153)

Each mask gets a chain (click toggles layer.layerMask.linked /
layer.vectorMask.linked) and a thumbnail; disabled masks are crossed out
and Shift-click toggles them. Vector masks render white/grey, cached and
pruned like the other row thumbnails. Thumbnails shrink on narrow rows so
the name keeps its minimum width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Channels/Paths: temporary layer mask and layer path rows, aspect thumbnails, bottom footers (#154)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:45:12 -04:00
Brandon ThomasandClaude Opus 5.5 ad86321738 Release 0.2.0; CI builds with --locked (#113)
Bump the workspace version to 0.2.0 (cargo xtask version set 0.2.0; Cargo.lock changes only
the 26 workspace crates). CI's test and clippy steps now pass --locked, as the release packaging
scripts do, so a stale Cargo.lock fails CI instead of the release build.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:21:13 -04:00
Gh0stlyandClaude Opus 5.5 5716c2f6f1 security: constrain automation filesystem access (#72)
Rebased onto main (#58/#61/#63/#66/#70/#74) by the maintainers: automation
open/save return and surface import/export warnings (app.open/app.save
reply with `warnings`), automation opens route .abr/.grd to the preset
libraries with script events suppressed, interactive opens keep open_file's
path/recent/notice handling.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:35:06 -04:00
Brandon ThomasandClaude Opus 5.5 d881785fde Photoshop brushes: .abr import, stylus pressure/tilt, 36 grouped presets, .grd import (#74)
* psd: .abr brush (v1/v2/v6+) and .grd gradient (v5) parsers and writers

Clean-room from public format descriptions: legacy computed/sampled brushes,
8BIM-sectioned samp/patt/desc files, 8/16-bit raw and PackBits tips. Bounded
(MAX_EDGE, MAX_TOTAL_BYTES), damaged brushes are skipped with warnings, hostile
input returns Err. Adds an abr fuzz target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* paint: 36 grouped built-in brush presets and new procedural tips

General / Dry Media / Wet Media / Special Effects groups (pressure size/opacity
variants, airbrush, pencils, charcoal, pastel, crayon, rake, watercolour, ink,
oil, foliage, grass, stipple, sparkle, halftone…). Leaf, grass, charcoal,
sponge, star and rake tips are generated in code. BrushPreset gains a group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* io: map .abr brushes onto BrushSettings

Tip shape, shape dynamics, scattering, texture (embedded patt patterns),
dual brush, colour dynamics, transfer, pose, noise, wet edges, build-up and
captured tool options; unmapped settings are reported as warnings. Includes
bench_abr (100-brush import, 5000-dab stroke).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* engine: brush.presets.importAbr and gradient.presets.importGrd

Path or base64 data, group per file (re-import replaces), unique names next to
the built-ins; graceful-failure tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ui: real stylus pressure/tilt, grouped Brushes panel, Import Brushes

Pen force from egui Touch events (Windows WM_POINTER via winit), Pointer Events
pressure/tilt/twist on the web, tilt via ui.pointer; mouse stays 1.0. .abr/.grd
open from File > Open, drag-and-drop, Brushes panel and Preset Manager. Preset
previews keyed on a cheap signature instead of the full brush JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:26:16 -04:00
Brandon ThomasandClaude Opus 5.5 7a5c1930b3 Sandboxed WebAssembly plug-ins: photocraft-plugins (wasmi), plugin.* commands, Filter › Plug-ins (#67)
* photocraft-plugins: sandboxed WebAssembly filter plug-ins (wasmi host, ABI v1)

A new L4 crate runs plug-ins in wasmi (pure Rust, MIT/Apache) with no host
imports, a fuel budget metered in slices with a wall-clock deadline, a memory
cap, a recursion cap and wasmi's strict module limits. Every band gets a fresh
instance; traps, budgets, bad manifests and bad pointers are all Errors.
Pixels go over as interleaved normalised f32 in tile-aligned bands, in
parallel on native, blended by the selection. Includes the example Invert
plug-in (no_std Rust, examples/plugins/invert-rs) built into a test fixture,
hostile-module tests, and docs/plugins.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: plugin.* commands (install, list, run, remove, reload)

plugin.run filters the active pixel layer, a targeted channel or a smart
object (recorded as a smart filter) as one undo step; Preferences >
Plug-ins gains Use Additional Plug-ins Folder, loaded when prefs apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* UI: Filter > Plug-ins lists installed plug-ins with generated dialogs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Plug-ins: bands never outgrow the memory cap; regenerate parity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bench: example plug-in Invert vs built-in Invert on 24 MP

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:08:28 -04:00
Brandon ThomasandClaude Opus 5.5 5bd3dac660 Camera raw: clean-room pure-Rust decoder (DNG, CR2, TIFF/EP raws) with develop pipeline (#65)
* raw: clean-room camera raw decoder and developer (photocraft-raw)

A new standalone crate (no workspace deps, wasm-safe, no unsafe) written
from TIFF 6.0, TIFF/EP, the DNG 1.7 spec, ITU-T T.81 Annex H and published
demosaicing papers:

- Lossless JPEG (LJ92) decoder: predictors 1-7, point transform, restart
  intervals, combined Huffman+extra-bits fast table.
- DNG: uncompressed (8-16 bit, packed) and LJ92, strips and tiles, CFA and
  LinearRaw, linearization, black level (repeat, deltas), white level,
  active area, default crop, ColorMatrix/CameraCalibration/ForwardMatrix
  interpolation by CCT, AnalogBalance, AsShotNeutral/WhiteXY,
  BaselineExposure, orientation.
- CR2 (LJ92 slices, SensorInfo borders, ColorData as-shot WB, measured
  black) and TIFF/EP raws (NEF/ARW/PEF uncompressed or LJ92, incl. Sony
  lossless ARW tiles, Nikon/Sony documented black and WB tags).
- Develop: black/white scaling, WB with highlight clipping, bilinear /
  Malvar-He-Cutler / AHD demosaic (rayon bands, sequential on wasm),
  camera -> XYZ D50 -> ProPhoto, exposure, gamma 1.8, 16-bit, orientation.
- Embedded JPEG preview finder; clear Unsupported errors for CR3, RAF,
  ORF, RW2, vendor-compressed NEF/ARW/PEF, lossy/JXL/float DNG, X-Trans.
- Limits checked before every allocation; testgen feature with an LJ92
  encoder and DNG/CR2/TIFF-EP writers; round-trip, colour-reference,
  truncation and corruption tests.

Registered as a standalone crate in xtask layers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* io: open camera raws through photocraft-raw (16-bit ProPhoto), preview fallback

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* raw: DNG GainMap opcode, fuzz targets, stress tools; open raws from the apps

- Apply DNG OpcodeList2 GainMap (lens shading, e.g. phone DNGs); report other opcodes.
- cargo-fuzz targets (develop, ljpeg) and a mutation stress example.
- Raw extensions in the desktop/web open dialogs and the batch folder list.
- file.open returns the import notes (how a raw was developed, preview fallback).
- codecs: the camera raw refusal points at photocraft-raw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* raw: clippy 1.99 (is_multiple_of, clamp)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:43:47 -04:00
Brandon ThomasandClaude Opus 5.5 297f7f8521 macOS: open files from Finder/Open With/Dock; file path, recent, warnings and .pcraft fixes (#63)
* File open/save: set the path everywhere, Open Recent, show errors and import/export warnings

File › Open, drag-and-drop onto the window and launch paths now share
PhotocraftApp::open_file: the document is named after the file (not the full
path), keeps its path so File › Save writes back, and goes to Open Recent.
Failures are shown as errors (status bar + a non-blocking notice).

Import/export warnings from photocraft-io are no longer thrown away: they show
in the status bar and in a dismissable notice (native and web), and the
control protocol returns them from app.open/app.save/file.open/file.save/
file.saveAs/file.saveACopy. .pcraft is in the Open and Save filters, and
File › Save writes .pcraft documents in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* macOS: open files from Finder, Open With and the Dock (Apple events)

Finder double-click, Open With, drops on the Dock icon and open -a deliver
files as kAEOpenDocuments Apple events, not argv, and winit 0.30 ignores them.
Register an NSAppleEventManager handler through the small fmv-macos-events
crate (MIT OR Apache-2.0, 168 lines, audited, pinned =0.1.0; it keeps the
Objective-C unsafe out of our forbid(unsafe_code) workspace) before the event
loop starts, so both the launching event and later ones reach the UI as
OsEvent::Open; the window is focused. The quit Apple event goes through the
normal close path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:18:03 -04:00
Gh0stly 8b1cbb741b security: authenticate control channels and bound transport resources (#61)
* security: authenticate and bound control transports

* fix: keep connection limiter compatible with current stable
2026-10-04 13:58:59 -04:00
Brandon ThomasandClaude Opus 5.5 69f0e25d5f Never crash: psd, codecs, format, io are panic-free (Rule 9) (#34)
- codecs: bump tiff 0.10 -> 0.11. tiff 0.10 indexed an empty SampleFormat
  list and panicked on a crafted TIFF (found by mutation fuzzing);
  regression test tiff_empty_sample_format_is_an_error.
- codecs: Image::new returns Result instead of panicking on size overflow.
- format: par_compress returns an error if a compression worker panics.
- io: par_map joins every worker before relaying a worker panic, so the
  thread scope can't panic on an unjoined thread.
- All four crates now deny unwrap/expect/panic/unimplemented/todo/
  unreachable outside tests.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:25:17 -04:00
Brandon ThomasandClaude Opus 4.8 ae7d802726 GPU: fall back to Rgba16Float when Rgba32Float isn't renderable (#7, #4) (#22)
Some drivers (e.g. Intel Vulkan on Linux/Windows) can't render to Rgba32Float,
so creating the 'fs_content' pipeline panicked with 'Format Rgba32Float is not
renderable' as soon as a document/image was opened. The accumulation format is
now chosen per adapter: Compositor::preferred_acc_format() keeps Rgba32Float
where it's a valid render target and falls back to Rgba16Float otherwise. The
compositor carries the format (pipelines, accumulation texture, byte size and
the readback half-decode all follow it); the app picks it from the eframe
adapter. Capable GPUs are unchanged (parity still exact, 18/18). A new test
forces the Rgba16Float path on any GPU and confirms it renders within display
tolerance (blend + adjustment). wasm green.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-04 02:27:54 -04:00
Brandon ThomasandClaude Opus 4.8 34d138c11f Links open in the system browser via a platform service (#14) (#20)
ctx.open_url is unreliable on native (notably Windows), so the Help menu,
the Discord buttons, and the start-page links did nothing. Added an open_url
Service hook wired to the `open` crate on native (ShellExecute on Windows);
links::open prefers it and falls back to ctx.open_url (web). link_row now
routes clicks through it instead of ui.hyperlink_to. Test drives every Help
link id + the open() helper through a mock service.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-04 01:34:18 -04:00
Brandon ThomasandClaude Opus 5.5 7750529bbb Release 0.1.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:55:27 -04:00
Brandon ThomasandClaude Opus 5.5 6f5aba08d3 Photomerge, Merge to HDR Pro, Lens Correction, Adaptive Wide Angle, Camera Raw, Vanishing Point
Harris+BRIEF/RANSAC panorama with bundle adjustment, seams and multi-band
blending (Auto-Align/Auto-Blend share it); Debevec-Malik HDR merge with
ghost removal and tone mapping; Brown-Conrady lens correction (filter,
batch, smart filter); wide-angle constraint mesh warp; Crop and
Straighten Photos; Camera Raw Filter (Basic, curves, mixer, grading,
detail, effects) with dialog; Vanishing Point planes (engine only).
Fixes warp_triangles tile binning.

Checkpoint also includes compiling in-progress work of the remaining agents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:23:49 -04:00
Brandon ThomasandClaude Opus 5.5 aa87012aab Analysis, notes, proof views, workspaces (+ in-progress work of other agents)
Image > Analysis (ruler, count, measurement scale/log/record, scale
marker), notes (PSD Anno round trip, Notes panel, import), Modifier Keys
panel, Proof Setup plates / Legacy Mac RGB / colour blindness (Vienot),
32-bit preview options, custom pixel aspect, extras options, New/Delete/
Lock Workspace. Parity 600/625, FLOOR 595.

Checkpoint: also contains compiling in-progress work from the parallel
panels, type styles, File menu, photo, filters, UI-parity and fidelity
agents, plus photocraft-cli serve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 22:49:07 -04:00
Brandon Thomas d98bee4c94 progress, release workflows 2026-10-01 10:49:51 -04:00
Brandon Thomas aad724f28e work in progress 2026-10-01 10:07:04 -04:00
Brandon Thomas 6b44c393fa continued work 2026-10-01 09:16:17 -04:00
Brandon Thomas 536a7346fb more algorithms, progress 2026-10-01 08:25:06 -04:00
Brandon Thomas b00b24739a more work 2026-09-30 11:36:20 -04:00
Brandon Thomas ae92ac20bf continued wip 2026-09-30 09:37:18 -04:00
Brandon Thomas 16941f7198 one-shot 2026-09-30 05:02:08 -04:00