mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
chore: commit all pending changes
This commit is contained in:
+101
-73
@@ -107,22 +107,31 @@ jobs:
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
# Smart caching for Rust builds - handles everything automatically
|
||||
# Smart caching for Rust builds with better restore keys
|
||||
- name: Setup Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
# Ensure cache is unique per target
|
||||
# Unique cache per target
|
||||
key: ${{ matrix.target }}
|
||||
# Cache on failure to speed up debugging
|
||||
# Use restore keys for better cache hits across version changes
|
||||
prefix-key: "v2-rust"
|
||||
# Save cache even on failure for debugging
|
||||
cache-on-failure: true
|
||||
# Working directory for the cache
|
||||
# Cache all crates to improve hit rate
|
||||
cache-all-crates: true
|
||||
# Working directory
|
||||
workspaces: "codex-rs -> target"
|
||||
# Additional cache directories
|
||||
cache-directories: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git
|
||||
|
||||
# Compiler cache across all OSes; integrates with GitHub Actions cache
|
||||
# Compiler cache across all OSes
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/[email protected]
|
||||
with:
|
||||
# Keep separate caches per compilation target
|
||||
# Separate caches per target
|
||||
cache-key: ${{ matrix.target }}
|
||||
|
||||
- name: Install mold (Linux)
|
||||
@@ -137,88 +146,106 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y musl-tools pkg-config
|
||||
|
||||
- name: Set version in Cargo.toml (workspace only)
|
||||
- name: Set version in Cargo.toml and Cargo.lock
|
||||
run: |
|
||||
VERSION="${{ needs.determine-version.outputs.version }}"
|
||||
echo "📝 Setting version to ${VERSION} in Cargo.toml files"
|
||||
echo "📝 Setting version to ${VERSION}"
|
||||
|
||||
# Update the workspace version in root Cargo.toml
|
||||
cd codex-rs
|
||||
# Use sed for all platforms (works on Windows bash too)
|
||||
|
||||
# Save the current version for replacement
|
||||
CURRENT_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
|
||||
echo "Current version in Cargo.toml: $CURRENT_VERSION"
|
||||
|
||||
# Update version in root Cargo.toml
|
||||
sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" Cargo.toml
|
||||
|
||||
# Verify the update
|
||||
echo "Updated Cargo.toml:"
|
||||
grep "^version = " Cargo.toml || true
|
||||
|
||||
# Update Cargo.lock to reflect only workspace version bumps, without
|
||||
# floating third-party dependency versions or git revisions.
|
||||
echo "🔒 Updating Cargo.lock for workspace members only"
|
||||
# Extract member directories from [workspace] table and update each workspace crate
|
||||
awk '/^\s*\[workspace\]/{ws=1} ws && /^members\s*=\s*\[/{f=1; next} f && /\]/{f=0} f{ gsub(/[",]/,""); if($1!="") print $1 }' Cargo.toml \
|
||||
| while IFS= read -r dir; do
|
||||
if [[ -f "$dir/Cargo.toml" ]]; then
|
||||
NAME=$(sed -n 's/^name\s*=\s*"\(.*\)"/\1/p' "$dir/Cargo.toml" | head -n1)
|
||||
if [[ -n "$NAME" ]]; then
|
||||
echo " • cargo update -p $NAME"
|
||||
cargo update -p "$NAME"
|
||||
fi
|
||||
# Update version in Cargo.lock WITHOUT regenerating it
|
||||
# This preserves all dependency versions and maintains cache validity
|
||||
if [ -f "Cargo.lock" ]; then
|
||||
echo "🔒 Updating version in existing Cargo.lock (preserving dependencies)"
|
||||
|
||||
# Get list of workspace members
|
||||
WORKSPACE_MEMBERS=$(awk '/^\s*\[workspace\]/{ws=1} ws && /^members\s*=\s*\[/{f=1; next} f && /\]/{f=0} f{ gsub(/[",]/,""); if($1!="") print $1 }' Cargo.toml)
|
||||
|
||||
# For each workspace member, get its package name and update version in Cargo.lock
|
||||
for dir in $WORKSPACE_MEMBERS; do
|
||||
if [ -f "$dir/Cargo.toml" ]; then
|
||||
# Update version in member's Cargo.toml
|
||||
sed -i.bak "s/^version = \".*\"/version = \"${VERSION}\"/" "$dir/Cargo.toml"
|
||||
|
||||
# Get the package name
|
||||
PKG_NAME=$(grep '^name = ' "$dir/Cargo.toml" | head -1 | sed 's/name = "\(.*\)"/\1/')
|
||||
|
||||
if [ -n "$PKG_NAME" ]; then
|
||||
echo " • Updating $PKG_NAME to version $VERSION in Cargo.lock"
|
||||
# Update this package's version in Cargo.lock
|
||||
# Use a more precise pattern that handles the package block structure
|
||||
awk -v pkg="$PKG_NAME" -v old="$CURRENT_VERSION" -v new="$VERSION" '
|
||||
/^\[\[package\]\]$/ { in_pkg=1; is_target=0; next_line_name=1 }
|
||||
in_pkg && next_line_name && /^name = / {
|
||||
next_line_name=0
|
||||
if ($0 == "name = \"" pkg "\"") {
|
||||
is_target=1
|
||||
}
|
||||
}
|
||||
in_pkg && is_target && /^version = / {
|
||||
print "version = \"" new "\""
|
||||
is_target=0
|
||||
in_pkg=0
|
||||
next
|
||||
}
|
||||
in_pkg && /^$/ { in_pkg=0; is_target=0 }
|
||||
{ print }
|
||||
' Cargo.lock > Cargo.lock.tmp && mv Cargo.lock.tmp Cargo.lock
|
||||
fi
|
||||
done
|
||||
|
||||
# Now normalize the Cargo.lock workspace versions to ensure consistency
|
||||
echo "🔧 Normalizing Cargo.lock workspace versions"
|
||||
# Build list of workspace crate names
|
||||
awk '/^\s*\[workspace\]/{ws=1} ws && /^members\s*=\s*\[/{f=1; next} f && /\]/{f=0} f{ gsub(/[",]/,""); if($1!="") print $1 }' Cargo.toml \
|
||||
| while IFS= read -r dir; do
|
||||
if [[ -f "$dir/Cargo.toml" ]]; then
|
||||
sed -n 's/^name\s*=\s*"\(.*\)"/\1/p' "$dir/Cargo.toml"
|
||||
fi
|
||||
done
|
||||
|
||||
# Also update any references to these packages in dependencies sections
|
||||
for dir in $WORKSPACE_MEMBERS; do
|
||||
if [ -f "$dir/Cargo.toml" ]; then
|
||||
PKG_NAME=$(grep '^name = ' "$dir/Cargo.toml" | head -1 | sed 's/name = "\(.*\)"/\1/')
|
||||
if [ -n "$PKG_NAME" ]; then
|
||||
# Update dependency references (these appear in different format)
|
||||
sed -i "s/\"$PKG_NAME $CURRENT_VERSION\"/\"$PKG_NAME $VERSION\"/g" Cargo.lock
|
||||
fi
|
||||
done \
|
||||
> .workspace-crate-names
|
||||
|
||||
if [[ -s .workspace-crate-names ]]; then
|
||||
awk -v ver="$VERSION" 'FNR==NR { if ($0!="") want[$0]=1; next }
|
||||
/^\[\[package\]\]$/ { pkg=""; print; next }
|
||||
/^name = \"/ && pkg=="" { if (match($0, /^name = \"([^\"]+)\"/, m)) pkg=m[1]; print; next }
|
||||
/^version = \"/ {
|
||||
if (pkg != "" && (pkg in want)) { print "version = \"" ver "\"" } else { print }
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' .workspace-crate-names Cargo.lock > Cargo.lock.tmp && mv Cargo.lock.tmp Cargo.lock
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ Cargo.lock updated (dependencies preserved)"
|
||||
else
|
||||
echo "⚠️ No Cargo.lock found, will be created during build"
|
||||
fi
|
||||
rm -f .workspace-crate-names
|
||||
|
||||
# Ensure Cargo.lock is regenerated for this runner's target so that
|
||||
# subsequent `cargo build --locked` does not fail due to missing
|
||||
# target-specific dependencies. This does not publish changes; it only
|
||||
# updates the lockfile in the workspace checkout for this job.
|
||||
echo "🔒 Regenerating Cargo.lock for current runner target"
|
||||
cargo generate-lockfile
|
||||
|
||||
# Clean up backup files
|
||||
find . -name "*.bak" -delete
|
||||
shell: bash
|
||||
|
||||
- name: Build binaries
|
||||
shell: bash
|
||||
env:
|
||||
# Use mold linker on Linux when available for faster linking
|
||||
RUSTFLAGS: ${{ contains(matrix.os, 'ubuntu') && '-C link-arg=-fuse-ld=mold' || '' }}
|
||||
# Skip macOS codesigning in CI to allow cross-arch builds
|
||||
# Use mold linker on Linux for faster linking
|
||||
# Optionally suppress warnings in release builds (remove -Awarnings if you want to see them)
|
||||
RUSTFLAGS: ${{ contains(matrix.os, 'ubuntu') && '-Awarnings -C link-arg=-fuse-ld=mold' || '-Awarnings' }}
|
||||
# Skip macOS codesigning in CI
|
||||
SKIP_SIGNING: '1'
|
||||
# sccache performs best with incremental disabled for release builds
|
||||
# Disable incremental compilation for release builds (better with sccache)
|
||||
CARGO_INCREMENTAL: "0"
|
||||
# Increase cache capacity a bit to improve hit rate across targets
|
||||
# Increase sccache cache size
|
||||
SCCACHE_CACHE_SIZE: "10G"
|
||||
# Show more detail on failures
|
||||
RUST_BACKTRACE: "1"
|
||||
run: |
|
||||
cd codex-rs
|
||||
# mozilla-actions/sccache-action sets RUSTC_WRAPPER and enables GHA cache
|
||||
command -v sccache >/dev/null 2>&1 && echo "Using sccache for compilation caching" || echo "sccache not available; proceeding without it"
|
||||
# Use all available cores
|
||||
|
||||
# Show sccache status
|
||||
command -v sccache >/dev/null 2>&1 && echo "Using sccache for compilation caching" || echo "sccache not available"
|
||||
|
||||
# Calculate parallel jobs
|
||||
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
|
||||
# Windows: Use NUMBER_OF_PROCESSORS env var
|
||||
export CARGO_BUILD_JOBS=$((${NUMBER_OF_PROCESSORS:-2} * 2))
|
||||
else
|
||||
# Unix-like: Prefer nproc if available, otherwise getconf/sysctl
|
||||
CORES=$((
|
||||
$( (command -v nproc >/dev/null 2>&1 && nproc) \
|
||||
|| (getconf _NPROCESSORS_ONLN 2>/dev/null) \
|
||||
@@ -228,10 +255,12 @@ jobs:
|
||||
export CARGO_BUILD_JOBS=$((CORES * 2))
|
||||
fi
|
||||
echo "Building with $CARGO_BUILD_JOBS parallel jobs"
|
||||
# Use --locked to ensure reproducible builds
|
||||
|
||||
# Build with --locked to ensure reproducible builds
|
||||
# If Cargo.lock doesn't exist or is invalid, this will fail (which is what we want)
|
||||
cargo build --release --locked --target ${{ matrix.target }} --bin code --bin code-tui --bin code-exec
|
||||
|
||||
# Show sccache stats if available (non-fatal)
|
||||
|
||||
# Show sccache stats
|
||||
command -v sccache >/dev/null 2>&1 && sccache --show-stats || true
|
||||
|
||||
- name: Prepare artifacts
|
||||
@@ -327,7 +356,7 @@ jobs:
|
||||
echo "skip_push=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Ensure the tag exists at HEAD, regardless of whether package.json changed
|
||||
# Ensure the tag exists at HEAD
|
||||
if ! git rev-parse "v${NEW_VERSION}" >/dev/null 2>&1; then
|
||||
git tag "v${NEW_VERSION}"
|
||||
echo "Created tag v${NEW_VERSION}"
|
||||
@@ -356,9 +385,8 @@ jobs:
|
||||
|
||||
- name: Push tag
|
||||
run: |
|
||||
# Push the tag even if no commit was made in this job
|
||||
# Push the tag even if no commit was made
|
||||
git push origin "v${{ steps.version.outputs.version }}" || true
|
||||
|
||||
|
||||
- name: Push changes
|
||||
run: |
|
||||
@@ -390,4 +418,4 @@ jobs:
|
||||
files: |
|
||||
release-assets/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
|
||||
+29
-8
@@ -20,6 +20,25 @@ fi
|
||||
|
||||
echo "Building code binary (${PROFILE} mode)..."
|
||||
|
||||
# Check Cargo.lock validity (fast, non-blocking check)
|
||||
if ! cargo metadata --locked --format-version 1 >/dev/null 2>&1; then
|
||||
echo "⚠️ Warning: Cargo.lock appears out of date or inconsistent"
|
||||
echo " This might mean:"
|
||||
echo " • You've modified Cargo.toml dependencies"
|
||||
echo " • You've changed workspace crate versions"
|
||||
echo " • The lockfile is missing entries"
|
||||
echo ""
|
||||
echo " Run 'cargo update' to update all dependencies, or"
|
||||
echo " Run 'cargo update -p <crate-name>' to update specific crates"
|
||||
echo ""
|
||||
echo " Continuing with unlocked build for development..."
|
||||
echo ""
|
||||
USE_LOCKED=""
|
||||
else
|
||||
# Lockfile is valid, use it for consistent builds
|
||||
USE_LOCKED="--locked"
|
||||
fi
|
||||
|
||||
# Select the cargo/rustc toolchain to match deploy
|
||||
# Prefer rustup with the toolchain pinned in rust-toolchain.toml or $RUSTUP_TOOLCHAIN
|
||||
USE_CARGO="cargo"
|
||||
@@ -57,13 +76,8 @@ fi
|
||||
# Build for native target (no --target flag) for maximum speed
|
||||
# This reuses the host stdlib and normal cache
|
||||
|
||||
# In fast dev profile, suppress compiler warnings for a clean output while
|
||||
# still surfacing errors. This does not affect other profiles.
|
||||
if [ "$PROFILE" = "dev-fast" ]; then
|
||||
export RUSTFLAGS="${RUSTFLAGS:-} -Awarnings"
|
||||
fi
|
||||
|
||||
${USE_CARGO} build --profile "${PROFILE}" --bin code --bin code-tui --bin code-exec
|
||||
# Build with or without --locked based on lockfile validity
|
||||
${USE_CARGO} build ${USE_LOCKED} --profile "${PROFILE}" --bin code --bin code-tui --bin code-exec
|
||||
|
||||
# Check if build succeeded
|
||||
if [ $? -eq 0 ]; then
|
||||
@@ -98,7 +112,14 @@ if [ $? -eq 0 ]; then
|
||||
echo " → Optimized for fast iteration (incremental builds enabled)"
|
||||
echo " → For production build, use: PROFILE=release-prod ./build-fast.sh"
|
||||
fi
|
||||
|
||||
# If lockfile was out of date, remind user
|
||||
if [ -z "$USE_LOCKED" ]; then
|
||||
echo ""
|
||||
echo "⚠️ Remember: Built without --locked due to Cargo.lock issues"
|
||||
echo " Consider running 'cargo update' and committing the changes"
|
||||
fi
|
||||
else
|
||||
echo "❌ Build failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Production build script - optimized for size and performance
|
||||
set -euo pipefail
|
||||
|
||||
cd codex-rs
|
||||
|
||||
echo "Building code binary (production release mode)..."
|
||||
echo "This will take longer but produce the most optimized binary."
|
||||
|
||||
# Build with the release-prod profile for maximum optimization
|
||||
cargo build --profile release-prod --bin code --target aarch64-apple-darwin
|
||||
|
||||
# Check if build succeeded
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Production build successful!"
|
||||
echo "Binary location: ./target/aarch64-apple-darwin/release-prod/code"
|
||||
|
||||
# Update symlinks for production binary
|
||||
mkdir -p ./target/release
|
||||
if [ -e "./target/release/code" ]; then
|
||||
rm -f ./target/release/code
|
||||
fi
|
||||
ln -sf ./aarch64-apple-darwin/release-prod/code ./target/release/code
|
||||
|
||||
# Update the symlink in codex-cli/bin for npm wrapper
|
||||
CODEX_CLI_BIN_CODE="../codex-cli/bin/code-aarch64-apple-darwin"
|
||||
if [ -e "$CODEX_CLI_BIN_CODE" ]; then
|
||||
rm -f "$CODEX_CLI_BIN_CODE"
|
||||
fi
|
||||
ln -sf ../../codex-rs/target/aarch64-apple-darwin/release-prod/code "$CODEX_CLI_BIN_CODE"
|
||||
|
||||
echo "✅ Symlinks updated"
|
||||
echo ""
|
||||
echo "Production binary ready: code"
|
||||
echo "Binary size: $(du -h ./target/aarch64-apple-darwin/release-prod/code | cut -f1)"
|
||||
echo ""
|
||||
echo "This build is optimized for distribution with:"
|
||||
echo " → Maximum link-time optimization (LTO=fat)"
|
||||
echo " → Single codegen unit for best performance"
|
||||
echo " → Symbols stripped for smaller size"
|
||||
else
|
||||
echo "❌ Build failed"
|
||||
exit 1
|
||||
fi
|
||||
Generated
+199
-195
File diff suppressed because it is too large
Load Diff
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Script to migrate from enum-based to trait-based HistoryCell
|
||||
|
||||
echo "Starting migration to trait-based HistoryCell..."
|
||||
|
||||
# Backup the old files
|
||||
cp codex-rs/tui/src/history_cell.rs codex-rs/tui/src/history_cell.rs.backup
|
||||
cp codex-rs/tui/src/chatwidget.rs codex-rs/tui/src/chatwidget.rs.backup
|
||||
|
||||
# Replace history_cell.rs with the new version
|
||||
cp codex-rs/tui/src/history_cell_new.rs codex-rs/tui/src/history_cell.rs
|
||||
|
||||
echo "Files backed up and new history_cell.rs in place"
|
||||
echo "Now manually updating chatwidget.rs..."
|
||||
+2
-1
@@ -8,7 +8,8 @@
|
||||
"build": "cd codex-rs && cargo build --release --bin code --bin code-tui --bin code-exec",
|
||||
"build:dev": "cd codex-rs && cargo build --bin code --bin code-tui --bin code-exec",
|
||||
"build:quick": "cd codex-rs && cargo build --release --bin code",
|
||||
"test:local": "cd codex-cli && npm link && code --version"
|
||||
"test:local": "cd codex-cli && npm link && code --version",
|
||||
"fix": "cargo fmt && cargo fix --allow-dirty && cargo clippy --fix --allow-dirty"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.5.3"
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Plot the virtual cursor path used in codex-rs/browser/src/page.rs.
|
||||
*
|
||||
* It mirrors the current algorithm: distance-based duration, optional curved midpoint,
|
||||
* rotation-driven overshoot timing (translation holds final after ~82%), and easing.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/plot_cursor_path.js --from 100,100 --to 900,520 --out /tmp/cursor_path.svg
|
||||
* node scripts/plot_cursor_path.js --from 200,300 --to 400,350 --no-curve --pxPerSec 120 --min 600 --max 4200
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
function parsePoint(s) {
|
||||
const [x, y] = String(s).split(',').map(Number);
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error(`Invalid point: ${s}`);
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
from: { x: 120, y: 90 },
|
||||
to: { x: 860, y: 520 },
|
||||
out: 'cursor_path.svg',
|
||||
fps: 60,
|
||||
// Mirror MOTION constants from page.rs (current tuning)
|
||||
pxPerSec: 120,
|
||||
min: 600,
|
||||
max: 4200,
|
||||
easing: 'cubic-bezier(0.18, 0.9, 0.18, 1)',
|
||||
arrowScale: 1.50,
|
||||
badgeScale: 1.70,
|
||||
badgeDelay: 40,
|
||||
jitter: 0,
|
||||
rotateMaxDeg: 16,
|
||||
arrowTilt: 0.85,
|
||||
badgeTilt: 0.60,
|
||||
overshootDeg: 4.0,
|
||||
arrowOvershootScale: 0.5,
|
||||
badgeOvershootScale: 0.85,
|
||||
overshootAt: 0.7,
|
||||
curveFactor: 0.25,
|
||||
curveMaxPx: 70,
|
||||
curveAlternate: true,
|
||||
tipX: 0,
|
||||
tipY: 0,
|
||||
badgeOffX: 12,
|
||||
badgeOffY: 4,
|
||||
noCurve: false,
|
||||
};
|
||||
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
const next = argv[i + 1];
|
||||
const setNum = (k, v) => { if (v === undefined) throw new Error(`Missing value for ${k}`); args[k] = Number(v); i++; };
|
||||
switch (a) {
|
||||
case '--from': args.from = parsePoint(next); i++; break;
|
||||
case '--to': args.to = parsePoint(next); i++; break;
|
||||
case '--out': args.out = next; i++; break;
|
||||
case '--fps': setNum('fps', next); break;
|
||||
case '--pxPerSec': setNum('pxPerSec', next); break;
|
||||
case '--min': setNum('min', next); break;
|
||||
case '--max': setNum('max', next); break;
|
||||
case '--no-curve': args.noCurve = true; break;
|
||||
default:
|
||||
if (a.startsWith('--')) {
|
||||
const k = a.slice(2);
|
||||
if (k in args) {
|
||||
const val = next;
|
||||
if (val === undefined) throw new Error(`Missing value for ${a}`);
|
||||
const n = Number(val);
|
||||
args[k] = Number.isFinite(n) ? n : val;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
||||
function dist(x0, y0, x1, y1) { const dx = x1 - x0, dy = y1 - y0; return Math.hypot(dx, dy); }
|
||||
function lerp(a, b, t) { return a + (b - a) * t; }
|
||||
function lerp2(a, b, t) { return { x: lerp(a.x, b.x, t), y: lerp(a.y, b.y, t) }; }
|
||||
|
||||
// Evaluate cubic-bezier easing at t; return y for x=t mapping
|
||||
function cubicBezierAt(p1x, p1y, p2x, p2y, t) {
|
||||
// Borrowed from https://github.com/gre/bezier-easing (MIT) inlined minimal.
|
||||
function A(aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; }
|
||||
function B(aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; }
|
||||
function C(aA1) { return 3.0 * aA1; }
|
||||
function calcBezier(aT, aA1, aA2) { return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; }
|
||||
function getSlope(aT, aA1, aA2) { return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); }
|
||||
|
||||
function getTforX(aX) {
|
||||
// Newton-Raphson
|
||||
let aT = aX;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const slope = getSlope(aT, p1x, p2x);
|
||||
if (slope === 0) return aT;
|
||||
const x = calcBezier(aT, p1x, p2x) - aX;
|
||||
aT -= x / slope;
|
||||
}
|
||||
return aT;
|
||||
}
|
||||
|
||||
const x = getTforX(t);
|
||||
const y = calcBezier(x, p1y, p2y);
|
||||
return y;
|
||||
}
|
||||
|
||||
function durationForDistance(d, motion) {
|
||||
let ms = (d / Math.max(1, motion.pxPerSec)) * 1000;
|
||||
if (motion.jitter > 0) {
|
||||
const j = (Math.random() * 2 - 1) * motion.jitter;
|
||||
ms = ms * (1 + j);
|
||||
}
|
||||
ms = Math.min(motion.max, Math.max(motion.min, ms));
|
||||
return Math.round(ms);
|
||||
}
|
||||
|
||||
function computePath(args) {
|
||||
const o = { ...args };
|
||||
const ax0 = Math.round(args.from.x - o.tipX);
|
||||
const ay0 = Math.round(args.from.y - o.tipY);
|
||||
const ax1 = Math.round(args.to.x - o.tipX);
|
||||
const ay1 = Math.round(args.to.y - o.tipY);
|
||||
const bx0 = ax0 + o.badgeOffX, by0 = ay0 + o.badgeOffY;
|
||||
const bx1 = ax1 + o.badgeOffX, by1 = ay1 + o.badgeOffY;
|
||||
|
||||
const d = dist(ax0, ay0, ax1, ay1);
|
||||
const base = durationForDistance(d, o);
|
||||
const aDur = Math.round(base * o.arrowScale);
|
||||
const bDur = Math.round(base * o.badgeScale);
|
||||
|
||||
// Curve midpoint
|
||||
let mx = (ax0 + ax1) / 2;
|
||||
let my = (ay0 + ay1) / 2;
|
||||
if (!o.noCurve) {
|
||||
const dx = ax1 - ax0, dy = ay1 - ay0;
|
||||
const len = Math.hypot(dx, dy) || 1;
|
||||
const nqx = -dy / len, nqy = dx / len;
|
||||
const curveMag = Math.min(o.curveMaxPx || 0, Math.max(0, d * (o.curveFactor || 0)));
|
||||
const curveSign = 1; // stateless plot (no alternation)
|
||||
mx = Math.round(mx + nqx * curveMag * curveSign);
|
||||
my = Math.round(my + nqy * curveMag * curveSign);
|
||||
}
|
||||
|
||||
const offsets = [0.0, 0.5, 0.82, 0.90, 1.0];
|
||||
for (let i = 1; i < offsets.length; i++) {
|
||||
if (!(offsets[i] > offsets[i - 1])) throw new Error('Non-increasing keyframe offsets');
|
||||
}
|
||||
|
||||
const pts = [ {x: ax0, y: ay0}, {x: mx, y: my}, {x: ax1, y: ay1} ];
|
||||
const easing = o.easing;
|
||||
const m = easing.match(/cubic-bezier\(([^)]+)\)/);
|
||||
const bez = m ? m[1].split(',').map(Number).map(v => clamp(v, -10, 10)) : [0.25, 1, 0.5, 1];
|
||||
|
||||
const samples = [];
|
||||
const totalMs = aDur;
|
||||
const step = Math.max(1, Math.round(1000 / o.fps));
|
||||
for (let tms = 0; tms <= totalMs; tms += step) {
|
||||
const t = tms / totalMs; // 0..1
|
||||
let segStart = 0, segEnd = 0.5, A = pts[0], B = pts[1];
|
||||
if (t >= 0.5 && t < 0.82) { segStart = 0.5; segEnd = 0.82; A = pts[1]; B = pts[2]; }
|
||||
else if (t >= 0.82) { segStart = 0.82; segEnd = 1.0; A = pts[2]; B = pts[2]; }
|
||||
const local = (t - segStart) / Math.max(1e-6, (segEnd - segStart));
|
||||
const eased = cubicBezierAt(bez[0], bez[1], bez[2], bez[3], clamp(local, 0, 1));
|
||||
const P = lerp2(A, B, eased);
|
||||
samples.push({ tms, x: P.x, y: P.y });
|
||||
}
|
||||
|
||||
const fallback = [];
|
||||
for (let tms = 0; tms <= totalMs; tms += step) {
|
||||
const t = tms / totalMs;
|
||||
const eased = cubicBezierAt(0.25, 1, 0.5, 1, t);
|
||||
const P = lerp2({x: ax0, y: ay0}, {x: ax1, y: ay1}, eased);
|
||||
fallback.push({ tms, x: P.x, y: P.y });
|
||||
}
|
||||
|
||||
return { aDur, bDur, pts: { start: {x: ax0, y: ay0}, mid: {x: mx, y: my}, end: {x: ax1, y: ay1} }, samples, fallback };
|
||||
}
|
||||
|
||||
function toSVG(data, width, height, margin = 20) {
|
||||
// Fit points into the viewport
|
||||
const xs = data.samples.map(p => p.x).concat([data.pts.start.x, data.pts.mid.x, data.pts.end.x]);
|
||||
const ys = data.samples.map(p => p.y).concat([data.pts.start.y, data.pts.mid.y, data.pts.end.y]);
|
||||
const minX = Math.min(...xs), maxX = Math.max(...xs);
|
||||
const minY = Math.min(...ys), maxY = Math.max(...ys);
|
||||
const spanX = Math.max(1, maxX - minX);
|
||||
const spanY = Math.max(1, maxY - minY);
|
||||
|
||||
const sx = (x) => margin + (x - minX) * (width - 2*margin) / spanX;
|
||||
const sy = (y) => margin + (y - minY) * (height - 2*margin) / spanY;
|
||||
|
||||
const path = data.samples.map((p, i) => `${i===0?'M':'L'} ${sx(p.x).toFixed(1)} ${sy(p.y).toFixed(1)}`).join(' ');
|
||||
const fbPath = data.fallback.map((p, i) => `${i===0?'M':'L'} ${sx(p.x).toFixed(1)} ${sy(p.y).toFixed(1)}`).join(' ');
|
||||
|
||||
const dots = [data.pts.start, data.pts.mid, data.pts.end]
|
||||
.map((p, i) => `<circle cx="${sx(p.x).toFixed(1)}" cy="${sy(p.y).toFixed(1)}" r="3" fill="${['#2a9d8f','#f4a261','#e76f51'][i]}"/>`).join('\n ');
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="#0b0d10"/>
|
||||
<g stroke="#444" stroke-width="1" fill="none" opacity="0.7">
|
||||
${Array.from({length: 10}).map((_,i)=>`<line x1="${margin}" x2="${width-margin}" y1="${margin + i*(height-2*margin)/10}" y2="${margin + i*(height-2*margin)/10}"/>`).join('\n ')}
|
||||
</g>
|
||||
<path d="${fbPath}" stroke="#6c757d" stroke-dasharray="4 3" stroke-width="2" fill="none"/>
|
||||
<path d="${path}" stroke="#00abff" stroke-width="2.5" fill="none"/>
|
||||
${dots}
|
||||
<text x="${margin}" y="${height - margin/2}" fill="#ddd" font-family="monospace" font-size="12">
|
||||
curved (blue) vs fallback (gray), duration: ${data.aDur}ms
|
||||
</text>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
const data = computePath(args);
|
||||
const svg = toSVG(data, 900, 480);
|
||||
fs.writeFileSync(args.out, svg, 'utf8');
|
||||
console.log(`Wrote ${args.out}`);
|
||||
console.log(`Duration (arrow): ${data.aDur} ms | samples: ${data.samples.length}`);
|
||||
console.log('Key points:', data.pts);
|
||||
console.log('First 5 samples:', data.samples.slice(0,5));
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try { main(); } catch (e) { console.error('Error:', e.message); process.exit(1); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user