吉里吉里Z 上の Elements ベース汎用ダイアログ/画面 UI (cycfi/elements + elements_modal) の作り方リファレンス。TJS で JSON / Dictionary 定義のダイアログを作る・出す・イベントを受ける・複数画面フロー(navigator)や常駐メニューを組む・入力/フォーカス/モーダルを制御する・Agent で動作検証する、といった場面で使う。基本的なダイアログ画面の作り方(最小例・レイアウト JSON スキーマ・ウィジェット一覧)から、モーダル/非モーダル/独立ウィンドウ/常駐フロー、複数インスタンス/z-order、DrawDevice 登録タイミング等のハマりどころまで網羅。Win32 ネイティブの WIN32Dialog(win32dialog プラグイン)とは別物。TJS2 言語仕様は skill `tjs2`、本体クラス API は `krkrz`、REPL/Agent 駆動は `krkrz-repl` を参照。
krkrz-webui
吉里吉里Z (krkrz) の -replweb HTTP+SSE サーバ (WebServer クラス) にブラウザ UI を載せて、本体アプリの操作/編集/観測パネルをブラウザ側に組み込む方法論。ゲーム本体は 3D 表示やゲーム内 UI に専念させ、編集ツール・インスペクター・ダッシュボード・REPL コンソールをブラウザ (別ウィンドウ/別PC) から使う構成を作るときに読む。プラグインや TJS がサーバへエンドポイントを追加公開する手順 (WebServer.register / serveStatic / broadcast)、ハンドラ呼び出し規約 (req %[method,path,query,body,bytes] → 文字列/octet/整数/辞書)、状態同期パターン (fetch POST + SSE /sub push + throttle + 差分/tick 配信)、既存 REPL コンソール (/events + /cmd) の UI 埋め込み、ブラウザのアプリモード起動 (Chromium --app / -webui)、json.dll 連携、TJS2 由来のハマりどころ (ローカル関数クロージャ不在→クラス化 / ブロックコメントのネスト誤爆 / startup 例外の致命性 / ハンドラは必ずメインスレッド実行) を網羅。エンジン側 WebServer クラスそのものの仕様は krkrz core doc/REPL.md、REPL/Agent 駆動は skill krkrz-repl、Elements ネイティブ UI は skill elements、TJS2 言語は skill tjs2、本体 API は skill krkrz を参照。
インストール方法を見る含まれるファイル(1)
- SKILL.md20.6 KB
SKILL.md(原文)
インストールする前に、エージェントに与えられる指示の中身を確認できます。
パスの基点: 本文の相対パスは engine ルート基準 (krkrz_dev では
src/core/を前置。下記参照)。作業ディレクトリが krkrz_dev 以外 (krkrz_android / krkrz_ios などの外枠や案件フォルダ) のときは${KRKRZ_BASE}/krkrz_dev/を前置して読む (echo $KRKRZ_BASEで実パスを確認。マシンごとに値が違うので絶対パスは書き込まない)。
krkrz replweb ベース ブラウザ UI 組み込み方法論
krkrz エンジンの -replweb HTTP+SSE サーバ (KRKRZ_REPL_WEB ビルド) に乗せて、
本体アプリの操作・編集・観測 UI をブラウザ側に組み込むための方法論。
「ゲーム本体 = 表示 + ゲーム内 UI、ブラウザ = 編集ツール + インスペクター + REPL」
という分離を作れる。Elements ネイティブ UI (skill elements) の制約 (レイアウト/
入力/コピペ/フォント) から解放され、UI 反復にエンジン再ビルドが要らないのが利点。
エンジン側 WebServer クラスそのものの仕様は krkrz core doc/REPL.md の
「ブラウザ REPL / Web サーバ」節が SSOT。本スキルはその上に UI を構築する側の
設計パターンをまとめる。
本文のエンジンパスは engine ルート相対 (doc/..., common/..., data/...)。
本スキルが置かれている umbrella (krkrz_dev) では src/core/ を前置して読む
(例: src/core/doc/REPL.md)。engine 単独チェックアウトならそのまま。
いつ使うか / 使わないか
- 使う: VRM/3D エディタ、ステージ/シーン編集、パラメータ調整パネル、ライブ インスペクター/ダッシュボード、リモート (別PC) 編集、REPL を UI に同居させたい。
- 使わない: ゲーム内で完結する常駐 UI・モーダルダイアログ (→ skill
elements)。 ゲームの一部として配布する対話 UI は Elements、開発/編集ツールはブラウザ、が目安。
前提
- engine が
KRKRZ_REPL_WEBビルドであること (WebServerクラスが登録される)。 - サーバ起動は
WebServer.start([port])をスクリプトから呼ぶのが基本 (既定 8899)。-replwebオプションは不要 = 利用者に毎回付けさせなくてよい (アプリの startup でif (!WebServer.active) WebServer.start(8899);)。-replweb[=port]を明示した場合は 本体が先に起動するので、WebServer.startは二重起動を避けて no-op になる。0.0.0.0:<port>で LAN 越し可 (WebServer.startAt("0.0.0.0", port)。信頼ネットワーク 限定・起動ログに警告)。待受 URL はWebServer.url/System.replWebURL。 - JSON I/O を使うなら
json.dll(Scripts.toJSONString/evalJSON)。下記参照。 - 検証には
krkrz-repl(-replfileチャネル //cmd) と curl が便利。
4 層アーキテクチャ
| 層 | 実体 | 役割 |
|---|---|---|
| ① サーバ | engine common/utils/ReplWebServer.cpp + TJS クラス WebServer (ReplWebIntf.cpp) | HTTP+SSE。ルーティング・静的配信・SSE チャネル。ハンドラは常にメインスレッド実行 |
| ② ハンドラ | ネイティブ (プラグイン) or TJS | /api/… や /app/… を処理。重い/型変換が要る処理はネイティブ、細かい操作は TJS |
| ③ アプリブリッジ | TJS (アプリ側スクリプト) | アプリのモデル層 (状態取得/操作関数) を HTTP ルートへ橋渡し + 静的配信登録 + SSE 配信 |
| ④ ブラウザ UI | data/ui/** の静的 HTML/JS | 単一ページ (ビルドレス vanilla JS で十分)。fetch POST + SSE 購読 |
最小手順
1. 静的 UI を配信する
// アプリ起動後 (startup.tjs 等) に一度
if (typeof global.WebServer != "undefined") {
WebServer.serveStatic("/ui/", "ui/"); // data/ui/** を /ui/** で配信 (json.dll 不要)
}
data/ui/index.html を置けば http://127.0.0.1:8899/ui/ で開ける。
静的配信は Storages 経由 (xp3 内でも可)。.. はトラバーサル拒否 (403)。
2. 操作エンドポイントを登録する
// TJS2 はローカル関数のレキシカルクロージャを持たない (下記ハマりどころ)。
// ハンドラはクラスのメンバにして objthis 経由で相互参照させる。
class AppBridge {
function AppBridge() {}
function handler(req) {
// req = %[ method, path, query, body(文字列), bytes(octet: body 非空時) ]
if (req.path == "/app/state") return Scripts.toJSONString(buildState());
var arg = (req.body != "") ? Scripts.evalJSON(req.body) : %[];
if (req.path == "/app/do") { app.doSomething(arg.x); push(); return "{\"ok\":true}"; }
return %[ "status" => 404, "body" => "{}" ];
}
function buildState() { return %[ /* アプリ状態 */ ]; }
function push() { WebServer.broadcast("state", Scripts.toJSONString(buildState())); }
}
global.bridge = new AppBridge();
WebServer.register("/app/", bridge.handler); // プレフィックス最長一致
3. ブラウザから叩く (index.html 内)
// 取得
const st = await (await fetch('/app/state')).json();
// 操作
await fetch('/app/do', {method:'POST', body: JSON.stringify({x:1})});
// 変更購読 (サーバ push)
const es = new EventSource('/sub/state');
es.onmessage = e => render(JSON.parse(e.data));
WebServer API 早見表
| メンバ | 用途 |
|---|---|
WebServer.register(prefix, handler) | 動的ハンドラ登録 (最長一致・上書き)。サーバ未起動でも保持され起動時から有効 |
WebServer.unregister(prefix) | 解除 |
WebServer.serveStatic(prefix, storageDir) | Storages 経由の静的配信。例 ("/ui/","ui/") |
WebServer.unserveStatic(prefix) | 静的マウント解除 |
WebServer.broadcast(channel, text) | /sub/<channel> 購読者へ SSE 配信 (改行可) |
WebServer.registerPanel(id, label, path) | 組み込み UI へタブを 1 枚足す (下記) |
WebServer.unregisterPanel(id) | パネルを外す |
WebServer.start([port]) / startAt(host, port) | サーバをスクリプトから起動 (-replweb 不要)。既に稼働中なら no-op。戻り値=稼働中か |
WebServer.stop() | サーバ停止 |
WebServer.openBrowser([url [, appMode=true]]) | url をブラウザで開く。appMode 時 Edge/Chrome を --app で試し不可なら既定ブラウザへ。url 省略で稼働中 URL。SDL 版でもアプリモード可 |
WebServer.active / WebServer.url | 稼働中か / 待受 URL |
組み込みルート: GET / = 埋め込み UI (Console / Watch のタブ) / GET /events = ログ SSE /
POST /cmd = TJS 評価 / GET /sub/<ch> = 汎用 SSE /
GET|POST /watch + /sub/watch = 監視式 / POST /pad/exec + GET|POST /pad/file = Pad / GET|POST /state + /sub/state = コントローラ / POST /bye (下記)。
組み込みルートは WebServer.register より先に判定されるので、これらのパスは
自前ハンドラで上書きできない (別名を使う)。
案件パネル (registerPanel) — 組み込み UI にタブを足す
自前 UI を丸ごと立てる前にこれを検討する。 serveStatic だけで別ページを
作ると Console / Watch / Pad とコントローラを失う。 パネルとして足せば同居できる。
WebServer.serveStatic("/tool/", "web/");
WebServer.registerPanel("mytool", "案件ツール", "/tool/tool.html");
- 中身は iframe。組み込みページへスクリプトを注入する方式ではないので、 組み込み UI の内部 DOM に依存しない (向こうが変わっても壊れない)
- 同一オリジンなのでパネルから
/cmd/watch/pad/execや自前registerエンドポイントを普通に叩ける - 同じ id で上書き。登録/解除は
/sub/panelsで開いているページへ即反映 - 中身はタブを初めて開いたときに読む (重いツールでも起動は遅くならない)
pathは/始まりのサーバ上パス。不正なら登録されずログに理由が出る (⚠ msys2 の bash から curl で叩くと/tool/...が Windows パスへ 自動変換されて弾かれる。ファイル body か PowerShell を使う)
ブラウザ UI とアプリの寿命をそろえる (両方向)
ブラウザを UI にする構成では片方だけ残るのがいちばん困る。engine 側で 両方向を閉じてある。
本体 → ブラウザが居なくなったら終了 (-replwebidle)
- 既定 5 秒で有効。
-replwebidle=noで無効、=<秒>で変更 - 一度でも SSE 購読が来てから武装する。だから購読を張らない
エージェント駆動 / API 面利用は落ちない。自前 UI は
EventSourceを 1 本張っておけばそれが «ブラウザが居る» 印になる - 複数タブは購読数で扱うので最後の 1 枚まで落ちない
- ページは
pagehide/beforeunloadでnavigator.sendBeacon('/bye')を投げると 猶予が ~2 秒へ前倒しされる (自前 UI でも同じことをすると閉じが速くなる)。 投げなくても<秒>で畳まれる
ブラウザ → 本体が居なくなったら閉じる
/sub/stateに{"exiting":true}が来たら即window.close()(engine がStop()で流す)- SSE が切れて 5 秒復帰しなければ close (クラッシュ / 強制終了)
- close が効かない通常タブは全面オーバーレイへフォールバック
- 自前 UI もこの 2 つを実装しておくと «本体だけ終わってページが残る» を防げる
ブラウザの自動オープン (-replwebopen)
既定は「ループバック束縛 かつ コンソール無し (GUI 起動)」のときだけ app モード。
端末から起動すると開かないので、開かせたいときは -replwebopen=app
(tab = 通常ウィンドウ / no = 抑止)。TJS からは WebServer.openBrowser。
⚠ engine の app モードは msedge.exe --app=<url> を ユーザの通常プロファイル
で起動する (プロファイル指定なし)。検証で --user-data-dir=<使い捨て> を
自分で渡すと Edge のオンボーディング (同期を促すダイアログ) がユーザの画面に
出るので、捨てプロファイルを使うなら --disable-sync --no-first-run --no-default-browser-check を添え、終わったら必ずウィンドウごと片付ける。
監視式 API (/watch)### Pad API (/pad/*) — 複数行スクリプトの実行と保存
POST /pad/exec— body をまるごと 1 回実行 (/cmdの 1 行実行と違い、 関数定義やループをそのまま流せる)。{"ok","result","error"}GET /pad/file?path=— ストレージから読む (text/plain)POST /pad/file?path=— ストレージへ書く。既定は 403。 本体を-replwebpad=<dir>で起動したときだけ、その接頭辞の配下へ書ける (⚠ セキュリティ境界ではなく «[保存] のうっかり» を防ぐ柵。/cmdで任意 TJS が 実行できる時点で全権限は開いている)
監視式 API (/watch) — 状態を張り込んで観測する
エンジン組み込み。式のリストを保持して、まとめて評価して返す。自前の インスペクターを書く前に、これで足りないか見ると早い。
| ルート | 説明 |
|---|---|
GET /watch | 一覧+現在値 (JSON)。評価しないのでポーリング安全。?eval=1 で評価してから返す |
POST /watch | form-urlencoded。op=add&expr=… / op=rm&id=… / op=edit&id=…&expr=… / op=clear / op=interval&ms=… / op=eval。成功なら GET と同じ JSON |
GET /sub/watch | 自動更新の push。値が変わったときだけ流れる (定数式を張っても無駄な配信は出ない) |
await fetch('/watch', {method:'POST', body:new URLSearchParams({op:'add', expr:'System.getTickCount()'})});
await fetch('/watch', {method:'POST', body:new URLSearchParams({op:'interval', ms:'500'})});
new EventSource('/sub/watch').onmessage = e => render(JSON.parse(e.data));
payload = {"interval":500,"entries":[{"id":1,"expr":"…","value":"…","error":false}]}。
interval は -1=off / 0=毎フレーム / 正値=ms (下限 100ms)。式が例外を投げても
value が "(error) …" になるだけで一覧は返る。
ハンドラ呼び出し規約
- 引数
req=%[ method, path, query, body, bytes ](bytes は body 非空時のみ octet)。 - 戻り値の解釈:
- 文字列 → 200
application/json - octet → 200
application/octet-stream - 整数 → そのステータスで空ボディ
- void → 204
- 辞書
%[status, mime, body(文字列 or octet)]→ 明示指定
- 文字列 → 200
- ハンドラ内の TJS 例外 → 500 (本文=メッセージ) +
WebServer: handler error:ログ。 - ハンドラは必ずメインスレッドで呼ばれる (
ReplMainQueueタスク)。GL/TJS/エンジン API を自由に触ってよい。長い処理はフレームを止めるので注意 (Elements 版と同じ制約)。
状態同期パターン (実践)
- client → server:
fetch POST(body=JSON)。スライダ等の連続入力は throttle (例 50ms、キー単位で最新値のみ送る)。連続反映系はpush()しない (操作元が真実。SSE 逆流で入力が上書きされるのを防ぐ)。 - server → client: SSE
/sub/state。- 各操作ルートの直後に
push()(辞書一発)。 - フレームループから ~4Hz で差分検知 push (前回 JSON と一致なら送らない)。 これで REPL 等 UI 外からの変更もブラウザに自動反映される。
- 各操作ルートの直後に
- エディタ DOM の再構築を壊さない: 高頻度で変わる観測データ (fps/ログ等) と 編集状態を分け、編集状態が変わったときだけ DOM を作り直す (観測は別 DOM を差分更新)。 スライダ操作中フラグを持ち、操作中は SSE 由来の再描画を抑止する。
REPL コンソールを UI に同居させる
エンジン組み込みの /events (ログ SSE、バックログ2000行・レベル別 cls 付き) と
/cmd (POST body=1行を TJS 評価、応答 body は継続入力中なら "1") をそのまま
ブラウザ UI 側で購読・POST するだけ。エンジン改変なしで編集 UI に REPL を統合できる。
履歴 (↑↓) と継続プロンプト切替はブラウザ側で実装する。
ブラウザをアプリモードで開く
Chromium 系の --app=<URL> でタブ/アドレスバー無しウィンドウになる。
本体の WebServer.openBrowser(url) を使うのが基本 (Edge → Chrome を --app で試し、
不可なら既定ブラウザへフォールバック)。エンジンが「URL を開く」(TVPShellExecute=
SDL は SDL_OpenURL) と「プログラムを引数付きで実行」(TVPExecuteProgram= デスクトップ
Windows は Win32 ShellExecute) を分けて実装しているので、SDL 版でもアプリモードで
開ける (System.shellExecute 直呼びは SDL で --app 引数が捨てられるため不可だった)。
WebServer.openBrowser(("" + WebServer.url) + "ui/"); // url 省略で稼働中サーバ URL
WebServer.start() を先に呼んでいればサーバは同期的に稼働 (WebServer.active=true) なので、
開くのを Timer で待つ必要はない (起動直後にそのまま openBrowser してよい)。
旧エンジン (openBrowser 未実装) 向けにフォールバックを書くなら
typeof WebServer.openBrowser != "undefined" で分岐し、無い場合のみ従来の
System.shellExecute("msedge.exe", "--app=" + url) 列を使う。
json.dll
/app/ の JSON I/O には json.dll (Scripts.toJSONString(v,indent) /
evalJSON(str) / evalJSONStorage(path,utf8) / saveJSON(path,v,utf8,indent)) が要る。
exe と同世代のものを plugin フォルダへ。ビューア/最小起動では未リンクのことがあるので、
ブリッジ側で自力 try { Plugins.link("json.dll"); } catch(e){} を試み、不可なら
静的配信 (/ui/) だけ登録して操作系を無効化するとフォールバックが綺麗。
★ハマりどころ (TJS2/krkrz 由来)
- TJS2 にローカル関数のレキシカルクロージャは無い: IIFE 内の関数同士の相互参照は
実行時「メンバが見つかりません」。→ ハンドラ/ヘルパはクラスのメンバにして
objthis(インスタンス) 経由で解決する。WebServer.register("/x/", obj.method)で渡すと呼び出し時 objthis がそのインスタンスになる。 - TJS2 のブロックコメントはネスト可能: コメント文中の
/ui/+*のような並びが/*と誤認され「コメントが終わらないまま終端に達した」。→ ブリッジ/ハンドラを書く .tjs は行コメント (//) のみにすると安全。 - startup スクリプト中の例外は致命: REPL 保護 (
TVPReplActive) はTVPCreateREPL()後に立つため、startup から呼ぶ登録スクリプトのロード失敗は プロセスごと落ちる。登録処理は例外を出さないよう防御的に書く/切り分ける。 - ハンドラはメインスレッド実行だが、HTTP スレッドは複数接続を捌く。共有状態は ハンドラ内 (メインスレッド) でのみ触る前提で設計する (engine が SubmitTask で 直列化してくれる)。
- 静的配信は json.dll 非依存。UI ページだけ先に出したいときは serveStatic を register より先に無条件で呼ぶ。
検証
- ハンドラ登録は
-replfileチャネル (skillkrkrz-repl) か/cmdから eval し、curl http://127.0.0.1:<port>/<path>で応答確認 (ブラウザ不要で往復テストできる)。 - SSE は
curl -N http://127.0.0.1:<port>/sub/<ch>で購読しつつ別で操作を投げる。 - 画面反映は
Agent.captureScreen(path)→ 画像を目視 (skillkrkrz-repl)。 - UI の JS 構文チェックは
<script>部を抜き出してnode --check。
リファレンス実装 (krkr_threepp)
VRM 立ち絵/箱庭エディタの完全な実例:
- engine:
common/utils/ReplWebServer.cpp(サーバ) /ReplWebIntf.cpp(TJS クラス) /doc/REPL.md(仕様 SSOT) - プラグイン:
main.cppのThreeWebApi(/api/three/をネイティブ公開。 リンク時に POST_REGIST からWebServer.register、unlink 時に解除)。UI 向け一括 JSON ダンプObject3D.treeJson/VRM.expressionListJson/morphListJson(UTF-8→ttstr 変換ヘルパで日本語対応。既定 std::string コンバータは ASCII 限定) - アプリ+UI:
data/webui.tjs(WebUIBridge,/app/…ブリッジ) +data/ui/index.html(5 モード編集 + サイドバー + REPL コンソール)。設計詳細はthreepp/docs/webui.md
関連スキル: krkrz-repl (REPL/Agent 駆動・-replweb 起動) / elements (ネイティブ UI) /
krkrz (本体 API: Storages/System/Plugins) / tjs2 (言語仕様) / dev-toolkit:msys2 (シェル)。
レビュー
まだレビューはありません。使ってみた感想をお寄せください。
同じリポジトリのスキル
概要と使いどころ
吉里吉里Z (kirikiri Z) 本体クラス API のリファレンス。TJS2 で吉里吉里Z 上のスクリプトを書く、レビューする、デバッグするときに使う。Layer / Window / Bitmap / System / Storages / Font / Plugins / Timer / Debug / AsyncTrigger / Scripts / BinaryStream / Matrix32 / Matrix44 / Rect / ImageFunction のコア API、サウンド系 (WaveSoundBuffer / SoundBuffer / VideoOverlay)、DrawDevice (BasicDrawDevice / SDLDrawDevice / OGLDrawDevice / NullDrawDevice)、OpenGL 描画系 (Canvas / Texture / ShaderProgram / Offscreen / VertexBinder / VertexBuffer)、および主要プラグイン提供クラス (HttpRequest / GdiPlus.* / WIN32Dialog / CSVParser / LineParser / Process / Pad / MenuItem / Unzip / Zip / PSD / SimpleHTTPServer) を網羅。**呼び出されたら必ず「共通パターン」と「クロスカッティング概念」を確認し、必要な詳細クラスは doc/reference/*.md を Read しに行くこと。** TJS2 言語そのものや組み込みクラス (Array / Dictionary / Math 等) は別 skill (`tjs2`) を参照。エンジン内部構造 (C++ 実装、レンダリングパイプライン詳細) は対象外。
吉里吉里Z (krkrz) の SDL3 / WINVER ビルドを REPL 経由でエージェントから駆動するためのリファレンス。krkrz を起動して TJS スクリプトを評価・検証・デバッグする、startup.tjs を介さず明示的に処理を開始する、入力イベント (キー/マウス) を注入する、画面をキャプチャして目視確認する、Elements ダイアログを観測・操作する、例外やダイアログ表示をコンソールで観測する、コアデモ全シーンを自動巡回してキャプチャで表示確認する (-demotest / -demotestcap)、といった場面で使う。**外部エージェントは console(CONIN$) に打てないので -replfile ファイルチャネルが本命**。起動フラグ (-repl / -replfile / -nostartup / -loglevel / -display / -ignoremouse)、ファイルチャネルのプロトコル、Agent API (入力注入 / captureScreen / dialogs / dialogClick)、ドットコマンド (.cap/.dlg/.click/.mem 等)、REPL 駆動時の挙動変更 (例外で即終了しない / inform と例外ダイアログがコンソールに出る) を網羅。TJS2 言語仕様そのものは skill `tjs2`、本体クラス API は skill `krkrz` を参照。
TJS2 (吉里吉里Z 内蔵スクリプト言語) の言語仕様と組み込みクラスのリファレンス。.tjs ファイル / *.ks (KAG) 内の埋め込みスクリプト / TJS2 コード断片 を扱う、書く、レビューする、デバッグするときに使う。JavaScript / TypeScript に似ているが文法と意味論が違うので、JS の感覚で書くと壊れる場合が多い。組み込みクラス (Array / Dictionary / Date / Math / RegExp / Exception) の API もここに集約。**呼び出されたら必ず「JS との主な違い」セクションを最初に確認し、その上で必要な詳細リファレンスを Read で取りに行くこと。** 吉里吉里Z 本体のクラス API (Window / Layer / System / Storages / Bitmap 等) や engine 内部はこのスキルの対象外。