Docs: refresh every UI screenshot, GIF and video from the current UI (#6763)

* Docs: refresh every UI screenshot, GIF and video from the current UI

Re-record the README demo, the rover build, the sample plate and the class-A surface pictures
with 0.22.1 in the default dark theme. The demo script is fixed for the current sketch dimension
boxes and MCP file limits, and the rover replay and class-A pictures now have scripts.

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

* Docs: cut the idle time out of the demo and rover recordings

The README demo plays at 6x and the rover replay at 15x with a constant-rate camera orbit, with the stretches where nothing on screen changes cut out by the new trim_idle.py.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-25 19:21:01 +00:00
1 parent d17b70241f
commit 8071096a99
17 files changed
+301 -19

No files matched your search

+8 -8
View File
@@ -51,10 +51,10 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/app/macos/exports/icon.icns | SolidCraft contributors | macOS fallback compiled by Apple's actool from the approved assets/icons/app/macos/source/SolidCraft.icon composition | MIT OR Apache-2.0 |
| assets/models/test_boat.bin | 3DBenchy.com | 3DBenchy.com | Public Domain |
| assets/models/make_test_boat.py | SolidCraft contributors | original script that packs a model into the compact file above | MIT OR Apache-2.0 |
| docs/screenshots/class-a-blend.png | SolidCraft contributors | screenshot of SolidCraft itself: two boxes bridged by `surface.blend` (a 40x10 sheet, `solidcraft-cli snapshot` of plan/evidence script ev_blend) | MIT OR Apache-2.0 |
| docs/screenshots/class-a-crown.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch crowned with `surface.patch crown=0.3` over a saddle rim (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 |
| docs/screenshots/class-a-boundary.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch through two sketch lines built by `surface.boundary` with `crown: 0.4` (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 |
| docs/screenshots/sample-plate.png | SolidCraft contributors | screenshot of SolidCraft itself (`solidcraft --sample`) | MIT OR Apache-2.0 |
| docs/screenshots/class-a-blend.png | SolidCraft contributors | screenshot of SolidCraft itself: a 24 mm and a 10 mm box bridged by a `surface.blend` sheet (G1 at both edges), rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
| docs/screenshots/class-a-crown.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch crowned with `surface.patch crown=0.3` over a skew four-line rim, rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
| docs/screenshots/class-a-boundary.png | SolidCraft contributors | screenshot of SolidCraft itself: a surface through two 3D sketch splines built by `surface.boundary`, rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
| docs/screenshots/sample-plate.png | SolidCraft contributors | screenshot of SolidCraft 0.22.1 itself (`solidcraft --sample`, default dark theme, 1600x1000) | MIT OR Apache-2.0 |
| assets/samples/exports/0.png | SolidCraft contributors | original Sample Plate from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/1.png | SolidCraft contributors | original Sheet Metal Bracket from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/2.png | SolidCraft contributors | original Hinge Assembly from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
@@ -62,10 +62,10 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/samples/exports/4.png | SolidCraft contributors | original Bolted Flange from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/5.png | SolidCraft contributors | original Vent Plate from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
| assets/samples/exports/scripts.json | SolidCraft contributors | SHA-256 fingerprints of the original sample scripts, generated with the shipped thumbnails | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.8.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.8.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.gif | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon (2× playback) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.mp4 | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon | MIT OR Apache-2.0 |
| docs/screenshots/demo.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.22.1 English desktop UI (default dark theme, software renderer) modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback, idle pauses cut) | MIT OR Apache-2.0 |
| docs/screenshots/demo.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.22.1 English desktop UI (default dark theme, software renderer) modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback, idle pauses cut) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.gif | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft 0.22.1 desktop UI (default dark theme, software renderer) replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon (15× playback, idle pauses cut, constant-rate camera orbit) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.mp4 | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft 0.22.1 desktop UI (default dark theme, software renderer) replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon | MIT OR Apache-2.0 |
| docs/design/ux-redesign/model-floating.png | SolidCraft contributors | original UX mockup (Model tab, floating layout) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
| docs/design/ux-redesign/model-compact.png | SolidCraft contributors | original UX mockup (Model tab, compact docked layout) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
| docs/design/ux-redesign/sketch.png | SolidCraft contributors | original UX mockup (Sketch tab) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
+2 -2
View File
@@ -46,9 +46,9 @@
<a href="https://trendshift.io/repositories/296178?utm_source=trendshift-badge&amp;utm_medium=badge&amp;utm_campaign=badge-trendshift-296178" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/296178/daily?language=Rust" alt="tangency-labs%2Fsolidcraft | Trendshift (Rust)" width="250" height="55"/></a>
</p>
![SolidCraft 0.8: model a Raspberry Pi 4 case from an empty design: a sketch dimensioned with parameters, extrude, fillets and shell, a rim rounded in one pick, standoffs patterned on the board's mounting holes, port cut-outs, vent slots, "Pi 4" engraved in a font, a PETG material with its mass in Properties, and a height change in Parameters](docs/screenshots/demo-0.8.gif)
![SolidCraft: model a Raspberry Pi 4 case from an empty design: a sketch dimensioned with parameters, extrude, fillets and shell, a rim rounded in one pick, standoffs patterned on the board's mounting holes, port cut-outs, vent slots, "Pi 4" engraved in a font, a PETG material with its mass in Properties, and a height change in Parameters](docs/screenshots/demo.gif)
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, the rim rounded in one pick, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots, "Pi 4" engraved in DejaVu Sans Bold and a PETG material defined in the design, with its mass in Properties, then height 24 → 32 mm in Parameters and it all rebuilds. 9× playback. [Full video](https://github.com/tangency-labs/solidcraft/raw/refs/heads/main/docs/screenshots/demo-0.8.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, the rim rounded in one pick, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots, "Pi 4" engraved in DejaVu Sans Bold and a PETG material defined in the design, with its mass in Properties, then height 24 → 32 mm in Parameters and it all rebuilds. 6× playback with the idle pauses cut. [Full video](https://github.com/tangency-labs/solidcraft/raw/refs/heads/main/docs/screenshots/demo.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
## Download
Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 602 KiB

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 630 KiB

After

Width:  |  Height:  |  Size: 729 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 MiB

After

Width:  |  Height:  |  Size: 8.3 MiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 653 KiB

+2 -2
View File
@@ -4,6 +4,6 @@
*SELENE, a scientific rover built entirely through SolidCraft's MCP server by
[@0xlillith](https://github.com/0xlillith/solidcraft-rover): 3,323 editable timeline steps that
rebuild with no errors, replayed here at 2× speed.
[Full video](https://github.com/tangency-labs/solidcraft/raw/refs/heads/main/screenshots/rover-build.mp4)
rebuild with no errors, replayed here at 15× speed with the idle moments cut.
[Full video](https://github.com/tangency-labs/solidcraft/raw/refs/heads/main/docs/screenshots/rover-build.mp4)
· [download the editable model](https://github.com/0xlillith/solidcraft-rover/releases/tag/v1.0.0).*
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Render the three class-A surfacing pictures of docs/screenshots with the running app.
solidcraft --control 7878 &
python3 examples/demo/class_a_shots.py --port 7878 OUTDIR
Each picture is built from commands on a new design and drawn by the control channel's
`ui.render` (1280 x 800, the window's theme, no origin axes):
class-a-blend.png a 24 mm box and a 10 mm box, 20 mm apart, bridged by `surface.blend` (G1 at both edges);
class-a-boundary.png `surface.boundary` through two 3D sketch splines;
class-a-crown.png `surface.patch crown=0.3` over the four straight edges of a skew rim
(the rim's own surface is hidden).
"""
import argparse
import os
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from enclosure_mcp import control # noqa: E402
def main():
ap = argparse.ArgumentParser()
ap.add_argument("outdir")
ap.add_argument("--port", type=int, default=7878)
a = ap.parse_args()
def ui(method, params=None):
r = control(a.port, method, params)
if not r.get("ok", False):
raise RuntimeError(f"{method} {params}: {r.get('error')}")
return r.get("result")
def ex(command, params):
return ui("engine.execute", {"command": command, "params": params})
def fresh():
ui("ui.documents", {"action": "new"})
time.sleep(1)
ui("ui.home", {"open": False})
ui("ui.set", {"showOrigin": False, "showSketches": False})
def shoot(name):
ui("ui.view", {"view": "iso"})
ui("ui.camera", {"fit": True})
time.sleep(1.5)
ui("ui.render", {"path": os.path.join(os.path.abspath(a.outdir), name), "width": 1280, "height": 800})
ui("ui.prefs", {"theme": "dark", "button": "apply"})
ui("ui.resize", {"width": 1920, "height": 1080})
fresh()
ex("solid.box", {"length": 40, "width": 40, "height": 24, "corner": [0, 0, 0], "body_name": "A"})
ex("solid.box", {"length": 40, "width": 40, "height": 10, "corner": [60, 0, 0], "body_name": "B"})
ex("surface.blend", {"from": {"body": "A", "edges": [[40, 20, 24]]}, "to": {"body": "B", "edges": [[60, 20, 10]]}, "continuity": ["g1", "g1"], "body_name": "Blend"})
shoot("class-a-blend.png")
fresh()
ex("sketch.create", {"plane": "XY", "name": "Rails"})
ex("sketch.options", {"sketch_3d": True})
u = [ex("sketch.spline3d", {"points": p})["curves"][0] for p in ([[0, 0, 0], [20, 0, 14], [40, 0, 0]], [[0, 30, 8], [20, 30, 0], [40, 30, 8]])]
ex("sketch.finish", {})
ex("surface.boundary", {"sketch": "Rails", "u": u, "body_name": "Patch"})
shoot("class-a-boundary.png")
fresh()
ex("sketch.create", {"plane": "XY", "name": "Rim"})
ex("sketch.options", {"sketch_3d": True})
p = [[0, 0, 0], [40, 0, 8], [40, 40, 0], [0, 40, 8]]
w = [ex("sketch.line3d", {"points": [p[i], p[j]]})["curves"][0] for i, j in ((0, 1), (0, 3), (3, 2), (1, 2))]
ex("sketch.finish", {})
ex("surface.boundary", {"sketch": "Rim", "u": [w[0], w[2]], "v": [w[1], w[3]], "body_name": "Rim surface"})
def mid(i, j):
return [(p[i][k] + p[j][k]) / 2 for k in range(3)]
ex("surface.patch", {"body": "Rim surface", "edges": [mid(0, 1), mid(0, 3), mid(3, 2), mid(1, 2)], "crown": 0.3, "body_name": "Crown"})
ui("ui.set", {"hiddenBodies": ["Rim surface"]})
shoot("class-a-crown.png")
if __name__ == "__main__":
main()
+9 -2
View File
@@ -682,7 +682,7 @@ def run_pi(d, release="0.8"):
d.tool("sketch.create")
d.click(d.at(world=[-5, 18, 2]), settle=0.9)
d.settle_camera()
loop = rectangle(d, [-8, -14, 2], [14, 1.5, 0])
loop = rectangle(d, [-8, -14, 2], [-14, 2.5, 0])
d.tool("sketch.finish")
d.view("home", 0.4)
extrude(d, [loop], "-wall")
@@ -717,6 +717,9 @@ def run_pi(d, release="0.8"):
d.orbit(-260, 10, steps=110, around=[0, 0, 10])
d.glide(away, 0.3)
d.settle_camera()
# Frame the finished 32 mm case whole, in the same three-quarter direction.
d.ui("ui.view", {"view": "fit", "animate": True})
d.settle_camera()
d.wait(1.0)
@@ -733,7 +736,11 @@ def main():
a = ap.parse_args()
if not math.isfinite(a.speed) or a.speed <= 0 or not math.isfinite(a.minimum_caption) or a.minimum_caption < 0:
ap.error("pause scale must be positive and caption duration nonnegative")
mcp = Mcp([a.cli, "mcp", "--connect", f"127.0.0.1:{a.port}"])
argv = [a.cli, "mcp", "--connect", f"127.0.0.1:{a.port}"]
if a.save:
# MCP file access is limited to the open design's folder and the recovery folder.
argv += ["--allow-path", os.path.dirname(os.path.abspath(a.save))]
mcp = Mcp(argv)
mcp.send("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "enclosure-demo-v2", "version": "2"}})
mcp.send("notifications/initialized", notify=True)
d = Demo(a.port, mcp, open(a.log, "w") if a.log else None, a.speed, a.minimum_caption)
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
# record_rover.sh DISPLAY PORT DESIGN OUTDIR [PLAYBACK_RATE]: record rover_replay.py replaying DESIGN's
# timeline in the app on X display DISPLAY (1920x1080, started with --control PORT) to
# OUTDIR/rover-build.mp4 (H.264) and .gif, accelerated PLAYBACK_RATE times (default 15) with the idle
# stretches cut (trim_idle.py).
# SOLIDCRAFT_DEMO_GIF_WIDTH and SOLIDCRAFT_DEMO_GIF_FPS size the GIF (default 800 px, 10 fps).
set -e
DISP=${1:-:0}; PORT=${2:-7878}; DESIGN=$3; OUT=$(cd "${4:-.}" && pwd); RATE=${5:-15}
HERE=$(cd "$(dirname "$0")" && pwd)
NAME=${SOLIDCRAFT_DEMO_NAME:-rover-build}
GIF_WIDTH=${SOLIDCRAFT_DEMO_GIF_WIDTH:-800}
GIF_FPS=${SOLIDCRAFT_DEMO_GIF_FPS:-10}
ffmpeg -loglevel info -nostats -y -f x11grab -draw_mouse 0 -video_size 1920x1080 -framerate 30 -i "$DISP" \
-c:v libx264 -preset veryfast -crf 16 -pix_fmt yuv420p "$OUT/raw.mp4" 2> "$OUT/ffmpeg.log" &
FF=$!
trap 'kill -INT $FF 2>/dev/null || true; wait $FF 2>/dev/null || true' EXIT
sleep 1
python3 "$HERE/rover_replay.py" --port "$PORT" "$DESIGN" | tee "$OUT/replay.log"
sleep 3
kill -INT $FF; wait $FF || true
FSTART=$(grep -o "start: [0-9.]*" "$OUT/ffmpeg.log" | head -1 | cut -d" " -f2)
A=$(awk '/^start/{print $2}' "$OUT/replay.log"); B=$(awk '/^end/{print $2}' "$OUT/replay.log")
SS=$(python3 -c "print(max(0,$A-$FSTART-0.5))"); TO=$(python3 -c "print($B-$FSTART+1.5)")
ffmpeg -loglevel error -y -ss "$SS" -to "$TO" -i "$OUT/raw.mp4" -an -c:v libx264 -preset veryfast -crf 14 -pix_fmt yuv420p "$OUT/real-time.mp4"
python3 "$HERE/trim_idle.py" "$OUT/real-time.mp4" "$OUT/$NAME.mp4" --rate "$RATE" --crf 23
ffmpeg -loglevel error -y -i "$OUT/$NAME.mp4" \
-vf "fps=$GIF_FPS,scale=$GIF_WIDTH:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=96:stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" \
"$OUT/$NAME.gif"
ls -la "$OUT/$NAME.mp4" "$OUT/$NAME.gif"
+11 -5
View File
@@ -4,12 +4,13 @@
# OUTDIR/enclosure-demo-STORY.mp4 (H.264) and .gif, with captions and a drawn pointer (the app
# gets synthetic pointer events, so the screen's own pointer never moves). Needs ffmpeg with
# x11grab and libass.
# PLAYBACK_RATE defaults to 1; label accelerated previews with their rate when publishing.
# PLAYBACK_RATE defaults to 1; label accelerated previews with their rate when publishing. Idle stretches
# (nothing on screen changes) are cut by trim_idle.py.
# SOLIDCRAFT_DEMO_NAME overrides the filename stem; PAUSE scales UI pauses, CAPTION is the
# minimum real-time caption duration, and HOLD keeps the live completed model on screen.
# For a readable 4x preview use SOLIDCRAFT_DEMO_CAPTION=4 and SOLIDCRAFT_DEMO_HOLD=8.
# SOLIDCRAFT_DEMO_GIF_WIDTH and SOLIDCRAFT_DEMO_GIF_FPS size the README GIF (default 800 px, 10 fps).
# The README demo (0.8): SOLIDCRAFT_DEMO_NAME=demo-0.8 SOLIDCRAFT_DEMO_PAUSE=0.4 SOLIDCRAFT_DEMO_CAPTION=9
# The README demo: SOLIDCRAFT_DEMO_NAME=demo SOLIDCRAFT_DEMO_PAUSE=0.4 SOLIDCRAFT_DEMO_CAPTION=9
# SOLIDCRAFT_DEMO_HOLD=10 record_ui.sh :74 7974 OUT pi 9
set -e
DISP=${1:-:0}; PORT=${2:-7878}; OUT=$(cd "${3:-.}" && pwd); STORY=${4:-pi}; RATE=${5:-1}
@@ -78,9 +79,14 @@ else:
select="" # Partial rehearsals have no health/save/reopen interval.
pathlib.Path(destination).write_text(select+f"setpts=N/(FRAME_RATE*TB*{rate})")
PY
# Accelerate the public composite: cursor, ripples and captions stay aligned with the UI.
ffmpeg -loglevel error -y -i "$OUT/real-time.mp4" -vf "$(cat "$OUT/public.filter")" -an \
-c:v libx264 -preset slow -crf 20 -pix_fmt yuv420p -movflags +faststart "$OUT/$NAME.mp4"
# Speed the public composite up RATE times and cut the idle stretches (cursor, ripples and captions
# stay aligned with the UI). A take with a private proof interval drops it first.
SRC="$OUT/real-time.mp4"
if grep -q select "$OUT/public.filter"; then
ffmpeg -loglevel error -y -i "$SRC" -vf "$(sed 's/,setpts.*//' "$OUT/public.filter"),setpts=N/(FRAME_RATE*TB)" -an -c:v libx264 -preset slow -crf 16 -pix_fmt yuv420p "$OUT/public-source.mp4"
SRC="$OUT/public-source.mp4"
fi
python3 "$HERE/trim_idle.py" "$SRC" "$OUT/$NAME.mp4" --rate "$RATE"
ffmpeg -loglevel error -y -i "$OUT/$NAME.mp4" \
-vf "fps=$GIF_FPS,scale=$GIF_WIDTH:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=96:stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" \
"$OUT/$NAME.gif"
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Replay a design's timeline in a running SolidCraft, for a screen recording (record_rover.sh).
solidcraft --control 7878 &
python3 examples/demo/rover_replay.py --port 7878 Rover-recorded.solidcraft
Opens the design, frames the finished model, rolls the timeline back to its start and then
forward in even steps (the History bar's roll-to, as `timeline.roll_to` over the control
channel) while the camera orbits the model at a constant rate; it ends a few seconds after the last step. Prints "start" and "end" lines with epoch times so the recorder can trim.
"""
import argparse
import math
import os
import sys
import threading
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from enclosure_mcp import control # noqa: E402
def main():
ap = argparse.ArgumentParser()
ap.add_argument("design")
ap.add_argument("--port", type=int, default=7878)
ap.add_argument("--rate", type=float, default=0.0133, help="orbit speed in radians per second of real time")
ap.add_argument("--turn", type=float, default=4, help="seconds the orbit goes on after the last step")
a = ap.parse_args()
def ui(method, params=None):
r = control(a.port, method, params)
if not r.get("ok", False):
raise RuntimeError(f"{method} {params}: {r.get('error')}")
return r.get("result")
ui("ui.prefs", {"language": "en", "theme": "dark", "button": "apply"})
ui("ui.resize", {"width": 1920, "height": 1080})
ui("ui.home", {"open": False})
ui("ui.file", {"path": os.path.abspath(a.design)})
time.sleep(1)
while ui("ui.inspect").get("file_busy"):
time.sleep(0.5)
ui("ui.home", {"open": False})
n = ui("engine.execute", {"command": "timeline.roll_to", "params": {}})["features"]
joints = control(a.port, "engine.execute", {"command": "joint.inspect", "params": {}}).get("result", {}).get("joints", [])
ui("ui.set", {"showSketches": False, "showOrigin": False, "hiddenJoints": [j["id"] for j in joints if "id" in j]})
ui("ui.view", {"view": "iso"})
ui("ui.camera", {"fit": True})
time.sleep(1.5)
cam = ui("ui.inspect")["camera"]
yaw0 = cam["yaw"]
# Keep the finished model in frame all through the turn.
ui("ui.camera", {"distance": cam["distance"] * 0.82})
time.sleep(0.5)
def execute(params):
r = control(a.port, "engine.execute", {"command": "timeline.roll_to", "params": params})
if not r.get("ok", False):
raise RuntimeError(r)
execute({"position": 0})
time.sleep(1.0)
print("start", time.time(), flush=True)
# The camera orbits at a constant rate for the whole replay (a thread, so it does not wait
# for the timeline steps): yaw = yaw0 + rate * seconds since the start.
t0 = time.time()
stop = threading.Event()
def orbit():
while not stop.is_set():
ui("ui.camera", {"yaw": yaw0 + a.rate * (time.time() - t0)})
time.sleep(0.03)
spin = threading.Thread(target=orbit, daemon=True)
spin.start()
steps = 90
for i in range(1, steps + 1):
pos = round(n * i / steps)
execute({"position": pos} if pos < n else {})
time.sleep(a.turn)
stop.set()
spin.join()
print("end", time.time(), flush=True)
if __name__ == "__main__":
main()
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""trim_idle.py IN OUT [--rate R] [--crf N]: speed a real-time screen recording up R times and cut
the idle stretches (nothing on screen changes: the pointer is still and the app is waiting).
A run of near-identical frames that would play for more than 0.15 s at the new speed is cut down
to 0.1 s; the last run (the held finished model) is kept for 0.5 s. Frames are compared at 320 px
wide, so a moving pointer, a drifting camera or a changing caption all count as activity.
Then every R-th remaining frame is kept at 30 fps and the result is encoded like the other
recordings (H.264 CRF 20, yuv420p, faststart). Pure Python and ffmpeg; no other dependencies."""
import argparse
import json
import subprocess
ap = argparse.ArgumentParser()
ap.add_argument("inp")
ap.add_argument("out")
ap.add_argument("--rate", type=float, default=1.0)
ap.add_argument("--crf", default="20")
ap.add_argument("--threshold", type=int, default=4, help="grey levels a pixel must change by")
a = ap.parse_args()
FPS = 30
info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v", "-show_entries", "stream=width,height", "-of", "json", a.inp]))["streams"][0]
W, H = info["width"], info["height"]
AW = 320
AH = int(AW * H / W) // 2 * 2
def frames(vf, size):
cmd = ["ffmpeg", "-v", "error", "-i", a.inp] + (["-vf", vf] if vf else []) + ["-f", "rawvideo", "-pix_fmt", "gray" if vf else "rgb24", "-"]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
while True:
b = p.stdout.read(size)
if len(b) < size:
break
yield b
active, prev = [], None
for f in frames(f"fps={FPS},scale={AW}:{AH}", AW * AH):
active.append(True if prev is None else sum(1 for x, y in zip(f, prev) if abs(x - y) > a.threshold) >= 2)
prev = f
n = len(active)
longest, keep_run, end_keep = int(0.15 * a.rate * FPS), int(0.1 * a.rate * FPS), int(0.5 * a.rate * FPS)
keep = [True] * n
i = cut = 0
while i < n:
if active[i]:
i += 1
continue
j = i
while j < n and not active[j]:
j += 1
k = end_keep if j == n else keep_run
if j - i > max(longest, k):
for t in range(i + k, j):
keep[t] = False
cut += j - i - k
i = j
kept = sum(keep)
print(f"{a.inp}: {n / FPS:.1f}s real time, {cut / FPS:.1f}s idle cut, {kept / FPS / a.rate:.1f}s at {a.rate}x")
enc = subprocess.Popen(["ffmpeg", "-v", "error", "-y", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{W}x{H}", "-r", str(FPS), "-i", "-", "-an", "-c:v", "libx264", "-preset", "slow", "-crf", a.crf, "-pix_fmt", "yuv420p", "-movflags", "+faststart", a.out], stdin=subprocess.PIPE)
j = 0
for idx, f in enumerate(frames(None, W * H * 3)):
if idx < n and keep[idx]:
if int(j / a.rate) != int((j - 1) / a.rate) or j == 0:
enc.stdin.write(f)
j += 1
enc.stdin.close()
enc.wait()