ohzi-core 13.3.0 → 15.0.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 (226) hide show
  1. package/README.md +2 -1
  2. package/build/index.mjs +1 -1
  3. package/build/index.mjs.map +1 -1
  4. package/package.json +12 -6
  5. package/src/Capabilities.ts +0 -2
  6. package/src/Debug.ts +38 -33
  7. package/src/Graphics.ts +2 -2
  8. package/src/Initializer.ts +2 -1
  9. package/src/SceneManager.ts +4 -5
  10. package/src/action_sequencer/NumberInterpolator.ts +2 -0
  11. package/src/blitter_index.ts +1 -2
  12. package/src/camera_controller/CameraController.ts +106 -63
  13. package/src/camera_controller/states/common/CommonCameraState.ts +2 -1
  14. package/src/components/AxisHelper.ts +1 -0
  15. package/src/components/Line.ts +35 -27
  16. package/src/components/PlaneHelper.ts +1 -1
  17. package/src/components/Text2D.ts +3 -1
  18. package/src/components/WorldImage.ts +27 -20
  19. package/src/components/sdf_text/SDFTextBatch.ts +16 -26
  20. package/src/dev_bridge/CameraBridge.ts +363 -0
  21. package/src/dev_bridge/CaptureService.ts +256 -0
  22. package/src/dev_bridge/CommandDispatcher.ts +114 -0
  23. package/src/dev_bridge/ConsoleBuffer.ts +286 -0
  24. package/src/dev_bridge/DebugDrawer.ts +399 -0
  25. package/src/dev_bridge/DevBridge.ts +195 -0
  26. package/src/dev_bridge/InputSynthesizer.ts +287 -0
  27. package/src/dev_bridge/PerformanceProbe.ts +118 -0
  28. package/src/dev_bridge/RenderModeRegistry.ts +66 -0
  29. package/src/dev_bridge/SceneEditor.ts +288 -0
  30. package/src/dev_bridge/SceneInspector.ts +388 -0
  31. package/src/dev_bridge/ViewNavigator.ts +101 -0
  32. package/src/dev_bridge/protocol.ts +31 -0
  33. package/src/index.ts +21 -14
  34. package/src/materials/AddMaterial.ts +14 -7
  35. package/src/materials/BlitNodeMaterial.ts +85 -0
  36. package/src/materials/BloomComposeMaterial.ts +21 -9
  37. package/src/materials/BoxBlurMaterial.ts +26 -6
  38. package/src/materials/ClearDepthNormalMaterial.ts +12 -6
  39. package/src/materials/DepthNormalMaterial.ts +25 -14
  40. package/src/materials/DualFilteringBlurMaterial.ts +32 -5
  41. package/src/materials/GaussianBlurMaterial.ts +36 -14
  42. package/src/materials/LineMaterial.ts +52 -0
  43. package/src/materials/LuminosityHighPassMaterial.ts +23 -10
  44. package/src/materials/NormalMaterial.ts +10 -11
  45. package/src/materials/SDFTextMaterial.ts +63 -16
  46. package/src/materials/SSAOComposeMaterial.ts +14 -5
  47. package/src/materials/SSAOMaterial.ts +123 -24
  48. package/src/materials/ScreenSpaceTextureMaterial.ts +19 -31
  49. package/src/materials/UnrealBloomComposeMaterial.ts +41 -32
  50. package/src/materials/WorldImageMaterial.ts +51 -0
  51. package/src/materials/deferred/DeferredPointLightMaterial.ts +45 -14
  52. package/src/materials/deferred/depth_normal_encoding.ts +36 -0
  53. package/src/primitives/Cube.ts +1 -1
  54. package/src/primitives/Sphere.ts +1 -1
  55. package/src/raycast/GroupRaycaster.ts +2 -2
  56. package/src/render_mode/BloomRender.ts +18 -4
  57. package/src/render_mode/DebugNormalsRender.ts +6 -1
  58. package/src/render_mode/DeferredRender.ts +5 -34
  59. package/src/render_mode/NormalAORender.ts +9 -19
  60. package/src/render_mode/UnrealBloomRender.ts +2 -1
  61. package/src/render_mode/VRRender.ts +6 -3
  62. package/src/render_utilities/Blitter.ts +4 -4
  63. package/src/render_utilities/Blurrer.ts +7 -0
  64. package/src/render_utilities/DualFilteringBlurrer.ts +26 -10
  65. package/src/resource_loader/AudioLoader.ts +5 -3
  66. package/src/resource_loader/HDRTextureLoader.ts +1 -1
  67. package/src/scenes/AbstractScene.ts +7 -3
  68. package/src/scenes/Compilator.ts +5 -1
  69. package/src/scenes/loading_states/LoadingState.ts +15 -7
  70. package/src/scenes/loading_states/RegularLoadingState.ts +2 -1
  71. package/src/utilities/CameraUtilities.ts +57 -79
  72. package/src/utilities/HTMLUtilities.ts +2 -1
  73. package/src/utilities/MeshSampler.ts +8 -1
  74. package/src/utilities/OrthographicFrustumPointFitter.ts +53 -4
  75. package/src/view_components/ApplicationView.ts +1 -1
  76. package/src/view_components/ViewComponent.ts +1 -1
  77. package/src/view_components/ViewManager.ts +7 -1
  78. package/src/view_components/transition/DrawIOAnimationSheet.ts +1 -1
  79. package/types/Capabilities.d.ts +0 -1
  80. package/types/SceneManager.d.ts +1 -1
  81. package/types/blitter_index.d.ts +1 -2
  82. package/types/camera_controller/CameraController.d.ts +9 -0
  83. package/types/components/Line.d.ts +3 -2
  84. package/types/components/WorldImage.d.ts +3 -2
  85. package/types/index.d.ts +2 -7
  86. package/types/materials/AddMaterial.d.ts +2 -2
  87. package/types/materials/BlitNodeMaterial.d.ts +18 -0
  88. package/types/materials/BloomComposeMaterial.d.ts +2 -2
  89. package/types/materials/BoxBlurMaterial.d.ts +2 -2
  90. package/types/materials/ClearDepthNormalMaterial.d.ts +2 -2
  91. package/types/materials/DepthNormalMaterial.d.ts +2 -2
  92. package/types/materials/DualFilteringBlurMaterial.d.ts +2 -2
  93. package/types/materials/GaussianBlurMaterial.d.ts +2 -2
  94. package/types/materials/LuminosityHighPassMaterial.d.ts +2 -2
  95. package/types/materials/NormalMaterial.d.ts +2 -2
  96. package/types/materials/SDFTextMaterial.d.ts +2 -2
  97. package/types/materials/SSAOComposeMaterial.d.ts +2 -2
  98. package/types/materials/SSAOMaterial.d.ts +2 -2
  99. package/types/materials/ScreenSpaceTextureMaterial.d.ts +2 -2
  100. package/types/materials/UnrealBloomComposeMaterial.d.ts +2 -2
  101. package/types/materials/deferred/DeferredPointLightMaterial.d.ts +2 -2
  102. package/types/render_mode/DeferredRender.d.ts +0 -2
  103. package/types/render_mode/NormalAORender.d.ts +0 -2
  104. package/types/render_utilities/Blitter.d.ts +2 -2
  105. package/types/render_utilities/DualFilteringBlurrer.d.ts +0 -2
  106. package/types/utilities/CameraUtilities.d.ts +0 -1
  107. package/types/utilities/OrthographicFrustumPointFitter.d.ts +14 -8
  108. package/build/index.module.js +0 -2
  109. package/build/index.module.js.map +0 -1
  110. package/src/components/EdgeMesh.ts +0 -150
  111. package/src/components/GeometryEdgeVisualizer.ts +0 -49
  112. package/src/components/MultiLinePath.ts +0 -25
  113. package/src/components/MultiLineText2D.ts +0 -65
  114. package/src/components/Shape2D.ts +0 -42
  115. package/src/components/Shape3D.ts +0 -38
  116. package/src/gpu_particles/GPUParticleSystem.ts +0 -122
  117. package/src/gpu_particles/ParticleAttribute.ts +0 -110
  118. package/src/gpu_particles/ParticlePositionAttribute.ts +0 -46
  119. package/src/materials/AlphaFilterMaterial.ts +0 -21
  120. package/src/materials/BaseShaderMaterial.ts +0 -20
  121. package/src/materials/BlitMaterial.ts +0 -27
  122. package/src/materials/DeferredRendererComposeMaterial.ts +0 -36
  123. package/src/materials/DisplayNormalTextureMaterial.ts +0 -18
  124. package/src/materials/FXAAMaterial.ts +0 -12
  125. package/src/materials/MedianFilterMaterial.ts +0 -46
  126. package/src/materials/SDFScreenTextMaterial.ts +0 -32
  127. package/src/materials/UnrealBlurMaterial.ts +0 -20
  128. package/src/materials/UnrealComposeMaterial.ts +0 -27
  129. package/src/materials/ViewPositionMaterial.ts +0 -19
  130. package/src/materials/gpu_particles/AttributeUpdateMaterial.ts +0 -35
  131. package/src/materials/gpu_particles/BasicParticleMaterial.ts +0 -25
  132. package/src/materials/gpu_particles/ParticleStorageMaterial.ts +0 -36
  133. package/src/materials/gpu_particles/PositionStorageMaterial.ts +0 -27
  134. package/src/render_utilities/MedianFilter.ts +0 -72
  135. package/src/render_utilities/ViewPositionRenderer.ts +0 -53
  136. package/src/shaders/add/add.frag +0 -28
  137. package/src/shaders/anti_aliasing/fxaa.frag +0 -72
  138. package/src/shaders/basic_color/basic_color.frag +0 -6
  139. package/src/shaders/basic_color/basic_color.vert +0 -10
  140. package/src/shaders/basic_line/basic_line.frag +0 -37
  141. package/src/shaders/basic_line/basic_line.vert +0 -37
  142. package/src/shaders/basic_texture/basic_texture.frag +0 -10
  143. package/src/shaders/basic_texture/basic_texture.vert +0 -17
  144. package/src/shaders/bloom/bloom_compose.frag +0 -22
  145. package/src/shaders/bloom/cheap_bloom_compose.frag +0 -13
  146. package/src/shaders/box_blur/box_blur.frag +0 -24
  147. package/src/shaders/box_blur/compose.frag +0 -31
  148. package/src/shaders/clear/clear_depth_normal.frag +0 -25
  149. package/src/shaders/copy/copy.frag +0 -7
  150. package/src/shaders/copy/copy.vert +0 -6
  151. package/src/shaders/deferred/blit_copy.vert +0 -11
  152. package/src/shaders/deferred/deferred_compose.frag +0 -59
  153. package/src/shaders/deferred/point_light.frag +0 -66
  154. package/src/shaders/deferred/point_light.vert +0 -13
  155. package/src/shaders/depth_normals/debug_normals.frag +0 -48
  156. package/src/shaders/depth_normals/depth_normals.frag +0 -30
  157. package/src/shaders/depth_normals/depth_normals.vert +0 -13
  158. package/src/shaders/dual_filter_blur/alpha_filter.frag +0 -14
  159. package/src/shaders/dual_filter_blur/downsample.frag +0 -21
  160. package/src/shaders/dual_filter_blur/upsample.frag +0 -24
  161. package/src/shaders/edge_visualizer/edge_visualizer.frag +0 -18
  162. package/src/shaders/edge_visualizer/edge_visualizer.vert +0 -16
  163. package/src/shaders/edges/corners.frag +0 -11
  164. package/src/shaders/edges/corners.vert +0 -31
  165. package/src/shaders/edges/edges.frag +0 -7
  166. package/src/shaders/edges/edges.vert +0 -27
  167. package/src/shaders/gaussian_blur/gaussian_blur.frag +0 -44
  168. package/src/shaders/gaussian_blur/unreal_bloom_compose.frag +0 -36
  169. package/src/shaders/gpu_particles/common_utils.glsl +0 -13
  170. package/src/shaders/gpu_particles/generic_storage.frag +0 -6
  171. package/src/shaders/gpu_particles/store/store_position.vert +0 -14
  172. package/src/shaders/gpu_particles/update/basic_update.frag +0 -21
  173. package/src/shaders/gpu_particles/visualize/visualize.frag +0 -11
  174. package/src/shaders/gpu_particles/visualize/visualize.vert +0 -12
  175. package/src/shaders/grid/grid.frag +0 -17
  176. package/src/shaders/grid/grid.vert +0 -10
  177. package/src/shaders/luminosity_high_pass/luminosity_high_pass.frag +0 -38
  178. package/src/shaders/median_filter/median_filter.frag +0 -134
  179. package/src/shaders/normal/normal.frag +0 -6
  180. package/src/shaders/normal/normal.vert +0 -6
  181. package/src/shaders/sdf_text/sdf_screen_text.frag +0 -51
  182. package/src/shaders/sdf_text/sdf_screen_text.vert +0 -42
  183. package/src/shaders/sdf_text/sdf_text.frag +0 -53
  184. package/src/shaders/sdf_text/sdf_text.vert +0 -42
  185. package/src/shaders/ssao/hbao.frag +0 -264
  186. package/src/shaders/ssao/ssao.frag +0 -95
  187. package/src/shaders/ssao/ssao.vert +0 -11
  188. package/src/shaders/ssao/ssao_compose.frag +0 -14
  189. package/src/shaders/transparent_mix/copy.frag +0 -7
  190. package/src/shaders/transparent_mix/transparent_mix.frag +0 -19
  191. package/src/shaders/transparent_mix/transparent_mix.vert +0 -8
  192. package/src/shaders/ui/ss_texture.frag +0 -11
  193. package/src/shaders/ui/ss_texture.vert +0 -32
  194. package/src/shaders/ui/ws_texture.vert +0 -39
  195. package/src/shaders/unreal_blur/unreal_blur.frag +0 -25
  196. package/src/shaders/unreal_blur/unreal_compose.frag +0 -24
  197. package/src/shaders/write_view_position/write_view_position.frag +0 -7
  198. package/src/shaders/write_view_position/write_view_position.vert +0 -9
  199. package/src/types.d.ts +0 -14
  200. package/types/components/EdgeMesh.d.ts +0 -16
  201. package/types/components/GeometryEdgeVisualizer.d.ts +0 -8
  202. package/types/components/MultiLinePath.d.ts +0 -8
  203. package/types/components/MultiLineText2D.d.ts +0 -12
  204. package/types/components/Shape2D.d.ts +0 -7
  205. package/types/components/Shape3D.d.ts +0 -6
  206. package/types/gpu_particles/GPUParticleSystem.d.ts +0 -19
  207. package/types/gpu_particles/ParticleAttribute.d.ts +0 -20
  208. package/types/gpu_particles/ParticlePositionAttribute.d.ts +0 -6
  209. package/types/materials/AlphaFilterMaterial.d.ts +0 -7
  210. package/types/materials/BaseShaderMaterial.d.ts +0 -4
  211. package/types/materials/BlitMaterial.d.ts +0 -4
  212. package/types/materials/DeferredRendererComposeMaterial.d.ts +0 -10
  213. package/types/materials/DisplayNormalTextureMaterial.d.ts +0 -4
  214. package/types/materials/FXAAMaterial.d.ts +0 -5
  215. package/types/materials/MedianFilterMaterial.d.ts +0 -8
  216. package/types/materials/SDFScreenTextMaterial.d.ts +0 -8
  217. package/types/materials/UIElementMaterial.d.ts +0 -4
  218. package/types/materials/UnrealBlurMaterial.d.ts +0 -5
  219. package/types/materials/UnrealComposeMaterial.d.ts +0 -5
  220. package/types/materials/ViewPositionMaterial.d.ts +0 -5
  221. package/types/materials/gpu_particles/AttributeUpdateMaterial.d.ts +0 -8
  222. package/types/materials/gpu_particles/BasicParticleMaterial.d.ts +0 -5
  223. package/types/materials/gpu_particles/ParticleStorageMaterial.d.ts +0 -5
  224. package/types/materials/gpu_particles/PositionStorageMaterial.d.ts +0 -5
  225. package/types/render_utilities/MedianFilter.d.ts +0 -12
  226. package/types/render_utilities/ViewPositionRenderer.d.ts +0 -12
@@ -0,0 +1,399 @@
1
+ import { SDFTextBatch } from '../components/sdf_text/SDFTextBatch';
2
+ import { WorldImage } from '../components/WorldImage';
3
+
4
+ import type { Material, Mesh, Object3D, Texture } from 'three';
5
+ import { Box3, Color as ThreeColor, Sphere, TextureLoader, Vector3 } from 'three';
6
+
7
+ type Color = number | string;
8
+
9
+ interface DebugLike
10
+ {
11
+ draw_cube(pos?: Vector3, size?: number, color?: Color): Object3D;
12
+ draw_sphere(pos: Vector3, size: number, color?: Color): Object3D;
13
+ draw_plane(width?: number, height?: number, color?: Color): Object3D;
14
+ draw_math_sphere(sphere: Sphere, color?: Color): Object3D;
15
+ draw_bounding_box(bb: Box3, color?: Color): Object3D;
16
+ draw_label(text: string, pos?: Vector3, size?: number, color?: Color): Object3D;
17
+ scene: Object3D;
18
+ }
19
+
20
+ interface DebugDrawRequest
21
+ {
22
+ shape?: unknown;
23
+ position?: unknown;
24
+ size?: unknown;
25
+ color?: unknown;
26
+ object?: unknown;
27
+ text?: unknown;
28
+ font?: unknown;
29
+ }
30
+
31
+ // An msdf-atlas-gen layout (the JSON it writes with -json) and its atlas image.
32
+ interface SDFFont
33
+ {
34
+ layout: unknown;
35
+ atlas: Texture;
36
+ }
37
+
38
+ type SDFFontLoader = (layout_url: string, atlas_url: string) => Promise<SDFFont>;
39
+
40
+ interface DebugDrawerOptions
41
+ {
42
+ // URL of the font sdf_text uses when the request names none.
43
+ sdf_font?: string;
44
+ // Loads a font. Defaults to fetching the layout and loading the atlas image.
45
+ load_sdf_font?: SDFFontLoader;
46
+ }
47
+
48
+ interface SDFGlyph
49
+ {
50
+ unicode: number;
51
+ planeBounds?: unknown;
52
+ }
53
+
54
+ interface SDFLayout
55
+ {
56
+ atlas: { width: number, height: number, yOrigin?: string };
57
+ glyphs: SDFGlyph[];
58
+ }
59
+
60
+ interface DebugDrawResult
61
+ {
62
+ id: string;
63
+ shape: string;
64
+ helpers: number;
65
+ }
66
+
67
+ interface DebugClearRequest
68
+ {
69
+ id?: unknown;
70
+ }
71
+
72
+ interface DebugClearResult
73
+ {
74
+ removed: number;
75
+ helpers: number;
76
+ }
77
+
78
+ const SHAPES = ['cube', 'sphere', 'plane', 'math_sphere', 'bounding_box', 'label', 'sdf_text'];
79
+
80
+ // At 48px a glyph is about 27 pixels wide, so 200 keeps the label canvas below the
81
+ // 8192 pixel texture limit WebGPU guarantees.
82
+ const MAX_LABEL_LENGTH = 200;
83
+
84
+ async function load_sdf_font(layout_url: string, atlas_url: string): Promise<SDFFont>
85
+ {
86
+ const response = await fetch(layout_url);
87
+
88
+ if (!response.ok)
89
+ {
90
+ throw new Error(`HTTP ${response.status}`);
91
+ }
92
+
93
+ // TextureLoader rejects with the image's error event, which says nothing useful.
94
+ const atlas_request = new TextureLoader().loadAsync(atlas_url).catch(() =>
95
+ {
96
+ throw new Error(`no atlas image at ${atlas_url}`);
97
+ });
98
+ const [layout, atlas] = await Promise.all([response.json() as Promise<unknown>, atlas_request]);
99
+
100
+ return { layout, atlas };
101
+ }
102
+
103
+ // Only helpers drawn through this class are tracked, so debug_clear never touches the
104
+ // helpers the app draws for itself into Debug.scene.
105
+ class DebugDrawer
106
+ {
107
+ private helpers = new Map<string, Object3D>();
108
+ private sdf_font: string | undefined;
109
+ private load_sdf_font: SDFFontLoader;
110
+ // Loaded fonts by layout URL. Every sdf_text helper shares its font's atlas, so
111
+ // clearing a helper never disposes it.
112
+ private sdf_fonts = new Map<string, Promise<SDFFont>>();
113
+
114
+ constructor(options: DebugDrawerOptions = {})
115
+ {
116
+ this.sdf_font = options.sdf_font;
117
+ this.load_sdf_font = options.load_sdf_font ?? load_sdf_font;
118
+ }
119
+
120
+ // sdf_text loads its font first, so it resolves later. Every other shape draws now.
121
+ draw(debug: DebugLike, root: Object3D, request: DebugDrawRequest): DebugDrawResult | Promise<DebugDrawResult>
122
+ {
123
+ const shape = typeof request.shape === 'string' && SHAPES.includes(request.shape) ? request.shape : null;
124
+
125
+ if (shape === null)
126
+ {
127
+ throw this.error('bad_request', `Unknown shape. Valid shapes: ${SHAPES.join(', ')}.`);
128
+ }
129
+
130
+ const position = this.position(request.position);
131
+ const size = this.size(request.size);
132
+ const color = this.color(request.color);
133
+
134
+ if (shape === 'sdf_text')
135
+ {
136
+ return this.draw_sdf_text(debug, this.text(request.text), this.font_url(request.font), position, size, color);
137
+ }
138
+
139
+ return this.track(shape, this.build(debug, root, shape, request, position, size, color));
140
+ }
141
+
142
+ private track(shape: string, helper: Object3D): DebugDrawResult
143
+ {
144
+ this.helpers.set(helper.uuid, helper);
145
+
146
+ return { id: helper.uuid, shape, helpers: this.helpers.size };
147
+ }
148
+
149
+ private async draw_sdf_text(debug: DebugLike, text: string, font_url: string, position: Vector3, size: number, color: Color | undefined): Promise<DebugDrawResult>
150
+ {
151
+ const font = await this.font(font_url);
152
+ const layout = this.sdf_layout(font.layout, font_url);
153
+
154
+ // SDFText looks glyphs up by UTF-16 unit (charCodeAt), so this check does too.
155
+ const drawable = Array.from({ length: text.length }, (_, i) => text.charCodeAt(i));
156
+
157
+ if (!drawable.some((unit) => layout.glyphs.some((glyph) => glyph.unicode === unit && glyph.planeBounds !== undefined)))
158
+ {
159
+ throw this.error('bad_request', `The font at ${font_url} has none of the characters of the text.`);
160
+ }
161
+
162
+ // One batch per helper, so debug_clear can remove each text on its own.
163
+ const batch = new SDFTextBatch(layout, font.atlas);
164
+ const element = batch.add_text(text);
165
+ element.set_position(position);
166
+ element.set_size(size);
167
+ element.set_color(new ThreeColor(color ?? 0xffffff));
168
+ // The text never changes, so one upload is enough.
169
+ batch.update(true);
170
+ debug.scene.add(batch);
171
+
172
+ return this.track('sdf_text', batch);
173
+ }
174
+
175
+ private font(url: string): Promise<SDFFont>
176
+ {
177
+ let font = this.sdf_fonts.get(url);
178
+
179
+ if (font === undefined)
180
+ {
181
+ font = this.load_sdf_font(url, url.replace(/\.json(?=$|[?#])/, '.png')).catch((cause: unknown) =>
182
+ {
183
+ // Forget the failure, so the next request tries again.
184
+ this.sdf_fonts.delete(url);
185
+
186
+ const reason = cause instanceof Error ? cause.message : typeof cause === 'string' ? cause : 'unknown error';
187
+
188
+ throw this.error('not_found', `Could not load the SDF font at ${url}: ${reason}`);
189
+ });
190
+
191
+ this.sdf_fonts.set(url, font);
192
+ }
193
+
194
+ return font;
195
+ }
196
+
197
+ private font_url(value: unknown): string
198
+ {
199
+ if (value === undefined && this.sdf_font === undefined)
200
+ {
201
+ throw this.error('bad_request', 'sdf_text needs a font: no default SDF font is configured.');
202
+ }
203
+
204
+ const url = value ?? this.sdf_font;
205
+
206
+ if (typeof url !== 'string' || !/\.json(?=$|[?#])/.test(url))
207
+ {
208
+ throw this.error('bad_request', 'font must be the URL of an msdf-atlas-gen .json layout, with its .png atlas next to it.');
209
+ }
210
+
211
+ return url;
212
+ }
213
+
214
+ private sdf_layout(value: unknown, url: string): SDFLayout
215
+ {
216
+ const layout = value as Partial<SDFLayout> | null;
217
+
218
+ if (typeof layout?.atlas?.width !== 'number' || typeof layout.atlas.height !== 'number' || !Array.isArray(layout.glyphs))
219
+ {
220
+ throw this.error('bad_request', `The font at ${url} is not an msdf-atlas-gen layout.`);
221
+ }
222
+
223
+ // The material maps atlas bounds counted from the bottom.
224
+ if (layout.atlas.yOrigin === 'top')
225
+ {
226
+ throw this.error('bad_request', `The font at ${url} has yOrigin top. Generate it with -yorigin bottom.`);
227
+ }
228
+
229
+ return layout as SDFLayout;
230
+ }
231
+
232
+ private build(debug: DebugLike, root: Object3D, shape: string, request: DebugDrawRequest, position: Vector3, size: number, color: Color | undefined): Object3D
233
+ {
234
+ switch (shape)
235
+ {
236
+ case 'cube':
237
+ return debug.draw_cube(position, size, color);
238
+
239
+ case 'sphere':
240
+ // Sphere's size is its radius, which is what the contract promises.
241
+ return debug.draw_sphere(position, size, color);
242
+
243
+ case 'plane':
244
+ {
245
+ const plane = debug.draw_plane(size, size, color);
246
+ plane.position.copy(position);
247
+
248
+ return plane;
249
+ }
250
+
251
+ case 'math_sphere':
252
+ return debug.draw_math_sphere(new Sphere(position, size), color);
253
+
254
+ case 'label':
255
+ // Size is the text height.
256
+ return debug.draw_label(this.text(request.text), position, size, color);
257
+
258
+ default:
259
+ return debug.draw_bounding_box(this.bounds(root, request.object), color);
260
+ }
261
+ }
262
+
263
+ clear(request: DebugClearRequest): DebugClearResult
264
+ {
265
+ if (request.id === undefined)
266
+ {
267
+ const removed = this.helpers.size;
268
+
269
+ this.helpers.forEach((helper) => this.dispose(helper));
270
+ this.helpers.clear();
271
+
272
+ return { removed, helpers: 0 };
273
+ }
274
+
275
+ const helper = typeof request.id === 'string' ? this.helpers.get(request.id) : undefined;
276
+
277
+ if (helper === undefined)
278
+ {
279
+ throw this.error('not_found', 'No helper with that id was drawn through debug_draw.');
280
+ }
281
+
282
+ this.dispose(helper);
283
+ this.helpers.delete(helper.uuid);
284
+
285
+ return { removed: 1, helpers: this.helpers.size };
286
+ }
287
+
288
+ private dispose(helper: Object3D): void
289
+ {
290
+ helper.removeFromParent();
291
+
292
+ helper.traverse((node) =>
293
+ {
294
+ const mesh = node as Partial<Mesh>;
295
+
296
+ mesh.geometry?.dispose();
297
+
298
+ const materials: Material[] = Array.isArray(mesh.material) ? mesh.material : mesh.material ? [mesh.material] : [];
299
+ materials.forEach((material) => material.dispose());
300
+
301
+ // A label owns its canvas texture.
302
+ if (node instanceof WorldImage)
303
+ {
304
+ node.material.uniforms._MainTex.value.dispose();
305
+ }
306
+ });
307
+ }
308
+
309
+ private bounds(root: Object3D, target: unknown): Box3
310
+ {
311
+ const { name, uuid } = (typeof target === 'object' && target !== null ? target : {}) as { name?: unknown, uuid?: unknown };
312
+
313
+ if (typeof uuid !== 'string' && typeof name !== 'string')
314
+ {
315
+ throw this.error('bad_request', 'bounding_box needs an object with a name or a uuid.');
316
+ }
317
+
318
+ // uuid wins over name, like SceneEditor.
319
+ const found = typeof uuid === 'string'
320
+ ? root.getObjectByProperty('uuid', uuid)
321
+ : root.getObjectByName(name as string);
322
+
323
+ if (found === undefined)
324
+ {
325
+ const label = typeof uuid === 'string' ? `uuid ${uuid}` : `name ${name as string}`;
326
+
327
+ throw this.error('not_found', `No object matching ${label} in the current scene.`);
328
+ }
329
+
330
+ const box = new Box3().setFromObject(found);
331
+
332
+ if (box.isEmpty())
333
+ {
334
+ throw this.error('bad_request', `Object "${found.name || found.uuid}" has no geometry, so its bounds are empty.`);
335
+ }
336
+
337
+ return box;
338
+ }
339
+
340
+ private text(value: unknown): string
341
+ {
342
+ if (typeof value !== 'string' || value.length === 0 || value.length > MAX_LABEL_LENGTH)
343
+ {
344
+ throw this.error('bad_request', `label and sdf_text need a text of 1 to ${MAX_LABEL_LENGTH} characters.`);
345
+ }
346
+
347
+ return value;
348
+ }
349
+
350
+ private position(value: unknown): Vector3
351
+ {
352
+ if (value === undefined)
353
+ {
354
+ return new Vector3();
355
+ }
356
+
357
+ if (!Array.isArray(value) || value.length !== 3 || !value.every((n) => typeof n === 'number' && Number.isFinite(n)))
358
+ {
359
+ throw this.error('bad_request', 'position must be [x, y, z] with finite numbers.');
360
+ }
361
+
362
+ return new Vector3(value[0] as number, value[1] as number, value[2] as number);
363
+ }
364
+
365
+ private size(value: unknown): number
366
+ {
367
+ if (value === undefined)
368
+ {
369
+ return 1;
370
+ }
371
+
372
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
373
+ {
374
+ throw this.error('bad_request', 'size must be a positive finite number.');
375
+ }
376
+
377
+ return value;
378
+ }
379
+
380
+ private color(value: unknown): Color | undefined
381
+ {
382
+ if (value === undefined || typeof value === 'number' || typeof value === 'string')
383
+ {
384
+ return value as Color | undefined;
385
+ }
386
+
387
+ throw this.error('bad_request', 'color must be a number or a string.');
388
+ }
389
+
390
+ private error(code: string, message: string): Error
391
+ {
392
+ const error: Error & { code?: string } = new Error(message);
393
+ error.code = code;
394
+
395
+ return error;
396
+ }
397
+ }
398
+
399
+ export { DebugDrawer };
@@ -0,0 +1,195 @@
1
+ import type { CommandHandler, HandlerTiming } from './CommandDispatcher';
2
+ import { CommandDispatcher } from './CommandDispatcher';
3
+ import type { AppInfo, Command, Reply } from './protocol';
4
+ import { PROTOCOL_VERSION } from './protocol';
5
+
6
+ const RETRY_DELAY_MS = 2000;
7
+
8
+ interface DevBridgeOptions
9
+ {
10
+ port: number;
11
+ app_info: () => AppInfo;
12
+ }
13
+
14
+ class DevBridge
15
+ {
16
+ dispatcher: CommandDispatcher;
17
+
18
+ private socket: WebSocket;
19
+ private options: DevBridgeOptions;
20
+ private retry_timer: number;
21
+ private disposed: boolean;
22
+
23
+ constructor()
24
+ {
25
+ this.socket = null;
26
+ this.options = null;
27
+ this.retry_timer = 0;
28
+ this.disposed = false;
29
+ this.dispatcher = new CommandDispatcher((reply) => this.send_reply(reply));
30
+ }
31
+
32
+ init(options: DevBridgeOptions)
33
+ {
34
+ this.options = options;
35
+ this.disposed = false;
36
+ this.connect();
37
+ }
38
+
39
+ register(cmd: string, timing: HandlerTiming, handler: CommandHandler)
40
+ {
41
+ this.dispatcher.register(cmd, timing, handler);
42
+ }
43
+
44
+ // Called from the application's on_frame_end so queued commands run at a
45
+ // safe frame boundary instead of part-way through a frame.
46
+ on_frame_end()
47
+ {
48
+ this.dispatcher.drain();
49
+ }
50
+
51
+ dispose()
52
+ {
53
+ this.disposed = true;
54
+ window.clearTimeout(this.retry_timer);
55
+
56
+ if (this.socket !== null)
57
+ {
58
+ this.socket.onclose = null;
59
+ this.socket.close();
60
+ this.socket = null;
61
+ }
62
+ }
63
+
64
+ private connect()
65
+ {
66
+ const options = this.options;
67
+
68
+ if (this.disposed || options === null)
69
+ {
70
+ return;
71
+ }
72
+
73
+ const socket = new WebSocket(`ws://127.0.0.1:${options.port}`);
74
+ this.socket = socket;
75
+
76
+ socket.onopen = () =>
77
+ {
78
+ socket.send(JSON.stringify({
79
+ event: 'hello',
80
+ protocol: PROTOCOL_VERSION,
81
+ app: options.app_info()
82
+ }));
83
+ };
84
+
85
+ socket.onmessage = (message) => this.on_message(message);
86
+
87
+ socket.onclose = () =>
88
+ {
89
+ if (this.socket === socket)
90
+ {
91
+ this.socket = null;
92
+ }
93
+
94
+ this.schedule_retry();
95
+ };
96
+
97
+ // A closed host raises both error and close. Retry is driven by close only.
98
+ socket.onerror = () => {};
99
+ }
100
+
101
+ private on_message(message: { data: unknown })
102
+ {
103
+ const raw = message.data;
104
+
105
+ if (typeof raw !== 'string')
106
+ {
107
+ return;
108
+ }
109
+
110
+ const frame = this.parse_frame(raw);
111
+
112
+ if (frame === null)
113
+ {
114
+ return;
115
+ }
116
+
117
+ if (frame.event === 'welcome')
118
+ {
119
+ console.info('[ohzi-mcp] bridge connected');
120
+ return;
121
+ }
122
+
123
+ if (frame.event === 'refused')
124
+ {
125
+ const reason = this.as_text(frame.reason);
126
+ const server_protocol = this.as_text(frame.server_protocol);
127
+
128
+ console.warn(`[ohzi-mcp] bridge refused: ${reason} (server protocol ${server_protocol}, this app speaks ${PROTOCOL_VERSION})`);
129
+ return;
130
+ }
131
+
132
+ if (typeof frame.id === 'string' && typeof frame.cmd === 'string')
133
+ {
134
+ this.dispatcher.handle(frame as unknown as Command);
135
+ }
136
+ }
137
+
138
+ private parse_frame(raw: string): Record<string, unknown> | null
139
+ {
140
+ try
141
+ {
142
+ const value: unknown = JSON.parse(raw);
143
+
144
+ if (typeof value !== 'object' || value === null)
145
+ {
146
+ return null;
147
+ }
148
+
149
+ return value as Record<string, unknown>;
150
+ }
151
+ catch
152
+ {
153
+ return null;
154
+ }
155
+ }
156
+
157
+ // Never let an object reach a template literal as '[object Object]'.
158
+ private as_text(value: unknown): string
159
+ {
160
+ if (typeof value === 'string')
161
+ {
162
+ return value;
163
+ }
164
+
165
+ if (typeof value === 'number' || typeof value === 'boolean')
166
+ {
167
+ return String(value);
168
+ }
169
+
170
+ return '(unknown)';
171
+ }
172
+
173
+ private send_reply(reply: Reply)
174
+ {
175
+ if (this.socket === null || this.socket.readyState !== WebSocket.OPEN)
176
+ {
177
+ return;
178
+ }
179
+
180
+ this.socket.send(JSON.stringify(reply));
181
+ }
182
+
183
+ private schedule_retry()
184
+ {
185
+ if (this.disposed)
186
+ {
187
+ return;
188
+ }
189
+
190
+ window.clearTimeout(this.retry_timer);
191
+ this.retry_timer = window.setTimeout(() => this.connect(), RETRY_DELAY_MS);
192
+ }
193
+ }
194
+
195
+ export { DevBridge };