diff --git a/src/mol-model-formats/shape/ply.ts b/src/mol-model-formats/shape/ply.ts index 752209cb3f5ad8ec4d61cd9cdd2fc40cf5884844..c8c2d9420a2904031582911be3e0e5c290211368 100644 --- a/src/mol-model-formats/shape/ply.ts +++ b/src/mol-model-formats/shape/ply.ts @@ -17,10 +17,53 @@ import { arrayMax, fillSerial } from 'mol-util/array'; import { Column } from 'mol-data/db'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { ColorNames } from 'mol-util/color/tables'; +import { deepClone } from 'mol-util/object'; // TODO support 'edge' and 'material' elements, see https://www.mathworks.com/help/vision/ug/the-ply-format.html -async function getPlyMesh(ctx: RuntimeContext, vertex: PlyTable, face: PlyList, groupIds: ArrayLike<number>, mesh?: Mesh) { +function createPlyShapeParams(vertex?: PlyTable) { + const options: [string, string][] = [['', '']] + const defaultValues = { group: '', red: '', green: '', blue: '' } + if (vertex) { + for (let i = 0, il = vertex.propertyNames.length; i < il; ++i) { + const name = vertex.propertyNames[i] + options.push([ name, name ]) + } + + // TODO hardcoded as convenience for data provided by MegaMol + if (vertex.propertyNames.includes('atomid')) defaultValues.group = 'atomid' + + if (vertex.propertyNames.includes('red')) defaultValues.red = 'red' + if (vertex.propertyNames.includes('green')) defaultValues.green = 'green' + if (vertex.propertyNames.includes('blue')) defaultValues.blue = 'blue' + } + + return { + ...Mesh.Params, + + coloring: PD.MappedStatic(defaultValues.red && defaultValues.green && defaultValues.blue ? 'vertex' : 'uniform', { + vertex: PD.Group({ + red: PD.Select(defaultValues.red, options, { label: 'Red Property' }), + green: PD.Select(defaultValues.green, options, { label: 'Green Property' }), + blue: PD.Select(defaultValues.blue, options, { label: 'Blue Property' }), + }, { isFlat: true }), + uniform: PD.Group({ + color: PD.Color(ColorNames.grey) + }, { isFlat: true }) + }), + grouping: PD.MappedStatic(defaultValues.group ? 'vertex' : 'none', { + vertex: PD.Group({ + group: PD.Select(defaultValues.group, options, { label: 'Group Property' }), + }, { isFlat: true }), + none: PD.Group({ }) + }), + } +} + +export const PlyShapeParams = createPlyShapeParams() +export type PlyShapeParams = typeof PlyShapeParams + +async function getMesh(ctx: RuntimeContext, vertex: PlyTable, face: PlyList, groupIds: ArrayLike<number>, mesh?: Mesh) { const builderState = MeshBuilder.createState(vertex.rowCount, vertex.rowCount / 4, mesh) const { vertices, normals, indices, groups } = builderState @@ -36,7 +79,7 @@ async function getPlyMesh(ctx: RuntimeContext, vertex: PlyTable, face: PlyList, const hasNormals = !!nx && !!ny && !!nz for (let i = 0, il = vertex.rowCount; i < il; ++i) { - if (i % 10000 === 0 && ctx.shouldUpdate) await ctx.update({ current: i, max: il, message: `adding vertex ${i}` }) + if (i % 100000 === 0 && ctx.shouldUpdate) await ctx.update({ current: i, max: il, message: `adding vertex ${i}` }) ChunkedArray.add3(vertices, x.value(i), y.value(i), z.value(i)) if (hasNormals) ChunkedArray.add3(normals, nx!.value(i), ny!.value(i), nz!.value(i)); @@ -44,10 +87,13 @@ async function getPlyMesh(ctx: RuntimeContext, vertex: PlyTable, face: PlyList, } for (let i = 0, il = face.rowCount; i < il; ++i) { - if (i % 10000 === 0 && ctx.shouldUpdate) await ctx.update({ current: i, max: il, message: `adding face ${i}` }) + if (i % 100000 === 0 && ctx.shouldUpdate) await ctx.update({ current: i, max: il, message: `adding face ${i}` }) - const { entries } = face.value(i) - ChunkedArray.add3(indices, entries[0], entries[1], entries[2]) + const { entries, count } = face.value(i) + if (count === 3) { + ChunkedArray.add3(indices, entries[0], entries[1], entries[2]) + } + // TODO support quadriliterals } const m = MeshBuilder.getMesh(builderState); @@ -57,24 +103,25 @@ async function getPlyMesh(ctx: RuntimeContext, vertex: PlyTable, face: PlyList, return m } -function getGrouping(count: number, column?: Column<number>) { - const ids = column ? column.toArray({ array: Int32Array }) : fillSerial(new Uint32Array(count)) +const int = Column.Schema.int + +type Grouping = { ids: ArrayLike<number>, map: ArrayLike<number> } +function getGrouping(vertex: PlyTable, props: PD.Values<PlyShapeParams>): Grouping { + const { grouping } = props + const { rowCount } = vertex + const column = grouping.name === 'vertex' ? vertex.getProperty(grouping.params.group) : undefined + + const ids = column ? column.toArray({ array: Uint32Array }) : fillSerial(new Uint32Array(rowCount)) const maxId = arrayMax(ids) // assumes uint ids const map = new Uint32Array(maxId + 1) for (let i = 0, il = ids.length; i < il; ++i) map[ids[i]] = i return { ids, map } } -async function getShape(ctx: RuntimeContext, plyFile: PlyFile, props: PD.Values<PlyShapeParams>, shape?: Shape<Mesh>) { - await ctx.update('async creation of shape from ply file') - - const { coloring, grouping } = props - - const vertex = plyFile.getElement('vertex') as PlyTable - if (!vertex) throw new Error('missing vertex element') - +type Coloring = { red: Column<number>, green: Column<number>, blue: Column<number> } +function getColoring(vertex: PlyTable, props: PD.Values<PlyShapeParams>): Coloring { + const { coloring } = props const { rowCount } = vertex - const int = Column.Schema.int let red: Column<number>, green: Column<number>, blue: Column<number> if (coloring.name === 'vertex') { @@ -88,16 +135,14 @@ async function getShape(ctx: RuntimeContext, plyFile: PlyFile, props: PD.Values< blue = Column.ofConst(b, rowCount, int) } - const face = plyFile.getElement('face') as PlyList - if (!face) throw new Error('missing face element') - - const { ids, map } = getGrouping(vertex.rowCount, grouping.name === 'vertex' ? vertex.getProperty(grouping.params.group) : undefined) + return { red, green, blue } +} - const mesh = await getPlyMesh(ctx, vertex, face, ids, shape && shape.geometry) +function createShape(plyFile: PlyFile, mesh: Mesh, coloring: Coloring, grouping: Grouping) { + const { red, green, blue } = coloring + const { ids, map } = grouping return Shape.create( - - 'test', plyFile, mesh, - + 'ply-mesh', plyFile, mesh, (groupId: number) => { const idx = map[groupId] return Color.fromRgb(red.value(idx), green.value(idx), blue.value(idx)) @@ -109,57 +154,64 @@ async function getShape(ctx: RuntimeContext, plyFile: PlyFile, props: PD.Values< ) } -function createPlyShapeParams(vertex?: PlyTable) { - const options: [string, string][] = [['', '']] - const defaultValues = { group: '', red: '', green: '', blue: '' } - if (vertex) { - for (let i = 0, il = vertex.propertyNames.length; i < il; ++i) { - const name = vertex.propertyNames[i] - options.push([ name, name ]) +function makeShapeGetter() { + let _plyFile: PlyFile | undefined + let _props: PD.Values<PlyShapeParams> | undefined + + let _shape: Shape<Mesh> + let _mesh: Mesh + let _coloring: Coloring + let _grouping: Grouping + + const getShape = async (ctx: RuntimeContext, plyFile: PlyFile, props: PD.Values<PlyShapeParams>, shape?: Shape<Mesh>) => { + + const vertex = plyFile.getElement('vertex') as PlyTable + if (!vertex) throw new Error('missing vertex element') + + const face = plyFile.getElement('face') as PlyList + if (!face) throw new Error('missing face element') + + let newMesh = false + let newColor = false + + if (!_plyFile || _plyFile !== plyFile) { + newMesh = true } - // TODO harcoded as convenience for data provided by MegaMol - if (vertex.propertyNames.includes('atomid')) defaultValues.group = 'atomid' + if (!_props || !PD.isParamEqual(PlyShapeParams.grouping, _props.grouping, props.grouping)) { + newMesh = true + } - if (vertex.propertyNames.includes('red')) defaultValues.red = 'red' - if (vertex.propertyNames.includes('green')) defaultValues.green = 'green' - if (vertex.propertyNames.includes('blue')) defaultValues.blue = 'blue' - } + if (!_props || !PD.isParamEqual(PlyShapeParams.coloring, _props.coloring, props.coloring)) { + newColor = true + } - return { - ...Mesh.Params, + if (newMesh) { + _coloring = getColoring(vertex, props) + _grouping = getGrouping(vertex, props) + _mesh = await getMesh(ctx, vertex, face, _grouping.ids, shape && shape.geometry) + _shape = createShape(plyFile, _mesh, _coloring, _grouping) + } else if (newColor) { + _coloring = getColoring(vertex, props) + _shape = createShape(plyFile, _mesh, _coloring, _grouping) + } - coloring: PD.MappedStatic(defaultValues.red && defaultValues.green && defaultValues.blue ? 'vertex' : 'uniform', { - vertex: PD.Group({ - red: PD.Select(defaultValues.red, options, { label: 'Red Property' }), - green: PD.Select(defaultValues.green, options, { label: 'Green Property' }), - blue: PD.Select(defaultValues.blue, options, { label: 'Blue Property' }), - }, { isFlat: true }), - uniform: PD.Group({ - color: PD.Color(ColorNames.grey) - }, { isFlat: true }) - }), - grouping: PD.MappedStatic(defaultValues.group ? 'vertex' : 'none', { - vertex: PD.Group({ - group: PD.Select(defaultValues.group, options, { label: 'Group Property' }), - }, { isFlat: true }), - none: PD.Group({ }) - }), + _plyFile = plyFile + _props = deepClone(props) + + return _shape } + return getShape } -export const PlyShapeParams = createPlyShapeParams() -export type PlyShapeParams = typeof PlyShapeParams - export function shapeFromPly(source: PlyFile, params?: {}) { return Task.create<ShapeProvider<PlyFile, Mesh, PlyShapeParams>>('Shape Provider', async ctx => { return { label: 'Mesh', data: source, params: createPlyShapeParams(source.getElement('vertex') as PlyTable), - getShape, + getShape: makeShapeGetter(), geometryUtils: Mesh.Utils } }) - } \ No newline at end of file diff --git a/src/mol-repr/shape/representation.ts b/src/mol-repr/shape/representation.ts index ebd0f7afcd791e1cb62c1a482beac1c677212c31..038609aa9fff1c638006af19e2adbb02c0b71b6a 100644 --- a/src/mol-repr/shape/representation.ts +++ b/src/mol-repr/shape/representation.ts @@ -56,9 +56,7 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa updateState.createNew = true } else if (shape && _shape && shape.id === _shape.id) { // console.log('same shape') - // trigger color update when shape has not changed - updateState.updateColor = true - updateState.updateTransform = true + // nothing to set } else if (shape && _shape && shape.id !== _shape.id) { // console.log('new shape') updateState.updateTransform = true diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts index a7f08f3cf1a3d8227703292d4c4078aeb862e499..bfb9311dcdfa65bf5febb10796124bd71d631643 100644 --- a/src/mol-util/param-definition.ts +++ b/src/mol-util/param-definition.ts @@ -284,7 +284,7 @@ export namespace ParamDefinition { return true; } - function isParamEqual(p: Any, a: any, b: any): boolean { + export function isParamEqual(p: Any, a: any, b: any): boolean { if (a === b) return true; if (!a) return !b; if (!b) return !a; diff --git a/src/tests/browser/render-shape.ts b/src/tests/browser/render-shape.ts index 1f5f39ec7da99640c4fd670a8fc6a05c2163a044..6fd4ace698f131bc5faecdb82ace7a1cd300a1df 100644 --- a/src/tests/browser/render-shape.ts +++ b/src/tests/browser/render-shape.ts @@ -96,7 +96,7 @@ async function getShape(ctx: RuntimeContext, data: MyData, props: {}, shape?: Sh const { centers, colors, labels, transforms } = data const mesh = await getSphereMesh(ctx, centers, shape && shape.geometry) const groupCount = centers.length / 3 - return shape || Shape.create( + return Shape.create( 'test', data, mesh, (groupId: number) => colors[groupId], // color: per group, same for instances () => 1, // size: constant