Skip to content

Latest commit

 

History

History
151 lines (122 loc) · 6.48 KB

File metadata and controls

151 lines (122 loc) · 6.48 KB

TortoiseBlocks Icon

Tortoise Blocks

Swift License Platform

A visual programming app for kids — snap blocks together, press Run, and watch the tortoise draw. Powered by TortoiseGraphics2, a turtle graphics engine written in Swift.

TortoiseBlocks on macOS

Features

  • Block editor — tap or drag & drop blocks (movement / pen / fill / control / boxes), nest them, edit arguments in place, reorder freely, drop one on the trash to throw it away, undo everything
  • Repeat, if, and boxes — loop a body, branch on a comparison (with an optional else), and keep a value in a named box 🌟 you can set and do arithmetic on — enough to write a program that grows as it draws
  • Dice 🎲 — any number or color slot can roll a random value on every run, so the same program draws a different picture each time
  • Live playback — a video-style transport: play, pause, step one command at a time, seek with a scrubber, change speed mid-run; the executing block stays highlighted in the workspace so kids can see exactly which block draws which line
  • Blocks → Swift — a syntax-colored code pane shows the equivalent Tortoise API program, as a bridge from blocks to text programming
  • 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
  • Thumbnails in Finder & Files — a QuickLook extension draws each document's last picture, so the app's own folder reads as a gallery of drawings instead of a row of identical icons
  • Export & share — SVG (vector, straight from the library) and PNG at 1x / 2x / 3x, on a transparent ground so a drawing drops onto anything, saved to a file or sent through the share sheet
  • English / Japanese — Japanese uses kid-friendly hiragana; adding a language is a single string-catalog edit

Requirements

  • Xcode 26+ (Swift 6.2)
  • Platforms iPadOS 26+ · macOS 26+ (visionOS planned)

Getting Started

git clone https://github.com/temoki/TortoiseBlocks
open TortoiseBlocks/TortoiseBlocks.xcodeproj   # select a destination and Run

The logic layer is an independent SwiftPM package with its own test suite:

cd TortoiseBlocks/TortoiseBlocksKit
swift test

Architecture

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

The runtime pipeline is one straight line:

[Block] ──BlockExpander──▶ [ExpandedCommand] ──▶ Tortoise.apply ──▶ TortoiseCanvas(_:player:)
   │                              │
   └─SwiftCodeGenerator──▶ code pane            └─ blockID ──▶ executing-block highlight

Randomness is resolved at expansion time and the evaluated command stream is kept, so an export always renders the drawing that actually ran, dice and all. It is not a screenshot of the canvas pane, though: exports crop tight to the drawing and leave the tortoise cursor out.

The QuickLook extension sits outside that pipeline entirely. It links no package: it reads the document, lifts one base64 field out of the JSON, and draws it — which is why a file written by a future version, or one holding a block kind it has never heard of, still gets a thumbnail.

File Format

A .tortoise document is JSON with an explicit, frozen wire format (hand-written coding keys, pinned by snapshot tests — renaming Swift identifiers can never break saved files):

{
  "blocks" : [
    { "id" : "", "kind" : { "penColor" : "purple" } },
    { "id" : "", "kind" : { "repeat" : {
        "count" : { "literal" : 36 },
        "body" : [
          { "id" : "", "kind" : { "forward" : { "random" : { "min" : 100, "max" : 200 } } } },
          { "id" : "", "kind" : { "turnRight" : { "literal" : 170 } } }
        ] } } }
  ],
  "schemaVersion" : 1,
  "thumbnail" : "iVBORw0KGgoAAAANSUhEUgAAAOYAAAEACAYAA…",
  "title" : "Random Star"
}

Keys are written sorted, so that ordering is the file's, not a choice of this document's; the block bodies above are folded up to fit the page.

That thumbnail is the drawing as a small PNG, base64-encoded, and it is what Finder and the Files app show. It appears once a document has been run, and is optional by presence — a document that has never run writes no such key and stays byte-identical to what an app that never heard of thumbnails produced — so it rides version 1 rather than forcing a bump. Nothing reads it back into the app; opening a document still starts with an empty canvas.

schemaVersion is the lowest version that can read the file, not the one that wrote it. A document is written as version 1 unless it uses a version-2 feature (a box or an if block), so a simple program stays byte-identical to what the first release produced and keeps opening in older builds. A file that asks for a newer version than the app knows is turned away with a plain "made with a newer version" message instead of a decode error.

Number slots are bounded, by the slot they sit in:

Slot Range
Forward / Backward −1000 … 1000
Turn Right / Turn Left −360 … 360
Pen Width 0 … 100
Repeat count 0 … 1000
Box values, condition operands, dice bounds −1000 … 1000

The editor refuses out-of-range input, but reading a document never validates or rewrites it: a value outside these ranges loads exactly as saved and is saturated to the nearest bound when the program runs. Box arithmetic saturates the same way, so a value can never run off to infinity.

License

MIT