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>
No files matched your search
@@ -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 |
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
<a href="https://trendshift.io/repositories/296178?utm_source=trendshift-badge&utm_medium=badge&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>
|
||||
|
||||

|
||||

|
||||
|
||||
*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
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 577 KiB After Width: | Height: | Size: 388 KiB |
|
Before Width: | Height: | Size: 602 KiB After Width: | Height: | Size: 537 KiB |
|
Before Width: | Height: | Size: 630 KiB After Width: | Height: | Size: 729 KiB |
|
Before Width: | Height: | Size: 8.8 MiB After Width: | Height: | Size: 8.3 MiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 653 KiB |
@@ -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).*
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||