lint/format

This commit is contained in:
Alexander Rose
2026-05-09 22:08:08 -07:00
parent 3e7614d75c
commit a3b54ff88c
9 changed files with 2002 additions and 2007 deletions

View File

@@ -55,7 +55,7 @@ describe('kin reader', () => {
expect(element.name).toEqual('x');
expect(element.position1Array.length).toEqual(7);
// @todo Add more tests
// TODO: Add more tests
expect.assertions(3);
});
@@ -64,7 +64,7 @@ describe('kin reader', () => {
const parsed = await parseKin(kinComplexString).run();
if (parsed.isError) return;
// @todo Add more complex tests
// TODO: Add more complex tests
});
});

View File

@@ -41,242 +41,242 @@ export class KinemageObject extends PluginStateObject.Create<KinemageData>({ nam
* Use PluginCommands.Camera.SetSnapshot so transitions and canvas props are handled properly.
*/
export async function applyViewSnapshot(plugin: PluginContext, snapshot: Partial<Camera.Snapshot>) {
if (!snapshot) return;
// Set background color to black
plugin.canvas3d?.setProps({
renderer: {
...plugin.canvas3d.props.renderer,
backgroundColor: Color(0x000000)
if (!snapshot) return;
// Set background color to black
plugin.canvas3d?.setProps({
renderer: {
...plugin.canvas3d.props.renderer,
backgroundColor: Color(0x000000)
}
});
// If the snapshot provides a target, adjust the canvas `sceneRadiusFactor` so the scene isn't clipped
// when we switch camera.
if (snapshot.target) {
try {
const boundingSphere = getPluginBoundingSphere(plugin);
if (boundingSphere && boundingSphere.radius > 0) {
const offset = Vec3.distance(snapshot.target as Vec3, boundingSphere.center);
const sceneRadiusFactor = (boundingSphere.radius + offset) / boundingSphere.radius;
plugin.canvas3d?.setProps({ sceneRadiusFactor });
}
} catch (e) {
// fallback: ignore errors and continue to set the camera snapshot
console.warn('Failed to adjust sceneRadiusFactor for view snapshot', e);
}
}
});
// If the snapshot provides a target, adjust the canvas `sceneRadiusFactor` so the scene isn't clipped
// when we switch camera.
if (snapshot.target) {
try {
const boundingSphere = getPluginBoundingSphere(plugin);
if (boundingSphere && boundingSphere.radius > 0) {
const offset = Vec3.distance(snapshot.target as Vec3, boundingSphere.center);
const sceneRadiusFactor = (boundingSphere.radius + offset) / boundingSphere.radius;
plugin.canvas3d?.setProps({ sceneRadiusFactor });
}
} catch (e) {
// fallback: ignore errors and continue to set the camera snapshot
console.warn('Failed to adjust sceneRadiusFactor for view snapshot', e);
}
}
await PluginCommands.Camera.SetSnapshot(plugin, { snapshot });
await PluginCommands.Camera.SetSnapshot(plugin, { snapshot });
}
/**
* Transform to parse Kinemage data from string/data input
*/
export const ParseKinemage = Transform({
name: 'sb-kinemage-parse',
display: { name: 'Parse Kinemage' },
from: [PluginStateObject.Data.String],
to: KinemageObject,
params: {
label: PD.Optional(PD.Text('', { description: 'Label for the Kinemage data' }))
}
name: 'sb-kinemage-parse',
display: { name: 'Parse Kinemage' },
from: [PluginStateObject.Data.String],
to: KinemageObject,
params: {
label: PD.Optional(PD.Text('', { description: 'Label for the Kinemage data' }))
}
})({
apply({ a, params }) {
return Task.create('Parse Kinemage', async ctx => {
const input = a.data;
let data: KinemageData;
apply({ a, params }) {
return Task.create('Parse Kinemage', async ctx => {
const input = a.data;
let data: KinemageData;
if (typeof input === 'string') {
// Parse from string content
const file = new File([input], 'input.kin', { type: 'text/plain' });
data = await KinemageData.open(file);
} else {
throw new Error('Unsupported input type for ParseKinemage');
}
if (typeof input === 'string') {
// Parse from string content
const file = new File([input], 'input.kin', { type: 'text/plain' });
data = await KinemageData.open(file);
} else {
throw new Error('Unsupported input type for ParseKinemage');
}
// Precompute camera snapshots for all views in all kinemages
for (const kinData of data.kinemages) {
(kinData as any).viewSnapshots = (kinData as any).viewSnapshots || Object.create(null);
for (const [viewKey, viewObj] of Object.entries(kinData.viewDict)) {
const center = Vec3.create(0, 0, 0);
if (viewObj.center) {
Vec3.set(center, viewObj.center[0], viewObj.center[1], viewObj.center[2]);
}
// Precompute camera snapshots for all views in all kinemages
for (const kinData of data.kinemages) {
(kinData as any).viewSnapshots = (kinData as any).viewSnapshots || Object.create(null);
for (const [viewKey, viewObj] of Object.entries(kinData.viewDict)) {
const center = Vec3.create(0, 0, 0);
if (viewObj.center) {
Vec3.set(center, viewObj.center[0], viewObj.center[1], viewObj.center[2]);
}
const orientation: Mat3 = Mat3.identity();
if (viewObj.matrix) {
Mat3.fromArray(orientation, viewObj.matrix, 0);
Mat3.transpose(orientation, orientation);
}
const orientation: Mat3 = Mat3.identity();
if (viewObj.matrix) {
Mat3.fromArray(orientation, viewObj.matrix, 0);
Mat3.transpose(orientation, orientation);
}
const zAxis = Vec3.create(0, 0, 1);
Vec3.transformMat3(zAxis, zAxis, orientation);
const zAxis = Vec3.create(0, 0, 1);
Vec3.transformMat3(zAxis, zAxis, orientation);
const yAxis = Vec3.create(0, 1, 0);
Vec3.transformMat3(yAxis, yAxis, orientation);
const yAxis = Vec3.create(0, 1, 0);
Vec3.transformMat3(yAxis, yAxis, orientation);
let distance = 100;
if (viewObj.span) {
distance = viewObj.span;
}
Vec3.scale(zAxis, zAxis, distance);
const position = Vec3.create(0, 0, 100);
Vec3.add(position, center, zAxis);
let distance = 100;
if (viewObj.span) {
distance = viewObj.span;
}
Vec3.scale(zAxis, zAxis, distance);
const position = Vec3.create(0, 0, 100);
Vec3.add(position, center, zAxis);
let radius = 100;
if (viewObj.zslab) {
const scale = viewObj.zslab / 200;
radius = 0.5 * distance * scale;
}
let radius = 100;
if (viewObj.zslab) {
const scale = viewObj.zslab / 200;
radius = 0.5 * distance * scale;
}
const snap: Camera.Snapshot = {
mode: 'orthographic',
fov: Math.PI / 4,
position,
up: yAxis,
target: center,
radius,
radiusMax: 1e4,
fog: 0,
clipFar: true,
minNear: 1,
minFar: 1
};
const snap: Camera.Snapshot = {
mode: 'orthographic',
fov: Math.PI / 4,
position,
up: yAxis,
target: center,
radius,
radiusMax: 1e4,
fog: 0,
clipFar: true,
minNear: 1,
minFar: 1
};
(kinData as any).viewSnapshots[viewKey] = snap;
}
}
(kinData as any).viewSnapshots[viewKey] = snap;
}
}
const label = params.label || data.kinemages[0]?.caption || 'Kinemage';
return new KinemageObject(data, { label, description: `Kinemage with ${data.kinemages.length} view(s)` });
});
}
const label = params.label || data.kinemages[0]?.caption || 'Kinemage';
return new KinemageObject(data, { label, description: `Kinemage with ${data.kinemages.length} view(s)` });
});
}
});
/**
* Transform to select a specific kinemage from parsed data
*/
export const SelectKinemage = Transform({
name: 'sb-kinemage-select',
display: { name: 'Select Kinemage' },
from: KinemageObject,
to: PluginStateObject.Format.Json,
params: (a) => {
const kinemages = a?.data?.kinemages || [];
const options = kinemages.map((k: Kinemage, i: number) => [i, k.pdbfile || k.caption || `Kinemage ${i}`] as const);
return {
index: PD.Select(0, options, { description: 'Which kinemage to use' })
};
}
name: 'sb-kinemage-select',
display: { name: 'Select Kinemage' },
from: KinemageObject,
to: PluginStateObject.Format.Json,
params: (a) => {
const kinemages = a?.data?.kinemages || [];
const options = kinemages.map((k: Kinemage, i: number) => [i, k.pdbfile || k.caption || `Kinemage ${i}`] as const);
return {
index: PD.Select(0, options, { description: 'Which kinemage to use' })
};
}
})({
apply({ a, params }) {
return Task.create('Select Kinemage', async ctx => {
const kinData = a.data.kinemages[params.index];
if (!kinData) {
throw new Error(`No kinemage found at index ${params.index}`);
}
apply({ a, params }) {
return Task.create('Select Kinemage', async ctx => {
const kinData = a.data.kinemages[params.index];
if (!kinData) {
throw new Error(`No kinemage found at index ${params.index}`);
}
const label = kinData.pdbfile || kinData.caption || `Kinemage ${params.index}`;
const label = kinData.pdbfile || kinData.caption || `Kinemage ${params.index}`;
// Store the kinemage data in a Format.Json node so downstream transforms can access it
return new PluginStateObject.Format.Json(
{ kinData },
{ label, description: kinData.text || '' }
);
});
}
// Store the kinemage data in a Format.Json node so downstream transforms can access it
return new PluginStateObject.Format.Json(
{ kinData },
{ label, description: kinData.text || '' }
);
});
}
});
export const KinemageShapePointsProvider = Transform({
name: 'sb-kinemage-shape-points-provider',
display: { name: 'Kinemage Shape Points Provider' },
from: PluginStateObject.Format.Json,
to: PluginStateObject.Shape.Provider,
params: {}
name: 'sb-kinemage-shape-points-provider',
display: { name: 'Kinemage Shape Points Provider' },
from: PluginStateObject.Format.Json,
to: PluginStateObject.Shape.Provider,
params: {}
})({
apply({ a }) {
return Task.create('Kinemage Points Shape Provider', async ctx => {
const kinData = (a.data as any).kinData as Kinemage;
if (!kinData) {
throw new Error('No kinData found in parent Format.Json node');
}
apply({ a }) {
return Task.create('Kinemage Points Shape Provider', async ctx => {
const kinData = (a.data as any).kinData as Kinemage;
if (!kinData) {
throw new Error('No kinData found in parent Format.Json node');
}
const provider = await shapePointsFromKin(kinData, { transforms: undefined }, 'Dots').runInContext(ctx);
return new PluginStateObject.Shape.Provider(provider as any, {
label: kinData.pdbfile || kinData.caption || 'Kinemage Points',
description: kinData.text || ''
});
});
}
const provider = await shapePointsFromKin(kinData, { transforms: undefined }, 'Dots').runInContext(ctx);
return new PluginStateObject.Shape.Provider(provider as any, {
label: kinData.pdbfile || kinData.caption || 'Kinemage Points',
description: kinData.text || ''
});
});
}
});
export const KinemageShapeLinesProvider = Transform({
name: 'sb-kinemage-shape-lines-provider',
display: { name: 'Kinemage Shape Lines Provider' },
from: PluginStateObject.Format.Json,
to: PluginStateObject.Shape.Provider,
params: {}
name: 'sb-kinemage-shape-lines-provider',
display: { name: 'Kinemage Shape Lines Provider' },
from: PluginStateObject.Format.Json,
to: PluginStateObject.Shape.Provider,
params: {}
})({
apply({ a }) {
return Task.create('Kinemage Lines Shape Provider', async ctx => {
const kinData = (a.data as any).kinData as Kinemage;
if (!kinData) {
throw new Error('No kinData found in parent Format.Json node');
}
apply({ a }) {
return Task.create('Kinemage Lines Shape Provider', async ctx => {
const kinData = (a.data as any).kinData as Kinemage;
if (!kinData) {
throw new Error('No kinData found in parent Format.Json node');
}
const provider = await shapeLinesFromKin(kinData).runInContext(ctx);
return new PluginStateObject.Shape.Provider(provider as any, {
label: kinData.pdbfile || kinData.caption || 'Kinemage Lines',
description: kinData.text || ''
});
});
}
const provider = await shapeLinesFromKin(kinData).runInContext(ctx);
return new PluginStateObject.Shape.Provider(provider as any, {
label: kinData.pdbfile || kinData.caption || 'Kinemage Lines',
description: kinData.text || ''
});
});
}
});
export const KinemageShapeMeshProvider = Transform({
name: 'sb-kinemage-shape-mesh-provider',
display: { name: 'Kinemage Shape Mesh Provider' },
from: PluginStateObject.Format.Json,
to: PluginStateObject.Shape.Provider,
params: {}
name: 'sb-kinemage-shape-mesh-provider',
display: { name: 'Kinemage Shape Mesh Provider' },
from: PluginStateObject.Format.Json,
to: PluginStateObject.Shape.Provider,
params: {}
})({
apply({ a }) {
return Task.create('Kinemage Mesh Shape Provider', async ctx => {
const kinData = (a.data as any).kinData as Kinemage;
if (!kinData) {
throw new Error('No kinData found in parent Format.Json node');
}
apply({ a }) {
return Task.create('Kinemage Mesh Shape Provider', async ctx => {
const kinData = (a.data as any).kinData as Kinemage;
if (!kinData) {
throw new Error('No kinData found in parent Format.Json node');
}
const provider = await shapeMeshFromKin(kinData).runInContext(ctx);
return new PluginStateObject.Shape.Provider(provider as any, {
label: kinData.pdbfile || kinData.caption || 'Kinemage Meshes',
description: kinData.text || ''
});
});
}
const provider = await shapeMeshFromKin(kinData).runInContext(ctx);
return new PluginStateObject.Shape.Provider(provider as any, {
label: kinData.pdbfile || kinData.caption || 'Kinemage Meshes',
description: kinData.text || ''
});
});
}
});
export const KinemageShapeSpheresProvider = Transform({
name: 'sb-kinemage-shape-spheres-provider',
display: { name: 'Kinemage Shape Spheres Provider' },
from: PluginStateObject.Format.Json,
to: PluginStateObject.Shape.Provider,
params: {}
name: 'sb-kinemage-shape-spheres-provider',
display: { name: 'Kinemage Shape Spheres Provider' },
from: PluginStateObject.Format.Json,
to: PluginStateObject.Shape.Provider,
params: {}
})({
apply({ a }) {
return Task.create('Kinemage Spheres Shape Provider', async ctx => {
const kinData = (a.data as any).kinData as Kinemage;
if (!kinData) {
throw new Error('No kinData found in parent Format.Json node');
}
apply({ a }) {
return Task.create('Kinemage Spheres Shape Provider', async ctx => {
const kinData = (a.data as any).kinData as Kinemage;
if (!kinData) {
throw new Error('No kinData found in parent Format.Json node');
}
const provider = await shapeSpheresFromKin(kinData).runInContext(ctx);
return new PluginStateObject.Shape.Provider(provider as any, {
label: kinData.pdbfile || kinData.caption || 'Kinemage Spheres',
description: kinData.text || ''
});
});
}
const provider = await shapeSpheresFromKin(kinData).runInContext(ctx);
return new PluginStateObject.Shape.Provider(provider as any, {
label: kinData.pdbfile || kinData.caption || 'Kinemage Spheres',
description: kinData.text || ''
});
});
}
});
export const KinemageExtension = PluginBehavior.create<{ autoAttach: boolean }>({
@@ -299,7 +299,7 @@ export const KinemageExtension = PluginBehavior.create<{ autoAttach: boolean }>(
// Some app hosts expose a global customControls registry; register there too so the card is visible
// even when no structure is loaded. Use `any` guards to avoid type errors if customControls isn't present.
if ((this.ctx as any).customControls && typeof (this.ctx as any).customControls.set === 'function') {
(this.ctx as any).customControls.set('kinemage', KinemageControls as any);
(this.ctx as any).customControls.set('kinemage', KinemageControls as any);
}
this.ctx.managers.dragAndDrop.addHandler(KinemageDragAndDropHandler.name, KinemageDragAndDropHandler.handle);
@@ -328,9 +328,9 @@ export const KinemageExtension = PluginBehavior.create<{ autoAttach: boolean }>(
this.ctx.dataFormats.remove('KIN');
// Remove right-panel controls
try { this.ctx.customStructureControls.delete(Tag.Representation); } catch {}
try { this.ctx.customStructureControls.delete(Tag.Representation); } catch { }
if ((this.ctx as any).customControls && typeof (this.ctx as any).customControls.delete === 'function') {
try { (this.ctx as any).customControls.delete('kinemage'); } catch {}
try { (this.ctx as any).customControls.delete('kinemage'); } catch { }
}
}
},
@@ -341,208 +341,208 @@ export const KinemageExtension = PluginBehavior.create<{ autoAttach: boolean }>(
/** Registerable method for handling dragged-and-dropped files */
interface DragAndDropHandler {
name: string,
handle: PluginDragAndDropHandler,
name: string,
handle: PluginDragAndDropHandler,
}
/** Helper function to create all shapes for a kinemage via proper transform chain */
async function createShapesForKinemage(plugin: PluginContext, update: StateBuilder.Root, kinDataSelector: StateObjectSelector<PluginStateObject.Format.Json>) {
const kinDataCell = plugin.state.data.cells.get(kinDataSelector.ref);
if (!kinDataCell?.obj?.data) return;
const kinDataCell = plugin.state.data.cells.get(kinDataSelector.ref);
if (!kinDataCell?.obj?.data) return;
const kinData = (kinDataCell.obj.data as any).kinData as Kinemage;
if (!kinData) return;
const kinData = (kinDataCell.obj.data as any).kinData as Kinemage;
if (!kinData) return;
// Generate all shape types that have data, each as child of the selected kinemage
if (kinData.dotLists.length > 0) {
await update
.to(kinDataSelector)
.apply(KinemageShapePointsProvider, {}, { state: { isGhost: true } })
.apply(StateTransforms.Representation.ShapeRepresentation3D);
}
if (kinData.vectorLists.length > 0) {
await update
.to(kinDataSelector)
.apply(KinemageShapeLinesProvider, {}, { state: { isGhost: true } })
.apply(StateTransforms.Representation.ShapeRepresentation3D);
}
if (kinData.ribbonLists.length > 0) {
await update
.to(kinDataSelector)
.apply(KinemageShapeMeshProvider, {}, { state: { isGhost: true } })
.apply(StateTransforms.Representation.ShapeRepresentation3D, { doubleSided: true });
}
if (kinData.ballLists.length > 0) {
await update
.to(kinDataSelector)
.apply(KinemageShapeSpheresProvider, {}, { state: { isGhost: true } })
.apply(StateTransforms.Representation.ShapeRepresentation3D);
}
// Generate all shape types that have data, each as child of the selected kinemage
if (kinData.dotLists.length > 0) {
await update
.to(kinDataSelector)
.apply(KinemageShapePointsProvider, {}, { state: { isGhost: true } })
.apply(StateTransforms.Representation.ShapeRepresentation3D);
}
if (kinData.vectorLists.length > 0) {
await update
.to(kinDataSelector)
.apply(KinemageShapeLinesProvider, {}, { state: { isGhost: true } })
.apply(StateTransforms.Representation.ShapeRepresentation3D);
}
if (kinData.ribbonLists.length > 0) {
await update
.to(kinDataSelector)
.apply(KinemageShapeMeshProvider, {}, { state: { isGhost: true } })
.apply(StateTransforms.Representation.ShapeRepresentation3D, { doubleSided: true });
}
if (kinData.ballLists.length > 0) {
await update
.to(kinDataSelector)
.apply(KinemageShapeSpheresProvider, {}, { state: { isGhost: true } })
.apply(StateTransforms.Representation.ShapeRepresentation3D);
}
}
/** Helper function to rebuild shapes for a kinemage (remove and recreate) */
export async function rebuildShapesForKinemage(plugin: PluginContext, kinDataSelector: StateObjectSelector<PluginStateObject.Format.Json>) {
// Store current camera snapshot
const curSnap = (plugin.canvas3d && (plugin.canvas3d as any).camera && (plugin.canvas3d as any).camera.getSnapshot)
? (plugin.canvas3d as any).camera.getSnapshot()
: undefined;
// Store current camera snapshot
const curSnap = (plugin.canvas3d && (plugin.canvas3d as any).camera && (plugin.canvas3d as any).camera.getSnapshot)
? (plugin.canvas3d as any).camera.getSnapshot()
: undefined;
const update = plugin.state.data.build();
const update = plugin.state.data.build();
// Remove all children of this kinemage node (shapes/representations)
const children = plugin.state.data.tree.children.get(kinDataSelector.ref);
if (children) {
for (const childRef of children.values()) {
update.delete(childRef);
// Remove all children of this kinemage node (shapes/representations)
const children = plugin.state.data.tree.children.get(kinDataSelector.ref);
if (children) {
for (const childRef of children.values()) {
update.delete(childRef);
}
}
}
// Recreate shapes
await createShapesForKinemage(plugin, update, kinDataSelector);
await update.commit();
// Recreate shapes
await createShapesForKinemage(plugin, update, kinDataSelector);
await update.commit();
// Restore camera
if (curSnap) {
try {
await applyViewSnapshot(plugin, curSnap);
} catch (e) {
console.warn('Failed to restore camera snapshot after recreating shapes', e);
// Restore camera
if (curSnap) {
try {
await applyViewSnapshot(plugin, curSnap);
} catch (e) {
console.warn('Failed to restore camera snapshot after recreating shapes', e);
}
}
}
}
/** Centralized helper to apply kinemage content into plugin state */
async function applyKinemageToState(plugin: PluginContext, data: string, label?: string) {
const update = plugin.state.data.build();
const update = plugin.state.data.build();
// Create String data node
const dataNode = update
.toRoot()
.apply(StateTransforms.Data.RawData, { data, label: label || 'Kinemage File' });
// Create String data node
const dataNode = update
.toRoot()
.apply(StateTransforms.Data.RawData, { data, label: label || 'Kinemage File' });
// Parse into KinemageObject
const parsedNode = dataNode
.apply(ParseKinemage, { label });
// Parse into KinemageObject
const parsedNode = dataNode
.apply(ParseKinemage, { label });
// Select first kinemage (default)
const selectedNode = parsedNode
.apply(SelectKinemage, { index: 0 });
// Select first kinemage (default)
const selectedNode = parsedNode
.apply(SelectKinemage, { index: 0 });
await update.commit();
await update.commit();
// Now create shapes from the selected kinemage
const shapeUpdate = plugin.state.data.build();
await createShapesForKinemage(plugin, shapeUpdate, selectedNode.selector);
await shapeUpdate.commit();
// Now create shapes from the selected kinemage
const shapeUpdate = plugin.state.data.build();
await createShapesForKinemage(plugin, shapeUpdate, selectedNode.selector);
await shapeUpdate.commit();
// Wait for bounding sphere and focus camera
async function waitForNonEmptyBoundingSphere(plugin: PluginContext, timeoutMs = 2000, pollMs = 50) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const bs = getPluginBoundingSphere(plugin);
if (bs && bs.radius > 0) return bs;
} catch { /* ignore */ }
await new Promise<void>(r => setTimeout(r, pollMs));
// Wait for bounding sphere and focus camera
async function waitForNonEmptyBoundingSphere(plugin: PluginContext, timeoutMs = 2000, pollMs = 50) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const bs = getPluginBoundingSphere(plugin);
if (bs && bs.radius > 0) return bs;
} catch { /* ignore */ }
await new Promise<void>(r => setTimeout(r, pollMs));
}
return null;
}
return null;
}
try {
const bs = await waitForNonEmptyBoundingSphere(plugin);
if (bs && bs.radius > 0 && plugin.canvas3d) {
await PluginCommands.Camera.Focus(plugin, { center: bs.center, radius: bs.radius, durationMs: 250 });
plugin.canvas3d?.commit();
try {
const bs = await waitForNonEmptyBoundingSphere(plugin);
if (bs && bs.radius > 0 && plugin.canvas3d) {
await PluginCommands.Camera.Focus(plugin, { center: bs.center, radius: bs.radius, durationMs: 250 });
plugin.canvas3d?.commit();
}
} catch (e) {
console.warn('Failed to apply initial kinemage view snapshot', e);
}
} catch (e) {
console.warn('Failed to apply initial kinemage view snapshot', e);
}
return selectedNode.selector;
return selectedNode.selector;
}
/** Programmatic loader: load a single File (a .kin) into the plugin state.
* Returns the ref to the selected kinemage node.
*/
export async function loadKinemageFile(plugin: PluginContext, file: File): Promise<StateObjectSelector<PluginStateObject.Format.Json> | undefined> {
const content = await file.text();
return await applyKinemageToState(plugin, content, file.name);
const content = await file.text();
return await applyKinemageToState(plugin, content, file.name);
}
/** DragAndDropHandler handler for `.kin` files */
const KinemageDragAndDropHandler: DragAndDropHandler = {
name: 'kin',
async handle(files: File[], plugin: PluginContext): Promise<boolean> {
let applied = false;
for (const file of files) {
if (file.name.toLowerCase().endsWith('.kin')) {
const ref = await loadKinemageFile(plugin, file);
applied = applied || !!ref;
}
}
return applied;
},
name: 'kin',
async handle(files: File[], plugin: PluginContext): Promise<boolean> {
let applied = false;
for (const file of files) {
if (file.name.toLowerCase().endsWith('.kin')) {
const ref = await loadKinemageFile(plugin, file);
applied = applied || !!ref;
}
}
return applied;
},
};
const KINFormatProvider: DataFormatProvider<{}, any, any> = DataFormatProvider({
label: 'KIN',
description: 'Kinemage',
category: 'Miscellaneous',
stringExtensions: ['kin', 'KIN'],
parse: async (plugin, data) => {
try {
// data is already a StateObjectRef to the raw data in the tree
// Build the transform chain from it
const builder = plugin.state.data.build()
.to(data)
.apply(ParseKinemage, {});
const selectedKin = builder
.apply(SelectKinemage, { index: 0 });
await builder.commit();
// Return the selector for the selected kinemage so visuals can use it
return { selectedKin: selectedKin.selector };
} catch (e) {
console.error('Failed to parse KIN file', e);
throw e;
}
},
visuals: async (plugin, data) => {
if (!data?.selectedKin) {
console.warn('[Kinemage] visuals: no selectedKin ref provided');
return;
}
// Create shapes from the selected kinemage
const shapeBuilder = plugin.state.data.build();
await createShapesForKinemage(plugin, shapeBuilder, data.selectedKin);
await shapeBuilder.commit();
// Wait for bounding sphere and focus camera
async function waitForNonEmptyBoundingSphere(plugin: PluginContext, timeoutMs = 2000, pollMs = 50) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
label: 'KIN',
description: 'Kinemage',
category: 'Miscellaneous',
stringExtensions: ['kin', 'KIN'],
parse: async (plugin, data) => {
try {
const bs = getPluginBoundingSphere(plugin);
if (bs && bs.radius > 0) return bs;
} catch { /* ignore */ }
await new Promise<void>(r => setTimeout(r, pollMs));
}
return null;
}
// data is already a StateObjectRef to the raw data in the tree
// Build the transform chain from it
const builder = plugin.state.data.build()
.to(data)
.apply(ParseKinemage, {});
try {
const bs = await waitForNonEmptyBoundingSphere(plugin);
if (bs && bs.radius > 0 && plugin.canvas3d) {
await PluginCommands.Camera.Focus(plugin, { center: bs.center, radius: bs.radius, durationMs: 250 });
plugin.canvas3d?.commit();
}
} catch (e) {
console.warn('Failed to focus camera on kinemage', e);
}
const selectedKin = builder
.apply(SelectKinemage, { index: 0 });
return undefined;
}
await builder.commit();
// Return the selector for the selected kinemage so visuals can use it
return { selectedKin: selectedKin.selector };
} catch (e) {
console.error('Failed to parse KIN file', e);
throw e;
}
},
visuals: async (plugin, data) => {
if (!data?.selectedKin) {
console.warn('[Kinemage] visuals: no selectedKin ref provided');
return;
}
// Create shapes from the selected kinemage
const shapeBuilder = plugin.state.data.build();
await createShapesForKinemage(plugin, shapeBuilder, data.selectedKin);
await shapeBuilder.commit();
// Wait for bounding sphere and focus camera
async function waitForNonEmptyBoundingSphere(plugin: PluginContext, timeoutMs = 2000, pollMs = 50) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const bs = getPluginBoundingSphere(plugin);
if (bs && bs.radius > 0) return bs;
} catch { /* ignore */ }
await new Promise<void>(r => setTimeout(r, pollMs));
}
return null;
}
try {
const bs = await waitForNonEmptyBoundingSphere(plugin);
if (bs && bs.radius > 0 && plugin.canvas3d) {
await PluginCommands.Camera.Focus(plugin, { center: bs.center, radius: bs.radius, durationMs: 250 });
plugin.canvas3d?.commit();
}
} catch (e) {
console.warn('Failed to focus camera on kinemage', e);
}
return undefined;
}
});

View File

@@ -19,7 +19,6 @@ import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
import { SpheresBuilder } from '../../mol-geo/geometry/spheres/spheres-builder';
import { Shape } from '../../mol-model/shape';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
//import { ValueCell } from '../../mol-util/value-cell';
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
export type KinData = {
@@ -28,10 +27,9 @@ export type KinData = {
}
function createKinShapePointsParams(kinemage?: Kinemage) {
return {
...Points.Params,
};
return {
...Points.Params,
};
}
export const KinShapePointsParams = createKinShapePointsParams();
export type KinShapePointsParams = typeof KinShapePointsParams
@@ -45,12 +43,12 @@ export const KinShapeLinesParams = createKinShapeLinesParams();
export type KinShapeLinesParams = typeof KinShapeLinesParams
function createKinShapeMeshParams(kinemage?: Kinemage) {
return {
...Mesh.Params,
//transparentBackfaces: PD.Select('on', PD.arrayToOptions(['off', 'on', 'opaque'] as const)),
//doubleSided: PD.Boolean(true), // make mesh double-sided by default
//ignoreLight: PD.Boolean(true), // ignore lighting so front/back show same color
};
return {
...Mesh.Params,
// transparentBackfaces: PD.Select('on', PD.arrayToOptions(['off', 'on', 'opaque'] as const)),
// doubleSided: PD.Boolean(true), // make mesh double-sided by default
// ignoreLight: PD.Boolean(true), // ignore lighting so front/back show same color
};
}
export const KinShapeMeshParams = createKinShapeMeshParams();
@@ -58,280 +56,280 @@ export type KinShapeMeshParams = typeof KinShapeMeshParams
function createKinShapeSpheresParams(kinemage?: Kinemage) {
return {
...Spheres.Params,
};
return {
...Spheres.Params,
};
}
export const KinShapeSpheresParams = createKinShapeSpheresParams();
export type KinShapeSpheresParams = typeof KinShapeSpheresParams;
function getVisibility(group: string, subGroup: string, masters: string[], kin: Kinemage) {
let visible = true;
// Check to see if this name references a master that is not visible. If so, then this whole list is not visible and we can skip it.
const masterDict = kin.masterDict;
for (let m = 0; m < masters.length; m++) {
const masterName = masters[m];
const masterInfo = masterDict[masterName];
if (masterInfo && !masterInfo.visible) {
visible = false;
break;
}
}
// Check to see if this name references a group that has the 'off' flag set. If so, this is not visible.
const groupDict = kin.groupDict;
const groupInfo = groupDict[group];
if (groupInfo && groupInfo.off) {
visible = false;
}
// Check to see if this name references a subgroup that it or its master has the 'off' flag set. If so, this is not visible.
const subgroupDict = kin.subgroupDict;
const subgroupInfo = subgroupDict[subGroup];
if (subgroupInfo) {
if (subgroupInfo.off) {
visible = false;
}
if (subgroupInfo.group) {
const groupInfo = groupDict[subgroupInfo.group];
if (groupInfo && groupInfo.off) {
visible = false;
}
}
}
return visible;
function getVisibility(group: string, subGroup: string, masters: string[], kin: Kinemage) {
let visible = true;
// Check to see if this name references a master that is not visible. If so, then this whole list is not visible and we can skip it.
const masterDict = kin.masterDict;
for (let m = 0; m < masters.length; m++) {
const masterName = masters[m];
const masterInfo = masterDict[masterName];
if (masterInfo && !masterInfo.visible) {
visible = false;
break;
}
}
// Check to see if this name references a group that has the 'off' flag set. If so, this is not visible.
const groupDict = kin.groupDict;
const groupInfo = groupDict[group];
if (groupInfo && groupInfo.off) {
visible = false;
}
// Check to see if this name references a subgroup that it or its master has the 'off' flag set. If so, this is not visible.
const subgroupDict = kin.subgroupDict;
const subgroupInfo = subgroupDict[subGroup];
if (subgroupInfo) {
if (subgroupInfo.off) {
visible = false;
}
if (subgroupInfo.group) {
const groupInfo = groupDict[subgroupInfo.group];
if (groupInfo && groupInfo.off) {
visible = false;
}
}
}
return visible;
}
async function getPoints(ctx: RuntimeContext, kin: Kinemage) {
const dotLists: DotList[] = kin.dotLists;
const builderState = PointsBuilder.create();
const colors: Color[] = [];
const labels: string[] = [];
const dotLists: DotList[] = kin.dotLists;
const builderState = PointsBuilder.create();
const colors: Color[] = [];
const labels: string[] = [];
// Every dot is in its own Molstar group because they may have colors and we look that up by group.
let index = 0;
// Every dot is in its own Molstar group because they may have colors and we look that up by group.
let index = 0;
for (let i = 0; i < dotLists.length; i++) {
const dotList = dotLists[i];
const positionArray = dotList.positionArray;
const colorArray = dotList.colorArray;
const labelArray = dotList.labelArray;
const masterArray = dotList.masterArray;
for (let i = 0; i < dotLists.length; i++) {
const dotList = dotLists[i];
const positionArray = dotList.positionArray;
const colorArray = dotList.colorArray;
const labelArray = dotList.labelArray;
const masterArray = dotList.masterArray;
// Check the visibility of all of our masters and skip this dot list if any of them are not visible.
const visible = getVisibility(dotList.group, dotList.subgroup, masterArray, kin);
if (!visible) { continue; }
// Check the visibility of all of our masters and skip this dot list if any of them are not visible.
const visible = getVisibility(dotList.group, dotList.subgroup, masterArray, kin);
if (!visible) { continue; }
const numDots = positionArray.length / 3
for (let j = 0; j < numDots; j++) {
// Skip this element if any master associated with any of its pointMasters are turned off.
const pointMasterNames = dotList.pointmasterArray[j];
let pmVisibility = true;
for (let pm = 0; pm < pointMasterNames.length; pm++) {
const pointMasterName = pointMasterNames[pm];
const masterName = kin.pointmasterDict[pointMasterName];
const masterInfo = kin.masterDict[masterName];
if (masterInfo && !masterInfo.visible) {
pmVisibility = false;
continue;
const numDots = positionArray.length / 3;
for (let j = 0; j < numDots; j++) {
// Skip this element if any master associated with any of its pointMasters are turned off.
const pointMasterNames = dotList.pointmasterArray[j];
let pmVisibility = true;
for (let pm = 0; pm < pointMasterNames.length; pm++) {
const pointMasterName = pointMasterNames[pm];
const masterName = kin.pointmasterDict[pointMasterName];
const masterInfo = kin.masterDict[masterName];
if (masterInfo && !masterInfo.visible) {
pmVisibility = false;
continue;
}
}
if (!pmVisibility) { continue; }
const group = index++;
builderState.add(positionArray[3 * j + 0], positionArray[3 * j + 1], positionArray[3 * j + 2], group);
// colorArray may be undefined; push a default color when not provided
colors.push(colorArray && colorArray.length > j ? colorArray[j] : Color.fromRgb(255, 255, 255));
// labelArray may be undefined; push an empty string when not provided
labels.push(labelArray && labelArray.length > j ? labelArray[j] : '');
}
}
if (!pmVisibility) { continue; }
let group = index++;
builderState.add(positionArray[3 * j + 0], positionArray[3 * j + 1], positionArray[3 * j + 2], group);
// colorArray may be undefined; push a default color when not provided
colors.push(colorArray && colorArray.length > j ? colorArray[j] : Color.fromRgb(255, 255, 255))
// labelArray may be undefined; push an empty string when not provided
labels.push(labelArray && labelArray.length > j ? labelArray[j] : '');
}
}
const points = builderState.getPoints();
return { points, colors, labels };
const points = builderState.getPoints();
return { points, colors, labels };
}
async function getLines(ctx: RuntimeContext, kin: Kinemage) {
const vectorLists: VectorList[] = kin.vectorLists;
const builderState = LinesBuilder.create();
const widths: number[] = [];
const colors: Color[] = [];
const labels: string[] = [];
const vectorLists: VectorList[] = kin.vectorLists;
const builderState = LinesBuilder.create();
const widths: number[] = [];
const colors: Color[] = [];
const labels: string[] = [];
// Every line is in its own Molstar group because they may have individual widths and we look
// up the width based on the group is in the size function.
let index = 0;
// Every line is in its own Molstar group because they may have individual widths and we look
// up the width based on the group is in the size function.
let index = 0;
for (let i = 0; i < vectorLists.length; i++) {
const vectorList = vectorLists[i];
const position1Array = vectorList.position1Array;
const position2Array = vectorList.position2Array;
const widthArray = vectorList.width;
const color1Array = vectorList.color1Array;
const color2Array = vectorList.color2Array;
const label1Array = vectorList.label1Array;
const label2Array = vectorList.label2Array;
const masterArray = vectorList.masterArray;
for (let i = 0; i < vectorLists.length; i++) {
const vectorList = vectorLists[i];
const position1Array = vectorList.position1Array;
const position2Array = vectorList.position2Array;
const widthArray = vectorList.width;
const color1Array = vectorList.color1Array;
const color2Array = vectorList.color2Array;
const label1Array = vectorList.label1Array;
const label2Array = vectorList.label2Array;
const masterArray = vectorList.masterArray;
// Check the visibility of all of our masters and skip this vector list if any of them are not visible.
const visible = getVisibility(vectorList.group, vectorList.subgroup, masterArray, kin);
if (!visible) { continue; }
// Check the visibility of all of our masters and skip this vector list if any of them are not visible.
const visible = getVisibility(vectorList.group, vectorList.subgroup, masterArray, kin);
if (!visible) { continue; }
const numLines = position1Array.length / 3
for (let j = 0; j < numLines; j++) {
// Skip this element if any master associated with any of its pointMasters are turned off.
const pointMasterNames = vectorList.pointmasterArray[j];
let pmVisibility = true;
for (let pm = 0; pm < pointMasterNames.length; pm++) {
const pointMasterName = pointMasterNames[pm];
const masterName = kin.pointmasterDict[pointMasterName];
const masterInfo = kin.masterDict[masterName];
if (masterInfo && !masterInfo.visible) {
pmVisibility = false;
continue;
const numLines = position1Array.length / 3;
for (let j = 0; j < numLines; j++) {
// Skip this element if any master associated with any of its pointMasters are turned off.
const pointMasterNames = vectorList.pointmasterArray[j];
let pmVisibility = true;
for (let pm = 0; pm < pointMasterNames.length; pm++) {
const pointMasterName = pointMasterNames[pm];
const masterName = kin.pointmasterDict[pointMasterName];
const masterInfo = kin.masterDict[masterName];
if (masterInfo && !masterInfo.visible) {
pmVisibility = false;
continue;
}
}
if (!pmVisibility) { continue; }
// Find the midpoint of the line because we're going to actually make
// two half-lines so that labels and selection work better.
const midX = (position1Array[3 * j + 0] + position2Array[3 * j + 0]) / 2;
const midY = (position1Array[3 * j + 1] + position2Array[3 * j + 1]) / 2;
const midZ = (position1Array[3 * j + 2] + position2Array[3 * j + 2]) / 2;
// Make the first half of the line from position1 to the midpoint, labeled and colored based on position1.
let group = index++;
builderState.add(position1Array[3 * j + 0], position1Array[3 * j + 1], position1Array[3 * j + 2],
midX, midY, midZ,
group);
// widthArray may be undefined; push NaN when width not provided
widths.push(widthArray && widthArray.length > j ? widthArray[j] : NaN);
// colorArray may be undefined; push a default color when not provided
colors.push(color1Array && color1Array.length > j ? color1Array[j] : Color.fromRgb(255, 255, 255));
// labelArray may be undefined; push an empty string when not provided
labels.push(label1Array && label1Array.length > j ? label1Array[j] : '');
// Make the second half of the line from the midpoint to position2, labeled and colored based on position2.
group = index++;
builderState.add(midX, midY, midZ,
position2Array[3 * j + 0], position2Array[3 * j + 1], position2Array[3 * j + 2],
group);
// widthArray may be undefined; push NaN when width not provided
widths.push(widthArray && widthArray.length > j ? widthArray[j] : NaN);
// colorArray may be undefined; push a default color when not provided
colors.push(color2Array && color2Array.length > j ? color2Array[j] : Color.fromRgb(255, 255, 255));
// labelArray may be undefined; push an empty string when not provided
labels.push(label2Array && label2Array.length > j ? label2Array[j] : '');
}
}
if (!pmVisibility) { continue; }
// Find the midpoint of the line because we're going to actually make
// two half-lines so that labels and selection work better.
const midX = (position1Array[3 * j + 0] + position2Array[3 * j + 0]) / 2;
const midY = (position1Array[3 * j + 1] + position2Array[3 * j + 1]) / 2;
const midZ = (position1Array[3 * j + 2] + position2Array[3 * j + 2]) / 2;
// Make the first half of the line from position1 to the midpoint, labeled and colored based on position1.
let group = index++;
builderState.add(position1Array[3 * j + 0], position1Array[3 * j + 1], position1Array[3 * j + 2],
midX, midY, midZ,
group);
// widthArray may be undefined; push NaN when width not provided
widths.push(widthArray && widthArray.length > j ? widthArray[j] : NaN)
// colorArray may be undefined; push a default color when not provided
colors.push(color1Array && color1Array.length > j ? color1Array[j] : Color.fromRgb(255, 255, 255))
// labelArray may be undefined; push an empty string when not provided
labels.push(label1Array && label1Array.length > j ? label1Array[j] : '')
// Make the second half of the line from the midpoint to position2, labeled and colored based on position2.
group = index++;
builderState.add(midX, midY, midZ,
position2Array[3 * j + 0], position2Array[3 * j + 1], position2Array[3 * j + 2],
group);
// widthArray may be undefined; push NaN when width not provided
widths.push(widthArray && widthArray.length > j ? widthArray[j] : NaN)
// colorArray may be undefined; push a default color when not provided
colors.push(color2Array && color2Array.length > j ? color2Array[j] : Color.fromRgb(255, 255, 255))
// labelArray may be undefined; push an empty string when not provided
labels.push(label2Array && label2Array.length > j ? label2Array[j] : '')
}
}
const lines = builderState.getLines();
return { lines, widths: new Float32Array(widths), colors, labels };
const lines = builderState.getLines();
return { lines, widths: new Float32Array(widths), colors, labels };
}
function addOffsetTriangle(builderState: MeshBuilder.State, a: Vec3, b: Vec3, c: Vec3, n: Vec3, offset: number) {
const aOffset = Vec3.add(Vec3(), a, Vec3.scale(Vec3(), n, offset));
const bOffset = Vec3.add(Vec3(), b, Vec3.scale(Vec3(), n, offset));
const cOffset = Vec3.add(Vec3(), c, Vec3.scale(Vec3(), n, offset));
MeshBuilder.addTriangleWithNormal(builderState, aOffset, bOffset, cOffset, n);
function addOffsetTriangle(builderState: MeshBuilder.State, a: Vec3, b: Vec3, c: Vec3, n: Vec3, offset: number) {
const aOffset = Vec3.add(Vec3(), a, Vec3.scale(Vec3(), n, offset));
const bOffset = Vec3.add(Vec3(), b, Vec3.scale(Vec3(), n, offset));
const cOffset = Vec3.add(Vec3(), c, Vec3.scale(Vec3(), n, offset));
MeshBuilder.addTriangleWithNormal(builderState, aOffset, bOffset, cOffset, n);
}
async function getMesh(ctx: RuntimeContext, kin: Kinemage) {
const ribbonObjects: RibbonObject[] = kin.ribbonLists;
const builderState = MeshBuilder.createState();
const colors: Color[] = [];
const labels: string[] = [];
const ribbonObjects: RibbonObject[] = kin.ribbonLists;
const builderState = MeshBuilder.createState();
const colors: Color[] = [];
const labels: string[] = [];
// Every triangle is in its own Molstar group because they may have individual colors and we look
// up the color based on the group is in the color function.
let index = 0;
// Every triangle is in its own Molstar group because they may have individual colors and we look
// up the color based on the group is in the color function.
let index = 0;
for (let ri = 0; ri < ribbonObjects.length; ri++) {
const ribbonObject = ribbonObjects[ri];
const coords = ribbonObject.positionArray;
const colorArray = ribbonObject.colorArray;
const labelArray = ribbonObject.labelArray;
const masterArray = ribbonObject.masterArray;
const pointMasterArray = ribbonObject.pointmasterArray;
for (let ri = 0; ri < ribbonObjects.length; ri++) {
const ribbonObject = ribbonObjects[ri];
const coords = ribbonObject.positionArray;
const colorArray = ribbonObject.colorArray;
const labelArray = ribbonObject.labelArray;
const masterArray = ribbonObject.masterArray;
const pointMasterArray = ribbonObject.pointmasterArray;
// Check the visibility of all of our masters and skip this ribbon object if any of them are not visible.
const visible = getVisibility(ribbonObject.group, ribbonObject.subgroup, masterArray, kin);
if (!visible) { continue; }
// Check the visibility of all of our masters and skip this ribbon object if any of them are not visible.
const visible = getVisibility(ribbonObject.group, ribbonObject.subgroup, masterArray, kin);
if (!visible) { continue; }
builderState.currentGroup = ri; /// @todo Base this on something in the file instead?
builderState.currentGroup = ri; // TODO: Base this on something in the file instead?
// The positionArray contains 3x as many entries as there are vertices since it's a catenation of x, y, z for each vertex.
// There are three vertices per triangle.
/// @todo Ribbon lighting is to be set up to make each pair of triangles look like a quad with the same normal.
const numTriangles = coords.length / 9;
let prevTriangleNormal: Vec3 | undefined = undefined;
for (let i = 0; i < numTriangles; i++) {
// Skip this element if any master associated with any of its pointMasters are turned off.
const pointMasterNames = pointMasterArray[3 * i];
let pmVisibility = true;
for (let pm = 0; pm < pointMasterNames.length; pm++) {
const pointMasterName = pointMasterNames[pm];
const masterName = kin.pointmasterDict[pointMasterName];
const masterInfo = kin.masterDict[masterName];
if (masterInfo && !masterInfo.visible) {
pmVisibility = false;
continue;
// The positionArray contains 3x as many entries as there are vertices since it's a catenation of x, y, z for each vertex.
// There are three vertices per triangle.
// TODO: Ribbon lighting is to be set up to make each pair of triangles look like a quad with the same normal.
const numTriangles = coords.length / 9;
let prevTriangleNormal: Vec3 | undefined = undefined;
for (let i = 0; i < numTriangles; i++) {
// Skip this element if any master associated with any of its pointMasters are turned off.
const pointMasterNames = pointMasterArray[3 * i];
let pmVisibility = true;
for (let pm = 0; pm < pointMasterNames.length; pm++) {
const pointMasterName = pointMasterNames[pm];
const masterName = kin.pointmasterDict[pointMasterName];
const masterInfo = kin.masterDict[masterName];
if (masterInfo && !masterInfo.visible) {
pmVisibility = false;
continue;
}
}
if (!pmVisibility) { continue; }
const vertexList: Vec3[] = [];
// Get the vertices for the triangle out of the position array and push them onto a list.
for (let j = 0; j < 3; j++) {
const v = Vec3.zero();
v[0] = coords[3 * (3 * i + j) + 0];
v[1] = coords[3 * (3 * i + j) + 1];
v[2] = coords[3 * (3 * i + j) + 2];
vertexList.push(v);
}
// Set the group per triangle so that we can do per-triangle coloring.
const group = index++;
builderState.currentGroup = group;
// colorArray may be undefined; push a default color when not provided.
// There is one color per group, even if we have two triangles in this group.
const color = colorArray && colorArray.length > i * 3 ? colorArray[3 * i] : Color.fromRgb(255, 255, 255);
colors.push(color);
// labelArray may be undefined; push an empty string when not provided
const label = labelArray && labelArray.length > i ? labelArray[i] : '';
labels.push(label);
// Find the vertics and normal for the triangle.
const a: Vec3 = vertexList[0];
const b: Vec3 = vertexList[1];
const c: Vec3 = vertexList[2];
// Put both orientations of the triangle. Add a small amount along the normal to make them
// not be exactly on top of each other so that we only see the front face of each.
let n = Vec3.zero();
Vec3.triangleNormal(n, a, b, c);
if (i % 2 === 1) {
// For ribbons, every other triangle is meant to be paired with the previous one to make a quad with the same normal.
// So use the same normal for every other triangle.
n = prevTriangleNormal || n;
}
prevTriangleNormal = n;
addOffsetTriangle(builderState, a, b, c, n, 0.01);
// Invert the normal for the back face.
Vec3.negate(n, n);
addOffsetTriangle(builderState, a, c, b, n, 0.01);
}
}
if (!pmVisibility) { continue; }
const vertexList: Vec3[] = [];
// Get the vertices for the triangle out of the position array and push them onto a list.
for (let j = 0; j < 3; j++) {
const v = Vec3.zero();
v[0] = coords[3 * (3 * i + j) + 0];
v[1] = coords[3 * (3 * i + j) + 1];
v[2] = coords[3 * (3 * i + j) + 2];
vertexList.push(v);
}
// Set the group per triangle so that we can do per-triangle coloring.
let group = index++;
builderState.currentGroup = group;
// colorArray may be undefined; push a default color when not provided.
// There is one color per group, even if we have two triangles in this group.
const color = colorArray && colorArray.length > i * 3 ? colorArray[3 * i] : Color.fromRgb(255, 255, 255);
colors.push(color);
// labelArray may be undefined; push an empty string when not provided
const label = labelArray && labelArray.length > i ? labelArray[i] : '';
labels.push(label);
// Find the vertics and normal for the triangle.
let a: Vec3 = vertexList[0];
let b: Vec3 = vertexList[1];
let c: Vec3 = vertexList[2];
// Put both orientations of the triangle. Add a small amount along the normal to make them
// not be exactly on top of each other so that we only see the front face of each.
let n = Vec3.zero();
Vec3.triangleNormal(n, a, b, c);
if (i % 2 === 1) {
// For ribbons, every other triangle is meant to be paired with the previous one to make a quad with the same normal.
// So use the same normal for every other triangle.
n = prevTriangleNormal || n;
}
prevTriangleNormal = n;
addOffsetTriangle(builderState, a, b, c, n, 0.01);
// Invert the normal for the back face.
Vec3.negate(n, n);
addOffsetTriangle(builderState, a, c, b, n, 0.01);
}
}
const mesh = MeshBuilder.getMesh(builderState);
return { mesh, colors, labels };
const mesh = MeshBuilder.getMesh(builderState);
return { mesh, colors, labels };
}
/**
@@ -339,88 +337,87 @@ async function getMesh(ctx: RuntimeContext, kin: Kinemage) {
* Returns an object with the Spheres geometry and a Float32Array with per-center radii (one entry per center, in the same order they were added).
*/
async function getSpheres(ctx: RuntimeContext, kin: Kinemage) {
const balls: BallList[] = kin.ballLists;
const builderState = SpheresBuilder.create();
const radii: number[] = [];
const colors: Color[] = [];
const labels: string[] = [];
const balls: BallList[] = kin.ballLists;
const builderState = SpheresBuilder.create();
const radii: number[] = [];
const colors: Color[] = [];
const labels: string[] = [];
// Every ball is in its own Molstar group because they may have individual radii and we look
// up the radius based on the group is in the size function.
let index = 0;
// Every ball is in its own Molstar group because they may have individual radii and we look
// up the radius based on the group is in the size function.
let index = 0;
for (let i = 0; i < balls.length; i++) {
const ballList = balls[i];
const positionArray = ballList.positionArray;
const radiusArray = ballList.radiusArray;
const colorArray = ballList.colorArray;
const masterArray = ballList.masterArray;
for (let i = 0; i < balls.length; i++) {
const ballList = balls[i];
const positionArray = ballList.positionArray;
const radiusArray = ballList.radiusArray;
const colorArray = ballList.colorArray;
const masterArray = ballList.masterArray;
// Check the visibility of all of our masters and skip this ball list if any of them are not visible.
const visible = getVisibility(ballList.group, ballList.subgroup, masterArray, kin);
if (!visible) { continue; }
// Check the visibility of all of our masters and skip this ball list if any of them are not visible.
const visible = getVisibility(ballList.group, ballList.subgroup, masterArray, kin);
if (!visible) { continue; }
const numBalls = positionArray.length / 3;
for (let j = 0; j < numBalls; j++) {
// Skip this element if any master associated with any of its pointMasters are turned off.
const pointMasterNames = ballList.pointmasterArray[j];
let pmVisibility = true;
for (let pm = 0; pm < pointMasterNames.length; pm++) {
const pointMasterName = pointMasterNames[pm];
const masterName = kin.pointmasterDict[pointMasterName];
const masterInfo = kin.masterDict[masterName];
if (masterInfo && !masterInfo.visible) {
pmVisibility = false;
continue;
const numBalls = positionArray.length / 3;
for (let j = 0; j < numBalls; j++) {
// Skip this element if any master associated with any of its pointMasters are turned off.
const pointMasterNames = ballList.pointmasterArray[j];
let pmVisibility = true;
for (let pm = 0; pm < pointMasterNames.length; pm++) {
const pointMasterName = pointMasterNames[pm];
const masterName = kin.pointmasterDict[pointMasterName];
const masterInfo = kin.masterDict[masterName];
if (masterInfo && !masterInfo.visible) {
pmVisibility = false;
continue;
}
}
if (!pmVisibility) { continue; }
const group = index++;
builderState.add(positionArray[3 * j + 0], positionArray[3 * j + 1], positionArray[3 * j + 2], group);
// radiusArray may be undefined; push NaN when radius not provided
radii.push(radiusArray && radiusArray.length > j ? radiusArray[j] : NaN);
// colorArray may be undefined; push a default color when not provided
colors.push(colorArray && colorArray.length > j ? colorArray[j] : Color.fromRgb(255, 255, 255));
// labelArray may be undefined; push an empty string when not provided
labels.push(ballList.labelArray && ballList.labelArray.length > j ? ballList.labelArray[j] : '');
}
}
if (!pmVisibility) { continue; }
const group = index++;
builderState.add(positionArray[3 * j + 0], positionArray[3 * j + 1], positionArray[3 * j + 2], group);
// radiusArray may be undefined; push NaN when radius not provided
radii.push(radiusArray && radiusArray.length > j ? radiusArray[j] : NaN);
// colorArray may be undefined; push a default color when not provided
colors.push(colorArray && colorArray.length > j ? colorArray[j] : Color.fromRgb(255, 255, 255));
// labelArray may be undefined; push an empty string when not provided
labels.push(ballList.labelArray && ballList.labelArray.length > j ? ballList.labelArray[j] : '');
}
}
const spheres = builderState.getSpheres();
return { spheres, radii: new Float32Array(radii), colors, labels };
const spheres = builderState.getSpheres();
return { spheres, radii: new Float32Array(radii), colors, labels };
}
function makePointsShapeGetter() {
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapePointsParams>, shape?: Shape<Points>) => {
// Get our points, adding them from all of the entries in the dot lists
const { points: _points, colors, labels } = await getPoints(ctx, kinData.source);
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapePointsParams>, shape?: Shape<Points>) => {
// Get our points, adding them from all of the entries in the dot lists
const { points: _points, colors, labels } = await getPoints(ctx, kinData.source);
// Color function signature: (groupId: number, instanceId: number) => Color
// For Lines the groupId corresponds to the line index (order added).
const colorFn = (group: number, instance: number) => {
return colors[group];
}
// Color function signature: (groupId: number, instanceId: number) => Color
// For Lines the groupId corresponds to the line index (order added).
const colorFn = (group: number, instance: number) => {
return colors[group];
};
// Label function signature: (groupId: number, instanceId: number) => string
// For Lines the groupId corresponds to the line index (order added).
const labelFn = (group: number, instance: number) => {
return labels[group];
}
// Label function signature: (groupId: number, instanceId: number) => string
// For Lines the groupId corresponds to the line index (order added).
const labelFn = (group: number, instance: number) => {
return labels[group];
};
let _shape: Shape<Points>;
_shape = Shape.create<Points>(
'kin-points',
kinData.source,
_points,
colorFn, // color function reads per-point colors
() => 1, // size function
labelFn // label function reads per-point labels
);
return _shape;
};
return getShape;
const _shape = Shape.create<Points>(
'kin-points',
kinData.source,
_points,
colorFn, // color function reads per-point colors
() => 1, // size function
labelFn // label function reads per-point labels
);
return _shape;
};
return getShape;
}
function makeLineShapeGetter() {
@@ -432,32 +429,31 @@ function makeLineShapeGetter() {
// Size function signature: (groupId: number, instanceId: number) => number
// For Lines the groupId corresponds to the line index (order added).
const sizeFn = (group: number, instance: number) => {
// We're specifying the radius, which is half the width.
let w = widths[group] / 2.0;
if (w < 1.0) { w = 1.0; }
return Number.isFinite(w) ? w : 1.0;
}
// We're specifying the radius, which is half the width.
let w = widths[group] / 2.0;
if (w < 1.0) { w = 1.0; }
return Number.isFinite(w) ? w : 1.0;
};
// Color function signature: (groupId: number, instanceId: number) => Color
// For Lines the groupId corresponds to the line index (order added).
const colorFn = (group: number, instance: number) => {
return colors[group];
}
return colors[group];
};
// Label function signature: (groupId: number, instanceId: number) => string
// For Lines the groupId corresponds to the line index (order added).
const labelFn = (group: number, instance: number) => {
return labels[group];
}
return labels[group];
};
let _shape: Shape<Lines>;
_shape = Shape.create<Lines>(
'kin-lines',
kinData.source,
_lines,
colorFn, // color function reads per-line colors
sizeFn, // size function reads per-line widths
labelFn // label function
const _shape = Shape.create<Lines>(
'kin-lines',
kinData.source,
_lines,
colorFn, // color function reads per-line colors
sizeFn, // size function reads per-line widths
labelFn // label function
);
return _shape;
};
@@ -466,37 +462,36 @@ function makeLineShapeGetter() {
function makeMeshShapeGetter() {
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapeMeshParams>, shape?: Shape<Mesh>) => {
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapeMeshParams>, shape?: Shape<Mesh>) => {
let { mesh: _mesh, colors, labels } = await getMesh(ctx, kinData.source);
// Ensure that _mesh is not undifined before we pass it to Shape.create. If it is undefined, create an empty mesh instead.
if (!_mesh) {
console.warn('No mesh could be created from the KIN data. Creating an empty mesh instead.');
_mesh = Mesh.createEmpty();
}
let { mesh: _mesh, colors, labels } = await getMesh(ctx, kinData.source);
// Ensure that _mesh is not undifined before we pass it to Shape.create. If it is undefined, create an empty mesh instead.
if (!_mesh) {
console.warn('No mesh could be created from the KIN data. Creating an empty mesh instead.');
_mesh = Mesh.createEmpty();
}
// Color function signature: (groupId: number, instanceId: number) => Color
// For Lines the groupId corresponds to the line index (order added).
const colorFn = (group: number, instance: number) => {
return colors[group];
}
// Color function signature: (groupId: number, instanceId: number) => Color
// For Lines the groupId corresponds to the line index (order added).
const colorFn = (group: number, instance: number) => {
return colors[group];
};
const labelFn = (group: number, instance: number) => {
return labels[group];
}
const labelFn = (group: number, instance: number) => {
return labels[group];
};
let _shape: Shape<Mesh>;
_shape = Shape.create<Mesh>(
'kin-mesh',
kinData.source,
_mesh,
colorFn, // color function reads per-triangle colors
() => 1, // size function
labelFn // label function
);
return _shape;
};
return getShape;
const _shape = Shape.create<Mesh>(
'kin-mesh',
kinData.source,
_mesh,
colorFn, // color function reads per-triangle colors
() => 1, // size function
labelFn // label function
);
return _shape;
};
return getShape;
}
/**
@@ -504,87 +499,86 @@ function makeMeshShapeGetter() {
*/
function makeSpheresShapeGetter() {
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapeSpheresParams>, shape?: Shape<Spheres>) => {
// Build spheres geometry and collect per-center radii
const { spheres: _spheres, radii, colors, labels } = await getSpheres(ctx, kinData.source);
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapeSpheresParams>, shape?: Shape<Spheres>) => {
// Build spheres geometry and collect per-center radii
const { spheres: _spheres, radii, colors, labels } = await getSpheres(ctx, kinData.source);
// size function signature: (groupId: number, instanceId: number) => number
// For Spheres the groupId corresponds to the center index (order added).
const sizeFn = (group: number, instance: number) => {
const r = radii[group];
return Number.isFinite(r) ? r : 1.0;
// size function signature: (groupId: number, instanceId: number) => number
// For Spheres the groupId corresponds to the center index (order added).
const sizeFn = (group: number, instance: number) => {
const r = radii[group];
return Number.isFinite(r) ? r : 1.0;
};
// Color function signature: (groupId: number, instanceId: number) => Color
// For Spheres the groupId corresponds to the center index (order added).
const colorFn = (group: number, instance: number) => {
return colors[group];
};
// Label function signature: (groupId: number, instanceId: number) => string
// For Spheres the groupId corresponds to the center index (order added).
const labelFn = (group: number, instance: number) => {
return labels[group];
};
const _shape = Shape.create<Spheres>(
'kin-spheres',
kinData.source,
_spheres,
colorFn, // color function reads per-center colors
sizeFn, // size function reads per-center radii
labelFn // label function
);
return _shape;
};
// Color function signature: (groupId: number, instanceId: number) => Color
// For Spheres the groupId corresponds to the center index (order added).
const colorFn = (group: number, instance: number) => {
return colors[group];
}
// Label function signature: (groupId: number, instanceId: number) => string
// For Spheres the groupId corresponds to the center index (order added).
const labelFn = (group: number, instance: number) => {
return labels[group];
}
let _shape: Shape<Spheres>;
_shape = Shape.create<Spheres>(
'kin-spheres',
kinData.source,
_spheres,
colorFn, // color function reads per-center colors
sizeFn, // size function reads per-center radii
labelFn // label function
);
return _shape;
};
return getShape;
return getShape;
}
export function shapePointsFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
return Task.create<ShapeProvider<KinData, Points, KinShapePointsParams>>('Kin Shape Points Provider', async ctx => {
return {
label: label ?? 'Points',
data: { source, transforms: params?.transforms },
params: createKinShapePointsParams(source),
getShape: makePointsShapeGetter(),
geometryUtils: Points.Utils
};
});
return Task.create<ShapeProvider<KinData, Points, KinShapePointsParams>>('Kin Shape Points Provider', async ctx => {
return {
label: label ?? 'Points',
data: { source, transforms: params?.transforms },
params: createKinShapePointsParams(source),
getShape: makePointsShapeGetter(),
geometryUtils: Points.Utils
};
});
}
export function shapeLinesFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
return Task.create<ShapeProvider<KinData, Lines, KinShapeLinesParams>>('Kin Shape Lines Provider', async ctx => {
return {
label: label ?? 'Lines',
data: { source, transforms: params?.transforms },
params: createKinShapeLinesParams(source),
getShape: makeLineShapeGetter(),
geometryUtils: Lines.Utils
};
});
return Task.create<ShapeProvider<KinData, Lines, KinShapeLinesParams>>('Kin Shape Lines Provider', async ctx => {
return {
label: label ?? 'Lines',
data: { source, transforms: params?.transforms },
params: createKinShapeLinesParams(source),
getShape: makeLineShapeGetter(),
geometryUtils: Lines.Utils
};
});
}
export function shapeMeshFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
return Task.create<ShapeProvider<KinData, Mesh, KinShapeMeshParams>>('Kin Shape Mesh Provider', async ctx => {
return {
label: label ?? 'Meshes',
data: { source, transforms: params?.transforms },
params: createKinShapeMeshParams(source),
getShape: makeMeshShapeGetter(),
geometryUtils: Mesh.Utils
};
});
return Task.create<ShapeProvider<KinData, Mesh, KinShapeMeshParams>>('Kin Shape Mesh Provider', async ctx => {
return {
label: label ?? 'Meshes',
data: { source, transforms: params?.transforms },
params: createKinShapeMeshParams(source),
getShape: makeMeshShapeGetter(),
geometryUtils: Mesh.Utils
};
});
}
export function shapeSpheresFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
return Task.create<ShapeProvider<KinData, Spheres, KinShapeSpheresParams>>('Kin Shape Spheres Provider', async ctx => {
return {
label: label ?? 'Spheres',
data: { source, transforms: params?.transforms },
params: createKinShapeSpheresParams(source),
getShape: makeSpheresShapeGetter(),
geometryUtils: Spheres.Utils
};
});
return Task.create<ShapeProvider<KinData, Spheres, KinShapeSpheresParams>>('Kin Shape Spheres Provider', async ctx => {
return {
label: label ?? 'Spheres',
data: { source, transforms: params?.transforms },
params: createKinShapeSpheresParams(source),
getShape: makeSpheresShapeGetter(),
geometryUtils: Spheres.Utils
};
});
}

View File

@@ -22,7 +22,7 @@ export type KinemageParams = typeof KinemageParams
export type KinemageProps = PD.Values<KinemageParams>
export const KinemageDataParams = {
...KinemageParams
...KinemageParams
};
export type KinemageDataParams = typeof KinemageDataParams
export type KinemageDataProps = PD.Values<KinemageDataParams>
@@ -37,7 +37,7 @@ interface KinemageData {
}
const FileSourceParams = {
input: PD.File({ accept: '.kin', multiple: false })
input: PD.File({ accept: '.kin', multiple: false })
};
type FileSourceProps = PD.Values<typeof FileSourceParams>
@@ -47,37 +47,37 @@ namespace KinemageData {
}
export const symbols = {
}
};
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;
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;
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');
}
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 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();
return { kinemages };
const kinemages = await task.run();
return { kinemages };
}
}
@@ -108,9 +108,9 @@ function isApplicable(structure: Structure) {
}
async function computeKinemageProps(ctx: CustomProperty.Context, data: Structure, props: Partial<KinemageProps>): Promise<KinemageData> {
// Return an empty KinemageData object since the actual data will be loaded asynchronously via the `open` method.
// This allows the property to be attached to the structure without blocking on file loading.
return {
kinemages: []
};
// Return an empty KinemageData object since the actual data will be loaded asynchronously via the `open` method.
// This allows the property to be attached to the structure without blocking on file loading.
return {
kinemages: []
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,31 +7,31 @@
import { ReaderResult as Result } from '../../../mol-io/reader/result';
import { Task, RuntimeContext } from '../../../mol-task';
import { Kinemage } from './schema';
import KinParser from './kinparser';
import { KinParser } from './kinparser';
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 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
// 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);
// 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();
}
}
return Result.success(kinemages);
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) {

View File

@@ -7,76 +7,76 @@
import { Color } from '../../../mol-util/color';
export interface Kinemage {
readonly comments: ReadonlyArray<string>
kinemage?: number,
onewidth?: any,
viewDict: { [id: number]: View },
pdbfile?: string,
text: string,
texts: string[],
captions: string[],
caption: string,
groupDict: { [k: string]: { [k: string]: boolean } },
subgroupDict: { [k: string]: any }, ///< Subgroup key is "GroupName:SubgroupName" to preserve tree structure
masterDict: { [k: string]: { indent: boolean, visible: boolean } },
pointmasterDict: { [k: string]: string }, ///< Maps from single-character name to master name for points, e.g. 'a' -> 'alta'
dotLists: DotList[],
vectorLists: VectorList[],
ballLists: BallList[],
ribbonLists: RibbonObject[],
groupsAnimate: string[],
activeAnimateGroup: number,
groupsAnimate2: string[],
activeAnimateGroup2: number,
viewSnapshots?: {} ///< Used to store view snapshots in behavior.ts to use in ui.tsx
readonly comments: ReadonlyArray<string>
kinemage?: number,
onewidth?: any,
viewDict: { [id: number]: View },
pdbfile?: string,
text: string,
texts: string[],
captions: string[],
caption: string,
groupDict: { [k: string]: { [k: string]: boolean } },
subgroupDict: { [k: string]: any }, // /< Subgroup key is "GroupName:SubgroupName" to preserve tree structure
masterDict: { [k: string]: { indent: boolean, visible: boolean } },
pointmasterDict: { [k: string]: string }, // /< Maps from single-character name to master name for points, e.g. 'a' -> 'alta'
dotLists: DotList[],
vectorLists: VectorList[],
ballLists: BallList[],
ribbonLists: RibbonObject[],
groupsAnimate: string[],
activeAnimateGroup: number,
groupsAnimate2: string[],
activeAnimateGroup2: number,
viewSnapshots?: {} // /< Used to store view snapshots in behavior.ts to use in ui.tsx
}
/** Common base for all list-like objects in a kinemage */
export interface KinListBase {
name?: string, ///< Optional name of the whole List
group: string, ///< Name of the group this List belongs to (may be '' if no group)
subgroup: string, ///< Name of the subgroup this List belongs to (may be '' if no subgroup)
nobutton: boolean, ///< Whether the list is a nobutton list (true if 'nobutton' keyword found)
masterArray: any[], ///< Array of master names per List, not per element
pointmasterArray: string[][] ///< Array of point master names per element
name?: string, // /< Optional name of the whole List
group: string, // /< Name of the group this List belongs to (may be '' if no group)
subgroup: string, // /< Name of the subgroup this List belongs to (may be '' if no subgroup)
nobutton: boolean, // /< Whether the list is a nobutton list (true if 'nobutton' keyword found)
masterArray: any[], // /< Array of master names per List, not per element
pointmasterArray: string[][] // /< Array of point master names per element
}
export interface DotList extends KinListBase {
labelArray: string[], ///< Array of labels per element
positionArray: number[], ///< Catenation of x, y, z for each element, 3x as many as elements
colorArray: Color[] ///< Color for each element, as many as elements
labelArray: string[], // /< Array of labels per element
positionArray: number[], // /< Catenation of x, y, z for each element, 3x as many as elements
colorArray: Color[] // /< Color for each element, as many as elements
}
export interface BallList extends KinListBase {
labelArray: string[], ///< Array of labels per element
positionArray: number[], ///< Catenation of x, y, z for each element, 3x as many as elements
colorArray: Color[], ///< Color for each element, as many as elements
radiusArray: number[] ///< A single radius per element
labelArray: string[], // /< Array of labels per element
positionArray: number[], // /< Catenation of x, y, z for each element, 3x as many as elements
colorArray: Color[], // /< Color for each element, as many as elements
radiusArray: number[] // /< A single radius per element
}
export interface RibbonObject extends KinListBase {
labelArray: string[], ///< Array of labels per element
positionArray: number[], ///< Catenation of x, y, z for each element, 9x as many as triangles (3 vertices per triangle)
colorArray: Color[], ///< Color for each element, as many as elements
breakArray: boolean[], ///< A single boolean per element indicating if there is a break there
pairTriangleNormals: boolean ///< Whether to pair every other triangle normal for lighting (true for ribbons, false for triangles)
labelArray: string[], // /< Array of labels per element
positionArray: number[], // /< Catenation of x, y, z for each element, 9x as many as triangles (3 vertices per triangle)
colorArray: Color[], // /< Color for each element, as many as elements
breakArray: boolean[], // /< A single boolean per element indicating if there is a break there
pairTriangleNormals: boolean // /< Whether to pair every other triangle normal for lighting (true for ribbons, false for triangles)
}
export interface VectorList extends KinListBase {
label1Array: string[], ///< Array of labels for the first half of each element
label2Array: string[], ///< Array of labels for the second half of each element
position1Array: number[], ///< Catenation of x, y, z for each element, 3x as many as elements
position2Array: number[], ///< Catenation of x, y, z for each element, 3x as many as elements
color1Array: Color[], ///< Color for first half of each element, as many as elements
color2Array: Color[], ///< Color for second half of each element, as many as elements
width: number[] ///< A single width per element
label1Array: string[], // /< Array of labels for the first half of each element
label2Array: string[], // /< Array of labels for the second half of each element
position1Array: number[], // /< Catenation of x, y, z for each element, 3x as many as elements
position2Array: number[], // /< Catenation of x, y, z for each element, 3x as many as elements
color1Array: Color[], // /< Color for first half of each element, as many as elements
color2Array: Color[], // /< Color for second half of each element, as many as elements
width: number[] // /< A single width per element
}
export interface View {
name?: string, ///< Optional name of the View
center?: number[], ///< X, Y, Z of the center of the view; the model rotates around this point
matrix?: number[], ///< Specifies and orthonormal rotation matrix defining view orientation
span?: number, ///< Specifies the (smaller of) width or height of the view in world coordinates at the center
zoom?: number, ///< Alternate zoom specification, indicates how much of the model is visible, 1=all, 2=half
zslab?: number ///< Distance from the center to the near and far clipping planes, 200 means same as span (half is percent of half span)
name?: string, // /< Optional name of the View
center?: number[], // /< X, Y, Z of the center of the view; the model rotates around this point
matrix?: number[], // /< Specifies and orthonormal rotation matrix defining view orientation
span?: number, // /< Specifies the (smaller of) width or height of the view in world coordinates at the center
zoom?: number, // /< Alternate zoom specification, indicates how much of the model is visible, 1=all, 2=half
zslab?: number // /< Distance from the center to the near and far clipping planes, 200 means same as span (half is percent of half span)
}

View File

@@ -12,8 +12,8 @@ import { Structure } from '../../mol-model/structure';
import { StructureRepresentation, StructureRepresentationStateBuilder } from '../../mol-repr/structure/representation';
import { ThemeRegistryContext } from '../../mol-theme/theme';
/// @todo Convert this approach to a more usual one that creates visuals during parse and shows them
/// during visuals.
// TODO: Convert this approach to a more usual one that creates visuals during parse and shows them
// during visuals.
const KinemageDataVisuals = {
};

View File

@@ -19,287 +19,287 @@ import { applyViewSnapshot, rebuildShapesForKinemage } from './behavior';
import { Kinemage } from './reader/schema';
interface KinemageControlState extends CollapsableState {
isBusy: boolean
isBusy: boolean
}
function nameFromString(s: string | undefined) {
// If this is undefined, return undefined.
if (!s) return undefined;
// Return up to the first 30 characters of the string.
return s.length > 30 ? s.substring(0, 30) + '...' : s;
// If this is undefined, return undefined.
if (!s) return undefined;
// Return up to the first 30 characters of the string.
return s.length > 30 ? s.substring(0, 30) + '...' : s;
}
export class KinemageControls extends CollapsableControls<{}, KinemageControlState> {
protected defaultState(): KinemageControlState {
return {
header: 'Kinemage',
isCollapsed: false,
isBusy: false,
// default hidden until a kinemage is present
isHidden: true,
brand: { accent: 'cyan', svg: undefined as any }
};
}
componentDidMount() {
// Listen for shape/state changes: when state tree cells are created or removed the visuals changed.
this.subscribe(this.plugin.state.data.events.cell.created, (e: any) => this.onCellCreated(e));
this.subscribe(this.plugin.state.data.events.cell.removed, () => this.onCellRemoved());
// also track cell state updates that may change labels / visibility
this.subscribe(this.plugin.state.data.events.cell.stateUpdated, () => this.forceUpdate());
// ensure initial visibility reflects current state
this.updateVisibility();
}
private onCellCreated(e: any) {
this.updateVisibility();
}
private onCellRemoved() {
this.updateVisibility();
}
private updateVisibility() {
const kinemages = this.getKinemageList();
this.setState({ isHidden: kinemages.length === 0 });
}
private getKinemageList(): Array<{ kinData: Kinemage, ref: string }> {
const result: Array<{ kinData: Kinemage, ref: string }> = [];
try {
const cells = (this.plugin.state.data as any).cells as Map<string, any>;
for (const [ref, entry] of cells) {
const obj = (entry as any).obj;
// Look for Format.Json nodes that contain kinData
if (obj && obj.data && (obj.data as any).kinData) {
result.push({ kinData: (obj.data as any).kinData, ref });
}
}
} catch (e) {
console.warn('Failed to enumerate kinemage nodes', e);
protected defaultState(): KinemageControlState {
return {
header: 'Kinemage',
isCollapsed: false,
isBusy: false,
// default hidden until a kinemage is present
isHidden: true,
brand: { accent: 'cyan', svg: undefined as any }
};
}
return result;
}
componentDidMount() {
// Listen for shape/state changes: when state tree cells are created or removed the visuals changed.
this.subscribe(this.plugin.state.data.events.cell.created, (e: any) => this.onCellCreated(e));
this.subscribe(this.plugin.state.data.events.cell.removed, () => this.onCellRemoved());
// also track cell state updates that may change labels / visibility
this.subscribe(this.plugin.state.data.events.cell.stateUpdated, () => this.forceUpdate());
private async applyView(kinData: Kinemage, viewKey: string) {
const snap = (kinData as any).viewSnapshots?.[viewKey];
if (snap) {
await applyViewSnapshot(this.plugin, snap as Partial<Camera.Snapshot>);
// ensure initial visibility reflects current state
this.updateVisibility();
}
}
private async toggleVisibility(kinData: Kinemage, kinRef: string, target: { type: 'group' | 'subgroup' | 'master', key: string }) {
try {
if (target.type === 'group') {
const g = kinData.groupDict[target.key];
if (g) g.off = !g.off;
} else if (target.type === 'subgroup') {
const s = kinData.subgroupDict[target.key];
if (s) s.off = !s.off;
} else {
const m = kinData.masterDict[target.key];
if (m) m.visible = !m.visible;
}
// Rebuild shapes for this kinemage using the state ref
await rebuildShapesForKinemage(this.plugin, { ref: kinRef } as any);
this.updateVisibility();
} catch (e) {
console.error('Failed to toggle kinemage visibility', e);
private onCellCreated(e: any) {
this.updateVisibility();
}
}
private async triggerAnimateForKin(kinData: Kinemage, kinRef: string, mode: 'animate' | '2animate') {
try {
if (mode === 'animate') {
kinData.activeAnimateGroup = (kinData.activeAnimateGroup + 1) % Math.max(1, kinData.groupsAnimate.length);
// Make only the active animate group visible, hide the others (if any)
for (let i = 0; i < kinData.groupsAnimate.length; i++) {
const groupName = kinData.groupsAnimate[i];
const groupInfo = kinData.groupDict[groupName];
if (groupInfo) {
groupInfo.off = (i !== kinData.activeAnimateGroup);
}
}
} else {
kinData.activeAnimateGroup2 = (kinData.activeAnimateGroup2 + 1) % Math.max(1, kinData.groupsAnimate2.length);
// Make only the active animate group visible, hide the others (if any)
for (let i = 0; i < kinData.groupsAnimate2.length; i++) {
const groupName = kinData.groupsAnimate2[i];
const groupInfo = kinData.groupDict[groupName];
if (groupInfo) {
groupInfo.off = (i !== kinData.activeAnimateGroup2);
}
}
}
// Rebuild shapes for this kinemage using the state ref
await rebuildShapesForKinemage(this.plugin, { ref: kinRef } as any);
this.updateVisibility();
} catch (e) {
console.error('Failed to trigger animate', e);
private onCellRemoved() {
this.updateVisibility();
}
}
renderControls() {
const kins = this.getKinemageList();
if (kins.length === 0) return <div style={{ padding: '6px' }}>No Kinemage data</div>;
private updateVisibility() {
const kinemages = this.getKinemageList();
this.setState({ isHidden: kinemages.length === 0 });
}
const blocks: React.ReactNode[] = [];
for (const { kinData, ref } of kins) {
const title = kinData.pdbfile || nameFromString(kinData.caption) || 'Kinemage';
const kinBlock: React.ReactNode[] = [];
private getKinemageList(): Array<{ kinData: Kinemage, ref: string }> {
const result: Array<{ kinData: Kinemage, ref: string }> = [];
// Title
kinBlock.push(
<div key={'title-' + title} style={{ padding: '6px', fontWeight: 'bold', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
{title}
</div>
);
// views
const viewEntries = Object.entries(kinData.viewDict || {});
if (viewEntries.length > 0) {
for (const [viewKey, viewObj] of viewEntries) {
const label = `View ${viewObj.name || `View ${viewKey}`}`;
kinBlock.push(
<div key={'view-' + title + '-' + viewKey} style={{ padding: '2px 6px' }}>
<button
className='msp-btn msp-btn-block'
onClick={() => this.applyView(kinData, viewKey)}
title={`Apply view: ${label}`}
>
{label}
</button>
</div>
);
}
}
// animate
if (kinData.groupsAnimate && kinData.groupsAnimate.length > 0) {
kinBlock.push(
<div key={'anim-' + title} style={{ padding: '2px 6px' }}>
<button
className='msp-btn msp-btn-block'
onClick={() => this.triggerAnimateForKin(kinData, ref, 'animate')}
title='Cycle through animation frames'
>
Animate
</button>
</div>
);
}
if (kinData.groupsAnimate2 && kinData.groupsAnimate2.length > 0) {
kinBlock.push(
<div key={'anim2-' + title} style={{ padding: '2px 6px' }}>
<button
className='msp-btn msp-btn-block'
onClick={() => this.triggerAnimateForKin(kinData, ref, '2animate')}
title='Cycle through second animation frames'
>
Animate2
</button>
</div>
);
}
// groups
for (const [groupKey, groupInfo] of Object.entries(kinData.groupDict || {})) {
if (!(groupInfo as any).nobutton) {
const visible = !(groupInfo as any).off;
// If this group is in animate or animate2, then add '*' before its groupKey name to indicate that it's an animation group
const isAnimate = kinData.groupsAnimate?.includes(groupKey) || kinData.groupsAnimate2?.includes(groupKey);
const label = isAnimate ? `* ${groupKey}` : groupKey;
kinBlock.push(
<div key={'group-' + title + '-' + groupKey} style={{ padding: '2px 6px' }}>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<input
type='checkbox'
checked={visible}
onChange={() => this.toggleVisibility(kinData, ref, { type: 'group', key: groupKey })}
style={{ marginRight: '6px' }}
/>
<span title={label}>{label}</span>
</label>
</div>
);
}
// If this group is not dominant, find any subgroups of this group and show them here (indented) unless they have nobutton set
if (!(groupInfo as any).dominant) {
for (const [subgroupKey, subgroupInfo] of Object.entries(kinData.subgroupDict || {})) {
if (subgroupKey.startsWith(groupKey + ':')) {
if ((subgroupInfo as any).nobutton) continue;
const visible = !(subgroupInfo as any).off;
const subgroupLabel = subgroupKey.split(':')[1];
kinBlock.push(
<div key={'subgroup-' + title + '-' + subgroupKey} style={{ padding: '2px 6px', paddingLeft: '24px' }}>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<input
type='checkbox'
checked={visible}
onChange={() => this.toggleVisibility(kinData, ref, { type: 'subgroup', key: subgroupKey })}
style={{ marginRight: '6px' }}
/>
<span title={subgroupLabel}>{subgroupLabel}</span>
</label>
</div>
);
try {
const cells = (this.plugin.state.data as any).cells as Map<string, any>;
for (const [ref, entry] of cells) {
const obj = (entry as any).obj;
// Look for Format.Json nodes that contain kinData
if (obj && obj.data && (obj.data as any).kinData) {
result.push({ kinData: (obj.data as any).kinData, ref });
}
}
}
} catch (e) {
console.warn('Failed to enumerate kinemage nodes', e);
}
}
// subgroups that don't belong to a group (standalone)
for (const [subgroupKey, subgroupInfo] of Object.entries(kinData.subgroupDict || {})) {
// if parent group present, those groups' subgroups are already shown when iterating groups
if (subgroupKey.indexOf(':') !== -1) {
// subgroups with parent group; skip here (shown under parent group)
continue;
}
if ((subgroupInfo as any).nobutton) continue;
const visible = !(subgroupInfo as any).off;
kinBlock.push(
<div key={'subgroup-' + title + '-' + subgroupKey} style={{ padding: '2px 6px' }}>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<input
type='checkbox'
checked={visible}
onChange={() => this.toggleVisibility(kinData, ref, { type: 'subgroup', key: subgroupKey })}
style={{ marginRight: '6px' }}
/>
<span title={subgroupKey}>{subgroupKey}</span>
</label>
</div>
);
}
// masters
for (const [masterKey, masterInfo] of Object.entries(kinData.masterDict || {})) {
const visible = !!(masterInfo && (masterInfo as any).visible);
kinBlock.push(
<div key={'master-' + title + '-' + masterKey} style={{ padding: '2px 6px' }}>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<input
type='checkbox'
checked={visible}
onChange={() => this.toggleVisibility(kinData, ref, { type: 'master', key: masterKey })}
style={{ marginRight: '6px' }}
/>
<span title={masterKey}>{masterKey}</span>
</label>
</div>
);
}
blocks.push(<div key={'kin-block-' + title} className='msp-control-group-wrapper'>{kinBlock}</div>);
return result;
}
return <>{blocks}</>;
}
private async applyView(kinData: Kinemage, viewKey: string) {
const snap = (kinData as any).viewSnapshots?.[viewKey];
if (snap) {
await applyViewSnapshot(this.plugin, snap as Partial<Camera.Snapshot>);
}
}
private async toggleVisibility(kinData: Kinemage, kinRef: string, target: { type: 'group' | 'subgroup' | 'master', key: string }) {
try {
if (target.type === 'group') {
const g = kinData.groupDict[target.key];
if (g) g.off = !g.off;
} else if (target.type === 'subgroup') {
const s = kinData.subgroupDict[target.key];
if (s) s.off = !s.off;
} else {
const m = kinData.masterDict[target.key];
if (m) m.visible = !m.visible;
}
// Rebuild shapes for this kinemage using the state ref
await rebuildShapesForKinemage(this.plugin, { ref: kinRef } as any);
this.updateVisibility();
} catch (e) {
console.error('Failed to toggle kinemage visibility', e);
}
}
private async triggerAnimateForKin(kinData: Kinemage, kinRef: string, mode: 'animate' | '2animate') {
try {
if (mode === 'animate') {
kinData.activeAnimateGroup = (kinData.activeAnimateGroup + 1) % Math.max(1, kinData.groupsAnimate.length);
// Make only the active animate group visible, hide the others (if any)
for (let i = 0; i < kinData.groupsAnimate.length; i++) {
const groupName = kinData.groupsAnimate[i];
const groupInfo = kinData.groupDict[groupName];
if (groupInfo) {
groupInfo.off = (i !== kinData.activeAnimateGroup);
}
}
} else {
kinData.activeAnimateGroup2 = (kinData.activeAnimateGroup2 + 1) % Math.max(1, kinData.groupsAnimate2.length);
// Make only the active animate group visible, hide the others (if any)
for (let i = 0; i < kinData.groupsAnimate2.length; i++) {
const groupName = kinData.groupsAnimate2[i];
const groupInfo = kinData.groupDict[groupName];
if (groupInfo) {
groupInfo.off = (i !== kinData.activeAnimateGroup2);
}
}
}
// Rebuild shapes for this kinemage using the state ref
await rebuildShapesForKinemage(this.plugin, { ref: kinRef } as any);
this.updateVisibility();
} catch (e) {
console.error('Failed to trigger animate', e);
}
}
renderControls() {
const kins = this.getKinemageList();
if (kins.length === 0) return <div style={{ padding: '6px' }}>No Kinemage data</div>;
const blocks: React.ReactNode[] = [];
for (const { kinData, ref } of kins) {
const title = kinData.pdbfile || nameFromString(kinData.caption) || 'Kinemage';
const kinBlock: React.ReactNode[] = [];
// Title
kinBlock.push(
<div key={'title-' + title} style={{ padding: '6px', fontWeight: 'bold', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
{title}
</div>
);
// views
const viewEntries = Object.entries(kinData.viewDict || {});
if (viewEntries.length > 0) {
for (const [viewKey, viewObj] of viewEntries) {
const label = `View ${viewObj.name || `View ${viewKey}`}`;
kinBlock.push(
<div key={'view-' + title + '-' + viewKey} style={{ padding: '2px 6px' }}>
<button
className='msp-btn msp-btn-block'
onClick={() => this.applyView(kinData, viewKey)}
title={`Apply view: ${label}`}
>
{label}
</button>
</div>
);
}
}
// animate
if (kinData.groupsAnimate && kinData.groupsAnimate.length > 0) {
kinBlock.push(
<div key={'anim-' + title} style={{ padding: '2px 6px' }}>
<button
className='msp-btn msp-btn-block'
onClick={() => this.triggerAnimateForKin(kinData, ref, 'animate')}
title='Cycle through animation frames'
>
Animate
</button>
</div>
);
}
if (kinData.groupsAnimate2 && kinData.groupsAnimate2.length > 0) {
kinBlock.push(
<div key={'anim2-' + title} style={{ padding: '2px 6px' }}>
<button
className='msp-btn msp-btn-block'
onClick={() => this.triggerAnimateForKin(kinData, ref, '2animate')}
title='Cycle through second animation frames'
>
Animate2
</button>
</div>
);
}
// groups
for (const [groupKey, groupInfo] of Object.entries(kinData.groupDict || {})) {
if (!(groupInfo as any).nobutton) {
const visible = !(groupInfo as any).off;
// If this group is in animate or animate2, then add '*' before its groupKey name to indicate that it's an animation group
const isAnimate = kinData.groupsAnimate?.includes(groupKey) || kinData.groupsAnimate2?.includes(groupKey);
const label = isAnimate ? `* ${groupKey}` : groupKey;
kinBlock.push(
<div key={'group-' + title + '-' + groupKey} style={{ padding: '2px 6px' }}>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<input
type='checkbox'
checked={visible}
onChange={() => this.toggleVisibility(kinData, ref, { type: 'group', key: groupKey })}
style={{ marginRight: '6px' }}
/>
<span title={label}>{label}</span>
</label>
</div>
);
}
// If this group is not dominant, find any subgroups of this group and show them here (indented) unless they have nobutton set
if (!(groupInfo as any).dominant) {
for (const [subgroupKey, subgroupInfo] of Object.entries(kinData.subgroupDict || {})) {
if (subgroupKey.startsWith(groupKey + ':')) {
if ((subgroupInfo as any).nobutton) continue;
const visible = !(subgroupInfo as any).off;
const subgroupLabel = subgroupKey.split(':')[1];
kinBlock.push(
<div key={'subgroup-' + title + '-' + subgroupKey} style={{ padding: '2px 6px', paddingLeft: '24px' }}>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<input
type='checkbox'
checked={visible}
onChange={() => this.toggleVisibility(kinData, ref, { type: 'subgroup', key: subgroupKey })}
style={{ marginRight: '6px' }}
/>
<span title={subgroupLabel}>{subgroupLabel}</span>
</label>
</div>
);
}
}
}
}
// subgroups that don't belong to a group (standalone)
for (const [subgroupKey, subgroupInfo] of Object.entries(kinData.subgroupDict || {})) {
// if parent group present, those groups' subgroups are already shown when iterating groups
if (subgroupKey.indexOf(':') !== -1) {
// subgroups with parent group; skip here (shown under parent group)
continue;
}
if ((subgroupInfo as any).nobutton) continue;
const visible = !(subgroupInfo as any).off;
kinBlock.push(
<div key={'subgroup-' + title + '-' + subgroupKey} style={{ padding: '2px 6px' }}>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<input
type='checkbox'
checked={visible}
onChange={() => this.toggleVisibility(kinData, ref, { type: 'subgroup', key: subgroupKey })}
style={{ marginRight: '6px' }}
/>
<span title={subgroupKey}>{subgroupKey}</span>
</label>
</div>
);
}
// masters
for (const [masterKey, masterInfo] of Object.entries(kinData.masterDict || {})) {
const visible = !!(masterInfo && (masterInfo as any).visible);
kinBlock.push(
<div key={'master-' + title + '-' + masterKey} style={{ padding: '2px 6px' }}>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<input
type='checkbox'
checked={visible}
onChange={() => this.toggleVisibility(kinData, ref, { type: 'master', key: masterKey })}
style={{ marginRight: '6px' }}
/>
<span title={masterKey}>{masterKey}</span>
</label>
</div>
);
}
blocks.push(<div key={'kin-block-' + title} className='msp-control-group-wrapper'>{kinBlock}</div>);
}
return <>{blocks}</>;
}
}