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,256 @@
|
|
|
1
|
+
const MAX_DIMENSION = 8192;
|
|
2
|
+
const BASE64_CHUNK_SIZE = 8192;
|
|
3
|
+
|
|
4
|
+
// A retina canvas reports physical pixels, so a fast capture is 4x the pixels
|
|
5
|
+
// the developer is actually looking at. Every one of those bytes is base64'd
|
|
6
|
+
// into the model's context, so cap the long edge unless asked otherwise.
|
|
7
|
+
const DEFAULT_MAX_FAST_SIZE = 1280;
|
|
8
|
+
|
|
9
|
+
export interface CaptureCanvas
|
|
10
|
+
{
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
toBlob(callback: (blob: Blob | null) => void, type?: string): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ScratchContext
|
|
17
|
+
{
|
|
18
|
+
drawImage(source: unknown, x: number, y: number, width: number, height: number): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ScratchCanvas
|
|
22
|
+
{
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
getContext(type: '2d'): ScratchContext | null;
|
|
26
|
+
toBlob(callback: (blob: Blob | null) => void, type?: string): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CaptureGraphics
|
|
30
|
+
{
|
|
31
|
+
canvas: CaptureCanvas;
|
|
32
|
+
take_screenshot(callback: (blob: Blob | null) => void, width?: number, height?: number): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Values arrive straight off the dev-bridge wire, so they are untrusted and
|
|
36
|
+
// every field is validated before use.
|
|
37
|
+
export interface CaptureOptions
|
|
38
|
+
{
|
|
39
|
+
mode?: unknown;
|
|
40
|
+
width?: unknown;
|
|
41
|
+
height?: unknown;
|
|
42
|
+
max_size?: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CaptureResult
|
|
46
|
+
{
|
|
47
|
+
mode: 'fast' | 'hires';
|
|
48
|
+
mime_type: string;
|
|
49
|
+
width: number;
|
|
50
|
+
height: number;
|
|
51
|
+
bytes: number;
|
|
52
|
+
data: string;
|
|
53
|
+
scaled_from?: { width: number; height: number };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Holds no OHZI singletons: the consumer injects the graphics surface and a
|
|
57
|
+
// camera predicate, so this stays testable without a WebGPU context.
|
|
58
|
+
class CaptureService
|
|
59
|
+
{
|
|
60
|
+
private graphics: CaptureGraphics;
|
|
61
|
+
private has_camera: () => boolean;
|
|
62
|
+
private create_canvas: () => ScratchCanvas;
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
graphics: CaptureGraphics,
|
|
66
|
+
has_camera: () => boolean,
|
|
67
|
+
create_canvas: () => ScratchCanvas = () => document.createElement('canvas') as unknown as ScratchCanvas
|
|
68
|
+
)
|
|
69
|
+
{
|
|
70
|
+
this.graphics = graphics;
|
|
71
|
+
this.has_camera = has_camera;
|
|
72
|
+
this.create_canvas = create_canvas;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async capture(options: CaptureOptions): Promise<CaptureResult>
|
|
76
|
+
{
|
|
77
|
+
if (options.mode === 'hires')
|
|
78
|
+
{
|
|
79
|
+
return this.capture_hires(options);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return this.capture_fast(options);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Reads the live canvas. Valid because the renderer is created with
|
|
86
|
+
// preserveDrawingBuffer: true, so the backbuffer still holds the last frame.
|
|
87
|
+
private async capture_fast(options: CaptureOptions): Promise<CaptureResult>
|
|
88
|
+
{
|
|
89
|
+
const canvas = this.graphics.canvas;
|
|
90
|
+
const target = this.scaled_size(canvas.width, canvas.height, options.max_size);
|
|
91
|
+
|
|
92
|
+
if (target !== null)
|
|
93
|
+
{
|
|
94
|
+
const scratch = this.draw_scaled(canvas, target.width, target.height);
|
|
95
|
+
|
|
96
|
+
if (scratch !== null)
|
|
97
|
+
{
|
|
98
|
+
const scaled_blob = await this.request_blob((callback) => scratch.toBlob(callback, 'image/png'));
|
|
99
|
+
const scaled = await this.build_result('fast', target.width, target.height, scaled_blob);
|
|
100
|
+
|
|
101
|
+
scaled.scaled_from = { width: canvas.width, height: canvas.height };
|
|
102
|
+
|
|
103
|
+
return scaled;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const blob = await this.request_blob((callback) => canvas.toBlob(callback, 'image/png'));
|
|
108
|
+
|
|
109
|
+
return this.build_result('fast', canvas.width, canvas.height, blob);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Returns null when no downscale should happen: an explicit opt-out, or a
|
|
113
|
+
// canvas already within the cap. Never upscales.
|
|
114
|
+
private scaled_size(width: number, height: number, max_size: unknown): { width: number; height: number } | null
|
|
115
|
+
{
|
|
116
|
+
const cap = typeof max_size === 'number' ? max_size : DEFAULT_MAX_FAST_SIZE;
|
|
117
|
+
|
|
118
|
+
if (!Number.isFinite(cap) || cap <= 0)
|
|
119
|
+
{
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const long_edge = Math.max(width, height);
|
|
124
|
+
|
|
125
|
+
if (long_edge <= cap)
|
|
126
|
+
{
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const ratio = cap / long_edge;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
width: Math.max(1, Math.round(width * ratio)),
|
|
134
|
+
height: Math.max(1, Math.round(height * ratio))
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Returns null if no 2D context is available, in which case the caller falls
|
|
139
|
+
// back to a native capture. A large image beats no image.
|
|
140
|
+
private draw_scaled(source: unknown, width: number, height: number): ScratchCanvas | null
|
|
141
|
+
{
|
|
142
|
+
const scratch = this.create_canvas();
|
|
143
|
+
|
|
144
|
+
scratch.width = width;
|
|
145
|
+
scratch.height = height;
|
|
146
|
+
|
|
147
|
+
const context = scratch.getContext('2d');
|
|
148
|
+
|
|
149
|
+
if (context === null)
|
|
150
|
+
{
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
context.drawImage(source, 0, 0, width, height);
|
|
155
|
+
|
|
156
|
+
return scratch;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async capture_hires(options: CaptureOptions): Promise<CaptureResult>
|
|
160
|
+
{
|
|
161
|
+
// Graphics.take_screenshot dereferences CameraManager.current directly and
|
|
162
|
+
// would throw a TypeError, so refuse with a code the caller can act on.
|
|
163
|
+
if (!this.has_camera())
|
|
164
|
+
{
|
|
165
|
+
throw this.error('no_camera', 'A hires capture needs an active camera; CameraManager.current is not set yet.');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const canvas = this.graphics.canvas;
|
|
169
|
+
const width = this.dimension(options.width, canvas.width);
|
|
170
|
+
const height = this.dimension(options.height, canvas.height);
|
|
171
|
+
|
|
172
|
+
const blob = await this.request_blob((callback) => this.graphics.take_screenshot(callback, width, height));
|
|
173
|
+
|
|
174
|
+
return this.build_result('hires', width, height, blob);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private dimension(value: unknown, fallback: number): number
|
|
178
|
+
{
|
|
179
|
+
if (typeof value === 'number')
|
|
180
|
+
{
|
|
181
|
+
return this.clamp(value);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return this.clamp(fallback);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// take_screenshot renders ceil(w/1024) * ceil(h/1024) tiles, so an unbounded
|
|
188
|
+
// size would stall the frame for a very long time.
|
|
189
|
+
private clamp(value: number): number
|
|
190
|
+
{
|
|
191
|
+
if (!Number.isFinite(value) || value < 1)
|
|
192
|
+
{
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return Math.min(Math.floor(value), MAX_DIMENSION);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private request_blob(request: (callback: (blob: Blob | null) => void) => void): Promise<Blob>
|
|
200
|
+
{
|
|
201
|
+
return new Promise((resolve, reject) =>
|
|
202
|
+
{
|
|
203
|
+
request((blob) =>
|
|
204
|
+
{
|
|
205
|
+
if (blob === null)
|
|
206
|
+
{
|
|
207
|
+
reject(this.error('capture_failed', 'The canvas produced no image data.'));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
resolve(blob);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async build_result(mode: 'fast' | 'hires', width: number, height: number, blob: Blob): Promise<CaptureResult>
|
|
217
|
+
{
|
|
218
|
+
const buffer = await blob.arrayBuffer();
|
|
219
|
+
const bytes = new Uint8Array(buffer);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
mode,
|
|
223
|
+
// Graphics asks toBlob for the malformed type 'image/png;base64;', which
|
|
224
|
+
// browsers fall back to PNG for. Report what the bytes actually are.
|
|
225
|
+
mime_type: 'image/png',
|
|
226
|
+
width,
|
|
227
|
+
height,
|
|
228
|
+
bytes: bytes.length,
|
|
229
|
+
data: this.to_base64(bytes)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private to_base64(bytes: Uint8Array): string
|
|
234
|
+
{
|
|
235
|
+
let binary = '';
|
|
236
|
+
|
|
237
|
+
// Chunked: spreading a full-resolution capture into String.fromCharCode
|
|
238
|
+
// overflows the call stack.
|
|
239
|
+
for (let i = 0; i < bytes.length; i += BASE64_CHUNK_SIZE)
|
|
240
|
+
{
|
|
241
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_SIZE));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return btoa(binary);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private error(code: string, message: string): Error
|
|
248
|
+
{
|
|
249
|
+
const error: Error & { code?: string } = new Error(message);
|
|
250
|
+
error.code = code;
|
|
251
|
+
|
|
252
|
+
return error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export { CaptureService };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Command, Reply } from './protocol';
|
|
2
|
+
|
|
3
|
+
// `unknown` already absorbs Promise<unknown>; run() normalises both via Promise.resolve.
|
|
4
|
+
export type CommandHandler = (args: Record<string, unknown>) => unknown;
|
|
5
|
+
export type HandlerTiming = 'immediate' | 'frame_end';
|
|
6
|
+
|
|
7
|
+
interface Registration
|
|
8
|
+
{
|
|
9
|
+
timing: HandlerTiming;
|
|
10
|
+
handler: CommandHandler;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface QueuedCommand
|
|
14
|
+
{
|
|
15
|
+
id: string;
|
|
16
|
+
args: Record<string, unknown>;
|
|
17
|
+
handler: CommandHandler;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class CommandDispatcher
|
|
21
|
+
{
|
|
22
|
+
handlers: Record<string, Registration>;
|
|
23
|
+
queue: QueuedCommand[];
|
|
24
|
+
|
|
25
|
+
private reply: (reply: Reply) => void;
|
|
26
|
+
|
|
27
|
+
constructor(reply: (reply: Reply) => void)
|
|
28
|
+
{
|
|
29
|
+
this.reply = reply;
|
|
30
|
+
this.handlers = {};
|
|
31
|
+
this.queue = [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
register(cmd: string, timing: HandlerTiming, handler: CommandHandler)
|
|
35
|
+
{
|
|
36
|
+
this.handlers[cmd] = { timing, handler };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
handle(command: Command)
|
|
40
|
+
{
|
|
41
|
+
const registration = this.handlers[command.cmd];
|
|
42
|
+
const args = command.args === undefined ? {} : command.args;
|
|
43
|
+
|
|
44
|
+
if (registration === undefined)
|
|
45
|
+
{
|
|
46
|
+
this.reply({
|
|
47
|
+
id: command.id,
|
|
48
|
+
ok: false,
|
|
49
|
+
error: { code: 'unknown_command', message: `Unknown command '${command.cmd}'` }
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (registration.timing === 'frame_end')
|
|
56
|
+
{
|
|
57
|
+
this.queue.push({ id: command.id, args, handler: registration.handler });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.run(command.id, args, registration.handler);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Called from MainApplication.on_frame_end() so rendering and mutation
|
|
65
|
+
// never happen part-way through a frame.
|
|
66
|
+
drain()
|
|
67
|
+
{
|
|
68
|
+
const queued = this.queue;
|
|
69
|
+
this.queue = [];
|
|
70
|
+
|
|
71
|
+
for (const command of queued)
|
|
72
|
+
{
|
|
73
|
+
this.run(command.id, command.args, command.handler);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private run(id: string, args: Record<string, unknown>, handler: CommandHandler)
|
|
78
|
+
{
|
|
79
|
+
try
|
|
80
|
+
{
|
|
81
|
+
Promise.resolve(handler(args))
|
|
82
|
+
.then((result) => this.reply({ id, ok: true, result }))
|
|
83
|
+
.catch((error) => this.reply({ id, ok: false, error: this.to_error(error) }));
|
|
84
|
+
}
|
|
85
|
+
catch (error)
|
|
86
|
+
{
|
|
87
|
+
this.reply({ id, ok: false, error: this.to_error(error) });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private to_error(error: unknown): { code: string; message: string }
|
|
92
|
+
{
|
|
93
|
+
const code = (error as { code?: string })?.code;
|
|
94
|
+
const message = (error as { message?: string })?.message;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
code: code === undefined ? 'handler_failed' : code,
|
|
98
|
+
message: typeof message === 'string' ? message : this.describe(error)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Avoids '[object Object]' reaching the developer for a thrown non-Error.
|
|
103
|
+
private describe(error: unknown): string
|
|
104
|
+
{
|
|
105
|
+
if (typeof error === 'string')
|
|
106
|
+
{
|
|
107
|
+
return error;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return `Handler failed with a non-Error value of type ${typeof error}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export { CommandDispatcher };
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
const DEFAULT_CAPACITY = 200;
|
|
2
|
+
const MAX_MESSAGE_LENGTH = 2000;
|
|
3
|
+
|
|
4
|
+
export type ConsoleLevel = 'log' | 'info' | 'warn' | 'error';
|
|
5
|
+
|
|
6
|
+
const LEVELS: ConsoleLevel[] = ['log', 'info', 'warn', 'error'];
|
|
7
|
+
|
|
8
|
+
export interface ConsoleEntry
|
|
9
|
+
{
|
|
10
|
+
seq: number;
|
|
11
|
+
level: ConsoleLevel;
|
|
12
|
+
message: string;
|
|
13
|
+
at: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ConsoleLike
|
|
17
|
+
{
|
|
18
|
+
log: (...args: unknown[]) => void;
|
|
19
|
+
info: (...args: unknown[]) => void;
|
|
20
|
+
warn: (...args: unknown[]) => void;
|
|
21
|
+
error: (...args: unknown[]) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ErrorSource
|
|
25
|
+
{
|
|
26
|
+
addEventListener(type: string, listener: (event: unknown) => void): void;
|
|
27
|
+
removeEventListener(type: string, listener: (event: unknown) => void): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Read options arrive off the dev-bridge wire and are therefore untrusted.
|
|
31
|
+
export interface ConsoleReadOptions
|
|
32
|
+
{
|
|
33
|
+
level?: unknown;
|
|
34
|
+
limit?: unknown;
|
|
35
|
+
since?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ConsoleReadResult
|
|
39
|
+
{
|
|
40
|
+
entries: ConsoleEntry[];
|
|
41
|
+
dropped: number;
|
|
42
|
+
total: number;
|
|
43
|
+
capacity: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A bounded log of console output and uncaught failures, so the bridge can
|
|
47
|
+
// report what a screenshot cannot show. Injected console and error source keep
|
|
48
|
+
// this testable outside a browser.
|
|
49
|
+
class ConsoleBuffer
|
|
50
|
+
{
|
|
51
|
+
private capacity: number;
|
|
52
|
+
private entries: ConsoleEntry[];
|
|
53
|
+
private seq: number;
|
|
54
|
+
private dropped: number;
|
|
55
|
+
private clock: () => number;
|
|
56
|
+
|
|
57
|
+
private target: ConsoleLike | null;
|
|
58
|
+
private source: ErrorSource | null;
|
|
59
|
+
private originals: Partial<Record<ConsoleLevel, (...args: unknown[]) => void>>;
|
|
60
|
+
private on_error: ((event: unknown) => void) | null;
|
|
61
|
+
private on_rejection: ((event: unknown) => void) | null;
|
|
62
|
+
|
|
63
|
+
constructor(capacity: number = DEFAULT_CAPACITY, clock: () => number = () => Date.now())
|
|
64
|
+
{
|
|
65
|
+
this.capacity = capacity > 0 ? Math.floor(capacity) : DEFAULT_CAPACITY;
|
|
66
|
+
this.entries = [];
|
|
67
|
+
this.seq = 0;
|
|
68
|
+
this.dropped = 0;
|
|
69
|
+
this.clock = clock;
|
|
70
|
+
this.target = null;
|
|
71
|
+
this.source = null;
|
|
72
|
+
this.originals = {};
|
|
73
|
+
this.on_error = null;
|
|
74
|
+
this.on_rejection = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
install(target: ConsoleLike, source?: ErrorSource)
|
|
78
|
+
{
|
|
79
|
+
this.dispose();
|
|
80
|
+
this.target = target;
|
|
81
|
+
|
|
82
|
+
for (const level of LEVELS)
|
|
83
|
+
{
|
|
84
|
+
// Explicitly typed: indexing with a union then calling .bind() widens to
|
|
85
|
+
// any, which the lint rules reject.
|
|
86
|
+
const original: (...args: unknown[]) => void = target[level];
|
|
87
|
+
this.originals[level] = original;
|
|
88
|
+
|
|
89
|
+
target[level] = (...args: unknown[]) =>
|
|
90
|
+
{
|
|
91
|
+
this.push(level, this.format(args));
|
|
92
|
+
original.apply(target, args);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (source !== undefined)
|
|
97
|
+
{
|
|
98
|
+
this.source = source;
|
|
99
|
+
|
|
100
|
+
this.on_error = (event) => this.push('error', this.event_message(event));
|
|
101
|
+
this.on_rejection = (event) => this.push('error', `Unhandled rejection: ${this.event_reason(event)}`);
|
|
102
|
+
|
|
103
|
+
source.addEventListener('error', this.on_error);
|
|
104
|
+
source.addEventListener('unhandledrejection', this.on_rejection);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
dispose()
|
|
109
|
+
{
|
|
110
|
+
const target = this.target;
|
|
111
|
+
|
|
112
|
+
if (target !== null)
|
|
113
|
+
{
|
|
114
|
+
for (const level of LEVELS)
|
|
115
|
+
{
|
|
116
|
+
const original = this.originals[level];
|
|
117
|
+
|
|
118
|
+
if (original !== undefined)
|
|
119
|
+
{
|
|
120
|
+
target[level] = original;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const source = this.source;
|
|
126
|
+
|
|
127
|
+
if (source !== null)
|
|
128
|
+
{
|
|
129
|
+
if (this.on_error !== null)
|
|
130
|
+
{
|
|
131
|
+
source.removeEventListener('error', this.on_error);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (this.on_rejection !== null)
|
|
135
|
+
{
|
|
136
|
+
source.removeEventListener('unhandledrejection', this.on_rejection);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.originals = {};
|
|
141
|
+
this.target = null;
|
|
142
|
+
this.source = null;
|
|
143
|
+
this.on_error = null;
|
|
144
|
+
this.on_rejection = null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
read(options: ConsoleReadOptions): ConsoleReadResult
|
|
148
|
+
{
|
|
149
|
+
const level = typeof options.level === 'string' ? options.level : null;
|
|
150
|
+
const since = typeof options.since === 'number' ? options.since : 0;
|
|
151
|
+
const limit = typeof options.limit === 'number' && options.limit > 0 ? Math.floor(options.limit) : 0;
|
|
152
|
+
|
|
153
|
+
let entries = this.entries.filter((entry) => entry.seq > since);
|
|
154
|
+
|
|
155
|
+
if (level !== null)
|
|
156
|
+
{
|
|
157
|
+
entries = entries.filter((entry) => entry.level === level);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (limit > 0 && entries.length > limit)
|
|
161
|
+
{
|
|
162
|
+
entries = entries.slice(entries.length - limit);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { entries, dropped: this.dropped, total: this.seq, capacity: this.capacity };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private push(level: ConsoleLevel, message: string)
|
|
169
|
+
{
|
|
170
|
+
this.seq++;
|
|
171
|
+
this.entries.push({ seq: this.seq, level, message, at: this.clock() });
|
|
172
|
+
|
|
173
|
+
while (this.entries.length > this.capacity)
|
|
174
|
+
{
|
|
175
|
+
this.entries.shift();
|
|
176
|
+
this.dropped++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private format(args: unknown[]): string
|
|
181
|
+
{
|
|
182
|
+
const message = args.map((arg) => this.describe(arg)).join(' ');
|
|
183
|
+
|
|
184
|
+
if (message.length <= MAX_MESSAGE_LENGTH)
|
|
185
|
+
{
|
|
186
|
+
return message;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const overflow = message.length - MAX_MESSAGE_LENGTH;
|
|
190
|
+
|
|
191
|
+
return `${message.slice(0, MAX_MESSAGE_LENGTH)} ... [truncated ${overflow} chars]`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Deliberately exhaustive: a log line rendered as '[object Object]' is worse
|
|
195
|
+
// than no log line at all.
|
|
196
|
+
private describe(value: unknown): string
|
|
197
|
+
{
|
|
198
|
+
if (typeof value === 'string')
|
|
199
|
+
{
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (value === null)
|
|
204
|
+
{
|
|
205
|
+
return 'null';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (value === undefined)
|
|
209
|
+
{
|
|
210
|
+
return 'undefined';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
214
|
+
{
|
|
215
|
+
return String(value);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (typeof value === 'function')
|
|
219
|
+
{
|
|
220
|
+
return '[function]';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (typeof value === 'symbol')
|
|
224
|
+
{
|
|
225
|
+
return '[symbol]';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (typeof value === 'bigint')
|
|
229
|
+
{
|
|
230
|
+
return `${value.toString()}n`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (value instanceof Error)
|
|
234
|
+
{
|
|
235
|
+
return `${value.name}: ${value.message}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try
|
|
239
|
+
{
|
|
240
|
+
const json = JSON.stringify(value);
|
|
241
|
+
|
|
242
|
+
return json === undefined ? '[unserialisable]' : json;
|
|
243
|
+
}
|
|
244
|
+
catch
|
|
245
|
+
{
|
|
246
|
+
return '[unserialisable]';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private event_message(event: unknown): string
|
|
251
|
+
{
|
|
252
|
+
if (typeof event === 'object' && event !== null)
|
|
253
|
+
{
|
|
254
|
+
const record = event as Record<string, unknown>;
|
|
255
|
+
|
|
256
|
+
if (typeof record.message === 'string')
|
|
257
|
+
{
|
|
258
|
+
return record.message;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (record.error !== undefined)
|
|
262
|
+
{
|
|
263
|
+
return this.describe(record.error);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return this.describe(event);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private event_reason(event: unknown): string
|
|
271
|
+
{
|
|
272
|
+
if (typeof event === 'object' && event !== null)
|
|
273
|
+
{
|
|
274
|
+
const record = event as Record<string, unknown>;
|
|
275
|
+
|
|
276
|
+
if (record.reason !== undefined)
|
|
277
|
+
{
|
|
278
|
+
return this.describe(record.reason);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return this.describe(event);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export { ConsoleBuffer };
|