@inweb/viewer-three 27.4.7 → 27.5.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 (64) hide show
  1. package/dist/extensions/components/AxesHelperComponent.js +3 -0
  2. package/dist/extensions/components/AxesHelperComponent.js.map +1 -1
  3. package/dist/extensions/components/AxesHelperComponent.min.js +1 -1
  4. package/dist/extensions/components/AxesHelperComponent.module.js +3 -0
  5. package/dist/extensions/components/AxesHelperComponent.module.js.map +1 -1
  6. package/dist/extensions/components/ExtentsHelperComponent.js +6 -2
  7. package/dist/extensions/components/ExtentsHelperComponent.js.map +1 -1
  8. package/dist/extensions/components/ExtentsHelperComponent.min.js +1 -1
  9. package/dist/extensions/components/ExtentsHelperComponent.module.js +6 -2
  10. package/dist/extensions/components/ExtentsHelperComponent.module.js.map +1 -1
  11. package/dist/extensions/components/GridHelperComponent.js +1 -0
  12. package/dist/extensions/components/GridHelperComponent.js.map +1 -1
  13. package/dist/extensions/components/GridHelperComponent.min.js +1 -1
  14. package/dist/extensions/components/GridHelperComponent.module.js +1 -0
  15. package/dist/extensions/components/GridHelperComponent.module.js.map +1 -1
  16. package/dist/extensions/components/LightHelperComponent.js +1 -0
  17. package/dist/extensions/components/LightHelperComponent.js.map +1 -1
  18. package/dist/extensions/components/LightHelperComponent.min.js +1 -1
  19. package/dist/extensions/components/LightHelperComponent.module.js +1 -0
  20. package/dist/extensions/components/LightHelperComponent.module.js.map +1 -1
  21. package/dist/viewer-three.js +1766 -438
  22. package/dist/viewer-three.js.map +1 -1
  23. package/dist/viewer-three.min.js +4 -4
  24. package/dist/viewer-three.module.js +1303 -403
  25. package/dist/viewer-three.module.js.map +1 -1
  26. package/extensions/components/AxesHelperComponent.ts +3 -0
  27. package/extensions/components/ExtentsHelperComponent.ts +5 -2
  28. package/extensions/components/GridHelperComponent.ts +1 -0
  29. package/extensions/components/LightHelperComponent.ts +1 -0
  30. package/lib/Viewer/Viewer.d.ts +5 -7
  31. package/lib/Viewer/components/CameraComponent.d.ts +1 -1
  32. package/lib/Viewer/components/ClippingPlaneComponent.d.ts +8 -0
  33. package/lib/Viewer/components/HighlighterComponent.d.ts +2 -2
  34. package/lib/Viewer/components/InfoComponent.d.ts +1 -1
  35. package/lib/Viewer/components/SectionsComponent.d.ts +15 -0
  36. package/lib/Viewer/components/WCSHelperComponent.d.ts +2 -2
  37. package/lib/Viewer/draggers/CuttingPlaneDragger.d.ts +6 -6
  38. package/lib/Viewer/draggers/OrbitDragger.d.ts +1 -1
  39. package/lib/Viewer/measurement/Snapper.d.ts +3 -3
  40. package/package.json +5 -5
  41. package/src/Viewer/Viewer.ts +50 -37
  42. package/src/Viewer/commands/index.ts +1 -1
  43. package/src/Viewer/components/BackgroundComponent.ts +1 -0
  44. package/src/Viewer/components/CameraComponent.ts +5 -6
  45. package/src/Viewer/{scenes/Helpers.ts → components/ClippingPlaneComponent.ts} +22 -12
  46. package/src/Viewer/components/HighlighterComponent.ts +9 -5
  47. package/src/Viewer/components/InfoComponent.ts +4 -4
  48. package/src/Viewer/components/SectionsComponent.ts +119 -0
  49. package/src/Viewer/components/SelectionComponent.ts +1 -1
  50. package/src/Viewer/components/WCSHelperComponent.ts +8 -6
  51. package/src/Viewer/components/index.ts +4 -0
  52. package/src/Viewer/draggers/CuttingPlaneDragger.ts +57 -34
  53. package/src/Viewer/draggers/MeasureLineDragger.ts +1 -1
  54. package/src/Viewer/draggers/OrbitDragger.ts +3 -3
  55. package/src/Viewer/helpers/SectionsHelper.js +1065 -0
  56. package/src/Viewer/helpers/WCSHelper.ts +24 -0
  57. package/src/Viewer/loaders/DynamicGltfLoader/DynamicGltfLoader.js +417 -92
  58. package/src/Viewer/loaders/DynamicGltfLoader/GltfStructure.js +76 -9
  59. package/src/Viewer/loaders/GLTFCloudDynamicLoader.ts +3 -2
  60. package/src/Viewer/loaders/GLTFFileDynamicLoader.ts +4 -2
  61. package/src/Viewer/measurement/Snapper.ts +4 -5
  62. package/src/Viewer/models/ModelImpl.ts +27 -3
  63. package/lib/Viewer/scenes/Helpers.d.ts +0 -7
  64. package/src/Viewer/postprocessing/SSAARenderPass.js +0 -245
@@ -88,6 +88,8 @@ export class GltfStructure {
88
88
  this._nextObjectId = 1;
89
89
  this.loadingAborted = false;
90
90
  this.criticalError = null;
91
+ // Holds decoded base64 buffer when the .gltf has an embedded data URI
92
+ this.embeddedBinaryChunk = null;
91
93
  }
92
94
 
93
95
  async initialize(loader) {
@@ -99,21 +101,53 @@ export class GltfStructure {
99
101
 
100
102
  this.json = json;
101
103
  this.loader = loader;
102
- this.uri = this.json.buffers[0].uri || "";
104
+
105
+ const bufferUri = this.json.buffers?.[0]?.uri || "";
106
+
107
+ // Detect embedded data URI (e.g. "data:application/octet-stream;base64,...")
108
+ // and decode it once into memory. The buffer URI is then cleared so that
109
+ // scheduleRequest serves data from memory instead of doing network requests.
110
+ if (bufferUri.startsWith("data:")) {
111
+ this.embeddedBinaryChunk = await this._decodeDataUri(bufferUri);
112
+ this.uri = "";
113
+ } else {
114
+ this.uri = bufferUri;
115
+ }
116
+ }
117
+
118
+ // Decode a data URI to ArrayBuffer using the browser's native fetch() —
119
+ // much faster than atob() + JS loop for large (10+ MB) base64 buffers.
120
+ async _decodeDataUri(dataUri) {
121
+ try {
122
+ const response = await fetch(dataUri);
123
+ return await response.arrayBuffer();
124
+ } catch (e) {
125
+ throw new Error(`DynamicLoader: Failed to decode embedded data URI: ${e.message}`);
126
+ }
103
127
  }
104
128
 
105
129
  clear() {
106
- this.json = null;
107
- this.loadController = null;
108
- this.pendingRequests = [];
109
130
  if (this.batchTimeout) {
110
131
  clearTimeout(this.batchTimeout);
111
132
  this.batchTimeout = null;
112
133
  }
113
134
 
114
- this.disposeMaterials();
115
- this.textureCache.clear();
116
- this.materials.clear();
135
+ this._rejectPendingRequests();
136
+
137
+ if (this.disposeMaterials) {
138
+ try {
139
+ this.disposeMaterials();
140
+ } catch (e) {
141
+ console.warn("DynamicLoader: error during disposeMaterials in clear():", e);
142
+ }
143
+ }
144
+ if (this.textureCache) this.textureCache.clear();
145
+ if (this.materials) this.materials.clear();
146
+
147
+ this.embeddedBinaryChunk = null;
148
+ this.json = null;
149
+ this.loadController = null;
150
+ this.uri = "";
117
151
 
118
152
  this.activeChunkLoads = 0;
119
153
  this.chunkQueue = [];
@@ -121,11 +155,38 @@ export class GltfStructure {
121
155
  this.criticalError = null;
122
156
  }
123
157
 
158
+ _rejectPendingRequests() {
159
+ const pending = this.pendingRequests;
160
+ this.pendingRequests = [];
161
+ if (!pending || pending.length === 0) return;
162
+
163
+ const cancelError = new Error("DynamicLoader: Structure cleared while requests pending");
164
+ for (let i = 0; i < pending.length; i++) {
165
+ const item = pending[i];
166
+ if (item && typeof item._reject === "function") {
167
+ try {
168
+ item._reject(cancelError);
169
+ } catch {
170
+ // Consumer may have already been disposed
171
+ }
172
+ }
173
+ }
174
+ }
175
+
124
176
  getJson() {
125
177
  return this.json;
126
178
  }
127
179
 
128
180
  scheduleRequest(request) {
181
+ // Fast path: buffer was an embedded data URI, decoded once into memory
182
+ if (this.embeddedBinaryChunk && !this.uri) {
183
+ return Promise.resolve({
184
+ buffer: this.embeddedBinaryChunk,
185
+ relOffset: request.offset,
186
+ length: request.length,
187
+ });
188
+ }
189
+
129
190
  return new Promise((resolve, reject) => {
130
191
  if (this.loadingAborted) {
131
192
  reject(
@@ -441,8 +502,13 @@ export class GltfStructure {
441
502
  return await this.textureLoader.loadAsync(fullUrl);
442
503
  } else if (image.bufferView !== undefined) {
443
504
  const bufferView = this.json.bufferViews[image.bufferView];
444
- const array = await this.getBufferView(bufferView.byteOffset || 0, bufferView.byteLength, 5121);
445
- const blob = new Blob([array], { type: image.mimeType });
505
+ const { buffer, relOffset } = await this.getBufferView(
506
+ bufferView.byteOffset || 0,
507
+ bufferView.byteLength,
508
+ 5121
509
+ );
510
+ const imageBytes = new Uint8Array(buffer, relOffset, bufferView.byteLength);
511
+ const blob = new Blob([imageBytes], { type: image.mimeType });
446
512
  const url = URL.createObjectURL(blob);
447
513
  const texture = await this.textureLoader.loadAsync(url);
448
514
  URL.revokeObjectURL(url);
@@ -479,6 +545,7 @@ export class GltfStructure {
479
545
  })
480
546
  );
481
547
  }
548
+ await this.flushBufferRequests();
482
549
  await Promise.all(texturePromises);
483
550
  }
484
551
 
@@ -58,9 +58,10 @@ export class GLTFCloudDynamicLoader extends Loader {
58
58
  const scene = new Group();
59
59
 
60
60
  this.gltfLoader = new DynamicGltfLoader(this.viewer.camera, scene, this.viewer.renderer);
61
- this.gltfLoader.memoryLimit = this.viewer.options.memoryLimit;
61
+ this.gltfLoader.setMemoryLimit(this.viewer.options.memoryLimit);
62
62
  this.gltfLoader.setVisibleEdges(this.viewer.options.edgeModel);
63
- // this.gltfLoader.setMaxConcurrentChunks(this.viewer.options.maxConcurrentChunks);
63
+ this.gltfLoader.setMaxConcurrentChunks(params.maxConcurrentChunks);
64
+ this.gltfLoader.setDracoLoader(params.dracoLoader);
64
65
 
65
66
  const modelImpl = new DynamicModelImpl(scene);
66
67
  modelImpl.id = model.file.id;
@@ -62,8 +62,10 @@ export class GLTFFileDynamicLoader extends Loader {
62
62
  const scene = new Group();
63
63
 
64
64
  this.gltfLoader = new DynamicGltfLoader(this.viewer.camera, scene, this.viewer.renderer);
65
- this.gltfLoader.memoryLimit = this.viewer.options.memoryLimit;
66
- this.gltfLoader.visibleEdges = this.viewer.options.edgeModel;
65
+ this.gltfLoader.setMemoryLimit(this.viewer.options.memoryLimit);
66
+ this.gltfLoader.setVisibleEdges(this.viewer.options.edgeModel);
67
+ this.gltfLoader.setMaxConcurrentChunks(params.maxConcurrentChunks);
68
+ this.gltfLoader.setDracoLoader(params.dracoLoader);
67
69
 
68
70
  const modelImpl = new DynamicModelImpl(scene);
69
71
  modelImpl.id = params.modelId || this.extractFileName(file);
@@ -32,7 +32,6 @@ import {
32
32
  Raycaster,
33
33
  Vector2,
34
34
  Vector3,
35
- WebGLRenderer,
36
35
  } from "three";
37
36
 
38
37
  const DESKTOP_SNAP_DISTANCE = 10;
@@ -47,16 +46,16 @@ const _projection = new Vector3();
47
46
 
48
47
  export class Snapper {
49
48
  public camera: Camera;
50
- public renderer: WebGLRenderer;
49
+ public clippingPlanes: Plane[];
51
50
  public canvas: HTMLCanvasElement;
52
51
  public threshold: number;
53
52
  private raycaster: Raycaster;
54
53
  private detectRadiusInPixels: number;
55
54
  private edgesCache: WeakMap<any, EdgesGeometry>;
56
55
 
57
- constructor(camera: Camera, renderer: WebGLRenderer, canvas: HTMLCanvasElement) {
56
+ constructor(camera: Camera, clippingPlanes: Plane[], canvas: HTMLCanvasElement) {
58
57
  this.camera = camera;
59
- this.renderer = renderer;
58
+ this.clippingPlanes = clippingPlanes;
60
59
  this.canvas = canvas;
61
60
 
62
61
  this.threshold = 0.0001;
@@ -109,7 +108,7 @@ export class Snapper {
109
108
  let intersects = this.raycaster.intersectObjects(objects, recursive);
110
109
 
111
110
  if (clip) {
112
- const clippingPlanes = this.renderer.clippingPlanes || [];
111
+ const clippingPlanes = this.clippingPlanes;
113
112
  clippingPlanes.forEach((plane: Plane) => {
114
113
  intersects = intersects.filter((intersect) => plane.distanceToPoint(intersect.point) >= 0);
115
114
  });
@@ -36,6 +36,7 @@ export class ModelImpl implements IModelImpl {
36
36
  private originalObjects: Set<Object3D>;
37
37
 
38
38
  constructor(scene: Object3D) {
39
+ this.id = "";
39
40
  this.scene = scene;
40
41
 
41
42
  this.handleToObjects = new Map();
@@ -275,7 +276,30 @@ export class ModelImpl implements IModelImpl {
275
276
  }
276
277
 
277
278
  getExtents(target: Box3): Box3 {
278
- this.scene.traverseVisible((object) => target.expandByObject(object));
279
+ const _box = new Box3();
280
+
281
+ function expandByObject(object: any, target: Box3) {
282
+ if (!object.geometry) return;
283
+
284
+ object.updateWorldMatrix(false, false);
285
+
286
+ // Use object bounds for the BatchedMesh, InstancedMesh and SkinnedMesh, use geometry
287
+ // bounds for others. See Box3.expandByObject() for more details.
288
+
289
+ if (object.boundingBox !== undefined) {
290
+ if (object.boundingBox === null) object.computeBoundingBox();
291
+ _box.copy(object.boundingBox);
292
+ } else {
293
+ if (object.geometry.boundingBox === null) object.geometry.computeBoundingBox();
294
+ _box.copy(object.geometry.boundingBox);
295
+ }
296
+
297
+ _box.applyMatrix4(object.matrixWorld);
298
+
299
+ target.union(_box);
300
+ }
301
+
302
+ this.scene.traverseVisible((object) => expandByObject(object, target));
279
303
  return target;
280
304
  }
281
305
 
@@ -285,8 +309,8 @@ export class ModelImpl implements IModelImpl {
285
309
 
286
310
  getVisibleObjects(): Object3D[] {
287
311
  const objects = [];
288
- this.scene.traverseVisible((object) => objects.push(object));
289
- return objects.filter((object) => object.userData.handle);
312
+ this.scene.traverseVisible((object) => object.userData.handle && objects.push(object));
313
+ return objects;
290
314
  }
291
315
 
292
316
  getObjectsByHandles(handles: string | string[]): Object3D[] {
@@ -1,7 +0,0 @@
1
- import { Scene, WebGLRenderer } from "three";
2
- export declare class Helpers extends Scene {
3
- private oldAutoClear;
4
- private oldClippingPlanes;
5
- onBeforeRender(renderer: WebGLRenderer): void;
6
- onAfterRender(renderer: WebGLRenderer): void;
7
- }
@@ -1,245 +0,0 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2002-2026, Open Design Alliance (the "Alliance").
3
- // All rights reserved.
4
- //
5
- // This software and its documentation and related materials are owned by
6
- // the Alliance. The software may only be incorporated into application
7
- // programs owned by members of the Alliance, subject to a signed
8
- // Membership Agreement and Supplemental Software License Agreement with the
9
- // Alliance. The structure and organization of this software are the valuable
10
- // trade secrets of the Alliance and its suppliers. The software is also
11
- // protected by copyright law and international treaty provisions. Application
12
- // programs incorporating this software must include the following statement
13
- // with their copyright notices:
14
- //
15
- // This application incorporates Open Design Alliance software pursuant to a
16
- // license agreement with Open Design Alliance.
17
- // Open Design Alliance Copyright (C) 2002-2026 by Open Design Alliance.
18
- // All rights reserved.
19
- //
20
- // By use of this software, its documentation or related materials, you
21
- // acknowledge and accept the above terms.
22
- ///////////////////////////////////////////////////////////////////////////////
23
-
24
- import { AdditiveBlending, Color, HalfFloatType, ShaderMaterial, UniformsUtils, WebGLRenderTarget } from "three";
25
- import { Pass, FullScreenQuad } from "three/examples/jsm/postprocessing/Pass.js";
26
- import { CopyShader } from "three/examples/jsm/shaders/CopyShader.js";
27
-
28
- export class SSAARenderPass extends Pass {
29
- constructor(scenes, camera, clearColor = 0x000000, clearAlpha = 0) {
30
- super();
31
- this.scenes = Array.isArray(scenes) ? scenes : [scenes];
32
- this.camera = camera;
33
- this.sampleLevel = 2;
34
- this.unbiased = true;
35
- this.stencilBuffer = false;
36
- this.clearColor = clearColor;
37
- this.clearAlpha = clearAlpha;
38
-
39
- this._sampleRenderTarget = null;
40
- this._oldClearColor = new Color();
41
- this._copyUniforms = UniformsUtils.clone(CopyShader.uniforms);
42
-
43
- this._copyMaterial = new ShaderMaterial({
44
- uniforms: this._copyUniforms,
45
- vertexShader: CopyShader.vertexShader,
46
- fragmentShader: CopyShader.fragmentShader,
47
- transparent: true,
48
- depthTest: false,
49
- depthWrite: false,
50
- premultipliedAlpha: true,
51
- blending: AdditiveBlending,
52
- });
53
-
54
- this._fsQuad = new FullScreenQuad(this._copyMaterial);
55
- }
56
-
57
- dispose() {
58
- if (this._sampleRenderTarget) {
59
- this._sampleRenderTarget.dispose();
60
- this._sampleRenderTarget = null;
61
- }
62
-
63
- this._copyMaterial.dispose();
64
-
65
- this._fsQuad.dispose();
66
- }
67
-
68
- setSize(width, height) {
69
- if (this._sampleRenderTarget) this._sampleRenderTarget.setSize(width, height);
70
- }
71
-
72
- render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) {
73
- if (!this._sampleRenderTarget) {
74
- this._sampleRenderTarget = new WebGLRenderTarget(readBuffer.width, readBuffer.height, {
75
- type: HalfFloatType,
76
- stencilBuffer: this.stencilBuffer,
77
- });
78
- this._sampleRenderTarget.texture.name = "SSAAMultiRenderPass.sample";
79
- }
80
-
81
- const jitterOffsets = _JitterVectors[Math.max(0, Math.min(this.sampleLevel, 5))];
82
-
83
- const autoClear = renderer.autoClear;
84
- renderer.autoClear = false;
85
-
86
- renderer.getClearColor(this._oldClearColor);
87
- const oldClearAlpha = renderer.getClearAlpha();
88
-
89
- const baseSampleWeight = 1.0 / jitterOffsets.length;
90
- const roundingRange = 1 / 32;
91
- this._copyUniforms["tDiffuse"].value = this._sampleRenderTarget.texture;
92
-
93
- const viewOffset = {
94
- fullWidth: readBuffer.width,
95
- fullHeight: readBuffer.height,
96
- offsetX: 0,
97
- offsetY: 0,
98
- width: readBuffer.width,
99
- height: readBuffer.height,
100
- };
101
-
102
- const originalViewOffset = Object.assign({}, this.camera.view);
103
-
104
- if (originalViewOffset.enabled) Object.assign(viewOffset, originalViewOffset);
105
-
106
- for (let i = 0; i < jitterOffsets.length; i++) {
107
- const jitterOffset = jitterOffsets[i];
108
-
109
- if (this.camera.setViewOffset) {
110
- this.camera.setViewOffset(
111
- viewOffset.fullWidth,
112
- viewOffset.fullHeight,
113
-
114
- viewOffset.offsetX + jitterOffset[0] * 0.0625,
115
- viewOffset.offsetY + jitterOffset[1] * 0.0625, // 0.0625 = 1 / 16
116
-
117
- viewOffset.width,
118
- viewOffset.height
119
- );
120
- }
121
-
122
- let sampleWeight = baseSampleWeight;
123
-
124
- if (this.unbiased) {
125
- const uniformCenteredDistribution = -0.5 + (i + 0.5) / jitterOffsets.length;
126
- sampleWeight += roundingRange * uniformCenteredDistribution;
127
- }
128
-
129
- this._copyUniforms["opacity"].value = sampleWeight;
130
- renderer.setClearColor(this.clearColor, this.clearAlpha);
131
- renderer.setRenderTarget(this._sampleRenderTarget);
132
- renderer.clear();
133
-
134
- this.scenes.forEach((scene) => renderer.render(scene, this.camera));
135
-
136
- renderer.setRenderTarget(this.renderToScreen ? null : writeBuffer);
137
-
138
- if (i === 0) {
139
- renderer.setClearColor(0x000000, 0.0);
140
- renderer.clear();
141
- }
142
-
143
- this._fsQuad.render(renderer);
144
- }
145
-
146
- if (this.camera.setViewOffset && originalViewOffset.enabled) {
147
- this.camera.setViewOffset(
148
- originalViewOffset.fullWidth,
149
- originalViewOffset.fullHeight,
150
-
151
- originalViewOffset.offsetX,
152
- originalViewOffset.offsetY,
153
-
154
- originalViewOffset.width,
155
- originalViewOffset.height
156
- );
157
- } else if (this.camera.clearViewOffset) {
158
- this.camera.clearViewOffset();
159
- }
160
-
161
- renderer.autoClear = autoClear;
162
- renderer.setClearColor(this._oldClearColor, oldClearAlpha);
163
- }
164
- }
165
-
166
- // These jitter vectors are specified in integers because it is easier.
167
- // I am assuming a [-8,8) integer grid, but it needs to be mapped onto [-0.5,0.5)
168
- // before being used, thus these integers need to be scaled by 1/16.
169
- //
170
- // Sample patterns reference: https://msdn.microsoft.com/en-us/library/windows/desktop/ff476218%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
171
- const _JitterVectors = [
172
- [[0, 0]],
173
- [
174
- [4, 4],
175
- [-4, -4],
176
- ],
177
- [
178
- [-2, -6],
179
- [6, -2],
180
- [-6, 2],
181
- [2, 6],
182
- ],
183
- [
184
- [1, -3],
185
- [-1, 3],
186
- [5, 1],
187
- [-3, -5],
188
- [-5, 5],
189
- [-7, -1],
190
- [3, 7],
191
- [7, -7],
192
- ],
193
- [
194
- [1, 1],
195
- [-1, -3],
196
- [-3, 2],
197
- [4, -1],
198
- [-5, -2],
199
- [2, 5],
200
- [5, 3],
201
- [3, -5],
202
- [-2, 6],
203
- [0, -7],
204
- [-4, -6],
205
- [-6, 4],
206
- [-8, 0],
207
- [7, -4],
208
- [6, 7],
209
- [-7, -8],
210
- ],
211
- [
212
- [-4, -7],
213
- [-7, -5],
214
- [-3, -5],
215
- [-5, -4],
216
- [-1, -4],
217
- [-2, -2],
218
- [-6, -1],
219
- [-4, 0],
220
- [-7, 1],
221
- [-1, 2],
222
- [-6, 3],
223
- [-3, 3],
224
- [-7, 6],
225
- [-3, 6],
226
- [-5, 7],
227
- [-1, 7],
228
- [5, -7],
229
- [1, -6],
230
- [6, -5],
231
- [4, -4],
232
- [2, -3],
233
- [7, -2],
234
- [1, -1],
235
- [4, -1],
236
- [2, 1],
237
- [6, 2],
238
- [0, 4],
239
- [4, 4],
240
- [2, 5],
241
- [7, 5],
242
- [5, 6],
243
- [3, 7],
244
- ],
245
- ];