Skip to content

Add web-side player and complete MVP implementation of Source Editor#3592

Merged
shlzxjp merged 13 commits into
mainfrom
feature/ashstarli
Jul 21, 2026
Merged

Add web-side player and complete MVP implementation of Source Editor#3592
shlzxjp merged 13 commits into
mainfrom
feature/ashstarli

Conversation

@Ashlllx

@Ashlllx Ashlllx commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

概述

pagx-playground(Web 预览站点)中新增两项面向用户的功能,并在
pagx-viewer 上补齐相应的播放控制 API。变更范围仅限
playground/pagx-playground/playground/pagx-viewer/,未改动 libpag
的公共 API、原生代码或 exporter。

变更内容

pagx-viewer:暴露播放 API

PAGXView 新增(C++ / Emscripten binding / TS 包装 / .d.ts):

  • play() / pause() / isPlaying()
  • currentTimeMicros() / durationMicros() / currentFrameRate()
  • setLoop() / isLoop()

int64_t 返回值统一用 double 传出(emscripten::bind 不支持 64 位整数)。

pagx-playground:播放控制条

  • 画布下方新增控制条:播放/暂停、上一帧/下一帧、循环切换、进度条与时间显示。
  • 循环按钮使用两个 SVG 图标("1" 表示单次,循环箭头表示循环),反映当前状态。
  • 进度条支持拖拽 seek,拖拽前处于播放态则松手后恢复播放。
  • 快捷键:Space / ← / → / L;在输入框内按键不拦截。
  • 静态 PAGX(duration 为 0)自动隐藏播放条。

pagx-playground:源码编辑器

  • 新增独立模块 src/editor/SourceEditor.ts + index.ts + styles.ts)。
  • 右侧滑出面板,L 快捷键开关,可拖拽调宽(使用 pointer capture,
    拖出浏览器窗口不卡死)。
  • 基于 CodeMirror 6:XML 语法高亮(VS Code Dark+ 配色)、行号、
    撤销/重做、选中匹配高亮;保留浏览器原生 Ctrl+F
  • 三个操作:Discard(回到上次加载的 XML)、Apply(校验后重载到
    PAGXView,不写文件)、Save(校验 + 重载 + 写回文件)。
  • 通过 DOMParser 校验 XML,失败时 toast 提示首个错误行。
  • 每次加载新文件时重置编辑历史,避免 Ctrl+Z 穿越文件。
  • 通过 window.pagx:loaded 事件 + onApply / onSave 回调对外通信,
    不与 playground 主流程耦合。

构建 / 基础设施

  • pagx-playground 新增 @codemirror/*@lezer/highlight 依赖,
    rollup 配置同步更新。
  • prebuild 脚本适配 pagx-viewer 目录结构调整。
  • 改进 Web 单线程编译路径以及lan模式方便开发

TODO

源码编辑器 MVP 已合入,以下改进将在后续 PR 中跟进:

  • 默认预览、双击进入编辑:仿浏览器 DevTools 交互,面板默认只读展示 XML,
    双击某节点后再进入可编辑态,避免误改。
  • 画布与编辑器双向选择联动:在画布选中元素时高亮对应 XML 节点,
    反之在编辑器中定位节点时高亮画布上对应的渲染对象。
  • 大文件性能优化:针对节点数较多的 PAGX,优化语法高亮与 apply 时的
    reparse/reload 路径,避免长文件下的输入卡顿。
  • XML 格式错误的结构化提示:当前仅通过 toast 提示首行错误,后续接入
    行内标记(gutter marker + 波浪线),并覆盖 schema 层的语义校验
    (如未知标签、必填属性缺失)。

@codecov-commenter

codecov-commenter commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.55%. Comparing base (9f25435) to head (2bf3d28).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3592      +/-   ##
==========================================
- Coverage   82.57%   82.55%   -0.03%     
==========================================
  Files         707      707              
  Lines       91382    91382              
  Branches    25871    25871              
==========================================
- Hits        75459    75438      -21     
- Misses      10435    10441       +6     
- Partials     5488     5503      +15     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread playground/pagx-playground/src/index.ts Outdated

// Keyboard shortcut: Space for play/pause
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.code !== 'Space') return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里 keydown 仅处理 Space。方向键 ←/→(上一帧/下一帧)以及 editor 的 L 之外没有其它键盘绑定,但 PR 描述与 README 均声称"快捷键:Space / ← / → / L"。建议补上 ArrowLeft/ArrowRight 调用 stepFrame(-1)/stepFrame(1),或修正文案。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已补上 ArrowLeft/ArrowRight 处理,index.ts 里 keydown 现在支持 Space(播放/暂停)+ ArrowLeft/ArrowRight(上/下一帧),L 键切换编辑器保留在 editor/index.ts。同时避开了 range slider 聚焦时的键冲突。感谢指出。


// Encodes XML, reparses and redraws the view. Shared by the editor's Apply
// and Save actions. Returns '' on success, otherwise an error message.
const applyXmlToView = (xmlText: string): string => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applyXmlToView 仅执行 parsePAGX→buildLayers→draw,缺少 loadPAGXData 中的 updateSize() 与 showPlaybackBar()。若编辑后 XML 的画布尺寸或时长(静态↔动画)发生变化,画布尺寸不会刷新、播放条显隐与时长显示会停留在旧状态。建议重载后复用这两步。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已抽出 refreshUIAfterReparse()(内部调用 updateSize() + showPlaybackBar()),loadPAGXData 和 applyXmlToView 都复用。这样编辑后 XML 修改画布尺寸或从静态图切换到动画时,backing store、播放条显隐、时长/帧显示都会一起刷新。感谢指出。

bool isPlaying() const;
int64_t currentTimeMicros() const;
int64_t durationMicros() const;
float currentFrameRate() const;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currentFrameRate() 实际只返回动画固定的 frameRate(),current 前缀易被误解为"实时测得 FPS"。建议改名为 frameRate() 与底层 PAGAnimation::frameRate() 保持一致。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同意,"current" 前缀确实容易被理解成"实时测得 FPS",实际只是文件固定的 PAGAnimation::frameRate()。已重命名 currentFrameRate() → frameRate(),同步改了 C++ 声明/实现、embind 导出(_frameRate)、TS 类型(_frameRate())、pagx-view.ts 包装方法以及 playground 三处调用点。

// when looping keep playing for the next cycle, otherwise park on the first frame so a
// finished single pass resets to the start instead of freezing on the last frame.
if (duration > 0) {
defaultAnimation->setCurrentTime(0);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

单次播放(非循环)自然结束时回退到第 0 帧并暂停,配合上层 100ms 定时器会使进度条/时间跳回 0.00s,而非停在最后一帧。多数播放器习惯停在末帧,请确认该行为是否符合预期。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已确认该行为符合预期, 暂不修改。

// Player-level loop switch that overrides the file's loop mode at cycle boundaries: the engine
// still drives in-cycle motion (including PingPong mirroring), but whether a completed cycle
// repeats or stops is decided here instead of by the file's LoopMode. Defaults to looping.
bool loopEnabled = true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loopEnabled 默认 true,且 loadPAGXData 每次加载都 setLoop(true),导致即使文件标记为 Once 也会被强制循环、忽略文件自身 LoopMode。请确认这是预期的产品行为。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已确认该行为符合预期, 暂不修改。

return 0.0f;
}

void PAGXView::setCurrentTimeMicros(int64_t micros) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setCurrentTimeMicros 只对 defaultAnimation 做绝对 seek,而 scene(嵌套自动播放合成)只按 delta 推进、无法随之定位。拖动进度条 seek 后,主时间轴与嵌套合成画面会出现错位。MVP 可接受,建议记录为已知限制。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MVP 阶段接受此限制。已在 PAGXView.cpp 函数上方补了 known-limitation 注释块。

Comment thread playground/pagx-playground/src/index.ts Outdated
function hidePlaybackUI(): void {
const canvas = document.getElementById('pagx-canvas') as HTMLCanvasElement;
const toolbar = document.getElementById('toolbar') as HTMLDivElement;
const playbackBar = document.getElementById('playback-bar') as HTMLDivElement;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里对 playback-bar 直接 as HTMLDivElement 后使用 .classList,未判空,而 showPlaybackBar 等处有判空,风格不一致。若后续 DOM 结构调整易 NPE,建议统一判空。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已改用可选链 document.getElementById('playback-bar')?.classList.add('hidden'),canvas / toolbar 一起统一到这种风格。

Comment thread playground/pagx-playground/src/index.ts Outdated
const canvas = document.getElementById('pagx-canvas') as HTMLCanvasElement;
const toolbar = document.getElementById('toolbar') as HTMLDivElement;
const navBtns = document.getElementById('nav-btns') as HTMLDivElement;
const playbackBar = document.getElementById('playback-bar') as HTMLDivElement;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同上:goHome 中对 playback-bar 未判空即使用 .classList,建议与其它位置统一判空处理。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已同 #7 一起改,goHome 里的 canvas / toolbar / nav-btns / playback-bar 四处全部改成可选链的风格,与 showPlaybackBar 一致。

// Detect the pagx-viewer arch by checking which glue file was copied into wasm-mt/.
// This must stay in sync with prebuild.js's auto-detection logic.
function detectGlueInfix() {
if (process.env.ARCH === 'wasm') return '.st';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里用 ARCH === "wasm" 表示单线程(多线程反而是 wasm-mt),而 prebuild.js 用 --arch st,两处关键字不一致且 wasm 作为"单线程"别名易误解。建议统一命名以提升可读性。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已统一。rollup.js 和 prebuild.js 现在都用 ARCH=st|mt,package.json scripts 里也是同一套词汇,三处一致。

Comment thread playground/pagx-playground/package.json Outdated
"@codemirror/state": "^6.7.1",
"@codemirror/view": "^6.43.6",
"@lezer/highlight": "^1.2.3",
"codemirror": "^6.0.2",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

codemirror 元包已重导出上面列出的 @codemirror/* 子包。同时显式列出两者需保证版本区间一致,否则可能引入重复实例;若无锁版本需要可考虑只保留其一。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已删除 codemirror 元包,只保留显式列出的 @codemirror/state、@codemirror/view、@lezer/highlight 等子包,避免版本漂移导致的重复实例。

@shlzxjp

shlzxjp commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

补充(非代码行,故留普通评论):PR 描述称"移除播放重构后已无引用的 GridBackground 与 ImagePatternMatrixCalculator 两个模块",但这两个文件并不在本 PR 的 diff 中,且在 PR HEAD 的 pagx-viewer/src/cpp 下也已不存在——应是此前已在 main 中删除。建议修正描述以免误导。

@shlzxjp
shlzxjp merged commit 1df68e9 into main Jul 21, 2026
9 checks passed
@shlzxjp
shlzxjp deleted the feature/ashstarli branch July 21, 2026 06:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants