diff --git a/server/collections/impl/EdanCollection.ts b/server/collections/impl/EdanCollection.ts index fec45433a..6e06aef1e 100644 --- a/server/collections/impl/EdanCollection.ts +++ b/server/collections/impl/EdanCollection.ts @@ -171,6 +171,9 @@ export class EdanCollection implements COL.ICollection { } async publish(idSystemObject: number, ePublishState: number): Promise { + + LOG.info(`EdanCollection.publish (idSystemObject: ${idSystemObject}`,LOG.LS.eDEBUG); + switch (ePublishState) { case COMMON.ePublishedState.eNotPublished: case COMMON.ePublishedState.eAPIOnly: diff --git a/server/collections/impl/PublishScene.ts b/server/collections/impl/PublishScene.ts index c3a647e78..f2271cea9 100644 --- a/server/collections/impl/PublishScene.ts +++ b/server/collections/impl/PublishScene.ts @@ -76,6 +76,7 @@ export class PublishScene { if (resource) resourceMap.set(SAC, resource); } + // LOG.info(`>>> PublishScene.computeResourceMap (scene: ${this.sceneFile} | ${H.Helpers.JSONStringify(resourceMap)})`,LOG.LS.eDEBUG); return resourceMap; } @@ -86,12 +87,16 @@ export class PublishScene { return false; } - if (!this.scene || !this.subject) + if (!this.scene || !this.subject) { + LOG.info(`PublishScene.publish cannot publish. no scene/subject. (scene: ${this.scene?.Name} | subject: ${this.subject?.Name})`,LOG.LS.eCOLL); return false; + } // stage scene - if (!await this.stageSceneFiles() || !this.sharedName) + if (!await this.stageSceneFiles() || !this.sharedName) { + LOG.error(`PublishScene.publish cannot stage files. (scene: ${this.scene?.Name} | sharedName: ${this.sharedName})`,LOG.LS.eCOLL); return false; + } // create EDAN 3D Package let edanRecord: COL.EdanRecord | null = await ICol.createEdan3DPackage(this.sharedName, this.sceneFile); @@ -356,10 +361,13 @@ export class PublishScene { return false; } + LOG.info(`PublishScene.collectAssets downloadMSXMap ${H.Helpers.JSONStringify(this.DownloadMSXMap)}`, LOG.LS.eDEBUG); + // first pass through assets: detect and record the scene file (first file to match *.svx.json); lookup supporting information // prepare to extract and discard the path to this file, so that the scene zip is "rooted" at the svx.json let stageRes: H.IOResults = { success: true }; for (const assetVersion of this.assetVersions) { + // grab our system object for this asset version const asset: DBAPI.Asset | null = await DBAPI.Asset.fetch(assetVersion.idAsset); LOG.info(`PublishScene.collectAssets considering assetVersion=${JSON.stringify(assetVersion, H.Helpers.saferStringify)} asset=${JSON.stringify(asset, H.Helpers.saferStringify)}`, LOG.LS.eCOLL); if (!asset) { @@ -368,6 +376,9 @@ export class PublishScene { } // determine if assetVersion is an attachment by examining metadata + // NOTE: isAttachment metadata is set in computeResourceMap to label an asset as a 'resource' (i.e. downloadable) + // however, usdz/draco files have dual use and are resources, but also connected to a voyager scene. below is a special + // case for this situation until downloadable is decoupled from Usage. let isAttachment: boolean = false; const SOAssetVersion: DBAPI.SystemObject | null = await assetVersion.fetchSystemObject(); if (!SOAssetVersion) { @@ -383,20 +394,34 @@ export class PublishScene { } } } else - LOG.error(`PublishScene.collectAssets unable to compute metadata for ${JSON.stringify(assetVersion, H.Helpers.saferStringify)}`, LOG.LS.eCOLL); + LOG.error(`PublishScene.collectAssets unable to compute metadata for ${H.Helpers.JSONStringify(assetVersion)}`, LOG.LS.eCOLL); + // if we have our asset and a system object (always the case?) then we see if this asset is in the download/resource + // map... if (asset.idSystemObject) { const modelSceneXref: DBAPI.ModelSceneXref | undefined = this.DownloadMSXMap.get(asset.idSystemObject ?? 0); - if (!modelSceneXref) + if (!modelSceneXref) { + // LOG.info(`>>> PublishScene.collectAssets adding to list, no MSX (${asset.FileName}|${isAttachment}|${H.Helpers.JSONStringify(metadataSet)}`, LOG.LS.eDEBUG); this.SacList.push({ idSystemObject: asset.idSystemObject, asset, assetVersion, metadataSet: isAttachment ? metadataSet : undefined }); - else { + } else { const model: DBAPI.Model | null = await DBAPI.Model.fetch(modelSceneXref.idModel); if (!model) { - LOG.error(`PublishScene.collectAssets unable to load model from xref ${JSON.stringify(modelSceneXref, H.Helpers.saferStringify)}`, LOG.LS.eCOLL); + LOG.error(`PublishScene.collectAssets unable to load model from xref ${H.Helpers.JSONStringify(modelSceneXref)}`, LOG.LS.eCOLL); return false; } + // LOG.info(`>>> PublishScene.collectAssets adding to list with MSX (${asset.FileName}|${isAttachment}|${H.Helpers.JSONStringify(modelSceneXref)}`, LOG.LS.eDEBUG); this.SacList.push({ idSystemObject: asset.idSystemObject, asset, assetVersion, model, modelSceneXref, metadataSet: isAttachment ? metadataSet : undefined }); + + // HACK: special case for handling dual-use assets (draco/usdz), which will be added as downloads/attachments by default. + // whe check if we're dealing with that asset and add like other scene referenced assets. (i.e. we omit the MSX and metadataSet) + if(modelSceneXref.Usage === 'App3D' || modelSceneXref.Usage === 'iOSApp3D') { + LOG.info(`>>> PublishScene.collectAssets adding draco/usdz assets again (${asset.FileName})`,LOG.LS.eDEBUG); + this.SacList.push({ idSystemObject: asset.idSystemObject, asset, assetVersion, metadataSet: undefined }); + } } + + } else { + LOG.info(`PublishScene.collectAssets not adding. no idSystemObject ${H.Helpers.JSONStringify(asset)}|${H.Helpers.JSONStringify(metadataSet)}`, LOG.LS.eDEBUG); } if (!this.sceneFile && assetVersion.FileName.toLowerCase().endsWith('.svx.json')) { @@ -427,10 +452,16 @@ export class PublishScene { } private async stageSceneFiles(): Promise { - if (this.SacList.length <= 0 || !this.scene) + if (this.SacList.length <= 0 || !this.scene) { + LOG.error(`PublishScene.stageSceneFiles casnnot stage files for scene (${this.sceneFile}). No scene or assets list (${this.scene}|${this.SacList.length})`,LOG.LS.eCOLL); return false; + } let stageRes: H.IOResults = { success: true }; + // log what will be included + const assets: string[] = this.SacList.map(SAC => { return `${SAC.asset.FileName}(${SAC.assetVersion.Version})`; }); + LOG.info(`PublishScene.stageSceneFiles collecting assets for scene (${this.sceneFile}): ${assets.join(',')}`,LOG.LS.eCOLL); + // second pass: zip up appropriate assets; prepare to copy downloads const zip: ZIP.ZipStream = new ZIP.ZipStream(); for (const SAC of this.SacList.values()) { @@ -704,11 +735,16 @@ export class PublishScene { switch (SAC.modelSceneXref.Usage?.replace('Download:', '').toLowerCase()) { case undefined: case 'webassetglblowuncompressed': category = 'Low resolution'; MODEL_FILE_TYPE = 'glb'; break; - case 'webassetglbarcompressed': category = 'Low resolution'; MODEL_FILE_TYPE = 'glb'; DRACO_COMPRESSED = true; break; - case 'usdz': category = 'iOS AR model'; MODEL_FILE_TYPE = 'usdz'; break; case 'objzipfull': category = 'Full resolution'; MODEL_FILE_TYPE = 'obj'; break; case 'objziplow': category = 'Low resolution'; MODEL_FILE_TYPE = 'obj'; break; case 'gltfziplow': category = 'Low resolution'; MODEL_FILE_TYPE = 'gltf'; break; + + // HACK: special case to account for USDZ/Draco models which have dual purpose 'usage' + // and requires specific Usage to work. fix when decoupling 'downloadable' property. + case 'usdz': + case 'iosapp3d': category = 'iOS AR model'; MODEL_FILE_TYPE = 'usdz'; break; + case 'webassetglbarcompressed': + case 'app3d': category = 'Low resolution'; MODEL_FILE_TYPE = 'glb'; DRACO_COMPRESSED = true; break; } } @@ -739,6 +775,8 @@ export class PublishScene { LOG.error(`PublishScene.updatePublishedState unable to update published state for ${JSON.stringify(this.systemObjectVersion, H.Helpers.saferStringify)}`, LOG.LS.eCOLL); return false; } + } else { + LOG.info(`PublishScene.updatePublishedState skipping.... ${H.Helpers.JSONStringify(ePublishedStateIntended)}`, LOG.LS.eDEBUG); } return true; diff --git a/server/config/index.ts b/server/config/index.ts index b8bf92ee0..6e039a3ce 100644 --- a/server/config/index.ts +++ b/server/config/index.ts @@ -108,7 +108,8 @@ export type ConfigType = { }; const oneDay = 24 * 60 * 60; -const fifteenMinutes = 1 * 15 * 60; +// const fifteenMinutes = 1 * 15 * 60; +const threeHours = 3 * 60 * 60; const debugSessionTimeout = false; export const Config: ConfigType = { @@ -118,7 +119,7 @@ export const Config: ConfigType = { auth: { type: process.env.PACKRAT_AUTH_TYPE == 'LDAP' ? AUTH_TYPE.LDAP : AUTH_TYPE.LOCAL, session: { - maxAge: !debugSessionTimeout ? (((process.env.NODE_ENV === 'production') ? fifteenMinutes : oneDay) * 1000) : 8000, // expiration time 15 minutes, in milliseconds + maxAge: !debugSessionTimeout ? (((process.env.NODE_ENV === 'production') ? threeHours : oneDay) * 1000) : 8000, // expiration time 15 minutes, in milliseconds checkPeriod: oneDay // prune expired entries every 24 hours }, ldap: { diff --git a/server/job/impl/Cook/JobCookSIGenerateDownloads.ts b/server/job/impl/Cook/JobCookSIGenerateDownloads.ts index 3cfada57d..f90fa8606 100644 --- a/server/job/impl/Cook/JobCookSIGenerateDownloads.ts +++ b/server/job/impl/Cook/JobCookSIGenerateDownloads.ts @@ -56,8 +56,12 @@ export class JobCookSIGenerateDownloadsParameters { this.units = units ? units : undefined; this.metaDataFile = metaDataFile ? metaDataFile : undefined; this.parameterHelper = parameterHelper ? parameterHelper : undefined; + this.units = units ? units : undefined; + this.metaDataFile = metaDataFile ? metaDataFile : undefined; + this.parameterHelper = parameterHelper ? parameterHelper : undefined; } + idScene: number | undefined; idModel: number | undefined; sourceMeshFile: string; // required @@ -135,7 +139,7 @@ export class JobCookSIGenerateDownloads extends JobCook { + // grab our Packrat Scene from the database. idScene is a parameter passed in when creating this object + // grab our Packrat Scene from the database. idScene is a parameter passed in when creating this object const sceneSource: DBAPI.Scene | null = this.idScene ? await DBAPI.Scene.fetch(this.idScene) : null; if (!sceneSource) @@ -183,7 +189,7 @@ export class JobCookSIGenerateDownloads extends JobCook>> model file results: ${H.Helpers.JSONStringify(currentItemResult)}`,LOG.LS.eDEBUG); @@ -350,7 +361,18 @@ export class JobCookSIGenerateDownloads extends JobCook>> model (${model.fileName}) linking to scene (${sceneSource.Name}|${SOV.idSystemObject})`,LOG.LS.eDEBUG); + const SOVAVX: DBAPI.SystemObjectVersionAssetVersionXref | null = + await DBAPI.SystemObjectVersionAssetVersionXref.addOrUpdate(SOV.idSystemObjectVersion, model.data.assetVersion.idAsset, model.data.assetVersion.idAssetVersion); + if (!SOVAVX) + LOG.error(`JobCookSIGenerateDownloads.processSceneFile unable create/update SystemObjectVersionAssetVersionXref for ${H.Helpers.JSONStringify(SOV)}, ${H.Helpers.JSONStringify(model.data.assetVersion)}`, LOG.LS.eJOB); + } + } // Add scene asset metadata for attachments // LOG.info('JobCookSIGenerateDownloads.createSystemObjects calling PublishScene.extractSceneMetadata', LOG.LS.eJOB); @@ -423,7 +445,7 @@ export class JobCookSIGenerateDownloads extends JobCook, MSX: DBAPI.ModelSceneXref } | null> { //Promise { + private async processModelFile(sceneSource: DBAPI.Scene, modelSource: DBAPI.Model, fileItem: FileProcessItem, RSR: STORE.ReadStreamResult, idUserCreator: number ): Promise<{ assetVersionOverrideMap: Map< number, number>, MSX: DBAPI.ModelSceneXref, assetVersion: DBAPI.AssetVersion|null } | null> { //Promise { // verify input if(!sceneSource || fileItem.fileName.length<=0 || idUserCreator < 0 || RSR == null) { @@ -467,12 +489,12 @@ export class JobCookSIGenerateDownloads extends JobCook { @@ -731,19 +753,8 @@ export class JobCookSIGenerateDownloads extends JobCook 0) { const SOX2: DBAPI.SystemObjectXref | null = await DBAPI.SystemObjectXref.wireObjectsIfNeeded(OG.item[0], scene); if (!SOX2) - return this.logError(`JobCookSIGenerateDownloads.processSceneFile unable to wire item ${JSON.stringify(OG.item[0], H.Helpers.saferStringify)} to Scene ${JSON.stringify(scene, H.Helpers.saferStringify)}: database error`); + return this.logError(`JobCookSIGenerateDownloads.processSceneFile unable to wire item ${H.Helpers.JSONStringify(OG.item[0])} to Scene ${H.Helpers.JSONStringify(scene)}: database error`); } - // LOG.info(`JobCookSIGenerateDownloads.processSceneFile[${svxFile}] wire ModelSource to Scene: ${JSON.stringify(SOX, H.Helpers.stringifyMapsAndBigints)}`, LOG.LS.eJOB); + // LOG.info(`JobCookSIGenerateDownloads.processSceneFile[${svxFile}] wire ModelSource to Scene: ${H.Helpers.JSONStringify(SOX)}`, LOG.LS.eJOB); } else { LOG.info(`JobCookSIGenerateDownloads.processSceneFile updating existing scene (${scene.Name}|${scene.EdanUUID})`,LOG.LS.eJOB); @@ -846,9 +857,9 @@ export class JobCookSIGenerateDownloads extends JobCook; + captionUris: Dictionary; + durations: Dictionary; } \ No newline at end of file diff --git a/server/types/voyager/model.ts b/server/types/voyager/model.ts index a5abfd46c..7941041c7 100644 --- a/server/types/voyager/model.ts +++ b/server/types/voyager/model.ts @@ -1,11 +1,10 @@ /* eslint-disable quotes, @typescript-eslint/brace-style */ /** * NOTE: this file is part of the definition of a Voyager scene, found in a .svx.json file. - * This was imported from Voyager's source/client/schema on 1/27/2023. It was then modified, + * This was imported from Voyager's source/client/schema on 1/24/2024. It was then modified, * minimally, to allow for use by Packrat. Ideally, in the future, we will extract out the * definition of this shared file format for use by both projects. */ - /** * 3D Foundation Project * Copyright 2019 Smithsonian Institution @@ -84,6 +83,9 @@ export interface IAnnotation taglist?: Dictionary; articleId?: string; imageUri?: string; + imageCredit?: Dictionary; + imageAltText?: Dictionary; + audioId?: string; style?: string; visible?: boolean; diff --git a/server/types/voyager/setup.ts b/server/types/voyager/setup.ts index 5d43ca642..73a460032 100644 --- a/server/types/voyager/setup.ts +++ b/server/types/voyager/setup.ts @@ -1,12 +1,11 @@ -/* eslint-disable quotes, @typescript-eslint/brace-style, @typescript-eslint/no-explicit-any */ +/* eslint-disable quotes, @typescript-eslint/brace-style */ /** * NOTE: this file is part of the definition of a Voyager scene, found in a .svx.json file. - * This was imported from Voyager's source/client/schema on 1/27/2023. It was then modified, + * This was imported from Voyager's source/client/schema on 1/24/2024. It was then modified, * minimally, to allow for use by Packrat. Ideally, in the future, we will extract out the * definition of this shared file format for use by both projects. */ - -// import { Dictionary } from "@ff/core/types"; +//import { Dictionary } from "@ff/core/types"; type Dictionary = Record; import { ELanguageType, TLanguageType } from "./common"; @@ -74,6 +73,8 @@ export interface IViewer exposure: number; toneMapping: boolean; gamma: number; + isWallMountAR: boolean; + arScale: number; annotationsVisible?: boolean; activeTags?: string; sortedTags?: string; @@ -169,6 +170,7 @@ export interface ISlicer axis: TSliceAxis; inverted: boolean; position: number; + color: number[]; } export type ITours = ITour[];