UNPKG

forge-convert-utils

Version:

Tools for converting Autodesk Forge file formats.

707 lines (706 loc) 31.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Writer = void 0; const path = __importStar(require("path")); const crypto_1 = __importDefault(require("crypto")); const fse = __importStar(require("fs-extra")); const util_1 = require("util"); const image_placeholders_1 = require("../common/image-placeholders"); const IMF = __importStar(require("../common/intermediate-format")); const MaxBufferSize = 5 << 20; const DefaultMaterial = { pbrMetallicRoughness: { baseColorFactor: [0.25, 0.25, 0.25, 1.0], metallicFactor: 0.0, roughnessFactor: 0.5 } }; function hasTextures(material) { var _a; return !!((_a = material === null || material === void 0 ? void 0 : material.maps) === null || _a === void 0 ? void 0 : _a.diffuse); } /** * Utility class for serializing parsed 3D content to local file system as glTF (2.0). */ class Writer { /** * Initializes the writer. * @param {IWriterOptions} [options={}] Additional writer options. */ constructor(options = {}) { this.bufferViewCache = new Map(); // Cache of existing buffer views, indexed by hash of the binary data they point to this.meshHashes = new Map(); // List of hashes of existing gltf.Mesh objects, used for deduplication this.bufferViewHashes = new Map(); // List of hashes of existing gltf.BufferView objects, used for deduplication this.accessorHashes = new Map(); // List of hashes of existing gltf.Accessor objects, used for deduplication this.pendingTasks = []; this.stats = { materialsDeduplicated: 0, meshesDeduplicated: 0, accessorsDeduplicated: 0, bufferViewsDeduplicated: 0 }; this.options = { maxBufferSize: (0, util_1.isNullOrUndefined)(options.maxBufferSize) ? MaxBufferSize : options.maxBufferSize, ignoreMeshGeometry: !!options.ignoreMeshGeometry, ignoreLineGeometry: !!options.ignoreLineGeometry, ignorePointGeometry: !!options.ignorePointGeometry, deduplicate: !!options.deduplicate, skipUnusedUvs: !!options.skipUnusedUvs, center: !!options.center, log: (options && options.log) || function (msg) { }, filter: options && options.filter || ((dbid, fragid) => true) }; // All these properties will be properly initialized in the 'reset' call this.manifest = {}; this.bufferStream = null; this.bufferSize = 0; this.baseDir = ''; this.activeSvfMaterials = []; } /** * Outputs scene into glTF. * @async * @param {IMF.IScene} imf Complete scene in intermediate, in-memory format. * @param {string} outputDir Path to output folder. */ async write(imf, outputDir) { this.reset(outputDir); const scene = this.createScene(imf); const scenes = this.manifest.scenes; scenes.push(scene); if (this.bufferStream) { const stream = this.bufferStream; this.pendingTasks.push(new Promise((resolve, reject) => { stream.on('finish', resolve); })); this.bufferStream.close(); this.bufferStream = null; this.bufferSize = 0; } await Promise.all(this.pendingTasks); // Remove empty attributes textures or images to avoid errors in glTF validation if (this.manifest.textures && this.manifest.textures.length === 0) delete this.manifest.textures; if (this.manifest.images && this.manifest.images.length === 0) delete this.manifest.images; const gltfPath = path.join(this.baseDir, 'output.gltf'); this.serializeManifest(this.manifest, gltfPath); this.options.log(`Closing gltf output: done`); this.options.log(`Stats: ${JSON.stringify(this.stats)}`); await this.postprocess(imf, gltfPath); } reset(outputDir) { this.baseDir = outputDir; this.manifest = { asset: { version: '2.0', generator: 'forge-convert-utils', copyright: '2019 (c) Autodesk' }, extensionsUsed: [ "KHR_texture_transform" ], buffers: [], bufferViews: [], accessors: [], meshes: [], materials: [], nodes: [], scenes: [], textures: [], images: [], scene: 0 }; this.bufferStream = null; this.bufferSize = 0; this.bufferViewCache.clear(); this.meshHashes = new Map(); this.bufferViewHashes = new Map(); this.accessorHashes = new Map(); this.pendingTasks = []; this.activeSvfMaterials = []; this.stats = { materialsDeduplicated: 0, meshesDeduplicated: 0, accessorsDeduplicated: 0, bufferViewsDeduplicated: 0 }; } async postprocess(imf, gltfPath) { } serializeManifest(manifest, outputPath) { fse.writeFileSync(outputPath, JSON.stringify(manifest, null, 4)); } createScene(imf) { fse.ensureDirSync(this.baseDir); let scene = { nodes: [] }; const manifestNodes = this.manifest.nodes; const manifestMaterials = this.manifest.materials; const rootNode = { children: [] }; // Root node with transform to glTF coordinate system const xformNode = { children: [] }; // Transform node with additional global transform (e.g., moving model to origin) scene.nodes.push(manifestNodes.push(rootNode) - 1); rootNode.children.push(manifestNodes.push(xformNode) - 1); // Setup transformation to glTF coordinate system const metadata = imf.getMetadata(); if (metadata['world up vector'] && metadata['world front vector'] && metadata['distance unit']) { const up = metadata['world up vector'].XYZ; const front = metadata['world front vector'].XYZ; const distanceUnit = metadata['distance unit'].value; if (up && front && distanceUnit) { const left = [ up[1] * front[2] - up[2] * front[1], up[2] * front[0] - up[0] * front[2], up[0] * front[1] - up[1] * front[0] ]; if (left[0] * left[0] + left[1] * left[1] + left[2] * left[2] > 0.0) { let scale = 1.0; switch (distanceUnit) { case 'centimeter': case 'cm': scale = 0.01; break; case 'millimeter': case 'mm': scale = 0.001; break; case 'foot': case 'ft': scale = 0.3048; break; case 'inch': case 'in': scale = 0.0254; break; default: // "meter" / "m" scale = 1.0; } rootNode.matrix = [ left[0] * scale, up[0] * scale, front[0] * scale, 0, left[1] * scale, up[1] * scale, front[1] * scale, 0, left[2] * scale, up[2] * scale, front[2] * scale, 0, 0, 0, 0, 1 ]; } else { console.warn('Could not compute world matrix, leaving it as identity...'); } } } // Setup translation to origin when enabled if (metadata['world bounding box'] && this.options.center) { const boundsMin = metadata['world bounding box'].minXYZ; const boundsMax = metadata['world bounding box'].maxXYZ; if (boundsMin && boundsMax) { let translation = [ -0.5 * (boundsMin[0] + boundsMax[0]), -0.5 * (boundsMin[1] + boundsMax[1]), -0.5 * (boundsMin[2] + boundsMax[2]) ]; xformNode.matrix = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, translation[0], translation[1], translation[2], 1 ]; } } const nodeIndices = xformNode.children; this.options.log(`Writing scene nodes...`); const { filter } = this.options; for (let i = 0, len = imf.getNodeCount(); i < len; i++) { const fragment = imf.getNode(i); // Currently we only support flat lists of objects, no hierarchies if (fragment.kind !== IMF.NodeKind.Object) { continue; } if (!filter(fragment.dbid, i)) { continue; } const material = imf.getMaterial(fragment.material); // Only output UVs if there are any textures or if the user specifically asked not to skip unused UVs const outputUvs = hasTextures(material) || !this.options.skipUnusedUvs; const node = this.createNode(fragment, imf, outputUvs); // Only output nodes that have a mesh if (!(0, util_1.isUndefined)(node.mesh)) { nodeIndices.push(manifestNodes.push(node) - 1); } } this.options.log(`Writing materials...`); if (this.options.deduplicate) { const hashes = []; const newMaterialIndices = new Uint16Array(imf.getMaterialCount()); for (const [i, activeMaterialID] of this.activeSvfMaterials.entries()) { const material = imf.getMaterial(activeMaterialID); const hash = this.computeMaterialHash(material); const match = hashes.indexOf(hash); if (match === -1) { // If this is a first occurrence of the hash in the array, output a new material newMaterialIndices[i] = manifestMaterials.length; manifestMaterials.push(this.createMaterial(material, imf)); hashes.push(hash); } else { // Otherwise skip the material, and record an index to the first match below this.options.log(`Skipping a duplicate material (hash: ${hash})`); newMaterialIndices[i] = match; this.stats.materialsDeduplicated++; } } // Update material indices in all mesh primitives for (const mesh of this.manifest.meshes) { for (const primitive of mesh.primitives) { if (!(0, util_1.isUndefined)(primitive.material)) { primitive.material = newMaterialIndices[primitive.material]; } } } } else { for (const activeMaterialID of this.activeSvfMaterials) { const material = imf.getMaterial(activeMaterialID); const mat = this.createMaterial(material, imf); manifestMaterials.push(mat); } } this.options.log(`Writing scene: done`); return scene; } createNode(fragment, imf, outputUvs) { let node = { name: fragment.dbid.toString() }; if (fragment.transform) { switch (fragment.transform.kind) { case IMF.TransformKind.Matrix: node.matrix = fragment.transform.elements; break; case IMF.TransformKind.Decomposed: if (fragment.transform.scale) { const s = fragment.transform.scale; node.scale = [s.x, s.y, s.z]; } if (fragment.transform.rotation) { const r = fragment.transform.rotation; node.rotation = [r.x, r.y, r.z, r.w]; } if (fragment.transform.translation) { const t = fragment.transform.translation; node.translation = [t.x, t.y, t.z]; } break; } } const geometry = imf.getGeometry(fragment.geometry); let mesh = undefined; switch (geometry.kind) { case IMF.GeometryKind.Mesh: mesh = this.createMeshGeometry(geometry, imf, outputUvs); break; case IMF.GeometryKind.Lines: mesh = this.createLineGeometry(geometry, imf); break; case IMF.GeometryKind.Points: mesh = this.createPointGeometry(geometry, imf); break; case IMF.GeometryKind.Empty: console.warn('Could not find mesh for fragment', fragment); break; } if (mesh && mesh.primitives.length > 0) { let materialID = this.activeSvfMaterials.indexOf(fragment.material); if (materialID === -1) { materialID = this.activeSvfMaterials.length; this.activeSvfMaterials.push(fragment.material); } for (const primitive of mesh.primitives) { primitive.material = materialID; } node.mesh = this.addMesh(mesh); } return node; } addMesh(mesh) { const meshes = this.manifest.meshes; const hash = this.computeMeshHash(mesh); const match = this.options.deduplicate ? this.meshHashes.get(hash) : undefined; if (match !== undefined) { this.options.log(`Skipping a duplicate mesh (${hash})`); this.stats.meshesDeduplicated++; return match; } else { if (this.options.deduplicate) { this.meshHashes.set(hash, this.meshHashes.size); } return meshes.push(mesh) - 1; } } createMeshGeometry(geometry, imf, outputUvs) { let mesh = { primitives: [] }; if (this.options.ignoreMeshGeometry) { return mesh; } // Output index buffer const indices = geometry.getIndices(); const indexBufferView = this.createBufferView(Buffer.from(indices.buffer, indices.byteOffset, indices.byteLength)); const indexBufferViewID = this.addBufferView(indexBufferView); const indexAccessor = this.createAccessor(indexBufferViewID, 5123, indexBufferView.byteLength / 2, 'SCALAR'); const indexAccessorID = this.addAccessor(indexAccessor); // Output vertex buffer const vertices = geometry.getVertices(); const positionBounds = this.computeBoundsVec3(vertices); // Compute bounds manually, just in case const positionBufferView = this.createBufferView(Buffer.from(vertices.buffer, vertices.byteOffset, vertices.byteLength)); const positionBufferViewID = this.addBufferView(positionBufferView); const positionAccessor = this.createAccessor(positionBufferViewID, 5126, positionBufferView.byteLength / 4 / 3, 'VEC3', positionBounds.min, positionBounds.max /*[fragmesh.min.x, fragmesh.min.y, fragmesh.min.z], [fragmesh.max.x, fragmesh.max.y, fragmesh.max.z]*/); const positionAccessorID = this.addAccessor(positionAccessor); // Output normals buffer let normalAccessorID = undefined; const normals = geometry.getNormals(); if (normals) { const normalBufferView = this.createBufferView(Buffer.from(normals.buffer, normals.byteOffset, normals.byteLength)); const normalBufferViewID = this.addBufferView(normalBufferView); const normalAccessor = this.createAccessor(normalBufferViewID, 5126, normalBufferView.byteLength / 4 / 3, 'VEC3'); normalAccessorID = this.addAccessor(normalAccessor); } // Output color buffer let colorAccessorID = undefined; const colors = geometry.getColors(); if (colors) { const colorBufferView = this.createBufferView(Buffer.from(colors.buffer, colors.byteOffset, colors.byteLength)); const colorBufferViewID = this.addBufferView(colorBufferView); const colorAccessor = this.createAccessor(colorBufferViewID, 5126, colorBufferView.byteLength / 4 / 4, 'VEC4'); colorAccessorID = this.addAccessor(colorAccessor); } // Output UV buffers let uvAccessorID = undefined; if (geometry.getUvChannelCount() > 0 && outputUvs) { const uvs = geometry.getUvs(0); const uvBufferView = this.createBufferView(Buffer.from(uvs.buffer, uvs.byteOffset, uvs.byteLength)); const uvBufferViewID = this.addBufferView(uvBufferView); const uvAccessor = this.createAccessor(uvBufferViewID, 5126, uvBufferView.byteLength / 4 / 2, 'VEC2'); uvAccessorID = this.addAccessor(uvAccessor); } mesh.primitives.push({ mode: 4, attributes: { POSITION: positionAccessorID, }, indices: indexAccessorID }); if (!(0, util_1.isUndefined)(normalAccessorID)) { mesh.primitives[0].attributes.NORMAL = normalAccessorID; } if (!(0, util_1.isUndefined)(colorAccessorID)) { mesh.primitives[0].attributes.COLOR_0 = colorAccessorID; } if (!(0, util_1.isUndefined)(uvAccessorID)) { mesh.primitives[0].attributes.TEXCOORD_0 = uvAccessorID; } return mesh; } createLineGeometry(geometry, imf) { let mesh = { primitives: [] }; if (this.options.ignoreLineGeometry) { return mesh; } // Output index buffer const indices = geometry.getIndices(); const indexBufferView = this.createBufferView(Buffer.from(indices.buffer, indices.byteOffset, indices.byteLength)); const indexBufferViewID = this.addBufferView(indexBufferView); const indexAccessor = this.createAccessor(indexBufferViewID, 5123, indexBufferView.byteLength / 2, 'SCALAR'); const indexAccessorID = this.addAccessor(indexAccessor); // Output vertex buffer const vertices = geometry.getVertices(); const positionBounds = this.computeBoundsVec3(vertices); const positionBufferView = this.createBufferView(Buffer.from(vertices.buffer, vertices.byteOffset, vertices.byteLength)); const positionBufferViewID = this.addBufferView(positionBufferView); const positionAccessor = this.createAccessor(positionBufferViewID, 5126, positionBufferView.byteLength / 4 / 3, 'VEC3', positionBounds.min, positionBounds.max); const positionAccessorID = this.addAccessor(positionAccessor); // Output color buffer let colorAccessorID = undefined; const colors = geometry.getColors(); if (colors) { const colorBufferView = this.createBufferView(Buffer.from(colors.buffer, colors.byteOffset, colors.byteLength)); const colorBufferViewID = this.addBufferView(colorBufferView); const colorAccessor = this.createAccessor(colorBufferViewID, 5126, colorBufferView.byteLength / 4 / 3, 'VEC3'); colorAccessorID = this.addAccessor(colorAccessor); } mesh.primitives.push({ mode: 1, attributes: { POSITION: positionAccessorID }, indices: indexAccessorID }); if (!(0, util_1.isUndefined)(colorAccessorID)) { mesh.primitives[0].attributes['COLOR_0'] = colorAccessorID; } return mesh; } createPointGeometry(geometry, imf) { let mesh = { primitives: [] }; if (this.options.ignorePointGeometry) { return mesh; } // Output vertex buffer const vertices = geometry.getVertices(); const positionBounds = this.computeBoundsVec3(vertices); const positionBufferView = this.createBufferView(Buffer.from(vertices.buffer, vertices.byteOffset, vertices.byteLength)); const positionBufferViewID = this.addBufferView(positionBufferView); const positionAccessor = this.createAccessor(positionBufferViewID, 5126, positionBufferView.byteLength / 4 / 3, 'VEC3', positionBounds.min, positionBounds.max); const positionAccessorID = this.addAccessor(positionAccessor); // Output color buffer let colorAccessorID = undefined; const colors = geometry.getColors(); if (colors) { const colorBufferView = this.createBufferView(Buffer.from(colors.buffer, colors.byteOffset, colors.byteLength)); const colorBufferViewID = this.addBufferView(colorBufferView); const colorAccessor = this.createAccessor(colorBufferViewID, 5126, colorBufferView.byteLength / 4 / 3, 'VEC3'); colorAccessorID = this.addAccessor(colorAccessor); } mesh.primitives.push({ mode: 0, attributes: { POSITION: positionAccessorID } }); if (!(0, util_1.isUndefined)(colorAccessorID)) { mesh.primitives[0].attributes['COLOR_0'] = colorAccessorID; } return mesh; } addBufferView(bufferView) { const bufferViews = this.manifest.bufferViews; const hash = this.computeBufferViewHash(bufferView); const match = this.options.deduplicate ? this.bufferViewHashes.get(hash) : undefined; if (match !== undefined) { this.options.log(`Skipping a duplicate buffer view (${hash})`); this.stats.bufferViewsDeduplicated++; return match; } else { if (this.options.deduplicate) { this.bufferViewHashes.set(hash, this.bufferViewHashes.size); } return bufferViews.push(bufferView) - 1; } } createBufferView(data) { const hash = this.computeBufferHash(data); const cache = this.bufferViewCache.get(hash); if (this.options.deduplicate && cache) { this.options.log(`Skipping a duplicate buffer (${hash})`); return cache; } const manifestBuffers = this.manifest.buffers; // Prepare new writable stream if needed if (this.bufferStream === null || this.bufferSize > this.options.maxBufferSize) { if (this.bufferStream) { const stream = this.bufferStream; this.pendingTasks.push(new Promise((resolve, reject) => { stream.on('finish', resolve); })); this.bufferStream.close(); this.bufferStream = null; this.bufferSize = 0; } const bufferUri = `${manifestBuffers.length}.bin`; manifestBuffers.push({ uri: bufferUri, byteLength: 0 }); const bufferPath = path.join(this.baseDir, bufferUri); this.bufferStream = fse.createWriteStream(bufferPath); } const bufferID = manifestBuffers.length - 1; const buffer = manifestBuffers[bufferID]; this.bufferStream.write(data); this.bufferSize += data.byteLength; const bufferView = { buffer: bufferID, byteOffset: buffer.byteLength, byteLength: data.byteLength }; buffer.byteLength += bufferView.byteLength; if (buffer.byteLength % 4 !== 0) { // Pad to 4-byte multiples const pad = 4 - buffer.byteLength % 4; this.bufferStream.write(new Uint8Array(pad)); this.bufferSize += pad; buffer.byteLength += pad; } if (this.options.deduplicate) { this.bufferViewCache.set(hash, bufferView); } return bufferView; } addAccessor(accessor) { const accessors = this.manifest.accessors; const hash = this.computeAccessorHash(accessor); const match = this.options.deduplicate ? this.accessorHashes.get(hash) : undefined; if (match !== undefined) { this.options.log(`Skipping a duplicate accessor (${hash})`); this.stats.accessorsDeduplicated++; return match; } else { if (this.options.deduplicate) { this.accessorHashes.set(hash, this.accessorHashes.size); } return accessors.push(accessor) - 1; } } createAccessor(bufferViewID, componentType, count, type, min, max) { const accessor = { bufferView: bufferViewID, componentType: componentType, count: count, type: type }; if (!(0, util_1.isUndefined)(min)) { accessor.min = min.map(Math.fround); } if (!(0, util_1.isUndefined)(max)) { accessor.max = max.map(Math.fround); } return accessor; } createMaterial(mat, imf) { var _a, _b; // console.log('writing material', mat) if (!mat) { return DefaultMaterial; } const diffuse = mat.diffuse; let material = { pbrMetallicRoughness: { baseColorFactor: [diffuse.x, diffuse.y, diffuse.z, 1.0], metallicFactor: mat.metallic, roughnessFactor: (mat.roughness > 1.0) ? 1.0 : mat.roughness } }; if (!(0, util_1.isUndefined)(mat.opacity) && mat.opacity < 1.0 && material.pbrMetallicRoughness.baseColorFactor) { material.alphaMode = 'BLEND'; material.pbrMetallicRoughness.baseColorFactor[3] = mat.opacity; } if (mat.maps) { const manifestTextures = this.manifest.textures; if (mat.maps.diffuse) { const textureID = manifestTextures.length; manifestTextures.push(this.createTexture(mat.maps.diffuse, imf)); material.pbrMetallicRoughness.baseColorTexture = { index: textureID, texCoord: 0, extensions: { "KHR_texture_transform": { scale: [(_a = mat.scale) === null || _a === void 0 ? void 0 : _a.x, (_b = mat.scale) === null || _b === void 0 ? void 0 : _b.y] } } }; } } return material; } createTexture(uri, imf) { const manifestImages = this.manifest.images; let imageID = manifestImages.findIndex(image => image.uri === uri); if (imageID === -1) { imageID = manifestImages.length; const normalizedUri = uri.toLowerCase().split(/[\/\\]/).join(path.sep); manifestImages.push({ uri: normalizedUri }); const filePath = path.join(this.baseDir, normalizedUri); fse.ensureDirSync(path.dirname(filePath)); let imageData = imf.getImage(normalizedUri); if (!imageData) { // Default to a placeholder image based on the extension switch (normalizedUri.substr(normalizedUri.lastIndexOf('.'))) { case '.jpg': case '.jpeg': imageData = image_placeholders_1.ImagePlaceholder.JPG; break; case '.png': imageData = image_placeholders_1.ImagePlaceholder.PNG; break; case '.bmp': imageData = image_placeholders_1.ImagePlaceholder.BMP; break; case '.gif': imageData = image_placeholders_1.ImagePlaceholder.GIF; break; default: throw new Error(`Unsupported image format for ${normalizedUri}`); } } fse.writeFileSync(filePath, imageData); } return { source: imageID }; } computeMeshHash(mesh) { return mesh.primitives.map(p => { return `${p.mode || ''}/${p.material || ''}/${p.indices}/${p.attributes['POSITION'] || ''}/${p.attributes['NORMAL'] || ''}/${p.attributes['TEXCOORD_0'] || ''}/${p.attributes['COLOR_0'] || ''}`; }).join('/'); } computeBufferViewHash(bufferView) { return `${bufferView.buffer}/${bufferView.byteLength}/${bufferView.byteOffset || ''}/${bufferView.byteStride || ''}`; } computeAccessorHash(accessor) { return `${accessor.type}/${accessor.componentType}/${accessor.count}/${accessor.bufferView || 'X'}`; } computeBufferHash(buffer) { const hash = crypto_1.default.createHash('md5'); hash.update(buffer); return hash.digest('hex'); } computeMaterialHash(material) { if (!material) { return 'null'; } const hash = crypto_1.default.createHash('md5'); hash.update(JSON.stringify(material)); // TODO return hash.digest('hex'); } computeBoundsVec3(array) { const min = [array[0], array[1], array[2]]; const max = [array[0], array[1], array[2]]; for (let i = 0; i < array.length; i += 3) { min[0] = Math.min(min[0], array[i]); max[0] = Math.max(max[0], array[i]); min[1] = Math.min(min[1], array[i + 1]); max[1] = Math.max(max[1], array[i + 1]); min[2] = Math.min(min[2], array[i + 2]); max[2] = Math.max(max[2], array[i + 2]); } return { min, max }; } } exports.Writer = Writer;