-
Notifications
You must be signed in to change notification settings - Fork 34
Fix: Support custom asset loading for GSplat (getter-only props) #316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -43,6 +43,7 @@ export type PropValidator<T, InstanceType> = { | |||||
| errorMsg: (value: unknown) => string; | ||||||
| default: T | unknown; | ||||||
| apply?: (container: InstanceType, props: Record<string, unknown>, key: string) => void; | ||||||
| readOnly?: boolean; | ||||||
| } | ||||||
|
|
||||||
| // A more generic schema type that works with both functions | ||||||
|
|
@@ -219,37 +220,36 @@ export function validatePropsWithDefaults<T extends object, InstanceType>( | |||||
| * @param props The props to apply | ||||||
| */ | ||||||
| export function applyProps<T extends Record<string, unknown>, InstanceType>( | ||||||
| instance: InstanceType, | ||||||
| schema: Schema<T, InstanceType>, | ||||||
| instance: InstanceType, | ||||||
| schema: Schema<T, InstanceType>, | ||||||
| props: T | ||||||
| ) { | ||||||
| Object.entries(props as Record<keyof T, unknown>).forEach(([key, value]) => { | ||||||
| if (key in schema) { | ||||||
| const propDef = schema[key as keyof T] as PropValidator<T[keyof T], InstanceType>; | ||||||
| if (propDef) { | ||||||
| if (propDef.apply) { | ||||||
| // Use type assertion to satisfy the type checker | ||||||
| propDef.apply(instance, props, key as string); | ||||||
| } else { | ||||||
| try { | ||||||
| (instance as Record<string, unknown>)[key] = value; | ||||||
| } catch (error) { | ||||||
| console.error(`Error applying prop ${key}: ${error}`); | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| }); | ||||||
| } | ||||||
| ) { | ||||||
| Object.entries(props).forEach(([key, value]) => { | ||||||
| if (!(key in schema)) return; | ||||||
| const propDef = schema[key] as PropValidator<T[keyof T], InstanceType> | undefined; | ||||||
| if (!propDef || propDef.readOnly) return; | ||||||
|
|
||||||
| if (propDef.apply) { | ||||||
| propDef.apply(instance, props, key as string); | ||||||
| } else { | ||||||
| try { | ||||||
| (instance as Record<string, unknown>)[key] = value; | ||||||
| } catch (error) { | ||||||
| console.error(`Error applying prop ${key}:`, error); | ||||||
| } | ||||||
| } | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
|
||||||
Copilot
AI
Mar 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The closing brace of the PropertyInfo type is indented with 2 spaces ( };) while the type body uses 4-space indentation. This is inconsistent with the rest of the file which uses 4-space indentation for type declarations. The closing brace should be at column 0, consistent with how PropValidator and Schema types are defined in this file.
| }; | |
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I normally have a lint script run on commit so I must have simply forgot to run the linter explicitly here should be a simple linter run to adapt some of the inline consistency.
Copilot
AI
Mar 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getter-only detection in createComponentDefinition uses Object.getOwnPropertyDescriptor((instance as any).constructor.prototype, key), which only looks at the direct prototype of the constructor. If a getter-only property is inherited from a grandparent class (not defined on the immediate prototype but on a higher ancestor), this check will return undefined and the property will not be marked as readOnly.
Note that getPseudoPublicProps has the same limitation (only traverses one prototype level), so such a property would likely not be included in the schema at all — meaning the current code is consistent. However, this is a latent bug: if getPseudoPublicProps is ever extended to walk the full prototype chain, the getter-only detection here would need to be updated to match. Consider using a prototype-walking loop (similar to how getPseudoPublicProps could be extended) or using Object.getOwnPropertyDescriptor on each level of the prototype chain to be safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was actually one concern with integration and why I decided to go with early exits at the beginning. I would personally suggest downstream making a test case for grandparent classes ( if its a point of interest ) and simply run it to detect any fault lines in the class structure for the future.
As of writing this there are ZERO explicit grandparent classes in the traditional inheritance sense for the schema generator. Instead, the current system uses a composition-based pattern with type definitions and factory functions. So this is a misguided concern and Copilot didn't really do its due diligence here.
Copilot
AI
Mar 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For schema entries that have an apply callback (Color, Vec2, Vec3, Vec4, Quat, Array, Null), the readOnly flag is not set on the schema entry itself — unlike types without an apply callback (Mat4, number, string, boolean, Material) which correctly spread ...(propertyInfo.readOnly && { readOnly: true }).
This inconsistency means:
- The outer guard in
applyProps(if (!propDef || propDef.readOnly) return) will never skip these props at the top level even when the property is getter-only, becausepropDef.readOnlyisundefinedfor these types. - Any external code inspecting
propDef.readOnlyto determine if a prop is read-only (e.g. for docs/tooling) will get incorrect results for getter-only Color, Vec2, Vec3, Vec4, Quat, Array, or Null props.
The early-return inside the apply callback does prevent the actual write, so no exception is thrown — but the approach is not consistent with the rest of the implementation. The schema entries for these types should also spread ...(propertyInfo.readOnly && { readOnly: true }) so propDef.readOnly is reliable as a public indicator of read-only status.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main goal was to implement early returns as I did not want to disrupt any preexisting code that may have rely on the type failure to catch I couldn't find anything that did this but my aim was to make the change with as little potential ripples as possible. The early exit is the simplest and easiest fix besides simply skipping getter only properties but, I wanted to provide a read-only property implementation as a proof of concept for further development. For types without an apply callback implementing the read-only property for consistency made sense by doing a simple append to the schema as the previous logic was based off this universal 'apply all' idea which is flawed.
This boils down to 2 things:
-
Firstly the segment of code addressing apply callbacks is the essential bugfix here and I can definitely see fully implementing the read-only property to the schema being beneficial. It likely would become a much bigger change that I am willing to undertake but wanted to wait for approval as insurance.
-
Secondly the read-only being a public indicator would allow for both custom debugging / a more informative error message so rather than 'is getter only' pops up something like property is of read only or more informative messages involving class access can be provided through the property as a point of reference.
NOTE: Testing can and should use this read-only property especially for asset creation it is likely required to revisit some older tests and add some insurance guards. I ran the test module and everything passed quite nicely however it passing nicely also likely means that the testing scripts should be more strict with class access properties as support for flexibility of asset expression IMO should be a test in and of itself as the original engine addresses custom assets cleanly and as a wrapper for the engine should ensure that feature carries over.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new
readOnlyguard inapplyPropsand the getter-only detection increateComponentDefinitionare the core of this fix but have no test coverage. Given that the codebase has component-level tests (e.g.,Screen.test.tsx,Script.test.tsx) and the fix is non-trivial, a test should be added to verify that: (1) a component schema entry for a getter-only prop hasreadOnly: true(for primitive/Mat4/Material types), (2)applyPropsdoes not throw or attempt to assign when a schema entry hasreadOnly: true, and (3) the original failing scenario (passing a customassetto<GSplat />) works without throwing.