@loaders.gl/tile-converter 5.0.0-alpha.11 → 5.0.0-alpha.14
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/LICENSE +1 -1
- package/README.md +180 -4
- package/bin/converter-v5.js +2 -0
- package/dist/converter.min.cjs +172 -182
- package/dist/i3s-server/bin/i3s-server.min.cjs +113 -158
- package/dist/index.cjs +18832 -19228
- package/dist/index.js +19566 -19968
- package/dist/v5/adapters.cjs +2002 -230
- package/dist/v5/adapters.d.ts +20 -0
- package/dist/v5/adapters.js +2005 -214
- package/dist/v5/archive-source.d.ts +36 -0
- package/dist/v5/browser-tileset-source.d.ts +55 -11
- package/dist/v5/browser.cjs +18 -0
- package/dist/v5/browser.d.ts +15 -1
- package/dist/v5/browser.js +26 -0
- package/dist/v5/conversion-api.d.ts +34 -0
- package/dist/v5/converter-cli.d.ts +1 -0
- package/dist/v5/converter-cli.js +1230 -0
- package/dist/v5/core.cjs +547 -77
- package/dist/v5/core.d.ts +3 -1
- package/dist/v5/core.js +540 -70
- package/dist/v5/feature-arrow.d.ts +1 -1
- package/dist/v5/i3s-mesh-conversion.d.ts +5 -1
- package/dist/v5/i3s-mesh-source-features.d.ts +2 -2
- package/dist/v5/i3s-mesh-source-material.d.ts +1 -1
- package/dist/v5/i3s-mesh-source.d.ts +2 -2
- package/dist/v5/i3s-selection-source.d.ts +70 -0
- package/dist/v5/index.cjs +15 -0
- package/dist/v5/index.d.ts +13 -1
- package/dist/v5/index.js +22 -0
- package/dist/v5/mesh-conversion.d.ts +21 -3
- package/dist/v5/mesh-draco.d.ts +2 -2
- package/dist/v5/mesh-features.d.ts +2 -2
- package/dist/v5/mesh-hierarchy.d.ts +47 -0
- package/dist/v5/mesh-source-features.d.ts +6 -5
- package/dist/v5/mesh-source-schema.d.ts +12 -0
- package/dist/v5/mesh-source-texture.d.ts +3 -3
- package/dist/v5/mesh-source.d.ts +10 -7
- package/dist/v5/mesh-tileset-sink.d.ts +5 -0
- package/dist/v5/mesh.d.ts +1 -1
- package/dist/v5/node-cli.d.ts +9 -0
- package/dist/v5/node-conversion.d.ts +34 -0
- package/dist/v5/node-feature-schema.d.ts +3 -0
- package/dist/v5/node-i3s-source.d.ts +41 -0
- package/dist/v5/node-source.d.ts +47 -0
- package/dist/v5/node.cjs +913 -0
- package/dist/v5/node.d.ts +6 -0
- package/dist/v5/node.js +890 -0
- package/dist/v5/point-cloud-attributes.d.ts +19 -0
- package/dist/v5/point-cloud-conversion.d.ts +1 -1
- package/dist/v5/point-cloud-copc.d.ts +44 -0
- package/dist/v5/point-cloud-file.d.ts +83 -0
- package/dist/v5/point-cloud-potree.d.ts +47 -0
- package/dist/v5/point-cloud-source-encoder.d.ts +1 -1
- package/dist/v5/point-cloud-source.d.ts +1 -1
- package/dist/v5/point-cloud-tileset.d.ts +44 -0
- package/dist/v5/point-cloud.d.ts +1 -1
- package/dist/v5/resource-manifest.d.ts +10 -1
- package/dist/v5/selected-mesh-source.d.ts +53 -0
- package/dist/v5/spatial-conversion.d.ts +1 -1
- package/dist/v5/tileset-source.d.ts +1 -1
- package/package.json +25 -32
- package/src/index.ts +4 -1
- package/src/v4/3d-tiles-converter/3d-tiles-converter.ts +11 -12
- package/src/v4/3d-tiles-converter/helpers/3d-tiles-content-converter.ts +7 -3
- package/src/v4/3d-tiles-converter/helpers/i3s-obb-to-3d-tiles-obb.ts +2 -2
- package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +4 -6
- package/src/v4/3d-tiles-converter/helpers/texture-atlas.ts +1 -1
- package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +2 -2
- package/src/v4/3d-tiles-converter/json-templates/tileset.ts +58 -39
- package/src/v4/converter-cli.ts +10 -4
- package/src/v4/deps-installer/deps-installer.ts +3 -3
- package/src/v4/i3s-converter/helpers/attribute-metadata-info.ts +1 -1
- package/src/v4/i3s-converter/helpers/batch-ids-extensions.ts +9 -10
- package/src/v4/i3s-converter/helpers/conversion-gltf.ts +108 -0
- package/src/v4/i3s-converter/helpers/coordinate-converter.ts +6 -5
- package/src/v4/i3s-converter/helpers/create-scene-server-path.ts +5 -6
- package/src/v4/i3s-converter/helpers/feature-attributes.ts +4 -4
- package/src/v4/i3s-converter/helpers/geometry-attributes.ts +1 -1
- package/src/v4/i3s-converter/helpers/geometry-converter.ts +69 -77
- package/src/v4/i3s-converter/helpers/gltf-attributes.ts +58 -74
- package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +6 -54
- package/src/v4/i3s-converter/helpers/node-debug.ts +7 -3
- package/src/v4/i3s-converter/helpers/node-index-document.ts +11 -9
- package/src/v4/i3s-converter/helpers/node-pages.ts +3 -4
- package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +2 -2
- package/src/v4/i3s-converter/helpers/tileset-traversal.ts +1 -1
- package/src/v4/i3s-converter/i3s-converter.ts +29 -31
- package/src/v4/i3s-converter/json-templates/geometry-definitions.ts +71 -66
- package/src/v4/i3s-converter/json-templates/layers.ts +97 -132
- package/src/v4/i3s-converter/json-templates/metadata.ts +31 -21
- package/src/v4/i3s-converter/json-templates/node.ts +60 -74
- package/src/v4/i3s-converter/json-templates/scene-server.ts +36 -27
- package/src/v4/i3s-converter/json-templates/shared-resources.ts +62 -123
- package/src/v4/i3s-converter/json-templates/store.ts +39 -57
- package/src/v4/i3s-converter/types.ts +4 -4
- package/src/v4/i3s-server/controllers/slpk-controller.ts +3 -3
- package/src/v4/i3s-server/utils/create-scene-server.ts +3 -3
- package/src/v4/lib/utils/builder-input.ts +8 -0
- package/src/v4/lib/utils/conversion-dump.ts +5 -1
- package/src/v4/lib/utils/file-utils.ts +1 -1
- package/src/v4/lib/utils/hash-string.ts +21 -0
- package/src/v4/lib/utils/lod-conversion-utils.ts +3 -3
- package/src/v4/pgm-loader.ts +2 -2
- package/src/v5/adapters.ts +53 -0
- package/src/v5/archive-source.ts +235 -0
- package/src/v5/browser-tileset-source.ts +418 -48
- package/src/v5/browser.ts +46 -0
- package/src/v5/conversion-api.ts +184 -17
- package/src/v5/conversion-archive.ts +2 -2
- package/src/v5/converter-cli.ts +28 -0
- package/src/v5/core.ts +7 -0
- package/src/v5/feature-arrow.ts +2 -2
- package/src/v5/i3s-mesh-conversion.ts +41 -3
- package/src/v5/i3s-mesh-sink.ts +1 -1
- package/src/v5/i3s-mesh-source-features.ts +3 -3
- package/src/v5/i3s-mesh-source-material.ts +18 -11
- package/src/v5/i3s-mesh-source.ts +3 -2
- package/src/v5/i3s-selection-source.ts +298 -0
- package/src/v5/index.ts +41 -0
- package/src/v5/mesh-conversion.ts +24 -4
- package/src/v5/mesh-draco.ts +2 -2
- package/src/v5/mesh-features.ts +3 -3
- package/src/v5/mesh-hierarchy.ts +648 -0
- package/src/v5/mesh-source-features.ts +165 -51
- package/src/v5/mesh-source-schema.ts +140 -0
- package/src/v5/mesh-source-texture.ts +49 -19
- package/src/v5/mesh-source.ts +80 -49
- package/src/v5/mesh-tileset-archive.ts +1 -1
- package/src/v5/mesh-tileset-sink.ts +2 -2
- package/src/v5/mesh.ts +1 -1
- package/src/v5/node-cli.ts +201 -0
- package/src/v5/node-conversion.ts +362 -0
- package/src/v5/node-feature-schema.ts +155 -0
- package/src/v5/node-i3s-source.ts +181 -0
- package/src/v5/node-source.ts +328 -0
- package/src/v5/node.ts +16 -0
- package/src/v5/point-cloud-attributes.ts +113 -0
- package/src/v5/point-cloud-conversion.ts +1 -1
- package/src/v5/point-cloud-copc.ts +289 -0
- package/src/v5/point-cloud-file.ts +306 -0
- package/src/v5/point-cloud-potree.ts +134 -0
- package/src/v5/point-cloud-source-encoder.ts +1 -1
- package/src/v5/point-cloud-source.ts +1 -1
- package/src/v5/point-cloud-spatial.ts +2 -2
- package/src/v5/point-cloud-tileset.ts +137 -0
- package/src/v5/point-cloud.ts +3 -3
- package/src/v5/resource-fetcher.ts +4 -1
- package/src/v5/resource-manifest.ts +40 -5
- package/src/v5/selected-mesh-source.ts +384 -0
- package/src/v5/spatial-conversion.ts +90 -19
- package/src/v5/tileset-source.ts +2 -2
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {Matrix4} from '@math.gl/core';
|
|
6
|
+
import {get3DTilesSpatialReference} from '@loaders.gl/tiles-3d';
|
|
7
|
+
import {
|
|
8
|
+
TileConversionError,
|
|
9
|
+
createBoundedMemoryTileConversionSink,
|
|
10
|
+
createTiles3DConversionSpatialContext
|
|
11
|
+
} from '@loaders.gl/tile-converter/v5/core';
|
|
12
|
+
import type {BrowserTilesetConversionInspection} from '@loaders.gl/tile-converter/v5/core';
|
|
13
|
+
import {createMeshTileset} from './mesh-tileset-sink.js';
|
|
14
|
+
import type {MeshTilesetSink} from './mesh-tileset-sink.js';
|
|
15
|
+
|
|
16
|
+
/** A source content declaration whose non-geometric semantics require explicit mapping. */
|
|
17
|
+
type SourceContent = {
|
|
18
|
+
/** Relative or absolute content resource URI. */
|
|
19
|
+
uri?: string;
|
|
20
|
+
/** Legacy alias for the content URI. */
|
|
21
|
+
url?: string;
|
|
22
|
+
/** Unmapped per-content metadata. */
|
|
23
|
+
metadata?: unknown;
|
|
24
|
+
/** Unmapped dataset content-group association. */
|
|
25
|
+
group?: unknown;
|
|
26
|
+
/** Unmapped content extensions. */
|
|
27
|
+
extensions?: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** JSON fields requiring explicit qualification before hierarchy preservation. */
|
|
31
|
+
type SourceTile = Omit<
|
|
32
|
+
NonNullable<BrowserTilesetConversionInspection['tileset']['root']>,
|
|
33
|
+
'content' | 'contents' | 'children'
|
|
34
|
+
> & {
|
|
35
|
+
/** Single content declaration, exclusive with contents. */
|
|
36
|
+
content?: SourceContent;
|
|
37
|
+
/** Ordered 1.1 contents with the same qualification as single content. */
|
|
38
|
+
contents?: SourceContent[];
|
|
39
|
+
/** Effective refinement; the root must declare this policy. */
|
|
40
|
+
refine?: 'ADD' | 'REPLACE';
|
|
41
|
+
/** Children with the same explicit JSON shape. */
|
|
42
|
+
children?: SourceTile[];
|
|
43
|
+
/** Unsupported source metadata. */
|
|
44
|
+
metadata?: unknown;
|
|
45
|
+
/** Unsupported view-dependent traversal volume. */
|
|
46
|
+
viewerRequestVolume?: unknown;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Explicit document fields requiring hierarchy and spatial qualification. */
|
|
50
|
+
type HierarchyDocument = Omit<BrowserTilesetConversionInspection['tileset'], 'root'> & {
|
|
51
|
+
/** Root with hierarchy qualification fields. */
|
|
52
|
+
root: SourceTile;
|
|
53
|
+
/** Optional dataset-level metric error. */
|
|
54
|
+
geometricError?: number;
|
|
55
|
+
/** Only the already-qualified native CRS extension can be retained. */
|
|
56
|
+
extensions?: Record<string, unknown>;
|
|
57
|
+
/** Required source extensions. */
|
|
58
|
+
extensionsRequired?: string[];
|
|
59
|
+
/** Dataset metadata requires separate authoring policy. */
|
|
60
|
+
metadata?: unknown;
|
|
61
|
+
/** Content groups require separate authoring policy. */
|
|
62
|
+
groups?: unknown;
|
|
63
|
+
/** Feature schemas require separate authoring policy. */
|
|
64
|
+
schema?: unknown;
|
|
65
|
+
/** External feature schemas require separate authoring policy. */
|
|
66
|
+
schemaUri?: unknown;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Output node with a box in its own frame and relative child placements. */
|
|
70
|
+
interface OutputTile {
|
|
71
|
+
/** Placement relative to the parent, absolute at the root. */
|
|
72
|
+
transform: number[];
|
|
73
|
+
/** Conservative box enclosing converted subtree geometry. */
|
|
74
|
+
boundingVolume: {box: number[]};
|
|
75
|
+
/** Scaled source error plus the reconstruction allowance, in meters. */
|
|
76
|
+
geometricError: number;
|
|
77
|
+
/** Explicit effective source refinement. */
|
|
78
|
+
refine: 'ADD' | 'REPLACE';
|
|
79
|
+
/** Converted GLB, absent on contentless source ancestors. */
|
|
80
|
+
content?: {uri: string};
|
|
81
|
+
/** Ordered converted GLBs sharing this tile's coordinate frame. */
|
|
82
|
+
contents?: {uri: string}[];
|
|
83
|
+
/** Original immediate children in declaration order. */
|
|
84
|
+
children?: OutputTile[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A qualified source tile; indexes retain original child order, including contentless ancestors. */
|
|
88
|
+
interface HierarchyNode {
|
|
89
|
+
/** Indexes of this tile's immediate children. */
|
|
90
|
+
readonly children: number[];
|
|
91
|
+
/** Ordered source content placements, empty on contentless ancestors. */
|
|
92
|
+
readonly resourceIds: string[];
|
|
93
|
+
/** Source metric error in the baked ECEF frame. */
|
|
94
|
+
readonly geometricError: number;
|
|
95
|
+
/** Effective source refinement, including inheritance. */
|
|
96
|
+
readonly refine: 'ADD' | 'REPLACE';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A bounded tree with a metric dataset error and no omitted content. */
|
|
100
|
+
interface MeshHierarchy {
|
|
101
|
+
/** Parent-before-child nodes in declaration order. */
|
|
102
|
+
readonly nodes: HierarchyNode[];
|
|
103
|
+
/** Dataset-level source error in meters. */
|
|
104
|
+
readonly geometricError: number;
|
|
105
|
+
/** Native source coordinate epoch; outputs must retain this same frame. */
|
|
106
|
+
readonly coordinateEpoch?: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Input and retained-byte policy for complete explicit hierarchy conversion. */
|
|
110
|
+
export interface MeshHierarchyTilesetSinkOptions {
|
|
111
|
+
/** Expanded inspection (inspectNestedTilesets: true) used by the selected source; at most 32 document placements. */
|
|
112
|
+
readonly inspection: BrowserTilesetConversionInspection;
|
|
113
|
+
/** Every inspected content placement, with no duplicates or omissions; at most 64. */
|
|
114
|
+
readonly resourceIds: readonly string[];
|
|
115
|
+
/** Maximum combined GLB and final JSON bytes retained by the sink. */
|
|
116
|
+
readonly maxTotalBytes: number;
|
|
117
|
+
/** Finite nonnegative position error allowance in meters, added to each source error. */
|
|
118
|
+
readonly maxPositionError: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Qualifies a complete explicit tree of inspected document placements before any geometry is read. */
|
|
122
|
+
export function prepareMeshHierarchy(
|
|
123
|
+
inspection: BrowserTilesetConversionInspection,
|
|
124
|
+
resourceIds: readonly string[]
|
|
125
|
+
): MeshHierarchy {
|
|
126
|
+
/** Rejects hierarchy profiles whose semantics cannot be retained. */
|
|
127
|
+
const rejectHierarchy: (message: string) => never = message => {
|
|
128
|
+
throw new TileConversionError('MESH_HIERARCHY_UNSUPPORTED', message);
|
|
129
|
+
};
|
|
130
|
+
if (!inspection.documents?.length || inspection.documents.length > 32)
|
|
131
|
+
rejectHierarchy(
|
|
132
|
+
'Hierarchy preservation requires expanded metadata inspection of at most 32 explicit document placements'
|
|
133
|
+
);
|
|
134
|
+
if (
|
|
135
|
+
!resourceIds.length ||
|
|
136
|
+
resourceIds.length > 64 ||
|
|
137
|
+
new Set(resourceIds).size !== resourceIds.length ||
|
|
138
|
+
resourceIds.length !== inspection.resources.length ||
|
|
139
|
+
inspection.resources.some(resource => !resourceIds.includes(resource.resourceId))
|
|
140
|
+
)
|
|
141
|
+
rejectHierarchy(
|
|
142
|
+
'Hierarchy preservation requires every content placement, without duplicates (at most 64)'
|
|
143
|
+
);
|
|
144
|
+
const entryTransform = new Matrix4();
|
|
145
|
+
if (
|
|
146
|
+
inspection.documents[0].parentResourceId !== undefined ||
|
|
147
|
+
!Array.isArray(inspection.documents[0].parentTransform) ||
|
|
148
|
+
inspection.documents[0].parentTransform.length !== 16 ||
|
|
149
|
+
inspection.documents[0].parentTransform.some((value, index) => value !== entryTransform[index])
|
|
150
|
+
)
|
|
151
|
+
rejectHierarchy('The entry document must have identity parent placement');
|
|
152
|
+
const documents = inspection.documents.map(placement => placement.tileset as HierarchyDocument);
|
|
153
|
+
const document = documents[0];
|
|
154
|
+
const externalDocuments = new Map<string, number>();
|
|
155
|
+
for (let index = 1; index < inspection.documents.length; index++) {
|
|
156
|
+
const identifier = inspection.documents[index].parentResourceId;
|
|
157
|
+
if (!identifier || externalDocuments.has(identifier))
|
|
158
|
+
rejectHierarchy(
|
|
159
|
+
'Every external document requires a distinct inspected parent content placement'
|
|
160
|
+
);
|
|
161
|
+
externalDocuments.set(identifier, index);
|
|
162
|
+
}
|
|
163
|
+
for (const document of documents) {
|
|
164
|
+
if (
|
|
165
|
+
document.metadata ||
|
|
166
|
+
document.groups ||
|
|
167
|
+
document.schema ||
|
|
168
|
+
document.schemaUri ||
|
|
169
|
+
Object.keys(document.extensions ?? {}).some(name => name !== 'EXT_geospatial_crs') ||
|
|
170
|
+
(document.extensionsRequired ?? []).some((name: string) => name !== 'EXT_geospatial_crs')
|
|
171
|
+
)
|
|
172
|
+
rejectHierarchy(
|
|
173
|
+
'Dataset metadata and unmapped extensions require a separate hierarchy profile'
|
|
174
|
+
);
|
|
175
|
+
if (!['1.0', '1.1'].includes(document.asset?.version ?? ''))
|
|
176
|
+
rejectHierarchy('Hierarchy preservation requires a 3D Tiles 1.0 or 1.1 document');
|
|
177
|
+
if (
|
|
178
|
+
document.geometricError !== undefined &&
|
|
179
|
+
(typeof document.geometricError !== 'number' ||
|
|
180
|
+
!Number.isFinite(document.geometricError) ||
|
|
181
|
+
document.geometricError < 0)
|
|
182
|
+
)
|
|
183
|
+
rejectHierarchy('Dataset geometricError must be finite nonnegative meters');
|
|
184
|
+
}
|
|
185
|
+
const spatialReferences = documents.map(document =>
|
|
186
|
+
get3DTilesSpatialReference(document as Parameters<typeof get3DTilesSpatialReference>[0])
|
|
187
|
+
);
|
|
188
|
+
const spatialReference = createTiles3DConversionSpatialContext(
|
|
189
|
+
spatialReferences[0]
|
|
190
|
+
).spatialReference;
|
|
191
|
+
if (
|
|
192
|
+
spatialReference.status !== 'native' ||
|
|
193
|
+
spatialReference.sourceCrs !== 'EPSG:4978' ||
|
|
194
|
+
spatialReference.heightReference !== 'ellipsoidal' ||
|
|
195
|
+
spatialReference.axisOrder !== 'xyz' ||
|
|
196
|
+
spatialReference.verticalUnitScale !== 1 ||
|
|
197
|
+
spatialReference.units?.some(unit => unit !== 'meter')
|
|
198
|
+
)
|
|
199
|
+
rejectHierarchy(
|
|
200
|
+
'Hierarchy preservation requires native EPSG:4978 xyz meters and ellipsoidal heights'
|
|
201
|
+
);
|
|
202
|
+
for (const reference of spatialReferences.slice(1)) {
|
|
203
|
+
// Match selected-mesh decoding: only an absent, unambiguous declaration inherits the entry frame.
|
|
204
|
+
const inherited =
|
|
205
|
+
reference.crs.state === 'absent' &&
|
|
206
|
+
reference.coordinateEpoch === undefined &&
|
|
207
|
+
!reference.verticalCrs &&
|
|
208
|
+
!reference.warnings.length;
|
|
209
|
+
const context = createTiles3DConversionSpatialContext(inherited ? spatialReference : reference);
|
|
210
|
+
if (
|
|
211
|
+
context.spatialReference.status !== 'native' ||
|
|
212
|
+
context.spatialReference.sourceCrs !== spatialReference.sourceCrs ||
|
|
213
|
+
context.spatialReference.heightReference !== spatialReference.heightReference ||
|
|
214
|
+
context.spatialReference.axisOrder !== spatialReference.axisOrder ||
|
|
215
|
+
context.spatialReference.verticalUnitScale !== 1 ||
|
|
216
|
+
context.spatialReference.units?.some(unit => unit !== 'meter') ||
|
|
217
|
+
context.spatialReference.coordinateEpoch !== spatialReference.coordinateEpoch
|
|
218
|
+
)
|
|
219
|
+
rejectHierarchy('External document CRS and coordinate epoch must match the entry frame');
|
|
220
|
+
}
|
|
221
|
+
const nodes: HierarchyNode[] = [];
|
|
222
|
+
const visitedDocuments = new Set([0]);
|
|
223
|
+
const stack = [
|
|
224
|
+
{
|
|
225
|
+
tile: document.root,
|
|
226
|
+
documentIndex: 0,
|
|
227
|
+
prefix: '',
|
|
228
|
+
path: [] as number[],
|
|
229
|
+
parent: -1,
|
|
230
|
+
transform: new Matrix4(),
|
|
231
|
+
refine: undefined as 'ADD' | 'REPLACE' | undefined
|
|
232
|
+
}
|
|
233
|
+
];
|
|
234
|
+
while (stack.length) {
|
|
235
|
+
const {
|
|
236
|
+
tile,
|
|
237
|
+
path,
|
|
238
|
+
parent,
|
|
239
|
+
documentIndex,
|
|
240
|
+
prefix,
|
|
241
|
+
transform: parentTransform,
|
|
242
|
+
refine: inheritedRefine
|
|
243
|
+
} = stack.pop()!;
|
|
244
|
+
const document = documents[documentIndex];
|
|
245
|
+
if (nodes.length >= 256) rejectHierarchy('Hierarchy preservation supports at most 256 tiles');
|
|
246
|
+
if (
|
|
247
|
+
!tile ||
|
|
248
|
+
typeof tile !== 'object' ||
|
|
249
|
+
Array.isArray(tile) ||
|
|
250
|
+
tile.implicitTiling !== undefined ||
|
|
251
|
+
tile.extensions !== undefined ||
|
|
252
|
+
tile.metadata !== undefined ||
|
|
253
|
+
tile.viewerRequestVolume !== undefined ||
|
|
254
|
+
(tile.content !== undefined && tile.contents !== undefined) ||
|
|
255
|
+
(tile.contents !== undefined &&
|
|
256
|
+
(!Array.isArray(tile.contents) ||
|
|
257
|
+
!tile.contents.length ||
|
|
258
|
+
document.asset?.version !== '1.1')) ||
|
|
259
|
+
(tile.children !== undefined && !Array.isArray(tile.children))
|
|
260
|
+
)
|
|
261
|
+
rejectHierarchy(
|
|
262
|
+
'Hierarchy tiles require explicit children, exclusive content/contents declarations and no unmapped metadata, extensions or request volumes'
|
|
263
|
+
);
|
|
264
|
+
const refine = tile.refine ?? inheritedRefine;
|
|
265
|
+
if (refine !== 'ADD' && refine !== 'REPLACE')
|
|
266
|
+
rejectHierarchy('Root refinement must be explicit ADD or REPLACE; children may inherit');
|
|
267
|
+
const matrix = tile.transform;
|
|
268
|
+
if (
|
|
269
|
+
matrix !== undefined &&
|
|
270
|
+
(!Array.isArray(matrix) ||
|
|
271
|
+
matrix.length !== 16 ||
|
|
272
|
+
matrix.some((value: unknown) => typeof value !== 'number' || !Number.isFinite(value)) ||
|
|
273
|
+
matrix[3] !== 0 ||
|
|
274
|
+
matrix[7] !== 0 ||
|
|
275
|
+
matrix[11] !== 0 ||
|
|
276
|
+
matrix[15] !== 1)
|
|
277
|
+
)
|
|
278
|
+
rejectHierarchy('Hierarchy transforms must be finite affine matrices');
|
|
279
|
+
const transform = new Matrix4(parentTransform).multiplyRight(matrix ?? new Matrix4());
|
|
280
|
+
const columns = [0, 1, 2].map(column =>
|
|
281
|
+
[0, 1, 2].reduce((sum, row) => sum + Math.abs(transform[column * 4 + row]), 0)
|
|
282
|
+
);
|
|
283
|
+
const rows = [0, 1, 2].map(row =>
|
|
284
|
+
[0, 1, 2].reduce((sum, column) => sum + Math.abs(transform[column * 4 + row]), 0)
|
|
285
|
+
);
|
|
286
|
+
// 1.0 errors are already meters; 1.1 applies the composed transform's largest scale.
|
|
287
|
+
const scale =
|
|
288
|
+
document.asset?.version === '1.0'
|
|
289
|
+
? 1
|
|
290
|
+
: Math.sqrt(Math.max(...columns)) * Math.sqrt(Math.max(...rows));
|
|
291
|
+
const geometricError = (tile.geometricError ?? NaN) * scale;
|
|
292
|
+
if (
|
|
293
|
+
typeof tile.geometricError !== 'number' ||
|
|
294
|
+
(tile.geometricError ?? -1) < 0 ||
|
|
295
|
+
!Number.isFinite(geometricError) ||
|
|
296
|
+
Array.from(transform).some(value => !Number.isFinite(value))
|
|
297
|
+
)
|
|
298
|
+
rejectHierarchy('Every hierarchy tile must declare a finite nonnegative metric error');
|
|
299
|
+
const contents = tile.contents ?? (tile.content ? [tile.content] : []);
|
|
300
|
+
const identifiers = contents.map(
|
|
301
|
+
(_, contentIndex) =>
|
|
302
|
+
`${prefix}tile-${path.length ? path.join('-') : 'root'}-content-${contentIndex}`
|
|
303
|
+
);
|
|
304
|
+
const externalIndexes = identifiers.map(identifier => externalDocuments.get(identifier));
|
|
305
|
+
const externalIndex = externalIndexes.find(index => index !== undefined);
|
|
306
|
+
for (const [contentIndex, content] of contents.entries()) {
|
|
307
|
+
if (
|
|
308
|
+
!content ||
|
|
309
|
+
typeof content !== 'object' ||
|
|
310
|
+
Array.isArray(content) ||
|
|
311
|
+
content.metadata !== undefined ||
|
|
312
|
+
content.group !== undefined ||
|
|
313
|
+
content.extensions !== undefined
|
|
314
|
+
)
|
|
315
|
+
rejectHierarchy('Content metadata, groups and extensions require a separate mapping');
|
|
316
|
+
if (
|
|
317
|
+
externalIndexes[contentIndex] === undefined &&
|
|
318
|
+
!inspection.resources.some(
|
|
319
|
+
resource =>
|
|
320
|
+
resource.resourceId === identifiers[contentIndex] &&
|
|
321
|
+
resource.documentIndex === documentIndex &&
|
|
322
|
+
resource.uri === (content.uri ?? content.url) &&
|
|
323
|
+
resource.tilePath.length === path.length &&
|
|
324
|
+
resource.tilePath.every((value, index) => value === path[index])
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
rejectHierarchy('Hierarchy content must match the inspected placement');
|
|
328
|
+
}
|
|
329
|
+
const resourceId = identifiers[0];
|
|
330
|
+
if (externalIndex !== undefined) {
|
|
331
|
+
if (
|
|
332
|
+
contents.length !== 1 ||
|
|
333
|
+
tile.contents !== undefined ||
|
|
334
|
+
tile.children?.length ||
|
|
335
|
+
visitedDocuments.has(externalIndex)
|
|
336
|
+
)
|
|
337
|
+
rejectHierarchy(
|
|
338
|
+
'External references require a single content, no source children and a unique document placement'
|
|
339
|
+
);
|
|
340
|
+
visitedDocuments.add(externalIndex);
|
|
341
|
+
const placement = inspection.documents[externalIndex];
|
|
342
|
+
if (
|
|
343
|
+
!Array.isArray(placement.parentTransform) ||
|
|
344
|
+
placement.parentTransform.length !== 16 ||
|
|
345
|
+
placement.parentTransform.some((value, index) => value !== transform[index])
|
|
346
|
+
)
|
|
347
|
+
rejectHierarchy('External document placement must match its referencing tile transform');
|
|
348
|
+
const contentUri = tile.content?.uri ?? tile.content?.url;
|
|
349
|
+
if (
|
|
350
|
+
!contentUri ||
|
|
351
|
+
!new URL(contentUri, inspection.documents[documentIndex].url).pathname
|
|
352
|
+
.toLowerCase()
|
|
353
|
+
.endsWith('.json')
|
|
354
|
+
)
|
|
355
|
+
rejectHierarchy('External documents require an inspected .json content reference');
|
|
356
|
+
}
|
|
357
|
+
const index = nodes.length;
|
|
358
|
+
nodes.push({
|
|
359
|
+
children: [],
|
|
360
|
+
resourceIds: externalIndex === undefined ? identifiers : [],
|
|
361
|
+
geometricError: Math.max(
|
|
362
|
+
geometricError,
|
|
363
|
+
externalIndex === undefined ? 0 : (documents[externalIndex].geometricError ?? 0)
|
|
364
|
+
),
|
|
365
|
+
refine
|
|
366
|
+
});
|
|
367
|
+
if (parent !== -1) nodes[parent].children.push(index);
|
|
368
|
+
if (externalIndex !== undefined) {
|
|
369
|
+
stack.push({
|
|
370
|
+
tile: documents[externalIndex].root,
|
|
371
|
+
path: [],
|
|
372
|
+
parent: index,
|
|
373
|
+
documentIndex: externalIndex,
|
|
374
|
+
prefix: resourceId + '/',
|
|
375
|
+
transform,
|
|
376
|
+
refine: undefined
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (nodes.length + stack.length + (tile.children?.length ?? 0) > 256)
|
|
380
|
+
rejectHierarchy('Hierarchy preservation supports at most 256 tiles');
|
|
381
|
+
for (let child = (tile.children?.length ?? 0) - 1; child >= 0; child--)
|
|
382
|
+
stack.push({
|
|
383
|
+
tile: tile.children![child],
|
|
384
|
+
documentIndex,
|
|
385
|
+
prefix,
|
|
386
|
+
path: [...path, child],
|
|
387
|
+
parent: index,
|
|
388
|
+
transform,
|
|
389
|
+
refine
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
if (
|
|
393
|
+
visitedDocuments.size !== documents.length ||
|
|
394
|
+
nodes.reduce((count, node) => count + node.resourceIds.length, 0) !== resourceIds.length ||
|
|
395
|
+
nodes.some(node => !node.resourceIds.length && !node.children.length)
|
|
396
|
+
)
|
|
397
|
+
rejectHierarchy('Contentless leaves or unmatched content placements cannot be preserved');
|
|
398
|
+
const geometricError = document.geometricError ?? nodes[0].geometricError;
|
|
399
|
+
if (typeof geometricError !== 'number' || !Number.isFinite(geometricError) || geometricError < 0)
|
|
400
|
+
rejectHierarchy('Dataset geometricError must be finite nonnegative meters');
|
|
401
|
+
return {
|
|
402
|
+
nodes,
|
|
403
|
+
geometricError: Math.max(geometricError, nodes[0].geometricError),
|
|
404
|
+
coordinateEpoch: spatialReference.coordinateEpoch
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Writes a complete explicit 3D Tiles 1.1 hierarchy, preserving child order and ADD/REPLACE.
|
|
410
|
+
* Pair with createSelectedMeshConversionSource({preserveHierarchy: true}) and the mesh codec.
|
|
411
|
+
* Primitive placements become ordered contents on their original tile. Extraction bakes source
|
|
412
|
+
* placements; relative
|
|
413
|
+
* output transforms place rebased GLBs exactly once. Bounds enclose all converted subtree
|
|
414
|
+
* vertices. Source volumes, request volumes and unmapped metadata are not silently carried over.
|
|
415
|
+
* Errors follow source-version transform semantics plus the position allowance.
|
|
416
|
+
* Limits bound retained bytes and 256 tree nodes, not peak decoding or serialization memory.
|
|
417
|
+
*/
|
|
418
|
+
export function createMeshHierarchyTilesetSink(
|
|
419
|
+
options: MeshHierarchyTilesetSinkOptions
|
|
420
|
+
): MeshTilesetSink {
|
|
421
|
+
const hierarchy = prepareMeshHierarchy(options.inspection, options.resourceIds);
|
|
422
|
+
const extensions = structuredClone(
|
|
423
|
+
(options.inspection.tileset as {extensions?: Record<string, unknown>}).extensions
|
|
424
|
+
);
|
|
425
|
+
const extensionsRequired = structuredClone(
|
|
426
|
+
(options.inspection.tileset as {extensionsRequired?: string[]}).extensionsRequired
|
|
427
|
+
);
|
|
428
|
+
const {maxPositionError} = options;
|
|
429
|
+
if (
|
|
430
|
+
!Number.isFinite(maxPositionError) ||
|
|
431
|
+
maxPositionError < 0 ||
|
|
432
|
+
hierarchy.nodes.some(node => !Number.isFinite(node.geometricError + maxPositionError)) ||
|
|
433
|
+
!Number.isFinite(hierarchy.geometricError + maxPositionError)
|
|
434
|
+
)
|
|
435
|
+
throw new TileConversionError(
|
|
436
|
+
'MESH_GEOMETRIC_ERROR_INVALID',
|
|
437
|
+
'Hierarchy position allowance and resulting errors must be finite nonnegative meters'
|
|
438
|
+
);
|
|
439
|
+
const memory = createBoundedMemoryTileConversionSink(options);
|
|
440
|
+
const placements = new Map<
|
|
441
|
+
string,
|
|
442
|
+
{
|
|
443
|
+
/** Validated absolute placement for rebased GLB content. */
|
|
444
|
+
readonly transform: readonly number[];
|
|
445
|
+
/** Snapshot of the target origin. */
|
|
446
|
+
readonly origin: readonly number[];
|
|
447
|
+
/** Snapshot of encoded local bounds; payload buffers belong only to the memory sink. */
|
|
448
|
+
readonly localBoundingBox: readonly (readonly number[])[];
|
|
449
|
+
/** Stable relative output name. */
|
|
450
|
+
readonly uri: string;
|
|
451
|
+
}[]
|
|
452
|
+
>();
|
|
453
|
+
const completedContents = new Set<string>();
|
|
454
|
+
const primitiveIdentifiers = new Set<string>();
|
|
455
|
+
let state: 'open' | 'writing' | 'closed' = 'open';
|
|
456
|
+
let completed = false;
|
|
457
|
+
return {
|
|
458
|
+
/** Exposes files only after complete finalization. */
|
|
459
|
+
getFiles: () => (completed ? memory.getFiles() : []),
|
|
460
|
+
/** Retains ordered codec outputs and requires a terminal primitive for each content. */
|
|
461
|
+
async write(resource, signal) {
|
|
462
|
+
signal?.throwIfAborted();
|
|
463
|
+
const identifier = resource.contentResourceId;
|
|
464
|
+
const {index: primitiveIndex, last: lastPrimitive} = resource.contentPrimitive ?? {
|
|
465
|
+
index: 0,
|
|
466
|
+
last: true
|
|
467
|
+
};
|
|
468
|
+
if (
|
|
469
|
+
state !== 'open' ||
|
|
470
|
+
!identifier ||
|
|
471
|
+
completedContents.has(identifier) ||
|
|
472
|
+
primitiveIdentifiers.has(resource.id) ||
|
|
473
|
+
primitiveIdentifiers.size >= 64 ||
|
|
474
|
+
!Number.isSafeInteger(primitiveIndex) ||
|
|
475
|
+
primitiveIndex !== (placements.get(identifier)?.length ?? 0) ||
|
|
476
|
+
typeof lastPrimitive !== 'boolean' ||
|
|
477
|
+
!hierarchy.nodes.some(node => node.resourceIds.includes(identifier))
|
|
478
|
+
)
|
|
479
|
+
throw new TileConversionError(
|
|
480
|
+
'MESH_HIERARCHY_PLACEMENT_INVALID',
|
|
481
|
+
'Hierarchy writes require ordered, distinct primitives for declared contents before close (at most 64)'
|
|
482
|
+
);
|
|
483
|
+
if (resource.spatialReference.coordinateEpoch !== hierarchy.coordinateEpoch)
|
|
484
|
+
throw new TileConversionError(
|
|
485
|
+
'MESH_SOURCE_EPOCH_MISMATCH',
|
|
486
|
+
'Hierarchy output coordinate epoch must match its source'
|
|
487
|
+
);
|
|
488
|
+
// Reuse the owning sink's frame, precision and bounds qualification.
|
|
489
|
+
const tile = createMeshTileset(resource, maxPositionError).root;
|
|
490
|
+
const nodeIndex = hierarchy.nodes.findIndex(node => node.resourceIds.includes(identifier));
|
|
491
|
+
const node = hierarchy.nodes[nodeIndex];
|
|
492
|
+
const sibling = node.resourceIds
|
|
493
|
+
.map(identifier => placements.get(identifier)?.[0])
|
|
494
|
+
.find(Boolean);
|
|
495
|
+
if (sibling && sibling.origin.some((value, axis) => value !== resource.origin[axis]))
|
|
496
|
+
throw new TileConversionError(
|
|
497
|
+
'MESH_HIERARCHY_PLACEMENT_INVALID',
|
|
498
|
+
'Contents on one tile must share an encoded origin'
|
|
499
|
+
);
|
|
500
|
+
const suffix = node.resourceIds.length > 1 ? '-' + node.resourceIds.indexOf(identifier) : '';
|
|
501
|
+
const primitiveSuffix =
|
|
502
|
+
primitiveIndex > 0 || !lastPrimitive ? '-primitive-' + primitiveIndex : '';
|
|
503
|
+
const uri = `meshes/${nodeIndex}${suffix}${primitiveSuffix}.glb`;
|
|
504
|
+
state = 'writing';
|
|
505
|
+
await memory.write(
|
|
506
|
+
{resourceId: uri, parts: [resource.glb], contentType: 'model/gltf-binary'},
|
|
507
|
+
signal
|
|
508
|
+
);
|
|
509
|
+
const contentPlacements = placements.get(identifier) ?? [];
|
|
510
|
+
contentPlacements.push({
|
|
511
|
+
transform: [...tile.transform!],
|
|
512
|
+
origin: [...resource.origin],
|
|
513
|
+
localBoundingBox: resource.localBoundingBox.map(bound => [...bound]),
|
|
514
|
+
uri
|
|
515
|
+
});
|
|
516
|
+
placements.set(identifier, contentPlacements);
|
|
517
|
+
primitiveIdentifiers.add(resource.id);
|
|
518
|
+
if (lastPrimitive) completedContents.add(identifier);
|
|
519
|
+
state = 'open';
|
|
520
|
+
},
|
|
521
|
+
/** Publishes original topology only after every source content has completed. */
|
|
522
|
+
async finalize(report) {
|
|
523
|
+
if (
|
|
524
|
+
state !== 'open' ||
|
|
525
|
+
completedContents.size !==
|
|
526
|
+
hierarchy.nodes.reduce((count, node) => count + node.resourceIds.length, 0)
|
|
527
|
+
)
|
|
528
|
+
throw new TileConversionError(
|
|
529
|
+
'MESH_HIERARCHY_INCOMPLETE',
|
|
530
|
+
'Finalization requires every declared hierarchy content'
|
|
531
|
+
);
|
|
532
|
+
state = 'writing';
|
|
533
|
+
const tiles: OutputTile[] = new Array(hierarchy.nodes.length);
|
|
534
|
+
const bounds: number[][][] = new Array(hierarchy.nodes.length);
|
|
535
|
+
const transforms = hierarchy.nodes.map(node => {
|
|
536
|
+
const placement = placements.get(node.resourceIds[0])?.[0];
|
|
537
|
+
return placement ? new Matrix4(placement.transform) : new Matrix4();
|
|
538
|
+
});
|
|
539
|
+
for (let index = hierarchy.nodes.length - 1; index >= 0; index--) {
|
|
540
|
+
const node = hierarchy.nodes[index];
|
|
541
|
+
const placement = placements.get(node.resourceIds[0])?.[0];
|
|
542
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
543
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
544
|
+
const contributors = node.children.map(child => bounds[child]);
|
|
545
|
+
const nodePlacements = node.resourceIds.flatMap(identifier => placements.get(identifier)!);
|
|
546
|
+
for (const placement of nodePlacements) {
|
|
547
|
+
contributors.push(
|
|
548
|
+
placement.localBoundingBox.map((bound, boundIndex) =>
|
|
549
|
+
bound.map((value, axis) => {
|
|
550
|
+
// Outward padding covers ECEF addition and later origin subtraction, including
|
|
551
|
+
// sub-ULP local extents that would otherwise collapse to an empty absolute box.
|
|
552
|
+
const padding =
|
|
553
|
+
8 *
|
|
554
|
+
Number.EPSILON *
|
|
555
|
+
Math.max(
|
|
556
|
+
1,
|
|
557
|
+
Math.abs(placement.origin[axis]),
|
|
558
|
+
...placement.localBoundingBox.map(limit => Math.abs(limit[axis]))
|
|
559
|
+
);
|
|
560
|
+
return value + placement.origin[axis] + (boundIndex ? padding : -padding);
|
|
561
|
+
})
|
|
562
|
+
)
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
for (const [lower, upper] of contributors)
|
|
566
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
567
|
+
minimum[axis] = Math.min(minimum[axis], lower[axis]);
|
|
568
|
+
maximum[axis] = Math.max(maximum[axis], upper[axis]);
|
|
569
|
+
}
|
|
570
|
+
if ([...minimum, ...maximum].some(value => !Number.isFinite(value)))
|
|
571
|
+
throw new TileConversionError(
|
|
572
|
+
'MESH_TILESET_BOUNDS_INVALID',
|
|
573
|
+
'Hierarchy absolute bounds must remain finite'
|
|
574
|
+
);
|
|
575
|
+
bounds[index] = [minimum, maximum];
|
|
576
|
+
const inverse = new Matrix4(transforms[index]).invert();
|
|
577
|
+
const center = Array.from(
|
|
578
|
+
inverse.transformAsPoint(minimum.map((value, axis) => value / 2 + maximum[axis] / 2))
|
|
579
|
+
);
|
|
580
|
+
const halfSize = minimum.map((value, axis) => maximum[axis] / 2 - value / 2);
|
|
581
|
+
const halfAxes = [0, 1, 2].flatMap(axis =>
|
|
582
|
+
Array.from(
|
|
583
|
+
inverse.transformAsVector(
|
|
584
|
+
halfSize.map((value, coordinate) => (coordinate === axis ? value : 0))
|
|
585
|
+
)
|
|
586
|
+
)
|
|
587
|
+
);
|
|
588
|
+
if ([...center, ...halfAxes].some(value => !Number.isFinite(value)))
|
|
589
|
+
throw new TileConversionError(
|
|
590
|
+
'MESH_TILESET_BOUNDS_INVALID',
|
|
591
|
+
'Hierarchy local bounds must remain finite'
|
|
592
|
+
);
|
|
593
|
+
tiles[index] = {
|
|
594
|
+
transform: Array.from(transforms[index]),
|
|
595
|
+
boundingVolume: {box: [...center, ...halfAxes]},
|
|
596
|
+
geometricError: node.geometricError + maxPositionError,
|
|
597
|
+
refine: node.refine,
|
|
598
|
+
...(nodePlacements.length > 1
|
|
599
|
+
? {contents: nodePlacements.map(placement => ({uri: placement.uri}))}
|
|
600
|
+
: placement
|
|
601
|
+
? {content: {uri: placement.uri}}
|
|
602
|
+
: {}),
|
|
603
|
+
...(node.children.length
|
|
604
|
+
? {
|
|
605
|
+
children: node.children.map(child => {
|
|
606
|
+
tiles[child].transform = Array.from(
|
|
607
|
+
new Matrix4(inverse).multiplyRight(transforms[child])
|
|
608
|
+
);
|
|
609
|
+
if (tiles[child].transform.some(value => !Number.isFinite(value)))
|
|
610
|
+
throw new TileConversionError(
|
|
611
|
+
'MESH_TILESET_BOUNDS_INVALID',
|
|
612
|
+
'Relative hierarchy transforms must remain finite'
|
|
613
|
+
);
|
|
614
|
+
return tiles[child];
|
|
615
|
+
})
|
|
616
|
+
}
|
|
617
|
+
: {})
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
const json = JSON.stringify({
|
|
621
|
+
asset: {version: '1.1'},
|
|
622
|
+
...(extensions && Object.keys(extensions).length
|
|
623
|
+
? {extensions, extensionsUsed: Object.keys(extensions)}
|
|
624
|
+
: {}),
|
|
625
|
+
...(extensionsRequired?.length ? {extensionsRequired} : {}),
|
|
626
|
+
geometricError: hierarchy.geometricError + maxPositionError,
|
|
627
|
+
root: tiles[0]
|
|
628
|
+
});
|
|
629
|
+
await memory.write({
|
|
630
|
+
resourceId: 'tileset.json',
|
|
631
|
+
parts: [json],
|
|
632
|
+
contentType: 'application/json'
|
|
633
|
+
});
|
|
634
|
+
await memory.finalize(report);
|
|
635
|
+
state = 'closed';
|
|
636
|
+
completed = true;
|
|
637
|
+
},
|
|
638
|
+
/** Discards retained resources and hides all files after failure. */
|
|
639
|
+
async abort(error) {
|
|
640
|
+
state = 'closed';
|
|
641
|
+
completed = false;
|
|
642
|
+
placements.clear();
|
|
643
|
+
completedContents.clear();
|
|
644
|
+
primitiveIdentifiers.clear();
|
|
645
|
+
await memory.abort(error);
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|