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.
- package/README.md +2 -1
- package/build/index.mjs +1 -1
- package/build/index.mjs.map +1 -1
- package/package.json +12 -6
- package/src/Capabilities.ts +0 -2
- package/src/Debug.ts +38 -33
- package/src/Graphics.ts +2 -2
- package/src/Initializer.ts +2 -1
- package/src/SceneManager.ts +4 -5
- package/src/action_sequencer/NumberInterpolator.ts +2 -0
- package/src/blitter_index.ts +1 -2
- package/src/camera_controller/CameraController.ts +106 -63
- package/src/camera_controller/states/common/CommonCameraState.ts +2 -1
- package/src/components/AxisHelper.ts +1 -0
- package/src/components/Line.ts +35 -27
- package/src/components/PlaneHelper.ts +1 -1
- package/src/components/Text2D.ts +3 -1
- package/src/components/WorldImage.ts +27 -20
- package/src/components/sdf_text/SDFTextBatch.ts +16 -26
- package/src/dev_bridge/CameraBridge.ts +363 -0
- package/src/dev_bridge/CaptureService.ts +256 -0
- package/src/dev_bridge/CommandDispatcher.ts +114 -0
- package/src/dev_bridge/ConsoleBuffer.ts +286 -0
- package/src/dev_bridge/DebugDrawer.ts +399 -0
- package/src/dev_bridge/DevBridge.ts +195 -0
- package/src/dev_bridge/InputSynthesizer.ts +287 -0
- package/src/dev_bridge/PerformanceProbe.ts +118 -0
- package/src/dev_bridge/RenderModeRegistry.ts +66 -0
- package/src/dev_bridge/SceneEditor.ts +288 -0
- package/src/dev_bridge/SceneInspector.ts +388 -0
- package/src/dev_bridge/ViewNavigator.ts +101 -0
- package/src/dev_bridge/protocol.ts +31 -0
- package/src/index.ts +21 -14
- package/src/materials/AddMaterial.ts +14 -7
- package/src/materials/BlitNodeMaterial.ts +85 -0
- package/src/materials/BloomComposeMaterial.ts +21 -9
- package/src/materials/BoxBlurMaterial.ts +26 -6
- package/src/materials/ClearDepthNormalMaterial.ts +12 -6
- package/src/materials/DepthNormalMaterial.ts +25 -14
- package/src/materials/DualFilteringBlurMaterial.ts +32 -5
- package/src/materials/GaussianBlurMaterial.ts +36 -14
- package/src/materials/LineMaterial.ts +52 -0
- package/src/materials/LuminosityHighPassMaterial.ts +23 -10
- package/src/materials/NormalMaterial.ts +10 -11
- package/src/materials/SDFTextMaterial.ts +63 -16
- package/src/materials/SSAOComposeMaterial.ts +14 -5
- package/src/materials/SSAOMaterial.ts +123 -24
- package/src/materials/ScreenSpaceTextureMaterial.ts +19 -31
- package/src/materials/UnrealBloomComposeMaterial.ts +41 -32
- package/src/materials/WorldImageMaterial.ts +51 -0
- package/src/materials/deferred/DeferredPointLightMaterial.ts +45 -14
- package/src/materials/deferred/depth_normal_encoding.ts +36 -0
- package/src/primitives/Cube.ts +1 -1
- package/src/primitives/Sphere.ts +1 -1
- package/src/raycast/GroupRaycaster.ts +2 -2
- package/src/render_mode/BloomRender.ts +18 -4
- package/src/render_mode/DebugNormalsRender.ts +6 -1
- package/src/render_mode/DeferredRender.ts +5 -34
- package/src/render_mode/NormalAORender.ts +9 -19
- package/src/render_mode/UnrealBloomRender.ts +2 -1
- package/src/render_mode/VRRender.ts +6 -3
- package/src/render_utilities/Blitter.ts +4 -4
- package/src/render_utilities/Blurrer.ts +7 -0
- package/src/render_utilities/DualFilteringBlurrer.ts +26 -10
- package/src/resource_loader/AudioLoader.ts +5 -3
- package/src/resource_loader/HDRTextureLoader.ts +1 -1
- package/src/scenes/AbstractScene.ts +7 -3
- package/src/scenes/Compilator.ts +5 -1
- package/src/scenes/loading_states/LoadingState.ts +15 -7
- package/src/scenes/loading_states/RegularLoadingState.ts +2 -1
- package/src/utilities/CameraUtilities.ts +57 -79
- package/src/utilities/HTMLUtilities.ts +2 -1
- package/src/utilities/MeshSampler.ts +8 -1
- package/src/utilities/OrthographicFrustumPointFitter.ts +53 -4
- package/src/view_components/ApplicationView.ts +1 -1
- package/src/view_components/ViewComponent.ts +1 -1
- package/src/view_components/ViewManager.ts +7 -1
- package/src/view_components/transition/DrawIOAnimationSheet.ts +1 -1
- package/types/Capabilities.d.ts +0 -1
- package/types/SceneManager.d.ts +1 -1
- package/types/blitter_index.d.ts +1 -2
- package/types/camera_controller/CameraController.d.ts +9 -0
- package/types/components/Line.d.ts +3 -2
- package/types/components/WorldImage.d.ts +3 -2
- package/types/index.d.ts +2 -7
- package/types/materials/AddMaterial.d.ts +2 -2
- package/types/materials/BlitNodeMaterial.d.ts +18 -0
- package/types/materials/BloomComposeMaterial.d.ts +2 -2
- package/types/materials/BoxBlurMaterial.d.ts +2 -2
- package/types/materials/ClearDepthNormalMaterial.d.ts +2 -2
- package/types/materials/DepthNormalMaterial.d.ts +2 -2
- package/types/materials/DualFilteringBlurMaterial.d.ts +2 -2
- package/types/materials/GaussianBlurMaterial.d.ts +2 -2
- package/types/materials/LuminosityHighPassMaterial.d.ts +2 -2
- package/types/materials/NormalMaterial.d.ts +2 -2
- package/types/materials/SDFTextMaterial.d.ts +2 -2
- package/types/materials/SSAOComposeMaterial.d.ts +2 -2
- package/types/materials/SSAOMaterial.d.ts +2 -2
- package/types/materials/ScreenSpaceTextureMaterial.d.ts +2 -2
- package/types/materials/UnrealBloomComposeMaterial.d.ts +2 -2
- package/types/materials/deferred/DeferredPointLightMaterial.d.ts +2 -2
- package/types/render_mode/DeferredRender.d.ts +0 -2
- package/types/render_mode/NormalAORender.d.ts +0 -2
- package/types/render_utilities/Blitter.d.ts +2 -2
- package/types/render_utilities/DualFilteringBlurrer.d.ts +0 -2
- package/types/utilities/CameraUtilities.d.ts +0 -1
- package/types/utilities/OrthographicFrustumPointFitter.d.ts +14 -8
- package/build/index.module.js +0 -2
- package/build/index.module.js.map +0 -1
- package/src/components/EdgeMesh.ts +0 -150
- package/src/components/GeometryEdgeVisualizer.ts +0 -49
- package/src/components/MultiLinePath.ts +0 -25
- package/src/components/MultiLineText2D.ts +0 -65
- package/src/components/Shape2D.ts +0 -42
- package/src/components/Shape3D.ts +0 -38
- package/src/gpu_particles/GPUParticleSystem.ts +0 -122
- package/src/gpu_particles/ParticleAttribute.ts +0 -110
- package/src/gpu_particles/ParticlePositionAttribute.ts +0 -46
- package/src/materials/AlphaFilterMaterial.ts +0 -21
- package/src/materials/BaseShaderMaterial.ts +0 -20
- package/src/materials/BlitMaterial.ts +0 -27
- package/src/materials/DeferredRendererComposeMaterial.ts +0 -36
- package/src/materials/DisplayNormalTextureMaterial.ts +0 -18
- package/src/materials/FXAAMaterial.ts +0 -12
- package/src/materials/MedianFilterMaterial.ts +0 -46
- package/src/materials/SDFScreenTextMaterial.ts +0 -32
- package/src/materials/UnrealBlurMaterial.ts +0 -20
- package/src/materials/UnrealComposeMaterial.ts +0 -27
- package/src/materials/ViewPositionMaterial.ts +0 -19
- package/src/materials/gpu_particles/AttributeUpdateMaterial.ts +0 -35
- package/src/materials/gpu_particles/BasicParticleMaterial.ts +0 -25
- package/src/materials/gpu_particles/ParticleStorageMaterial.ts +0 -36
- package/src/materials/gpu_particles/PositionStorageMaterial.ts +0 -27
- package/src/render_utilities/MedianFilter.ts +0 -72
- package/src/render_utilities/ViewPositionRenderer.ts +0 -53
- package/src/shaders/add/add.frag +0 -28
- package/src/shaders/anti_aliasing/fxaa.frag +0 -72
- package/src/shaders/basic_color/basic_color.frag +0 -6
- package/src/shaders/basic_color/basic_color.vert +0 -10
- package/src/shaders/basic_line/basic_line.frag +0 -37
- package/src/shaders/basic_line/basic_line.vert +0 -37
- package/src/shaders/basic_texture/basic_texture.frag +0 -10
- package/src/shaders/basic_texture/basic_texture.vert +0 -17
- package/src/shaders/bloom/bloom_compose.frag +0 -22
- package/src/shaders/bloom/cheap_bloom_compose.frag +0 -13
- package/src/shaders/box_blur/box_blur.frag +0 -24
- package/src/shaders/box_blur/compose.frag +0 -31
- package/src/shaders/clear/clear_depth_normal.frag +0 -25
- package/src/shaders/copy/copy.frag +0 -7
- package/src/shaders/copy/copy.vert +0 -6
- package/src/shaders/deferred/blit_copy.vert +0 -11
- package/src/shaders/deferred/deferred_compose.frag +0 -59
- package/src/shaders/deferred/point_light.frag +0 -66
- package/src/shaders/deferred/point_light.vert +0 -13
- package/src/shaders/depth_normals/debug_normals.frag +0 -48
- package/src/shaders/depth_normals/depth_normals.frag +0 -30
- package/src/shaders/depth_normals/depth_normals.vert +0 -13
- package/src/shaders/dual_filter_blur/alpha_filter.frag +0 -14
- package/src/shaders/dual_filter_blur/downsample.frag +0 -21
- package/src/shaders/dual_filter_blur/upsample.frag +0 -24
- package/src/shaders/edge_visualizer/edge_visualizer.frag +0 -18
- package/src/shaders/edge_visualizer/edge_visualizer.vert +0 -16
- package/src/shaders/edges/corners.frag +0 -11
- package/src/shaders/edges/corners.vert +0 -31
- package/src/shaders/edges/edges.frag +0 -7
- package/src/shaders/edges/edges.vert +0 -27
- package/src/shaders/gaussian_blur/gaussian_blur.frag +0 -44
- package/src/shaders/gaussian_blur/unreal_bloom_compose.frag +0 -36
- package/src/shaders/gpu_particles/common_utils.glsl +0 -13
- package/src/shaders/gpu_particles/generic_storage.frag +0 -6
- package/src/shaders/gpu_particles/store/store_position.vert +0 -14
- package/src/shaders/gpu_particles/update/basic_update.frag +0 -21
- package/src/shaders/gpu_particles/visualize/visualize.frag +0 -11
- package/src/shaders/gpu_particles/visualize/visualize.vert +0 -12
- package/src/shaders/grid/grid.frag +0 -17
- package/src/shaders/grid/grid.vert +0 -10
- package/src/shaders/luminosity_high_pass/luminosity_high_pass.frag +0 -38
- package/src/shaders/median_filter/median_filter.frag +0 -134
- package/src/shaders/normal/normal.frag +0 -6
- package/src/shaders/normal/normal.vert +0 -6
- package/src/shaders/sdf_text/sdf_screen_text.frag +0 -51
- package/src/shaders/sdf_text/sdf_screen_text.vert +0 -42
- package/src/shaders/sdf_text/sdf_text.frag +0 -53
- package/src/shaders/sdf_text/sdf_text.vert +0 -42
- package/src/shaders/ssao/hbao.frag +0 -264
- package/src/shaders/ssao/ssao.frag +0 -95
- package/src/shaders/ssao/ssao.vert +0 -11
- package/src/shaders/ssao/ssao_compose.frag +0 -14
- package/src/shaders/transparent_mix/copy.frag +0 -7
- package/src/shaders/transparent_mix/transparent_mix.frag +0 -19
- package/src/shaders/transparent_mix/transparent_mix.vert +0 -8
- package/src/shaders/ui/ss_texture.frag +0 -11
- package/src/shaders/ui/ss_texture.vert +0 -32
- package/src/shaders/ui/ws_texture.vert +0 -39
- package/src/shaders/unreal_blur/unreal_blur.frag +0 -25
- package/src/shaders/unreal_blur/unreal_compose.frag +0 -24
- package/src/shaders/write_view_position/write_view_position.frag +0 -7
- package/src/shaders/write_view_position/write_view_position.vert +0 -9
- package/src/types.d.ts +0 -14
- package/types/components/EdgeMesh.d.ts +0 -16
- package/types/components/GeometryEdgeVisualizer.d.ts +0 -8
- package/types/components/MultiLinePath.d.ts +0 -8
- package/types/components/MultiLineText2D.d.ts +0 -12
- package/types/components/Shape2D.d.ts +0 -7
- package/types/components/Shape3D.d.ts +0 -6
- package/types/gpu_particles/GPUParticleSystem.d.ts +0 -19
- package/types/gpu_particles/ParticleAttribute.d.ts +0 -20
- package/types/gpu_particles/ParticlePositionAttribute.d.ts +0 -6
- package/types/materials/AlphaFilterMaterial.d.ts +0 -7
- package/types/materials/BaseShaderMaterial.d.ts +0 -4
- package/types/materials/BlitMaterial.d.ts +0 -4
- package/types/materials/DeferredRendererComposeMaterial.d.ts +0 -10
- package/types/materials/DisplayNormalTextureMaterial.d.ts +0 -4
- package/types/materials/FXAAMaterial.d.ts +0 -5
- package/types/materials/MedianFilterMaterial.d.ts +0 -8
- package/types/materials/SDFScreenTextMaterial.d.ts +0 -8
- package/types/materials/UIElementMaterial.d.ts +0 -4
- package/types/materials/UnrealBlurMaterial.d.ts +0 -5
- package/types/materials/UnrealComposeMaterial.d.ts +0 -5
- package/types/materials/ViewPositionMaterial.d.ts +0 -5
- package/types/materials/gpu_particles/AttributeUpdateMaterial.d.ts +0 -8
- package/types/materials/gpu_particles/BasicParticleMaterial.d.ts +0 -5
- package/types/materials/gpu_particles/ParticleStorageMaterial.d.ts +0 -5
- package/types/materials/gpu_particles/PositionStorageMaterial.d.ts +0 -5
- package/types/render_utilities/MedianFilter.d.ts +0 -12
- 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 };
|