diff --git a/.claude/skills/screenshots/SKILL.md b/.claude/skills/screenshots/SKILL.md index a59a914..6a07647 100644 --- a/.claude/skills/screenshots/SKILL.md +++ b/.claude/skills/screenshots/SKILL.md @@ -21,7 +21,7 @@ only evidence that counts. ruby Tools/ipad-shots.rb # iPad: 4 shots × 2 languages, ~8 min ruby Tools/macos-shots.rb # Mac: 4 shots × 2 languages ruby Tools/visionos-shots.rb # Vision Pro: 3 shots × 2 languages -ruby Tools/screenshots.rb # always: strip alpha, optimise, rebuild site/shots +ruby Tools/screenshots.rb # always: strip alpha, optimise, rebuild site/shots and docs/ ``` All three take a name filter (`ruby Tools/ipad-shots.rb star`) and end by @@ -53,7 +53,9 @@ composite. A capture with *real* transparency stops the run instead, because choosing a background would change the picture and that is a person's decision. It then runs `oxipng -o max --strip safe`, which is lossless and worth about a -fifth of the bytes, and regenerates `site/shots/`. +fifth of the bytes, and regenerates `site/shots/` — and the README's two +pictures, `docs/Screenshot.png` (the Mac tree capture) and `docs/Viewer.png` +(the Vision Pro one), which are derived on exactly the same terms. **That last part is the reason it is a script and not a paragraph.** The site images are downscaled copies of seven captures, so a reshoot that stops at diff --git a/CLAUDE.md b/CLAUDE.md index 4081f20..1e134b9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,7 @@ xcrun simctl io screenshot shot.png # 3840x2160, with an alpha chan # The App Store listing (appstore/). The check needs no key and no bundle; # the other two need ASC_ISSUER_ID / ASC_KEY_ID / ASC_PRIVATE_KEY_PATH. -ruby Tools/screenshots.rb # after ANY reshoot: strip alpha, optimise, rebuild site/shots +ruby Tools/screenshots.rb # after ANY reshoot: strip alpha, optimise, rebuild site/shots and docs/ ruby fastlane/metadata_check.rb # what CI runs on every pull request bundle exec fastlane ios metadata_diff # live listing vs what is written bundle exec fastlane ios metadata_push # upload (mac for the other listing) diff --git a/README.md b/README.md index 1c4f0a0..6174fb6 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ graphics engine written in Swift. [**App Store**](https://apps.apple.com/app/id6798677334) · [Privacy](https://temoki.github.io/TortoiseBlocks/privacy.html) -TortoiseBlocks on macOS +Tortoise Blocks on macOS: a block named 🌳 that calls itself twice, and the fractal tree it draws. ## Features @@ -41,6 +41,11 @@ graphics engine written in Swift. - **Blocks → Swift** — a syntax-colored code pane shows the equivalent [Tortoise API](https://github.com/temoki/TortoiseGraphics2) program, as a bridge from blocks to text programming +- **A viewer on Apple Vision Pro** — drawings are made on iPad and Mac; open + one on Vision Pro and it lies on a real table in front of you, at whatever + size you like, with a 3D tortoise standing on the paper walking the line as + it appears. The blocks and the generated Swift open in windows either side of + it, so a headset never has to choose between them the way one screen does - **Documents** — a standard document app: `.tortoise` files (JSON), iCloud Drive / Files integration, its own folder under On My iPad, autosave, system undo; a new document can start from a sample program @@ -53,11 +58,14 @@ graphics engine written in Swift. - **English / Japanese** — Japanese uses kid-friendly hiragana; adding a language is a single string-catalog edit +A drawing open on Apple Vision Pro: the blocks in a window on the left, the transport in the middle, the generated Swift on the right, and the star itself on a sheet on the table with the tortoise standing on it. + ## Requirements - **Xcode** 26+ (Swift 6.2) -- **Platforms** iPadOS 26+ · macOS 26+ · visionOS 26+ (the same three-pane app, - in a window on Vision Pro) +- **Platforms** iPadOS 26+ · macOS 26+ · visionOS 26+ — the three-pane editor + on iPad and Mac, and on Vision Pro a viewer for what they made, with no + editing in it at all ## Getting Started @@ -77,15 +85,18 @@ swift test ``` TortoiseBlocks/ -├── TortoiseBlocksKit/ # UI-independent SwiftPM package (depends on TortoiseCore only) -│ ├── Model/ # Block tree, frozen JSON format, pure editing functions -│ ├── Engine/ # BlockExpander: block tree → command stream (+ blockID tags) -│ └── CodeGen/ # SwiftCodeGenerator: block tree → Swift source (+ tokenizer) -├── App/ # SwiftUI document app (palette | workspace | canvas) -├── ThumbnailExtension/ # QuickLook thumbnails — reads one field, links nothing -├── appstore/ # The store listing: text per locale, screenshots per platform -├── fastlane/ # Pushes appstore/ to App Store Connect. The only Ruby here -└── site/ # The published website (GitHub Pages); docs/ is not published +├── TortoiseBlocksKit/ # UI-independent SwiftPM package (depends on TortoiseCore only) +│ ├── Model/ # Block tree, frozen JSON format, pure editing functions +│ ├── Engine/ # BlockExpander: block tree → command stream (+ blockID tags) +│ └── CodeGen/ # SwiftCodeGenerator: block tree → Swift source (+ tokenizer) +├── App/ # SwiftUI document app (palette | workspace | canvas) +│ └── Views/Viewer/ # visionOS only: the drawing on the table, and its three windows +├── ThumbnailExtension/ # QuickLook thumbnails — reads one field, links nothing +├── TortoiseBlocksUITests/ # Not a test suite so much as a camera: it shoots the captures +├── Tools/ # The capture rigs, and the Blender script the 3D tortoise comes from +├── appstore/ # The store listing: text per locale, screenshots per platform +├── fastlane/ # Pushes appstore/ to App Store Connect +└── site/ # The published website (GitHub Pages); docs/ is not published ``` The runtime pipeline is one straight line: @@ -184,9 +195,22 @@ goes up on demand, by hand: ```bash bundle exec fastlane metadata_check # the files alone, no network, no key bundle exec fastlane ios metadata_diff # live listing against what is written -bundle exec fastlane ios metadata_push # upload (mac for the other listing) +bundle exec fastlane ios metadata_push # upload (mac, visionos for the others) ``` +There are three listings and two sets of text: iOS and macOS share +`appstore/metadata`, while visionOS reads `appstore/metadata-visionos`, because +the app there is a viewer and the description that sells the editor would be +describing a product that does not exist. The app-level fields the App Store +keeps once per app rather than per platform — name, subtitle, privacy URL — +are checked byte-identical across both, since whichever lane runs last would +otherwise quietly overwrite the others. + +The captures are made by the rigs in [Tools/](Tools/): one command each for +iPad, Mac and Vision Pro, all ending in `Tools/screenshots.rb`, which flattens +the alpha channel App Store Connect rejects, optimises every PNG, and +regenerates the website's downscaled copies from the same pictures. + ## License [MIT](LICENSE) diff --git a/Tools/screenshots.rb b/Tools/screenshots.rb index 3df6890..5434ab3 100755 --- a/Tools/screenshots.rb +++ b/Tools/screenshots.rb @@ -40,6 +40,7 @@ ROOT = Pathname.new(__dir__).parent SCREENSHOTS = ROOT / "appstore" / "screenshots" SITE_SHOTS = ROOT / "site" / "shots" +DOCS = ROOT / "docs" # Which captures the website uses, and how big. A curated subset rather than a # rule — the code pane is on the Mac half of the page and not the iPad half — @@ -69,6 +70,23 @@ } }.freeze +# The README's two pictures, on the same terms as the site copies and for the +# same reason: what a reshoot silently leaves behind is the picture nobody has +# open while shooting. Same recipe, too — quantised to 256 colours — which is +# what makes rerunning this reproduce the committed files rather than rewrite +# them. +# +# The Mac one is the *tree* on purpose. It is the only capture in the set that +# shows a block calling itself, which is what the README spends its longest +# bullet on, and the platform section is the Mac's other job on the page +# already. Both README images were composed by hand before the rigs existed — +# the same scenes, shot separately — so this refreshes them rather than +# replacing them, and the window sits a little differently for it. +DOCS_DERIVED = { + "macos/en-US/4_tree_canvas" => { target: "Screenshot.png", size: "2560x1600" }, + "visionos/en-US/1_star_table" => { target: "Viewer.png", size: "1600x900" } +}.freeze + def run(*command) return if system(*command) @@ -134,4 +152,15 @@ def flatten(path) run("oxipng", "-q", "-o", "max", "--strip", "safe", *Pathname.glob(SITE_SHOTS / "*.png").map(&:to_s)) puts "site/shots regenerated" +DOCS_DERIVED.each do |capture, spec| + source = SCREENSHOTS / "#{capture}.png" + next warn("missing #{relative(source)}, skipping docs/#{spec[:target]}") unless source.exist? + + target = DOCS / spec[:target] + run("magick", source.to_s, "-resize", spec[:size], "-dither", "None", + "-colors", "256", "-strip", target.to_s) + run("oxipng", "-q", "-o", "max", "--strip", "safe", target.to_s) +end +puts "docs images regenerated" + exit(MetadataCheck.report ? 0 : 1) diff --git a/docs/Screenshot.png b/docs/Screenshot.png index 8dbcf75..52e7a15 100644 Binary files a/docs/Screenshot.png and b/docs/Screenshot.png differ diff --git a/docs/Viewer.png b/docs/Viewer.png new file mode 100644 index 0000000..efdb7f4 Binary files /dev/null and b/docs/Viewer.png differ