@edv4h/usketch-plugin-mode7 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +102 -0
  2. package/dist/index.d.ts +7 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +5 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/mode7-camera.d.ts +32 -0
  7. package/dist/mode7-camera.d.ts.map +1 -0
  8. package/dist/mode7-camera.js +56 -0
  9. package/dist/mode7-camera.js.map +1 -0
  10. package/dist/mode7-layers.d.ts +45 -0
  11. package/dist/mode7-layers.d.ts.map +1 -0
  12. package/dist/mode7-layers.js +112 -0
  13. package/dist/mode7-layers.js.map +1 -0
  14. package/dist/mode7-runtime.d.ts +14 -0
  15. package/dist/mode7-runtime.d.ts.map +1 -0
  16. package/dist/mode7-runtime.js +220 -0
  17. package/dist/mode7-runtime.js.map +1 -0
  18. package/dist/mode7-service.d.ts +50 -0
  19. package/dist/mode7-service.d.ts.map +1 -0
  20. package/dist/mode7-service.js +39 -0
  21. package/dist/mode7-service.js.map +1 -0
  22. package/dist/mode7-store.d.ts +74 -0
  23. package/dist/mode7-store.d.ts.map +1 -0
  24. package/dist/mode7-store.js +143 -0
  25. package/dist/mode7-store.js.map +1 -0
  26. package/dist/mode7-transform.d.ts +56 -0
  27. package/dist/mode7-transform.d.ts.map +1 -0
  28. package/dist/mode7-transform.js +123 -0
  29. package/dist/mode7-transform.js.map +1 -0
  30. package/dist/plugin.d.ts +27 -0
  31. package/dist/plugin.d.ts.map +1 -0
  32. package/dist/plugin.js +44 -0
  33. package/dist/plugin.js.map +1 -0
  34. package/dist/register-mode7-actions.d.ts +8 -0
  35. package/dist/register-mode7-actions.d.ts.map +1 -0
  36. package/dist/register-mode7-actions.js +88 -0
  37. package/dist/register-mode7-actions.js.map +1 -0
  38. package/dist/register-mode7-hud.d.ts +6 -0
  39. package/dist/register-mode7-hud.d.ts.map +1 -0
  40. package/dist/register-mode7-hud.js +130 -0
  41. package/dist/register-mode7-hud.js.map +1 -0
  42. package/package.json +49 -0
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @edv4h/usketch-plugin-mode7
2
+
3
+ ファミコン/SNES の **Mode 7** のように、Canvas を「奥へ傾く地平面」として擬似3D視点で見る
4
+ **ビューモード**プラグイン。実際の Shape をそのまま傾けて表示する(編集ではなく閲覧)。ON/OFF トグル、既定 OFF。
5
+
6
+ ## 仕組み
7
+
8
+ - `perspective()+rotateX(pitch)+rotateZ(yaw)` の **CSS 3D 変換**を、`document.head` に挿した単一の `<style>`
9
+ シート(`!important` ルール)で「板コンテンツ層」の外側ラッパ div に当てる。各レイヤーは `canvas.tsx` で
10
+ `data-layer-id` 付き div にラップされ、ルールは `data-mode7-stage="on"` を付けたメインコンテナ配下に
11
+ スコープされる。コンテナ検出は **`data-testid` ではなく `[data-layer-id]` の親**で行う(`data-testid` は
12
+ ビルドで除去され実行時に存在しないため)。スタイルシート方式なので React の再レンダで消えず、後からマウント
13
+ する層にも適用され、per-element の注入も `MutationObserver` も要らない。コアの座標変換(affine `{x,y,zoom}`)には触れない。
14
+ - 傾ける対象は **allow 方式**で、既定は「**固定UIクロム以外のほぼ全層**」(背景/Shape/GPU に加え、
15
+ 選択・コネクタ・各種オーバーレイ層も含む。除外は `vim-status-line`/`vim-which-key`/`vim-help`/
16
+ `side-panel`/`presentation-overlay` の固定UI)。**HUD の「Mode 7 レイヤー」パネルから実行時に選択できる**
17
+ (例: Shape 層のオン/オフ)。HUD 自身やプラグインの sky/fog/capture 層は選択肢から除外され、絶対に
18
+ 傾かない(`skipLayerIds` で除外を追加可)。既定一覧は `DEFAULT_TILT_LAYER_IDS` を参照。
19
+ - **sky / fog / capture** の3つの非傾斜オーバーレイ層を追加。capture 層が板のポインタを捕捉して
20
+ 3D 下での編集破綻を防ぎ、ドラッグ=地上移動(viewport pan)・wheel=ズームでカメラを操作する。
21
+ - **取り込み範囲(capture frame)**: 3Dビューが地面として映すのは「切替時のビューポート世界矩形」だけで、
22
+ その外の Shape は初期3Dビューに出ない(本方式は遠方を描画しない=現ビューポート分のみ。前進パンで到達は可)。
23
+ これを可視化するため、切替 ON の瞬間にビューポート世界矩形をスナップショットし、**フラットモードで
24
+ ワールド固定の破線枠**として描く(HUD トグル `取り込み範囲` / アクション `取り込み範囲の表示切替` で ON/OFF)。
25
+ 枠外の Shape が一目で分かるので、切替前の配置調整に使える。枠自体は非傾斜・非固定レイヤーで shape 化しない。
26
+ - 状態は**ビュー限定(per-viewer・非永続・非同期)** = shape 化しない(presentation と同類)。
27
+
28
+ ## 使い方
29
+
30
+ ```ts
31
+ import { createMode7Plugin } from "@edv4h/usketch-plugin-mode7";
32
+
33
+ createApp({
34
+ plugins: [
35
+ // …renderer / 各 shape プラグインと一緒に登録する
36
+ createMode7Plugin({
37
+ enabledInitially: false, // 既定 OFF(HUD/サービスで opt-in)
38
+ camera: { pitch: 55, fov: 600 }, // 初期カメラ(部分指定可)
39
+ look: { sky: "#0b1026", fog: 0.35 },
40
+ // tiltLayerIds: [...], // 傾ける層を上書き
41
+ // shortcuts: { toggle: "…" }, // キーバインド(opt-in・既定なし)
42
+ }),
43
+ ],
44
+ });
45
+ ```
46
+
47
+ 有効化は HUD の「3Dビュー切替」アクション、サービス `getMode7Api(app.services)?.toggle()`、
48
+ またはイベント `app.events.emit("mode7:toggle")` のいずれでも可能。
49
+
50
+ ## カメラ
51
+
52
+ | パラメータ | 意味 | 範囲 |
53
+ | --- | --- | --- |
54
+ | `pitch` | 地平面の傾き(0=真上, 大=斜め) | 0–85° |
55
+ | `yaw` | 旋回(進行方向の回転) | -180–180° |
56
+ | `fov` | CSS `perspective()` 距離(小=遠近強) | 200–2000px |
57
+ | `horizon` | 地平線の画面高さ割合(ピボット) | 0.1–0.9 |
58
+
59
+ 見た目: `sky`(空色)/ `fog`(フォグ濃度 0–1)/ `fogColor`。
60
+
61
+ **描画距離(`drawDistance`, HUD「描画距離(Canvas)」)**: 近端(画面下=カメラ手前)から**Canvas
62
+ (ワールド)単位**でどこまで地面を描くか。tilt 層は `overflow:visible`(3D保持のため)で描くので、
63
+ `clip-path` でこの距離にクリップして境界を作る(`clip-path` は `overflow` と違い 3D を潰さない)。
64
+
65
+ 既定は **1500**(開いた瞬間に地面が少し奥まで敷かれる)。`0`=**auto**=ビューポート箱で切る。
66
+
67
+ - `0`(auto) → 箱で切る。箱の外に Shape が露出せず、グリッド(`bg-grid`)と同じ範囲でピタッと切れる(浮かない)。
68
+ - **箱内の距離** → その手前だけ残す(遠方の散らばりを消す・近距離クローズアップ)。
69
+ - **箱を超える距離** → クリップを**全方向へ**負 inset で広げ(上=奥, 左右=扇状に広がる地面, 画面外は
70
+ canvas-container が clip)、**同時に `bg-grid:set-overscan` イベントでグリッドを箱外へ延長**する
71
+ (延長px = `distance*zoom - 箱高 + 余白`)。これで grid と Shape が一緒に奥+横へ伸び、遠方 Shape の
72
+ 下にもグリッドが敷かれる(専用グリッド不要)。`viewport:changed` でズームにも追従。
73
+
74
+ ※ Shape は DOM 上には残る(コアのカリングではなく描画クリップ)ので、真のジオメトリ間引きは別途。
75
+
76
+ ## アクション(`mode7:<key>` で emit・HUD Controls に自動表示・サービス)
77
+
78
+ `toggle` / `pitch-up` / `pitch-down` / `yaw-left` / `yaw-right` / `fov-narrow` / `fov-wide` / `reset` /
79
+ `capture-frame`(取り込み範囲の表示切替)。
80
+
81
+ ショートカットは **opt-in・既定なし**(共有レジストリは `event.key` 照合で macOS の Option+英字合成 /
82
+ Shift+数字変換に弱く、既定を持たせると layout により無反応になるため)。`shortcuts` オプションで任意に割当。
83
+
84
+ ## サービス API
85
+
86
+ ```ts
87
+ import { getMode7Api } from "@edv4h/usketch-plugin-mode7";
88
+ const api = getMode7Api(app.services);
89
+ api?.toggle();
90
+ api?.setPitch(60);
91
+ api?.setYaw(30);
92
+ ```
93
+
94
+ `Mode7Api`: `isActive` / `enable` / `disable` / `toggle` / `getCamera` / `setPitch` / `setYaw` /
95
+ `setFov` / `setHorizon` / `adjust` / `getLook` / `setSky` / `setFog` / `setFogColor` / `reset` /
96
+ `getTiltLayers` / `setTiltLayers` / `toggleTiltLayer` / `getDrawDistance` / `setDrawDistance` /
97
+ `isCaptureFrameVisible` / `setCaptureFrameVisible` / `toggleCaptureFrame` / `getCaptureRect` / `onChange`。
98
+
99
+ ## スコープ外(v2 送り)
100
+
101
+ 3D 中の編集(perspective 逆投影して `screenToWorld` を補正)/WebGPU 真3D/Shape の billboard 立て起こし/
102
+ 視差・霧・地平線もやの高度化/minimap の3Dプレビュー。
@@ -0,0 +1,7 @@
1
+ export { CAMERA_LIMITS, type Camera, DEFAULT_CAMERA, } from "./mode7-camera.js";
2
+ export { getMode7Api, type Mode7Api, mode7Service } from "./mode7-service.js";
3
+ export type { Look } from "./mode7-store.js";
4
+ export { drawDistanceClip, drawDistanceOverscanPx, fogBackground, skyBackground, type TiltTransform, tiltTransform, } from "./mode7-transform.js";
5
+ export { createMode7Plugin, type Mode7PluginOptions } from "./plugin.js";
6
+ export type { Mode7ActionKey, Mode7Shortcuts, } from "./register-mode7-actions.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,aAAa,EACb,KAAK,MAAM,EACX,cAAc,GACd,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,KAAK,QAAQ,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC9E,YAAY,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EACN,gBAAgB,EAChB,sBAAsB,EACtB,aAAa,EACb,aAAa,EACb,KAAK,aAAa,EAClB,aAAa,GACb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACzE,YAAY,EACX,cAAc,EACd,cAAc,GACd,MAAM,6BAA6B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { CAMERA_LIMITS, DEFAULT_CAMERA, } from "./mode7-camera.js";
2
+ export { getMode7Api, mode7Service } from "./mode7-service.js";
3
+ export { drawDistanceClip, drawDistanceOverscanPx, fogBackground, skyBackground, tiltTransform, } from "./mode7-transform.js";
4
+ export { createMode7Plugin } from "./plugin.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,aAAa,EAEb,cAAc,GACd,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAiB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE9E,OAAO,EACN,gBAAgB,EAChB,sBAAsB,EACtB,aAAa,EACb,aAAa,EAEb,aAAa,GACb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,iBAAiB,EAA2B,MAAM,aAAa,CAAC"}
@@ -0,0 +1,32 @@
1
+ export interface Camera {
2
+ pitch: number;
3
+ yaw: number;
4
+ fov: number;
5
+ horizon: number;
6
+ }
7
+ /** Allowed ranges. pitch stays < 90 so the plane never goes fully edge-on
8
+ * (which would collapse to a line); horizon stays off the very edges. */
9
+ export declare const CAMERA_LIMITS: {
10
+ readonly pitch: {
11
+ readonly min: 0;
12
+ readonly max: 85;
13
+ };
14
+ readonly fov: {
15
+ readonly min: 200;
16
+ readonly max: 2000;
17
+ };
18
+ readonly horizon: {
19
+ readonly min: 0.1;
20
+ readonly max: 0.9;
21
+ };
22
+ };
23
+ export declare const DEFAULT_CAMERA: Camera;
24
+ /** Wrap a degree value into (-180, 180]. */
25
+ export declare function wrapDeg(deg: number): number;
26
+ /** Clamp every field to its valid range (pitch/fov/horizon clamped, yaw wrapped). */
27
+ export declare function clampCamera(c: Camera): Camera;
28
+ /** Apply a partial change and re-clamp. Non-finite inputs fall back to limits. */
29
+ export declare function updateCamera(c: Camera, patch: Partial<Camera>): Camera;
30
+ /** Nudge fields by deltas (e.g. from keyboard actions) and re-clamp. */
31
+ export declare function adjustCamera(c: Camera, delta: Partial<Camera>): Camera;
32
+ //# sourceMappingURL=mode7-camera.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode7-camera.d.ts","sourceRoot":"","sources":["../src/mode7-camera.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,MAAM;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;0EAC0E;AAC1E,eAAO,MAAM,aAAa;aACzB,KAAK;iBAAI,GAAG,EAAE,CAAC;iBAAE,GAAG,EAAE,EAAE;;aACxB,GAAG;iBAAI,GAAG,EAAE,GAAG;iBAAE,GAAG,EAAE,IAAI;;aAC1B,OAAO;iBAAI,GAAG,EAAE,GAAG;iBAAE,GAAG,EAAE,GAAG;;CACpB,CAAC;AAEX,eAAO,MAAM,cAAc,EAAE,MAAuD,CAAC;AAOrF,4CAA4C;AAC5C,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM3C;AAED,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAO7C;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAEtE;AAED,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAOtE"}
@@ -0,0 +1,56 @@
1
+ // Pure, React-free camera model for the Mode 7 view (so the runtime, service, HUD,
2
+ // and unit tests import it without pulling in React). The camera is GEOMETRY only —
3
+ // it describes how the flat board plane is tilted into a receding ground plane:
4
+ // - pitch … ground tilt in degrees (0 = flat top-down, higher = more oblique)
5
+ // - yaw … heading rotation in degrees (spin the ground around the view axis)
6
+ // - fov … CSS `perspective()` distance in px (smaller = stronger perspective)
7
+ // - horizon … pivot line as a fraction of viewport height (0=top … 1=bottom);
8
+ // where the ground meets the sky after the tilt.
9
+ // Appearance (sky color, fog) is separate — see `mode7-transform.ts`.
10
+ /** Allowed ranges. pitch stays < 90 so the plane never goes fully edge-on
11
+ * (which would collapse to a line); horizon stays off the very edges. */
12
+ export const CAMERA_LIMITS = {
13
+ pitch: { min: 0, max: 85 },
14
+ fov: { min: 200, max: 2000 },
15
+ horizon: { min: 0.1, max: 0.9 },
16
+ };
17
+ export const DEFAULT_CAMERA = { pitch: 55, yaw: 0, fov: 600, horizon: 0.45 };
18
+ function clamp(v, min, max) {
19
+ if (!Number.isFinite(v))
20
+ return min;
21
+ return Math.min(max, Math.max(min, v));
22
+ }
23
+ /** Wrap a degree value into (-180, 180]. */
24
+ export function wrapDeg(deg) {
25
+ if (!Number.isFinite(deg))
26
+ return 0;
27
+ let d = deg % 360;
28
+ if (d > 180)
29
+ d -= 360;
30
+ if (d <= -180)
31
+ d += 360;
32
+ return d;
33
+ }
34
+ /** Clamp every field to its valid range (pitch/fov/horizon clamped, yaw wrapped). */
35
+ export function clampCamera(c) {
36
+ return {
37
+ pitch: clamp(c.pitch, CAMERA_LIMITS.pitch.min, CAMERA_LIMITS.pitch.max),
38
+ yaw: wrapDeg(c.yaw),
39
+ fov: clamp(c.fov, CAMERA_LIMITS.fov.min, CAMERA_LIMITS.fov.max),
40
+ horizon: clamp(c.horizon, CAMERA_LIMITS.horizon.min, CAMERA_LIMITS.horizon.max),
41
+ };
42
+ }
43
+ /** Apply a partial change and re-clamp. Non-finite inputs fall back to limits. */
44
+ export function updateCamera(c, patch) {
45
+ return clampCamera({ ...c, ...patch });
46
+ }
47
+ /** Nudge fields by deltas (e.g. from keyboard actions) and re-clamp. */
48
+ export function adjustCamera(c, delta) {
49
+ return clampCamera({
50
+ pitch: c.pitch + (delta.pitch ?? 0),
51
+ yaw: c.yaw + (delta.yaw ?? 0),
52
+ fov: c.fov + (delta.fov ?? 0),
53
+ horizon: c.horizon + (delta.horizon ?? 0),
54
+ });
55
+ }
56
+ //# sourceMappingURL=mode7-camera.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode7-camera.js","sourceRoot":"","sources":["../src/mode7-camera.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,oFAAoF;AACpF,gFAAgF;AAChF,kFAAkF;AAClF,mFAAmF;AACnF,oFAAoF;AACpF,gFAAgF;AAChF,+DAA+D;AAC/D,sEAAsE;AAStE;0EAC0E;AAC1E,MAAM,CAAC,MAAM,aAAa,GAAG;IAC5B,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;IAC1B,GAAG,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE;IAC5B,OAAO,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE;CACtB,CAAC;AAEX,MAAM,CAAC,MAAM,cAAc,GAAW,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAErF,SAAS,KAAK,CAAC,CAAS,EAAE,GAAW,EAAE,GAAW;IACjD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,GAAG,CAAC;IACpC,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;AACxC,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,OAAO,CAAC,GAAW;IAClC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IACpC,IAAI,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC;IAClB,IAAI,CAAC,GAAG,GAAG;QAAE,CAAC,IAAI,GAAG,CAAC;IACtB,IAAI,CAAC,IAAI,CAAC,GAAG;QAAE,CAAC,IAAI,GAAG,CAAC;IACxB,OAAO,CAAC,CAAC;AACV,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,WAAW,CAAC,CAAS;IACpC,OAAO;QACN,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC;QACvE,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;QACnB,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC;QAC/D,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC;KAC/E,CAAC;AACH,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,YAAY,CAAC,CAAS,EAAE,KAAsB;IAC7D,OAAO,WAAW,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;AACxC,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,YAAY,CAAC,CAAS,EAAE,KAAsB;IAC7D,OAAO,WAAW,CAAC;QAClB,KAAK,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;QACnC,GAAG,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;QAC7B,GAAG,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;QAC7B,OAAO,EAAE,CAAC,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;KACzC,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,45 @@
1
+ import type { PluginContext } from "@edv4h/usketch-shared";
2
+ import type { Mode7Store } from "./mode7-store.js";
3
+ export declare function SkyLayer({ store }: {
4
+ store: Mode7Store;
5
+ }): import("react").JSX.Element | null;
6
+ /**
7
+ * The "capture frame": a fixed, world-anchored rectangle marking the region the 3D
8
+ * ground plane covered at the last switch-on. Shown only in FLAT mode (so it never
9
+ * fights the tilt) when the HUD toggle is on — it lets the user see which shapes fall
10
+ * inside the captured region vs. outside (outside = won't appear in the initial 3D
11
+ * view). It is a NON-tilted, non-fixed layer, so the viewport transform anchors it to
12
+ * world space; `zoom` (from the layer render ctx) keeps the border ~constant on screen.
13
+ */
14
+ export declare function CaptureFrameLayer({ store, zoom }: {
15
+ store: Mode7Store;
16
+ zoom: number;
17
+ }): import("react").JSX.Element | null;
18
+ export declare function FogLayer({ store }: {
19
+ store: Mode7Store;
20
+ }): import("react").JSX.Element | null;
21
+ /**
22
+ * A live checklist (contributed as a HUD panel) of the board's layers, letting the
23
+ * user pick which ones render in 3D — e.g. tick the Shape layer(s). Reads the layer
24
+ * list from `ctx.layers.getLayers()` at render (re-rendered on any store change) and
25
+ * excludes the control surfaces that must never tilt (HUD + the plugin's overlays).
26
+ */
27
+ export declare function LayerPicker({ ctx, store, excluded, }: {
28
+ ctx: PluginContext;
29
+ store: Mode7Store;
30
+ excluded: readonly string[];
31
+ }): import("react").JSX.Element;
32
+ export interface CaptureHandlers {
33
+ /** Drag delta in screen px (content-follows-cursor). */
34
+ onPan(dx: number, dy: number): void;
35
+ /** Wheel zoom: raw deltaY and the zoom center in container-local px. */
36
+ onZoom(deltaY: number, center: {
37
+ x: number;
38
+ y: number;
39
+ }): void;
40
+ }
41
+ export declare function CaptureLayer({ store, handlers, }: {
42
+ store: Mode7Store;
43
+ handlers: CaptureHandlers;
44
+ }): import("react").JSX.Element | null;
45
+ //# sourceMappingURL=mode7-layers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode7-layers.d.ts","sourceRoot":"","sources":["../src/mode7-layers.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAiBnD,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,UAAU,CAAA;CAAE,sCAaxD;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,sCAmBrF;AAED,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,UAAU,CAAA;CAAE,sCAaxD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAC3B,GAAG,EACH,KAAK,EACL,QAAQ,GACR,EAAE;IACF,GAAG,EAAE,aAAa,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B,+BAwCA;AAED,MAAM,WAAW,eAAe;IAC/B,wDAAwD;IACxD,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,wEAAwE;IACxE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC/D;AAED,wBAAgB,YAAY,CAAC,EAC5B,KAAK,EACL,QAAQ,GACR,EAAE;IACF,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,eAAe,CAAC;CAC1B,sCAoCA"}
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useSyncExternalStore } from "react";
3
+ import { fogBackground, fogOpacity, skyBackground } from "./mode7-transform.js";
4
+ function useMode7(store) {
5
+ return useSyncExternalStore(store.subscribe, store.getState, store.getState);
6
+ }
7
+ /** Center of a pointer/wheel event relative to the layer box (== canvas container,
8
+ * since the layer is inset:0 and untilted) — the space the store's viewport uses. */
9
+ function localCenter(e) {
10
+ const r = e.currentTarget.getBoundingClientRect();
11
+ return { x: e.clientX - r.left, y: e.clientY - r.top };
12
+ }
13
+ export function SkyLayer({ store }) {
14
+ const { active, camera, look } = useMode7(store);
15
+ if (!active)
16
+ return null;
17
+ return (_jsx("div", { style: {
18
+ position: "absolute",
19
+ inset: 0,
20
+ pointerEvents: "none",
21
+ background: skyBackground(look.sky, camera.horizon),
22
+ } }));
23
+ }
24
+ /**
25
+ * The "capture frame": a fixed, world-anchored rectangle marking the region the 3D
26
+ * ground plane covered at the last switch-on. Shown only in FLAT mode (so it never
27
+ * fights the tilt) when the HUD toggle is on — it lets the user see which shapes fall
28
+ * inside the captured region vs. outside (outside = won't appear in the initial 3D
29
+ * view). It is a NON-tilted, non-fixed layer, so the viewport transform anchors it to
30
+ * world space; `zoom` (from the layer render ctx) keeps the border ~constant on screen.
31
+ */
32
+ export function CaptureFrameLayer({ store, zoom }) {
33
+ const { active, showCaptureFrame, captureRect } = useMode7(store);
34
+ if (active || !showCaptureFrame || !captureRect)
35
+ return null;
36
+ const border = 2 / (zoom || 1); // world px → ~2 screen px under the viewport scale
37
+ return (_jsx("div", { style: {
38
+ position: "absolute",
39
+ left: captureRect.x,
40
+ top: captureRect.y,
41
+ width: captureRect.width,
42
+ height: captureRect.height,
43
+ boxSizing: "border-box",
44
+ border: `${border}px dashed rgba(80, 140, 255, 0.9)`,
45
+ background: "rgba(80, 140, 255, 0.06)",
46
+ pointerEvents: "none",
47
+ } }));
48
+ }
49
+ export function FogLayer({ store }) {
50
+ const { active, camera, look } = useMode7(store);
51
+ if (!active || fogOpacity(look.fog) <= 0)
52
+ return null;
53
+ return (_jsx("div", { style: {
54
+ position: "absolute",
55
+ inset: 0,
56
+ pointerEvents: "none",
57
+ background: fogBackground(look.fogColor, look.fog, camera.horizon),
58
+ } }));
59
+ }
60
+ /**
61
+ * A live checklist (contributed as a HUD panel) of the board's layers, letting the
62
+ * user pick which ones render in 3D — e.g. tick the Shape layer(s). Reads the layer
63
+ * list from `ctx.layers.getLayers()` at render (re-rendered on any store change) and
64
+ * excludes the control surfaces that must never tilt (HUD + the plugin's overlays).
65
+ */
66
+ export function LayerPicker({ ctx, store, excluded, }) {
67
+ const state = useMode7(store);
68
+ const selected = new Set(state.tiltLayers);
69
+ const excludedSet = new Set(excluded);
70
+ const layers = ctx.layers
71
+ .getLayers()
72
+ .filter((l) => !excludedSet.has(l.id))
73
+ .slice()
74
+ .sort((a, b) => a.order - b.order);
75
+ return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 4, opacity: state.active ? 1 : 0.55 }, children: [_jsx("div", { style: { fontSize: 11, color: "var(--fg-tertiary, #888)" }, children: "3D \u306B\u3059\u308B\u30EC\u30A4\u30E4\u30FC" }), layers.length === 0 ? (_jsx("div", { style: { fontSize: 11, color: "var(--fg-tertiary, #888)" }, children: "\u30EC\u30A4\u30E4\u30FC\u304C\u3042\u308A\u307E\u305B\u3093" })) : (layers.map((l) => (_jsxs("label", { style: {
76
+ display: "flex",
77
+ alignItems: "center",
78
+ gap: 6,
79
+ fontSize: 12,
80
+ cursor: "pointer",
81
+ }, children: [_jsx("input", { type: "checkbox", checked: selected.has(l.id), onChange: () => store.toggleTiltLayer(l.id) }), _jsx("span", { children: l.id })] }, l.id))))] }));
82
+ }
83
+ export function CaptureLayer({ store, handlers, }) {
84
+ const { active } = useMode7(store);
85
+ const drag = useRef(null);
86
+ if (!active)
87
+ return null;
88
+ const end = (e) => {
89
+ e.stopPropagation();
90
+ drag.current = null;
91
+ };
92
+ return (_jsx("div", {
93
+ // Full-viewport capture surface. `stopPropagation` keeps the container's
94
+ // tool handlers (draw/select) from firing — editing is suspended in this view.
95
+ style: { position: "absolute", inset: 0, cursor: "grab", touchAction: "none" }, onPointerDown: (e) => {
96
+ e.stopPropagation();
97
+ e.currentTarget.setPointerCapture?.(e.pointerId);
98
+ drag.current = { x: e.clientX, y: e.clientY };
99
+ }, onPointerMove: (e) => {
100
+ if (!drag.current)
101
+ return;
102
+ e.stopPropagation();
103
+ const dx = e.clientX - drag.current.x;
104
+ const dy = e.clientY - drag.current.y;
105
+ drag.current = { x: e.clientX, y: e.clientY };
106
+ handlers.onPan(dx, dy);
107
+ }, onPointerUp: end, onPointerCancel: end, onWheel: (e) => {
108
+ e.stopPropagation();
109
+ handlers.onZoom(e.deltaY, localCenter(e));
110
+ } }));
111
+ }
112
+ //# sourceMappingURL=mode7-layers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode7-layers.js","sourceRoot":"","sources":["../src/mode7-layers.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { PluginContext } from "@edv4h/usketch-shared";
2
+ import type { Mode7Store } from "./mode7-store.js";
3
+ /** The plugin's own layer ids (never tilted). */
4
+ export declare const SKY_LAYER_ID = "mode7-sky";
5
+ export declare const FOG_LAYER_ID = "mode7-fog";
6
+ export declare const CAPTURE_LAYER_ID = "mode7-capture";
7
+ export declare const CAPTURE_FRAME_LAYER_ID = "mode7-capture-frame";
8
+ /**
9
+ * Wire the runtime to a store. The set of tilted layers is read live from the
10
+ * store (user-selectable via the HUD). Returns a teardown that clears every
11
+ * injected transform, unregisters the overlay layers, and drops all listeners.
12
+ */
13
+ export declare function setupMode7Runtime(ctx: PluginContext, store: Mode7Store): () => void;
14
+ //# sourceMappingURL=mode7-runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode7-runtime.d.ts","sourceRoot":"","sources":["../src/mode7-runtime.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAe,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,iDAAiD;AACjD,eAAO,MAAM,YAAY,cAAc,CAAC;AACxC,eAAO,MAAM,YAAY,cAAc,CAAC;AACxC,eAAO,MAAM,gBAAgB,kBAAkB,CAAC;AAChD,eAAO,MAAM,sBAAsB,wBAAwB,CAAC;AAoC5D;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,IAAI,CA8KnF"}
@@ -0,0 +1,220 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { CaptureFrameLayer, CaptureLayer, FogLayer, SkyLayer } from "./mode7-layers.js";
3
+ import { drawDistanceClip, drawDistanceOverscanPx, tiltTransform } from "./mode7-transform.js";
4
+ /** The plugin's own layer ids (never tilted). */
5
+ export const SKY_LAYER_ID = "mode7-sky";
6
+ export const FOG_LAYER_ID = "mode7-fog";
7
+ export const CAPTURE_LAYER_ID = "mode7-capture";
8
+ export const CAPTURE_FRAME_LAYER_ID = "mode7-capture-frame";
9
+ /** Wheel-zoom sensitivity (matches viewport-nav's deltaY-proportional feel). */
10
+ const WHEEL_ZOOM = 0.0015;
11
+ /** Extra px added to the grid overscan so it comfortably covers the clip line (rounding). */
12
+ const OVERSCAN_BUFFER = 16;
13
+ /**
14
+ * The main canvas container: the element that directly wraps the board's layer
15
+ * divs. We locate it via `[data-layer-id]` (every layer wrapper is a direct child
16
+ * of the container) rather than `data-testid="canvas-container"` — `data-testid` is
17
+ * a test-only hook that the production/app build strips out, so it is absent in the
18
+ * running app and the old selector silently matched nothing (the tilt never applied).
19
+ * If several containers exist (e.g. a minimap that also renders layers), the largest
20
+ * by area wins.
21
+ */
22
+ function mainContainer() {
23
+ if (typeof document === "undefined")
24
+ return null;
25
+ let best = null;
26
+ let bestArea = 0;
27
+ const seen = new Set();
28
+ for (const el of document.querySelectorAll("[data-layer-id]")) {
29
+ const parent = el.parentElement;
30
+ if (!parent || seen.has(parent))
31
+ continue;
32
+ seen.add(parent);
33
+ const r = parent.getBoundingClientRect();
34
+ const area = r.width * r.height;
35
+ if (area > bestArea) {
36
+ bestArea = area;
37
+ best = parent;
38
+ }
39
+ }
40
+ return best;
41
+ }
42
+ /**
43
+ * Wire the runtime to a store. The set of tilted layers is read live from the
44
+ * store (user-selectable via the HUD). Returns a teardown that clears every
45
+ * injected transform, unregisters the overlay layers, and drops all listeners.
46
+ */
47
+ export function setupMode7Runtime(ctx, store) {
48
+ const STAGE_ATTR = "data-mode7-stage";
49
+ let styleEl = null;
50
+ const ensureStyleEl = () => {
51
+ if (styleEl || typeof document === "undefined")
52
+ return;
53
+ styleEl = document.createElement("style");
54
+ styleEl.setAttribute("data-mode7", "");
55
+ document.head.appendChild(styleEl);
56
+ };
57
+ /** Keep an attribute-selector value valid (layer ids are simple, but be safe). */
58
+ const attrValue = (id) => id.replace(/["\\]/g, "");
59
+ // Drive the background grid's overscan (only re-emit on change) so it extends past the
60
+ // viewport to stay under the far content a large draw distance reveals. No package
61
+ // dependency on bg-grid — a plain event it listens for (a no-op if bg-grid is absent).
62
+ let lastOverscan = -1;
63
+ const driveGridOverscan = (px) => {
64
+ if (px === lastOverscan)
65
+ return;
66
+ lastOverscan = px;
67
+ ctx.events.emit("bg-grid:set-overscan", { px });
68
+ };
69
+ const applyTilt = () => {
70
+ const container = mainContainer();
71
+ if (!container)
72
+ return;
73
+ container.setAttribute(STAGE_ATTR, "on");
74
+ ensureStyleEl();
75
+ if (!styleEl)
76
+ return;
77
+ const { camera, drawDistance } = store.getState();
78
+ const { transform, transformOrigin } = tiltTransform(camera);
79
+ // Clip each tilted wrapper (so overflow:visible doesn't float content past the
80
+ // grid); a finite draw distance tightens the top, one beyond the viewport lets it
81
+ // extend (negative inset) with the grid overscanned to match. Distance is canvas
82
+ // units → maps to px via zoom + container height, so this re-runs on viewport (zoom)
83
+ // changes. clip-path keeps the tilt (overflow flattens).
84
+ const zoom = ctx.store.getViewport().zoom;
85
+ const heightPx = container.getBoundingClientRect().height;
86
+ const clip = drawDistanceClip(drawDistance, zoom, heightPx);
87
+ const overscan = drawDistanceOverscanPx(drawDistance, zoom, heightPx);
88
+ driveGridOverscan(overscan > 0 ? overscan + OVERSCAN_BUFFER : 0);
89
+ const clipRule = `clip-path:${clip} !important;`;
90
+ // `overflow:visible` + `transform-style:preserve-3d` are essential for layers
91
+ // like `dom-shapes`: their wrapper has `overflow:hidden` (which the CSS spec
92
+ // forces `transform-style` to `flat`) and an inner viewport-transform div that
93
+ // Chrome composites separately — so without these the SHAPES render flat in
94
+ // screen space while only the wrapper tilts. Overriding overflow keeps the
95
+ // whole subtree in the 3D context; the canvas container still clips.
96
+ // A DESCENDANT selector (space, not `>`) matches the wrapper regardless of nesting.
97
+ const layerRules = store
98
+ .getState()
99
+ .tiltLayers.map((id) => `[${STAGE_ATTR}="on"] [data-layer-id="${attrValue(id)}"]{transform:${transform} !important;transform-origin:${transformOrigin} !important;overflow:visible !important;transform-style:preserve-3d !important;${clipRule}}`)
100
+ .join("");
101
+ styleEl.textContent = layerRules;
102
+ };
103
+ const clearAll = () => {
104
+ if (styleEl)
105
+ styleEl.textContent = "";
106
+ if (typeof document !== "undefined") {
107
+ for (const el of document.querySelectorAll(`[${STAGE_ATTR}]`))
108
+ el.removeAttribute(STAGE_ATTR);
109
+ }
110
+ driveGridOverscan(0); // restore the grid to the viewport when the view is off
111
+ };
112
+ /**
113
+ * The current viewport as a WORLD rectangle (== `LayerRenderContext.viewportBounds`,
114
+ * recomputed here because the runtime has no render ctx): screen box [0,0,W,H]
115
+ * inverted through the affine viewport (`world = (screen - vp) / zoom`).
116
+ */
117
+ const viewportWorldRect = () => {
118
+ const c = mainContainer();
119
+ if (!c)
120
+ return null;
121
+ const r = c.getBoundingClientRect();
122
+ const vp = ctx.store.getViewport();
123
+ if (!vp.zoom || r.width <= 0 || r.height <= 0)
124
+ return null;
125
+ return {
126
+ x: -vp.x / vp.zoom,
127
+ y: -vp.y / vp.zoom,
128
+ width: r.width / vp.zoom,
129
+ height: r.height / vp.zoom,
130
+ };
131
+ };
132
+ // Snapshot the capture rect when the view is switched ON (the frame then shows
133
+ // "what the 3D ground covered" back in flat mode), and seed it the first time the
134
+ // overlay is switched on with nothing captured yet.
135
+ let prevActive = false;
136
+ const maybeCapture = () => {
137
+ const st = store.getState();
138
+ const rising = st.active && !prevActive;
139
+ // Update the edge tracker BEFORE setCaptureRect — it notifies synchronously and
140
+ // re-enters sync()/maybeCapture(), so a stale `prevActive` would loop forever.
141
+ prevActive = st.active;
142
+ if (rising || (st.showCaptureFrame && !st.captureRect)) {
143
+ const rect = viewportWorldRect();
144
+ if (rect)
145
+ store.setCaptureRect(rect);
146
+ }
147
+ };
148
+ const sync = () => {
149
+ if (store.getState().active)
150
+ applyTilt();
151
+ else
152
+ clearAll();
153
+ maybeCapture();
154
+ };
155
+ // Camera drive from the capture layer (reuse the affine viewport).
156
+ const handlers = {
157
+ onPan: (dx, dy) => ctx.store.panBy(dx, dy),
158
+ onZoom: (deltaY, center) => {
159
+ const factor = Math.exp(-deltaY * WHEEL_ZOOM);
160
+ ctx.store.zoomTo(ctx.store.getViewport().zoom * factor, center);
161
+ },
162
+ };
163
+ // Register the three overlay layers once; they self-hide when inactive.
164
+ ctx.layers.register({
165
+ id: SKY_LAYER_ID,
166
+ order: 1,
167
+ fixed: true,
168
+ render: () => _jsx(SkyLayer, { store: store }),
169
+ });
170
+ ctx.layers.register({
171
+ id: FOG_LAYER_ID,
172
+ order: 9000,
173
+ fixed: true,
174
+ render: () => _jsx(FogLayer, { store: store }),
175
+ });
176
+ ctx.layers.register({
177
+ id: CAPTURE_LAYER_ID,
178
+ order: 9500,
179
+ fixed: true,
180
+ render: () => _jsx(CaptureLayer, { store: store, handlers: handlers }),
181
+ });
182
+ // The capture-frame overlay is NON-fixed (world-anchored via the viewport
183
+ // transform) so it pans/zooms with the board; it self-hides unless flat + toggled.
184
+ ctx.layers.register({
185
+ id: CAPTURE_FRAME_LAYER_ID,
186
+ order: 8000,
187
+ render: (rc) => _jsx(CaptureFrameLayer, { store: store, zoom: rc.viewport.zoom }),
188
+ });
189
+ // Re-sync on active / camera / tilt-layer changes.
190
+ const unsubscribe = store.subscribe(sync);
191
+ // The draw-distance clip is in canvas units → depends on the board zoom + container
192
+ // height, so re-apply the tilt on viewport changes and window resize while active.
193
+ const reapplyIfActive = () => {
194
+ if (store.getState().active)
195
+ applyTilt();
196
+ };
197
+ const offViewport = ctx.store.onMutation((e) => {
198
+ if (e.type === "viewport:changed")
199
+ reapplyIfActive();
200
+ });
201
+ const onResize = typeof window !== "undefined" ? reapplyIfActive : null;
202
+ if (onResize)
203
+ window.addEventListener("resize", onResize);
204
+ // Initial apply (honors enabledInitially).
205
+ sync();
206
+ return () => {
207
+ clearAll();
208
+ styleEl?.remove();
209
+ styleEl = null;
210
+ unsubscribe();
211
+ offViewport();
212
+ if (onResize)
213
+ window.removeEventListener("resize", onResize);
214
+ ctx.layers.unregister(SKY_LAYER_ID);
215
+ ctx.layers.unregister(FOG_LAYER_ID);
216
+ ctx.layers.unregister(CAPTURE_LAYER_ID);
217
+ ctx.layers.unregister(CAPTURE_FRAME_LAYER_ID);
218
+ };
219
+ }
220
+ //# sourceMappingURL=mode7-runtime.js.map