HN 日本語サマリー

← 一覧へ戻る
プログラミング

LibreCAD in the Browser

LibreCAD in the Browser (magik.net)

68 pointsby devttyeu5 コメント

要約

この投稿では、WebAssembly(Wasm)を使用して、デスクトップアプリケーションである2D CADソフトウェアLibreCADをブラウザ上で直接実行可能にした方法について説明しています。QtフレームワークとEmscriptenを活用し、ネイティブアプリケーションの全機能(DXF/DWGファイル対応、レイヤー、ブロックなど)をブラウザで動作させることができました。特に、ネストされたダイアログやイベントループの処理といったWebAssemblyの課題を、JSPI(WebAssembly JavaScript Promise Integration)を用いて解決した点が特筆されます。これにより、ローカルにソフトウェアをインストールすることなく、手軽に2D図面を作成できるようになりました。

全文翻訳

magik.net LibreCAD in your browser 投稿日 2026年6月29日 · 更新日 2026年7月1日 時々、フロアプランのような簡単な2D図面をスケッチする必要が出てきます。私はCADユーザーではなく、5本の線と円を描くだけのためにCADをインストールしたくありません。無料のオプションはすべてデスクトップアプリです。何かをダウンロードし、インストールし、起動し、ローカルで作業します。それは自分のデスクにいるときは問題ありませんが、今は2026年であり、タブを開いて描画できないのは少し奇妙だと感じました。特にログインせずにです。 私はLibreCADを実際に使ったことも聞いたこともありませんでしたし、ターゲットユーザーでもありません。ただ簡単なスケッチができるものが欲しかっただけです。そこで、別のデスクトップインストールの試みをやめ、オープンソースアプリを見つけて、OpenCodeでGLM-5.2に指示を出してWebAssemblyにポートすることにしました。プロセス全体が非常にハンズオフで「簡単」だったことを指摘する価値はありますが、これはQtチームがWasmサポートに非常に真剣に投資していること、そしてWasmエコシステム全体がこの時点で非常に成熟していることのおかげです。これはモデルができることの限界でしたが、いくつかのガイダンスでうまくいきました。ネイティブビジョンサポートといくつかのコンピューター使用能力があれば、モデルが問題を自律的にデバッグするのに大いに役立つでしょう。GLM-5.2はその能力を欠いています。 結果は以下の通りです。アプリケーション全体(ビューアでもサブセットでもない)がWebAssemblyにコンパイルされ、このサイトで実行されています。クリックするとロードされます(ソース: github.com/magik6k/LibreCAD-Web)。 LibreCADを起動 → 初回ロードは約18MB(Brotli圧縮)。その後、ブラウザがキャッシュします。最近のChromiumベースのブラウザ(ChromeまたはEdge 137+)が必要です。このポートはWebAssembly JSPIに依存しており、FirefoxとSafariはまだ提供していません。理由は後述します。 以下のコンテンツはすべてLLMによって書かれていますが、アプリがブラウザで動作するように見えるのと同様に、技術的にはほぼ正確に見えます。ただし、詳しく調べるとひどいバグが含まれている可能性があります。これは純粋にFAFOスタイルのノーエフォートプロジェクトです。何を期待しますか? LibreCADとは LibreCADは、無料のGPLライセンスの2D CADアプリケーションです。DXFおよびDWGファイルを読み書きし、レイヤー、ブロック、寸法、ハッチング、および2D CADツールに期待されるほとんどの機能をサポートしています。Qtの上にC++で構築されており、QCad時代から存在しています。このポートは、EmscriptenとQtの公式WebAssemblyプラットフォームサポートを通じて、まったく同じC++ソースコードをWebAssemblyにコンパイルします。JavaScriptによる再実装はありません。Webネイティブのフォークもありません。サーバーサイドレンダリングもありません。実際のデスクトップアプリケーションがブラウザタブで実行されています。 どのように行われたか 最初の90%は機械的な作業でした。ツールチェーンをセットアップし、コンパイルし、GUIを起動し、ファイルを配線しました。最後の10%—モーダルダイアログを実際に機能させること—が興味深い問題であり、ツールチェーン全体を再構築する必要がありました。正直なバージョンは以下の通りです。 ツールチェーンとコンパイル Ubuntu 24.04、Emscripten、およびQtを含むDockerイメージ。完全なLibreCADソースは、Qt独自のqt.toolchain.cmake(生のEmscriptenツールチェーンファイルはfind_package(Qt6)を失敗させます)を使用して.wasmバイナリにコンパイルおよびリンクされます。デスクトップ専用の起動パス—CLI引数解析、スプラッシュスクリーン、初回実行ダイアログ、バージョンチェックネットワーク—は、デスクトップビルドが変更されないように、#ifndef Q_OS_WASMで保護されています。 GUIの起動 WebAssembly用のQtはWebGLを通じてレンダリングし、プラットフォームプラグインを通じてブラウザイベントを提供します。メインウィンドウが起動し、ツールバーとドックが表示され、キャンバスがマウスとキーボード入力を受け取ります。ここまでは順調です。 難しい部分:ネストされたダイアログとexec() LibreCADは本格的なデスクトップアプリであり、デスクトップアプリはイベントループに絶えず再突入します。QDialog::exec()はダイアログを閉じるまでブロックし、コンボボックスのドロップダウンは独自のループをスピンし、設定ダイアログから開かれたカラーピッカーはさらに別のループをネストします。Webでは、メインスレッドをブロックすることはできません。ページをフリーズせずに「ここで待つ」方法はありません。したがって、exec()は単に返りません。Emscriptenの答えはAsyncifyです。これは、ブロッキング呼び出しがブラウザにアンワインドして後で再開できるようにバイナリを書き直します。Qtはそれをサポートしており、ほとんどのQt-WASMアプリがそれを使用しています。これは1つのレベルでは機能します。Asyncifyは一度に1つの呼び出し深度しか中断できません。したがって、ダイアログは正常に開きますが、ダイアログ内のコンボボックスをクリックしたり、アプリケーション設定からカラーピッカーを開いたりすると、2番目のサスペンドは場所がなくなり、アプリ全体が停止します。設定がドロップダウンとカラーボタンでいっぱいのCADプログラムにとっては、これは荒いエッジではなく、使用不可能でした。 解決策はJSPI(WebAssembly JavaScript Promise Integration)です。ネイティブブラウザのサスペンドメカニズムであり、Asyncifyとは異なり、任意にネストできます。Qt 6.9はそれをターゲットにできます(-device-option QT_EMSCRIPTEN_ASYNCIFY=2)が、ネイティブWebAssembly例外(-fwasm-exceptions)が必要です。そして、プリビルドされたQtパッケージはどちらも提供していません。そのため、ポートは現在、JSPI + Wasm例外を有効にして、WebAssembly用にQt 6.9をソースからビルドしています。 それが本当のパズルを明らかにしました。JSPIは、WebAssemblyスタックが「プロミッシング」関数を通じて入力された場合にのみサスペンドを許可しますが、Emscriptenはmain()のみをそのようにマークします。しかし、main()が返ると(Webではそうしなければなりません)、すべてのブラウザイベント—ダイアログを開くすべてのクリック—は、プロミッシングではない独自の新しいスタックで到着するため、サスペンドはWebAssembly.promisingなしでのサスペンドを試みて中止します。 これを機能させるには、3つの協調的な変更が必要でした。 QtのDOMイベントハンドラをemscripten::async() embind関数として登録します。これにより、すべてのマウス/キーイベントがプロミッシングフレーム内で実行されます(これにはEmscripten 4.0+が必要です。古いシリーズには起動時に中止するembind+JSPIバグがありました)。イベントループからトリガーされるサスペンドも機能するように、Qtのタイマーと投稿されたイベントコールバックを同じ方法でラップします。 main()を非同期形式に再構築します。アプリを作成して返します。ブラウザがループを駆動します。なぜなら、-fwasm-exceptionsは古い「無限ループをシミュレートする」トリックと互換性がないからです。 これにより、QDialog::exec()、コンボボックスのドロップダウン、ネストされたカラーピッカー、コンテキストメニューのサブメニューが、任意のネスト深度で機能するようになりました。アプリケーションレベルのダイアログの書き直しは不要です。プラットフォームが正しいことを行います。 キャンバスを高速化する 最初の動作ビルドは、実用的なウィンドウサイズで4〜5fpsで描画されました。プロファイルの結果、フレーム時間のほぼすべてが1つのQt関数、blend_untransformed_generic_rgb64に費やされていました。原因は、QtのWebAssemblyバックストアのピクセルフォーマットでした。これはストレートアルファのRGBA8888サーフェス(HTMLキャンバスが要求するもの)です。Qtの高速パスラスターフォーマットの1つではないため、描画をウィンドウにブレンドするたびに、64ビット/ピクセルの汎用ブレンドにフォールバックしていました。これはフレームあたり3回発生します。 バックストアをプリマルチプライARGB32(Qtの最も最適化されたフォーマットで、描画されているレイヤーと一致)に切り替えると、コンポジットがSIMDパスに送られ、フラッシュ時に1回のフォーマット変換でキャンバスが必要とするRGBAバイトが生成されます。フレームレートは約3倍になりました。これはエンジン全体での勝利であり、キャンバスハックではありません。 ファイルシステムなしのファイル ブラウザには実際のファイルシステムがなく、QtのヘルパーAPI(getOpenFileContent / saveFileContent)がこのJSPIビルドでバイトを確実に配信しないことが判明しました。openは空のバッファを返し、saveは書き込み可能なストリームピッカーをチャンク化しようとしましたが、ダウンロードは生成されませんでした。どちらも現在、薄いJavaScriptシムを介して行われています。openはファイルを選択し、JSで読み込み、バイトをEmscriptenのインメモリファイルシステム(MEMFS)に直接書き込み、パスでロードします。saveはMEMFSにシリアライズし、バイトをBlobと合成ダウンロードリンクに渡します。 CADフォント(47個の.lffファイル)とハッチパターンは、30MBのデータパッケージとしてバンドルされ、MEMFSに事前にロードされます。アプリケーション設定は、IndexedDBを介してリロード間で永続化されます。プロダクションBrotli圧縮により、総転送量は約70MBから約18MBに削減されます。 PDFエクスポートはQPdfWriter(QtGuiにあり、利用できないPrintSupportモジュールなしで生存します)を介して行われ、結果をダウンロードします。カスタムHTMLシェルは、Qtのデフォルトローダーをスプラッシュスクリーンとプログレスバーに置き換えます。