@gltf-transform/extensions
Version:
Adds extension support to @gltf-transform/core
181 lines (157 loc) • 5.27 kB
text/typescript
import { ExtensionProperty, type IProperty, type Nullable, PropertyType } from '@gltf-transform/core';
import { KHR_XMP_JSON_LD } from '../constants.js';
type Term = string;
type TermDefinition = string | Record<string, string>;
type Value = string | number | boolean;
const PARENT_TYPES: PropertyType[] = [
PropertyType.ROOT,
PropertyType.SCENE,
PropertyType.NODE,
PropertyType.MESH,
PropertyType.MATERIAL,
PropertyType.TEXTURE,
PropertyType.ANIMATION,
];
interface IPacket extends IProperty {
// https://json-ld.org/spec/latest/json-ld/#the-context
context: Record<Term, TermDefinition>;
properties: Record<string, Value | Record<string, unknown>>;
}
/**
* Defines an XMP packet associated with a Document or Property. See {@link KHRXMP}.
*/
export class Packet extends ExtensionProperty<IPacket> {
public declare propertyType: 'Packet';
public declare parentTypes: typeof PARENT_TYPES;
public declare extensionName: typeof KHR_XMP_JSON_LD;
public static EXTENSION_NAME: typeof KHR_XMP_JSON_LD = KHR_XMP_JSON_LD;
protected init(): void {
this.extensionName = KHR_XMP_JSON_LD;
this.propertyType = 'Packet';
this.parentTypes = PARENT_TYPES;
}
protected getDefaults(): Nullable<IPacket> {
return Object.assign(super.getDefaults(), { context: {}, properties: {} });
}
/**********************************************************************************************
* Context.
*/
/**
* Returns the XMP context definition URL for the given term.
* See: https://json-ld.org/spec/latest/json-ld/#the-context
* @param term Case-sensitive term. Usually a concise, lowercase, alphanumeric identifier.
*/
public getContext(): Record<Term, TermDefinition> {
return this.get('context');
}
/**
* Sets the XMP context definition URL for the given term.
* See: https://json-ld.org/spec/latest/json-ld/#the-context
*
* Example:
*
* ```typescript
* packet.setContext({
* dc: 'http://purl.org/dc/elements/1.1/',
* model3d: 'https://schema.khronos.org/model3d/xsd/1.0/',
* });
* ```
*
* @param term Case-sensitive term. Usually a concise, lowercase, alphanumeric identifier.
* @param definition URI for XMP namespace.
*/
public setContext(context: Record<Term, TermDefinition>): this {
return this.set('context', { ...context });
}
/**********************************************************************************************
* Properties.
*/
/**
* Lists properties defined in this packet.
*
* Example:
*
* ```typescript
* packet.listProperties(); // → ['dc:Language', 'dc:Creator', 'xmp:CreateDate']
* ```
*/
public listProperties(): string[] {
return Object.keys(this.get('properties'));
}
/**
* Returns the value of a property, as a literal or JSONLD object.
*
* Example:
*
* ```typescript
* packet.getProperty('dc:Creator'); // → {"@list": ["Acme, Inc."]}
* packet.getProperty('dc:Title'); // → {"@type": "rdf:Alt", "rdf:_1": {"@language": "en-US", "@value": "Lamp"}}
* packet.getProperty('xmp:CreateDate'); // → "2022-01-01"
* ```
*/
public getProperty(name: string): Value | Record<string, unknown> | null {
const properties = this.get('properties');
return name in properties ? properties[name] : null;
}
/**
* Sets the value of a property, as a literal or JSONLD object.
*
* Example:
*
* ```typescript
* packet.setProperty('dc:Creator', {'@list': ['Acme, Inc.']});
* packet.setProperty('dc:Title', {
* '@type': 'rdf:Alt',
* 'rdf:_1': {'@language': 'en-US', '@value': 'Lamp'}
* });
* packet.setProperty('model3d:preferredSurfaces', {'@list': ['vertical']});
* ```
*/
public setProperty(name: string, value: Value | Record<string, unknown>): this {
this._assertContext(name);
const properties = { ...this.get('properties') };
if (value) {
properties[name] = value;
} else {
delete properties[name];
}
return this.set('properties', properties);
}
/**********************************************************************************************
* Serialize / Deserialize.
*/
/**
* Serializes the packet context and properties to a JSONLD object.
*/
public toJSONLD(): Record<string, unknown> {
const context = copyJSON(this.get('context'));
const properties = copyJSON(this.get('properties'));
return { '@context': context, ...properties };
}
/**
* Deserializes a JSONLD packet, then overwrites existing context and properties with
* the new values.
*/
public fromJSONLD(jsonld: Record<string, unknown>): this {
jsonld = copyJSON(jsonld);
// Context.
const context = jsonld['@context'] as Record<Term, TermDefinition>;
if (context) this.set('context', context);
delete jsonld['@context'];
// Properties.
return this.set('properties', jsonld as Record<string, string | Record<string, unknown>>);
}
/**********************************************************************************************
* Validation.
*/
/** @hidden */
private _assertContext(name: string) {
const prefix = name.split(':')[0];
if (!(prefix in this.get('context'))) {
throw new Error(`${KHR_XMP_JSON_LD}: Missing context for term, "${name}".`);
}
}
}
function copyJSON<T>(object: T): T {
return JSON.parse(JSON.stringify(object));
}