diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dd2079a02c96133684f60967045ffb4fc246b4d..b4db179818e9737f01fb45474f49171cbb606499 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Note that since we don't clearly distinguish between a public and private interf - Set default outline scale back to 1 - Improved DCD reader cell angle handling (intepret near 0 angles as 90 deg) - Handle more residue/atom names commonly used in force-fields +- Add USDZ support to ``geo-export`` extension. ## [v2.1.0] - 2021-07-05 diff --git a/src/extensions/geo-export/controls.ts b/src/extensions/geo-export/controls.ts index 0fe4c61a3e7fcb879c5502a1c21b9a45dc495d13..90c30ef9a7c5607a7e53eef7434c33b0f1ed7aaa 100644 --- a/src/extensions/geo-export/controls.ts +++ b/src/extensions/geo-export/controls.ts @@ -13,15 +13,17 @@ import { PluginStateObject } from '../../mol-plugin-state/objects'; import { StateSelection } from '../../mol-state'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { SetUtils } from '../../mol-util/set'; -import { ObjExporter } from './obj-exporter'; import { GlbExporter } from './glb-exporter'; +import { ObjExporter } from './obj-exporter'; import { StlExporter } from './stl-exporter'; +import { UsdzExporter } from './usdz-exporter'; export const GeometryParams = { format: PD.Select('glb', [ ['glb', 'glTF 2.0 Binary (.glb)'], ['stl', 'Stl (.stl)'], - ['obj', 'Wavefront (.obj)'] + ['obj', 'Wavefront (.obj)'], + ['usdz', 'Universal Scene Description (.usdz)'] ]) }; @@ -44,11 +46,12 @@ export class GeometryControls extends PluginComponent { const renderObjects = this.plugin.canvas3d?.getRenderObjects()!; const filename = this.getFilename(); - const boundingBox = Box3D.fromSphere3D(Box3D(), this.plugin.canvas3d?.boundingSphereVisible!); - let renderObjectExporter: GlbExporter | ObjExporter | StlExporter; + const style = getStyle(this.plugin.canvas3d?.props.renderer.style!); + const boundingSphere = this.plugin.canvas3d?.boundingSphereVisible!; + const boundingBox = Box3D.fromSphere3D(Box3D(), boundingSphere); + let renderObjectExporter: GlbExporter | ObjExporter | StlExporter | UsdzExporter; switch (this.behaviors.params.value.format) { case 'glb': - const style = getStyle(this.plugin.canvas3d?.props.renderer.style!); renderObjectExporter = new GlbExporter(style, boundingBox); break; case 'obj': @@ -57,6 +60,9 @@ export class GeometryControls extends PluginComponent { case 'stl': renderObjectExporter = new StlExporter(boundingBox); break; + case 'usdz': + renderObjectExporter = new UsdzExporter(style, boundingBox, boundingSphere.radius); + break; default: throw new Error('Unsupported format.'); } diff --git a/src/extensions/geo-export/glb-exporter.ts b/src/extensions/geo-export/glb-exporter.ts index c915a1e4508de82db6dce6be1263079e123b36c1..bff1c816765dca67f4df02b782abcbfeb3e4bfc6 100644 --- a/src/extensions/geo-export/glb-exporter.ts +++ b/src/extensions/geo-export/glb-exporter.ts @@ -264,7 +264,7 @@ export class GlbExporter extends MeshExporter<GlbData> { } } - getData() { + async getData() { const binaryBufferLength = this.byteOffset; const gltf = { @@ -334,7 +334,7 @@ export class GlbExporter extends MeshExporter<GlbData> { } async getBlob(ctx: RuntimeContext) { - return new Blob([this.getData().glb], { type: 'model/gltf-binary' }); + return new Blob([(await this.getData()).glb], { type: 'model/gltf-binary' }); } constructor(private style: Style, boundingBox: Box3D) { diff --git a/src/extensions/geo-export/mesh-exporter.ts b/src/extensions/geo-export/mesh-exporter.ts index 41837c2ad8d2f20bbe8318dfc289e5db82a5d44a..51774da9905501bc7ec90ceb8a17a78f55af023f 100644 --- a/src/extensions/geo-export/mesh-exporter.ts +++ b/src/extensions/geo-export/mesh-exporter.ts @@ -4,6 +4,7 @@ * @author Sukolsak Sakshuwong <sukolsak@stanford.edu> */ +import { sort, arraySwap } from '../../mol-data/util'; import { GraphicsRenderObject } from '../../mol-gl/render-object'; import { MeshValues } from '../../mol-gl/renderable/mesh'; import { LinesValues } from '../../mol-gl/renderable/lines'; @@ -22,6 +23,7 @@ import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder'; import { sizeDataFactor } from '../../mol-geo/geometry/size-data'; import { Vec3 } from '../../mol-math/linear-algebra'; import { RuntimeContext } from '../../mol-task'; +import { Color } from '../../mol-util/color/color'; import { decodeFloatRGB } from '../../mol-util/float-packing'; import { RenderObjectExporter, RenderObjectExportData } from './render-object-exporter'; @@ -111,6 +113,70 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements return interpolated.array; } + protected static quantizeColors(colorArray: Uint8Array, vertexCount: number) { + if (vertexCount <= 1024) return; + const rgb = Vec3(); + const min = Vec3(); + const max = Vec3(); + const sum = Vec3(); + const colorMap = new Map<Color, Color>(); + const colorComparers = [ + (colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[0] - Color.toVec3(rgb, colors[j])[0]), + (colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[1] - Color.toVec3(rgb, colors[j])[1]), + (colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[2] - Color.toVec3(rgb, colors[j])[2]), + ]; + + const medianCut = (colors: Color[], l: number, r: number, depth: number) => { + if (l > r) return; + if (l === r || depth >= 10) { + // Find the average color. + Vec3.set(sum, 0, 0, 0); + for (let i = l; i <= r; ++i) { + Color.toVec3(rgb, colors[i]); + Vec3.add(sum, sum, rgb); + } + Vec3.round(rgb, Vec3.scale(rgb, sum, 1 / (r - l + 1))); + const averageColor = Color.fromArray(rgb, 0); + for (let i = l; i <= r; ++i) colorMap.set(colors[i], averageColor); + return; + } + + // Find the color channel with the greatest range. + Vec3.set(min, 255, 255, 255); + Vec3.set(max, 0, 0, 0); + for (let i = l; i <= r; ++i) { + Color.toVec3(rgb, colors[i]); + for (let j = 0; j < 3; ++j) { + Vec3.min(min, min, rgb); + Vec3.max(max, max, rgb); + } + } + let k = 0; + if (max[1] - min[1] > max[k] - min[k]) k = 1; + if (max[2] - min[2] > max[k] - min[k]) k = 2; + + sort(colors, l, r + 1, colorComparers[k], arraySwap); + + const m = (l + r) >> 1; + medianCut(colors, l, m, depth + 1); + medianCut(colors, m + 1, r, depth + 1); + }; + + // Create an array of unique colors and use the median cut algorithm. + const colorSet = new Set<Color>(); + for (let i = 0; i < vertexCount; ++i) { + colorSet.add(Color.fromArray(colorArray, i * 3)); + } + const colors = Array.from(colorSet); + medianCut(colors, 0, colors.length - 1, 0); + + // Map actual colors to quantized colors. + for (let i = 0; i < vertexCount; ++i) { + const color = colorMap.get(Color.fromArray(colorArray, i * 3)); + Color.toArray(color!, colorArray, i * 3); + } + } + protected static getInstance(input: AddMeshInput, instanceIndex: number) { const { mesh, meshes } = input; if (mesh !== undefined) { @@ -278,7 +344,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements } } - abstract getData(): D; + abstract getData(ctx: RuntimeContext): Promise<D>; abstract getBlob(ctx: RuntimeContext): Promise<Blob>; } \ No newline at end of file diff --git a/src/extensions/geo-export/obj-exporter.ts b/src/extensions/geo-export/obj-exporter.ts index 6d5d716f859214a8ce67938c92829036b418dbbf..d68363a928442423d37bacbe8c61f9ff416e8856 100644 --- a/src/extensions/geo-export/obj-exporter.ts +++ b/src/extensions/geo-export/obj-exporter.ts @@ -4,7 +4,6 @@ * @author Sukolsak Sakshuwong <sukolsak@stanford.edu> */ -import { sort, arraySwap } from '../../mol-data/util'; import { asciiWrite } from '../../mol-io/common/ascii'; import { Box3D } from '../../mol-math/geometry'; import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra'; @@ -69,70 +68,6 @@ export class ObjExporter extends MeshExporter<ObjData> { } } - private static quantizeColors(colorArray: Uint8Array, vertexCount: number) { - if (vertexCount <= 1024) return; - const rgb = Vec3(); - const min = Vec3(); - const max = Vec3(); - const sum = Vec3(); - const colorMap = new Map<Color, Color>(); - const colorComparers = [ - (colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[0] - Color.toVec3(rgb, colors[j])[0]), - (colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[1] - Color.toVec3(rgb, colors[j])[1]), - (colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[2] - Color.toVec3(rgb, colors[j])[2]), - ]; - - const medianCut = (colors: Color[], l: number, r: number, depth: number) => { - if (l > r) return; - if (l === r || depth >= 10) { - // Find the average color. - Vec3.set(sum, 0, 0, 0); - for (let i = l; i <= r; ++i) { - Color.toVec3(rgb, colors[i]); - Vec3.add(sum, sum, rgb); - } - Vec3.round(rgb, Vec3.scale(rgb, sum, 1 / (r - l + 1))); - const averageColor = Color.fromArray(rgb, 0); - for (let i = l; i <= r; ++i) colorMap.set(colors[i], averageColor); - return; - } - - // Find the color channel with the greatest range. - Vec3.set(min, 255, 255, 255); - Vec3.set(max, 0, 0, 0); - for (let i = l; i <= r; ++i) { - Color.toVec3(rgb, colors[i]); - for (let j = 0; j < 3; ++j) { - Vec3.min(min, min, rgb); - Vec3.max(max, max, rgb); - } - } - let k = 0; - if (max[1] - min[1] > max[k] - min[k]) k = 1; - if (max[2] - min[2] > max[k] - min[k]) k = 2; - - sort(colors, l, r + 1, colorComparers[k], arraySwap); - - const m = (l + r) >> 1; - medianCut(colors, l, m, depth + 1); - medianCut(colors, m + 1, r, depth + 1); - }; - - // Create an array of unique colors and use the median cut algorithm. - const colorSet = new Set<Color>(); - for (let i = 0; i < vertexCount; ++i) { - colorSet.add(Color.fromArray(colorArray, i * 3)); - } - const colors = Array.from(colorSet); - medianCut(colors, 0, colors.length - 1, 0); - - // Map actual colors to quantized colors. - for (let i = 0; i < vertexCount; ++i) { - const color = colorMap.get(Color.fromArray(colorArray, i * 3)); - Color.toArray(color!, colorArray, i * 3); - } - } - protected async addMeshWithColors(input: AddMeshInput) { const { mesh, values, isGeoTexture, webgl, ctx } = input; @@ -256,7 +191,7 @@ export class ObjExporter extends MeshExporter<ObjData> { } } - getData() { + async getData() { return { obj: StringBuilder.getString(this.obj), mtl: StringBuilder.getString(this.mtl) @@ -264,7 +199,7 @@ export class ObjExporter extends MeshExporter<ObjData> { } async getBlob(ctx: RuntimeContext) { - const { obj, mtl } = this.getData(); + const { obj, mtl } = await this.getData(); const objData = new Uint8Array(obj.length); asciiWrite(objData, obj); const mtlData = new Uint8Array(mtl.length); diff --git a/src/extensions/geo-export/render-object-exporter.ts b/src/extensions/geo-export/render-object-exporter.ts index 7c099d3ddb23f9243bc9b93d2cd36727f704a7a5..542a457c1c887b77be82046ae1ab9a78a24ac353 100644 --- a/src/extensions/geo-export/render-object-exporter.ts +++ b/src/extensions/geo-export/render-object-exporter.ts @@ -9,12 +9,12 @@ import { WebGLContext } from '../../mol-gl/webgl/context'; import { RuntimeContext } from '../../mol-task'; export type RenderObjectExportData = { - [k: string]: string | Uint8Array | undefined + [k: string]: string | Uint8Array | ArrayBuffer | undefined } export interface RenderObjectExporter<D extends RenderObjectExportData> { readonly fileExtension: string add(renderObject: GraphicsRenderObject, webgl: WebGLContext, ctx: RuntimeContext): Promise<void> | undefined - getData(): D + getData(ctx: RuntimeContext): Promise<D> getBlob(ctx: RuntimeContext): Promise<Blob> } \ No newline at end of file diff --git a/src/extensions/geo-export/stl-exporter.ts b/src/extensions/geo-export/stl-exporter.ts index 22b00e2a37cae2511da139becdd14400cfab3ce6..35812cbe4959bd4c3abfe8d7b70f5ffe18574ab3 100644 --- a/src/extensions/geo-export/stl-exporter.ts +++ b/src/extensions/geo-export/stl-exporter.ts @@ -89,7 +89,7 @@ export class StlExporter extends MeshExporter<StlData> { } } - getData() { + async getData() { const stl = new Uint8Array(84 + 50 * this.triangleCount); asciiWrite(stl, `Exported from Mol* ${PLUGIN_VERSION}`); @@ -106,7 +106,7 @@ export class StlExporter extends MeshExporter<StlData> { } async getBlob(ctx: RuntimeContext) { - return new Blob([this.getData().stl], { type: 'model/stl' }); + return new Blob([(await this.getData()).stl], { type: 'model/stl' }); } constructor(boundingBox: Box3D) { diff --git a/src/extensions/geo-export/ui.tsx b/src/extensions/geo-export/ui.tsx index 09a843c87cde560ffc1b0049231e21019c9db7f4..546e47da42d0a35a012f090daafe6e5a5cefd7b1 100644 --- a/src/extensions/geo-export/ui.tsx +++ b/src/extensions/geo-export/ui.tsx @@ -7,7 +7,7 @@ import { merge } from 'rxjs'; import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base'; import { Button } from '../../mol-plugin-ui/controls/common'; -import { GetAppSvg, CubeSendSvg } from '../../mol-plugin-ui/controls/icons'; +import { GetAppSvg, CubeScanSvg, CubeSendSvg } from '../../mol-plugin-ui/controls/icons'; import { ParameterControls } from '../../mol-plugin-ui/controls/parameters'; import { download } from '../../mol-util/download'; import { GeometryParams, GeometryControls } from './controls'; @@ -18,6 +18,7 @@ interface State { export class GeometryExporterUI extends CollapsableControls<{}, State> { private _controls: GeometryControls | undefined; + private isARSupported: boolean | undefined; get controls() { return this._controls || (this._controls = new GeometryControls(this.plugin)); @@ -32,6 +33,9 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> { } protected renderControls(): JSX.Element { + if (this.isARSupported === undefined) { + this.isARSupported = !!document.createElement('a').relList?.supports?.('ar'); + } const ctrl = this.controls; return <> <ParameterControls @@ -45,6 +49,13 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> { disabled={this.state.busy || !this.plugin.canvas3d?.reprCount.value}> Save </Button> + {this.isARSupported && ctrl.behaviors.params.value.format === 'usdz' && + <Button icon={CubeScanSvg} + onClick={this.viewInAR} style={{ marginTop: 1 }} + disabled={this.state.busy || !this.plugin.canvas3d?.reprCount.value}> + View in AR + </Button> + } </>; } @@ -75,4 +86,22 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> { this.setState({ busy: false }); } } + + viewInAR = async () => { + try { + this.setState({ busy: true }); + const data = await this.controls.exportGeometry(); + this.setState({ busy: false }); + const a = document.createElement('a'); + a.rel = 'ar'; + a.href = URL.createObjectURL(data.blob); + // For in-place viewing of USDZ on iOS, the link must contain a single child that is either an img or picture. + // https://webkit.org/blog/8421/viewing-augmented-reality-assets-in-safari-for-ios/ + a.appendChild(document.createElement('img')); + setTimeout(() => URL.revokeObjectURL(a.href), 4E4); // 40s + setTimeout(() => a.dispatchEvent(new MouseEvent('click'))); + } catch { + this.setState({ busy: false }); + } + } } \ No newline at end of file diff --git a/src/extensions/geo-export/usdz-exporter.ts b/src/extensions/geo-export/usdz-exporter.ts new file mode 100644 index 0000000000000000000000000000000000000000..d8e7a89deb5ee9e25e4b5cc9f0630059570ce01c --- /dev/null +++ b/src/extensions/geo-export/usdz-exporter.ts @@ -0,0 +1,262 @@ +/** + * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Sukolsak Sakshuwong <sukolsak@stanford.edu> + */ + +import { Style } from '../../mol-gl/renderer'; +import { asciiWrite } from '../../mol-io/common/ascii'; +import { Box3D } from '../../mol-math/geometry'; +import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra'; +import { PLUGIN_VERSION } from '../../mol-plugin/version'; +import { RuntimeContext } from '../../mol-task'; +import { StringBuilder } from '../../mol-util'; +import { Color } from '../../mol-util/color/color'; +import { zip } from '../../mol-util/zip/zip'; +import { MeshExporter, AddMeshInput } from './mesh-exporter'; + +// avoiding namespace lookup improved performance in Chrome (Aug 2020) +const v3fromArray = Vec3.fromArray; +const v3transformMat4 = Vec3.transformMat4; +const v3transformMat3 = Vec3.transformMat3; +const mat3directionTransform = Mat3.directionTransform; + +// https://graphics.pixar.com/usd/docs/index.html + +export type UsdzData = { + usdz: ArrayBuffer +} + +export class UsdzExporter extends MeshExporter<UsdzData> { + readonly fileExtension = 'usdz'; + private meshes: string[] = []; + private materials: string[] = []; + private materialSet = new Set<number>(); + private centerTransform: Mat4; + + private static getMaterialKey(color: Color, alpha: number) { + return color * 256 + Math.round(alpha * 255); + } + + private addMaterial(color: Color, alpha: number) { + const materialKey = UsdzExporter.getMaterialKey(color, alpha); + if (this.materialSet.has(materialKey)) return; + this.materialSet.add(materialKey); + const [r, g, b] = Color.toRgbNormalized(color); + this.materials.push(` +def Material "material${materialKey}" +{ + token outputs:surface.connect = </material${materialKey}/shader.outputs:surface> + def Shader "shader" + { + uniform token info:id = "UsdPreviewSurface" + color3f inputs:diffuseColor = (${r},${g},${b}) + float inputs:opacity = ${alpha} + float inputs:metallic = ${this.style.metalness} + float inputs:roughness = ${this.style.roughness} + token outputs:surface + } +} +`); + } + + protected async addMeshWithColors(input: AddMeshInput) { + const { mesh, values, isGeoTexture, webgl, ctx } = input; + + const t = Mat4(); + const n = Mat3(); + const tmpV = Vec3(); + const stride = isGeoTexture ? 4 : 3; + + const groupCount = values.uGroupCount.ref.value; + const colorType = values.dColorType.ref.value; + const tColor = values.tColor.ref.value.array; + const uAlpha = values.uAlpha.ref.value; + const dTransparency = values.dTransparency.ref.value; + const tTransparency = values.tTransparency.ref.value; + const aTransform = values.aTransform.ref.value; + const instanceCount = values.uInstanceCount.ref.value; + + let interpolatedColors: Uint8Array; + if (colorType === 'volume' || colorType === 'volumeInstance') { + interpolatedColors = UsdzExporter.getInterpolatedColors(mesh!.vertices, mesh!.vertexCount, values, stride, colorType, webgl!); + UsdzExporter.quantizeColors(interpolatedColors, mesh!.vertexCount); + } + + await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount }); + + for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) { + if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 }); + + const { vertices, normals, indices, groups, vertexCount, drawCount } = UsdzExporter.getInstance(input, instanceIndex); + + Mat4.fromArray(t, aTransform, instanceIndex * 16); + Mat4.mul(t, this.centerTransform, t); + mat3directionTransform(n, t); + + const vertexBuilder = StringBuilder.create(); + const normalBuilder = StringBuilder.create(); + const indexBuilder = StringBuilder.create(); + + // position + for (let i = 0; i < vertexCount; ++i) { + v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * stride), t); + StringBuilder.writeSafe(vertexBuilder, (i === 0) ? '(' : ',('); + StringBuilder.writeFloat(vertexBuilder, tmpV[0], 10000); + StringBuilder.writeSafe(vertexBuilder, ','); + StringBuilder.writeFloat(vertexBuilder, tmpV[1], 10000); + StringBuilder.writeSafe(vertexBuilder, ','); + StringBuilder.writeFloat(vertexBuilder, tmpV[2], 10000); + StringBuilder.writeSafe(vertexBuilder, ')'); + } + + // normal + for (let i = 0; i < vertexCount; ++i) { + v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n); + StringBuilder.writeSafe(normalBuilder, (i === 0) ? '(' : ',('); + StringBuilder.writeFloat(normalBuilder, tmpV[0], 100); + StringBuilder.writeSafe(normalBuilder, ','); + StringBuilder.writeFloat(normalBuilder, tmpV[1], 100); + StringBuilder.writeSafe(normalBuilder, ','); + StringBuilder.writeFloat(normalBuilder, tmpV[2], 100); + StringBuilder.writeSafe(normalBuilder, ')'); + } + + // face + for (let i = 0; i < drawCount; ++i) { + const v = isGeoTexture ? i : indices![i]; + if (i > 0) StringBuilder.writeSafe(indexBuilder, ','); + StringBuilder.writeInteger(indexBuilder, v); + } + + // color + const faceIndicesByMaterial = new Map<number, number[]>(); + for (let i = 0; i < drawCount; i += 3) { + let color: Color; + switch (colorType) { + case 'uniform': + color = Color.fromNormalizedArray(values.uColor.ref.value, 0); + break; + case 'instance': + color = Color.fromArray(tColor, instanceIndex * 3); + break; + case 'group': { + const group = isGeoTexture ? UsdzExporter.getGroup(groups, i) : groups[indices![i]]; + color = Color.fromArray(tColor, group * 3); + break; + } + case 'groupInstance': { + const group = isGeoTexture ? UsdzExporter.getGroup(groups, i) : groups[indices![i]]; + color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3); + break; + } + case 'vertex': + color = Color.fromArray(tColor, indices![i] * 3); + break; + case 'vertexInstance': + color = Color.fromArray(tColor, (instanceIndex * vertexCount + indices![i]) * 3); + break; + case 'volume': + color = Color.fromArray(interpolatedColors!, (isGeoTexture ? i : indices![i]) * 3); + break; + case 'volumeInstance': + color = Color.fromArray(interpolatedColors!, (instanceIndex * vertexCount + (isGeoTexture ? i : indices![i])) * 3); + break; + default: throw new Error('Unsupported color type.'); + } + + let alpha = uAlpha; + if (dTransparency) { + const group = isGeoTexture ? UsdzExporter.getGroup(groups, i) : groups[indices![i]]; + const transparency = tTransparency.array[instanceIndex * groupCount + group] / 255; + alpha *= 1 - transparency; + } + + this.addMaterial(color, alpha); + + const materialKey = UsdzExporter.getMaterialKey(color, alpha); + let faceIndices = faceIndicesByMaterial.get(materialKey); + if (faceIndices === undefined) { + faceIndices = []; + faceIndicesByMaterial.set(materialKey, faceIndices); + } + faceIndices.push(i / 3); + } + + // If this mesh uses only one material, bind it to the material directly. + // Otherwise, use GeomSubsets to bind it to multiple materials. + let materialBinding: string; + if (faceIndicesByMaterial.size === 1) { + const materialKey = faceIndicesByMaterial.keys().next().value; + materialBinding = `rel material:binding = </material${materialKey}>`; + } else { + const geomSubsets: string[] = []; + faceIndicesByMaterial.forEach((faceIndices: number[], materialKey: number) => { + geomSubsets.push(` + def GeomSubset "g${materialKey}" + { + uniform token elementType = "face" + uniform token familyName = "materialBind" + int[] indices = [${faceIndices.join(',')}] + rel material:binding = </material${materialKey}> + } +`); + }); + materialBinding = geomSubsets.join(''); + } + + this.meshes.push(` +def Mesh "mesh${this.meshes.length}" +{ + int[] faceVertexCounts = [${new Array(drawCount / 3).fill(3).join(',')}] + int[] faceVertexIndices = [${StringBuilder.getString(indexBuilder)}] + point3f[] points = [${StringBuilder.getString(vertexBuilder)}] + normal3f[] primvars:normals = [${StringBuilder.getString(normalBuilder)}] ( + interpolation = "vertex" + ) + uniform token subdivisionScheme = "none" + ${materialBinding} +} +`); + } + } + + async getData(ctx: RuntimeContext) { + const header = `#usda 1.0 +( + customLayerData = { + string creator = "Mol* ${PLUGIN_VERSION}" + } + metersPerUnit = 1 +) +`; + const usda = [header, ...this.materials, ...this.meshes].join(''); + const usdaData = new Uint8Array(usda.length); + asciiWrite(usdaData, usda); + const zipDataObj = { + ['model.usda']: usdaData + }; + return { + usdz: await zip(ctx, zipDataObj, true) + }; + } + + async getBlob(ctx: RuntimeContext) { + const { usdz } = await this.getData(ctx); + return new Blob([usdz], { type: 'model/vnd.usdz+zip' }); + } + + constructor(private style: Style, boundingBox: Box3D, radius: number) { + super(); + const t = Mat4(); + // scale the model so that it fits within 1 meter + Mat4.fromUniformScaling(t, Math.min(1 / (radius * 2), 1)); + // translate the model so that it sits on the ground plane (y = 0) + Mat4.translate(t, t, Vec3.create( + -(boundingBox.min[0] + boundingBox.max[0]) / 2, + -boundingBox.min[1], + -(boundingBox.min[2] + boundingBox.max[2]) / 2 + )); + this.centerTransform = t; + } +} \ No newline at end of file diff --git a/src/mol-plugin-ui/controls/icons.tsx b/src/mol-plugin-ui/controls/icons.tsx index 3f76182e0b85ba556224c18e846eea91e08d9d20..e069c42e3222f1378116c54a20aa7246904248c2 100644 --- a/src/mol-plugin-ui/controls/icons.tsx +++ b/src/mol-plugin-ui/controls/icons.tsx @@ -41,6 +41,9 @@ export function MoleculeSvg() { return _Molecule; } const _CubeOutline = <svg width='24px' height='24px' viewBox='0 0 24 24' strokeWidth='0.1px'><path d="M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z" /></svg>; export function CubeOutlineSvg() { return _CubeOutline; } +const _CubeScan = <svg width='24px' height='24px' viewBox='0 0 24 24' strokeWidth='0.1px'><path d="M17,22V20H20V17H22V20.5C22,20.89 21.84,21.24 21.54,21.54C21.24,21.84 20.89,22 20.5,22H17M7,22H3.5C3.11,22 2.76,21.84 2.46,21.54C2.16,21.24 2,20.89 2,20.5V17H4V20H7V22M17,2H20.5C20.89,2 21.24,2.16 21.54,2.46C21.84,2.76 22,3.11 22,3.5V7H20V4H17V2M7,2V4H4V7H2V3.5C2,3.11 2.16,2.76 2.46,2.46C2.76,2.16 3.11,2 3.5,2H7M13,17.25L17,14.95V10.36L13,12.66V17.25M12,10.92L16,8.63L12,6.28L8,8.63L12,10.92M7,14.95L11,17.25V12.66L7,10.36V14.95M18.23,7.59C18.73,7.91 19,8.34 19,8.91V15.23C19,15.8 18.73,16.23 18.23,16.55L12.75,19.73C12.25,20.05 11.75,20.05 11.25,19.73L5.77,16.55C5.27,16.23 5,15.8 5,15.23V8.91C5,8.34 5.27,7.91 5.77,7.59L11.25,4.41C11.5,4.28 11.75,4.22 12,4.22C12.25,4.22 12.5,4.28 12.75,4.41L18.23,7.59Z" /></svg>; +export function CubeScanSvg() { return _CubeScan; } + const _CubeSend = <svg width='24px' height='24px' viewBox='0 0 24 24' strokeWidth='0.1px'><path d="M16,4L9,8.04V15.96L16,20L23,15.96V8.04M16,6.31L19.8,8.5L16,10.69L12.21,8.5M0,7V9H7V7M11,10.11L15,12.42V17.11L11,14.81M21,10.11V14.81L17,17.11V12.42M2,11V13H7V11M4,15V17H7V15" /></svg>; export function CubeSendSvg() { return _CubeSend; }