Open javagl opened 1 year ago
Agreed, the documentation covers the API broadly but is lacking in tutorials or cookbooks at the moment. Related topics: #808, #864.
... "Emitter" example there is not really complete. The ExtensionProperty does not have a type parameter (so there should be an IEmitter interface definition)
This example is written in JavaScript, not TypeScript. You're free to write extensions in either language. Like all TypeScript syntax, the IEmitter interface is purely a compile-time concept — it does not exist at runtime, and does not need to be provided if implementing the extension in JavaScript. I generally use JavaScript in examples to keep things simpler for readers, but perhaps it has the opposite effect here. Editors like VSCode offer excellent type hints with TypeScript, which I've found helpful in this project.
... a specific example is that of the role of the TextureInfo that is associated with a Texture...
This is probably beyond what an extension cookbook can cover, though perhaps Concepts should cover it. The TextureInfo class represents data from the glTF specification's own "TextureInfo" and "Sampler" property types. A Material (or Material extension) holds references to Texture and TextureInfo pairs, instead of the Material -> TextureInfo -> Texture nesting of the glTF specification.
A somewhat "higher-level" question about custom extensions (that could also be mentioned briefly in an extended documentation section about extensions in general):
Iff someone implements support for a custom extension, what is the preferred way of dealing with that? Should/could such an implementation eventually become a PR into the extensions
package? Or should/will there be something like a third-party-extensions
package? Or is the preferred way to ~"just publish it as an own package in npm"?
Discussed briefly under "roadmap" in the contributing guide, and agree it would make sense in a cookbook too —
Most official Khronos Group extensions (those prefixed with KHR) will be implemented on a rolling basis — pull requests are very welcome. Multi-vendor extensions (prefixed with EXT) may be included on a case-by-case basis. Single-vendor extensions (any other prefix) are unlikely to be included directly in the project, although glTF-Transform does provide APIs to build and maintain implementations for those extensions externally.
Some implementations for NEEDLE, OMI, WIRED, and unratified KHR extensions exist on NPM. I've been meaning to keep a list of those, but for now I usually search npm. It's very easy to pull these extensions from NPM into a script-based pipeline with glTF Transform. Using them in the glTF Transform CLI is more experimental.
It's probably difficult to establish a guideline that covers the criteria that are relevant for the "case-by-case" decision. Maybe also because there's not even a clear process about how extensions end up in the Khronos repo to begin with.
A first, very drafty state of the implementation of EXT_mesh_features
is currently at https://github.com/CesiumGS/3d-tiles-tools/blob/8c64997786546b224819cbfc7599fe17ad1746ba/src/contentProcessing/gltftransform , including the comments that I quoted above. If you have time to quickly skim over that, to see whether something stands out as being completely and utterly wrong, that would be great. (What's with these init
methods, for example?). Otherwise, I would (test and clean that up as part of the broader task, and) consider to pull out the comments into a PR for some sort of "cookbook" section in /docs/.../extensions.md
.
If you have time to quickly skim over that, ...
No red flags that I see, a few quick thoughts:
getDefaults()
, like an empty array for channels.What's with these init methods, for example?
The reason for init()
has to do with JavaScript's inheritance model, as demonstrated here. If we just declare extension properties as class fields or in the subclass constructor, they won't be available when the parent constructor runs, and we need some of them to correctly initialize the class. It's a bit ugly, but declaring them in the class body, and defining them in init()
gets around that issue, because the parent constructor invokes init()
when it needs them:
... criteria that are relevant for the "case-by-case" decision ...
Definitely subjective, but mostly comes down to (1) does this feel like something I can easily maintain, and (2) do I have a positive impression of the extension itself. I tend to view each implementation of an EXT extension as an "upvote" for that thing eventually becoming a KHR extension, so I would not necessarily implement something I view as niche, harmful, or not ready for use.
Carefully avoiding to further talk about your impression about an extension that 'you' proposed ;-), and focussing on the init
question:
I expected ~"something like that". There are technical subtleties of property/field initializations, particularly when inheritance comes into play. I mainly know that from other languages (e.g. ones where adding the final
keyword can dramatically change certain results). But I'm not familiar enought with the TypeScript/JavaScript class compilation/initialization behavior to know the exact effects of certain combinations of the readonly
, abstract
, declare
, or override
keywords...
From a naive, high-level, perspective, and looking at the code part that you quoted and the redundancy in
public declare propertyType: 'InstancedMesh';
this. propertyType = 'IntsancedMesh';
public declare parentTypes: [PropertyType.NODE];
this. parentTypes = [PropertyType.NODE];
I had to wonder whether it wasn't possible to avoid one of them. That typo is not part of the quote, but intentionally inserted to emphasize that point.
Specifically, I wondered why it was necessary to store these things as properties in the first place. Declaring (abstract
!) methods for accessing these values in the base class could solve that: Extending classes have to implement that, and can return a fixed value (so I'd not expect any noteworthy performance implications).
FWIW, I played with three versions of such classes:
init
(that shows the wrong behavior of the property being undefined
in the constructor)init
(similar to what is currently done in glTF-Transform
)There's that special case of the static
property that adds another dimension of complexity, but I don't know whether the fact that this is not initialized (yet) in the constructor is relevant here (that is also the case for the one with the init
method, so it might not be).
The output of the following test is
Without init:
Static name in base without init is undefined
Static name in derived without init is EXAMPLE_PROPERTY_WITHOUT_INIT
Instance name without init is undefined (that's wrong)
With init
Static name in base with init is undefined
Static name in derived with init is EXAMPLE_PROPERTY_WITH_INIT
Instance name with init is ExamplePropertyWithInit (that's right)
With getter
Static name in base with getter is undefined
Static name in derived with getter is EXAMPLE_PROPERTY_WITH_GETTER
Instance name with getter is ExamplePropertyWithGetter (that's also right)
from the code
//============================================================================
// Without init
abstract class ExtensionPropertyWithoutInit {
public static EXTENSION_NAME: string;
public abstract readonly extensionName: string;
constructor() {
console.log(
"Static name in base without init is " +
ExtensionPropertyWithoutInit.EXTENSION_NAME
);
}
}
class ExamplePropertyWithoutInit extends ExtensionPropertyWithoutInit {
static override EXTENSION_NAME = "EXAMPLE_PROPERTY_WITHOUT_INIT";
public declare extensionName: string;
constructor() {
super();
console.log(
"Static name in derived without init is " +
ExamplePropertyWithoutInit.EXTENSION_NAME
);
console.log(
"Instance name without init is " + this.extensionName + " (that's wrong)"
);
}
}
//============================================================================
// With init
abstract class ExtensionPropertyWithInit {
public static EXTENSION_NAME: string;
public abstract readonly extensionName: string;
constructor() {
this.init();
console.log(
"Static name in base with init is " +
ExtensionPropertyWithInit.EXTENSION_NAME
);
}
abstract init(): void;
}
class ExamplePropertyWithInit extends ExtensionPropertyWithInit {
static override EXTENSION_NAME = "EXAMPLE_PROPERTY_WITH_INIT";
public declare extensionName: string;
constructor() {
super();
console.log(
"Static name in derived with init is " +
ExamplePropertyWithInit.EXTENSION_NAME
);
console.log(
"Instance name with init is " + this.extensionName + " (that's right)"
);
}
init() {
this.extensionName = "ExamplePropertyWithInit";
}
}
//============================================================================
// With getter
abstract class ExtensionPropertyWithGetter {
public static readonly EXTENSION_NAME: string;
constructor() {
console.log(
"Static name in base with getter is " +
ExtensionPropertyWithGetter.EXTENSION_NAME
);
}
abstract getExtensionName(): string;
}
class ExamplePropertyWithGetter extends ExtensionPropertyWithGetter {
static override readonly EXTENSION_NAME = "EXAMPLE_PROPERTY_WITH_GETTER";
constructor() {
super();
console.log(
"Static name in derived with getter is " +
ExamplePropertyWithGetter.EXTENSION_NAME
);
console.log(
"Instance name with getter is " +
this.getExtensionName() +
" (that's also right)"
);
}
override getExtensionName(): string {
return "ExamplePropertyWithGetter";
}
}
function run() {
console.log("Without init:");
new ExamplePropertyWithoutInit();
console.log("With init");
new ExamplePropertyWithInit();
console.log("With getter");
new ExamplePropertyWithGetter();
}
run();
But...
... the exact effects of certain combinations of the readonly, abstract, declare, or override keywords
The important thing really is that none of them affect the compiled code – only whether your compiler shows you warnings, or does not.
I had to wonder whether it wasn't possible to avoid one of them.
I have certainly spent time trying. :) I'd hoped that public class fields might work, but they have the same behavior. I'm still hoping something like that might exist someday, or that typed JavaScript type annotations might have cleaner semantics.
I agree that methods would also solve the problem, but it doesn't necessarily feel better to me. Semantically I would prefer that these be properties, anything else could raise the same questions you're asking of init()
. The extra few lines have not affected maintenance so far.
Getters (the language feature, not a method) would also be an option:
class B extends A {
get type () { return 'B' }
}
const b = new B();
console.log( b.type );
Possibly an extension could do this now, without defining init, and it would just work? Just unsure if this would satisfy TypeScript without more changes upstream.
Carefully avoiding to further talk about your impression about an extension that 'you' propsed
Well I don't think it is harmful, at least. 😅 On complexity and broader appeal I am not sure yet, probably a question of "when" not "if".
Getters (the language feature, not a method) would also be an option:
From a quick test, ...
abstract get extensionName () : string;
...
override get extensionName(): string { ... }
...
console.log("Instance name with getter is " + this.extensionName + " (that's also right)");
//==============================================================^ no () here!
seems to work as well, but all that with a few uncertainties about the exact behavior.
(Completely subjective: I don't like these get
getters. You never know what they do. At the 'call site', x = foo.example
might be a plain assignment of a plain property value, or a call to a get example()
function that could do anything, and there is no rasonable way to distinguish that by just reading the code - but that's a somewhat unrelated rant about the quirks of JS...)
Haha yes. I would consider it bad practice for a getter to do anything other than returning a value, or perhaps lazy initialization of that value. Most common use case I've found is that the getter simply returns a value, the setter sets a value and also marks a dirty flag on the instance or something like that. Anything more is a bit too magical for me. :)
This refers to the documentation and implementation of custom extensions, as described in the "Custom Extensions" section at https://gltf-transform.donmccurdy.com/extensions . This is not directly (or not only) a technical "feature request", so here is only a short summary of the "Feature Request" template points:
The context is that I tried to implement basic support for the
EXT_mesh_features
extension. And even though this extension is (in its current form) structurally relatively simple, it was not sooo easy to implement support for that.Referring to the "Custom Extensions" section on the website, created from this MD file: The "Emitter" example there is not really complete. The
ExtensionProperty
does not have a type parameter (so there should be anIEmitter
interface definition). There are some issues with the properties and static properties of theExtensionProperty
class. (The details might be related to my cluelessness about TypeScript, technical details aboutdeclare
andoverride
and class initialization and whatnot, but ... for example, I wondered what thatinit
method does, actually...). And beyond that, certain concepts may be obvious for someone who created them, and has implemented several extensions, and knows the underlying mechanisms ofproperty-graph
for the same reasons. But for others, "understanding" certain concepts just by looking at the examples may be difficult. A specific example is that of the role of theTextureInfo
that is associated with aTexture
. Yeah, it is used in many examples, but ... what is this about, exactly?As a hopefully more constructive note: I started implementing that extension. And there are some preliminary notes that I dropped into my local files. These may end up in a similar form in the implementation, eventually. But they are written in a somewhat generic form, and thus, could count as some sort of "cookbook"/"tutorial":
For the
...Def
interfaces that define the JSON structure:For the
I...
interfaces that define the structure of the "model" classes (but that are not exposed!)For the actual "model" classes that are visible to the user:
Further comments or hints could refer to the actual
Extension
class. This could be things that are "simple" in hindight - for example, point out that it hascreate...
methods for the "model" classes, and how they are used in theread
method. But it may also refer to information about where and why to callcontext.setTextureInfo
, or details, like what exactly is happening in a line likefrom one example implementation.
On a more technical side:
It would be nice if the redundancy between the
Something
/ISomething
/SomethingDef
definitions could be reduced. Or if something like the line about thetexture
above could be hidden behind someconst texture = context.getTextureFor(textureInfoDef)
or so. But I due to a lack of background knowledge, I sometimes have to assume that there is no easier solution for certain things, so this is just a vague "Feature Wish" and not a "Feature Request" for now.