From b991d59b2134ea9aa86902c88fcd83496b493573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 8 Oct 2026 23:26:32 -0400 Subject: [PATCH] Site: isometric geometry; comment checks scan JS, TS and CSS by tokens --- site/TASKS.md | 2 +- site/video/demo/make.py | 26 +++++++++++--- site/video/demo/repo.sh | 4 +-- site/video/record/lib.sh | 18 ++++++---- site/video/record/mouse.swift | 15 +++++++- site/video/record/pdf.sh | 44 +++++++++++++++++++++++ site/video/record/show.sh | 63 +++++++++++++++++++++++++++++++++ site/video/record/signature.py | 21 +++++++++++ site/video/record/table.sh | 42 +++++++++++----------- site/video/record/tree.sh | 40 +++++++++++++++++++++ tests/scripts/comments-rules.sh | 10 ++++++ tests/scripts/comments.py | 48 ++++++++++++++++++++++--- 12 files changed, 292 insertions(+), 41 deletions(-) create mode 100644 site/video/record/pdf.sh create mode 100644 site/video/record/show.sh create mode 100644 site/video/record/signature.py create mode 100644 site/video/record/tree.sh diff --git a/site/TASKS.md b/site/TASKS.md index 7651d35..9fea745 100644 --- a/site/TASKS.md +++ b/site/TASKS.md @@ -8,7 +8,7 @@ - 地址 workers.dev;中英两种语言;Vite 8 + pnpm;文件不超过 500 行,注释一行十字以内,无 dead code,出错即暴露,不过度设计。 - [x] S1 工程骨架:`site/` 下 Vite 8 + pnpm、TypeScript、无框架;`wrangler.jsonc` 只用静态资源(`assets.directory`,`404-page`);`pnpm build` 出 `dist/`,`pnpm preview` 可看;构建失败即报错 — R1 PASS -- [ ] S2 等轴几何:一种投影(真等轴,30°),无透视;积木由同一颜色的顶、左、右三面明暗构成,不用投影阴影;网格单位与高度单位;屏幕面(贴截图的平面)沿同一角度 +- [~] S2 等轴几何:一种投影(真等轴,30°),无透视;积木由同一颜色的顶、左、右三面明暗构成,不用投影阴影;网格单位与高度单位;屏幕面(贴截图的平面)沿同一角度 — R1 面间条纹(改 SVG 加底轮廓);R2 假环(只约束屏上相叠的积木,加回归);R3 注释超长;R4 comments.py 漏查 ts/css 跨行注释(加回归);R5 字符串里的 `/*` 误报、行尾注释漏查(改为逐字扫描,加回归)。五轮已满,修复带回归测试,不再送审。屏幕面随 S5 建 - [ ] S3 首屏模型:一张桌面上的微缩模型,六个应用是六座积木建筑,按区域排布;高度表达层级;应用之间的真实连接(ElyMark→ElyTree、导出→ElyPDF)是道路;载入时逐格生长、按节拍落位 - [ ] S4 交互:积木悬停上浮一格、按下下沉一格;控件在悬停、聚焦或靠近时才出现;键盘可达,焦点可见;系统减少动效时不播放生长 - [ ] S5 六个区域:每个应用一个区域,真实截图贴在等轴屏幕面上,正文只写做什么;状态用具象小道具(离线、开源、本地文件) diff --git a/site/video/demo/make.py b/site/video/demo/make.py index 2891878..f9f6e61 100644 --- a/site/video/demo/make.py +++ b/site/video/demo/make.py @@ -7,7 +7,8 @@ from xml.sax.saxutils import escape from pptx import Presentation from pptx.dml.color import RGBColor -from pptx.util import Emu, Pt +from pptx.enum.shapes import MSO_SHAPE +from pptx.util import Emu, Inches, Pt OUT = Path(sys.argv[1]) OUT.mkdir(parents=True, exist_ok=True) @@ -200,11 +201,23 @@ def pptx(path): for r in p.runs: r.font.size, r.font.color.rgb, r.font.bold, r.font.name = Pt(size), color, bold, "Helvetica Neue" + def place(shape, left, top, width, height): + shape.left, shape.top, shape.width, shape.height = Inches(left), Inches(top), Inches(width), Inches(height) + + def bar(slide, left, top, width, height): + shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(left), Inches(top), Inches(width), Inches(height)) + shape.fill.solid() + shape.fill.fore_color.rgb = accent + shape.line.fill.background() + title = deck.slides.add_slide(deck.slide_layouts[0]) title.shapes.title.text = "Coastal Trails 2027" title.placeholders[1].text = "Spring edition · Planning review" + place(title.shapes.title, 1.0, 2.2, 11.33, 1.4) + place(title.placeholders[1], 1.0, 3.7, 11.33, 0.8) paint(title.shapes.title.text_frame, 54, ink, True) paint(title.placeholders[1].text_frame, 24, accent) + bar(title, 0, 6.9, 13.333, 0.6) slides = [ ("What is new", ["Twelve new routes on the northern cliffs", "A fold-out overview map", "Tide tables for every harbor walk", "Water-resistant, lay-flat binding"]), @@ -216,12 +229,15 @@ def pptx(path): for heading, bullets in slides: slide = deck.slides.add_slide(deck.slide_layouts[1]) slide.shapes.title.text = heading + place(slide.shapes.title, 1.3, 0.6, 10.73, 1.1) paint(slide.shapes.title.text_frame, 40, ink, True) - body = slide.placeholders[1].text_frame - body.text = bullets[0] + bar(slide, 1.0, 0.75, 0.12, 0.8) + body = slide.placeholders[1] + place(body, 1.3, 2.0, 10.73, 4.6) + body.text_frame.text = bullets[0] for line in bullets[1:]: - body.add_paragraph().text = line - paint(body, 26, RGBColor(0x2B, 0x33, 0x30)) + body.text_frame.add_paragraph().text = line + paint(body.text_frame, 28, RGBColor(0x2B, 0x33, 0x30)) deck.core_properties.author = "Coastal Trails" deck.save(path) diff --git a/site/video/demo/repo.sh b/site/video/demo/repo.sh index 98a6df8..bad3491 100644 --- a/site/video/demo/repo.sh +++ b/site/video/demo/repo.sh @@ -15,7 +15,7 @@ day=0 # Commits all changes as $1 with message $2. commit() { day=$((day + 1)) - local when="2027-02-$(printf %02d "$day")T$((9 + day % 8)):$((10 + day * 7 % 50)):00" + local when="2026-09-$(printf %02d "$day")T$((9 + day % 8)):$((10 + day * 7 % 50)):00" git add -A GIT_AUTHOR_NAME="$1" GIT_COMMITTER_NAME="$1" GIT_AUTHOR_EMAIL="$(tr 'A-Z ' 'a-z.' <<<"$1")@coastaltrails.example" \ GIT_COMMITTER_EMAIL="$(tr 'A-Z ' 'a-z.' <<<"$1")@coastaltrails.example" \ @@ -106,7 +106,7 @@ commit "Ines Okafor" "Survey Dune Meadows" sed -i '' 's/Two steep descents/Two steep, short descents/' routes/cliff-path-north.md commit "Ines Okafor" "Copy edit Cliff Path North" day=$((day + 1)) -GIT_AUTHOR_DATE="2027-02-$(printf %02d "$day")T16:20:00" GIT_COMMITTER_DATE="2027-02-$(printf %02d "$day")T16:20:00" \ +GIT_AUTHOR_DATE="2026-09-$(printf %02d "$day")T16:20:00" GIT_COMMITTER_DATE="2026-09-$(printf %02d "$day")T16:20:00" \ git merge -q --no-ff maps -m "Merge the map key" git switch -q -c tide-tables diff --git a/site/video/record/lib.sh b/site/video/record/lib.sh index 09baa70..86409e7 100644 --- a/site/video/record/lib.sh +++ b/site/video/record/lib.sh @@ -42,9 +42,9 @@ launch() { rm -rf "$home" local data="$home/Library/Application Support/ElyOffice/$app" mkdir -p "$data" - printf '{"version":1,"window":{"mode":"windowed","x":%s,"y":%s,"width":%s,"height":%s}}\n' \ - "$win_x" "$win_y" "$win_w" "$win_h" >"$data/settings.json" - HOME=$home ELY_APPEARANCE=light ELY_LOCALE=en ELY_NO_DIALOG=1 ELY_LOG=info ELY_REDUCE_MOTION=0 \ + printf '{"version":1,"window":{"mode":"windowed","x":%s,"y":%s,"width":%s,"height":%s}%s}\n' \ + "$win_x" "$win_y" "$win_w" "$win_h" "${PANEL_LEFT:+,\"panels\":{\"left\":$PANEL_LEFT}}" >"$data/settings.json" + env HOME="$home" ELY_APPEARANCE=light ELY_LOCALE=en ELY_NO_DIALOG=1 ELY_LOG=info ELY_REDUCE_MOTION=0 \ ${EXTRA_ENV:-} "$root/target/release/$app" "$@" >"$out/logs/$app.out" 2>"$out/logs/$app.err" & app_pid=$! } @@ -72,7 +72,7 @@ wait_ax() { return 1 } -# Center of the first AX element whose line has $1, as "x y". +# Center "x y" of the first AX line matching $1. where() { local i at for i in $(seq 20); do @@ -140,11 +140,15 @@ print(next(round(x / scale) for x in range(start, im.size[0]) if im.getpixel((x, PY } -# Starts capturing the window region of the main screen. +# Captures the window, or a given region in points. record_start() { local x y w h - read -r x y w h < <(frame) - [ "$w" = "$win_w" ] && [ "$h" = "$win_h" ] || echo "note: window is ${w}x${h}, not ${win_w}x${win_h}" >&2 + if [ $# -eq 5 ]; then + x=$2 y=$3 w=$4 h=$5 + else + read -r x y w h < <(frame) + [ "$w" = "$win_w" ] && [ "$h" = "$win_h" ] || echo "note: window is ${w}x${h}" >&2 + fi rm -f "$out/rec/$1.mov" cap_name=$1 ffmpeg -hide_banner -loglevel error -f avfoundation -capture_cursor 1 -framerate 60 -pixel_format nv12 -i "0:none" \ diff --git a/site/video/record/mouse.swift b/site/video/record/mouse.swift index 1a27bbe..6f5b72a 100644 --- a/site/video/record/mouse.swift +++ b/site/video/record/mouse.swift @@ -1,4 +1,4 @@ -// Human-paced mouse: move, click, double, drag, scroll. +// Human-paced mouse: move, click, double, drag, path, scroll. import CoreGraphics import Foundation @@ -55,6 +55,19 @@ case "drag": glide(to: b, seconds: n.count > 4 ? n[4] : 0.7, dragging: true) usleep(120_000) post(.leftMouseUp, b) +case "path": + // Seconds, then x y pairs; drags through them. + let seconds = n[0] + let points = stride(from: 1, to: n.count - 1, by: 2).map { CGPoint(x: n[$0], y: n[$0 + 1]) } + glide(to: points[0], seconds: pace(points[0])) + usleep(150_000) + post(.leftMouseDown, points[0]) + for p in points.dropFirst() { + post(.leftMouseDragged, p) + usleep(useconds_t(seconds / Double(points.count) * 1_000_000)) + } + usleep(100_000) + post(.leftMouseUp, points[points.count - 1]) case "scroll": // Pixel deltas, spread over time; negative scrolls down. let total = n[0], seconds = n.count > 1 ? n[1] : 1.0 diff --git a/site/video/record/pdf.sh b/site/video/record/pdf.sh new file mode 100644 index 0000000..2623514 --- /dev/null +++ b/site/video/record/pdf.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# ElyPDF: scroll pages, highlight, draw and place a signature. +source "$(dirname "$0")/lib.sh" + +us_input +launch ely-pdf "$demo/Coastal Trails Guide.pdf" +wait_log "pdf: first tile drawn" 60 +front +sleep 4 +x=$win_x +y=$win_y +menu() { + osascript -e "tell application \"System Events\" to tell (first process whose unix id is $app_pid) to click menu item \"$2\" of menu \"$1\" of menu bar 1" >/dev/null +} +record_start pdf +pause 0.6 +mark "scroll from page 1 to page 2" +mouse move $((x + 900)) $((y + 520)) 0.5 +mouse scroll -2200 2.4 +pause 0.4 +menu View "Fit Page" +mark "Fit Page: whole page 2" +pause 0.8 +menu Annotate Highlight +mark "highlight tool; drag over two lines" +mouse drag $((x + 688)) $((y + 522)) $((x + 1118)) $((y + 538)) 1.0 +mark "highlight added" +pause 0.8 +keys "key code 53" +menu Document "Add Visible Signature…" +mark "Visible signature dialog" +pause 0.6 +mouse path 1.8 $(python3 "$video/record/signature.py" $((x + 770)) $((y + 495)) 280) +mark "signature drawn" +pause 0.5 +read -r gx gy < <(where "title=Place on Page") +mouse click "$gx" "$gy" +pause 0.5 +mouse click $((x + 960)) $((y + 710)) +mark "signature placed on page" +pause 0.6 +mouse move $((x + 1350)) $((y + 640)) 0.6 +pause 1.4 +record_stop pdf diff --git a/site/video/record/show.sh b/site/video/record/show.sh new file mode 100644 index 0000000..3d504c3 --- /dev/null +++ b/site/video/record/show.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# ElyShow: add animations, preview them, then present. +source "$(dirname "$0")/lib.sh" + +us_input +launch ely-show "$demo/Coastal Trails 2027.pptx" +wait_log "lok: first tile drawn" 60 +front +sleep 4 +x=$win_x +y=$win_y +# Picks effect $1 rows above the Add Animation button. +animate() { + local ax ay + read -r ax ay < <(where "title=Add Animation") + mouse click "$ax" "$ay" + pause 0.6 + mouse click $((ax - 19)) $((ay - $1)) +} +record_start show +pause 0.8 +mark "select the title" +mouse click $((x + 750)) $((y + 352)) +pause 0.6 +mouse move $((x + 1440)) $((y + 600)) 0.5 +mouse scroll -900 0.8 +pause 0.4 +mark "Add Animation > Fly In" +animate 359 +pause 0.8 +mark "select the subtitle" +mouse click $((x + 750)) $((y + 430)) +pause 0.6 +mark "Add Animation > Fade In" +animate 387 +pause 0.8 +read -r px py < <(where "title=Preview Animations") +mark "Preview Animations" +mouse click "$px" "$py" +pause 4.0 +record_stop show + +# The slide show fills the screen; record its middle. +mouse move 250 1100 0.4 +keys "key code 96" +sleep 3 +record_start show-play 491 30 2027 1140 +pause 0.8 +mark "Space: title flies in" +keys "key code 49" +pause 1.6 +mark "Space: subtitle fades in" +keys "key code 49" +pause 1.6 +mark "Right: slide 2" +keys "key code 124" +pause 2.0 +mark "Right: slide 3" +keys "key code 124" +pause 2.0 +record_stop show-play +keys "key code 53" +pause 1 diff --git a/site/video/record/signature.py b/site/video/record/signature.py new file mode 100644 index 0000000..494d7c4 --- /dev/null +++ b/site/video/record/signature.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Prints a looping signature stroke as x y pairs.""" +import math +import sys + +cx, cy, w = float(sys.argv[1]), float(sys.argv[2]), float(sys.argv[3]) +turns = 6 +a = 0.8 * w / (turns * 2 * math.pi) +b = 1.6 * a +left = cx - w / 2 +points = [] +for i in range(301): + t = i / 300 * turns * 2 * math.pi + lift = (2.6 if i < 50 else 1.4) * (1 + 0.35 * math.sin(t / 3)) + rise = lift * b * (1 - math.cos(t)) + points.append((left + a * t - b * math.sin(t) + 0.35 * rise, cy - rise + 0.04 * w * (i / 300))) +end_x, end_y = points[-1] +for i in range(1, 61): + s = i / 60 + points.append((end_x - s * 0.95 * w * 0.9, end_y + 0.07 * w * math.sin(s * math.pi) + 0.03 * w * s)) +print(" ".join(f"{x:.0f} {y:.0f}" for x, y in points)) diff --git a/site/video/record/table.sh b/site/video/record/table.sh index b6dfef5..b8bb60a 100644 --- a/site/video/record/table.sh +++ b/site/video/record/table.sh @@ -15,52 +15,52 @@ menu() { osascript -e "tell application \"System Events\" to tell (first process whose unix id is $app_pid) to click menu item \"$2\" of menu \"$1\" of menu bar 1" >/dev/null } record_start table -pause 0.8 +pause 0.6 mark "click B9" mouse click $(cell 242 357) -pause 0.4 +pause 0.2 mark "typing =SUM(B2:B7)" typed "=SUM(B2:B7)" pause 0.4 keys "key code 36" mark "formula entered" -pause 0.5 +pause 0.3 mouse click $(cell 242 357) -pause 0.4 +pause 0.2 mark "fill handle drag B9 to H9" -mouse drag $(cell 295 370) $(cell 880 362) 1.0 -pause 0.8 +mouse drag $(cell 295 370) $(cell 880 362) 0.8 +pause 0.6 mark "select A1:H7" -mouse drag $(cell 118 145) $(cell 880 305) 0.8 -pause 0.4 +mouse drag $(cell 118 145) $(cell 880 305) 0.6 +pause 0.2 menu Data AutoFilter mark "AutoFilter on" -pause 0.7 +pause 0.5 mouse click $(cell 921 147) mark "Total filter menu open" -pause 0.8 +pause 0.6 read -r sx sy < <(where $'title=Sort Z to A\t') mouse click "$sx" "$sy" mark "sorted Z to A by Total" -pause 1.2 -mouse click $(cell 1100 600) -pause 0.3 +pause 1.0 +mouse click $(cell 1000 420) +pause 0.2 mark "select A1:G7" -mouse drag $(cell 118 145) $(cell 774 305) 0.8 -pause 0.4 +mouse drag $(cell 118 145) $(cell 774 305) 0.6 +pause 0.2 menu Insert "Chart…" mark "Insert Chart dialog" -pause 0.8 +pause 0.5 read -r ix iy < <(where $'AXButton\tvalue=\ttitle=Insert\t') mouse click $((ix - 87)) $((iy - 185)) typed "Sales by product" -pause 0.4 +pause 0.3 mouse click "$ix" "$iy" mark "chart inserted" -pause 1.0 -mouse drag $(cell 520 300) $(cell 520 610) 0.9 +pause 0.8 +mouse drag $(cell 520 300) $(cell 520 610) 0.8 mark "chart moved below the table" -pause 0.6 -mouse click $(cell 1100 700) +pause 0.4 +mouse click $(cell 1050 520) pause 1.5 record_stop table diff --git a/site/video/record/tree.sh b/site/video/record/tree.sh new file mode 100644 index 0000000..f7bda7a --- /dev/null +++ b/site/video/record/tree.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# ElyTree: diff, stage a hunk, commit, then the history graph. +source "$(dirname "$0")/lib.sh" + +repo=/Users/Shared/Projects/field-notes +bash "$video/demo/repo.sh" "$repo" +us_input +PANEL_LEFT=540 EXTRA_ENV="ELY_GIT=/usr/bin/git" launch ely-tree "$repo" +wait_log "tree: watching" 30 +front +sleep 3 +x=$win_x +y=$win_y +record_start tree +pause 0.8 +mark "open the changed file's diff" +mouse click $((x + 92)) $((y + 157)) +pause 1.4 +mark "Stage Hunk (first hunk only)" +mouse click $((x + 1544)) $((y + 150)) +pause 1.0 +mark "type commit summary" +mouse click $((x + 250)) $((y + 736)) +pause 0.3 +typed "Harbor Loop: best at low tide" +pause 0.5 +mark "Commit to main" +mouse click $((x + 477)) $((y + 855)) +pause 1.2 +mark "History tab: graph" +mouse click $((x + 145)) $((y + 85)) +pause 1.2 +mark "open the new commit" +mouse click $((x + 260)) $((y + 220)) +pause 1.0 +mouse move $((x + 1000)) $((y + 700)) 0.8 +pause 1.5 +record_stop tree +rm -rf "$repo" +rmdir /Users/Shared/Projects 2>/dev/null || true diff --git a/tests/scripts/comments-rules.sh b/tests/scripts/comments-rules.sh index 4fb8f81..4a4ecb8 100755 --- a/tests/scripts/comments-rules.sh +++ b/tests/scripts/comments-rules.sh @@ -30,6 +30,16 @@ for ext in ts css; do check || fail "a short .$ext comment failed" rm "$work/site/src/a.$ext" done +printf 'const glob = "/*.css";\n' >"$work/site/src/a.ts" +check || fail "a /* inside a .ts string failed" +printf 'const a = 1; // One two three four five six seven eight nine ten eleven.\n' >"$work/site/src/a.ts" +! check || fail "an eleven-word trailing .ts comment passed" +rm "$work/site/src/a.ts" +printf 'a::after { content: "/*"; }\n' >"$work/site/src/a.css" +check || fail "a /* inside a .css string failed" +printf 'a { color: red; } /* One two three four five six seven eight nine ten eleven. */\n' >"$work/site/src/a.css" +! check || fail "an eleven-word trailing .css comment passed" +rm "$work/site/src/a.css" printf '// One two three four five six seven eight nine ten eleven.\n' >"$work/tests/scripts/a.swift" ! check || fail "an eleven-word Swift comment passed" echo "ok: comment rules" diff --git a/tests/scripts/comments.py b/tests/scripts/comments.py index 613ddce..548e78a 100755 --- a/tests/scripts/comments.py +++ b/tests/scripts/comments.py @@ -33,11 +33,42 @@ LINE_COMMENT = { ".h": re.compile(r"^\s*(?://[!/]?\s?(.*)|/\*\s?(.*?)\s*\*/)$"), ".nsi": re.compile(r"^\s*;\s?(.*)$"), ".swift": re.compile(r"^\s*//\s?(.*)$"), - ".js": re.compile(r"^\s*(?://\s?(.*)|/\*\*?\s?(.*?)\s*\*/)$"), - ".ts": re.compile(r"^\s*(?://\s?(.*)|/\*\*?\s?(.*?)\s*\*/)$"), - ".css": re.compile(r"^\s*/\*\s?(.*?)\s*\*/$"), } +# Scanned by tokens: strings skipped, trailing comments found. +SCANNED = {".js": True, ".ts": True, ".css": False} + + +def scan(text, slashes): + """Comments as (first line, last line, text); strings skipped.""" + found, i, line, n = [], 0, 1, len(text) + while i < n: + c = text[i] + if c in "\"'`": + i += 1 + while i < n and text[i] != c: + if text[i] == "\\": + i += 1 + elif text[i] == "\n": + line += 1 + i += 1 + elif text.startswith("/*", i): + end = text.find("*/", i + 2) + end = n if end < 0 else end + body = text[i + 2:end] + found.append((line, line + body.count("\n"), body.strip("* \n"))) + line += body.count("\n") + i = end + 1 + elif slashes and text.startswith("//", i): + end = text.find("\n", i) + end = n if end < 0 else end + found.append((line, line, text[i + 2:end].lstrip("/!").strip())) + i = end - 1 + elif c == "\n": + line += 1 + i += 1 + return found + def comment_re(path): if path.suffix == ".patch": @@ -49,6 +80,15 @@ too_long = [] repeated = {} for pattern in PATTERNS: for path in sorted(root.glob(pattern)): + if path.suffix in SCANNED: + last = None + for first, final, said in scan(path.read_text(), SCANNED[path.suffix]): + if len(said.split()) > 10: + too_long.append(f"{path.relative_to(root)}:{first}: {said[:60]}") + if final > first or last == first - 1: + repeated.setdefault(path, first) + last = final + continue comment = comment_re(path) previous = None lines = path.read_text().split("\n") @@ -65,7 +105,7 @@ for pattern in PATTERNS: if line.startswith("#!"): continue # A block comment left open spans lines. - if path.suffix in (".c", ".h", ".js", ".ts", ".css") and re.search(r"/\*(?!.*\*/)", line): + if path.suffix in (".c", ".h") and re.search(r"/\*(?!.*\*/)", line): repeated.setdefault(path, number) match = comment.match(line) said = match and next(group for group in match.groups() if group is not None)