From 062b6d645762d8225fdda32e06cbf379ecaa8737 Mon Sep 17 00:00:00 2001 From: David Gordon Date: Tue, 19 Sep 2023 22:53:11 -0700 Subject: [PATCH 01/16] turn model transform into a job --- .../src/utils/objectToCommandLineArgs.ts | 59 +++++++++ .../components/assets/CompressionPanel.tsx | 10 -- .../properties/ModelTransformProperties.tsx | 10 +- .../src/assets/classes/ModelTransform.ts | 8 ++ .../model-transform/model-transform.class.ts | 40 +++--- .../model-transform.helpers.ts | 124 +++++++++++++----- .../model-transform/model-transform.job.ts | 67 ++++++++++ 7 files changed, 252 insertions(+), 66 deletions(-) create mode 100644 packages/common/src/utils/objectToCommandLineArgs.ts create mode 100644 packages/server-core/src/assets/model-transform/model-transform.job.ts diff --git a/packages/common/src/utils/objectToCommandLineArgs.ts b/packages/common/src/utils/objectToCommandLineArgs.ts new file mode 100644 index 0000000000..3c2d7d96d8 --- /dev/null +++ b/packages/common/src/utils/objectToCommandLineArgs.ts @@ -0,0 +1,59 @@ +function castValue(value, type) { + switch (type.toLowerCase()) { + case 'number': + return parseFloat(value) + case 'boolean': + return value === 'true' + case 'string': + return value + default: + return value + } +} + +export function objectToArgs(obj: any, prefix = '') { + const args: string[] = [] + + for (const [key, value] of Object.entries(obj)) { + let newPrefix = prefix ? `${prefix}_${key}` : `${key}` + + if (Array.isArray(value)) { + value.forEach((item, index) => { + args.push(...objectToArgs(item, `${newPrefix}_${index}`)) + }) + } else if (value !== null && typeof value === 'object') { + args.push(...objectToArgs(value, newPrefix)) + } else { + const type = typeof value + args.push(`--${newPrefix}_${type}`, String(value)) + } + } + + return args +} + +export function argsToObject(args: string[]): any { + const obj: Record = {} + for (let i = 0; i < args.length; i += 2) { + const arg = args[i] + const value = args[i + 1] + + const keys: string[] = arg.slice(2).split('_') + const type = keys.pop() + const parsedKeys = keys.map((key, i) => (isNaN(Number.parseFloat(keys[i])) ? key.toLowerCase() : Number(key))) + + let current = obj + + parsedKeys.forEach((key, index) => { + if (index === keys.length - 1) { + current[key] = castValue(value, type) + } else { + if (current[key] === undefined) { + current[key] = typeof keys[index + 1] === 'number' ? [] : {} + } + current = current[key] + } + }) + } + return obj +} diff --git a/packages/editor/src/components/assets/CompressionPanel.tsx b/packages/editor/src/components/assets/CompressionPanel.tsx index 8d7b74002a..3f58be1766 100644 --- a/packages/editor/src/components/assets/CompressionPanel.tsx +++ b/packages/editor/src/components/assets/CompressionPanel.tsx @@ -26,7 +26,6 @@ Ethereal Engine. All Rights Reserved. import { t } from 'i18next' import React from 'react' -import { API } from '@etherealengine/client-core/src/API' import Button from '@etherealengine/client-core/src/common/components/Button' import Menu from '@etherealengine/client-core/src/common/components/Menu' import { uploadToFeathersService } from '@etherealengine/client-core/src/util/upload' @@ -123,15 +122,6 @@ export default function CompressionPanel({ openCompress.set(false) } - /** @todo */ - const compressContent = async () => { - const props = fileProperties.value - compressProperties.src.set(props.type === 'folder' ? `${props.url}/${props.key}` : props.url) - const compressedPath = await API.instance.client.service('ktx2-encode').create(compressProperties.value) - await onRefreshDirectory() - openCompress.set(false) - } - return ( >) => async () => { transforming.set(true) const modelSrc = modelState.src.value - const nuPath = await Engine.instance.api.service(modelTransformPath).create({ - src: modelSrc, - transformParameters: transformParms.value - }) + const nuPath = await Engine.instance.api.service(modelTransformPath).create(transformParms.value) transformHistory.set([modelSrc, ...transformHistory.value]) const [_, directoryToRefresh, fileName] = /.*\/(projects\/.*)\/([\w\d\s\-_.]*)$/.exec(nuPath)! await FileBrowserService.fetchFiles(directoryToRefresh) @@ -182,10 +179,7 @@ export default function ModelTransformProperties({ console.log('saved baked model') //perform gltf transform console.log('transforming model at ' + bakedPath + '...') - const transformedPath = await Engine.instance.api.service(modelTransformPath).create({ - src: bakedPath, - transformParameters: transformParms.value - }) + const transformedPath = await Engine.instance.api.service(modelTransformPath).create(transformParms.value) console.log('transformed model into ' + transformedPath) onChangeModel(transformedPath) } diff --git a/packages/engine/src/assets/classes/ModelTransform.ts b/packages/engine/src/assets/classes/ModelTransform.ts index 3c8c6a6edc..3be9d80d6a 100644 --- a/packages/engine/src/assets/classes/ModelTransform.ts +++ b/packages/engine/src/assets/classes/ModelTransform.ts @@ -101,6 +101,7 @@ export type ResourceTransforms = { } export type ModelTransformParameters = ExtractedImageTransformParameters & { + src: string dst: string resourceUri: string split: boolean @@ -108,6 +109,7 @@ export type ModelTransformParameters = ExtractedImageTransformParameters & { instance: boolean dedup: boolean flatten: boolean + join: { enabled: boolean options: JoinOptions @@ -117,27 +119,33 @@ export type ModelTransformParameters = ExtractedImageTransformParameters & { enabled: boolean options: PaletteOptions } + prune: boolean reorder: boolean resample: boolean + weld: { enabled: boolean tolerance: number } + meshoptCompression: { enabled: boolean options: GLTFPackOptions } + dracoCompression: { enabled: boolean options: DracoOptions } + modelFormat: 'glb' | 'gltf' resources: ResourceTransforms } export const DefaultModelTransformParameters: ModelTransformParameters = { + src: '', dst: '', resourceUri: '', modelFormat: 'gltf', diff --git a/packages/server-core/src/assets/model-transform/model-transform.class.ts b/packages/server-core/src/assets/model-transform/model-transform.class.ts index c96fff22ed..18481b30a6 100644 --- a/packages/server-core/src/assets/model-transform/model-transform.class.ts +++ b/packages/server-core/src/assets/model-transform/model-transform.class.ts @@ -23,27 +23,21 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ +import { ModelTransformParameters } from '@etherealengine/engine/src/assets/classes/ModelTransform' +import { Application } from '@etherealengine/server-core/declarations' import { ServiceInterface } from '@feathersjs/feathers' import appRootPath from 'app-root-path' import path from 'path' +import config from '../../appconfig' -import { ModelTransformParameters } from '@etherealengine/engine/src/assets/classes/ModelTransform' -import { Application } from '@etherealengine/server-core/declarations' - -import { RootParams } from '../../api/root-params' -import { transformModel } from './model-transform.helpers' - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ModelTransformParams extends RootParams { - src: string - transformParameters: ModelTransformParameters - filter?: string -} +import { BadRequest } from '@feathersjs/errors' +import { createExecutorJob } from '../../projects/project/project-helper' +import { getModelTransformJobBody, transformModel } from './model-transform.helpers' /** * A class for Model Transform service */ -export class ModelTransformService implements ServiceInterface { +export class ModelTransformService implements ServiceInterface { app: Application rootPath: string @@ -59,19 +53,29 @@ export class ModelTransformService implements ServiceInterface { + const createParams: ModelTransformParameters = data + if (!config.kubernetes.enabled) { + return transformModel(this.app, createParams) + } try { - const transformParms = createParams.transformParameters + const transformParms = createParams const [commonPath, extension] = this.processPath(createParams.src) const inPath = `${commonPath}.${extension}` const outPath = transformParms.dst ? `${commonPath.replace(/[^/]+$/, transformParms.dst)}.${extension}` : `${commonPath}-transformed.${extension}` const resourceUri = transformParms.resourceUri ?? '' - return await transformModel(this.app, { src: inPath, dst: outPath, resourceUri, parms: transformParms }) + const jobBody = await getModelTransformJobBody(this.app, createParams) + const jobLabelSelector = `etherealengine/jobName=${jobBody.metadata!.name},etherealengine/release=${ + process.env.RELEASE_NAME + },etherealengine/modelTransformer=true` + const jobFinishedPromise = createExecutorJob(this.app, jobBody, jobLabelSelector, 600) + await jobFinishedPromise + return } catch (e) { - console.error('error transforming model') - console.error(e) + console.log('error transforming model', e) + throw new BadRequest('error transforming model', e) } } } diff --git a/packages/server-core/src/assets/model-transform/model-transform.helpers.ts b/packages/server-core/src/assets/model-transform/model-transform.helpers.ts index 98dcbc8bf7..a15f5d351d 100644 --- a/packages/server-core/src/assets/model-transform/model-transform.helpers.ts +++ b/packages/server-core/src/assets/model-transform/model-transform.helpers.ts @@ -23,7 +23,11 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { Application } from '@feathersjs/koa/lib' +import { + ExtractedImageTransformParameters, + extractParameters, + ModelTransformParameters +} from '@etherealengine/engine/src/assets/classes/ModelTransform' import { BufferUtils, Document, @@ -37,6 +41,7 @@ import { } from '@gltf-transform/core' import { EXTMeshGPUInstancing, KHRTextureBasisu } from '@gltf-transform/extensions' import { dedup, draco, flatten, join, palette, partition, prune, reorder, weld } from '@gltf-transform/functions' +import * as k8s from '@kubernetes/client-node' import appRootPath from 'app-root-path' import { execFileSync } from 'child_process' import { createHash } from 'crypto' @@ -46,13 +51,11 @@ import path from 'path' import sharp from 'sharp' import { MathUtils } from 'three' -import { - ExtractedImageTransformParameters, - extractParameters, - ModelTransformParameters -} from '@etherealengine/engine/src/assets/classes/ModelTransform' - +import { objectToArgs } from '@etherealengine/common/src/utils/objectToCommandLineArgs' import { fileBrowserPath } from '@etherealengine/engine/src/schemas/media/file-browser.schema' +import { Application } from '../../../declarations' +import config from '../../appconfig' +import { getPodsData } from '../../cluster/pods/pods-helper' import { getContentType } from '../../util/fileUtils' import { EEMaterial } from '../extensions/EE_MaterialTransformer' import { EEResourceID } from '../extensions/EE_ResourceIDTransformer' @@ -327,8 +330,8 @@ function hashBuffer(buffer: Uint8Array): string { return hash.digest('hex') } -export async function transformModel(app: Application, args: ModelTransformArguments) { - const parms = args.parms +export async function transformModel(app: Application, args: ModelTransformParameters) { + const parms = args const serverDir = path.join(appRootPath.path, 'packages/server') const tmpDir = path.join(serverDir, 'tmp') const BASIS_U = path.join(appRootPath.path, 'packages/server/public/loader_decoders/basisu') @@ -404,19 +407,19 @@ export async function transformModel(app: Application, args: ModelTransformArgum let initialSrc = args.src /* Meshopt Compression */ - if (args.parms.meshoptCompression.enabled) { + if (args.meshoptCompression.enabled) { const segments = args.src.split('.') const ext = segments.pop() const base = segments.join('.') initialSrc = `${base}-meshopt.${ext}` let packArgs = `-i ${args.src} -o ${initialSrc} -noq ` - if (!args.parms.meshoptCompression.options.mergeMaterials) { + if (!args.meshoptCompression.options.mergeMaterials) { packArgs += `-km ` } - if (!args.parms.meshoptCompression.options.mergeNodes) { + if (!args.meshoptCompression.options.mergeNodes) { packArgs += `-kn ` } - if (args.parms.meshoptCompression.options.compression) { + if (args.meshoptCompression.options.compression) { packArgs += `-cc ` } execFileSync( @@ -434,17 +437,17 @@ export async function transformModel(app: Application, args: ModelTransformArgum /* ID unnamed resources */ unInstanceSingletons(document) - args.parms.split && (await split(document)) - args.parms.combineMaterials && (await combineMaterials(document)) - args.parms.instance && (await myInstance(document)) - args.parms.dedup && (await document.transform(dedup())) - args.parms.flatten && (await document.transform(flatten())) - args.parms.join.enabled && (await document.transform(join(args.parms.join.options))) - if (args.parms.palette.enabled) { + args.split && (await split(document)) + args.combineMaterials && (await combineMaterials(document)) + args.instance && (await myInstance(document)) + args.dedup && (await document.transform(dedup())) + args.flatten && (await document.transform(flatten())) + args.join.enabled && (await document.transform(join(args.join.options))) + if (args.palette.enabled) { removeUVsOnUntexturedMeshes(document) - await document.transform(palette(args.parms.palette.options)) + await document.transform(palette(args.palette.options)) } - args.parms.prune && (await document.transform(prune())) + args.prune && (await document.transform(prune())) /* Separate Instanced Geometry */ const instancedNodes = root @@ -456,11 +459,11 @@ export async function transformModel(app: Application, args: ModelTransformArgum }) /* PROCESS MESHES */ - if (args.parms.weld.enabled) { - await document.transform(weld({ tolerance: args.parms.weld.tolerance })) + if (args.weld.enabled) { + await document.transform(weld({ tolerance: args.weld.tolerance })) } - if (args.parms.reorder) { + if (args.reorder) { await document.transform( reorder({ encoder: MeshoptEncoder, @@ -470,8 +473,8 @@ export async function transformModel(app: Application, args: ModelTransformArgum } /* Draco Compression */ - if (args.parms.dracoCompression.enabled) { - await document.transform(draco(args.parms.dracoCompression.options)) + if (args.dracoCompression.enabled) { + await document.transform(draco(args.dracoCompression.options)) } /* /Draco Compression */ @@ -498,7 +501,7 @@ export async function transformModel(app: Application, args: ModelTransformArgum (resource) => resource.enabled && resource.resourceId === resourceId ) const mergedParms = { - ...parms, + ...args, ...(resourceParms ? extractParameters(resourceParms) : {}) } as ExtractedImageTransformParameters const fileName = toPath(texture) @@ -577,9 +580,9 @@ export async function transformModel(app: Application, args: ModelTransformArgum } let result if (parms.modelFormat === 'glb') { - const data = await io.writeBinary(document) + const data = Buffer.from(await io.writeBinary(document)) const [savePath, fileName] = fileUploadPath(args.dst) - result = await app.service(fileBrowserPath).patch(null, { + result = await app.service('file-browser').patch(null, { path: savePath, fileName, body: data, @@ -649,3 +652,64 @@ export async function transformModel(app: Application, args: ModelTransformArgum fs.existsSync(tmpDir) && (await execFileSync('rm', ['-R', tmpDir])) return result } + +export async function getModelTransformJobBody( + app: Application, + createParams: ModelTransformParameters +): Promise { + const apiPods = await getPodsData( + `app.kubernetes.io/instance=${config.server.releaseName},app.kubernetes.io/component=api`, + 'api', + 'Api', + app + ) + const image = apiPods.pods[0].containers.find((container) => container.name === 'etherealengine')!.image + + const command = [ + 'npx', + 'cross-env', + 'ts-node', + '--swc', + 'packages/server-core/src/assets/model-transform/model-transform.job.ts', + ...objectToArgs(createParams) + ] + + return { + metadata: { + name: `${process.env.RELEASE_NAME}-${createParams.src}-${createParams.dst}-transform`, + labels: { + 'etherealengine/modelTransformer': 'true', + 'etherealengine/transformSource': createParams.src, + 'etherealengine/transformDestination': createParams.dst, + 'etherealengine/release': process.env.RELEASE_NAME! + } + }, + spec: { + template: { + metadata: { + labels: { + 'etherealengine/modelTransformer': 'true', + 'etherealengine/transformSource': createParams.src, + 'etherealengine/transformDestination': createParams.dst, + 'etherealengine/release': process.env.RELEASE_NAME! + } + }, + spec: { + serviceAccountName: `${process.env.RELEASE_NAME}-etherealengine-api`, + containers: [ + { + name: `${process.env.RELEASE_NAME}-${createParams.src}-${createParams.dst}-transform`, + image, + imagePullPolicy: 'IfNotPresent', + command, + env: Object.entries(process.env).map(([key, value]) => { + return { name: key, value: value } + }) + } + ], + restartPolicy: 'Never' + } + } + } + } +} diff --git a/packages/server-core/src/assets/model-transform/model-transform.job.ts b/packages/server-core/src/assets/model-transform/model-transform.job.ts new file mode 100644 index 0000000000..041a75a158 --- /dev/null +++ b/packages/server-core/src/assets/model-transform/model-transform.job.ts @@ -0,0 +1,67 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import appRootPath from 'app-root-path' +import cli from 'cli' +import dotenv from 'dotenv-flow' + +import { argsToObject } from '@etherealengine/common/src/utils/objectToCommandLineArgs' +import { ModelTransformParameters } from '@etherealengine/engine/src/assets/classes/ModelTransform' +import { ServerMode } from '@etherealengine/server-core/src/ServerState' +import { createFeathersKoaApp } from '@etherealengine/server-core/src/createApp' +import { transformModel } from './model-transform.helpers' + +const modelTransformParameters: ModelTransformParameters = argsToObject(process.argv.slice(3)) + +dotenv.config({ + path: appRootPath.path, + silent: true +}) + +const db = { + username: process.env.MYSQL_USER ?? 'server', + password: process.env.MYSQL_PASSWORD ?? 'password', + database: process.env.MYSQL_DATABASE ?? 'etherealengine', + host: process.env.MYSQL_HOST ?? '127.0.0.1', + port: process.env.MYSQL_PORT ?? 3306, + dialect: 'mysql', + url: '' +} + +db.url = process.env.MYSQL_URL ?? `mysql://${db.username}:${db.password}@${db.host}:${db.port}/${db.database}` + +cli.enable('status') + +cli.main(async () => { + try { + const app = createFeathersKoaApp(ServerMode.API) + await app.setup() + await transformModel(app, modelTransformParameters) + cli.exit(0) + } catch (err) { + console.log(err) + cli.fatal(err) + } +}) From 964ab71f1d84f64f24c060a62f1cadb98abf8264 Mon Sep 17 00:00:00 2001 From: David Gordon Date: Tue, 19 Sep 2023 22:56:19 -0700 Subject: [PATCH 02/16] licensing --- .../src/utils/objectToCommandLineArgs.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/common/src/utils/objectToCommandLineArgs.ts b/packages/common/src/utils/objectToCommandLineArgs.ts index 3c2d7d96d8..68648e73e0 100644 --- a/packages/common/src/utils/objectToCommandLineArgs.ts +++ b/packages/common/src/utils/objectToCommandLineArgs.ts @@ -1,3 +1,28 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + function castValue(value, type) { switch (type.toLowerCase()) { case 'number': From 47c5575555d476e8bf4186af534130f1de78ad6f Mon Sep 17 00:00:00 2001 From: David Gordon Date: Thu, 21 Sep 2023 17:05:39 -0700 Subject: [PATCH 03/16] checkpoint --- .vscode/launch.json | 6 ++++++ .../properties/ModelTransformProperties.tsx | 3 ++- .../model-transform/model-transform.class.ts | 3 ++- scripts/delete-containers.sh | 16 ++++++++++++++ scripts/delete-database.sh | 21 +++++++++++++++++++ 5 files changed, 47 insertions(+), 2 deletions(-) create mode 100755 scripts/delete-containers.sh create mode 100755 scripts/delete-database.sh diff --git a/.vscode/launch.json b/.vscode/launch.json index f13755a218..5dcef09406 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -96,6 +96,12 @@ "request": "launch", "type": "node-terminal", }, + { + "command": "npm run dev-tabs", + "name": "npm run dev-tabs", + "request": "launch", + "type": "node-terminal", + }, { "command": "npm run prepare-database", "name": "npm run prepare-database", diff --git a/packages/editor/src/components/properties/ModelTransformProperties.tsx b/packages/editor/src/components/properties/ModelTransformProperties.tsx index 88217cff18..7f0cd323a7 100644 --- a/packages/editor/src/components/properties/ModelTransformProperties.tsx +++ b/packages/editor/src/components/properties/ModelTransformProperties.tsx @@ -133,7 +133,8 @@ export default function ModelTransformProperties({ (modelState: State>) => async () => { transforming.set(true) const modelSrc = modelState.src.value - const nuPath = await Engine.instance.api.service(modelTransformPath).create(transformParms.value) + await Engine.instance.api.service(modelTransformPath).create(transformParms.value) + const nuPath = modelSrc.replace(/\.glb$/, '-transformed.glb') transformHistory.set([modelSrc, ...transformHistory.value]) const [_, directoryToRefresh, fileName] = /.*\/(projects\/.*)\/([\w\d\s\-_.]*)$/.exec(nuPath)! await FileBrowserService.fetchFiles(directoryToRefresh) diff --git a/packages/server-core/src/assets/model-transform/model-transform.class.ts b/packages/server-core/src/assets/model-transform/model-transform.class.ts index 18481b30a6..2fddc26be6 100644 --- a/packages/server-core/src/assets/model-transform/model-transform.class.ts +++ b/packages/server-core/src/assets/model-transform/model-transform.class.ts @@ -55,7 +55,8 @@ export class ModelTransformService implements ServiceInterface { async create(data: any): Promise { const createParams: ModelTransformParameters = data - if (!config.kubernetes.enabled) { + console.log('config', config) + if (!config.kubernetes?.enabled) { return transformModel(this.app, createParams) } try { diff --git a/scripts/delete-containers.sh b/scripts/delete-containers.sh new file mode 100755 index 0000000000..81ae82f00b --- /dev/null +++ b/scripts/delete-containers.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# List all Docker container IDs +container_ids=$(docker ps -aq) + +# Check if there are any containers to delete +if [ -z "$container_ids" ]; then + echo "No Docker containers found." +else + # Delete each container by ID + for container_id in $container_ids; do + docker rm -f "$container_id" + echo "Deleted container: $container_id" + done + echo "All Docker containers have been deleted." +fi diff --git a/scripts/delete-database.sh b/scripts/delete-database.sh new file mode 100755 index 0000000000..bdfa7f0151 --- /dev/null +++ b/scripts/delete-database.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# MySQL username and password +DB_USER="server" +DB_PASSWORD="password" + +# Database name to drop +DB_NAME="etherealengine" + +# Change to the "packages/server-core" directory +cd packages/server-core + +# Run MySQL and provide the password +mysql -h 127.0.0.1 -u "$DB_USER" -p"$DB_PASSWORD" < Date: Wed, 11 Oct 2023 17:30:24 -0700 Subject: [PATCH 04/16] class adjustment --- .../src/assets/model-transform/model-transform.class.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server-core/src/assets/model-transform/model-transform.class.ts b/packages/server-core/src/assets/model-transform/model-transform.class.ts index 2fddc26be6..fc5237813c 100644 --- a/packages/server-core/src/assets/model-transform/model-transform.class.ts +++ b/packages/server-core/src/assets/model-transform/model-transform.class.ts @@ -71,7 +71,8 @@ export class ModelTransformService implements ServiceInterface { const jobLabelSelector = `etherealengine/jobName=${jobBody.metadata!.name},etherealengine/release=${ process.env.RELEASE_NAME },etherealengine/modelTransformer=true` - const jobFinishedPromise = createExecutorJob(this.app, jobBody, jobLabelSelector, 600) + const jobId = `model-transform-${inPath}-${outPath}-${resourceUri}` + const jobFinishedPromise = createExecutorJob(this.app, jobBody, jobLabelSelector, 600, jobId) await jobFinishedPromise return } catch (e) { From d07725cb3169f5caf9d83d5f127d56d717f79ba8 Mon Sep 17 00:00:00 2001 From: David Gordon Date: Fri, 13 Oct 2023 17:52:23 -0700 Subject: [PATCH 05/16] move model transform into engine package --- .../properties/ModelTransformProperties.tsx | 1 + .../compression/ModelTransformFunctions.ts | 640 ++++++++++++++++++ .../compression/ModelTransformLoader.ts | 90 +++ .../extensions/EE_MaterialTransformer.ts | 322 +++++++++ .../extensions/EE_ResourceIDTransformer.ts | 104 +++ .../extensions/MOZ_LightmapTransformer.ts | 154 +++++ .../model-transform/model-transform.class.ts | 3 - .../model-transform.helpers.ts | 20 +- 8 files changed, 1328 insertions(+), 6 deletions(-) create mode 100644 packages/engine/src/assets/compression/ModelTransformFunctions.ts create mode 100644 packages/engine/src/assets/compression/ModelTransformLoader.ts create mode 100644 packages/engine/src/assets/compression/extensions/EE_MaterialTransformer.ts create mode 100644 packages/engine/src/assets/compression/extensions/EE_ResourceIDTransformer.ts create mode 100644 packages/engine/src/assets/compression/extensions/MOZ_LightmapTransformer.ts diff --git a/packages/editor/src/components/properties/ModelTransformProperties.tsx b/packages/editor/src/components/properties/ModelTransformProperties.tsx index 7f0cd323a7..97c7717b0f 100644 --- a/packages/editor/src/components/properties/ModelTransformProperties.tsx +++ b/packages/editor/src/components/properties/ModelTransformProperties.tsx @@ -76,6 +76,7 @@ export default function ModelTransformProperties({ const transformParms = useHookstate({ ...DefaultModelTransformParameters, + src: modelState.src.value, modelFormat: modelState.src.value.endsWith('.gltf') ? 'gltf' : 'glb' }) diff --git a/packages/engine/src/assets/compression/ModelTransformFunctions.ts b/packages/engine/src/assets/compression/ModelTransformFunctions.ts new file mode 100644 index 0000000000..105f7ef963 --- /dev/null +++ b/packages/engine/src/assets/compression/ModelTransformFunctions.ts @@ -0,0 +1,640 @@ +import { getContentType } from '@etherealengine/common/src/utils/getContentType' + +import { + ExtractedImageTransformParameters, + extractParameters, + ModelTransformParameters +} from '@etherealengine/engine/src/assets/classes/ModelTransform' +import { + BufferUtils, + Document, + Format, + Buffer as glBuffer, + Material, + Mesh, + Node, + Primitive, + Texture +} from '@gltf-transform/core' +import { EXTMeshGPUInstancing } from '@gltf-transform/extensions' +import { dedup, draco, flatten, join, palette, partition, prune, reorder, weld } from '@gltf-transform/functions' +import appRootPath from 'app-root-path' +import { execFileSync } from 'child_process' +import { createHash } from 'crypto' +import fs from 'fs' +import { MeshoptEncoder } from 'meshoptimizer' +import path from 'path' +import { MathUtils } from 'three' + +import { fileBrowserPath } from '@etherealengine/engine/src/schemas/media/file-browser.schema' +import { Application } from '@feathersjs/feathers/lib/declarations' +import { EEMaterial } from './extensions/EE_MaterialTransformer' +import { EEResourceID } from './extensions/EE_ResourceIDTransformer' +import ModelTransformLoader from './ModelTransformLoader' + +/** + * + * @param doc + * @param batchExtension + * @param mesh + * @param count + * @returns + */ +const createBatch = (doc: Document, batchExtension: EXTMeshGPUInstancing, mesh: Mesh, count) => { + return mesh.listPrimitives().map((prim) => { + const buffer = prim.getAttribute('POSITION')?.getBuffer() ?? doc.createBuffer() + + const batchTranslation = doc + .createAccessor() + .setType('VEC3') + .setArray(new Float32Array(3 * count)) + .setBuffer(buffer) + const batchRotation = doc + .createAccessor() + .setType('VEC4') + .setArray(new Float32Array(4 * count)) + .setBuffer(buffer) + const batchScale = doc + .createAccessor() + .setType('VEC3') + .setArray(new Float32Array(3 * count)) + .setBuffer(buffer) + + return batchExtension + .createInstancedMesh() + .setAttribute('TRANSLATION', batchTranslation) + .setAttribute('ROTATION', batchRotation) + .setAttribute('SCALE', batchScale) + }) +} + +function pruneUnusedNodes(nodes: Node[], logger) { + let node: Node | undefined + let unusedNodes = 0 + while ((node = nodes.pop())) { + if ( + node.listChildren().length || + node.getCamera() || + node.getMesh() || + node.getSkin() || + node.listExtensions().length + ) { + continue + } + const nodeParent = node.getParentNode() as Node + if (nodeParent instanceof Node) { + nodes.push(nodeParent) + } + node.dispose() + unusedNodes++ + console.log(`Pruned ${unusedNodes} nodes.`) + } +} + +function removeUVsOnUntexturedMeshes(document: Document) { + document + .getRoot() + .listMeshes() + .map((mesh) => { + const prims = mesh.listPrimitives() + if (prims.length === 1) { + const prim = prims[0] + const material = prim.getMaterial() + if ( + material && + (material.getBaseColorTexture() || + material.getNormalTexture() || + material.getEmissiveTexture() || + material.getOcclusionTexture() || + material.getMetallicRoughnessTexture()) + ) { + return + } + prim.setAttribute('TEXCOORD_0', null) + prim.setAttribute('TEXCOORD_1', null) + } + }) +} + +const split = async (document: Document) => { + const root = document.getRoot() + const scene = root.listScenes()[0] + const toSplit = root.listNodes().filter((node) => { + const mesh = node.getMesh() + const prims = mesh?.listPrimitives() + return mesh && prims && prims.length > 1 + }) + const primMeshes = new Map() + toSplit.map((node) => { + const mesh = node.getMesh()! + const nuNodes: Node[] = [] + mesh.listPrimitives().map((prim, primIdx) => { + if (!primMeshes.has(prim)) { + primMeshes.set(prim, document.createMesh(mesh.getName() + '-' + primIdx).addPrimitive(prim)) + } else { + console.log('found cached prim') + } + const nuNode = document.createNode(node.getName() + '-' + primIdx).setMesh(primMeshes.get(prim)) + node.getSkin() && nuNode.setSkin(node.getSkin()) + ;(node.getParentNode() ?? scene).addChild(nuNode) + nuNode.setMatrix(node.getMatrix()) + nuNodes.push(nuNode) + }) + node.listChildren().map((child) => { + nuNodes[0]?.addChild(child) + }) + node.detach() + }) + toSplit.map((node) => { + const mesh = node.getMesh()! + mesh.listPrimitives().map((prim, primIdx) => { + if (primIdx > 0) { + mesh.removePrimitive(prim) + } + }) + node.setMesh(null) + }) +} + +const myInstance = async (document: Document) => { + const root = document.getRoot() + const scene = root.listScenes()[0] + const batchExtension = document.createExtension(EXTMeshGPUInstancing) + const meshes = root.listMeshes() + console.log('meshes:', meshes) + const nodes = root.listNodes().filter((node) => node.getMesh()) + const table = nodes.reduce( + (_table, node) => { + const mesh = node.getMesh() + const idx = meshes.findIndex((mesh2) => mesh?.equals(mesh2)) + _table[idx] = _table[idx] ?? [] + _table[idx].push(node) + return _table + }, + {} as Record + ) + console.log('table:', table) + const modifiedNodes = new Set() + Object.entries(table) + .filter(([_, _nodes]) => _nodes.length > 1) + .map(([meshIdx, _nodes]) => { + const mesh = meshes[meshIdx] + console.log('mesh:', mesh, 'nodes:', nodes) + const batches = createBatch(document, batchExtension, mesh, _nodes.length) + batches.map((batch) => { + const batchTranslate = batch.getAttribute('TRANSLATION')! + const batchRotate = batch.getAttribute('ROTATION')! + const batchScale = batch.getAttribute('SCALE')! + const batchNode = document.createNode().setMesh(mesh).setExtension('EXT_mesh_gpu_instancing', batch) + scene.addChild(batchNode) + _nodes.map((node, i) => { + batchTranslate.setElement(i, node.getWorldTranslation()) + batchRotate.setElement(i, node.getWorldRotation()) + batchScale.setElement(i, node.getWorldScale()) + node.setMesh(null) + modifiedNodes.add(node) + }) + }) + console.log('modified nodes: ', modifiedNodes) + pruneUnusedNodes([...modifiedNodes], document.getLogger()) + }) +} + +function unInstanceSingletons(document: Document) { + const root = document.getRoot() + root + .listNodes() + .filter((node) => (node.getExtension('EXT_mesh_gpu_instancing') as any)?.listAttributes()?.[0].getCount() === 1) + .map((node) => { + console.log('removed instanced singleton', node.getName()) + node.setExtension('EXT_mesh_gpu_instancing', null) //delete instancing + }) +} + +export type ModelTransformArguments = { + src: string + dst: string + resourceUri: string + parms: ModelTransformParameters +} + +export async function combineMaterials(document: Document) { + const root = document.getRoot() + const cache: Material[] = [] + console.log('combining materials...') + root.listMaterials().map((material) => { + const eeMat = material.getExtension('EE_material') + const dupe = cache.find((cachedMaterial) => { + const cachedEEMat = cachedMaterial.getExtension('EE_material') + if (eeMat !== null && cachedEEMat !== null) { + return ( + eeMat.prototype === cachedEEMat.prototype && + ((eeMat.args === cachedEEMat.args) === null || (cachedEEMat.args && eeMat.args?.equals(cachedEEMat.args))) + ) + } else return material.equals(cachedMaterial) + }) + if (dupe !== undefined) { + console.log('found duplicate material...') + let dupeCount = 0 + root + .listMeshes() + .flatMap((mesh) => mesh.listPrimitives()) + .map((prim) => { + if (prim.getMaterial() === material) { + prim.setMaterial(dupe) + dupeCount++ + } + }) + console.log('replaced ' + dupeCount + ' materials') + } else { + cache.push(material) + } + }) +} + +export async function combineMeshes(document: Document) { + const root = document.getRoot() + const prims = root.listMeshes().flatMap((mesh) => mesh.listPrimitives()) + const matMap = new Map() + for (const prim of prims) { + const material = prim.getMaterial() + if (material) { + if (!matMap.has(material)) { + matMap.set(material, []) + } + const matPrims = matMap.get(material) + matPrims?.push(prim) + } + } + const nuPrims = [...matMap.entries()].map(([material, prims]) => { + const nuPrim = document.createPrimitive() + nuPrim.setMaterial(material) + prims.map((prim) => { + prim.listSemantics().map((key) => { + const accessor = prim.getAttribute(key)! + let nuAttrib = nuPrim.getAttribute(key) + if (!nuAttrib) { + nuPrim.setAttribute(key, accessor) + nuAttrib = accessor + } else { + nuAttrib.setArray( + BufferUtils.concat([Uint8Array.from(nuAttrib.getArray()!), Uint8Array.from(accessor.getArray()!)]) + ) + } + }) + }) + return nuPrim + }) + root.listNodes().map((node) => { + if (node.getMesh()) { + node.setMesh(null) + } + }) + nuPrims.map((nuPrim) => { + root.listScenes()[0].addChild(document.createNode().setMesh(document.createMesh().addPrimitive(nuPrim))) + }) +} + +function hashBuffer(buffer: Uint8Array): string { + const hash = createHash('sha256') + hash.update(buffer) + return hash.digest('hex') +} + +export async function transformModel(app: Application, args: ModelTransformParameters) { + const parms = args + const serverDir = path.join(appRootPath.path, 'packages/server') + const tmpDir = path.join(serverDir, 'tmp') + const BASIS_U = path.join(appRootPath.path, 'packages/server/public/loader_decoders/basisu') + const GLTF_PACK = path.join(appRootPath.path, 'packages/server/public/loader_decoders/gltfpack') + const toTmp = (fileName) => { + return `${tmpDir}/${fileName}` + } + + /** + * + * @param {string} mimeType + * @returns + */ + const mimeToFileType = (mimeType) => { + switch (mimeType) { + case 'image/jpg': + case 'image/jpeg': + return 'jpg' + case 'image/png': + return 'png' + case 'image/ktx2': + return 'ktx2' + default: + return null + } + } + + const fileTypeToMime = (fileType) => { + switch (fileType) { + case 'jpg': + return 'image/jpg' + case 'png': + return 'image/png' + case 'ktx2': + return 'image/ktx2' + default: + return null + } + } + + const resourceName = /*'model-resources'*/ path.basename(args.src).slice(0, path.basename(args.src).lastIndexOf('.')) + const resourcePath = args.resourceUri + ? path.join(path.dirname(args.src), args.resourceUri) + : path.join(path.dirname(args.src), resourceName) + const projectRoot = path.join(appRootPath.path, 'packages/projects') + + const toValidFilename = (name: string) => { + const result = name.replace(/[\s]/g, '-') + return result + } + let pathIndex = 0 + const toPath = (element: Texture | glBuffer, index?: number) => { + if (element instanceof Texture) { + if (element.getURI()) { + return path.basename(element.getURI()) + } else { + pathIndex++ + return `${toValidFilename(element.getName())}-${pathIndex}-.${mimeToFileType(element.getMimeType())}` + } + } else if (element instanceof glBuffer) { + return `buffer-${index}-${Date.now()}.bin` + } else throw new Error('invalid element to find path') + } + + const fileUploadPath = (fUploadPath: string) => { + const pathCheck = /.*\/packages\/projects\/(.*)\/([\w\d\s\-_.]*)$/ + const [_, savePath, fileName] = + pathCheck.exec(fUploadPath) ?? pathCheck.exec(path.join(path.dirname(args.src), fUploadPath))! + return [savePath, fileName] + } + + const { io } = await ModelTransformLoader() + + let initialSrc = args.src + /* Meshopt Compression */ + if (args.meshoptCompression.enabled) { + const segments = args.src.split('.') + const ext = segments.pop() + const base = segments.join('.') + initialSrc = `${base}-meshopt.${ext}` + let packArgs = `-i ${args.src} -o ${initialSrc} -noq ` + if (!args.meshoptCompression.options.mergeMaterials) { + packArgs += `-km ` + } + if (!args.meshoptCompression.options.mergeNodes) { + packArgs += `-kn ` + } + if (args.meshoptCompression.options.compression) { + packArgs += `-cc ` + } + execFileSync( + GLTF_PACK, + packArgs.split(/\s+/).filter((x) => !!x) + ) + } + /* /Meshopt Compression */ + + const document = await io.read(initialSrc) + + await MeshoptEncoder.ready + + const root = document.getRoot() + + /* ID unnamed resources */ + unInstanceSingletons(document) + args.split && (await split(document)) + args.combineMaterials && (await combineMaterials(document)) + args.instance && (await myInstance(document)) + args.dedup && (await document.transform(dedup())) + args.flatten && (await document.transform(flatten())) + args.join.enabled && (await document.transform(join(args.join.options))) + if (args.palette.enabled) { + removeUVsOnUntexturedMeshes(document) + await document.transform(palette(args.palette.options)) + } + args.prune && (await document.transform(prune())) + + /* Separate Instanced Geometry */ + const instancedNodes = root + .listNodes() + .filter((node) => !!node.getMesh()?.getExtension('EXT_mesh_gpu_instancing')) + .map((node) => [node, node.getParent()]) + instancedNodes.map(([node, parent]) => { + node instanceof Node && parent?.removeChild(node) + }) + + /* PROCESS MESHES */ + if (args.weld.enabled) { + await document.transform(weld({ tolerance: args.weld.tolerance })) + } + + if (args.reorder) { + await document.transform( + reorder({ + encoder: MeshoptEncoder, + target: 'performance' + }) + ) + } + + /* Draco Compression */ + if (args.dracoCompression.enabled) { + await document.transform(draco(args.dracoCompression.options)) + } + /* /Draco Compression */ + + /* /PROCESS MESHES */ + + /* Return Instanced Geometry to Scene Graph */ + instancedNodes.map(([node, parent]) => { + node instanceof Node && parent?.addChild(node) + }) + + /* PROCESS TEXTURES */ + if (parms.textureFormat !== 'default') { + const textures = root + .listTextures() + .filter( + (texture) => + (mimeToFileType(texture.getMimeType()) !== parms.textureFormat && !!texture.getSize()) || + texture.getSize()?.reduce((x, y) => Math.max(x, y))! > parms.maxTextureSize + ) + for (const texture of textures) { + const oldImg = texture.getImage() + const resourceId = texture.getExtension('EEResourceID')?.resourceId + const resourceParms = parms.resources.images.find( + (resource) => resource.enabled && resource.resourceId === resourceId + ) + const mergedParms = { + ...args, + ...(resourceParms ? extractParameters(resourceParms) : {}) + } as ExtractedImageTransformParameters + + const imgDoc = new Document() + const imgRoot = imgDoc.getRoot() + const nuTexture = imgDoc.createTexture(texture.getName()) + nuTexture.setImage(oldImg!) + nuTexture.setMimeType(texture.getMimeType()) + + /* + + Old command line processing for image resizing + + const fileName = toPath(texture) + const oldPath = toTmp(fileName) + const resizeExtension = mergedParms.textureFormat === 'ktx2' ? 'png' : mergedParms.textureFormat + const resizedPath = oldPath.replace( + new RegExp(`\\.${mimeToFileType(texture.getMimeType())}$`), + `-resized.${resizeExtension}` + ) + if (!fs.existsSync(tmpDir)) { + fs.mkdirSync(tmpDir) + } + fs.writeFileSync(oldPath, oldImg!) + const xResizedName = fileName.replace( + new RegExp(`\\.${mimeToFileType(texture.getMimeType())}$`), + `-resized.${mergedParms.textureFormat}` + ) + const nuFileName = fileName.replace( + new RegExp(`\\.${mimeToFileType(texture.getMimeType())}$`), + `-transformed.${mergedParms.textureFormat}` + ) + const nuPath = `${tmpDir}/${nuFileName}` + + try { + if (path.extname(oldPath) === '.ktx2') { + console.warn('cannot resize ktx2 compressed image at ' + oldPath) + continue + } + const img = await sharp(oldPath) + const metadata = await img.metadata() + let resizedDimension = 2 + while ( + resizedDimension * 2 <= + Math.min(mergedParms.maxTextureSize, Math.max(metadata.width, metadata.height)) + ) { + resizedDimension *= 2 + } + //resize the image to be no larger than the max texture size + await img + .resize(resizedDimension, resizedDimension, { + fit: 'fill' + }) + .toFormat(resizeExtension) + .toFile(resizedPath.replace(/\.[\w\d]+$/, `.${resizeExtension}`)) + console.log('handled image file ' + oldPath) + } catch (e) { + console.error('error while handling image ' + oldPath) + console.error(e) + }*/ + + /* + if (mergedParms.textureFormat === 'ktx2') { + //KTX2 Basisu Compression + document.createExtension(KHRTextureBasisu).setRequired(true) + + + const basisArgs = `-ktx2 ${resizedPath} -q ${mergedParms.textureCompressionQuality} ${ + mergedParms.textureCompressionType === 'uastc' ? '-uastc' : '' + } ${mergedParms.textureCompressionType === 'uastc' ? '-uastc_level ' + mergedParms.uastcLevel : ''} ${ + mergedParms.textureCompressionType === 'etc1' ? '-comp_level ' + mergedParms.compLevel : '' + } ${ + mergedParms.textureCompressionType === 'etc1' && mergedParms.maxCodebooks + ? '-max_endpoints 16128 -max_selectors 16128' + : '' + } ${mergedParms.linear ? '-linear' : ''} ${mergedParms.flipY ? '-y_flip' : ''} ${ + mergedParms.mipmap ? '-mipmap' : '' + }` + .split(/\s+/) + .filter((x) => !!x) + execFileSync(BASIS_U, basisArgs) + execFileSync('mv', [`${serverDir}/${xResizedName}`, nuPath]) + console.log('loaded ktx2 image ' + nuPath) + } else { + execFileSync('mv', [resizedPath, nuPath]) + } + texture.setImage(fs.readFileSync(nuPath)) + texture.setMimeType(fileTypeToMime(mergedParms.textureFormat) ?? texture.getMimeType()) + */ + } + } + let result + if (parms.modelFormat === 'glb') { + const data = Buffer.from(await io.writeBinary(document)) + const [savePath, fileName] = fileUploadPath(args.dst) + result = await app.service('file-browser').patch(null, { + path: savePath, + fileName, + body: data, + contentType: getContentType(args.dst) + }) + console.log('Handled glb file') + } else if (parms.modelFormat === 'gltf') { + ;[root.listBuffers(), root.listMeshes(), root.listTextures()].forEach((elements) => + elements.map((element: Texture | Mesh | glBuffer) => { + let elementName = '' + if (element instanceof Texture) { + elementName = hashBuffer(element.getImage()!) + } else if (element instanceof Mesh) { + elementName = hashBuffer(Uint8Array.from(element.listPrimitives()[0].getAttribute('POSITION')!.getArray()!)) + } else if (element instanceof glBuffer) { + const bufferPath = path.join(path.dirname(args.src), element.getURI()) + const bufferData = fs.readFileSync(bufferPath) + elementName = hashBuffer(bufferData) + } + element.setName(elementName) + }) + ) + document.transform( + partition({ + animations: true, + meshes: root.listMeshes().map((mesh) => mesh.getName()) + }) + ) + const { json, resources } = await io.writeJSON(document, { format: Format.GLTF, basename: resourceName }) + if (!fs.existsSync(resourcePath)) { + await app.service(fileBrowserPath).create(resourcePath.replace(projectRoot, '') as any) + } + json.images?.map((image) => { + const nuURI = path.join( + args.resourceUri ? args.resourceUri : resourceName, + `${image.name}.${mimeToFileType(image.mimeType)}` + ) + resources[nuURI] = resources[image.uri!] + delete resources[image.uri!] + image.uri = nuURI + }) + const defaultBufURI = MathUtils.generateUUID() + '.bin' + json.buffers?.map((buffer) => { + buffer.uri = path.join( + args.resourceUri ? args.resourceUri : resourceName, + path.basename(buffer.uri ?? defaultBufURI) + ) + }) + Object.keys(resources).map((uri) => { + const localPath = path.join(resourcePath, path.basename(uri)) + resources[localPath] = resources[uri] + delete resources[uri] + }) + const doUpload = (uri, data) => { + const [savePath, fileName] = fileUploadPath(uri) + return app.service(fileBrowserPath).patch(null, { + path: savePath, + fileName, + body: data, + contentType: getContentType(uri) + }) + } + await Promise.all(Object.entries(resources).map(([uri, data]) => doUpload(uri, data))) + result = await doUpload(args.dst.replace(/\.glb$/, '.gltf'), Buffer.from(JSON.stringify(json))) + console.log('Handled gltf file') + } + fs.existsSync(tmpDir) && (await execFileSync('rm', ['-R', tmpDir])) + return result +} diff --git a/packages/engine/src/assets/compression/ModelTransformLoader.ts b/packages/engine/src/assets/compression/ModelTransformLoader.ts new file mode 100644 index 0000000000..1ad9dcd570 --- /dev/null +++ b/packages/engine/src/assets/compression/ModelTransformLoader.ts @@ -0,0 +1,90 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import { NodeIO } from '@gltf-transform/core' +import { + EXTMeshGPUInstancing, + EXTMeshoptCompression, + KHRDracoMeshCompression, + KHRLightsPunctual, + KHRMaterialsClearcoat, + KHRMaterialsEmissiveStrength, + KHRMaterialsPBRSpecularGlossiness, + KHRMaterialsSpecular, + KHRMaterialsTransmission, + KHRMaterialsUnlit, + KHRMeshQuantization, + KHRTextureBasisu, + KHRTextureTransform +} from '@gltf-transform/extensions' +import fetch from 'cross-fetch' +import draco3d from 'draco3dgltf' +import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer' +import { FileLoader } from 'three' + +import { EEMaterialExtension } from './extensions/EE_MaterialTransformer' +import { MOZLightmapExtension } from './extensions/MOZ_LightmapTransformer' + +const transformHistory: string[] = [] +export default async function ModelTransformLoader() { + const io = new NodeIO(fetch, {}).setAllowHTTP(true) + io.registerExtensions([ + KHRLightsPunctual, + KHRMaterialsSpecular, + KHRMaterialsClearcoat, + KHRMaterialsPBRSpecularGlossiness, + KHRMaterialsUnlit, + KHRMaterialsEmissiveStrength, + KHRMaterialsTransmission, + KHRDracoMeshCompression, + EXTMeshGPUInstancing, + EXTMeshoptCompression, + KHRMeshQuantization, + KHRTextureBasisu, + KHRTextureTransform, + MOZLightmapExtension, + EEMaterialExtension + ]) + io.registerDependencies({ + 'meshopt.decoder': MeshoptDecoder, + 'meshopt.encoder': MeshoptEncoder, + 'draco3d.decoder': await draco3d.createDecoderModule(), + 'draco3d.encoder': await draco3d.createEncoderModule() + }) + return { + io, + load: async (src, noHistory = false) => { + const loader = new FileLoader() + loader.setResponseType('arraybuffer') + const data = (await loader.loadAsync(src)) as ArrayBuffer + if (!noHistory) transformHistory.push(src) + return io.readBinary(new Uint8Array(data)) + }, + //load: io.read, + get prev(): string | undefined { + return transformHistory.length > 0 ? transformHistory[0] : undefined + } + } +} diff --git a/packages/engine/src/assets/compression/extensions/EE_MaterialTransformer.ts b/packages/engine/src/assets/compression/extensions/EE_MaterialTransformer.ts new file mode 100644 index 0000000000..612d245ee1 --- /dev/null +++ b/packages/engine/src/assets/compression/extensions/EE_MaterialTransformer.ts @@ -0,0 +1,322 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import { + Extension, + ExtensionProperty, + IProperty, + Nullable, + Property, + PropertyType, + ReaderContext, + Texture, + TextureInfo, + WriterContext +} from '@gltf-transform/core' + +import { KHRTextureTransform } from '@gltf-transform/extensions' + +const EXTENSION_NAME = 'EE_material' + +interface IEEArgEntry extends IProperty { + type: string + contents: any +} + +interface IEEArgs extends IProperty { + [field: string]: any +} + +interface IEEMaterial extends IProperty { + uuid: string + name: string + prototype: string + args: EEMaterialArgs + plugins: string[] +} + +interface EEArgsDef { + type?: string + contents?: any +} + +interface EEArgs { + [field: string]: EEArgsDef +} + +interface EEMaterialDef { + uuid?: string + name?: string + prototype?: string + args?: EEArgs + plugins?: string[] +} + +export class EEMaterialArgs extends Property { + public declare propertyType: 'EEMaterialArgs' + public declare parentTypes: ['EEMaterial'] + protected init(): void { + this.propertyType = 'EEMaterialArgs' + this.parentTypes = ['EEMaterial'] + } + + protected getDefaults(): Nullable { + return Object.assign(super.getDefaults() as IProperty, { + extras: {} + }) + } + + public getProp(field: string) { + return this.get(field) + } + + public getPropRef(field: string) { + return this.getRef(field) + } + + public setProp(field: string, value: any) { + this.set(field, value) + } + public setPropRef(field: string, value: any) { + this.setRef(field, value) + } +} + +export class EEArgEntry extends Property { + public declare propertyType: 'EEMaterialArgEntry' + public declare parentTypes: ['EEMaterialArgs'] + protected init(): void { + this.propertyType = 'EEMaterialArgEntry' + this.parentTypes = ['EEMaterialArgs'] + } + + protected getDefaults(): Nullable { + return Object.assign(super.getDefaults() as IProperty, { + name: '', + type: '', + contents: null, + extras: {} + }) + } + + public get type() { + return this.get('type') + } + public set type(val: string) { + this.set('type', val) + } + public get contents() { + return this.get('contents') + } + public set contents(val) { + this.set('contents', val) + } +} + +export class EEMaterial extends ExtensionProperty { + public static EXTENSION_NAME = EXTENSION_NAME + public declare extensionName: typeof EXTENSION_NAME + public declare propertyType: 'EEMaterial' + public declare parentTypes: [PropertyType.MATERIAL] + + protected init(): void { + this.extensionName = EXTENSION_NAME + this.propertyType = 'EEMaterial' + this.parentTypes = [PropertyType.MATERIAL] + } + + protected getDefaults(): Nullable { + return Object.assign(super.getDefaults() as IProperty, { + uuid: '', + name: '', + prototype: '', + args: null, + plugins: [] + }) + } + + public get uuid() { + return this.get('uuid') + } + public set uuid(val: string) { + this.set('uuid', val) + } + public get name() { + return this.get('name') + } + public set name(val: string) { + this.set('name', val) + } + public get prototype() { + return this.get('prototype') + } + public set prototype(val: string) { + this.set('prototype', val) + } + public get args() { + return this.getRef('args') + } + public set args(val) { + this.setRef('args', val) + } + public get plugins() { + return this.get('plugins') + } + public set plugins(val: string[]) { + this.set('plugins', val) + } +} + +export class EEMaterialExtension extends Extension { + public readonly extensionName = EXTENSION_NAME + public static readonly EXTENSION_NAME = EXTENSION_NAME + + textureInfoMap: Map = new Map() + materialInfoMap: Map = new Map() + public read(readerContext: ReaderContext): this { + const materialDefs = readerContext.jsonDoc.json.materials || [] + let textureUuidIndex = 0 + let materialUuidIndex = 0 + materialDefs.map((def, idx) => { + if (def.extensions?.[EXTENSION_NAME]) { + const eeMaterial = new EEMaterial(this.document.getGraph()) + readerContext.materials[idx].setExtension(EXTENSION_NAME, eeMaterial) + + const eeDef = def.extensions[EXTENSION_NAME] as EEMaterialDef + + if (eeDef.uuid) { + eeMaterial.uuid = eeDef.uuid + } + if (eeDef.name) { + eeMaterial.name = eeDef.name + } + if (eeDef.prototype) { + eeMaterial.prototype = eeDef.prototype + } + if (eeDef.args) { + //eeMaterial.args = eeDef.args + const processedArgs = new EEMaterialArgs(this.document.getGraph()) + const materialArgsInfo = Object.keys(eeDef.args) + const materialUuid = materialUuidIndex.toString() + materialUuidIndex++ + this.materialInfoMap.set(materialUuid, materialArgsInfo) + processedArgs.setExtras({ uuid: materialUuid }) + Object.entries(eeDef.args).map(([field, argDef]) => { + const nuArgDef = new EEArgEntry(this.document.getGraph()) + nuArgDef.type = argDef.type! + if (argDef.type === 'texture') { + const value = argDef.contents + const texture = value ? readerContext.textures[value.index] : null + if (texture) { + const textureInfo = new TextureInfo(this.document.getGraph()) + readerContext.setTextureInfo(textureInfo, value) + if (texture && value.extensions?.KHR_texture_transform) { + const extensionData = value.extensions.KHR_texture_transform + const transform = new KHRTextureTransform(this.document).createTransform() + extensionData.offset && transform.setOffset(extensionData.offset) + extensionData.scale && transform.setScale(extensionData.scale) + extensionData.rotation && transform.setRotation(extensionData.rotation) + extensionData.texCoord && transform.setTexCoord(extensionData.texCoord) + textureInfo.setExtension('KHR_texture_transform', transform) + } + const uuid = textureUuidIndex.toString() + textureUuidIndex++ + texture.setExtras({ uuid }) + this.textureInfoMap.set(uuid, textureInfo) + } + nuArgDef.contents = texture + processedArgs.setPropRef(field, nuArgDef) + } else { + nuArgDef.contents = argDef.contents + processedArgs.setProp(field, nuArgDef) + } + }) + eeMaterial.args = processedArgs + } + if (eeDef.plugins) { + eeMaterial.plugins = eeDef.plugins + } + } + }) + return this + } + + public write(writerContext: WriterContext): this { + const json = writerContext.jsonDoc + this.document + .getRoot() + .listMaterials() + .map((material) => { + const eeMaterial = material.getExtension(EXTENSION_NAME) + if (eeMaterial) { + const matIdx = writerContext.materialIndexMap.get(material)! + const matDef = json.json.materials![matIdx] + const extensionDef: EEMaterialDef = { + uuid: eeMaterial.uuid, + name: eeMaterial.name, + prototype: eeMaterial.prototype, + plugins: eeMaterial.plugins + } + const matArgs = eeMaterial.args + if (matArgs) { + extensionDef.args = {} + const materialArgsInfo = this.materialInfoMap.get(matArgs.getExtras().uuid as string)! + materialArgsInfo.map((field) => { + let value: EEArgEntry + try { + value = matArgs.getPropRef(field) as EEArgEntry + } catch (e) { + value = matArgs.getProp(field) as EEArgEntry + } + if (value.type === 'texture') { + const argEntry = new EEArgEntry(this.document.getGraph()) + argEntry.type = 'texture' + const texture = value.contents as Texture + if (texture) { + const uuid = texture.getExtras().uuid as string + const textureInfo = this.textureInfoMap.get(uuid)! + argEntry.contents = writerContext.createTextureInfoDef(texture, textureInfo) + } else { + argEntry.contents = null + } + extensionDef.args![field] = { + type: argEntry.type, + contents: argEntry.contents + } + } else { + extensionDef.args![field] = { + type: value.type, + contents: value.contents + } + } + }) + } + matDef.extensions = matDef.extensions || {} + matDef.extensions[EXTENSION_NAME] = extensionDef + } + }) + return this + } +} diff --git a/packages/engine/src/assets/compression/extensions/EE_ResourceIDTransformer.ts b/packages/engine/src/assets/compression/extensions/EE_ResourceIDTransformer.ts new file mode 100644 index 0000000000..d5cf5cbf3b --- /dev/null +++ b/packages/engine/src/assets/compression/extensions/EE_ResourceIDTransformer.ts @@ -0,0 +1,104 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import { + Extension, + ExtensionProperty, + IProperty, + Nullable, + PropertyType, + ReaderContext, + WriterContext +} from '@gltf-transform/core' + +import { ResourceID } from '@etherealengine/engine/src/assets/classes/ModelTransform' + +const EXTENSION_NAME = 'EE_resourceID' + +interface IEEResourceID extends IProperty { + resourceId: ResourceID +} + +interface EEResourceIDDef { + resourceId?: ResourceID +} + +export class EEResourceID extends ExtensionProperty { + public static EXTENSION_NAME: string = EXTENSION_NAME + public declare extensionName: typeof EXTENSION_NAME + public declare propertyType: 'EEResourceID' + public declare parentTypes: [PropertyType.TEXTURE, PropertyType.PRIMITIVE] + + protected init(): void { + this.extensionName = EXTENSION_NAME + this.propertyType = 'EEResourceID' + this.parentTypes = [PropertyType.TEXTURE, PropertyType.PRIMITIVE] + } + + protected getDefaults(): Nullable { + return Object.assign(super.getDefaults() as IProperty, { + resourceId: '' as ResourceID + }) + } + + public get resourceId(): ResourceID { + return this.get('resourceId') + } + + public set resourceId(resourceId: ResourceID) { + this.set('resourceId', resourceId) + } +} + +export class EEResourceIDExtension extends Extension { + public readonly extensionName: string = EXTENSION_NAME + public static readonly EXTENSION_NAME: string = EXTENSION_NAME + + public read(readerContext: ReaderContext): this { + const jsonDoc = readerContext.jsonDoc + ;(jsonDoc.json.textures || []).map((def, idx) => { + if (def.extensions?.[EXTENSION_NAME]) { + const eeResourceID = new EEResourceID(this.document.getGraph()) + readerContext.textures[idx].setExtension(EXTENSION_NAME, eeResourceID) + const eeDef = def.extensions[EXTENSION_NAME] as EEResourceIDDef + eeDef.resourceId && (eeResourceID.resourceId = eeDef.resourceId) + } + }) + return this + } + + public write(writerContext: WriterContext): this { + const jsonDoc = writerContext.jsonDoc + this.document + .getRoot() + .listTextures() + .map((texture, index) => { + const eeResourceID = texture.getExtension(EXTENSION_NAME) as EEResourceID + if (!eeResourceID) return + const textureDef = jsonDoc.json.textures![index] + }) + return this + } +} diff --git a/packages/engine/src/assets/compression/extensions/MOZ_LightmapTransformer.ts b/packages/engine/src/assets/compression/extensions/MOZ_LightmapTransformer.ts new file mode 100644 index 0000000000..a0d816840c --- /dev/null +++ b/packages/engine/src/assets/compression/extensions/MOZ_LightmapTransformer.ts @@ -0,0 +1,154 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import { + Extension, + ExtensionProperty, + IProperty, + Nullable, + PropertyType, + ReaderContext, + WriterContext +} from '@gltf-transform/core' + +const EXTENSION_NAME = 'MOZ_lightmap' + +interface IMOZLightmap extends IProperty { + index: number + texCoord: number + intensity: number + extensions: Record | null +} + +export class MOZLightmap extends ExtensionProperty { + public static EXTENSION_NAME = EXTENSION_NAME + public declare extensionName: typeof EXTENSION_NAME + public declare propertyType: 'Lightmap' + public declare parentTypes: [PropertyType.MATERIAL] + + protected init(): void { + this.extensionName = EXTENSION_NAME + this.propertyType = 'Lightmap' + this.parentTypes = [PropertyType.MATERIAL] + } + + protected getDefaults(): Nullable { + return Object.assign(super.getDefaults() as IProperty, { + index: -1, + texCoord: 1, + intensity: 1, + extensions: {} + }) + } + + public get intensity() { + return this.get('intensity') + } + public set intensity(val: number) { + this.set('intensity', val) + } + + public get texCoord() { + return this.get('texCoord') + } + public set texCoord(val: number) { + this.set('texCoord', val) + } + + public get index() { + return this.get('index') + } + public set index(idx: number) { + this.set('index', idx) + } + + public get extensions(): Record | null { + return this.get('extensions') + } + public set extensions(exts: Record | null) { + this.set('extensions', exts) + } +} + +interface MozLightmapDef { + index?: number + texCoord?: number + intensity?: number + extensions?: Record +} + +export class MOZLightmapExtension extends Extension { + public readonly extensionName = EXTENSION_NAME + public static readonly EXTENSION_NAME = EXTENSION_NAME + + public read(readerContext: ReaderContext): this { + const materialDefs = readerContext.jsonDoc.json.materials || [] + const textureDefs = readerContext.jsonDoc.json.textures || [] + materialDefs.forEach((def, idx) => { + if (def.extensions && def.extensions[EXTENSION_NAME]) { + const mozLightmap = new MOZLightmap(this.document.getGraph()) + readerContext.materials[idx].setExtension(EXTENSION_NAME, mozLightmap) + + const lightmapDef = def.extensions[EXTENSION_NAME] as MozLightmapDef + + if (lightmapDef.intensity !== undefined) { + mozLightmap.intensity = lightmapDef.intensity + } + if (lightmapDef.index !== undefined) { + mozLightmap.index = lightmapDef.index + } + if (lightmapDef.texCoord !== undefined) { + mozLightmap.texCoord = lightmapDef.texCoord + } + if (lightmapDef.extensions !== undefined) { + mozLightmap.extensions = lightmapDef.extensions + } + } + }) + return this + } + + public write(writerContext: WriterContext): this { + const json = writerContext.jsonDoc + this.document + .getRoot() + .listMaterials() + .forEach((material) => { + const mozLightmap = material.getExtension(EXTENSION_NAME) + if (mozLightmap) { + const matIdx = writerContext.materialIndexMap.get(material)! + const matDef = json.json.materials![matIdx] + matDef.extensions = matDef.extensions ?? {} + matDef.extensions[EXTENSION_NAME] = { + intensity: mozLightmap.intensity, + index: mozLightmap.index, + texCoord: mozLightmap.texCoord, + extensions: mozLightmap.extensions + } as MozLightmapDef + } + }) + return this + } +} diff --git a/packages/server-core/src/assets/model-transform/model-transform.class.ts b/packages/server-core/src/assets/model-transform/model-transform.class.ts index ab7639383d..e4f50f7460 100644 --- a/packages/server-core/src/assets/model-transform/model-transform.class.ts +++ b/packages/server-core/src/assets/model-transform/model-transform.class.ts @@ -36,11 +36,8 @@ import { getModelTransformJobBody, transformModel } from './model-transform.help import { BadRequest } from '@feathersjs/errors/lib' import { KnexAdapterParams } from '@feathersjs/knex/lib' -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ModelTransformParams extends KnexAdapterParams { - src: string transformParameters: ModelTransformParameters - filter?: string } /** diff --git a/packages/server-core/src/assets/model-transform/model-transform.helpers.ts b/packages/server-core/src/assets/model-transform/model-transform.helpers.ts index a15f5d351d..f99176fac9 100644 --- a/packages/server-core/src/assets/model-transform/model-transform.helpers.ts +++ b/packages/server-core/src/assets/model-transform/model-transform.helpers.ts @@ -39,7 +39,7 @@ import { Primitive, Texture } from '@gltf-transform/core' -import { EXTMeshGPUInstancing, KHRTextureBasisu } from '@gltf-transform/extensions' +import { EXTMeshGPUInstancing } from '@gltf-transform/extensions' import { dedup, draco, flatten, join, palette, partition, prune, reorder, weld } from '@gltf-transform/functions' import * as k8s from '@kubernetes/client-node' import appRootPath from 'app-root-path' @@ -48,7 +48,6 @@ import { createHash } from 'crypto' import fs from 'fs' import { MeshoptEncoder } from 'meshoptimizer' import path from 'path' -import sharp from 'sharp' import { MathUtils } from 'three' import { objectToArgs } from '@etherealengine/common/src/utils/objectToCommandLineArgs' @@ -504,6 +503,17 @@ export async function transformModel(app: Application, args: ModelTransformParam ...args, ...(resourceParms ? extractParameters(resourceParms) : {}) } as ExtractedImageTransformParameters + + const imgDoc = new Document() + const imgRoot = imgDoc.getRoot() + const nuTexture = imgDoc.createTexture(texture.getName()) + nuTexture.setImage(oldImg!) + nuTexture.setMimeType(texture.getMimeType()) + + /* + + Old command line processing for image resizing + const fileName = toPath(texture) const oldPath = toTmp(fileName) const resizeExtension = mergedParms.textureFormat === 'ktx2' ? 'png' : mergedParms.textureFormat @@ -550,11 +560,14 @@ export async function transformModel(app: Application, args: ModelTransformParam } catch (e) { console.error('error while handling image ' + oldPath) console.error(e) - } + }*/ + /* if (mergedParms.textureFormat === 'ktx2') { //KTX2 Basisu Compression document.createExtension(KHRTextureBasisu).setRequired(true) + + const basisArgs = `-ktx2 ${resizedPath} -q ${mergedParms.textureCompressionQuality} ${ mergedParms.textureCompressionType === 'uastc' ? '-uastc' : '' } ${mergedParms.textureCompressionType === 'uastc' ? '-uastc_level ' + mergedParms.uastcLevel : ''} ${ @@ -576,6 +589,7 @@ export async function transformModel(app: Application, args: ModelTransformParam } texture.setImage(fs.readFileSync(nuPath)) texture.setMimeType(fileTypeToMime(mergedParms.textureFormat) ?? texture.getMimeType()) + */ } } let result From ca74be8381784007803e46520586274e4e543c7f Mon Sep 17 00:00:00 2001 From: David Gordon Date: Sun, 15 Oct 2023 21:14:12 -0700 Subject: [PATCH 06/16] checkpoint --- .../compression/ModelTransformFunctions.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/engine/src/assets/compression/ModelTransformFunctions.ts b/packages/engine/src/assets/compression/ModelTransformFunctions.ts index 105f7ef963..c55742f7e4 100644 --- a/packages/engine/src/assets/compression/ModelTransformFunctions.ts +++ b/packages/engine/src/assets/compression/ModelTransformFunctions.ts @@ -1,3 +1,28 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + import { getContentType } from '@etherealengine/common/src/utils/getContentType' import { From 085863b45f0b892d870a03565294702f8b2fc939 Mon Sep 17 00:00:00 2001 From: David Gordon Date: Mon, 16 Oct 2023 17:49:01 -0700 Subject: [PATCH 07/16] convert fs and path library calls to client code --- packages/common/src/utils/miscUtils.ts | 10 ++ .../properties/ModelTransformProperties.tsx | 28 ++++-- .../compression/ModelTransformFunctions.ts | 95 +++++++++---------- 3 files changed, 75 insertions(+), 58 deletions(-) diff --git a/packages/common/src/utils/miscUtils.ts b/packages/common/src/utils/miscUtils.ts index 4d6f041ad9..818b1bdeb9 100644 --- a/packages/common/src/utils/miscUtils.ts +++ b/packages/common/src/utils/miscUtils.ts @@ -70,3 +70,13 @@ export function arraysAreEqual(arr1: any[], arr2: any[]): boolean { return true } + +export function pathJoin(...parts: string[]): string { + const separator = '/' + const replace = new RegExp(separator + '{1,}', 'g') + return parts.join(separator).replace(replace, separator) +} + +export function baseName(path: string): string { + return path.split(/[\\/]/).pop()! +} diff --git a/packages/editor/src/components/properties/ModelTransformProperties.tsx b/packages/editor/src/components/properties/ModelTransformProperties.tsx index 97c7717b0f..0b1a69c7c6 100644 --- a/packages/editor/src/components/properties/ModelTransformProperties.tsx +++ b/packages/editor/src/components/properties/ModelTransformProperties.tsx @@ -48,6 +48,8 @@ import { getModelResources } from '@etherealengine/engine/src/scene/functions/lo import { useHookstate } from '@etherealengine/hyperflux' import { getMutableState, State } from '@etherealengine/hyperflux/functions/StateFunctions' +import { transformModel as clientSideTransformModel } from '@etherealengine/engine/src/assets/compression/ModelTransformFunctions' +import { modelTransformPath } from '@etherealengine/engine/src/schemas/assets/model-transform.schema' import exportGLTF from '../../functions/exportGLTF' import { SelectionState } from '../../services/SelectionServices' import BooleanInput from '../inputs/BooleanInput' @@ -58,8 +60,6 @@ import TexturePreviewInput from '../inputs/TexturePreviewInput' import CollapsibleBlock from '../layout/CollapsibleBlock' import GLTFTransformProperties from './GLTFTransformProperties' import LightmapBakerProperties from './LightmapBakerProperties' - -import { modelTransformPath } from '@etherealengine/engine/src/schemas/assets/model-transform.schema' import './ModelTransformProperties.css' export default function ModelTransformProperties({ @@ -73,7 +73,7 @@ export default function ModelTransformProperties({ const selectionState = useHookstate(getMutableState(SelectionState)) const transforming = useHookstate(false) const transformHistory = useHookstate([]) - + const isClientside = useHookstate(false) const transformParms = useHookstate({ ...DefaultModelTransformParameters, src: modelState.src.value, @@ -134,7 +134,11 @@ export default function ModelTransformProperties({ (modelState: State>) => async () => { transforming.set(true) const modelSrc = modelState.src.value - await Engine.instance.api.service(modelTransformPath).create(transformParms.value) + if (isClientside.value) { + await clientSideTransformModel(transformParms.value) + } else { + await Engine.instance.api.service(modelTransformPath).create(transformParms.value) + } const nuPath = modelSrc.replace(/\.glb$/, '-transformed.glb') transformHistory.set([modelSrc, ...transformHistory.value]) const [_, directoryToRefresh, fileName] = /.*\/(projects\/.*)\/([\w\d\s\-_.]*)$/.exec(nuPath)! @@ -207,9 +211,19 @@ export default function ModelTransformProperties({ onChange={(transformParms: ModelTransformParameters) => {}} /> {!transforming.value && ( - + <> + + { + isClientside.set(val) + }} + /> + + + )} {transforming.value &&

Transforming...

} {transformHistory.length > 0 && } diff --git a/packages/engine/src/assets/compression/ModelTransformFunctions.ts b/packages/engine/src/assets/compression/ModelTransformFunctions.ts index c55742f7e4..cf4f69b430 100644 --- a/packages/engine/src/assets/compression/ModelTransformFunctions.ts +++ b/packages/engine/src/assets/compression/ModelTransformFunctions.ts @@ -43,20 +43,16 @@ import { } from '@gltf-transform/core' import { EXTMeshGPUInstancing } from '@gltf-transform/extensions' import { dedup, draco, flatten, join, palette, partition, prune, reorder, weld } from '@gltf-transform/functions' -import appRootPath from 'app-root-path' -import { execFileSync } from 'child_process' import { createHash } from 'crypto' -import fs from 'fs' import { MeshoptEncoder } from 'meshoptimizer' -import path from 'path' -import { MathUtils } from 'three' +import { LoaderUtils, MathUtils } from 'three' +import { baseName, pathJoin } from '@etherealengine/common/src/utils/miscUtils' import { fileBrowserPath } from '@etherealengine/engine/src/schemas/media/file-browser.schema' -import { Application } from '@feathersjs/feathers/lib/declarations' +import { Engine } from '../../ecs/classes/Engine' import { EEMaterial } from './extensions/EE_MaterialTransformer' import { EEResourceID } from './extensions/EE_ResourceIDTransformer' import ModelTransformLoader from './ModelTransformLoader' - /** * * @param doc @@ -326,15 +322,8 @@ function hashBuffer(buffer: Uint8Array): string { return hash.digest('hex') } -export async function transformModel(app: Application, args: ModelTransformParameters) { +export async function transformModel(args: ModelTransformParameters) { const parms = args - const serverDir = path.join(appRootPath.path, 'packages/server') - const tmpDir = path.join(serverDir, 'tmp') - const BASIS_U = path.join(appRootPath.path, 'packages/server/public/loader_decoders/basisu') - const GLTF_PACK = path.join(appRootPath.path, 'packages/server/public/loader_decoders/gltfpack') - const toTmp = (fileName) => { - return `${tmpDir}/${fileName}` - } /** * @@ -368,11 +357,8 @@ export async function transformModel(app: Application, args: ModelTransformParam } } - const resourceName = /*'model-resources'*/ path.basename(args.src).slice(0, path.basename(args.src).lastIndexOf('.')) - const resourcePath = args.resourceUri - ? path.join(path.dirname(args.src), args.resourceUri) - : path.join(path.dirname(args.src), resourceName) - const projectRoot = path.join(appRootPath.path, 'packages/projects') + const resourceName = baseName(args.src).slice(0, baseName(args.src).lastIndexOf('.')) + const resourcePath = pathJoin(LoaderUtils.extractUrlBase(args.src), args.resourceUri || resourceName) const toValidFilename = (name: string) => { const result = name.replace(/[\s]/g, '-') @@ -382,7 +368,7 @@ export async function transformModel(app: Application, args: ModelTransformParam const toPath = (element: Texture | glBuffer, index?: number) => { if (element instanceof Texture) { if (element.getURI()) { - return path.basename(element.getURI()) + return baseName(element.getURI()) } else { pathIndex++ return `${toValidFilename(element.getName())}-${pathIndex}-.${mimeToFileType(element.getMimeType())}` @@ -395,7 +381,7 @@ export async function transformModel(app: Application, args: ModelTransformParam const fileUploadPath = (fUploadPath: string) => { const pathCheck = /.*\/packages\/projects\/(.*)\/([\w\d\s\-_.]*)$/ const [_, savePath, fileName] = - pathCheck.exec(fUploadPath) ?? pathCheck.exec(path.join(path.dirname(args.src), fUploadPath))! + pathCheck.exec(fUploadPath) ?? pathCheck.exec(pathJoin(LoaderUtils.extractUrlBase(args.src), fUploadPath))! return [savePath, fileName] } @@ -403,6 +389,7 @@ export async function transformModel(app: Application, args: ModelTransformParam let initialSrc = args.src /* Meshopt Compression */ + /* if (args.meshoptCompression.enabled) { const segments = args.src.split('.') const ext = segments.pop() @@ -423,6 +410,7 @@ export async function transformModel(app: Application, args: ModelTransformParam packArgs.split(/\s+/).filter((x) => !!x) ) } + */ /* /Meshopt Compression */ const document = await io.read(initialSrc) @@ -593,28 +581,37 @@ export async function transformModel(app: Application, args: ModelTransformParam if (parms.modelFormat === 'glb') { const data = Buffer.from(await io.writeBinary(document)) const [savePath, fileName] = fileUploadPath(args.dst) - result = await app.service('file-browser').patch(null, { + result = await Engine.instance.api.service('file-browser').patch(null, { path: savePath, fileName, body: data, - contentType: getContentType(args.dst) + contentType: (await getContentType(args.dst)) || '' }) console.log('Handled glb file') } else if (parms.modelFormat === 'gltf') { - ;[root.listBuffers(), root.listMeshes(), root.listTextures()].forEach((elements) => - elements.map((element: Texture | Mesh | glBuffer) => { - let elementName = '' - if (element instanceof Texture) { - elementName = hashBuffer(element.getImage()!) - } else if (element instanceof Mesh) { - elementName = hashBuffer(Uint8Array.from(element.listPrimitives()[0].getAttribute('POSITION')!.getArray()!)) - } else if (element instanceof glBuffer) { - const bufferPath = path.join(path.dirname(args.src), element.getURI()) - const bufferData = fs.readFileSync(bufferPath) - elementName = hashBuffer(bufferData) - } - element.setName(elementName) - }) + await Promise.all( + [root.listBuffers(), root.listMeshes(), root.listTextures()].map( + async (elements) => + await Promise.all( + elements.map(async (element: Texture | Mesh | glBuffer) => { + let elementName = '' + if (element instanceof Texture) { + elementName = hashBuffer(element.getImage()!) + } else if (element instanceof Mesh) { + elementName = hashBuffer( + Uint8Array.from(element.listPrimitives()[0].getAttribute('POSITION')!.getArray()!) + ) + } else if (element instanceof glBuffer) { + const bufferPath = pathJoin(LoaderUtils.extractUrlBase(args.src), element.getURI()) + const response = await fetch(bufferPath) + const arrayBuffer = await response.arrayBuffer() + const bufferData = new Uint8Array(arrayBuffer) + elementName = hashBuffer(bufferData) + } + element.setName(elementName) + }) + ) + ) ) document.transform( partition({ @@ -623,11 +620,11 @@ export async function transformModel(app: Application, args: ModelTransformParam }) ) const { json, resources } = await io.writeJSON(document, { format: Format.GLTF, basename: resourceName }) - if (!fs.existsSync(resourcePath)) { - await app.service(fileBrowserPath).create(resourcePath.replace(projectRoot, '') as any) - } + + await Engine.instance.api.service(fileBrowserPath).create(resourcePath as any) + json.images?.map((image) => { - const nuURI = path.join( + const nuURI = pathJoin( args.resourceUri ? args.resourceUri : resourceName, `${image.name}.${mimeToFileType(image.mimeType)}` ) @@ -637,29 +634,25 @@ export async function transformModel(app: Application, args: ModelTransformParam }) const defaultBufURI = MathUtils.generateUUID() + '.bin' json.buffers?.map((buffer) => { - buffer.uri = path.join( - args.resourceUri ? args.resourceUri : resourceName, - path.basename(buffer.uri ?? defaultBufURI) - ) + buffer.uri = pathJoin(args.resourceUri ? args.resourceUri : resourceName, baseName(buffer.uri ?? defaultBufURI)) }) Object.keys(resources).map((uri) => { - const localPath = path.join(resourcePath, path.basename(uri)) + const localPath = pathJoin(resourcePath, baseName(uri)) resources[localPath] = resources[uri] delete resources[uri] }) - const doUpload = (uri, data) => { + const doUpload = async (uri, data) => { const [savePath, fileName] = fileUploadPath(uri) - return app.service(fileBrowserPath).patch(null, { + return Engine.instance.api.service(fileBrowserPath).patch(null, { path: savePath, fileName, body: data, - contentType: getContentType(uri) + contentType: (await getContentType(uri)) || '' }) } await Promise.all(Object.entries(resources).map(([uri, data]) => doUpload(uri, data))) result = await doUpload(args.dst.replace(/\.glb$/, '.gltf'), Buffer.from(JSON.stringify(json))) console.log('Handled gltf file') } - fs.existsSync(tmpDir) && (await execFileSync('rm', ['-R', tmpDir])) return result } From 165f0ba6045b8ab759d28318cce0d8bfcec1f119 Mon Sep 17 00:00:00 2001 From: David Gordon Date: Thu, 19 Oct 2023 15:28:01 -0700 Subject: [PATCH 08/16] debugging --- packages/server-core/src/assets/ModelTransformLoader.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/server-core/src/assets/ModelTransformLoader.ts b/packages/server-core/src/assets/ModelTransformLoader.ts index 1ad9dcd570..95447ee9b5 100644 --- a/packages/server-core/src/assets/ModelTransformLoader.ts +++ b/packages/server-core/src/assets/ModelTransformLoader.ts @@ -67,11 +67,13 @@ export default async function ModelTransformLoader() { MOZLightmapExtension, EEMaterialExtension ]) + const dracoDecoder = await draco3d.createDecoderModule() + const dracoEncoder = await draco3d.createEncoderModule() io.registerDependencies({ 'meshopt.decoder': MeshoptDecoder, 'meshopt.encoder': MeshoptEncoder, - 'draco3d.decoder': await draco3d.createDecoderModule(), - 'draco3d.encoder': await draco3d.createEncoderModule() + 'draco3d.decoder': dracoDecoder, + 'draco3d.encoder': dracoEncoder }) return { io, From 9831147503bbe61cf7916d9bd3af87f8a19a7821 Mon Sep 17 00:00:00 2001 From: David Gordon Date: Fri, 20 Oct 2023 12:53:51 -0700 Subject: [PATCH 09/16] Fix missing draco wasm files on client WIP fix buffer upload --- .../client/public/draco_decoder_gltf.wasm | Bin 0 -> 192420 bytes packages/client/public/draco_encoder.wasm | Bin 0 -> 374400 bytes .../src/utils/CommonKnownContentTypes.ts | 3 +- packages/common/src/utils/miscUtils.ts | 30 ++++++++++++++++-- .../compression/ModelTransformFunctions.ts | 23 +++++++++----- 5 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 packages/client/public/draco_decoder_gltf.wasm create mode 100755 packages/client/public/draco_encoder.wasm diff --git a/packages/client/public/draco_decoder_gltf.wasm b/packages/client/public/draco_decoder_gltf.wasm new file mode 100644 index 0000000000000000000000000000000000000000..469904ebcb7b83523db73913a19c349528105ff8 GIT binary patch literal 192420 zcmeFa3!q+CS?9gh+WWno{hpk+C6R=dRaow_U3hI`+>x-y=(}?b|No!R6%5+PQ1x-p^5i&q6!TJ7Gr1(JzR&I)+Ejk29!*yutDAJQ)IA;UDVo6b<&q2j_$9+W&Nt+Ib~S z|1{0fxGRc&zt{8GjG)Ndfj*4?$>^`oyRlFF%{qd!!+)Jt;PuR2mbFylYzJ^ci^5s! zakDK|%s-%`)ovN!0UQb-vhmljt0(ORA85Ga=#{?mo&V?)ux44e%S)GZz>mL3uuj=C z@A#Yjz2V*fdi>#V_;6@<8R+_<2lArx1MRnZ|IL(tiM!Rmlo(_bx-jJ-2Vvv74D-=O4YlY9-y&vCyJ{Bzwe2mg(N^IG>` zf`6X-rQrXqyD#_`xvhS)`-Kqp>~V`>?;iIx;rcypYk2V<_n$-f<~{C5e0YgcU#irX z?QuUJ!pkjnQK?HxyzgkNfvN+^WV7 zDDf3ayv-6{sl-<)vEJkUQwTfuxStK-l0EJ}hH&W~_a8#IY>)ey5H8>2?hWCJJ?;Y` z?A+ttAHuFZ?x#a|#vb>+5U$+gekz2k_PC!6;p#o^o)Dh7$Gta%-Fw_mgz&6A?mZ!V z{T}z@A$-Fg_wEp$y~o`h!Zmx`kA?7@J?=+Cc!G+bi|b>rb;B2qE6P)dJ-vQ3{x^D+(d)hQ4tTmO_N9uBYdbARFg+`q>Ewf`&skbkZJjQ>mjhyH*2|KtDE|AGIs{}cbm z{*U~@J6_}emw(V7dcgml|9yYIf8N}aE}5`NngeH+S(*W#{~4c2D-oi{G35 zME0KS$Fo%8-PzsQkBv+GsFirmInO(H?p@izJANd4XZGLy4`(0o|33Sn><6=JpY)ql z{onlD+q2*B|1Q@4w(R?}Yh&_n`0tD5H*Y$34p@I6``+xkvhU2^n%zY;-;=#1`|j+{ z?3}r|H)i*TPldU2pZLUY&0e2QOCb8iUW7QQvSK3w|R@Lm2p8O=NWo&Jdboc~Av zPkz%s`2Xua?>BwcZ~7a*>F@mWpY;6a&;0m5&HQxs?S0=l6c$Q%*{myF-Ce2zk9=uM zaFwrbTdv&Vu*G*@`(<+YI$OSM)|a6SmV@l|>+W~F?;@4&i$hnx!4k@FMXqv3{M9_k z-mP+#KR~_N{`&3ryT!{lmbt$4tL*Z8q5kZroc^b>`e5l_v3x;r{j4BMc0uTuS(#rD zy8Te7#D-xTpbUzB<<*cikdc!*7fyOrV+3Y>(d8%QMR^( zT&P{Hvnys@{rOKZo`RzV(hq`5FwMyNeR>1ZMf*BOm1S!;m`TMw!|Ujsufy_XjL940 zShh4iyqAG0>%;fk|LQS4w^qh=hcy0I%w__@kftPinJXIVuo_8@H7Xgt5rcToKQDi3E z9gocD+yt4mCdrI$CNiV)RmqHWBQvO2^w*G?h~+2}f~O+GUje~4R0mzHNp}VH#KCh6 zs0o9i4~G)_S6S`JkR8}xIio_P0yLXJyFesjhbz0qP~bt3UD31nK1B8hND?8nv*BOb zIdY^U5#;J~_`iGLrIlYgfH;H3{r;u;>2SF^hIH9fh8^x{u-k~Ohn=vXe-jrQVEO(E zE`cB5YygN*b&QUwrUUy5LRUY5YW#tv+VMR5iuwHj(}BQ;9)aT}FLI;h%V%l3e&T_l zSAOL}fYkZ&V%33)C{THmK577Yq4AI)XnL&vt3B%LRtXQ8K6V)x@PI5)5g|udDeAjm zMgFJa+AmeEK2*AS@LCj4@t${KeqQValy2}M8u#@dKA`NCmpnRS#?f3cz-1_XaX9zc zAsRZI?-qtZM;K(KWB37OahL^5yhU0gU(k>ER=Qyj(1FzEog|uM2+wowLPu3K%7prO zpkx}glkEZ-lIvN0`!eG9J`@j4fx3mos&(b8i**35XocE9bCi7|8N-Uw`iWei|A2aT0P=(zI)bBK@Sx~4CG*-%++%n={nyvfi^IaFID-K6G!ZK#4#<;+N}Jzvgw_8!FSe z5Pq328D)rQa%vSBvc3P@pd3*p#Z{ zY{=Vi-gEV9dV^p0uJ~Gbw$2AHfRFG0gps_f?`>Xwt$F#`-*xr60ogwt5VB!GBXGIk z;e~MmV>~4cO-zy;25nwC zSyxc=Ks>67mQwk|bBrC*D>X1trP?L)2qr`nKSz=ZXwne9qek*2I(9Y>r0~@fqqK9OT|>8>8$MT zng^Aim%7_MZ^mOPvKtKpi|rq@hAo+~VSm|LY#R524k|}xVkp~#nF`7z!=e~P1d{V+ zpG}-H_F<40bH$+Rib2m6AM+5@zi{wGm2Y-NC!sD(JTZLkJXh3*(X?UTg{TV`6OOoL zewp~MKO<rOq-ogLOF;)wLU)j@v$X7w#CQx_yBs9yDC1eiH{cuKXl-NurnsS zELQvC`0M8QxGg>oL({6_4Ze7#bU&TzM?J5xKq5;@2}jU-?XJ>0Xi*Q1kc2zqX^_y} zzi()k5)yYwqP!AAbPwDhWZ7@yZN4rpsg^4%St^WX@hXO=bdp3{!nH#nlBIKPX$FeG z2tsnQkf7Jn>koM^H5-Oj)YT?&p{al}DrcP6Q~;BWE8wkoIh_tPY_66`ElC4}jg6|M zN(4YB9t1=Mt{SuY&@E82l-1I5eVFHMa(Dx zfnMNd<<1a6Rd${e71W0xcFQF{+&;ubDcKXH14aXwpj6h74?>D^(45MN_E5)^Km2*W zyiY&fJ~Uyh0-`wbz{SWGhQI8UGZ7O*1r&_a%dEH*N)Qp*m=Dg1FF7xCe^Vv;qIubU z&S+q(o3Gu@NFsxVDvIY%Rr2_zlE+&m$OJFighSPkMgcM$43tegqDI%K`^Jz(+sf6l zZOfohZ|h;BPL#lvZ5wlBIPyD=H&EyIRV~442E8fB!3ISQS~b$wx?6Yl?`xGgDhClF ztYpPs1ZaCiWRaty}x6R#IdPrUollFS})qXvnP3JLT5U-ow&WR|7~^ zxAz-kpS@ULw=6wVwys2`^ZiJt7$B$U4cgS`lgfC?afx`sV4_wL4t455=B_151z5{U zM-hs^Gec?JI-u%QE0}{eG;(!mjtbHRfKZ8@jgSZM$RJ{{EfE5@&1H(2i4XwQSk=sn&T`c@QG-Q7cZ8H= zpmY%&Rkv(MYh2cZ%u2=CJQ7u?U#?|QBV{Jardq^>u|63P@vT34lf@g;WCF)G*~IZ} z6A+X#(emU7$bXK;iw%yeI!o1zjF+~ImzL??3F9Rz`HpfwP%)qy)GOzyu`ym+3{@&3 zd@T|G&{Dd?wre8hr%7g=}}ay_r7R?mV{v+y>L5 zhT&cFG$VGfaA@hy&EUCnvn{wK2XDVyeg?cmBov(Zva@@3mceclszrQkbBV3ZC2x!9 z+D!|oZio!?g@N2XYgUX3GDy}wHcfx#bbuu4Od$QslLBdmg18yeAVE*3jwN+Af%LC@ z!V-}W0&K^@Qg1box~BsqbvA+Yl#>EUV&Et&bxsFJ>TCk({E)C@w$bsh)L9Lr_UQmg zolPJ;^`t;*uLcs1)v42xI-5W`H%nORo(L^jIAJAu%j7*}A%N%vlxlW|d-kk49>JT9 zO8$gkla@J(s4{uEt3fBFM|w#RVTFLI{787^s61p9f^6xTWJGW+4zY?P>W0kH3_T*O z{UpBk?i|jPy-S>PFS?GV>d9u^60U<#vzxGe`Zp|MvlG|40i$of0$9VY zxB~A?-ZOJBIrbHguI4w1@2EM2XDlYeXpq^yo2|u^#jRD(bk0g!%VI<`Wt)FzreZ{G z%<^{3tx!Gr-}t>$uMB``aHk}ZHTg`5QnMqFc&^3iMn*vhK17*b0W zn29Io8$*XsM-xGc){J_+N|%WlOm}%@ht2*Sb%CIPOkTjluEbR2-`=r1ERq>@xL0;l zhpkQPu;}%~I-JZ>IUxARlO2{klW7+_sIiT;4<1kvqdKldil}H1O6BS8sGDO*u6|WGbC@)`0c{FnXnJBNV%|MI|{2aMuUt^Yk{uVL<=vG01jpO$i$_2GJYAjbtYBVX5E3E>E%0G&c*8e?;J!_oo95_eUFB$G-Dp(=W7O^Q^;9Ixn*0Y+<A-Swp_R5|j+}@> z4mDjRSL$j_G0~FTdZ-S^UT(d{F)DOue5RH?=&(u&3^g@K81pU&{hzc_CAB?tE6 zcN-prlgEq*)q$nsuwpY?&=Mxx4 zt$Pqw9^=e{S_=%Pk3p@kR^T<;am2mW?9?5dZ+)FH+>RsuwKCUgugU%Vw2tJK6BtQ! zXouls1dGb1)xm}sXdHh=sGC~gPq9I>c>O`El4&81it@r0cLvQO+2jK2or2YoM-i{l z5Z|1Kw$AP&)>nmja7MC)b>=}>TZS-&Om;9iaGsk-#stfp+iRI!S#{>CZiG%&%wVz1 z=i&YCV7AJMow7WDr441aYhH--jBcLV;++vEAK3Niqf9NlFq* z?xGS)uZ3g8w|7d$D&p88=#O_~+8lMryrouB*uuH>q9b8dY%nCiBpUC>EL|Srl}Z{( zz)-NnN*<|E#7=}DY(^nz8I4>(9{sAdWUFd0_xQvm~KsK znDD<1U4W-A3>)K#X<*>uec(d~Yv8YYCGts{y3F>Q5zrvogJ^jgV0M`j=!d2K{6;4& z9njxh^M+z%SBWnweXlhl>Q9fM9*?T#qlloFEPc&u_Y7b~JvPAQ(MF(Nz}!SdHWLtW z(l2uj5O&g%DJybpU_3=}8zeVb%hk31UzXHv5$VA#tH9nfl|g`Uk<6U?AQ1G4E- zIb1-34@Qo%t)``UC)=Uoi?4)&s9!*^ngU~<3?QF0n|^J2Ll=QV;6B6Xky?!?BHaRL z%V=~A1QaM<25Z5m{!<6qBeOhQI!UMkz;xq=LJD%!{W_b9?Tmqb( zL<1}ib%{SrfSAb0WnCLnNBMv#L8GaoDII4@VuWD{r4qhEFJX_#*@+loF% zXG4Nc;TJ|uQpB9kM{T~PtEnTV)eyj)mBt?|u%zI?0ccLL&?Lc1vUAaS*V_Q|IHlGNPeXtZf^oBEk)#}u<`|+ZAk+Ht3)3WV{{2z#5e`a zEMn5f^fizc?}I8583`gbSZ*gs;&G`_ z196{gD1>OYQ2~8q(4R|DSQ;l=ujlJuaf>n2@5~MUWuoWEL)hWaFzkwZEnU46E{UzH z5ynwXVr&^O)-2v1_8#37S)v1|*u=g1#Yjjgs*C|CJsEC|b2jQtvQ{@0eKiV84PFyA z7T>@!i~6&lBm;&k=3Zw}I|7*9aP%#pQ^|bwykNK(%W=OgK^(B~3bTTcFWq8?AHA%4Me4e<{JMKlmL5QDIem#SgR^icGeG)-e;y(mTDHpr#}!BfTp5w}*- zKTQxw0~kq3dRvG_++%7)vXZW%GR&S7r%>$9vL`h!4@p%wREX?j#uOwXU-crZIR3yI z#?MV&fcXRruEop|>N{s`O*3(>EY6=Qz|x+YKTY1BNJvZyIpla3lC}D67N12&fJKfU z-50~KZc-4s0cc`5S{9e1DNuJh9pUZA9W@ARVp+z&24wJ_5B_!Ox4N&U{~9s0v8;1a zR$G%{q1pvHdvvIF3wXu6VL^sgP7$izA+bI8v@EWW%Fs3{v<+YU^Xo!e+eP7Hc>Iet z0n5mo=SnjZeA#X0VG#oAt$Tl^NsEVfycOx(o8O*zdx)|Yi!*XDm_9ixTl=BrDMmB1 zR3%xZ>5@~OnOzBMoS-#W&vWhr2J4|q9ucfE=`4~r3G1m$*{*~&&fFTTU+W`YA3k#V zB&^4o#~tG}&gHJa>#5A;u7p*{u$2#`f3lBwJ#y%ZM+9q}&gf-6f8eWlq zkzU=lyC&#Wp}P~JSJ^5@(ZH$9{;q^IP69WuMtVK`1g%aTEOj`6#We1Jh;W_o(8^6a3SGFVrw*5TY;d7} z4qwb4=WI>$$tody^P?af&$*`HeW!R!!0?V{8TKe<8&;J}xpRZXNG%v{(+Un%)>jN` za$c&vRQGSJFp%W>fBZckdyiLC?PGe8*@S(JMnosttQ4sqbF7(8uLKWurdXu^4nx z<<3Z^=@_7-)*e}(6s{QcNh@YocE?aGd??Ta=47X8BxMLp_X&qXXiI&H7>HwzYeE4< zzqMprWR-BHDlQk=tW|!aDqv|0(potjbE6JBOUyjD;7q}2e3b{GvGnYtp0|jjn0dO2>2n5Fn zve^cEAYW35#VK0KtQAp!8lO+E)-2Ay;Wsr4S!l;VYCRGH_~{_!=Cr2(Vr>fO4ZgG4 zg!AI=Kl^IC|LiTl&`B7QuXUl5VC1cRp%dD9>s;t?AK0HmZvzkJkYc#ymC)V9>^UE2 z;-YYL%`x}=M2xfFCX*DH<0qRu!08;sqsWJ=iM7^HhE>99bqN4a<_GLJp^#>0iv!cU zj`%DJj+Dc$A&_7M2Kp!(MFpH>n|!QM5XU^%M599F64ekEVP=uakQ5Wm#vKF2S=2^k zjB$*$LOhU6@u0VjF!j%WG;*@yGoxuFqCROFY5O>lJ)kLZ>o{@SD6t$TZXPABA14yw zOvRD$v9E0CkVt^w82r#IjR4eOvl&qs4bC*_+UTFIM1qk$QYBSL43{CGA4z@C*wjghvVUog@ssQ(7WDGHNG0MZDSw!wLJb-To;#*pQwX))O8e-s(xh z(CMF&d1+}so z_{Lf<)H|w;N=kGuN>{Vb`7>vcoEA`TYbCL*6yALuyV&&R+za(q&b&a6J4w+hVmU~P zy)Uv=F6Mt>%6%RCo6DIWH=V8>S0*RWkf33P_h@Ls2pZl5 z4UYzz`0en;-=q~XW4J!ZYTpNmcz&jSXIl6B;KlEFd(}%n9;&Y|n|J44BDi}$aa_0+ z%^`Bk{G!2)>7tM$QE1Xi#ALR8IfcX(6=w6TAEp#%SET z>-DSb?bYLLP5)U0Jvoj|?nj0fji)VyUSm90LWpUdU~%qx#?Bz*F$v)~&qEi#cxrsd zeLc;?JJF339Ny`nm05|m9j%qfl`DBt+FBer94RwVZVH8*yh5F#ycFz)J8f+s)Hg|& zQ(tRe{E-ZraC`WQL30ZD|B68)lHWc1L8bF=r+6JR2$W9iZFQ$=G+&S!*h3p%%bnNXTvUJ!fBul5iilL)4 zw_Dq#ig(LS&>G^~PKS-tz!4%uz`>i~;4!vwlq=demLI!p(%LCx?c9g8^S}ygr%|R4 z*4o?Dd(R0${usA%-ln~7qyA2sIuHKRgsC%g3`?g;KPi-)jDZu!;Ux@EW8es`6Ekp{ z644|Yo3rEoPMbTAczpj^nK?%u{kTn|Qi$1T`Q^^p7{~GWr@5@Muk+Da!ZbXQ1rDb* zkRu>i!_HQ|lY-&&*gXwm2;xM<#K4XrCIa;6M0E@)M0HLQhSO#QCF6r_epk-M7|#Ss zPT2f7Z2_K1aH2KzNWeM0){tU7Srh3IIK{s)3VswLJ%AJAJdXsNFQy4Z+=hiUClcqO zCu_%f8nJO|aUO%_Wa2ytE?GVbdwgbLS~E7<|JZS!HQ+w31w`XboX3wm?Js7WCw6(# z_>sx5R-7l|t1&Rvi1Vz0my-?fua+(!A`HyqYzL~0BFkcnSE3(ABBJXq~&w6;CwMHpCn@MjH_oSfC8ahMAJ0M{c8{zRao&2}2c z_n(#R^Xuza?)rsVw4>FHD`wo8-OCnA-z*$rJ7Anln2!9QX(tZ)0G$XN94CzBmEbVb=j7oyp51dI zaM-Hh1`dnyjNmw79z%3Y@GHwfwJ4{@@&V-3*S%Iw~%k@V2h$=PjY4s5J(n{tc zV&#PC)ee9;MI^+bw~)d~N_7h<*qEev!f0Gr%(Nn%Fv9OeRr-ZlE^;EnZIdRCNPit~ z!sM8(uc63o8SF?@p70{v)2d1jdvlkUA3HkLT{-($(5Z$P?2N~RUIizQzFsYM9QFhb z#=5n5^mHpzH!cMXn6^1dI8Lu_ZH~@;c*jV={<7Tgq+o4<>+$q!(#m5^d*6#zXxc{m z;CPyLeUG%OEwSDGLQ|>}i%K4Qik0q6D%R`F)qcc^6$|G2v6@|hDpRu)*phahShn)z ztX@B^GZEt?lGbapR2!|tIVw6Z^X|CYCvIrdhc2s-^K_9_&RkT7%auCX;3dM!w8DUc z5BR53vRYY3KC#10yOmk!XeT1-n|7G#zzp_eN!e3{E*sM6No*gZRh^iotz#G%Y3rD6 zOXOfn4$>IdeHeVw+GiOO+GwV|)pCe8*nx)Wpx4*fz9Bz&Tovkv1QPp}teZi%AZZ)V z+&=cOR2h4YY=2r@T*Wa^YTQ=b0fV;c2qZghOF+e|mhc?e{iH2hK&7p4Ucj^a3>=79 zSjeZFj^0@4YMDE1wi6$qHTnOC5?ViwHs9iGvyq}ncgmDi8;E*w)BK`w2L3^C+P%B~64nEWFHf_=f z1SS}2py-4B4^(W9dJ^aT(Dn!J`X)}hse>H^$G$#bS!QP?FWIhr`ni8!)mB)vEZ+L2 zN)gn<_y`tBb+!edNG<}~gN@#_L0M<`@KbeZ>tAMD|G2{B&FuWMk>JuU#sS=GbDgay z)p0G^E^b|7FECJkzyMskMQwI4Cn%%2*2`-w04Tclb*aBgFXikshI!XK#yZ!4z1ef9 zBv9$_)!3qTB5?MMuHB9QmU-0yo^;fd4nR-mM&Wu0#UeP{P^nXGIL?M;qq-Y2rw-1a zs_j;-4kymo_C`CRhVTOgVK`e#$}4+8^L^95yDNV8o8Re(owN-h-TTqu$}XKX2|cQ6 zkPI!w`oXstgfY{RJSjh!AaY$`XP!0v*J^b3hZqm+g1&hd2k^LAcd1#ec1Bj}E3N2y zUbd@>5i%Oaz7aP;_u8FGv9?AN>er_0G}u}h$`N+f{YnSpi4b~`!^u*%hb@f@n@#y9 z>_i-wGh&RCWT04rfkJLRRE)MAMz61QmrW#ATus}M=y%Rte!9yo^@hQL6m?z_(_V1| zttJyA`lgdA6R#R2ou+V!sk+78u{*zT%%G(%6uCCa-jZm8I!PsM&eYL#E0 z88DDmW%mZEuiEpAb+}Qt!kN)QUL?Z)vPVAzZTc+s)$p6qCQt<|umiA;ibanAsCE;! z4BoEv3ja&L^w{-iz2V0*_?pp#UgtR)pd}?n_gg37g3pAI$jX?o6JF&^{&7Yo zM-J%NP~-_;x#m<-Axsy+8ng2@biQCn2T}}uL`{tlg{&yhTM}jOnn$>;%N*A^CFnh6 zWWSOna40N~5oGp@L<30l;jR6M@`gS{6c{uOePj>>M-Pb28u}O~k!wUBdY_lsBwa@@ zZ_rr{kvhSnNbIII~s#5bTsHv35#&Nv*<=fBjwRs_fUqvX&{P+JDjxE z)dhzSa#TXL*_qG*z)YuKynrU)u507t8XnN!vlswQwUz3DmDfiNsRQ>&O%|g$3hV76 z4LgdDQ+SbICa1*w5L3#T9<)bQI2bzAk$TafmqJ#tHlZNX$#6RXYGFxl$ay`s(<4y1 zmBZw+di&yFUY(a!0U+>Bo!tzN!Ee$xyi34!G(H&y0hXi=*rwS{$f=F1l$0$ZBvLeH z2#bYAZldenF5^XfsP|2ukfg#(WfaRFg2A!(LFqbdx5?Rb&dM;j9o9)_0|+zf$>goHFyLsE8wzY7-c{jT;OuWsGJa7;9g_FrFD8RgEWS# zE4Y|eL0vA?}L(eI(lwamSmbccspAvX0gObq2uRf zJFD3{s=>kA^}eCxy;sg1ykj_9t}BajKqbAhPoZ@X!bjcWV82AfwK{y+j$Bw*uG?|( zw^hB@m%TfNJ=+>2Z}W&77PK;}Z~fPoeZZOc_WI!*ls?1R_n>D}Gz{mSI~#1r)6Ht| zcA#MMuZkD1w;tRs1=B0zp6}Si>z_Mc_3ZX(sowJ03gqngH*Y{xQ9HeOF2ZfPu2n!8 zWh$(uaLp_OTXJx@{>>$DdugT)u^p51j^A)Nl_ zU=65TCNeJ^&WMdvhh4>6i_kjMYs5m{hz0EOC>9_+i3NLH!vo6=u|S7FJ7>BlI>M7N zibFU+T;c>ff;Alyf|&?Rk;fZOumYHwi4!Pu=_rqEJiFy`Cr~tUz!J8rrm`hw$QwCu zNGHIUI4~OrVQJz3E}0Sshyu>oT}2d3mC}!+M1dV?3g_!W#w7QQlh(^V0wwCN*=@U- z>@g0rI?bnE(cdY~Z)zL$cU$cCmiQ3oQ&H@!aXxh$7;F9+&nLt zV2-~;_T;JB_M21mxglL+w}7KC!c!C^7#9eC=w4=qX@`|n+f~StIWYGj4k8CQ{0VDt zBg`RH5~&hfLKYHHggWS`Z$R=J^;=YPEU-4l??*vKcT9h+)Kwry5vLbKHoaD1IK$U9 zz&F_w1NG5@5|n|=31K~w$lceM-8+We35krL>zDlraXazt^}}_NrxVf|!Wm0zsOEM^ zi4eMq&%EBq(4=)20dA7o6oRC6cUoF^N7CAAbh6JU>PnJ-st!oVGR;g#>lu^QT|5HO z`yr!f9;|>tAw+52kJ8#Us^+UnYYqZx7yqw$s`Yn0UQQOgzsi|KI9-2Z7nqnN7#%Q% zUl)&EhQtU42D29Nu^f?FojV-jxx?ZgaB1d>i>8}ws#UMH!O%n{aB~sD;OMJu&UAi$7U##F?PWo(YyLM658nZndplVR05&G4l%dC}A#QVT6wVB9o zGg;2qy*0UF?YfL?uvp(rw2(JKOL`QSa{<>3-soa0LRJheu^Nd>$(Gn?80S=Zuzo&P zV?85fuhZk3fd=)M+A(U+g<6zJN0g-y{`jjUwCr#*B(ehYjOQ_Mz|ro*FgkaQc6Fo~ z1ga6jSBs~bK~F^L7&*W@;BO48B9FSyc=)bQa5YEJNhivT!oXF|eP-W_1e- zWT9$8tcFqL7N68~uv+j|S5`AkW!SR1;Qm#F3RZ8fxaVA7q!Z?f%`QMP)Bi%*pOw{E zfA&-2FuYR&Rdu0L>h&aet-IJNr~6MbjQBKA{5M{e6uj{ZtbIA+HG_wZ=h+AR$i#y5p{6ED2{hviJFN%UNOIa z7#aj8tZRd6>cp(K+m!X$Bc@u7y%vO9AN#f_$mWW$J$=a3Mc}v zyJi&vRz>GI2iY=|iOm+&PxVoS?|pzCWEu%evPnP-+kyT3%31M%(Tk|Nc7vxf=r~hi zkhZ#HsET$)(2;7;>ym^tlaZ44c33Szc3kuZVxcEN48uuubCw#d>!4W-j5bX$B)Cut zqwDNgv5ry^Z?vXA`J(Q=48}L++L5nSZ(B4f!Y0UzGys%Am@f&Kz|3D5JT+#=^wlAG z(2JOy9$i7a$8Zk1XoT5W1_9wec&bQ>zanq?4a22J8LrkZ>P0%V>Me`>EYz{oId#oO z^4(Tr>4?hIyi49{3ZWSTPB|ToLF^$TtUgxirKP~FU|+q8(z}92)biE3>rH8KicO?g z|12#?WvWg~uJ3}TajISY{k%j1)vx;!fn^JH$;<^=32>iT4W>4I{ca`MyOW>q>QV-~ z7zo9=g`Wx9=WyOIavceRiJdA2nhWtC>Mnx$5=rd3QGV_{%R46YHNC{V`s>4Mn>)VXT= znv-BMLUS-i7YW;?T!t~00ZCWP#xZLcN>32!jY1`3897%jE)mTtrveNkO6MkgJX;kb8L!I4>xkz<7rvW>1est^EkO_M4TgSAD@#0QsI6GDF(zvW>s z=;eroM)}@_>dA2c>mu84GHh^t5t8RN zPvzNQU1wHAMu2_bn$qTjooUV>n622uRF!1S>VtMA+B`ES`r@Fd6lb7Ynp{fT=EIQJ*bv%*M0$c-@$Ix?2xN21CG1JjH48 zfOWG`ivTGd&KkrHeOPGBNkN4iGexH~kYG|gLXl43egb8qHJC+f5Jd(b$|x*@%_zwf zcT#W})*faZ`S;5ngzGTNi2KsDwS_tMHE(17>#Bi z%LYgV%y2)Nsxuo3iK?^k487X#0cJ9@2ZYExk(BU7+#{sU7#&g~5f$<)5fy%PLsX15 z8AMcJL{y|0QN_VSNEAb_PKu~#K-2^U#Rr4Xm5MxDppA$sKvZEhzdD7|g%Fh<5S93YI2%M| z44n&^aHSCy2ozC4g|T-$#4@TEy%I?+aTlutBK=Kp7mF8PXI1v?A6> zLLu%N1S^$9?X2%K^5yjr8pd$<=xRI%96^(DohTb&p-t5Y+vakX3QB;EN!f@XWM+^E zfcQj$v15YK?yk*`5{wWi^Cx0X>`Pdm4iYg*@{e@$q4}*3T(22F08>`aN%)|8W$z=^ z%tTd(h{9YLS@M72V03`p3gmccN5FwaycNjFf665-M=v z?8*kgM<;rbsL;em5tZx)@fdy4dJt0zGm)qgN%f4RSeM9+zapua)pRW$lt?Oivqm9_ zq{xG=B|VW8+#E^DK8#5Us+cAzEa8R`SMk$n??4qZM!oT)vEGRXMSzZw5?aYLaH0=EOpW5W?4DALt5Ar}r0zTf7= zSlcMyLh1N=)W;lR^0k6KC;-I=IA{Dp#xTa6zN978(JMP<0fEWAuRui6?Zn|GZ%LNa zW7%anBSCNRA4zUTFXECy^p`b)c6qO?drn3dO%c#X6@mxj6hlhIdM8$S+*)Z7Bu=}z z`@3h;^)8GyQ#rE?M7x}2O+H}}u`z4%+eC9lxmmzZztSBrvt?hMWP-@r)J>a1$?P70 zhy}G?P%{M+%Lerke$$FO86!OU8t@KNyf%d`(I~dz1{r)VRNSJhasJhUJ_V6BvOd|% zo7Xtt{IIA>-q}&uRll_gqL?QzIFZ+UYMY9s2;n_RZ-$EHJ+>MPXhjYdS+HT0J*NQm zkUEGD9zsJRTjjFwWeK1)^h7{(nT~YSmT;XCZ^BvNdMS{J({KZGZ?O_vFlH1K>CH$= zeke6?!{~GkOrcl}@Zq=yv?d}R1Y6G$Gc5Nr(lsJP+E-ZwhTk^;Jp{bWMHokhH&^dg2n7~DTW1JsI7ybeD{mRI zV43U3qs@*;5{>My3Y*~QO@eGGaw#&C zMOT~ltPZ9e4j2b}&>QKBI-)06oYtFV_b^-xJTsVPS=OEX`&cNavxsXi_b4llz5<-^ zUYd@MQcsJ^Erm#d=U;BhuH>zj@(S5ogdcI#E!fPCuox8#YZDGq6FTK2-iVKGt(&0` zK_Ck3Ngy_FvZi91O_r!Y7a+7|a!kN7@_1S-Ruv@; zJ?%1|p4ydA)C{@+D;Xl36%O;JXDBRk$DdlJOOhO!W8OjR8-cyz9%A8()vJKk2<&W> zlS+*O*~?^cFa1V)OYjOlT*$Z#9Q3wLWYbeaRzxZsDcIM(cOANqh?Wy<$C5&BM;V0DUlb1I3OSjbJE(IEuDJIvg z1D0X9dtT^`k&mztFQ>O$NS_gB3^BIrvkAT${`FZBazFC4RPa-3-OZVI6bxPOQEo;qDOdRo|TJSVYI3ZSA6wfDJel*&dQ>wkA$RmA*4Ro<*ZnbP+}pi$_9nE|6PCF=pN1C47~M z%!{wr6I~@ZrQ4phmf^QO^N-P!593#fI_;s zIj(^dVr|?RI4N}7|DwzwiH9zwLbP&$nG};EQXjF2N7EeyTsPeC-U>#!=iuNyKHj-y zEycMH8e>hFmYwq4pk>5@1$>qH(=zZ?wgfJnWPPH<(9JYyx8MkFd)sdJPb+O(EXdoI zSeCc)X;~TbZLlsXAI({nEOt}FHc;2bh8N-^;@({i(w{;zMt~8BIG_d~kO@Ez-BAnB z^(G!{A>2?5D8r!WkCTW@SHwT7DEOek)A0PNkGn+Z#NfOt5~66#*P9?Pu>$Y$*6w)| zM;`AHONyL2_b1fl<)FK_RJV4aUa>s&Z}aVISOxEcBTD48{+N0sC%n0A^|qyYhRK4A zQLSfgup&!LZfNMYIS8qxWg5m2i`#cpil$V`fXoMH#XGY1^tRGD80~-@a4rg1vZtZB@{%mm*eh+(B z^SF1F%Zn_Zt*2SQdW(Bk+0(&5! zDNj@Aqc)?#Iw{+}S|61BOHsCLEAuVU@YT}nD7wV=*iv?xCasAW`I8Tj|1mq(f>N6e zyOKp^iy_UoICp{5nC9m>yBm>Fpy%|vGENTlAN10JLI5LpV0J4u4=joD0V$Jo3 zk#X*6PAe`x6ki3bW|$0SlFtAT22S1qN*U}v@5>mch8I~ThDV?9_|K%@S{3onvEODv zi*HsL?Mu4Od89PjIrgKs*d1893%-<5T{KlT&s$S*a^I*{Dt-LSY}d1srXu zO>rBuBv}+=UwBa3cP~C!NBhn+gb;2w?TwXyI5a~^au4-o8zWf#_n8()9^fdp2w-V> zwzO5+9VGJ(ci;SVObuoPmiRNdJE)By9&NpoN_e+l*LAKHTE3MraeGKu-xBUZtMVRd zoFn~yySLdd6CmqKpeuyzCdwQI80N$m4X_|gjc|M%zzABO5@6OC zy+09*jR8A8##mWn+(kr(-X!vggVwZV)TCiuIYOjSn$P!os0X`hCkw*1g!f?vE@6y8 z2Ujr%_g-K8Rfs+oJkP=(tWGt#K8B1*>pzVc(Lz{Ix?+p=8CwccrMeA~fXlkIztnoA zQkUXl;3He`5X#75T$+oSKeQkl5iWcII7dzz{+xM9?7*R3>^TakoGf7s;&C}hi?(99 zk>`fz#jzSzH*-t`8&r&ZTN>atN8q-u4&2rV+-55>0#455F@U3hfRoV!z5!Q7aNb1WjBRzbHoFo5A!xHy&0G-9*L(R@>ajvAI?qELP|(?H9#??3E4}vLeYeUu20yKOlR~)ENJ7RyA%i`j54a#@S!z=McX%Rcbw?Xe|3%J^X2g}x2qqe~;~o6zi4Cg7gz z&ml@uOaJ0QvWYLnO!_PuZXaN$wyY%uIPQ4PtfPRODAUobVN)y`Q@~^{7J=h92z}}g zJEAkb#0y}FNQ$tDn$a)KaI-l@H4ZmgMeNH^Bg1*&OaxtSEa#s$8^|OD(raW8V$oz! zwNag6>F@!gdf7V>(aaevSL@d0!Fz`~m63ulCKAE2!;LVuSsf(j_2>PvB)1NB!ubqE zwxBk@p2Y{F)(GBDv?gUs5&A&P|Nyn zBsSf#9b&u?l+;8#GQuN};^x=-@O{8bznWt;kUOl1pdcXg3I^9pPE)d^PGT(3rqeiN zw*rifLoUl~Y4Bu;JoSxn;ER|R6~QOxYinRD&j~91_E*J zd$SRHJt@03q%zJ;Z)}>`9-B0IB~5IP_3^>NFD3Tl!?yDaJ}tk2B-4=~nzpOJh}r>R zKzjKQ5*-wx@dBK%DaRCwdJjhpIG#g0O+=+p5q-6CcA(Cr7maSK`8%{18u}>b&6MAG zH_8BA0y9RfIZqq`2?9GRssvODF9Hhkj7fxxwI+d_rf^8AFJ5Mx5)rrII&pu}uQdz( zHOmSTZIR~rpjjk+o?~&+{`&C4On30Z?XyWV&V^!)fpW95hyk0XqDgOyuWItT_Cs5p06xzjVuMQSf(&V7 zz>!9#4^mj2Y#~4?BKfF;ebm9xlM&9&2O5~ibTFfd4hF2)xb{4tgTb6QyFPO9#Q9)i zPu598mwcH;2eUSSU$Ix)%Q@7dsL+=E0<5&;V%0B;NP>d2ayBZohIJ)zE`Z007lj`N?NnMP!_?Qa*Rb@>y#IAB*z&aQIS@&uF`^$md5OpGA35 zM1F4WbjfG!DMKNjjC|Hy_oRI00#D`#5WBJ#rUdldqa~o(x%CwT`peLOwgsvZ(D-B1 z2J~Popa+jt0nHvVFSF<|Z$KZ;zapR?i2*Ijivs$gvrm_Ro;!H~Jy0($Uh z3FxmxI5bc7g&np3~>G7!u2TnPTa}04TEImd|=)sqagdTiFLO&7-Ey{}$`o1IX zbcyJTPhLdd=T^$-xns%bGaoG(&626F7|~ybGWv*9U{XT5Tq&dvr-SMhhd!(o0FTK) z2a{@FB6f876)F8lq_mM<6w`-~_|qk(oB80AiFF)K6T+ilv}S}?x1}HVXvt|}*qWut z2EXiwMENz*6{ih7v3oizjpyUEp*@)J)GwG{4^}?JX+wK3ZAg)c$$7=VndFZ1@##yP zsgElPhI5*GBFFgz#CdFICV&qy*sEG%PbS9N#^f$&5?BKsb{WlPkWMEVPx&%mA|bt?8;(R@qiDtuVED7`Iwc zExt`zZCx3Sv!Nj_E@O1Hq>M2QOg;^Dh`H_2(&irJ7#$3{az7#K(p17qYmpjjY1qO> z&otQ%HZ(onrQ_CDc30?&pw!-z$<=K}UIu`cwAsX^Eqwz3>+=Kvll{hZSnb$R8Yqg4 zN#G}OPNk~WG#(?hT@RM?*P0-Vz+F`Pp;iSIPl;K<%0RgBLIz^zEK0v-f=xs-cehqo0Jj2HrM494apKEV$8Jcg0+`7A0oS5ynNM zEGCD*K^*)V*V#nyv{#|bpIz(*I_mqx!0LtX$O?{8#I~2ht9KgEGc*H@3ZF~)22DQv zE^+MOOg=M_b@l12ueiG%dWSOGr(rY=?#^JUlLaZ76a_TncH2l$vCt|?AssH+9(*0R z0C48&wzF-Gka5j|2Hx)d2&wLd_YPiOKj^rBAs@U9`N~>+9reuQ_N5oFy@ZtrgO_gP z5o@@}HkmN5Zfm)i9%j!EozB$hc3PaFS$C$dpQW+XG&cWLHjioJvM!EG{~P(r!jc;h zQ|z)rUg;<#TaLiuoj6pI{Czp=vn)r@ip#P^TXrBM-MWR{eoT~dHaII8sAG1rP+9;A zC|kn9_@ribfg*-t$F(h6J~7+&xL}0kYRxhXCM{d{RI|4~o5To`0OI1QHq2+xm$RCb zmh`0p&gR#Y@I>)DtbCc@bC6@eYhOsS{iHjGu!S`#H7*a(d|@yEyc(r zrfADVXRHyf1hbq%t$Kh1fyM6Q#uHpk) z6@GB_L$k)C`iY>_gF_eeK(pwvnLH0hK9&@{D5h?W4`%j@kA%>>!zhp8*;K24T4B~m zH;H#G8>lzoh5`Vh8@jJ%>Bl9`xrG`DvY+qof2-DV8Mu@;S~pd#9;h$SzJoq|wxD9@ ztN73*xcp*$2MxWy?B2!i!@u~JD%bO3>;FV*>(1{f^Eb%j4eIF}4+Hb5_P-j11 zI8V7UzY9g)xeLbQE|Rh}`ogg>cfI8;Rr`aNzUeQ1<{f|kq4z!gde$6RQ5tH0_gky% zF5qCG?_$kLM-Qzl))FT=RL*R7TC3LE?zDm^+wMqYt&{8#5w0)OI+T8~mC+HZOE72R07^H1LVaNAa zD3f-Jc~vTmiqIfN4j7;XVGDRJD2f-FUXYN?rU0ND8-POq*jiDRXfzGhI*5*M$|ALH zHStJACR!tp$3WD@Caj(YO^4jjIqm#u?6*Bwj*>PQ`S(9z|c}Dgw+{^66gnl3M_GGsAOo9 zx2BGibHf>Z#+r)Bxiu}+;dN6iyG3Q=E7)ybHB_m_Sii&_Z`(i}4{{2LyiweYIbK=V zjA0V>R#tp6u-OjW#YIZ$s1CEWLz5Tr+gWIY5m}7RbdKi%>BwZ(%8++-WHRny`1X1#)4CaxXDAme^2Oo&@bY>`9`~$2+kF+@lntsELbBP`8$)>&mvt`?GAmm zVE#ZBrKTBZ@+dia#ex_T4>X2^5mU=C{0yM62rHNJ)7_PXd z&n~9ebWS?FL9O7_)xT}Wc0cq{9Hrml13WZ%lZ+l3FU9_!PY$NqHIu|Ki8_gEGFKga&G`PiSf z9Q)Igj{WItkNxTVV}H8v*q<&s_NV{Cdg5dC`9o`b`?WQ`eRz#;P{}dd`@|aG9$e$w zAFlE3kJtG2XKQ@>^EJLbw8pp3t?}*QHNL$@R`D@d;0S1R5f8Q5(vQLa3z+s8S+)+vSy;*U=a<$xN0b>zxko zO;>V+;^T$yTC8q#0u`s)My;Jmyq8?5i@}AGB5oT`YQk{82m_agHhJOOp^@w)e+0aY zdTa+Q1etV-KPVDlm!+5)+EP$R1#mYwVN&}91Er%1IEh*3`yvYHfOn zu4Id&98QC}>3vwh63L%2>v)lU!y#PSnzEkpm^*STA@;z zf*OceFoM51f?s}#g1;?-zx7KL{EH&^+rQ+%7v=Aaz+YNvwUkl5u1cMxe8n}5&P8=b z`4&xu!4u^}kD`1wNZIzSm86X^jK~t@YZpz`7Ui?AYIVv_Qm>_$s0ylPB)tj-b7RDg z#!K5ea26S-)kbT(jkstavew67}IVnN_*jbe*tayW1ynzg)quTORCD2CI7US_u zLQEVn7^_ZomLQ6QV$*JU)9%zPB`dXQO4bf|%p^n680Zm2a_Fw$)rnfcbZ8WV+HKVx zq8aE4AUEho9Kvv>9Q4Ih1FB~OBhO=#qF^}x5dk`)4p}dSU|+esH%WH}2YKBLf_6-@ zISn8gG6klP-wYYsI!V?^p&jUAlpDm_kO4OrmBw*_mQtI#3M}VNZ!n{C;t>4;fj0$> zz=3x38dyS3;dfp~RQ+yW*PM2IGdd9Y%K7_FjbPWUmrj5CjtoA6V**6q0#f)ar@ zMi012Do)JdWMN&+1xE0*V@RZ^oFHUWFoP@@8>6qXxXhGc3b(k3c&-kX2$YC($6Pxn zE(1^-3Q9gI=o z$d0b1Wnd#fQ|cJoSzXs_Jj@)LZa}1gXKLAivO(N5*gB{rT4dTE;h}ORR48TN^)p!o`EKSdvHF;K?R@I1}m8Q*gATuoeLc4Qb z1sQpz4ZaB}IZ;QM-%~xFX;h~0Ms-ncJdOHzO`{g|271>OO%j}@nkpE5qQ_8{h=-)n z`Vk$reYiYR{cc&S+^2IM+o#V|$r+Kb9SS=riMSe>Fd$s}p;0!J=P?9keS@+^Ci#6L z1a8qvv;!@0qj+XimS`=L)wy2T=TV}W6`0ejqU-Hl)w+3IV}w#7n+{F{L42O0x3ks2 zD6DLAlZ+;;2O+Dcl>9c3*8YJ@38)%S5 z(1Yyv*9mF1{k_L9VsWF!M`t`CjL!{XcBxB<0>(AkwRw5{;fyEf&J2c9j7Lsdl1N6c zn$D0u0ycpsJx%CW$>#e-Y6teg(@7^6({YR!e@%!Csj0ZoZDK&EYJ*_IQGwy963Q6sI#ckPHr>9zpD%gc{yVcMa*FLKz8cVIED#Ei8-bhnHQ2-RVcSs zcIYxED#;?zL+wU0M(%?PNQd)gEqBF1Ck(lcmB=Ec+3JbNDs5215(FsXq!>k4xvXRq zr9DDMkvu{1(+3>D#Ze#d2TiHTQVjQ097?Fc;$mg|_H?R5EQzhDevk_!fp{Ex(T>K` ze0w-bk{z5#(&O4Q*MO{{)2gh(6V{5F4dq$4S5MGkdKfXZRBm8#L=acwD3gKD;0hFS z7-A1`CJUD?p9i~&pON)cXiRAnupD2QEojuc<}jmI;f4$n(M=T4kv_&)L?A(S3k2ZA z=RgYRI{RA&fEmM)I+$p%#Ud5jN~mZFR`HF( zyVZ>9vWZ`2!i_iV!UzZk7U9Y!*nuobzE~C!Aah|H&LZ0rD0rx&GeYx>y$gYzD`aIP zep9Mb4itfsG$Urflf>CMR1%ZLwjG;j>+=Tcv4XjlPfBJWO0a{X*NLVjwfeTo!niBq zSMB5n`m!=!LqAyok^nM#vEnV}vBjBuO^zHHbnE^d!R- zK%>wTUVxCXN#af8)v^=`M+1z^R#P_)Kti%QHp_md*aw{rVB!&LQp2c^4&iE(%vtPa ztPL?^!;VP`qG*k}!L&(2dM2bBL0aU8DyGKA%pNMRTsqt|k%xd0CKEy-Ci@jZGCHI= zYCQSofWVs5>G!LSS?sksW2m#t;KGK_|%_@*ep zrMWTNc>wd0oh{Qc?ZlXiv-2q6Wk$4Y!dH$n1A&WYwJ6Z3lX#2@G6ou<$7h5%ql)=} z7S$~WW;AS&?lwb0M@eKVg1xdXi{QR;yt$nqTXvM+MFN8mwu4aoFP)sZc3G#D;mbW^l0$h(KP~*u55r4UG+wE zrHf{aq|}ti&$6j@qca_n$tWwPkqFGhDTc0S#*X!VeEEduiCZJ3SO7QG*^UO8M6N2h zjJe-gj6&@5xfsbc-$N#hZXF$&Fqmf+c)DkWdBurXFp*X`Vx>I}8{b{QvILPv>B}<| zzQVT&8cGm1jjipH-F2-;x(jrv}q7zQ(paV)&3Q{W<$ ztV%EYLVXy*NeNq=G9+UMxP>-+w68)+j>1f%B3S>jf4{2ul$I_Lg~<5d4jnZMD8(b7 zDd|D)8A^pa_`pUYn`QO-O7m{7_ITo4lHY5uhDKN_CkIXIOEW4(uHy zc$|&mmT;Y_WtP+Wk7SyxA=B!2=46=1vKvamf>q?LA{(M9P)Khof`6teig3jB^hEH0 z3Y1Aq$!lj=2()QYi~0wZOhRXs;#@O29#?;&e?ic}5lGYdqWH^mlDM1jL~pBfx0N#h!vudJ2Jh5!+0EBZL9D8UhfUm7+sdF@&4GtsqX!$Ry@ zL)Qd+_77v!ZlZ^hs3M1WYlg*hQ1c38dR7oAP<@*r9vZNF{)*xQWM{lJ7U$cZRlRND z))+k(6kXwrLOHW?2I9${jL8C)c6+F}I}2h{M~Qjs>Y}ncY*Qzns9-xZ$>SnnCCMux zOp%W5g44d9uXF353l?q}mo|Y}yK#-cEOycX=mPpf{Y~8nVgY*#zjNV}kya8^TIB_u zL9=Ko2Za$nKnU2{KtSiW)))ea#g7oDKouk0ia4WaRoWIwOXRXeOx6SEd786<{1OV( zu|B;YqhG^5qhl|X&4SECSHuS&q;*hrgp1p{ zS^6A9&X8}+%oxP&PeO<^2Ivy@glC)Nf@H>cmUc0b zsaYi(T?|C(15oM*f;bY8_E^YIQy)7Pbc+}OP+*$*ER@yYe3JRN^M|p$HChXyVZJGx zV>I!vW<@5h$T5lem_td-XKfnJwKzB}M_8Tt(v?pN-WdT#=EHO!F<%Ui3Fl(1 zCXhQ#Sd0-2mL$w1VbMwOeQU+c{KV8xCr$l?I9sQwAI;W<)u(>=OutemHrh*lZmq^%p$2V#8@d(7*Iu`F>O$ZG=|Pu zg~nt}tfVn$wpn;UAYYeFO^AHWjmkKa+;0prJ-;;{t;Hm8Xu?nnOP$<+YX$H%5=k;s zuSqj|k_-BJmA>w5zU~}pAQ9GTF2jw2=^1S|e)&lFhY^#^Hw_tT z1ekQK*X>+E)&UBBL}3!AWRr5CRnt6Utpc6Uio8*gG!Dj;K3N?s$U~=pPlJ^yaqDRW zOGF4*B0}@2MrsZ;64=>5I`c3KV!X7-U9*h*|J}00va$ zSWB3XR#YL3Xic8xCLV}!JXneGczKhE^H~DsDq1wsy{Z&V$4xf+dMU{Z(Owv;UpBs) zm$My^Wh=7Cn_FRqy^Dl|KdL1a;*MH;_nSJ!d$7kEq>*?JT=$-MeT1%*xMN4_;B!oO z00dKx&~6-e88PzyG-ZlM%tT{aI@cAU(jjz^$r56E?B`e~HT#X` zTe7UogahvIcpD+RmbCZK_TTl{WH`>ruEaPgCQos8M_4?gT2GNuJDC5?`~T10y9Zl#odtgTaqjcnd;4^&CAHOUYoFsb zw4r5pT#ei#i}5`|(E|xAW|WDB@*k=~2~Kz0sN1sQsxgwl9!3;`Xn>he!AV>df~G*O z2r`qR7?LOhGr&xEWH7{}oQeNPQm#xYbzwcXnpL1?^OD)*~#@KS-efHjK zugAB(_xjeih($TiCQsMdbC-5?FJHMa>KeywZN#2WX8`Ci0RS>wRrq~xZ|=^`pj(p< zfJb2+pm_oP#K!RWb9ZYg$sfR^h-kd33B>QO(AO(E<5J^j&j3sPO^=n|np-nSBaz9Qz0g0es`p1_u zOSZYf8&n2E5BHH5y|WS+LtN7p9G+yW+pxMh0P!F?0IU}?tK;A=wHPApvtGh*l2Yn! zLm;wcGkSzxuihq7Svk_q)IrreED{zAvnb z&|RV;Q-M#-dfi@v3<;>Ye|m_TN6R&jO3l5Bzg#n-MwsGCp6=Qs{;hR~=pGf2oLbCc zgaBDkY>9ZHkx#bDF`I%e?>FPvO(B~1JLO(;7jwvkH7@R3)m7OiA19CXqOH z@0`TmdsA{AhqVDK5+Ypm(M9UT`4Xk3;ij;103>1w!Fu{jPwGc6UKRG<2 zvQy0?Q|m-DJknJlk&OkG^3=Gglm(1qUY7&xs_|!Q{Uy;67MuJ8XZ3}7U1G|Rk936Z zb+!@g(pAKiHH1oWTxKJ>@d2DT0>%P)b!F%c%FSpXE$=Ym_%V7w_4`XDC@BddloUXZ zRvJgoL9BJ0`4H{ZENs#xxCpK|qLK{j;@Bzx={2!+IWf`on#XW zTYe38U>j$|JPhY~kZ0g@C60o)C17Ih)3j-30wSdJmO{c%K6QHpp2hyS`nUqAx@+SH zJ|U0eN&WFLtYcnlK90u!R2Nk7sMkT7U2|8(4)xdtK9T?gT5Bj@^TI6X9W~ZNnPbKb zz{`lf4IsY;(AONPOBKPG(yyTSpb7!DD&yD5Dq+X3?^bAhml&Dz*4BoYrDRF!F*Bi4AZNu!c!A%x^(azd9x` zj4{Ru->FtoCln_M;Yi}6OtAt(j==B4=M0vO7% zjf0WF*3;NvF3=h@mNL87X*w)j%SyEWQ9n+*6&@l?Wk*t5UY?l=5Luc3|9JJPqZ0RGTd5A6md zc{V+LZPa^ojhE(R?t+XSmVqz8)9!b*P6?V41giju7z1=Y0kA=(QH+{SQEUl>v=&q0 z8c%Zpy*?fJ=?W4pmLU`O#c@Yf`)a5|znBnr&A|GBbHGX;4uGJlnLh+;Idx>L^-EB?ho{{AN3ThtsT{eW}Lbd>=0v+&)4wy7Rba| z5M>H&TDN`l0|XQq=>QePipOgQ6w2~EWQ|z;ILO7gNSq=y$&v7<#Og)n3yPspZdk2w zXhEqiZ}fA(S3wkGsG|K4eS7+z*|xit0Faa`-o+Rh^!oBZXZRAkb)*sgW3I?!FuD8ob87oY*W9(-Nc242H(XNdLErd$MYv??p1 zi=8ow8Z;~Z!uB8^27jMKZYq6TRjdbB+B#%^Mc{ixQKO&=GA1*c4DD2vr|{qO(%TaR zJ`j&?E%>h?SA4zXRe?<6uoM?$VZZ|MEMq|F)NQeaM#>7g-#{D5fv9$sreh&8xLG)A zg1el-&mhmA*W=G@v$-s07S_1R$Om{AMjr5fJ&e5V8M&6LaT@13rXv|SUYaFF4nc%~ zgskb)K~BDrSQofEkW|bktGIIVh$i5>;zVR_!->dPgX@d`y#PqkPV`=u3_w_(zf2mr zD+G6I*DmvXqB<6wn`sz${xZi%HZ9K&b}974hvZ{2bx8?Pw5AXmcz(OCq+z-1TQKlqbiplb{Mu&8jE?^A~f zM+z8SHKV0)1OzVke80S)bg}&GKeJKzdJhlzdOf){8Tx^Tax(?{f=g#(48ZJABFmI9 zOaU|lrRQ+b1(yP47ONDioH~f=N>1&kY~{595s<$WcTMHgL=(zGARZ%Wi;jzutcr1MkiVuyX%5}o5q;S=;xuhO{f77&_ zA;~mCNIUQoF4)we^n*(}slwI^4#4zyKAc68`?0@C8sK?vr39JEId}Am)zrI)*B5#k zke{Ay5VX%6L~ujL;kpqS20du*b{pl8-}rDHpQUB#yN1ps#QPoOBe|@|ud~oI zmvh%J0%(J`XTmyGN(Wyld(>DYFV+(6xulMKj3xEOUZ*vr^iW#uVOa3OUJeW@21Y0b z5lKx);EpMtA;7|E#UK!@w~_|`r5J>NZ!kxeT=2186x!YLImL`-5NuatB_Y_BSP4K| zW+Q8<$^{#VMO9R})KPmAhdmJsmM!p3rgk=HdCA4)2*JfQRRvtHSf@au(KuT;Sl+Vk zOrzso%E611i3bzvtr@_)kcotIM@)-C;cB@k5qH61qq%*%8vqDgr*3#@3_)hT7=T3( z9Wj7xRfp12t06+-%ZDk~u%kQ>HAgmqgb)lAQFeN2*(O$*A>aNEmw4XBjSM=dmE&oTdh~80|LE3 zB~~vA4D@r79xuVd>X`A6@d*oh-NOP`prKq5 z;}AogYvC7>3&mSDkrY)qeDs-X3a^mqY29!bY#brPf37+&ROL-jY$(lPrwby~gPLqS z+6+~hz}K#XGhAI&F5wL9AR1PRKwm1Lmno8laE2Tu8)**?F$C2s4G~R`t-k%*T_3s= zL)?xbZeQmR+3i|}xUdGUe~7f@*Lq{FF+`_v$GD~v=^}m+_KQG^UkrnlL3~>x@0Nu} zxZw#jtcfDJSiQ(-TGI-HtBw7&DR*z{rg_z(i;Sa0)T+s;c_Oh**^xp<27xOQD2Zq;(aI^0? zj@sz=JD_C}vAT@Gz;aWAaZF*ujCy3K=g$*40FHbjv4uSMI8Ot!t+<|_xZuamXFWf* z`s&outCOp*o~tiRzN2fIq4*MVqfyST^!24=Np^eJf+Z_LVO$vi%-y8Qn`3#+P z?Fseo1`h0^U*@gI72YP+h1+if|3-6M$Z8RfUSh)pA2V_HA5X(?V?2q7vJ?nWQ`nvu zHX;Kc=Gj8bPazNtSK5qjAtJR?T;s{IxMer^kb8a?_(08(oQv{GWOa~q}Z_3 zj){kV@hH(2snQ&w(x3lvG}x{4eD$OB0-|Pj*X1n6>(PPN)L|AdEzT){PHKF+-6YxX zVR{Nh+Dg0BO143O%Fu=?l=1)=?mMhK{1P~`*?3H(#(6?aZlJ*IhRpJ!iCfVYD{LAh z8>XnLc+xWDz5*8EO9kkS;Rni^R~}P`y$5~D!4=dP!R%Mmf{7!`D)AhL#{aBFT+UHG z6KS7=Mp69GCbiRWxFL@&^Gn00df+(gluJsNLK;TXwJ}GY58^y>sq`S43NxjbXoSpw zrz-vte7UMy6olBDDQpi2zRV*ZE1m%)DtS(&v=6JMo)DGXe#FYRBGhcCPP!BAWOi9=p|uB?w@0tF#+D*Od_R$`Wgle z5diMFYP&s$LrhX)pfyA$Q;_&1vAx&&w$x%4o2(g2r9A6~XYK*aw;0*w?P=OQ6NXYY z82ddRBNm8p;Qv~T8)r9pp>v;^>-f1ljTv0*PBUbSe+0ga=Kb!p*cIOIPBZw--Dy+y zk7?bi({2?w!D+rcNI{Mu8(<>u+ZzX_GA{|3uo!7;%dX?u_tFemEug~OubqCi0H&%v zQR@K*jIIxGgVYDXH1HoC!g}y75366SpJ!So^b;XQ@nRc(Hn9G4b&+0Mh z)v0r5{~Rtsh>f_*GGC+-8bMkO{e~(w;i$}cOvQGX!ibcMOh#-D+Lf7wBT-7(#IR<6 zWNaAX=VrJ3x(Y51?)EECQ7ntJs{4{0HCUZlAd5}4QiVCG*x?G)kT%^;s9KT(AV}V61#yt#`22S{g{Z<#|qx;yjS!DzR)lJHvbF39D0q&3Tcp^YU}BVGDh>o*gNROcxSdt~9len|%2&)7f-KE})yji|CqZp$tAWjIp{G{D1o1 z%eh6lI|4I#zJ|uwp{r;`j6oo$n!9N?;y{+Q2m6vXD^rJ&F#H!~O~+li=?o+fj@W_|L#u&*FuiN+Ls>DSTk6yG0!*_ldFCcDUGUm? z!I2+GBIdGmL0mK?+!h_&%GL2r*=I8)30WD}4JEM2S&7O>G< z*JK6y@lp~>ho-UGW7=qe^eWW7ikOFa87O%8N)&8hDTk?2oD+dEliX}uEP~1O6DPac zrI>`BUTN2Xww)N(cz_SV3Ib=Cmg40YrUlhw%lKW?9Ys=cZg74O|7%H6L@oF|o&=_@ zwknS{hq#7abne~Z6ZO4*i%`E0s^8rn!fZRH#!}~JWojZ38B8AwVq_kgkELpbQ;CfCEk-u==^N5 z%AJ~zs2o9VgWIIIP;!b#=8!4E1A-0VkyFjorHHqWBw1zpIi?4W1IL^E&IT&+=%+euVpYS3+}5#lO7S5#MBSTZ2~_ zjtt-GpO?4-Kpu7=0wd54RlqDl)S+e_tRJvHt61TRmLFReAPLC}9X6b{&t^(2OX%q6 zDbT?T0YLpiB+Q5al=R(t;E}%DwSl%)Vuo@7vUm^iCI<tHodSQErPHaV#dZ@AS{#N`(Hpn;+LKTlKrG6h6p_h5h|cR^H+l=C z&#J-GCuXYRxrMYH?3MI@mmD05?jf57oFcSj%sL4hnQsp`5w1_8!QLy^;vch;H)4D%6Iw?fDQ^q2O!%g zNpmCP*tn!lYIHxx{*XN9{<3t8wQV!eNH*a|!SiUq`1zK4scBIGBuop`AFC%PL2_+A z^p4xAOG?Yi-BF#SgYJdv>ThbgyEZ;{SNm*aWwh;y`cFA+WNQ(# z{lAZg{T;;<=d!MRV%B1~3nA45xTcFln=~kB)!r$R)_cfU0Xd~#3Y{Qu?}BorfO3fwdyxfp=d%zxpN zKVqg_#s=8^CUYAjFmhwc2A^Hj?=*m6<62;dyv34+Um)4&$TAqx7XM0sp){m)P_QBR zm_QfyxiH6>W?9`6y_`An$_aS9lS+dpKz=YMC%l;D{6df0VP+}BMORiS!AYAWYqIRo z3>o5AWSfez$FeLqaa(mFLWQY-;BYH|lDaihOUBn-ke2nF4C43Kso(~-c`EDS6YtJu z1*h}N)9&z%I?hD+s9%!ugGih#8@;83n_R=dWt6thh69V9b8Rzz#krVkd}hG{kMcp( zQHtHo{z^Z*uOAN)A=dXGwIHSM%gP?SJswc*AvCFc_ONSp(JL-n>75J!+x&l8awHB19kSd+>Nx@Bs<~(Z}+e59l?CdM@7HT+CmO-`+f)^>DEs zO4x&sWd%=UsV8n{^GJO8(c3Y^<%%TM);cI05oo~FKcrCZS$mI@8% zJ!WT&{nt`fR7r$TpSxSyeSo#-tgBOvosCt3P^p{J*?dK3mtSRP^A(+4e${sticWS* zuL0S3`~!7aykoV_`KV@PS9vbl66o!Fjk=s2>LVxFn09CpJ9)@0>U%~{?sXG4%dr%aZ*c;3 zhuoX)TdHkeX0@!(snzT2GptTza@Udw4?U!qGx%MhEmqSsw&~7iP!z~x&EP(Ra3*dA z-G`ZI$V0`y{12C-y3>Zb6MuT$>HEWWjlLOuRQd+p)3>(lcCR1U<=vdV&zt)Rw(hpk zfpBl3aGBC`TfIwTlJ4KgzL#ycJgvGF9;kTp3KYX{O)Yt!Q$sH{VLUM z@?-yk<(O|T_vHmnZfh`TFK|!aJ!;oB;e;)BE&D0FoMI%bQ*Qek70C_oUX{bZHacM- z$TDJR1-J%*7YL_P@w=fH8|Lmt-HliR>JMZXoTuO%ZR3S@Yb!JGsP`CtDbd&LFAL(1 zi|`nH?)pGU{D~WyKC?Dpzw}5AImRR0fIGsRwV*vd72bo*9eMc=H62saxz)YmBeXrypGp$EH*OGmGd;YRRL z!WFR=(+F$zVNM%Nl1a7~6cT12v-}K#m7mE(U{CLbM(|+v{H&S3mzv>G(Cx@2yb~1U za*WSP`9{%QK4B9UMb(mE{EnQCIKfB*TXc@xaRs~)vqG6a$*%g2sicV2@flf-#Q7$1 zo^I}!!pT-=eIsN-PKd} z`5^h-@o)FqCyv(``scD|43lAmYD^1)0QSfufn%fipdEf}jOU;v*eFsj`xNDcK4p&` zu6UOr56pCVdX%gg0Rbvplw-PaN3_I^LO_M@lrE@o#6IxwS5WVuTBXxKzY|}mvy6ZD zUyBjp;fM?m8i~hWb!ez&W2iY4X?*_o@LOalMmK)C`AfeU2oP3UvHy>z&33}F@2OAb zC#ShQ|6ZxO8vpdT*<;&N@~RQ!FtSp%;mQV34w7rcrOP`4!hM)|m)lL~!>}*NVhwz$ z-&ubhp42($nts(&gI{%PzW(HL#;HsbKWb?F2ZR=*mFeuF)M^CX^{lEQepiMzI95%0)f-EAl&R&IBP7b&FbinGRCb9oy=SQKYotknfFM2|bG%lqa5t+(i_^9l zw=NtW`3??%8CoOzPq%2z#W}*v)-U7!ybRUE(%%MWYXlCLs>5{jHwcf&lHuUHgcs;1 zl++lGYc2j@^$sVZaniAF=$XWj(lgJztN?4CEV+nchlI=6Krt3WT=ze!>ciI^^>R?J ze8stF)#YL?)@VXOH2D z-XR4y1O-%3PmhK`Vf<+Uhq(!^ZYs;_$=6#F{bF-QYHBWly|W|V`&W2uhU_C@t{o=7 zNw7@>hnU+Pd4a$%ooDRQq8#H6ZF>E-s=^w4uTvhof!GniqxpL;K{W-H$)bf1{K8&S z)hNaQ?|!yzI5L(Mm;_9F0gs0s|H2dVXTDT%DmisrR*lB*?h1yb+pCneU^Aca)iaR6 zyKJJhSC4^GjiSkk^UCLoVhZjtXC5|5u>YVS9LP%hr&8D$ll4d(UqP)JT>{uJkc zegOK7OlNtRijK5^NBYq!~xlpW}J z(m1ue7f-nuCQeGJtS>nO?fmGI7t!!L?s918OBorXlc}=5+(NH-AQBPn>@2WC=?t@0 zJ+aS=hcM^zaC7l+r#wWZgDD>x|6Z?D9Pp-?bQ;_-mL{Kvw_B~JQTY`5OstG4pG>nRe}r{J z#zRZl&~`UY0fp~}lfw{Dq^E*4DWDp@X_Ka#ePzg{8Rwieb&Y_sOR>lSBZ%AyCRd{) zb9n2LgyIZ_lu)#r5~?&Oc`TX>a!(1>Tt*?5%fbYQ4M0 zZn{lR`GF>)go->#tSQN@n5#~8u@_GbbH|c@0MVboFoY8ycAYs@a|0{-Cc+6m;7kN# zx`1#}hh%7>D45l;%tSAo%q5ioo^Aj{*u%7HJ;#CohF*nMSto#j+szJjSKg1b3K`?X zu;k>T)H7X*Ch(C7rydM8MN_VF8hl8F;kM%^qUj3QL1(>aDy12WZa$drm9j8Wea`+9$Q{LZR+ zPi6Y2zdwF^MX)oglJoyEe;UQ{v-y*$$aHpUsyx1BvJ+n5e|{uGNz5`jQfbt6+P%BQ z4*|pS21C0%#)?Lr^x0-;l>S10gzZrH6y7CF(aGb9B$bnYx;liV;S552!5MF>ZjIpb z#UPH-nV%lR0&b6<|0j!{|Lv>yoWoP>c}SDns&2ZToRnyQft=P;JkHNd)+Ya;MgTnf z8=+6D?gH|-I3_S+0twB}_T?zWX1w_qL#4iIY1BvIk4ofH8b!0~B>A6zA^WQa`uTno z>+{d{qhQCLn1QS7GMr(3eu8nV0U;Ek)SG>)2W*Na4xxvYO4Mn1ztJ*`7K-)3(VT9^ zp)#RM#>bm|WsELfBZ2Wh(_DdkY-P)3X=+Do_Mlecsdq*629!iXXFIO6OT(FJLBGp2c- z!bEeJpBUO<-X{gT3*`BU?Vtb!#`D`j0jK!+Z2s){`Aq(lME+oFAdxTTPf6qp`BN~& z6crMA*8KzYYz(nu#7Ihr~W#SVf`H(CnB;8 zD#`q$EJk8Mlz6c@Hc&MF1{b(9DvDp?8ZdT>;b`6k%5~EwDj+i zPkiH_W3$X78zD_)pDUFVm$X$HsJoRBYkjoO))29_MSDX4DRFT~A|fClKEmw?v+esr zWKRe^{C4&!3zDlY`Zk46I)VtuI5r#ikqD|@ir7N{CMJx5m?DxeQmcqWtPGD>JZ3f* zF*s94apt&?g0Vb&%+_*2Sg*MLL2Wxc?^d%x{|PlZuQ4dt8KI2mCY*#i^;jUl=vrOk z&Gt$|D&g*)ry6q%mT@!1IR{SAwWpO*t`rhOi>9;NXQYLoKnEAaa<>r^Z1plor-XUM;wzNH83j%I_j30@Hdi5jW6@^A09v zaR$c&tR&f7KPcUbZb)CB|K#=?)6&XB8lE0UKnd;G2!Dm1a-e+Q(RAP<_$E^YB$z%4 zKS9isQxy*0B1M@!q3{mBF5Rj7tkxFyMtCct`RKZU<{ zqn8b($(7t1^$nu z@_(Ylq^`z4Q#TDO>GVh61uKo(HnUNrA2LCaGu4eEJyB5oy4DMA+_m|!*(eHhBOjq0 z=kC7mp5OXm=mV|zgu|0WRsA;^?^7ndY35I{9W$WDtEWG#(9jmIiGC(@>07F*+Rr_j z*cfx1+7QQ$IhMDf`Pxw&2lk8|pmPHr_#BcwGq2^1&G=ig6jO0V4#D+oxt=K3u3S%+ zYi~9_NI>&cyJEZ@l>*_X$D?Y#?w@A--_}gs0#pbELAAC5Dj4lr0t;5t1+Z8z7FcTn zi}_rDaxLK2nKwW%B}jk_^6SN}eerv#Ix;ltNJ65%5H_1OM1YGO1OYGvYjYv&3}*le z3W)Or*6BKa!cXgUeCk^8Q_Ba0B29qso&rLiOCAZuyUX=-xgLb>x$^w|0~CLxZ8kGX z120GD<_WTGK?ayXINZjGV%rP2gn``T@F*Q)&!vwwJ%Thtm_wj9oMb#=^-?V!*aVTKxf7I@p#Gz3wAaB(SZxk2?EhMf07@F*K|5$xs1x zp0?ssJs4Ih8b%T)5p9I24S=pz3uA=my>cyaXw%00tm7)RFbZ%2_4dbh^O!8(PG)2G)w7zz8t?#pHoLaI-GR#2^8Ml^K1h7+#qn;7v&fu+2(V?S zt8MpTlz6jA(ya4FKd;2eDw5ZTBoO8xQFNhO)4dQLtD_fEr?Bmayo$Ay^ER%26;3ak zbhi=gQAhM!yquZdFrT1K9ZO{|E~b)Ya=3^%G08MD#EUq)5ijC_K8I<5`?fnAqK+H^ z#0_w$1XTN`gzGt%Sb?>WyA_}B+&;`>m>-$l*xfj9NNv>3>(1Y9o19WRJD1p(2t`lb zwfwP5(-{|zgYZl=w(Pu_3)vRl%4Kx_^dz8r6AiLnm39aXkGm<>V-VY|-B>drW-fN4 zP!#1D?V-M)YcUo@*bg+@JU+YWiRsbq28-@=H!U;Jx6uY$0^Q5m#$L{^>(+E(26kxt zEtoA|ciaUxqN;snB<3`F5Zfm4XE-B!G>RSa7MokQ)f2czVl|E@eV31PV^!gGTzq@J zywDvwUf%UZ_J$&JHxF)7Eqm+EA1Fl-5fuR%XqzdMve_W*mcYtYh;IF2>@(QA=$_T? z$i+BwW1V47x;IBKY0?0;q@C$&kGsA8E?*w%Z;{0eRgAwpJ7RYzz1(HjRokfHt7nOg063JSl}5@D{7|YvbBxI;YyB_#XsHByR`oFJ&u_*?z(%M137w$ zUwxG~^_XS3*j?f$Pzx}^DUsultwb7KT(b<-MtdDFa=~*o+A{ClyoK}1GL>`ew4NNR6&=;L(Uep zBArvDy8+w>z*{=dI!w$^2Q}Lnz>DkY3h?4O&-aU$uL@q&+CM??vPBxCMG3m`+ox|r z73J$&fHN&1?M|bbLN_WFaC2~h6CHd^~<*#6WKl3{K`|zJqfA!M5 z9{4``W%gHG3`;zm36t5Djs?u@bUVU@2#V0wmZ^rVjgZz7-?GSHr)!CCX+X9eSyI4m z7q4*YW#DT2VmHDP-|8)KFj5MjYEg695|2RUYR-3PZJq&vf?`Q^b=)y5o5do526?z1 zoAJL>he7^pz3Khvl0k09$%T1N%>K&Va$dsutC;TJ^~Sd{{^4;wK@t_)9zhEiDVBYx zF?rmQyIfIri)Bv{9qQgjw1uZg4QB31{1wPV%A6$ z{CvLVF>lfSljw3-3RsXg03{)j-)qwC_ zzP`?Ab)YP=@EKM;+i(E7gcjj2;Nn5NC)#DnS;Lv1;{oqC2E0d4ILcIqM-kV*8%;vzFhQ&i>k3^UI1DIQt$=~@LK_K@PSu?YHozGXjc*Z7v8hjdf}1jQrDHV}HTTSGiHy?Cr(GcZ}I zJ}4e3&ic-r?;wdH9xJO!@rb@VerrmqSCVJ3G=tz^@n`(0qxJ4$CnY%Ve;r;V-p|1I z6V$x7URQ0iHe&ZBf%)tgVF?}Nlo%-SkozLRPyX-q%z0SF7*&;ZH6)QNGEQOL=8D^m z)bTpnc2R7rDI>LPnwYgWdSY;TUTtEqKx1M+Vp8)h$cDHP$;R*zv5IRU8#YCFp0Q>uE6u1JyIdg~ zLVeVjC7rz`(%I{MP|*M^*d1lkkDiA2)Y1=8vd{iF&Gt3qm$#v6K{d#7L1`g1c%B-|iXbH15?>Xc7H}x?enj7sF`hefn_=hXLLecbL`D$8) z5{nGEknv&EV~_hXC#hLQ6#YCuI?c{ogihp~j8L7$t5dwnROYeKAConri!hKghLusf zSI*5dX!LZI1SIE42ta+QOBapW&g`vMqY+jGGVV^}4%PlKD~j)_CVs7nxcc}NNn7F< zX~c~~4ERPAai_v?0>zdKyQXwQ;85|TakeA7tk4G;=`!bY?( zSyhlR9~8Y7CS1jD*9tEV+l<8XQ2Gl!r<$$@ib}yVL3nNrVx??;zH{Mafz9_VDI2p z^G|iRg9X+6MH07iUrOJXzW~HT1S+%l@sB=iezEGVrz1noUEjx}$0Kvu{3qTV63Wu7|&VT$gU_n>xIUs8xEE&y0 zgJP*V7`?0Ln;bx*);9hvl%I_LHdG&nYXE%$nUO0am<%|JE)c$M(r)h)YlG-9s z$$u3=0#nUm#-6N`XDaAcGTzNNq%xN|-O-rv(<$(|8pT+y&s=;%A zF)B#b5Sx(5v*6vSs5LfUE;`z8TPz)gX~e{qZq&1C{wzC`toQHU7WKNT=>)Sr{*CSa zY$moWV6M&U>0Sf!i4gqXT}rb_E;eFSo{!VOaB2#>q0k8mfvnP~JM)NU<@RJ!3Z@}s zTuMfs4T@(eU$&ZWHHFs2)HH4|&^%E#^&wPS^D0cGGzteAGZRelrH?g+! zpXfKSLJ)UU$0F`GHf=`O@un>jRHVtvAr;UNtE{p;b}QdE8y}CzUN(kvgz$06dQ-!S z7z%A&R%$6ns9GfdnnhJx^P{de7|letH*SORZ@mnEp0&r-9px-61abgO!e_uQS|nZD zZ57AHc5V59P(b3&x#-fR(_ws5s_B_5v=19c?Qf&k;po*>G1UKEhkr za!-wH!Y5#~u}0FSd4Z30M`+ZvF!+S8S{Dxm=q7uuM zRLneMKR-gvax`e(Hr{4>W?_`)h)77#1VVF8=X@?`$i-AfkN+bR|Y z@$C4FB+`0<__cmVW$*&OHtwjfy}~I&qlDVkxign#J;y_yBB1m3w$p7DzsS=N==KP` zM7=O+>WJiPsAx=9JQRpLBu6ZPHvu)~fyU%GA>`PsSac#TiSTl<^D)`PgQ_gZ2-Pj9 zqbz_-zMaTsqJ$F`hSj4q?TtT5B4o6Px~mGK3kEt02C5PvW3*ya77u!hh3KgNC~AtB z)!Mm6K&K8!d2UO0Fs7MLX$g+P2k1Id&gk36ufO5RYi-yMmKy$S&EbV$7hSp&q5-XP zY@r61zQzXL2&h~EOB-Nm_x-wnE9FACDj67c#z<>do}+o7FJMp=>{ZX4pgEc!j}FJ2xZ@MVUyx)GYQ@<0}Lh#AVY_D_J~O0 z?<_aB50DTKxCtOoS7ck1{W{k{DA{M4gHlj=Bj2KP?Zkb3MpYE04KY4t`jeR|g6hc; z=o|s`w(BW3{-0}7SG7+C2vlY0yocI5M3SJLnr*!R_@VIoIGK|qf)YP8sw0iyN-c+3 zou<<^Pz%-a??#+`ONCjpQuQP0;F z>2gInX9 zd8~>QC;iSp`9D^ZzkzVRcYK`Zwhiwh>_|{+)Q={A9TtA>n(s)eWg{~_&GJkD5BUGl z!{ciFLOpyrvQ~f}!*k!RzlaA)Y(S|#!HEI9kLSQ)(lK)G1F#VsNqMFOQc|?D1OwRT zjGN?bHXinbYYrWE6g>su!8@|WleuKW<74w|dyW=xJ0H%W{L|y{v(1;$(HPgY`^lR{ zKkZ=l>9iZY7_Esz&n`o;nX8^Gps3#QP0q-ik57BY_W4jDO79oLUyNH2#Ha%pG3tO9 zqrP&^vaMr>cRDr6K|dCS7o%%t$jAD!=r5VrJ21O~gkAxzXi$MOrBHfC^Ew3xfZq@CrpVRCgWF zp|wL{0)c+oN$IvjIL7IxLB+>l`HYDt@|rgHW>iqgq^ibu!g-k{I#MR$waDdvsm76l zdF^DC3yq96pJX~O`b+$)^@F6Q8-sm?VI99Ph22mZDjWSirr2T_(~Yd6Mw3Gebypx( zU8wE+SdXOc4&D^$=#G$%zap7g)E-IBn8P%^vv#^b)e{!{;v-#U+(T#H!wbBlW91j0 zx@R+b?$fm4Vs7ju+gSO`FX@9{4Z&-loA{F9@4BPztb^t@GC*HTSXTSrQ1>Oe}@WCF5X zy_#T@$@hZ{Y2J3({LR~P+>;U!bA-6J)}Dfl^gCCTO0-|mU=W;7c+e4GWzDuE=Fs`?y{xLueIdKxB8@+79|=6-(s3V;kJk27P-^8}ut zP>MBN@-s9opJsZfd{*#`XNXYJN5XJCA157euQLlKs%q()3WKRIZ3{jU430So*6xI2 z3W{o>fySwT7iXw%{!Q$)8e#&d&g7_0t$Z3gBWp} z60jPhS1=uuA8E(`;u|o+)}KOvk@sd^O)LJ@&Hn0t!4dbiDv+UB+t^s&kbPrgK7?u2 z7z@u<&5L3Cdi$^yI5|-Hy|YcIFpToFxxjZ{oe9GW3vC+5vg*m)(4Rj?LvX9R^{efd z_nZ9I-AU(K{*7Ah*u=visEaw~iYwATr)E3R2ai~Z7Vl0jOPTSfpZXXE-_8r1wXzY0 z9VRmM(FSD@q-Y>YDdz!HjOSJlyRh~t`?{@v?**n7gHruv-Qaf3%7%X}y}|&nH+x<4 z>8H9~n_Kf_m-37t{-wuxet1aO*!~=Hh!4#gPW=SBgAMuZPPezH&ND===p3WQ`6<%b z$K4mQS1bJ_IBmI)>=#1iz^Slx{+pWWscy&K0+(#jx1@Bqy!!57m;1E8(OdahjI^)$ zz_%bSJ%E9R85do_rT|}BGnw2Jt#nQ%kIkD1*LRHv7Liy)Zue296Dea{7^4RwNsU`I3` z;prgpKJ(vT&czV}AO}b**3|{4mg9sf&S;n+H0HSjquJ3W1dY9K^_?b@lXfpyt(jB_swp+r6v) z_>}bQ0IksVZfrt2^zrH8yGW?W1f(Qvfh1Vnrc^&W9?ZOrGP+bo_6i$cCQ6cy;Y2jdHt2nVtPgkXb zYtz8cwX4#=@b-1k!0>H223P54g$7;$|D%r8v6gCI3;thOi}(*40Qs(8EQ}`iuhPIl zv2a~9KpoORB~t`5V&*Do0K6Y)V5oVS1`OvDBdFwspqIV15)l!$zW*92$ZRR*G zouksyWXBmlXloW7D_si@%~&UrltvddcKiQ=j%FM4wR;KTUBA~frJ|rTg;U65?zqS` zajcLWK}Yg7&fu0h@f+2yMB*csI&)JSE@+eV5xhuz6pIsl8D95TfyO%#D;7rkx%kS3 zl@FJwE9Q7D(q2-lW9J~>RcEcs9a&u-RKj8%Yj}Lkid{Kml3%W7!3Cm6tb5^u52Me@ zLHAJ_fHkNyOhjG$g0W~yZ=t>P3&Ofw6@y@~9Nz~ZxP;%ziXvwO*}=>i;Vt5dkVe7>bpUja1SyE}w+Uly3!H-m@1z?mafr_w0p_ zdltHLjiaP7Em>O1IhAn6MO&q%{LWS*XcUq&cV@|3TKHt)ngJ0wOM!@MiaIs%G({A0 z9$CWj&U3E^8C#KY^tzD|7wT6M8L93Sk+Jr?(q&})JF()hf{kSH`0}t3W%3Qe##D7* z5gX}!!A8B)=igaJ&XdG8vYy1fS;h)>#(j^SWIc&{IKp*dqxnt(1E1*I`5HDZv&KIY z*ocGp4Z_BBJ^%T@#^=}TtrBdo(+g?Dk3yQAI0KP)8I_bL#s&v z`d&eR!6ZNK6=1g=tQGA5SzNVC8bVbxgmB{m^IY33+H`p&=OV zj)vg+ElQnQ!j_VLx%RbEdmk6STzl(x`~yN53R|fj2xMY}X>8q??%3-QkS9T?y}6uE zT??{kK7~t1Spox=o|4fmo&q#^Gu3+G>~Jm~Y>*mz1~y%#S%)4UaY3~c5KKmXRARjX zENcT;BJ@aAfu$Byg*fDKCnG`l{L=Vq6!EV&MX;9s22upI|GA@xFOKRlkuyv7TTQ;$ zWVLDZztqmrC*uQ%K_^xf9Unkzau zm{-?e%WG+g7q*ODwc|%o*1D)DXO}zU zJV@@WF{tt8jvoaE>hd=<)j3$UPqy@u#69DUdyKcZH&)K~7;kaEbATo9$dXy|&-u$) zE^U>T`gT^i@`hX)VLQxwF=vf+9iZYV6o@Qk|Hk-l)@+1N*FmLLf;Eh7TFt-kTU8vN zY%tpA%WqyYA-tX3=}uSxftXW6hztRYz5zFti#ld-2K5?8Lm2|971JJ<>HM!~97gD= z7(|TV`uO`A!j(ZN$lx&$jLV}l)C8Pn42#+gX`IuO`OXKyaV*qtJ5Hu&vN{@&q*OBk zk4_OvrTi7o6?r=sJ+uidz-M71R{apX@Wf-JCdljkw!b2P&QI#gezl(4|G4yDMZ(Zf zmojyzqhb0}Jcj$Lzcy3j=*n|r=yL}cc{V3;}?i;CRO7Gx9%VgANs@kp8+PUAL)aho*WTI@=>FoYP?5|RMDYj@XDXPn_q1r;XeU1IlgK+BvZD$j zd|8q^fD!3Zi)9xM+Ik|#TTYT1B5bJ1W2VR>=fW-E%ewgTkUZi+rgq&qeXt9wOTHFw z@8<^vaj%&q?lCLiVH6n%MkiTfe0!T?I+Fa}_T?097!c}r~f zlN$Kf@^8cx5mG*ua)I$JQp5zt!f(OK6bE28B|1P$et-A+@MotSgk5|7!@3&<5S16^ z|MIu0$-i;6Pp7G%bQ|h19uhzzV1YjIKpmiUtQ$+=CMnpwhfP}{h{9{G#EqmGM*$!k z*d|{3&yN)~D0+`bZ5_pez0l#Yn~`GQ5BqayJ6aP~A@bMMw8n5g$hW@GjX4j}0YevD zJUg1tCz(JEMs441Jd98_K@A<5)shQn!-zmyi3420mM>tV;j#3o`&vs|d)|SDE)YKV zwfVn_tu$`H2RGO~!>502FWV>dA7sDD)RV|hxxK&S^-E7rL80o&sOe(?*xG zD?sRp7`uSgYwo-SUXLreo+ecSpE3OYgWxF^+yg@a)ouu?ZqG}cUCunOdoJYp*^TBe zqNeldUPWJmUKkA!rzfNCxJNRAquyn$olTvHXErkJco3@Bv?C!&)zP3k zYQb8*qJ$iVz)ri-bT@Ukp3ej5Z_2jKK3M5Rs%T;AL}#-(Q!R@q!)SM3d9*=qPrE=| zcY8Jo(t*qg!{xe*EpRibY}mAxk8n@}ArTfmD@VgR4ol{D#KXT!eS~i#z5# zX9Sh%Sk`frJ32Vw4h~gKzAGCFX~$VQ9=%5AL}SU$xdbKXozg$#SWK^JexP+Y$;t_j z8?@^>=9c+?NKc@25AuMyP77kdgG2-w-UYD?k|mP~esHr7KHFcMnTxV?K$?S3Z9Z|?`NHvOuTW1)uSla4Q!pCN37y^S zNui-7#_-^`_sotnTuuNj9q-w}ZZ`%K3#soWsk;i@>_@Y~e!`R`qY?cW&4P$YM-17M z9RLai*Eeo4f06r-=$Q7F{GP$w7H3|loGGnkGMY*eImRX&HHw@!&xlcnGZ=kw@PSh(_jHwp ztt#+T+%|<&NQuUQNp*E^1>DRN=Jvugwp=j1bRhT{yb{LkKr(AhK`AbCtQ(Qd^HSmr zP!0=Cev}9yrL&1sp4(45`zsj3o|7JoQC=C0F<2Milw0~RL}qzd@FKaJry$UixS7{e zbeQ5lpiOnANS7e1=_Y~Qfq{dzD}K)wcga(3Q82ZT?pAV@O}R>BKt{Vd#<>Kc;jqZS z5{G||sRPSG)W6h2Ftt+o!q}04YzFr!nr^2CuiWmInY_t9*@9(dalOM)GBqhMc9cFnkY3u;#3&4~7Gh@q^Y?!l0gORAMn?TpBr4+i} z0NNWt*OcaU%wo{>Br%;@Lrfd%z(LIz4s?W(a<6gh26$7UF#!NM*_rAJZ6yMA+>kI3 zOf)?+#=gD?gI02MXLEGtl66Hb{pil-=l}tUA>XjkC4-64c*2tFG1w9T0(f>IBLDqG@hJ^ z>cB*CoJxYS1mC_HN&A)$%2?5>OCqQhs$1kB5GQXuyXg)5hr62*pH$q?nZk*K4 z9kb2N4t5BaN@DdwGKjPxf|N?i8*-)Su;IYc+OE!Ty`;v$j|Dz4=a`{< zCTr8{*ck?T36T!IryMh*&0NGK_fj*z^`6=2cpzLbInXnXFF=e&*8_thcntjlLtF)-t1DOv?^M(jU3Ie22x`b0929E$}4qZ0G#sP2qWjVD&uCny`+n z>Rk7xCGZKbH}wF+UFu7`P?JY3x(S%LAjzp8I0L|ZaTJ2%g^5@QyOk7qF$cgP1D(Kd zCz}SxsD(&Jn$s$JpWwK{JaC}-ZJf7a2X3}AQKWKK9!ybIM$&lFF zALDu(kG7Ng$pH__k>mjijSiuVJ}NGLXx8c~wQhz(L!oCugV(5W+vZ3M$~bOs?&BjB z`HF^K`2*lV@$iGBZ?HwpiMsBNS9+AwkB;<-oz$w9^Xgke%0jtM!hL}nM`5y9GmI%5 zvUKR2dD9vXz-eTWg*DzvpUHCpgjuYaKv zclcwdPo1tGK-8xr*!r0FVXEP#a4SK2C3YMb^Vlri4}zpzNal0J==EP+(%_*onERT{ zy}d}fZmCl?T$#RA65v-0D6-s$)H{MBsN;g)JPwl6tAy3Hz-gyJI{Op2cV&Nu7HZsR z00Nf2FeSz^Hk-wLpX{t83W?>1rYuH`p>oQ+se}4t81mmS)Fn#;Pvo`7K#|bu5wz;9 z!TgSOJ3Lm4ieKS3iC?iqz`KX1Zx&fBmF@<5Hhj&~`xb@*cW2@*ijeS2(}{;C7Y|Pj z4^dC(QBS zCGEVDowoZ7x+)#ghTv@TY>2t1H;aZwbRXot_jqI|TN%js3k?gyu_T6LeH8f7Om0Bq z{-SCh{$_a$I5*D4)R#C{3(tHmE6}#ET~~TyCZk<`+k%og+l`Ci z&L}3OS=uShqK43HsDCljrf*Q+qXjz4UYWmL2bP;%n8`dg`J1)Eel%ofR@)L;d4jXk zj43>s#FJ1XIMRf~Oh-w~#J}+;mLXjsNrZE|3g+KcWA@Q9B@Xb1GK^!JP^(SZrS+%^$-5u&l(2EPtpK;h)o2g;1ej2 zfVoA5LL#042}E5&0-y|#5Z4_yPYNV>5|F485(ZBP6t=)-M*gGq_Zg& zJbBA(E!~i4ZFY>lf7)crBa`#eAP?Hi+XSS~fObt1ftj127CIYwn=FpAA7(kat$Ocl z{bkk4NEl@vowiR6bdA(aRDIT&JlCggiU;Z%6$}nkR>{KViquWa{djX%TC;sIbPL-W-zG!g-op;^kIwV5?=jeLP13(2v`q)#*Sz>78;$Y;)R zaZ0R0fASYYg}M8@>#FhPIFo^HUIkm^nR?nlA6PHyQ8_XjP36Lr{v(h%;vxYK(FW4h z7L?ohRW*h7p)JKyXyFza|Hc}}uysvrEvskrSH`>}#&8{hSzKs1-3o{lq&`;2QdMcXc3gCN%dTlmGwBAi^oI;~f_9~hR2`j)%T<3;xas4jK^1?@r(OLtkA zdgaLAK5H&5$ZCi~?LcwQ4rmL&LwX0+RJ;ut%||iOu)Pr3Q8Q4=Jx9*IYimJSQ9pk1 zsCa7qOVhbPol;|2VhQJKUwNrDs)KnlX=XAR3x`0O@Y2*XQDau`%IhnA=)ByFhaigW z!i9K?0)|PPUIM4GqM8lxxS(LbN+&7iT`W4+yj7pEc3=gm@YQ{&m|%BN8#c`qY7uG6mFt0%sp|`H< z&!;EdI`N@}M@KPsz=Wc6o29+n_dnSaFQW;{1jQG24?9UM|(-oM< zcR48_RiP^!UaF=Jzhw@p0qYo2=Lk<2tR^pv!BuuL0J38zu0?O*i4yDnC_*n^GvBbW z1=d}4e4%q!lib@$I=}d(o$h8)^8y2_OorXW+)}SdMLu* zIFc5<8yu5Na6^$WcXNKi5L7#Ey<9nA$gInB5YtZEh$R9}DG4q!e=63;()i>151Nd8 z^et6YQRmP9t#h9LLN!D+aT!6(iaq`*l$p_DxhhrWCyhl^DBCRAKv~6buRPHOndff} zwE^cL057h-%*nV#PR3jKoLTjlk{ygT+4OVFFHj{mgKf(wW_TE3m(Es(x4{a8dIa*e zDo*H+^sw|+o>e^7nvBt7drq&TqlkcOceE-Uwe|B9E6F35I;xJi(oq31WB=Boqbz7G zb(Ht4lF;8U>&u3FEgS^n=KRg~$2n?p*dZOO6B@#XKjRx2s)JJ-@5>yI?A3Jd-vpo2 zyJDXxH<{e*6DznWJTc>UsOsk_TOg+OypoPz@a{3IcvNj>d|GV(Kk*n$eymP&fh5NB zK9jJ>{{F;yz8P;<^Uq|_@lUQ=PoRZ$^8Yw*n0^coJTYVnwocxI++l9c0b$OvqZLMi z)(6IJ=K@#r#QdjzE25Jkq-b>U#mT>iM`=QU?c@V9*ze@K!>#98K>>xkbr;i8If$s( zKcs+nQ0`lU7?dzooaeTM-U+r(%;wn$pA+Py#LY@#`vgEe^fwo<$9)E zPsR1Ns*5Yp`{nt$a(!g{cI$U6fWgW@R{-EyJ8{P_cvyWKUEHeXFGsoYjBj%K)mgO! z7_0JbVL834xAgG++s`-!C~AaNSwADOwHyITT0K5ieye&FRXmJbkWQO%@)r9HVILdc z6P94K?d0yvT0>9jL#ojdncmf+W^tV`&tV*@g2BT*W(2jm8UNF^*^aYZLX7wY0Un+u z<(VmC4oJkOOKsga&r5;O2~4a#_^^SdA#q_fpp(r23G|7usb~bv9_70~5g_nzXD}A+ zCe&$1!_?uLN1~vWWIxkhEKR{lYMY}J?~_Ta4s(b0mv-W&!DBChH|L#UG)$Xz-+ICw z36r!knGO9&)*a{8Y|mYYP+Ded5%frrFbny_Gh7gq{6hA)Qrw2Nwg&IhRCDmLvxg{J zmU@gDp@|GG#U?bk6q-XHU<`-=wHiWTSXRXx2ABc6euqlq9gI)g4M_4K4f20am0@6J zvCjt`4@hA?uf{)D$1z5w^TpX}zI?#!Vt?V*5xe4`jEsddcWp#bQ!319&D|F55(J0| zjlMFoV;r1)sQ+?x0fIb$Ja9r2m4O8}Pd_6JMek~1!w|)X=eIt2XskjCr)Ajk4w^+; z!8`CKKO-NbhL?U5Vu^9GCLfdrptNb!Dd=wR_J9bM-5&iJlZ-=ZqY1u^`h~<{7z;zo zWmsz&ydO(6{I;O-ILR>DzdEJ=fO}DbF))68>Op%5CC*FJ@%_c$h~^u6VPJhTd8YP| z8fK_YeTPMPwc8329F7+H1DWLDa;=b^${s)sS)_#*wO>1`vg81y)J8NP-KVQi}JyUh{Ow4wEMkx zwNL*Q0RYX-wm;qN-PJzjJrP|V>J&_Bcfq7cFq=#Y7@Orp_SVP1a|BjG0u++}%F5Q~ ztnCI|!U!Rn1`y*#h#l4==x#G&vs?P_Sg(zoFwH^Jam?wA zb^Rx2W5Ui|;lD&UK=EG?wjoe05BnLtayc?cWVy{5{dkhNeEcH(I#^#YrqeF>fN9@M zH}Gh~lR%ptDUft3GG>-g=&xt!785G;o+gd#{%GAiJcjVX+P&&-QjYO~wQ99-L}IiS zQe!fUeBGO~y?#b!qpr*egh_hd`eU<0hWKee`4B#uVz@)y=LE!HM@GS)JEp+|B*BQwG z3GTSPc?g=)Dd8ye`9{e2+W2!ygJA9y+;N-EIU#!1_y$5f!oR~lB_xh^3z~`{j`R3l_O!bVdtfM_NeUHI?NbMN{4%LcP9gP619bxOQ8l`r~59J7ens&HWH`XLaf8I1$s!8i0d3$!6SCD)Gvlb{U!LQK~o@P=%?FyH&mIOJks@U8SF!X zM86OEgU6CicNi7mxX(X#&(>2r>l!s3JW__={M4_ye~B*j703vAqd!d>n7L9pyDwZr zkEt?amCg8&9f_OU!qTi8~QE;5@+DC=DgRfG1w{+S8M}IDpGkQ zY&&3gpGdL`Yo|Tm&&$H_rsv$TTl$%3HtkJjOuiqX& zoSM^1zYIzuBIcj@CxM#){M^D?;w14z#25cz7Bg%qw6vnybxU_V`b%H&O2-dmm7n>? zefO?%jQRYNzuvz+xzr&wkws_e-dkT?TO9xX72M&3NP6t6hC6)vwQ+~n#u~EnYp{m@ zY{42}EK)W&{}5=p!Wza`Weu&YK|^enHDm>;UJtAx6zpIPyp}a^S!NAe18Z2~0;on% zbmUfm%Vd59n8Fqe0Ubj!4JExNY^s#}F~Al~VrN0%+xf;2H&{vV4kZ#YMCKqH;ydaX zv2Gc4cqDmYFr`-V76x+&1(eUQqK4046#Zw>HJiayvNHx0$WxY0RhWbO;@SD;%(NHb z#yyYF6%eX@@`bIQF9f~G7hW1j>uP)<2ro3u>){Ju!Ai&Xf2H`s__nO+I(f)9UVrKF z57$}vrC;sY(<8xyEY2|oy@?a#BZ5IAMJl(sB3JaK+ndK$6)jRR0(TnI<-~<8gILKC zl&OvYJ6O@R{);QP=@TkE*V46K8#m3$4|3CTOk>fv4l-4w{g=m7`wIT_nd;-t{#ON4 z{jp_6AXojI`cx&IJ-Q(N#|GkmXz>t@xlsPyi-+jV<>8mXl*yZ;i+mp3>CZ%`#y_;& zr2x;L9>W~}V9okKvdb2aR4{G$37i3YRL%f5YBg%ujH=5Ffi6SBcH;3NcQFJTrR0hH zK=u_M;6Gzo5JnOljAwgM8dpZM8SA9}ilx#Ff$l|AXyE~tS5$M0h`OHAy?PyHK-7FS z`EorqfsdT^+OSrUAj655@`YG8^%lT)@Bn+j6RN|`1crOX%lT299v+66FT)2QK!rDf zhMZ;&#GMATvqR`RhIH&>oN;BgX!G7U({J*Uq_V}>CO_}5PkwWx-Jz2NXObs}HXy}M z)K1oqhsZtEJYP#{u=<#6!~olATH~O?B}Y*|>5rj(ju<^$CA2uoge2r>82K$~7*WAg z`>fm23b{Y4+ma>F)dwWiT75uPt$d(^XNu5rxusuapsDs!G&--Q4NXNG(cthEUTD9< zKrb$Ir5v63aW~^MNVM*T`JLfaLR@r{IIui6``meZ^GR$`7XgPAjoSp6I3d4w%g!ax;YZN=zg&`y5w z|8MVX0PH%h`hMTX?!MjkcJ(B!td({p-FKrTa*&<<-c{lxda^7_{uHs3m^M^dOKW?z zE3LGl~Z08>J7N&+bjke1M<;Fh$ct=rPZw0yX2TKfB+ zGxxo_yHBztJGN=F_UOKQ@7$R)XU;iu=FFKhlqf>C3kYMs(WAB*il>j}YQi)NE0Vu2 zU=Y@Oo?sl+76-Bz{(p(Z7_ksR%_zDkqmBsBbC5}EOZ;omy|iVmD%S@4JoBxTSO zJ^Q%X3Rb*-bM6gq&AkBt%x&6C$1((WnGbKXs_j%IGNO2F#DB^}PEcCA#`EVDB?{QL zCYx(r5NEr~f;A=!iaLuE!~6zuOR@S3hJ-E-{w&xUtNn^63NhPAc-AMcIQwPRLHos_ zspyJ(z6Cd==lO=d0y~f7!U`H2YJsd$&s(@b-aKaSAI-@sgkcXp)+Dl6e^lMeY8oF% zEmR>-5pGP^k4e+RVi+`7sfrlsoVCU&P>$wLYlSB-1r)oLF&H0y94J)4(>YnQ@LDb5 z83qR0EF*iT%obEiIEH~f^*kP47B@#k0(6hf#@$8v4$S>{(>M; z9vJ8ZE&57wrkwyf=I@$U1GEW}i*`=hj8_B+FUp3?FY{HSy~doZ?&L9kdmuHLF!fXj zqEM_?`bbTsZ!m`0EGTZ@*Ih8_s+xp`HmU0bK5$1*iwT$q+kPZwp*nzb0P^zu# z;N)rI0ap_B-N(K2fwLq@9`0mnl(#DL4L9kUQ?FXrs_J&C4o=iGc$_jtGM@xtC8LBa zi5S6r+W!4yE~y%kw#lJT!orrNEw@u<3LI~oDq&7d^vCm`Hp6JU&r{^22HE8b5K27< zg|ii|%Y`ffXi5x7l=)#MEg;#1b5L0%OhE=PiVj(L7v5Q=R5o(~%Nuw^oDvT~l5th* z=E~(EhkSvQ$AYd+eeKX;$5tS}E=2v}X4|3aaGRLH3`^^R3DbJOc}TQ-AazdAzSK1~ zs*Cm!9=29kpp``ubn2E{u8@_*OsXPwzJMuP0by`hlcP(g77};+Wza7Q^@Ak&GGc7f zv&H@bp8+-eOs(G;&X|s%n~I;;CJC%4JUjBArz>{kpUvG^+e$Mw<#$v58tg;7U*jL@ z{f9XS7~w6X)tTfztd%?V(NPiuPQk4{aFo__$%sdf*^*gEyrmo81cVqSFO?!16tc@g zt7k6WtSnYU{h3dBWFo$Pm{PU37s4VM3Uzg;gi?YrsxMgw@;C=r%MqJYitA?D8dQ)h zV;3RLa;!rtY7Bt~8FV(=Mok`hy=5K;2)fa+sK@q86PZCD6558nf*mr{bx=8_Sr)3c zk_~d%potQxXEoW?LyV8q{uN$#3liDJWxy6vleH26oF2;KWCI&K{nl-3Sk{+kAQqMk zXSBZCS{&L51))4?8#>LMwbfN?I$v83ZA_HW&MW6f*+jh=?6eFF7+Zw!*?5bgD7>&w z(gr9p6-&8lKJia=(d$GWZEBFaM8&kATP=u)%jInexLOw)uI8Hi<0IT39SwA&`g|}vVI&!6m{}h$e$)Q0C6+*xo8LSgWHVXr^Jg$hg))JUYNE52GUWRRxj_t3Z&2mRvbzb5*qrM||;12WVsz1;i zFtc55(eEHF3q^uKR3#ZtaZP(LF`KLPZ4MBe#Ke~rnP#_2w4v4bMwAXw82P6>MNT7O zT5CMLE-ctUb!=F|k$4~MYh9qhgeGxn?09~!c1s5F!J5_1grI3iF@mPi&JK@Q0au)i zJ7(&+o0AG}5v!~Tl@Q_f>qOZ)$b^Jkkc?hzw_kpDT#;muoKesL?rj2AObprd%tZdEHewD(SCYDB+kWz?x5qkbIfJ+yE2K&=8A72UJhJw3vezdM5RKe!u@4cHSY8#G zFfap%Rmt~yVvCZtae#g0>DaDyS}gBex@fjYQ7>)L%tC-Mlvp=uZEYL`noo+1F=D`c ztxG+Nj>61CWo$6i#brH^5QgcHNpO+60Y1ODaNGCR!WfKD_Ss{LYm~ z!lNEmu-igu5mMA0x%54yA}Wzr<`L{`kBSiJ!;7fG%E! zu@C}LZBaHEaDzE@YiSAiR;=YEtIS?)Exh#K5jgc}>Vr2#8?6{h<# zxs6&tnpGIdzOj*bM!n8}+*4!Rs|y?dt@HFUFrZxFA;F|q{`df^$VZ)`o4(j)G-5;o z@0E%6X6iyaoiq@ywNbIcxNM40M`Mtp%S{n08M6Z!W1R^L>0<+QCzrm2nFb>sF;_J@ zS+QM>5-gsBG+a1Qkm-Iv|MZ37OjxyASAPT1`sjAyZ_>h&E)}QYlL;kKCk&5Wf!Q== zNB2cNs-H0?=t9N|8pE-vEzSWRSVC_!%F3Zan-}OloAe`!)%zNSB+4|}X_dt(fbaon zL^~*;lBEEIwxr}T3W%V9D3#u_ZZs%>sl4?ea#G0YvUf)@Nnk;f-6EHIMshzZF9=;U z)l+W0479*{YDO5N4x;kX7PE|ZA=?Wz;3qH^pYw6`B=6~n9f|eFb~mTcwREvuG+@g` zi!F9DCj}8=nOXp>ID1W`p;_v=Z5Tx}*g;+#f#e?yU{Yp4d}msKwvQ0VvEFNv_b&Ur z198Zs&qqhA2Q40-3At=&j-2`CD|8-+niV0Q(568Xpe`<9HJ3+{on$(SAg`=wP(dPu zCJ}asMdq0KfpMk;db24Z%oJ|WY6=q#HUya7s{m zPi>811TZ%t=bNm{ym3I&M&mfKh^VApWZhQ(G$L1fqprhkm&?;^H|YgS)^nzHaY~JA zUAyDn>RNrnD4kis>oGRQ9=YZg>^X=FbR#Y~h>5u`TC0iYE+5nAE`kkd^kC_J<>(=! zKa2;tWTsH!N?Tuq{4CZ%J%=kOKgmo%^1q^>d}bJe%|#@92`bl@U$38beVJNLTMcK}IGTfEbT1HUR0R4M1%Wg$-mln$lhhxP=SD z(0-jO$>3p=0=Gk&RDmO53PPPqSOFz13c^KW%`zz_Q7q{~cy{R_uwqV9OC4|K!afDS zturzkFPRIw2@*0vw6oBP-ckzQ`QAW2H5r8h2v6S%;n}d?T!Bf}Rz=_@ryb49bQ0&*4qM-!hC0f=mRwj?@TXZw!Wn2>%7DO~!yi=KrCyia*JK2Wm zlj>~8kgStZc^Y#(K$XF`i$#CL@yyYIv?pZFZG3RUj0ExLcK&bqbt4Fs)*;Q{tzokM)4YXf#X?g zl%ScV?hqo3!JlQHw6TQZqNCN=>N^AIhUye_A&QTUK8i6b-!u;O?Wm4CgvkUzVIO+LQz3TSd*5|16UfX2+{dPIXfm#WQ9 zMGY*_IQo%90sq5$Jye68H(1A`xWaKXtWqt(Og|fI4=6jJX)5eXXaG55bxPvlbPmHY z{idw0H&Kwe(wD^XDi&U5_cO7+ZU6ICkE57T|D9&4X6#{vXQbhnA3_(hXNI|WREtPm zbVWtc^CN=TYikHHV=Nz?i#MEk{AgUQwxcjX*(9yhgf*Hlg-+IkPBdMibSdf9W^n+t z>-T>6`Y4Lt@jc&_6hEk6w|_loDn20>u$$U$S9gOdT`<#f?jlaQ0t_&W1+KzK(d4SU(SfuoxQfNO2<;hveSsLgSu zrIS|B3p9nb_*G>Ht9)@MtwKvDEqnSBoU}H|Noym{NU4(+MB`p<%bm1ny5>hRyttEA zL5nLFb<(1wmM&U`YyQDS3(Y{)@qg;SRaU?7k6g4gty=LuOaQ`ktKbuWLR>Z%EvAs> zk7aOeO*YwR9tN!<41|f+Jgs*4FFdXGLyNm-^%!TXkO60_NzZ_@)nI;iUL3OHD5PlN zXO7cTR?v>d7yIS1WzTlYWy^j|CR?;s)_xBrTg`2YrX}0rt?`z#cVWJhEG@)a;%&A& zxL~ef1$PZA1lKSaI=F_}F942X`(>_S_AA{uO4-y=&(-9}le&tO;XLMxNCrs*QjsKt z$Tg$`j9~lJY`w!>L+mo>ce0B%Tth5d>@OOwA)AGo=6|q*yN6`mLiz!y556zk>Pf8} zvNzUuo1g2K&$Tr0Gi!Jvk41^=Q+YK1!u?@l^JG5hw%rRdO3U>lFUdh8p2FRRn~swT zhi=El(NrQ&u{P8N%@*$0383pV>t~dz+nU)rm9fssU;t&pK^dxwa#`F8MU`vvxL|UO z;ws0a_H4z=t6i$+IjmF`4tXAS5=(a}uA=W@hv@0Zug&paVXgYyHdK(SvR0XG*2QV- zvJmu{q0p@U=9J(T3kzGTcIE&ppRsqMC~cO#Au`3(4Xdefk|kubuGu_H6FEMlb@-;Z z;Y>zXhk-(s)8MACiYjR=$O5pQ)pC5?$fcWQSqv}?8Mb_63uU&jfe~6`%X%cE=Ih}z zS)4eI!Jkx>frU@a_kh4%RrsZ*K@r&{TAAw5B7qJ}D2H5CmbbnZ@r~PAxfsTPbUDVB ze}b!uH5`NWzV0ft!s_g-mRB?x)vAj1e`+CZ^smfHtnJ~S$9R@-%v)Vc^=WA=kXjs=#y@dDFOXLooaI2hBWVh!v6pEqFR7-WObw08& ze8n}TM>l-?dq8lki_r9Kwi1zv(Y-OoEIG4_*v$fMhP%$rfs2cD2lf<%>b`Cr6Gazj zWTHvTQRS4vWL3^6otmv9O;slk6rC`e zmf=`$KA@g1-7)Q%)!s|)nZas$WL_g+*F9A1dn}Kk;$tf=bsNN>!21WO4h5ehJAfGF96mamTHv#X;_4f(J0!mFc_ zw+)lD)?lF@^ue?`DV4IJ7#IvE<}yV51a*?kBBD&aG=n7OekYBDQ-K)U3{)=Y2GAPK z(DJB@Vb00P@S2=f@adJsw#!Htk0G+N*nkmA5K)u(55(yi7KL^bi5ef4zYYgjY_YM3vdq8AP7_K!IylEj za6z#~im9ESWeyRpXfwpZ2!v}yrzx8gZU`UIp<}E>uo5>7g0)~_)gq>H#g<^KVh00X zCJ|Vn&Qz$j*=sSrB$<&zbDf{rhPkCnir{Ll1?~z&q;-DtJlA_)=f}ZIG-aBV?D_4! z&TjxawE-ANR{>ff*MVNWraWn#tphz7v~`4aez^>ocx<)KZ&G-cQTlS$`At{CaY55o z^R!1qTa&cTZ%vUB=uNTl3uO-l4}|XExvldzT7jByIjxYLLrM~;GS$Pm<0Z60(n|Cg zded|b5GD_@(6^>1q(j+N;{4h8DjUj7{b5{zv_u5jOd}s%#+W5dyg?y?+pH!n_rL9uta@{Kn zu{PwF=^m9wUgK;C59*W*NR=0F#<*xl55GxlNU*U+xefEs}RZ z9#fPnj$qEzgKYLP!RXCyBK4iwz72|Ck;?PCW1ZI7%jugyw!!s<4*YVFDmW3@A$h%V zPZ$UD*7j7YxW6irNK!?+8fe&s$3ZuT2R|_xt|e8#jmZhgk_{}m20t+msI%NpOfSPA zsF}LHHa{_dm7f>}TltCg(25>+*zDm1W>Z{UYG8ueU*q^|T#qRxt}vT4?bt*;yw+Sa zlF-(K*_@}`%LFA>UM38o=4H}NcQphZMC}7Hxa@9vncxqAeOdidF*Or|O4+?k+TF_p zW@Ar-yiGXURD^_A&frwhcS5aUJfbQN_2{GvkrwEEi>0608X#?Q8kHLtZ11IQckGl#9p`+ zWA5?`#H_nO__VUQ;alp4bUf)H_5u(ZdWgBev zjx@2B%?&ep?r@kPWR~`V`LtP2En9s@C-N_HUOs+>@-Wc}@^Wp%!(>g|frp9wifT4L z*zM=H0P}7bnM>2dL?TFIlAH~4t2|6{7d)v^Wr>QkDKFz;a#29}<`%EtJWOb(M2bpi zc$fe?ZIg!yV&@cir7vs&RL6Xlf;x|I!;%8slWy)uK&8&-6>&o<^ms`p1~Jy|WW*AJ zOKXWS4RK+eQo}XxT$%xZ;L=MIW7=h9n{C{~gaYJYf~{bG#=}HHNYaWnmRJTffkj_S zw5Yd&U?Gccn-WaaE{Vc&Esqm8QUi<`((OY$+iZ9Y1S!49ga?^4u0bm{c(x-gon%7} z100U`=$FT=Fon3v(Qk6BKIHVIC$f_60ifZ;muEs+b9o#vxuURn|*aK;ksrf$h_m$*e9CSs?! zY#t{56HUrJOq`?WG9IWS9YZMO8Y&4P>%A(p>txWz#sMDKabjRWtwx8%b%_1uk>QfJ zJ8HDtJxn41;H@)PqPw6l`@_0>n6xvc)o~xvnbVZec1Dd@yT0O0X8Om~q@bTAY_gBn ztOsm$Aj-I!FmK>Cbv4Odx~oZn=HQ&*t|o;#t|sQvlW|EAX}$zk6DuzD*NCVMyqT*B zawX$xvXS;8Phn^15LXi%{mj*b=&N!y(Q97n3lYCuO+t5>o0q*OiaS)UJQsE~DP+An zUd+|RHV&kq3sG{ldNEfM=r2U+k$}ocOqi=lL82-kLnMvg3vno#elM)7;A%3!q<^iu znhccxC`SNds5>aO#IyxLHqkfUl9!!%n+Pm-H4&vFdZ1dfkIB_U#Ht{5T(o;g%7S%* z@h{?~Ss?B!ZE1lzRzSWhULb5h7<!+0!3Ar=n1%ooys~||#N=CdS4+#*99mLdrTAD* z$%rV40AdF|O{_Hb`c+pcmVlj$f!X7>MeXt2*2En>K^n=AkxX<66 z@XKkxoJagiPCgW_glEvb$H)kaNsokQ9|_kd!xh!Z@_syAKN+r{(N%|3ubIeyEdWU|)j;ayev^JXnP?f@cBhtz5xi`RO;c>&IYr67NEd9v*`O3r=0JA(cTxkZ1ZbUO8R5)#Jvfr2UcTtwc3=QxwU8 z5#3o%9x?aD^d27%-IvmPTs(AN(LKc-1>zhl%r)6;Q0rKc2x=XR9*vs3o=}U1HT#N> zb4;-qw-sAtb3a#4JcL^vAF zE1&4Rn$x)=e8B3D`A@k#KH@*AL*Bc6tbB7WPDMr1J$~R`93lKT^hD!N;{Ka8HG{TF zPh#uLejPdPP3Kv$-!j@zq_rZ=qUh}EZQ6NRft}D%DrmqQTGjdZF+8Vu;=H3UK`jZ-CS4&D_tdLY9BmN1B|&fPQK0g+I)>f#K4~KtUt@d( z+)XxUJH%BW-iT?YDze`I^+OgAZ1%rckvi9_0fa+f5~Bm1iaO8<^ghG%ms7=)wc0ew zj`&z9s7Va7EuH$La&%@9X9Mv)4y~NzWL6UL-^`Iu zv^dXEjQpZkcy=r2TszOS3pi{##Oa|pK;o`lNG=*y>2llr1pfBPr4h321ZygWsMer8 zZ(F01JP*a1tpM*IM;P6mMYTv*@!>#38B8j+Yhj*YaRLY8;%3VsW(}oNBY6^x#yxsY=fJ z!Kpe3IDGgt#8v!#S;1ysp36EjN=L@#5(ffMM}e+) zm<6y#BwjIQSKrQaero)phKr+a}^GP^sLo#a%xWLkEs@LtPte7CP%X)>z0a(5InFVX7k25>i z%Wkm%F=xtzauoxTHo8`;yZjLNf$A=~gE1&}A<4T;dEq{0y1HF#(N^DJGwXVc_)c{C zIn!0-Jvxi#pq;YSzl{;)`QH_cxMp)Y08TlD`p{b&-O7-DACzoc*$IBHRSd%dh!OY9W82D{#QPW@_M(3z#-!Cyuv?ap;u!}SWoR3Yw}uGrFqY}=1kG85+MllIw_Be|^W=sU=HtD9Uw&ZA6)k(FmAG{Qy~s1!M` z1q;ft4<)w?$Zf=)eTDF#e8TsMk|IfbRxxzcv}u%LX^1;c{E$m2FktoSywB+*bg=*j zzSgj!Cq?mfPU;j2pK}j2jpw)f9dW9hX>!oNtgHNQ!!N6A#$w!ymw4s8gfmTsmxSm@ zdNsVhlg@!jcL+q**6mpCaQf?jSs@f!km#j|8)d;AY&$G9s10uwzw|WD?o{XL8{b<| zUdrXPu&l%4N&WNYImgT18qHQy5es07PPOKxrkL}16IdbD zG#|>b@>SQ-CdyiP#VakZ2Ou;}svkBvAysDWplu=*fPg6EINy>0Eg&pV2<` z9nRgFEsn^sfExtE_Ie&udtF&P!zgn$rlrUsuS@doC*i{Dts>#lY<=QqSe03pNT9&y z5CnR;%X8_=*$j*#_0E0eKgr<*VgX)VMzRJa13Ul7Mp3vK)wGJn&BnkhiURUx2mbMLWDAW24r*Rt2(dwNLJK~Ws4^Z@Lz;>X_<5R>L75L!^wMtfbyH9<4H8>;*%^$KJa*g9z;+ibhJWv z;M%)a*u5Bc&uW-axrIf>Laj|eOOk?Gm@VLRRh2waD98@oFyE}M6|#WR!fa|m1z2o|^fD5*AFcPYMnJFfw~2C+bq% zc*af_%GPF+m=kOSspqqXfU?_o!K&L5$g2TLT2%NX&84V@ja*(xsK|1VTRB+C=h&)Q zD}ymDqBw+xFh&Md)X1Rm2>)DI!ML+WJthIpNUO1(1yOAiXqU4Tp*P?&{vpb<_#ZB` z{@fuUV83=s2n3UfLb8ZGZ8Xt_JRMBf14q_BJZ{1ssb0z4mVZ2#FPw9Wf;Jyu!qc)> zUI%~`b+(sAj5zNnOfgQzMc=cK-@_qQdCL&zO_<>PbOZU@?yYZIWUR*Iq_c$Uid{)5 z=sO~9ExfaC*j7JB9+4j(m+P@>TgFA@x^C zz7+AEcOmn{wabJ|xGn!^n;)vum^EUemPAH278%jGR1gzL0TLLZorp#$Wn8nQEBD1c|O%UxG3J}Qh%d-r787z0tY9eEjf1Fqg_L|LTn$`^R z+A&_(gW3DbixV9FT+ou-K`!6SqZ1Rj#T4q#4v*(|azzW$;A}u6*`RbWglZcxxHi^hF&V@+?{VKf`+A1Z?x%(O{p=pFUzm&hTEmvj| zBU4k!2#(C#8AP$Q$o(t{3rE^@hP@Zn1BFlJsaK(7TPl)6EmLjri!_WPI@jtcNrz2I zI+&7l0}{HP)EP>OLj+Xch>)~tBE*b@H`Sz5nl_P@QBlCAs??3gv#7A0sTTzL&DGBT zZXgKT&`U%R#AB1iFx~5}qPmtpSjw}?8IDG_d7jkOy{d}+)F5ER>Ol z&hP=o$c6;UOv}&!5tv77-Lx?+aP_oIbZs7`q6=B@(rFpTms4JLT2^f_E$dr3EnCx= zmd!QhPSif9X&LJ%%)-|Aw2XeHuQe^x1C(`^ZoI9mZ)((!Qi zX<2pUw5*S5nN)w82Myb_tk-URTITaRnRZr8%jR5d>bvl?Opc6(qU^Nnh^pbug zs9Z`2H$qH1lZ>z@yG+QW^KHSctOO_52?-WtRvR951sMS{ zO$>zNo>g;Flz^lj-Z>PksJj@@21IIQHYiJlivjVh$w?IiTn#Z$^cglqx}0>Ct05{gyIsWexA$tSn>_@we0H_AQrM(TofA12H!*OmrJ=A2)1>AB0_+nrx` zuI#4|Gj5rpsAx}`KBE4FL4c~lP#8<}vM^ArYf&JM>nLDI76wD%oGtlM9MPEQ9!5ZU z^lufQCbC?@m}pWbN7|mSxxPX))p)K0c@yd;EwQ7PbiQMJ{>dNV-Eo3kIl%3`}Wx{c>FHt3N>d zEUxkxJ+^0)h+@F@bHC+SHl^1>C1wpCJKvIae zItx&*JnyBpv#1$wwOcAL*0%AXW5b|M%;M4?Z&UAWHg4jw4fCl&VgntBPiyasH))vI zA8(?2^@}AhT_!E$TQU&hUk%_H-+-(}8wXgS8rT`}IT1dp{a)(U6{QY(Sv&j^UwV;o zqe%M2J2U_qE>$nG^HY!{j(!kcNSfse9pa#c@ih<&n9?$ zoI9gO)IzMHr1vc6DtDhdlavmcL8;GXM=~YVe>SagKS7;(&st?&*;SoJ&!7xM`u`S?d4hbvS+1pN&BAMT};c`rP^_PN$%Szv13oiyU|xV_T(R` zp&uh>wc5TJs)pKcVttSUOP0($$r5I~8G=NwhISE>gmIWHizvhi%*G%A(Y_3*k}iEJ zs6FX?93vPL6RIRc<@vbt%%0pssED2C;`Vo&l@2!OO^TXP@=LxdEJLd$$I*hSfbWKq!n%8OH4Q={Wj`t(>K_? zHR|=n{VZ89B1i)3264gL3OdR*z3*?lL`0BHV%lV&s!jUCs3!?!V{a~|_I?n##mCwK ziOYT-Ncibz zeE|FH>%%a2i>h`A zOI()nYgJlP(2L9kvY_!B0B&Ulrv;4wKrAY<<@(BkHBH6oiD0$@D08O@-jSkVSwW4K z?DT@LstZA_Nl-xLl*YhB8umol0u1t3JZPOX$s)&3<7?wzO%2p!(z&WOlJ3dSoo z6|5!|*AP4Xz#A2dwWy&X;t}ERS@ShU!mPnB9B#xbwhWKKUNnnP@>#5@CrB{Wv|@Xc)3&Pz!PeuGYP*&U5^^9ZK_wXy=;_-#M#dzrFRZ%St2&;BYL)R*nxUieVfR? zdL3=zz7VgoB?3ZPvLmKrvq@6NYerK`wnYZUo2&Zd|BIes{;LK;?HEHK@x%iM~@6 z)vhbvDn{!XgZ!c_^*N z4I~ZwQ!~#I$|-1jH>GmS4Wx%rh@zn@HBd|A;Y=Su(uVU&yk06G1FmLFAttoIzL_ozI!wv}=y`;MO@;>k$i(*2i0lG#&$n7{Q_dV73U5Gwc$bSB zEhKij3b{-1+VnMd42$WVzU^t=+P73F3=*h9{;Hc@u=i`=y)s}3dq*QX#fCUIbF81O zDl+bH%$J7%DCixwUUil*jR&gZ6*Oo1K0GAfitKaoI5p}}CWN=&vP zpyUB07JZ#q6n#tdL(L*l!fo@ZjsYwX{ZXcBpt8V^m=E<6DP`dVQBMW0@TZ>bjMv}Z z-AQwahj$%VZSE%|SKKMZhMv+e&0NW-%xJtjrE$qvIkJn$dgATqFi;so3P2G#6UDVN zwo_)T#a3e>w#bk8TgM6Td5=NkqcH&oH&^cKxWI} zA#i?xM=m~Ar35-3;;NwO{D6OGN5*j`k?!CmJ8-Z~gQ{?Vb<)~PcsP$kpbQ#S(_u&+ zUh0+v!(%tUDGoOM)}`*b(QCO-*f`PgJeObtg3BW8ANLiG9Alo~NxjZJ3W1yIRG z#wUDgVE9@IZ1}--8lrG8apNV|fZTmA4@5HLVxg-ic02W$T<2@TFmjA(Co4 zCbA>QmNquPrE>ZT4}+vIE-NgP6kdvIkfqbJyS3d%eUCij=r))6CVB$^%ZC1790I*v z0JC!F$F%F{ZN6MR^shi~^opVv&l2IA4fnS(F{#9{6Rr3>iI5TIyS5Q8A=9Vy1e?DO zk+c^)++o@5;m*dxf#$=w@sJ%1>F0PS%Ehf6*|HtY-{XOQpd;VmVNlcc#r$rRfi_%# zab?c=^CJNY|9nF zx*wOerie`pRyoo%Arv(R(8b>5f62gp?h1j$kHmUYo9!tDEABCsMsMV&>lG%uV^ zdEs>3p%`CcE99^H;Yg6;C0Iq1=#(E$r?Oy3_O~nv$DMjxh%p~S)N(D7r;Y3rtYEOJ zaW?7Pqw`8S-c3xaoCu$y)yb?$!eUw$=Kw+XH03*VD0WqA7*T57hfl$f!E%bnIq%2wd@YpVrVYi^?P0B?!{))hrjZ*lz*_CtJ0KK3$Gfq z)A^w$%B&drWLqPzIEP!!V9~vhJ2uN;jNh(>R~wv0(TyaB>=51-OR*(eiY>7et1|-% zKDh8ftbvm!%nCKE`(Wa}*h#w&7A!a_B#I9vjI>r?1#4M{zi+<8+W@5^WBFJ)k7Qv3 zr6JTL$;YO46K3g3BQzBOO=FNQ$k<{Py0OLUZY(hrRvTNqN*h8aGiNBTlMSp5m&Ur) ze?N*AVa%A+vExFMI`)QXQpZHk5dskGt&p|c<{pYxlun#{vSv|B9BV%JHbq>@CJX4U ziE5SAXJg?a`=FzTkytvs$BQDMjAHr_RxjkE|I83bSz%hGSs<`zVrYJVsOS{HygQv% zHS3c1BeRHG(8Y<^ri^f6}i) za49>!{>p#}#te34prk|S*hARyAOuUlhc3a%G9EBrg0SeW1-6X7+FWg8KwGJ=iW^$+ zfGTmeL+r{L(+*yZ3dD5SZ?RkjRtT$YffW==7(PYrULh+WQKhk_JfCg979X4XyYKAC z;6u%5$b^88Zf`I6P<&UH2~B)3BlGvomoJJB#)VfDAKDbdm{f|_yKYJB`}%9t2_g7y z_G)(URVRe}SDg^d2_0$hRVRd3!U^HYevM+6>V$A9xyyf1d3Bu-2AC8U@M18D;x_F| zbV85<8pi9|=HPWe%KuvE#Z!4}UGsl;0MOF`Rez6P?*hYT0-HPEW;i}@Ph zUBEEGdLM?85|`B10N^qR*%bF_MPfgW1gWoqJ(yG@qNG2p;uFDPFUzA2S#47pz&FMH zTE*{kUjr+(;cE~|9rVI?_$9vdBF)#picHbPnXU#G6$;*}Uuc)0T;^+#g>nl}ny*2h zHZ1kWeeC7XKTTf)Zc2KR@ipL%T3Y!UY|458T%NB10aCNR2Ha$P4Y<23UxUpT^EE&{ z&4T%od=2muczJvc&=Ft6*8nRP;j&RCSNj?a$nJDuUjsYGH}y5x6wI8L-;_G{ipXxHeF5_@DSC7ZRDX$t>lpJnCbs<0V_KR6n2WP%(p z%)vntK=mI@G}>rpmMZ-HQuxim!RNm2>(H3{_OG7%`Wk9MpcGhU%sLuei+O{>vH09& z=OUTyCwRf>cWFn29hFwd^nwlM58v4l)gLUCf97rPD@IX1ilPFEpHUmBozy`pkvd6T zq%x^O>L%Sbc4T4d@ci&(eQ9Z7=I&EV)3+a9n!0cL$in>bI~S&oFWxi1aO{RC`ux`r z4TwU&K!Jx29iKkEbm+vw%(3Zs{{HEOd*_bawk_Vgi$HSxUN6bA&CSgp zj`qH@e*NvyyB2vB&m2EIf9%B6(#+j+)A5n%!}CX`7h)idm+qV9<%v^E@x9aY$EKGS z-hXYhZ)#?4`bfMqA9BVg=4XyC#SNeqvsq@2&n(SM&CPt{w3QXgzBYQ-@x@aoPRuVX zQPQ!Q<5Wev7ir;&S5rr`uMQX}GmG)@c`A&LO)uWJJbwyhy!8GP)7M6~S+>3Ek@y~< zUY>cHRv%IGY1n=9N3vi$oO&^JWNK-u#j8e5{{G;ph3WYInT4fOQ*-e>r;Z<1Z^ehF z=0H^qy3pUp=<79Ht0cC=MC(ZFNxh^Eq^n39NusAd(k4=%r;))1J$>z|spCsC-$-XI zrzoeXhp0m|6}}s4(qWeRqUb5;?-}yxZw`Xq%(b61K-xmuO4>$>NrFEiy^ge<^mB@|$5*{OaC`~so;o}Y2RIDZQMY&@=6Xbo#ouSRzJco-Nib)0HR(+x zvPC;d*O0CyrMxzN$Mnfl(;#JPu2EwH81kvDAE8Y1@#B6k%=>1pgCwcg~H0m3fxNv=o*UukcT9~KT7jCe2^-`DctG}oDJ;`+!=`EyeyWYyZ+I1c2 zZKSu8-a*<;N@-xES+5Xz@ASgz<_eb|q8{N;e|yVOROh;fbiM9LLjFF|jimjg1EiZs zH1c!@O{(ArVmanh^Xi0?_HQW7O;F^=C(TyjZCb}`ksZU z6P5!WYf#^TnVB1IAp7uWtJ3$vYzG=|8_m0Ze&P7^!ktryryKN_f;c?UvJfZ4R}ZR@ zm6*6vJT+Uo8oPHQ;f|yQr+j7DO|dgFd?7f}>`g?Dt<0KXRq@Yr40NKcU*TGTru6$e z_}v?RkMTPWzkiH@acB7bWzjsp^{1<7@m7*#iD*&R3(=z9=}%?pyW2>Y^WBpS%D3~) zLDJWfRNp&Et?Ijj=Xa6CNNHQA`F#&5ZR>EWKEBcQoSDP7p{PyGy?QNm z9($I)Oxs#Pakv-w?<38Sj*@0cX?rFwp*?}k=NIY=(^IdSgdDvrQ1?tNPQUV?y58^} z6gJ7aC!w>V6gU2KRX@y;B#+b&y6W!{6g$bPW2ECG(c?U+mCU)B=O;*M|GkRi?a=YiVp~g)YNaFYQZl^eB^z)4>0J(l?US)^8%6A)O_iBc(F(0e+t+r8M~}j|UfQ zpC(Re`%W`~`etDM7Sgwp)V>Ev-$wd&(g#Rs`yS%=caYNdy{cnCi`Gfjd?C}eFETOw zo51@)(uYXjN%}5Q3jbvZ(yxrnZ_$b?g6=CL=377~efI6(Rk(f(+Ig1W`qNdo{%(@w zgm9h8i^rIpOU^t@`dcK?*!PfH$%~tL{=KAth2hDSy7k`axuvNq)hw73@F&>!0+VE- z{(3PSe3Tmz1D0-CZkC1+p^kbyQNPnC3oRm_=m_^rQ>3`EgK zx&Aci?~}yqPmz9x^s}U&Bc;6lF@FC%DecqOB(Jv^7XrL5Zd7_j`l1on0k^TWG?T6PeZ?$Ob6+zw5AzrO6l5X)WTfCn>L%&SY zC;EGV>o0TtIO$hN;-7y=`c=|1q)(7i{`p7z{xwock1wu2yd14<>J<+HtLRIAXSPMr zC%Jx#^y{QlulNmqzm;?i={nM2svtBmHxd-iNw}hGZ9MsxihxpQOx3-$*9@ z_Qz55FSvf5r1JkI>Gw(5^8bMQXG#Bx^sh;ONJ{(1}XAU#L=kEC?H;!(c&Pu%}!(jSxl3+ca-zC?PS z^e3eMM*8oh6ehD@u7>Hru@iH$ua6i`GVig^YXPod@X8q3N5+J|Ox{#;Ras3-vX31_ zV3mw|I%fTZ-IlO)tF2;(}0PsA~}Bf#WQHOfTLL)(UUGdvSUJOdYsm=n%j(4Cq56jVeqA zHMEIFL0JRK3@;FOiDY)2)Kc9=hVXiSCh9+oo;GGd#CSRn4WGlRoGlEx-X+;!-l{N3das= zA$hS;KV9uawFvBL_#e~Qhxwf{#zWhq=zo*v|B(KYl(y$92lI<|`W0g%lx&NYucuws z(X{nEzx8KVIyOgATlKg5^*AVSUmz7pZKMk6wNl0s@7%_fcnxw-ax`S%bPeNnNBe>80Sv*DYTvS2qzRc)tj|;H&=NZ$;(b z$~_Cj8ty7Unhtl}{LT(XD~G$DOsh<^H;3ECXk-*c+38R;!0@+*@2Vu?ZAR;wWekno z!Xnqy!ZKFW_ga%rdsf4z;9Sply`*$l-oWqXuw4JbZ%5^0O`nkyQwxjJvAbm4H$8=; z32qwhG&3||0^s_&`BUk_<_(lD(RYrzDArP*t5Pu}StFEmuWK%<|IzP638~8_nXPzPZ}U?X?{C2*o0q2805F? zi0Xg(*{J+~It6oq)JEzcl}X*CwWReVX36Gyv5o8NNm8|h@;8&lNK&`nM!KGKBk2I? zcG4ZBuOm&7rb+jbW=Y3LCrJ;GzLoSHqz{t5kMskiA0hn&>2cCWNk2#WdD5?vK0*2v z={HECH5&Pf!TWydS_$)a^Q;xzKgIJb?7z(YO87;K`s*ScAw5SYRNsiH>f3YC?mcUw z^WXmn`q-bfMZft&Lb3e~4h{|t4iAnDjt-6ujt@=@P7dxG8XOuL8Xg)M8XX!N8XuY% znjG3SJUBcwJUl!yJUTo!JU%=zJUP5;WN>6?WO!s`WOQU~WPD^|WO8KJ=-}wk=DO^#1aOioVj+69QasD2kk@8Yvv zJi2XY@X(>dr%zAaJ#+ui(D(knZH4e6{SD-CYH0VfeV)9pWxfM zsl~1xt4c!$Bw;!LY18ctrIUe&F?hbg;<>zgR!Ye@Qg zIy~FS{Wam)>G14Y?%zzhV`*2@caLu*%kCo&&zqijJXf2~^IYHGu{1o`(wi^jdz3ta zM}N<8Rn>$1Gi3D*4dI%D)8F*M0^Wlv_w(dYx%wmao8=#F<{x%n!SDfj^bLpRSlRmK wVEASv^UeIJr9<=g9K!qXUWpw0=DP3=ZEFyi-l@&+y#^`x`$P|T{>{1n3!i-vWB>pF literal 0 HcmV?d00001 diff --git a/packages/client/public/draco_encoder.wasm b/packages/client/public/draco_encoder.wasm new file mode 100755 index 0000000000000000000000000000000000000000..397c7de96aacfa45ebbf16c47dc47c13fe4cdce6 GIT binary patch literal 374400 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 z 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 z

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 literal 0 HcmV?d00001 diff --git a/packages/common/src/utils/CommonKnownContentTypes.ts b/packages/common/src/utils/CommonKnownContentTypes.ts index b81976dd26..f8318c1053 100644 --- a/packages/common/src/utils/CommonKnownContentTypes.ts +++ b/packages/common/src/utils/CommonKnownContentTypes.ts @@ -46,7 +46,8 @@ export const CommonKnownContentTypes = { tsx: 'application/octet-stream', ts: 'application/octet-stream', js: 'application/octet-stream', - json: 'application/json' + json: 'application/json', + bin: 'application/octet-stream' } export const MimeTypeToExtension = { diff --git a/packages/common/src/utils/miscUtils.ts b/packages/common/src/utils/miscUtils.ts index 818b1bdeb9..c01fd8ccc8 100644 --- a/packages/common/src/utils/miscUtils.ts +++ b/packages/common/src/utils/miscUtils.ts @@ -73,8 +73,34 @@ export function arraysAreEqual(arr1: any[], arr2: any[]): boolean { export function pathJoin(...parts: string[]): string { const separator = '/' - const replace = new RegExp(separator + '{1,}', 'g') - return parts.join(separator).replace(replace, separator) + + return parts + .map((part, index) => { + // If it's the first part, we only want to remove trailing slashes + if (index === 0) { + while (part.endsWith(separator)) { + part = part.substring(0, part.length - 1) + } + } + // If it's the last part, we only want to remove leading slashes + else if (index === parts.length - 1) { + while (part.startsWith(separator)) { + part = part.substring(1) + } + } + // For all other parts, remove leading and trailing slashes + else { + while (part.startsWith(separator)) { + part = part.substring(1) + } + while (part.endsWith(separator)) { + part = part.substring(0, part.length - 1) + } + } + + return part + }) + .join(separator) } export function baseName(path: string): string { diff --git a/packages/engine/src/assets/compression/ModelTransformFunctions.ts b/packages/engine/src/assets/compression/ModelTransformFunctions.ts index cf4f69b430..4310886ddb 100644 --- a/packages/engine/src/assets/compression/ModelTransformFunctions.ts +++ b/packages/engine/src/assets/compression/ModelTransformFunctions.ts @@ -53,6 +53,9 @@ import { Engine } from '../../ecs/classes/Engine' import { EEMaterial } from './extensions/EE_MaterialTransformer' import { EEResourceID } from './extensions/EE_ResourceIDTransformer' import ModelTransformLoader from './ModelTransformLoader' + +import config from '@etherealengine/common/src/config' + /** * * @param doc @@ -358,7 +361,7 @@ export async function transformModel(args: ModelTransformParameters) { } const resourceName = baseName(args.src).slice(0, baseName(args.src).lastIndexOf('.')) - const resourcePath = pathJoin(LoaderUtils.extractUrlBase(args.src), args.resourceUri || resourceName) + const resourcePath = pathJoin(LoaderUtils.extractUrlBase(args.src), args.resourceUri || resourceName + '_resources') const toValidFilename = (name: string) => { const result = name.replace(/[\s]/g, '-') @@ -379,7 +382,8 @@ export async function transformModel(args: ModelTransformParameters) { } const fileUploadPath = (fUploadPath: string) => { - const pathCheck = /.*\/packages\/projects\/(.*)\/([\w\d\s\-_.]*)$/ + const relativePath = fUploadPath.replace(config.client.fileServer, '') + const pathCheck = /projects\/(.*)\/([\w\d\s\-_.]*)$/ const [_, savePath, fileName] = pathCheck.exec(fUploadPath) ?? pathCheck.exec(pathJoin(LoaderUtils.extractUrlBase(args.src), fUploadPath))! return [savePath, fileName] @@ -581,12 +585,13 @@ export async function transformModel(args: ModelTransformParameters) { if (parms.modelFormat === 'glb') { const data = Buffer.from(await io.writeBinary(document)) const [savePath, fileName] = fileUploadPath(args.dst) - result = await Engine.instance.api.service('file-browser').patch(null, { + const uploadArgs = { path: savePath, fileName, body: data, contentType: (await getContentType(args.dst)) || '' - }) + } + result = await Engine.instance.api.service('file-browser').patch(null, uploadArgs) console.log('Handled glb file') } else if (parms.modelFormat === 'gltf') { await Promise.all( @@ -620,8 +625,9 @@ export async function transformModel(args: ModelTransformParameters) { }) ) const { json, resources } = await io.writeJSON(document, { format: Format.GLTF, basename: resourceName }) - - await Engine.instance.api.service(fileBrowserPath).create(resourcePath as any) + const folderURL = resourcePath.replace(config.client.fileServer, '') + //await Engine.instance.api.service(fileBrowserPath).remove(folderURL) + await Engine.instance.api.service(fileBrowserPath).create(folderURL) json.images?.map((image) => { const nuURI = pathJoin( @@ -643,12 +649,13 @@ export async function transformModel(args: ModelTransformParameters) { }) const doUpload = async (uri, data) => { const [savePath, fileName] = fileUploadPath(uri) - return Engine.instance.api.service(fileBrowserPath).patch(null, { + const args = { path: savePath, fileName, body: data, contentType: (await getContentType(uri)) || '' - }) + } + return Engine.instance.api.service(fileBrowserPath).patch(null, args) } await Promise.all(Object.entries(resources).map(([uri, data]) => doUpload(uri, data))) result = await doUpload(args.dst.replace(/\.glb$/, '.gltf'), Buffer.from(JSON.stringify(json))) From 3ebe6137c8be50e4c7b428cb693a0299088792c4 Mon Sep 17 00:00:00 2001 From: David Gordon Date: Sat, 21 Oct 2023 18:33:10 -0700 Subject: [PATCH 10/16] checkpoint --- .../compression/ModelTransformFunctions.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/engine/src/assets/compression/ModelTransformFunctions.ts b/packages/engine/src/assets/compression/ModelTransformFunctions.ts index 4310886ddb..65b26fa774 100644 --- a/packages/engine/src/assets/compression/ModelTransformFunctions.ts +++ b/packages/engine/src/assets/compression/ModelTransformFunctions.ts @@ -383,10 +383,10 @@ export async function transformModel(args: ModelTransformParameters) { const fileUploadPath = (fUploadPath: string) => { const relativePath = fUploadPath.replace(config.client.fileServer, '') - const pathCheck = /projects\/(.*)\/([\w\d\s\-_.]*)$/ - const [_, savePath, fileName] = + const pathCheck = /projects\/([^/]+)\/assets\/([\w\d\s\-_.]*)$/ + const [_, projectName, fileName] = pathCheck.exec(fUploadPath) ?? pathCheck.exec(pathJoin(LoaderUtils.extractUrlBase(args.src), fUploadPath))! - return [savePath, fileName] + return [projectName, fileName] } const { io } = await ModelTransformLoader() @@ -584,7 +584,8 @@ export async function transformModel(args: ModelTransformParameters) { let result if (parms.modelFormat === 'glb') { const data = Buffer.from(await io.writeBinary(document)) - const [savePath, fileName] = fileUploadPath(args.dst) + const [projectName, fileName] = fileUploadPath(args.dst) + /* const uploadArgs = { path: savePath, fileName, @@ -592,6 +593,16 @@ export async function transformModel(args: ModelTransformParameters) { contentType: (await getContentType(args.dst)) || '' } result = await Engine.instance.api.service('file-browser').patch(null, uploadArgs) + */ + /*dispatchAction( + BufferHandlerExtension.saveBuffer({ + { + name: fileName, + byteLength: data.byteLength, + + } + }) + )*/ console.log('Handled glb file') } else if (parms.modelFormat === 'gltf') { await Promise.all( From be5df2a228e81ba6af898baab8cae7d6bbb890d5 Mon Sep 17 00:00:00 2001 From: dinomut1 Date: Tue, 24 Oct 2023 16:51:44 -0700 Subject: [PATCH 11/16] create UploadRequestSystem + State --- packages/editor/src/pages/EditorPage.tsx | 3 +- .../src/systems/UploadRequestSystem.tsx | 26 +++++++++++ .../gltf/extensions/BufferHandlerExtension.ts | 43 +++++++------------ .../src/assets/state/UploadRequestState.ts | 17 ++++++++ 4 files changed, 61 insertions(+), 28 deletions(-) create mode 100644 packages/editor/src/systems/UploadRequestSystem.tsx create mode 100644 packages/engine/src/assets/state/UploadRequestState.ts diff --git a/packages/editor/src/pages/EditorPage.tsx b/packages/editor/src/pages/EditorPage.tsx index eb8bad01f1..f8c283834a 100644 --- a/packages/editor/src/pages/EditorPage.tsx +++ b/packages/editor/src/pages/EditorPage.tsx @@ -51,6 +51,7 @@ import { GizmoSystem } from '../systems/GizmoSystem' import { ModelHandlingSystem } from '../systems/ModelHandlingSystem' import { useDefaultLocationSystems } from '@etherealengine/client-core/src/world/useDefaultLocationSystems' +import { UploadRequestSystem } from '../systems/UploadRequestSystem' // ensure all our systems are imported, #9077 const EditorSystemsReferenced = [useDefaultLocationSystems] @@ -59,7 +60,7 @@ const editorSystems = () => { startSystems([EditorFlyControlSystem, EditorControlSystem, EditorCameraSystem, GizmoSystem], { before: PresentationSystemGroup }) - startSystems([ModelHandlingSystem], { with: SimulationSystemGroup }) + startSystems([ModelHandlingSystem, UploadRequestSystem], { with: SimulationSystemGroup }) startSystems([EditorInstanceNetworkingSystem, ClientNetworkingSystem, RenderInfoSystem], { after: PresentationSystemGroup diff --git a/packages/editor/src/systems/UploadRequestSystem.tsx b/packages/editor/src/systems/UploadRequestSystem.tsx new file mode 100644 index 0000000000..ee7ded2462 --- /dev/null +++ b/packages/editor/src/systems/UploadRequestSystem.tsx @@ -0,0 +1,26 @@ +import { defineSystem } from '@etherealengine/engine/src/ecs/functions/SystemFunctions' +import { defineAction, getMutableState, useState } from '@etherealengine/hyperflux' +import { useEffect } from 'react' + +import { UploadRequestState } from '@etherealengine/engine/src/assets/state/UploadRequestState' +import { uploadProjectFiles } from '../functions/assetFunctions' + +const clearUploadQueueAction = defineAction({ + type: 'ee.editor.clearUploadQueueAction' +}) + +export const UploadRequestSystem = defineSystem({ + uuid: 'ee.editor.UploadRequestSystem', + reactor: () => { + const uploadRequestState = useState(getMutableState(UploadRequestState)) + useEffect(() => { + const uploadRequests = uploadRequestState.queue.value + if (uploadRequests.length === 0) return + const uploadPromises = uploadRequests.map((uploadRequest) => { + return uploadProjectFiles(uploadRequest.projectName, [uploadRequest.file], true) + }) + uploadRequestState.queue.set([]) + }, [uploadRequestState.queue.length]) + return null + } +}) diff --git a/packages/engine/src/assets/exporters/gltf/extensions/BufferHandlerExtension.ts b/packages/engine/src/assets/exporters/gltf/extensions/BufferHandlerExtension.ts index 3409d6bd7d..c1b1dac890 100644 --- a/packages/engine/src/assets/exporters/gltf/extensions/BufferHandlerExtension.ts +++ b/packages/engine/src/assets/exporters/gltf/extensions/BufferHandlerExtension.ts @@ -27,11 +27,12 @@ import { sha3_256 } from 'js-sha3' import { Event, LoaderUtils, MathUtils, Mesh, Object3D } from 'three' import matches, { Validator } from 'ts-matches' -import { defineAction, dispatchAction } from '@etherealengine/hyperflux' +import { NO_PROXY, defineAction, dispatchAction, getMutableState } from '@etherealengine/hyperflux' import iterateObject3D from '../../../../scene/util/iterateObject3D' import { AssetLoader } from '../../../classes/AssetLoader' import { getProjectName, getRelativeURI, modelResourcesPath } from '../../../functions/pathResolver' +import { UploadRequestState } from '../../../state/UploadRequestState' import { GLTFExporterPlugin, GLTFWriter } from '../GLTFExporter' import { ExporterExtension } from './ExporterExtension' @@ -138,21 +139,15 @@ export default class BufferHandlerExtension extends ExporterExtension implements const projectSpaceModelName = this.resourceURI ? LoaderUtils.resolveURL(uri, LoaderUtils.extractUrlBase(modelName)) : modelName - const saveParms: BufferJson & { buffer: ArrayBuffer } = { - name, - byteLength: buffer.byteLength, - uri: this.resourceURI ? projectSpaceModelName.replace(/^assets\//, '') : uri, - buffer - } + const finalURI = this.resourceURI ? projectSpaceModelName.replace(/^assets\//, '') : uri imageDef.uri = uri imageDef.mimeType = `image/${AssetLoader.getAssetType(uri)}` - dispatchAction( - BufferHandlerExtension.saveBuffer({ - saveParms, - projectName, - modelName: projectSpaceModelName - }) - ) + const blob = new Blob([buffer]) + const file = new File([blob], finalURI) + const uploadRequestState = getMutableState(UploadRequestState) + const queue = uploadRequestState.queue.get(NO_PROXY) + const nuQueue = [...queue, { file, projectName }] + uploadRequestState.queue.set(nuQueue) }) ) } @@ -190,20 +185,14 @@ export default class BufferHandlerExtension extends ExporterExtension implements uri } json.buffers[index] = bufferDef - - const saveParms = { - ...bufferDef, - uri: this.resourceURI ? projectSpaceModelName.replace(/^assets\//, '') : uri, - buffer: buffers[index] - } if (!this.bufferCache[name]) { - dispatchAction( - BufferHandlerExtension.saveBuffer({ - projectName, - modelName: projectSpaceModelName, - saveParms - }) - ) + const finalURI = this.resourceURI ? projectSpaceModelName.replace(/^assets\//, '') : uri + const blob = new Blob([buffers[index]]) + const file = new File([blob], finalURI) + const uploadRequestState = getMutableState(UploadRequestState) + const queue = uploadRequestState.queue.get(NO_PROXY) + const nuQueue = [...queue, { file, projectName }] + uploadRequestState.queue.set(nuQueue) this.bufferCache[name] = uri } else { bufferDef.uri = this.bufferCache[name] diff --git a/packages/engine/src/assets/state/UploadRequestState.ts b/packages/engine/src/assets/state/UploadRequestState.ts new file mode 100644 index 0000000000..6fedf3cf83 --- /dev/null +++ b/packages/engine/src/assets/state/UploadRequestState.ts @@ -0,0 +1,17 @@ +import { defineState } from '@etherealengine/hyperflux' + +export type UploadRequest = { + file: File + projectName: string +} + +export const UploadRequestState = defineState({ + name: 'UploadRequestState', + initial: { + queue: [] as UploadRequest[] + } +}) + +export function executionPromiseKey(request: UploadRequest) { + return `${request.projectName}-${request.file.name}` +} From 17c7397ba5fda4d4e0665fd68e0aaa284f21b07b Mon Sep 17 00:00:00 2001 From: dinomut1 Date: Tue, 24 Oct 2023 16:52:23 -0700 Subject: [PATCH 12/16] working baseline client-side model transform --- .../src/systems/UploadRequestSystem.tsx | 25 +++++++++++++++++ .../compression/ModelTransformFunctions.ts | 28 +++++++++++++++---- .../src/assets/state/UploadRequestState.ts | 25 +++++++++++++++++ 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/systems/UploadRequestSystem.tsx b/packages/editor/src/systems/UploadRequestSystem.tsx index ee7ded2462..83feb70f7e 100644 --- a/packages/editor/src/systems/UploadRequestSystem.tsx +++ b/packages/editor/src/systems/UploadRequestSystem.tsx @@ -1,3 +1,28 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + import { defineSystem } from '@etherealengine/engine/src/ecs/functions/SystemFunctions' import { defineAction, getMutableState, useState } from '@etherealengine/hyperflux' import { useEffect } from 'react' diff --git a/packages/engine/src/assets/compression/ModelTransformFunctions.ts b/packages/engine/src/assets/compression/ModelTransformFunctions.ts index 65b26fa774..f2c67a2742 100644 --- a/packages/engine/src/assets/compression/ModelTransformFunctions.ts +++ b/packages/engine/src/assets/compression/ModelTransformFunctions.ts @@ -23,8 +23,6 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { getContentType } from '@etherealengine/common/src/utils/getContentType' - import { ExtractedImageTransformParameters, extractParameters, @@ -55,6 +53,8 @@ import { EEResourceID } from './extensions/EE_ResourceIDTransformer' import ModelTransformLoader from './ModelTransformLoader' import config from '@etherealengine/common/src/config' +import { getMutableState, NO_PROXY } from '@etherealengine/hyperflux' +import { UploadRequestState } from '../state/UploadRequestState' /** * @@ -383,7 +383,7 @@ export async function transformModel(args: ModelTransformParameters) { const fileUploadPath = (fUploadPath: string) => { const relativePath = fUploadPath.replace(config.client.fileServer, '') - const pathCheck = /projects\/([^/]+)\/assets\/([\w\d\s\-_.]*)$/ + const pathCheck = /projects\/([^/]+)\/assets\/([\w\d\s\-_./]*)$/ const [_, projectName, fileName] = pathCheck.exec(fUploadPath) ?? pathCheck.exec(pathJoin(LoaderUtils.extractUrlBase(args.src), fUploadPath))! return [projectName, fileName] @@ -642,7 +642,7 @@ export async function transformModel(args: ModelTransformParameters) { json.images?.map((image) => { const nuURI = pathJoin( - args.resourceUri ? args.resourceUri : resourceName, + args.resourceUri ? args.resourceUri : resourceName + '_resources', `${image.name}.${mimeToFileType(image.mimeType)}` ) resources[nuURI] = resources[image.uri!] @@ -651,13 +651,14 @@ export async function transformModel(args: ModelTransformParameters) { }) const defaultBufURI = MathUtils.generateUUID() + '.bin' json.buffers?.map((buffer) => { - buffer.uri = pathJoin(args.resourceUri ? args.resourceUri : resourceName, baseName(buffer.uri ?? defaultBufURI)) + buffer.uri = pathJoin(args.resourceUri ? args.resourceUri : resourcePath, baseName(buffer.uri ?? defaultBufURI)) }) Object.keys(resources).map((uri) => { const localPath = pathJoin(resourcePath, baseName(uri)) resources[localPath] = resources[uri] delete resources[uri] }) + /* const doUpload = async (uri, data) => { const [savePath, fileName] = fileUploadPath(uri) const args = { @@ -670,6 +671,23 @@ export async function transformModel(args: ModelTransformParameters) { } await Promise.all(Object.entries(resources).map(([uri, data]) => doUpload(uri, data))) result = await doUpload(args.dst.replace(/\.glb$/, '.gltf'), Buffer.from(JSON.stringify(json))) + */ + const doUpload = (buffer, uri) => { + const [projectName, fileName] = fileUploadPath(uri) + const file = new File([buffer], fileName) + const uploadRequestState = getMutableState(UploadRequestState) + const queue = uploadRequestState.queue.get(NO_PROXY) + uploadRequestState.queue.set([...queue, { file, projectName }]) + } + Object.entries(resources).map(([uri, data]) => { + doUpload(new Blob([data]), uri) + }) + let finalPath = args.dst.replace(/\.glb$/, '.gltf') + if (!finalPath.endsWith('.gltf')) { + finalPath += '.gltf' + } + doUpload(new Blob([JSON.stringify(json)], { type: 'application/json' }), finalPath) + console.log('Handled gltf file') } return result diff --git a/packages/engine/src/assets/state/UploadRequestState.ts b/packages/engine/src/assets/state/UploadRequestState.ts index 6fedf3cf83..dce99623d4 100644 --- a/packages/engine/src/assets/state/UploadRequestState.ts +++ b/packages/engine/src/assets/state/UploadRequestState.ts @@ -1,3 +1,28 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + import { defineState } from '@etherealengine/hyperflux' export type UploadRequest = { From daaf1e5ded8a59a30e06c02d92ed1a133a5f7e3f Mon Sep 17 00:00:00 2001 From: dinomut1 Date: Fri, 27 Oct 2023 21:48:12 -0700 Subject: [PATCH 13/16] client-side KTX2 Compression and image resize fix relative URIs for images and buffers --- .../compression/ModelTransformFunctions.ts | 106 +++++++++++++++--- .../extensions/EE_MaterialTransformer.ts | 17 ++- 2 files changed, 101 insertions(+), 22 deletions(-) diff --git a/packages/engine/src/assets/compression/ModelTransformFunctions.ts b/packages/engine/src/assets/compression/ModelTransformFunctions.ts index f2c67a2742..b158b23a76 100644 --- a/packages/engine/src/assets/compression/ModelTransformFunctions.ts +++ b/packages/engine/src/assets/compression/ModelTransformFunctions.ts @@ -39,8 +39,20 @@ import { Primitive, Texture } from '@gltf-transform/core' -import { EXTMeshGPUInstancing } from '@gltf-transform/extensions' -import { dedup, draco, flatten, join, palette, partition, prune, reorder, weld } from '@gltf-transform/functions' +import { EXTMeshGPUInstancing, KHRTextureBasisu } from '@gltf-transform/extensions' +import { + dedup, + draco, + flatten, + join, + palette, + partition, + prune, + reorder, + textureResize, + TextureResizeOptions, + weld +} from '@gltf-transform/functions' import { createHash } from 'crypto' import { MeshoptEncoder } from 'meshoptimizer' import { LoaderUtils, MathUtils } from 'three' @@ -48,7 +60,7 @@ import { LoaderUtils, MathUtils } from 'three' import { baseName, pathJoin } from '@etherealengine/common/src/utils/miscUtils' import { fileBrowserPath } from '@etherealengine/engine/src/schemas/media/file-browser.schema' import { Engine } from '../../ecs/classes/Engine' -import { EEMaterial } from './extensions/EE_MaterialTransformer' +import { EEMaterial, EEMaterialExtension } from './extensions/EE_MaterialTransformer' import { EEResourceID } from './extensions/EE_ResourceIDTransformer' import ModelTransformLoader from './ModelTransformLoader' @@ -56,6 +68,8 @@ import config from '@etherealengine/common/src/config' import { getMutableState, NO_PROXY } from '@etherealengine/hyperflux' import { UploadRequestState } from '../state/UploadRequestState' +import { KTX2Encoder } from '@etherealengine/xrui/core/textures/KTX2Encoder' +import { getPixels } from 'ndarray-pixels' /** * * @param doc @@ -473,17 +487,22 @@ export async function transformModel(args: ModelTransformParameters) { node instanceof Node && parent?.addChild(node) }) + const textures = root.listTextures() + const eeMaterialExtension: EEMaterialExtension | undefined = root + .listExtensionsUsed() + .find((ext) => ext.extensionName === 'EE_material') as EEMaterialExtension + if (eeMaterialExtension) { + textures.push(...eeMaterialExtension.textures) + } + /* PROCESS TEXTURES */ if (parms.textureFormat !== 'default') { - const textures = root - .listTextures() - .filter( - (texture) => - (mimeToFileType(texture.getMimeType()) !== parms.textureFormat && !!texture.getSize()) || - texture.getSize()?.reduce((x, y) => Math.max(x, y))! > parms.maxTextureSize - ) + let ktx2Encoder: KTX2Encoder | null = null for (const texture of textures) { const oldImg = texture.getImage() + if (!oldImg) continue + const oldSize = texture.getSize() + if (!oldSize) continue const resourceId = texture.getExtension('EEResourceID')?.resourceId const resourceParms = parms.resources.images.find( (resource) => resource.enabled && resource.resourceId === resourceId @@ -493,11 +512,50 @@ export async function transformModel(args: ModelTransformParameters) { ...(resourceParms ? extractParameters(resourceParms) : {}) } as ExtractedImageTransformParameters - const imgDoc = new Document() - const imgRoot = imgDoc.getRoot() - const nuTexture = imgDoc.createTexture(texture.getName()) - nuTexture.setImage(oldImg!) - nuTexture.setMimeType(texture.getMimeType()) + if ( + mimeToFileType(texture.getMimeType()) === mergedParms.textureFormat && + oldSize.reduce((x, y) => Math.max(x, y))! < mergedParms.maxTextureSize + ) + continue + + if (oldSize.reduce((x, y) => Math.max(x, y))! > mergedParms.maxTextureSize) { + const imgDoc = new Document() + const nuTexture = imgDoc.createTexture(texture.getName()) + nuTexture.setExtras(texture.getExtras()) + nuTexture.setImage(oldImg!) + nuTexture.setMimeType(texture.getMimeType()) + const resizeParms: TextureResizeOptions = { + size: [mergedParms.maxTextureSize, mergedParms.maxTextureSize] + } + await imgDoc.transform(textureResize(resizeParms)) + const originalName = texture.getName() + texture.copy(nuTexture) + texture.setName(originalName) + } + + if (mergedParms.textureFormat === 'ktx2' && texture.getMimeType() !== 'image/ktx2') { + if (!ktx2Encoder) { + ktx2Encoder = new KTX2Encoder() + } + const texturePixels = await getPixels(texture.getImage()!, texture.getMimeType()) + const clampedData = new Uint8ClampedArray(texturePixels.data as Uint8Array) + const imgSize = texture.getSize()! + const imgData = new ImageData(clampedData, ...imgSize) + + const compressedData = await ktx2Encoder.encode(imgData, { + uastc: mergedParms.textureCompressionType === 'uastc', + qualityLevel: mergedParms.textureCompressionQuality, + srgb: !mergedParms.linear, + mipmaps: mergedParms.mipmap, + yFlip: mergedParms.flipY + }) + + document.createExtension(KHRTextureBasisu).setRequired(true) + + texture.setImage(new Uint8Array(compressedData)) + texture.setMimeType('image/ktx2') + console.log('compressed image ' + texture.getName() + ' to ktx2') + } /* @@ -605,6 +663,14 @@ export async function transformModel(args: ModelTransformParameters) { )*/ console.log('Handled glb file') } else if (parms.modelFormat === 'gltf') { + const eeMaterialExtension: EEMaterialExtension | undefined = root + .listExtensionsUsed() + .find((ext) => ext.extensionName === 'EE_material') as EEMaterialExtension + if (eeMaterialExtension) { + for (const texture of eeMaterialExtension.textures) { + document.createTexture().copy(texture) + } + } await Promise.all( [root.listBuffers(), root.listMeshes(), root.listTextures()].map( async (elements) => @@ -643,7 +709,7 @@ export async function transformModel(args: ModelTransformParameters) { json.images?.map((image) => { const nuURI = pathJoin( args.resourceUri ? args.resourceUri : resourceName + '_resources', - `${image.name}.${mimeToFileType(image.mimeType)}` + `${image.uri ? image.uri.split('.')[0] : image.name}.${mimeToFileType(image.mimeType)}` ) resources[nuURI] = resources[image.uri!] delete resources[image.uri!] @@ -651,7 +717,10 @@ export async function transformModel(args: ModelTransformParameters) { }) const defaultBufURI = MathUtils.generateUUID() + '.bin' json.buffers?.map((buffer) => { - buffer.uri = pathJoin(args.resourceUri ? args.resourceUri : resourcePath, baseName(buffer.uri ?? defaultBufURI)) + buffer.uri = pathJoin( + args.resourceUri ? args.resourceUri : resourceName + '_resources', + baseName(buffer.uri ?? defaultBufURI) + ) }) Object.keys(resources).map((uri) => { const localPath = pathJoin(resourcePath, baseName(uri)) @@ -680,7 +749,8 @@ export async function transformModel(args: ModelTransformParameters) { uploadRequestState.queue.set([...queue, { file, projectName }]) } Object.entries(resources).map(([uri, data]) => { - doUpload(new Blob([data]), uri) + const blob = new Blob([data], { type: fileTypeToMime(uri.split('.').pop()!)! }) + doUpload(blob, uri) }) let finalPath = args.dst.replace(/\.glb$/, '.gltf') if (!finalPath.endsWith('.gltf')) { diff --git a/packages/engine/src/assets/compression/extensions/EE_MaterialTransformer.ts b/packages/engine/src/assets/compression/extensions/EE_MaterialTransformer.ts index 612d245ee1..a00ac796db 100644 --- a/packages/engine/src/assets/compression/extensions/EE_MaterialTransformer.ts +++ b/packages/engine/src/assets/compression/extensions/EE_MaterialTransformer.ts @@ -193,6 +193,8 @@ export class EEMaterialExtension extends Extension { public readonly extensionName = EXTENSION_NAME public static readonly EXTENSION_NAME = EXTENSION_NAME + textures: Texture[] = [] + textureInfoMap: Map = new Map() materialInfoMap: Map = new Map() public read(readerContext: ReaderContext): this { @@ -232,6 +234,12 @@ export class EEMaterialExtension extends Extension { if (texture) { const textureInfo = new TextureInfo(this.document.getGraph()) readerContext.setTextureInfo(textureInfo, value) + const uuid = textureUuidIndex.toString() + if (texture.getExtras().uuid === undefined) { + texture.setExtras({ uuid }) + textureUuidIndex++ + this.textures.push(texture) + } if (texture && value.extensions?.KHR_texture_transform) { const extensionData = value.extensions.KHR_texture_transform const transform = new KHRTextureTransform(this.document).createTransform() @@ -241,9 +249,6 @@ export class EEMaterialExtension extends Extension { extensionData.texCoord && transform.setTexCoord(extensionData.texCoord) textureInfo.setExtension('KHR_texture_transform', transform) } - const uuid = textureUuidIndex.toString() - textureUuidIndex++ - texture.setExtras({ uuid }) this.textureInfoMap.set(uuid, textureInfo) } nuArgDef.contents = texture @@ -297,7 +302,11 @@ export class EEMaterialExtension extends Extension { if (texture) { const uuid = texture.getExtras().uuid as string const textureInfo = this.textureInfoMap.get(uuid)! - argEntry.contents = writerContext.createTextureInfoDef(texture, textureInfo) + const docTexture = this.document + .getRoot() + .listTextures() + .find((t) => t.getExtras().uuid === uuid)! + argEntry.contents = writerContext.createTextureInfoDef(docTexture, textureInfo) } else { argEntry.contents = null } From 2ce1656c7cd029587f1d98d8535ff2bd19140bf8 Mon Sep 17 00:00:00 2001 From: dinomut1 Date: Mon, 30 Oct 2023 09:11:58 -0700 Subject: [PATCH 14/16] add basic client-side meshopt support --- .../compression/ModelTransformFunctions.ts | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/engine/src/assets/compression/ModelTransformFunctions.ts b/packages/engine/src/assets/compression/ModelTransformFunctions.ts index b158b23a76..82df839dbc 100644 --- a/packages/engine/src/assets/compression/ModelTransformFunctions.ts +++ b/packages/engine/src/assets/compression/ModelTransformFunctions.ts @@ -39,7 +39,7 @@ import { Primitive, Texture } from '@gltf-transform/core' -import { EXTMeshGPUInstancing, KHRTextureBasisu } from '@gltf-transform/extensions' +import { EXTMeshGPUInstancing, EXTMeshoptCompression, KHRTextureBasisu } from '@gltf-transform/extensions' import { dedup, draco, @@ -432,10 +432,42 @@ export async function transformModel(args: ModelTransformParameters) { /* /Meshopt Compression */ const document = await io.read(initialSrc) + const root = document.getRoot() await MeshoptEncoder.ready + /* + let primitives = root.listMeshes() + .flatMap((mesh) => mesh.listPrimitives()) + primitives = primitives.filter((primitive, index) => + primitives.findIndex((primitive2) => primitive2.equals(primitive)) === index + ) + for(const primitive of primitives) { + //STEP 1: Pre-process the mesh to improve index and vertex locality which increases compression ratio + const indices = new Uint32Array(primitive.getIndices()!.getArray()!) + const [remap, unique] = MeshoptEncoder.reorderMesh(indices, true, true) + const attributes = primitive.listAttributes() + for (const attribute of attributes) { + const oldAttributeArray = attribute.getArray()! + const reorderedAttributeArray = new Uint8Array(unique) + for (let i = 0; i < unique; i++) { + reorderedAttributeArray[i] = oldAttributeArray[remap[i]] + } + attribute.setArray(reorderedAttributeArray) + //STEP 2: Quantize data, either manually using integer or normalized integer format as a target, or using filter encoders + + //STEP 3: Encode data - const root = document.getRoot() + } + + } + */ + + if (args.meshoptCompression.enabled) { + const meshoptCompression = document.createExtension(EXTMeshoptCompression).setRequired(true) + meshoptCompression.setEncoderOptions({ + method: EXTMeshoptCompression.EncoderMethod.FILTER + }) + } /* ID unnamed resources */ unInstanceSingletons(document) From 8d20959bda3fa8b0a8b391d4202ba52b0891d40e Mon Sep 17 00:00:00 2001 From: dinomut1 Date: Wed, 1 Nov 2023 15:41:23 -0700 Subject: [PATCH 15/16] change server-side code to use engine transformer --- .../model-transform.helpers.ts | 638 +----------------- .../model-transform/model-transform.job.ts | 4 +- 2 files changed, 3 insertions(+), 639 deletions(-) diff --git a/packages/server-core/src/assets/model-transform/model-transform.helpers.ts b/packages/server-core/src/assets/model-transform/model-transform.helpers.ts index f99176fac9..a83e70bf51 100644 --- a/packages/server-core/src/assets/model-transform/model-transform.helpers.ts +++ b/packages/server-core/src/assets/model-transform/model-transform.helpers.ts @@ -23,649 +23,13 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { - ExtractedImageTransformParameters, - extractParameters, - ModelTransformParameters -} from '@etherealengine/engine/src/assets/classes/ModelTransform' -import { - BufferUtils, - Document, - Format, - Buffer as glBuffer, - Material, - Mesh, - Node, - Primitive, - Texture -} from '@gltf-transform/core' -import { EXTMeshGPUInstancing } from '@gltf-transform/extensions' -import { dedup, draco, flatten, join, palette, partition, prune, reorder, weld } from '@gltf-transform/functions' +import { ModelTransformParameters } from '@etherealengine/engine/src/assets/classes/ModelTransform' import * as k8s from '@kubernetes/client-node' -import appRootPath from 'app-root-path' -import { execFileSync } from 'child_process' -import { createHash } from 'crypto' -import fs from 'fs' -import { MeshoptEncoder } from 'meshoptimizer' -import path from 'path' -import { MathUtils } from 'three' import { objectToArgs } from '@etherealengine/common/src/utils/objectToCommandLineArgs' -import { fileBrowserPath } from '@etherealengine/engine/src/schemas/media/file-browser.schema' import { Application } from '../../../declarations' import config from '../../appconfig' import { getPodsData } from '../../cluster/pods/pods-helper' -import { getContentType } from '../../util/fileUtils' -import { EEMaterial } from '../extensions/EE_MaterialTransformer' -import { EEResourceID } from '../extensions/EE_ResourceIDTransformer' -import ModelTransformLoader from '../ModelTransformLoader' - -/** - * - * @param doc - * @param batchExtension - * @param mesh - * @param count - * @returns - */ -const createBatch = (doc: Document, batchExtension: EXTMeshGPUInstancing, mesh: Mesh, count) => { - return mesh.listPrimitives().map((prim) => { - const buffer = prim.getAttribute('POSITION')?.getBuffer() ?? doc.createBuffer() - - const batchTranslation = doc - .createAccessor() - .setType('VEC3') - .setArray(new Float32Array(3 * count)) - .setBuffer(buffer) - const batchRotation = doc - .createAccessor() - .setType('VEC4') - .setArray(new Float32Array(4 * count)) - .setBuffer(buffer) - const batchScale = doc - .createAccessor() - .setType('VEC3') - .setArray(new Float32Array(3 * count)) - .setBuffer(buffer) - - return batchExtension - .createInstancedMesh() - .setAttribute('TRANSLATION', batchTranslation) - .setAttribute('ROTATION', batchRotation) - .setAttribute('SCALE', batchScale) - }) -} - -function pruneUnusedNodes(nodes: Node[], logger) { - let node: Node | undefined - let unusedNodes = 0 - while ((node = nodes.pop())) { - if ( - node.listChildren().length || - node.getCamera() || - node.getMesh() || - node.getSkin() || - node.listExtensions().length - ) { - continue - } - const nodeParent = node.getParentNode() as Node - if (nodeParent instanceof Node) { - nodes.push(nodeParent) - } - node.dispose() - unusedNodes++ - console.log(`Pruned ${unusedNodes} nodes.`) - } -} - -function removeUVsOnUntexturedMeshes(document: Document) { - document - .getRoot() - .listMeshes() - .map((mesh) => { - const prims = mesh.listPrimitives() - if (prims.length === 1) { - const prim = prims[0] - const material = prim.getMaterial() - if ( - material && - (material.getBaseColorTexture() || - material.getNormalTexture() || - material.getEmissiveTexture() || - material.getOcclusionTexture() || - material.getMetallicRoughnessTexture()) - ) { - return - } - prim.setAttribute('TEXCOORD_0', null) - prim.setAttribute('TEXCOORD_1', null) - } - }) -} - -const split = async (document: Document) => { - const root = document.getRoot() - const scene = root.listScenes()[0] - const toSplit = root.listNodes().filter((node) => { - const mesh = node.getMesh() - const prims = mesh?.listPrimitives() - return mesh && prims && prims.length > 1 - }) - const primMeshes = new Map() - toSplit.map((node) => { - const mesh = node.getMesh()! - const nuNodes: Node[] = [] - mesh.listPrimitives().map((prim, primIdx) => { - if (!primMeshes.has(prim)) { - primMeshes.set(prim, document.createMesh(mesh.getName() + '-' + primIdx).addPrimitive(prim)) - } else { - console.log('found cached prim') - } - const nuNode = document.createNode(node.getName() + '-' + primIdx).setMesh(primMeshes.get(prim)) - node.getSkin() && nuNode.setSkin(node.getSkin()) - ;(node.getParentNode() ?? scene).addChild(nuNode) - nuNode.setMatrix(node.getMatrix()) - nuNodes.push(nuNode) - }) - node.listChildren().map((child) => { - nuNodes[0]?.addChild(child) - }) - node.detach() - }) - toSplit.map((node) => { - const mesh = node.getMesh()! - mesh.listPrimitives().map((prim, primIdx) => { - if (primIdx > 0) { - mesh.removePrimitive(prim) - } - }) - node.setMesh(null) - }) -} - -const myInstance = async (document: Document) => { - const root = document.getRoot() - const scene = root.listScenes()[0] - const batchExtension = document.createExtension(EXTMeshGPUInstancing) - const meshes = root.listMeshes() - console.log('meshes:', meshes) - const nodes = root.listNodes().filter((node) => node.getMesh()) - const table = nodes.reduce( - (_table, node) => { - const mesh = node.getMesh() - const idx = meshes.findIndex((mesh2) => mesh?.equals(mesh2)) - _table[idx] = _table[idx] ?? [] - _table[idx].push(node) - return _table - }, - {} as Record - ) - console.log('table:', table) - const modifiedNodes = new Set() - Object.entries(table) - .filter(([_, _nodes]) => _nodes.length > 1) - .map(([meshIdx, _nodes]) => { - const mesh = meshes[meshIdx] - console.log('mesh:', mesh, 'nodes:', nodes) - const batches = createBatch(document, batchExtension, mesh, _nodes.length) - batches.map((batch) => { - const batchTranslate = batch.getAttribute('TRANSLATION')! - const batchRotate = batch.getAttribute('ROTATION')! - const batchScale = batch.getAttribute('SCALE')! - const batchNode = document.createNode().setMesh(mesh).setExtension('EXT_mesh_gpu_instancing', batch) - scene.addChild(batchNode) - _nodes.map((node, i) => { - batchTranslate.setElement(i, node.getWorldTranslation()) - batchRotate.setElement(i, node.getWorldRotation()) - batchScale.setElement(i, node.getWorldScale()) - node.setMesh(null) - modifiedNodes.add(node) - }) - }) - console.log('modified nodes: ', modifiedNodes) - pruneUnusedNodes([...modifiedNodes], document.getLogger()) - }) -} - -function unInstanceSingletons(document: Document) { - const root = document.getRoot() - root - .listNodes() - .filter((node) => (node.getExtension('EXT_mesh_gpu_instancing') as any)?.listAttributes()?.[0].getCount() === 1) - .map((node) => { - console.log('removed instanced singleton', node.getName()) - node.setExtension('EXT_mesh_gpu_instancing', null) //delete instancing - }) -} - -export type ModelTransformArguments = { - src: string - dst: string - resourceUri: string - parms: ModelTransformParameters -} - -export async function combineMaterials(document: Document) { - const root = document.getRoot() - const cache: Material[] = [] - console.log('combining materials...') - root.listMaterials().map((material) => { - const eeMat = material.getExtension('EE_material') - const dupe = cache.find((cachedMaterial) => { - const cachedEEMat = cachedMaterial.getExtension('EE_material') - if (eeMat !== null && cachedEEMat !== null) { - return ( - eeMat.prototype === cachedEEMat.prototype && - ((eeMat.args === cachedEEMat.args) === null || (cachedEEMat.args && eeMat.args?.equals(cachedEEMat.args))) - ) - } else return material.equals(cachedMaterial) - }) - if (dupe !== undefined) { - console.log('found duplicate material...') - let dupeCount = 0 - root - .listMeshes() - .flatMap((mesh) => mesh.listPrimitives()) - .map((prim) => { - if (prim.getMaterial() === material) { - prim.setMaterial(dupe) - dupeCount++ - } - }) - console.log('replaced ' + dupeCount + ' materials') - } else { - cache.push(material) - } - }) -} - -export async function combineMeshes(document: Document) { - const root = document.getRoot() - const prims = root.listMeshes().flatMap((mesh) => mesh.listPrimitives()) - const matMap = new Map() - for (const prim of prims) { - const material = prim.getMaterial() - if (material) { - if (!matMap.has(material)) { - matMap.set(material, []) - } - const matPrims = matMap.get(material) - matPrims?.push(prim) - } - } - const nuPrims = [...matMap.entries()].map(([material, prims]) => { - const nuPrim = document.createPrimitive() - nuPrim.setMaterial(material) - prims.map((prim) => { - prim.listSemantics().map((key) => { - const accessor = prim.getAttribute(key)! - let nuAttrib = nuPrim.getAttribute(key) - if (!nuAttrib) { - nuPrim.setAttribute(key, accessor) - nuAttrib = accessor - } else { - nuAttrib.setArray( - BufferUtils.concat([Uint8Array.from(nuAttrib.getArray()!), Uint8Array.from(accessor.getArray()!)]) - ) - } - }) - }) - return nuPrim - }) - root.listNodes().map((node) => { - if (node.getMesh()) { - node.setMesh(null) - } - }) - nuPrims.map((nuPrim) => { - root.listScenes()[0].addChild(document.createNode().setMesh(document.createMesh().addPrimitive(nuPrim))) - }) -} - -function hashBuffer(buffer: Uint8Array): string { - const hash = createHash('sha256') - hash.update(buffer) - return hash.digest('hex') -} - -export async function transformModel(app: Application, args: ModelTransformParameters) { - const parms = args - const serverDir = path.join(appRootPath.path, 'packages/server') - const tmpDir = path.join(serverDir, 'tmp') - const BASIS_U = path.join(appRootPath.path, 'packages/server/public/loader_decoders/basisu') - const GLTF_PACK = path.join(appRootPath.path, 'packages/server/public/loader_decoders/gltfpack') - const toTmp = (fileName) => { - return `${tmpDir}/${fileName}` - } - - /** - * - * @param {string} mimeType - * @returns - */ - const mimeToFileType = (mimeType) => { - switch (mimeType) { - case 'image/jpg': - case 'image/jpeg': - return 'jpg' - case 'image/png': - return 'png' - case 'image/ktx2': - return 'ktx2' - default: - return null - } - } - - const fileTypeToMime = (fileType) => { - switch (fileType) { - case 'jpg': - return 'image/jpg' - case 'png': - return 'image/png' - case 'ktx2': - return 'image/ktx2' - default: - return null - } - } - - const resourceName = /*'model-resources'*/ path.basename(args.src).slice(0, path.basename(args.src).lastIndexOf('.')) - const resourcePath = args.resourceUri - ? path.join(path.dirname(args.src), args.resourceUri) - : path.join(path.dirname(args.src), resourceName) - const projectRoot = path.join(appRootPath.path, 'packages/projects') - - const toValidFilename = (name: string) => { - const result = name.replace(/[\s]/g, '-') - return result - } - let pathIndex = 0 - const toPath = (element: Texture | glBuffer, index?: number) => { - if (element instanceof Texture) { - if (element.getURI()) { - return path.basename(element.getURI()) - } else { - pathIndex++ - return `${toValidFilename(element.getName())}-${pathIndex}-.${mimeToFileType(element.getMimeType())}` - } - } else if (element instanceof glBuffer) { - return `buffer-${index}-${Date.now()}.bin` - } else throw new Error('invalid element to find path') - } - - const fileUploadPath = (fUploadPath: string) => { - const pathCheck = /.*\/packages\/projects\/(.*)\/([\w\d\s\-_.]*)$/ - const [_, savePath, fileName] = - pathCheck.exec(fUploadPath) ?? pathCheck.exec(path.join(path.dirname(args.src), fUploadPath))! - return [savePath, fileName] - } - - const { io } = await ModelTransformLoader() - - let initialSrc = args.src - /* Meshopt Compression */ - if (args.meshoptCompression.enabled) { - const segments = args.src.split('.') - const ext = segments.pop() - const base = segments.join('.') - initialSrc = `${base}-meshopt.${ext}` - let packArgs = `-i ${args.src} -o ${initialSrc} -noq ` - if (!args.meshoptCompression.options.mergeMaterials) { - packArgs += `-km ` - } - if (!args.meshoptCompression.options.mergeNodes) { - packArgs += `-kn ` - } - if (args.meshoptCompression.options.compression) { - packArgs += `-cc ` - } - execFileSync( - GLTF_PACK, - packArgs.split(/\s+/).filter((x) => !!x) - ) - } - /* /Meshopt Compression */ - - const document = await io.read(initialSrc) - - await MeshoptEncoder.ready - - const root = document.getRoot() - - /* ID unnamed resources */ - unInstanceSingletons(document) - args.split && (await split(document)) - args.combineMaterials && (await combineMaterials(document)) - args.instance && (await myInstance(document)) - args.dedup && (await document.transform(dedup())) - args.flatten && (await document.transform(flatten())) - args.join.enabled && (await document.transform(join(args.join.options))) - if (args.palette.enabled) { - removeUVsOnUntexturedMeshes(document) - await document.transform(palette(args.palette.options)) - } - args.prune && (await document.transform(prune())) - - /* Separate Instanced Geometry */ - const instancedNodes = root - .listNodes() - .filter((node) => !!node.getMesh()?.getExtension('EXT_mesh_gpu_instancing')) - .map((node) => [node, node.getParent()]) - instancedNodes.map(([node, parent]) => { - node instanceof Node && parent?.removeChild(node) - }) - - /* PROCESS MESHES */ - if (args.weld.enabled) { - await document.transform(weld({ tolerance: args.weld.tolerance })) - } - - if (args.reorder) { - await document.transform( - reorder({ - encoder: MeshoptEncoder, - target: 'performance' - }) - ) - } - - /* Draco Compression */ - if (args.dracoCompression.enabled) { - await document.transform(draco(args.dracoCompression.options)) - } - /* /Draco Compression */ - - /* /PROCESS MESHES */ - - /* Return Instanced Geometry to Scene Graph */ - instancedNodes.map(([node, parent]) => { - node instanceof Node && parent?.addChild(node) - }) - - /* PROCESS TEXTURES */ - if (parms.textureFormat !== 'default') { - const textures = root - .listTextures() - .filter( - (texture) => - (mimeToFileType(texture.getMimeType()) !== parms.textureFormat && !!texture.getSize()) || - texture.getSize()?.reduce((x, y) => Math.max(x, y))! > parms.maxTextureSize - ) - for (const texture of textures) { - const oldImg = texture.getImage() - const resourceId = texture.getExtension('EEResourceID')?.resourceId - const resourceParms = parms.resources.images.find( - (resource) => resource.enabled && resource.resourceId === resourceId - ) - const mergedParms = { - ...args, - ...(resourceParms ? extractParameters(resourceParms) : {}) - } as ExtractedImageTransformParameters - - const imgDoc = new Document() - const imgRoot = imgDoc.getRoot() - const nuTexture = imgDoc.createTexture(texture.getName()) - nuTexture.setImage(oldImg!) - nuTexture.setMimeType(texture.getMimeType()) - - /* - - Old command line processing for image resizing - - const fileName = toPath(texture) - const oldPath = toTmp(fileName) - const resizeExtension = mergedParms.textureFormat === 'ktx2' ? 'png' : mergedParms.textureFormat - const resizedPath = oldPath.replace( - new RegExp(`\\.${mimeToFileType(texture.getMimeType())}$`), - `-resized.${resizeExtension}` - ) - if (!fs.existsSync(tmpDir)) { - fs.mkdirSync(tmpDir) - } - fs.writeFileSync(oldPath, oldImg!) - const xResizedName = fileName.replace( - new RegExp(`\\.${mimeToFileType(texture.getMimeType())}$`), - `-resized.${mergedParms.textureFormat}` - ) - const nuFileName = fileName.replace( - new RegExp(`\\.${mimeToFileType(texture.getMimeType())}$`), - `-transformed.${mergedParms.textureFormat}` - ) - const nuPath = `${tmpDir}/${nuFileName}` - - try { - if (path.extname(oldPath) === '.ktx2') { - console.warn('cannot resize ktx2 compressed image at ' + oldPath) - continue - } - const img = await sharp(oldPath) - const metadata = await img.metadata() - let resizedDimension = 2 - while ( - resizedDimension * 2 <= - Math.min(mergedParms.maxTextureSize, Math.max(metadata.width, metadata.height)) - ) { - resizedDimension *= 2 - } - //resize the image to be no larger than the max texture size - await img - .resize(resizedDimension, resizedDimension, { - fit: 'fill' - }) - .toFormat(resizeExtension) - .toFile(resizedPath.replace(/\.[\w\d]+$/, `.${resizeExtension}`)) - console.log('handled image file ' + oldPath) - } catch (e) { - console.error('error while handling image ' + oldPath) - console.error(e) - }*/ - - /* - if (mergedParms.textureFormat === 'ktx2') { - //KTX2 Basisu Compression - document.createExtension(KHRTextureBasisu).setRequired(true) - - - const basisArgs = `-ktx2 ${resizedPath} -q ${mergedParms.textureCompressionQuality} ${ - mergedParms.textureCompressionType === 'uastc' ? '-uastc' : '' - } ${mergedParms.textureCompressionType === 'uastc' ? '-uastc_level ' + mergedParms.uastcLevel : ''} ${ - mergedParms.textureCompressionType === 'etc1' ? '-comp_level ' + mergedParms.compLevel : '' - } ${ - mergedParms.textureCompressionType === 'etc1' && mergedParms.maxCodebooks - ? '-max_endpoints 16128 -max_selectors 16128' - : '' - } ${mergedParms.linear ? '-linear' : ''} ${mergedParms.flipY ? '-y_flip' : ''} ${ - mergedParms.mipmap ? '-mipmap' : '' - }` - .split(/\s+/) - .filter((x) => !!x) - execFileSync(BASIS_U, basisArgs) - execFileSync('mv', [`${serverDir}/${xResizedName}`, nuPath]) - console.log('loaded ktx2 image ' + nuPath) - } else { - execFileSync('mv', [resizedPath, nuPath]) - } - texture.setImage(fs.readFileSync(nuPath)) - texture.setMimeType(fileTypeToMime(mergedParms.textureFormat) ?? texture.getMimeType()) - */ - } - } - let result - if (parms.modelFormat === 'glb') { - const data = Buffer.from(await io.writeBinary(document)) - const [savePath, fileName] = fileUploadPath(args.dst) - result = await app.service('file-browser').patch(null, { - path: savePath, - fileName, - body: data, - contentType: getContentType(args.dst) - }) - console.log('Handled glb file') - } else if (parms.modelFormat === 'gltf') { - ;[root.listBuffers(), root.listMeshes(), root.listTextures()].forEach((elements) => - elements.map((element: Texture | Mesh | glBuffer) => { - let elementName = '' - if (element instanceof Texture) { - elementName = hashBuffer(element.getImage()!) - } else if (element instanceof Mesh) { - elementName = hashBuffer(Uint8Array.from(element.listPrimitives()[0].getAttribute('POSITION')!.getArray()!)) - } else if (element instanceof glBuffer) { - const bufferPath = path.join(path.dirname(args.src), element.getURI()) - const bufferData = fs.readFileSync(bufferPath) - elementName = hashBuffer(bufferData) - } - element.setName(elementName) - }) - ) - document.transform( - partition({ - animations: true, - meshes: root.listMeshes().map((mesh) => mesh.getName()) - }) - ) - const { json, resources } = await io.writeJSON(document, { format: Format.GLTF, basename: resourceName }) - if (!fs.existsSync(resourcePath)) { - await app.service(fileBrowserPath).create(resourcePath.replace(projectRoot, '') as any) - } - json.images?.map((image) => { - const nuURI = path.join( - args.resourceUri ? args.resourceUri : resourceName, - `${image.name}.${mimeToFileType(image.mimeType)}` - ) - resources[nuURI] = resources[image.uri!] - delete resources[image.uri!] - image.uri = nuURI - }) - const defaultBufURI = MathUtils.generateUUID() + '.bin' - json.buffers?.map((buffer) => { - buffer.uri = path.join( - args.resourceUri ? args.resourceUri : resourceName, - path.basename(buffer.uri ?? defaultBufURI) - ) - }) - Object.keys(resources).map((uri) => { - const localPath = path.join(resourcePath, path.basename(uri)) - resources[localPath] = resources[uri] - delete resources[uri] - }) - const doUpload = (uri, data) => { - const [savePath, fileName] = fileUploadPath(uri) - return app.service(fileBrowserPath).patch(null, { - path: savePath, - fileName, - body: data, - contentType: getContentType(uri) - }) - } - await Promise.all(Object.entries(resources).map(([uri, data]) => doUpload(uri, data))) - result = await doUpload(args.dst.replace(/\.glb$/, '.gltf'), Buffer.from(JSON.stringify(json))) - console.log('Handled gltf file') - } - fs.existsSync(tmpDir) && (await execFileSync('rm', ['-R', tmpDir])) - return result -} export async function getModelTransformJobBody( app: Application, diff --git a/packages/server-core/src/assets/model-transform/model-transform.job.ts b/packages/server-core/src/assets/model-transform/model-transform.job.ts index 041a75a158..2b4bbd0d52 100644 --- a/packages/server-core/src/assets/model-transform/model-transform.job.ts +++ b/packages/server-core/src/assets/model-transform/model-transform.job.ts @@ -29,9 +29,9 @@ import dotenv from 'dotenv-flow' import { argsToObject } from '@etherealengine/common/src/utils/objectToCommandLineArgs' import { ModelTransformParameters } from '@etherealengine/engine/src/assets/classes/ModelTransform' +import { transformModel } from '@etherealengine/engine/src/assets/compression/ModelTransformFunctions' import { ServerMode } from '@etherealengine/server-core/src/ServerState' import { createFeathersKoaApp } from '@etherealengine/server-core/src/createApp' -import { transformModel } from './model-transform.helpers' const modelTransformParameters: ModelTransformParameters = argsToObject(process.argv.slice(3)) @@ -58,7 +58,7 @@ cli.main(async () => { try { const app = createFeathersKoaApp(ServerMode.API) await app.setup() - await transformModel(app, modelTransformParameters) + await transformModel(modelTransformParameters) cli.exit(0) } catch (err) { console.log(err) From 0174addd5de7ee4f0b1e5f2e99dde79c4aeeeb14 Mon Sep 17 00:00:00 2001 From: dinomut1 Date: Wed, 1 Nov 2023 15:52:56 -0700 Subject: [PATCH 16/16] fix transformModel call --- .../src/assets/model-transform/model-transform.class.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server-core/src/assets/model-transform/model-transform.class.ts b/packages/server-core/src/assets/model-transform/model-transform.class.ts index e4f50f7460..ef1b26a270 100644 --- a/packages/server-core/src/assets/model-transform/model-transform.class.ts +++ b/packages/server-core/src/assets/model-transform/model-transform.class.ts @@ -30,11 +30,11 @@ import appRootPath from 'app-root-path' import path from 'path' import config from '../../appconfig' -import { createExecutorJob } from '../../projects/project/project-helper' -import { getModelTransformJobBody, transformModel } from './model-transform.helpers' - +import { transformModel } from '@etherealengine/engine/src/assets/compression/ModelTransformFunctions' import { BadRequest } from '@feathersjs/errors/lib' import { KnexAdapterParams } from '@feathersjs/knex/lib' +import { createExecutorJob } from '../../projects/project/project-helper' +import { getModelTransformJobBody } from './model-transform.helpers' export interface ModelTransformParams extends KnexAdapterParams { transformParameters: ModelTransformParameters @@ -63,7 +63,7 @@ export class ModelTransformService implements ServiceInterface { const createParams: ModelTransformParameters = data console.log('config', config) if (!config.kubernetes?.enabled) { - return transformModel(this.app, createParams) + return transformModel(createParams) } try { const transformParms = createParams