Modifying the kinemage parser to be able to read more than one kinemage entry from the same file. Hooking in a drag and drop handler on .kin files that for now just stores things into a global variable. Adjusting the parsing of kinemage through the original plugin path to handle the new parsing.

This commit is contained in:
Russ Taylor
2026-02-03 14:14:36 -05:00
parent 1322882444
commit 2909a209c3
8 changed files with 158 additions and 18 deletions

View File

@@ -16,6 +16,7 @@ import { MoleculeType } from '../../mol-model/structure/model/types';
import { AccessibleSurfaceArea } from '../../mol-model-props/computed/accessible-surface-area/shrake-rupley';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { KinemageData } from './prop';
import { Kinemage } from '../../mol-io/reader/kin/schema';
const LARGE_CA_THRESHOLD = 5000;
const DEFAULT_UPDATE_INTERVAL = 10;
@@ -194,12 +195,15 @@ export async function calculate(runtime: RuntimeContext, structure: Structure, p
v3scale(center, center, 0.5);
const extent = adjustExtent(ctx, membrane, center);
const kinemages: Kinemage[] = [];
const activeKinemage = -1
return {
planePoint1: membrane.planePoint1,
planePoint2: membrane.planePoint2,
normalVector,
centroid: center,
radius: extent
radius: extent,
kinemages, activeKinemage
};
}

View File

@@ -12,6 +12,7 @@ import { KinemageDataProvider, KinemageData } from './prop';
import { StateObjectRef, StateTransformer, StateTransform } from '../../mol-state';
import { Task } from '../../mol-task';
import { PluginBehavior } from '../../mol-plugin/behavior';
import { PluginDragAndDropHandler } from '../../mol-plugin-state/manager/drag-and-drop';
import { KinemageDataRepresentationProvider, KinemageDataParams, KinemageDataRepresentation } from './representation';
import { HydrophobicityColorThemeProvider } from '../../mol-theme/color/hydrophobicity';
import { PluginStateObject, PluginStateTransform } from '../../mol-plugin-state/objects';
@@ -20,9 +21,18 @@ import { DefaultQueryRuntimeTable } from '../../mol-script/runtime/query/compile
import { StructureSelectionQuery, StructureSelectionCategory } from '../../mol-plugin-state/helpers/structure-selection-query';
import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
import { GenericRepresentationRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
import { Vec3 } from '../../mol-math/linear-algebra';
const Tag = KinemageData.Tag;
/// @todo Remove once we store into the proper location in the drag and drop handler
/** Global KinemageInfo that is used to display */
let g_kinemageInfo: KinemageData = {
kinemages: [], activeKinemage: -1
/// @todo Remove these when we no longer need them.
, planePoint1: Vec3(), planePoint2: Vec3(), normalVector: Vec3(), radius: 0, centroid: Vec3()
};
export const KinemageExtension = PluginBehavior.create<{ autoAttach: boolean }>({
name: 'kinemage-data-prop',
category: 'custom-props',
@@ -50,6 +60,9 @@ export const KinemageExtension = PluginBehavior.create<{ autoAttach: boolean }>(
return [refs, 'Membrane Orientation'];
});
this.ctx.builders.structure.representation.registerPreset(KinemageDataPreset);
console.log('XXX KinemageExtension register - adding drag and drop handler for ', KINDragAndDropHandler.name);
this.ctx.managers.dragAndDrop.addHandler(KINDragAndDropHandler.name, KINDragAndDropHandler.handle);
}
update(p: { autoAttach: boolean }) {
@@ -69,6 +82,8 @@ export const KinemageExtension = PluginBehavior.create<{ autoAttach: boolean }>(
this.ctx.genericRepresentationControls.delete(Tag.Representation);
this.ctx.builders.structure.representation.unregisterPreset(KinemageDataPreset);
this.ctx.managers.dragAndDrop.removeHandler(KINDragAndDropHandler.name);
}
},
params: () => ({
@@ -171,6 +186,43 @@ export const KinemageDataPreset = StructureRepresentationPresetProvider({
export function tryCreateKinemageData(plugin: PluginContext, structure: StateObjectRef<PluginStateObject.Molecule.Structure>, params?: StateTransformer.Params<KinemageData3D>, initialState?: Partial<StateTransform.State>) {
const state = plugin.state.data;
const KinemageData = state.build().to(structure)
.applyOrUpdateTagged('membrane-orientation-3d', KinemageData3D, params, { state: initialState });
.applyOrUpdateTagged('kinemage-3d', KinemageData3D, params, { state: initialState });
return KinemageData.commit({ revertOnError: true });
}
/** Registerable method for handling dragged-and-dropped files */
interface DragAndDropHandler {
name: string,
handle: PluginDragAndDropHandler,
}
/** DragAndDropHandler handler for `.kin` files */
const KINDragAndDropHandler: DragAndDropHandler = {
name: 'kin',
/** Load .kin files. Append to previous plugin state.
* If multiple files are provided, append them all.
* Select the last-loaded one from the list.
* Return `true` if at least one file has been loaded. */
async handle(files: File[], plugin: PluginContext): Promise<boolean> {
let applied = false;
for (const file of files) {
if (file.name.toLowerCase().endsWith('.kin')) {
const task = Task.create('Load KIN file', async ctx => {
console.log('XXX loading KIN file ', file.name); /// @todo Remove when done debugging
const kinInfo = await KinemageData.open(file);
for (const kinData of kinInfo.kinemages) {
g_kinemageInfo.kinemages.push(kinData);
g_kinemageInfo.activeKinemage = g_kinemageInfo.kinemages.length - 1;
}
console.log('XXX accumulated Kinemages size ', g_kinemageInfo.kinemages.length, ', active is ', g_kinemageInfo.activeKinemage);
/// @todo See what loadMVS() ... LoadMolstarTree() ... MolstarLoadingActions().primitives() ... applyPrimitiveVisuals() does
});
console.log('XXX plugin.runTask');
await plugin.runTask(task);
applied = true;
}
}
console.log('XXX KINDragAndDropHandler applied=', applied);
return applied;
},
};

View File

@@ -16,6 +16,11 @@ import { Vec3 } from '../../mol-math/linear-algebra';
import { QuerySymbolRuntime } from '../../mol-script/runtime/query/base';
import { CustomPropSymbol } from '../../mol-script/language/symbol';
import { Type } from '../../mol-script/language/type';
import { Task } from '../../mol-task';
import { Kinemage } from '../../mol-io/reader/kin/schema';
import { parseKin } from '../../mol-io/reader/kin/parser';
export const KinemageDataParams = {
...KinemageParams
@@ -26,6 +31,7 @@ export type KinemageDataProps = PD.Values<KinemageDataParams>
export { KinemageData };
interface KinemageData {
/// @todo Remove these
// point in membrane boundary
readonly planePoint1: Vec3,
// point in opposite side of membrane boundary
@@ -34,17 +40,32 @@ interface KinemageData {
readonly normalVector: Vec3,
// the radius of the membrane layer
readonly radius: number,
readonly centroid: Vec3
readonly centroid: Vec3,
/**
* List of Kinemages read from one or more files.
*/
readonly kinemages: Kinemage[],
/**
* Index of the active KinemageData
*/
activeKinemage: number
}
const FileSourceParams = {
input: PD.File({ accept: '.kin', multiple: false })
};
type FileSourceProps = PD.Values<typeof FileSourceParams>
namespace KinemageData {
export enum Tag {
Representation = 'membrane-orientation-3d'
Representation = 'kinemage-3d'
}
const pos = Vec3();
export const symbols = {
isTransmembrane: QuerySymbolRuntime.Dynamic(CustomPropSymbol('computed', 'membrane-orientation.is-transmembrane', Type.Bool),
isTransmembrane: QuerySymbolRuntime.Dynamic(CustomPropSymbol('computed', 'kinemage.is-kinemage', Type.Bool),
ctx => {
const { unit, structure } = ctx.element;
const { x, y, z } = StructureProperties.atom;
@@ -56,6 +77,44 @@ namespace KinemageData {
return isInMembranePlane(pos, normalVector, planePoint1, planePoint2);
})
};
async function loadKinemageData(data: string): Promise<Kinemage[]> {
const task = parseKin(data);
const result = await task.run();
if (result.isError) {
throw new Error('Failed to parse KIN data');
}
return result.result;
}
export async function open(file: FileSourceProps | File): Promise<KinemageData> {
let fileToRead: File;
if (file instanceof File) {
fileToRead = file;
} else if (file && file.input && file.input.file) {
fileToRead = file.input.file;
} else {
throw new Error('No file given');
}
const task = Task.create('Load KIN file', async ctx => {
const data = await fileToRead.text();
const kinemages = await loadKinemageData(data);
return kinemages;
});
const kinemages = await task.run();
const activeKinemage = kinemages.length - 1;
/// @todo Remove these once we no longer need them
const planePoint1 = Vec3.create(0, 0, 0);
const planePoint2 = Vec3.create(0, 0, 0);
const normalVector = Vec3.create(0, 0, 1);
const radius = 0;
const centroid = Vec3.create(0, 0, 0);
return { kinemages, activeKinemage, planePoint1, planePoint2, normalVector, radius, centroid };
}
}
export const KinemageDataProvider: CustomStructureProperty.Provider<KinemageDataParams, KinemageData> = CustomStructureProperty.createProvider({

View File

@@ -74,9 +74,9 @@ export function KinemageDataRepresentation(ctx: RepresentationContext, getParams
}
export const KinemageDataRepresentationProvider = StructureRepresentationProvider({
name: 'membrane-orientation',
label: 'Membrane Orientation',
description: 'Displays a grid of points representing membrane layers.',
name: 'kinemage',
label: 'Kinemage',
description: 'Displays data from an Kinemage.',
factory: KinemageDataRepresentation,
getParams: getKinemageDataParams,
defaultValues: PD.getDefaultValues(KinemageDataParams),

View File

@@ -45,7 +45,8 @@ describe('kin reader', () => {
it('basic', async () => {
const parsed = await parseKin(kinString).run();
if (parsed.isError) return;
const kinemage = parsed.result;
if (parsed.result.length != 1) return;
const kinemage = parsed.result[0];
const vectors = kinemage.vectorLists;
expect(vectors.length).toEqual(1);

View File

@@ -9,15 +9,33 @@ import { Task, RuntimeContext } from '../../../mol-task';
import { Kinemage } from './schema';
import KinParser from './ngl-based-parser';
async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<Kinemage>> {
async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<Kinemage[]>> {
const kinemages: Kinemage[] = [];
// Split the data into sections based on the '@kinemage' keyword, which indicates one or more kinemages in the file.
// Handle the case where there is no '@kinemage' keyword by parsing the entire file.
const kinemageSections = data.split(/@kinemage\s+\d+/); // Split based on '@kinemage' keyword followed by a number
const NGLParser = new KinParser(data);
const kinData = NGLParser.kinemage;
return Result.success(kinData);
// If there are one or more @kinemage sections, ignore the portion before the first one.
// This will either be an empty string (if the first section starts at the beginning of the file)
// or header data that is not part of a particular kinemage. This has the effect of removing
// the header data even in the case where there is a single @kinemage keyword.
if (kinemageSections.length > 1) {
kinemageSections.shift();
}
for (const section of kinemageSections) {
if (section.trim()) { // Ignore empty sections
const NGLParser = new KinParser(section.trim());
const kinData = NGLParser.kinemage;
kinemages.push(kinData);
}
}
return Result.success(kinemages);
}
export function parseKin(data: string) {
return Task.create<Result<Kinemage>>('Parse KIN', async ctx => {
return Task.create<Result<Kinemage[]>>('Parse KIN', async ctx => {
return await parseInternal(data, ctx);
});
}

View File

@@ -43,6 +43,7 @@ export const KinProvider = DataFormatProvider({
category: ShapeFormatCategory,
stringExtensions: ['kin'],
parse: async (plugin, data) => {
// This returns the last kinemage in the file if there are multiple
const format = plugin.state.data.build()
.to(data)
.apply(StateTransforms.Data.ParseKin, {}, { state: { isGhost: true } });
@@ -63,7 +64,7 @@ export const KinProvider = DataFormatProvider({
export const BuiltInShapeFormats = [
['ply', PlyProvider] as const,
['kin', KinProvider] as const,
/// @todo Replaced the plugin loader with an extension ['kin', KinProvider] as const,
] as const;
export type BuiltInShapeFormat = (typeof BuiltInShapeFormats)[number][0]

View File

@@ -11,6 +11,7 @@ import { CIF } from '../../mol-io/reader/cif';
import * as DSN6 from '../../mol-io/reader/dsn6/parser';
import * as PLY from '../../mol-io/reader/ply/parser';
import * as KIN from '../../mol-io/reader/kin/parser';
import { Kinemage } from '../../mol-io/reader/kin/schema';
import { parsePsf } from '../../mol-io/reader/psf/parser';
import { PluginContext } from '../../mol-plugin/context';
import { StateObject, StateTransformer } from '../../mol-state';
@@ -414,9 +415,13 @@ const ParseKin = PluginStateTransform.BuiltIn({
})({
apply({ a }) {
return Task.create('Parse KIN', async ctx => {
const parsed = await KIN.parseKin(a.data).runInContext(ctx);
if (parsed.isError) throw new Error(parsed.message);
return new SO.Format.Kin(parsed.result, { label: parsed.result.comments[0] || 'KIN Data' });
const parsedList = await KIN.parseKin(a.data).runInContext(ctx);
if (parsedList.isError) throw new Error(parsedList.message);
// Read the last entry, handling the case where it is an error
const kinemages: Kinemage[] = parsedList.result;
const parsed = kinemages.length ? parsedList.result[kinemages.length - 1] : undefined;
if (!parsed) return StateObject.Null;
return new SO.Format.Kin(parsed, { label: parsed.comments[0] || 'KIN Data' });
});
}
});