Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 8e750b1

Browse files
committedFeb 11, 2025
feat(plugin): prep
1 parent a47eaaa commit 8e750b1

File tree

5 files changed

+401
-8
lines changed

5 files changed

+401
-8
lines changed
 
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
Auto-generated by: angular-three-plugin:gltf<% if (size) { %>
3+
Size: <%= size %><% } %><% if (header) { %>
4+
<%= header %><% } %><% if (extras) { %>
5+
<%= extras %><% } %>
6+
*/
7+
8+
import type * as THREE from 'three';
9+
import { Group<%= threeImports %> } from 'three';
10+
import { extend, type NgtThreeElements, NgtObjectEvents<% if (args) { %>, NgtArgs<% } %> } from 'angular-three';
11+
import { Component, ChangeDetectionStrategy, CUSTOM_ELEMENTS_SCHEMA, input, viewChild, ElementRef, inject, effect<% if (animations.length) { %>, computed, model<% } %> } from '@angular/core';
12+
import { injectGLTF } from 'angular-three-soba/loaders';
13+
import type { GLTF } from 'three-stdlib';<% if (animations.length) { %>
14+
import { injectAnimations, type NgtsAnimationClips, type NgtsAnimationApi } from 'angular-three-soba/misc';<% } %><% if (perspective) { %>
15+
import { NgtsPerspectiveCamera } from 'angular-three-soba/cameras';<% } %><% if (orthographic) { %>
16+
import { NgtsOrthographicCamera } from 'angular-three-soba/cameras';<% } %>
17+
<% if (useImportAttribute) { %>
18+
// @ts-expect-error - import .glb/.gltf file
19+
import <%= gltfName %> from '.<%= gltfPath %>' with { loader: 'file' };
20+
<% } %>
21+
<% if (preload) { %>
22+
injectGLTF(() => <% if (useImportAttribute) { %><%= gltfName %><% } else { %>'<%= gltfPath %>'<% } %>);
23+
<% } %>
24+
<% if (animations.length) { %>
25+
type ActionName = <% animations.map(clip => "\""+ clip.name + "\"").join(" | ") %>;
26+
type <%= gltfAnimationTypeName %> = NgtsAnimationClips<ActionName>;
27+
export type <%= gltfAnimationApiTypeName %> = Exclude<NgtsAnimationApi<<%= gltfAnimationTypeName %>>, { get isReady(): false }>;
28+
<% } %>
29+
export type <%= gltfResultTypeName %> = GLTF & {
30+
nodes: {
31+
<% meshes.map(({ name, type }) => "\'" + name + "\'" + ": THREE." + type).join(';\n') %>
32+
<% bones.map(({ name, type }) => "\'" + name + "\'" + ": THREE." + type).join(';\n') %>
33+
};
34+
materials: {
35+
<% materials.map(({ name, type }) => "\'" + name + "\'" + ": THREE." + type).join(';\n') %>
36+
};<% if (animations.length) { %>
37+
animations: <%= gltfAnimationTypeName %>[];<% } %>
38+
};
39+
40+
@Component({
41+
selector: '<%= selector %>',
42+
template: `
43+
@if (gltf(); as gltf) {
44+
<ngt-group #model [parameters]="options()" [dispose]="null">
45+
<%= scene %>
46+
47+
<ng-content />
48+
</ngt-group>
49+
}
50+
`,
51+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
52+
changeDetection: ChangeDetectionStrategy.OnPush,
53+
hostDirectives: [
54+
{
55+
directive: NgtObjectEvents,
56+
outputs: ['click', 'dblclick', 'contextmenu', 'pointerup', 'pointerdown', 'pointerover', 'pointerout', 'pointerenter', 'pointerleave', 'pointermove', 'pointermissed', 'pointercancel', 'wheel'],
57+
},
58+
],<% if (angularImports.length) { %>
59+
imports: [<% angularImports.join(', ') %>]<% } %>
60+
})
61+
export class <%= className %> {
62+
protected readonly Math = Math;
63+
64+
options = input({} as Partial<NgtThreeElements['ngt-group']>);<% if (animations.length) { %>
65+
animations = model<<%= gltfAnimationApiTypeName %>>();<% } %>
66+
67+
modelRef = viewChild<ElementRef<Group>>('model');
68+
69+
protected gltf = injectGLTF<<%= gltfResultTypeName %>>(() => ${options.importattribute && !url.startsWith("http") ? gltfName : `"${url}"`}${gltfOptions ? `, ${JSON.stringify(gltfOptions)}` : ""});
70+
71+
constructor() {
72+
extend({ Group${ngtTypesArr.length ? ", " + ngtTypesArr.join(", ") : ""} });
73+
74+
${
75+
hasAnimations
76+
? `
77+
const animations = injectAnimations(this.gltf, this.modelRef);
78+
effect(() => {
79+
if (animations.${useNewAnimationApi ? "isReady" : "ready()"}) {
80+
this.animations.set(animations);
81+
}
82+
}${options.ngVer < 19 ? ", { allowSignalWrites: true }" : ""})
83+
`
84+
: ""
85+
}
86+
87+
const objectEvents = inject(NgtObjectEvents, { host: true });
88+
effect(() => {
89+
const model = this.modelRef()?.nativeElement;
90+
if (!model) return;
91+
92+
objectEvents.ngtObjectEvents.set(model);
93+
}${options.ngVer < 19 ? ", { allowSignalWrites: true }" : ""});
94+
}
95+
}

‎libs/plugin/src/generators/gltf/files/src/index.ts.template

Lines changed: 0 additions & 1 deletion
This file was deleted.

‎libs/plugin/src/generators/gltf/gltf.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,55 @@
11
import { Tree } from '@nx/devkit';
2+
import { GenerateNGT } from './utils/generate-ngt';
23

3-
export interface GltfGeneratorSchema {}
4+
export interface GltfGeneratorSchema {
5+
modelPath: string;
6+
output: string;
7+
className: string;
8+
selectorPrefix: string;
9+
draco: boolean;
10+
bones: boolean;
11+
meta: boolean;
12+
shadows: boolean;
13+
precision: number;
14+
console: boolean;
15+
instance: boolean;
16+
instanceAll: boolean;
17+
resolution: number;
18+
keepMeshes: boolean;
19+
keepMaterials: boolean;
20+
keepAttributes: boolean;
21+
format: string;
22+
simplify: boolean;
23+
ratio: number;
24+
error: number;
25+
verbose: boolean;
26+
}
427

528
export async function gltfGenerator(tree: Tree, options: GltfGeneratorSchema) {
6-
const test = await import('@rosskevin/gltfjsx');
7-
console.log(test);
29+
const { loadGLTF, AnalyzedGLTF, gltfTransform, Log, allPruneStrategies } = await import('@rosskevin/gltfjsx');
30+
31+
const gltf = await loadGLTF('');
32+
33+
const analyzed = new AnalyzedGLTF(
34+
gltf,
35+
{
36+
log: new Log({ debug: options.verbose, silent: false }),
37+
bones: options.bones,
38+
meta: options.meta,
39+
shadows: options.shadows,
40+
instance: options.instance,
41+
instanceall: options.instanceAll,
42+
keepgroups: false,
43+
keepnames: true,
44+
precision: options.precision,
45+
},
46+
allPruneStrategies,
47+
);
48+
49+
const generateNGT = new GenerateNGT(analyzed, options);
50+
51+
const test = await generateNGT.print(analyzed.gltf.scene);
52+
853
// await formatFiles(tree);
954
}
1055

‎libs/plugin/src/generators/gltf/schema.json

Lines changed: 127 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,138 @@
33
"title": "Generate component from GLTF",
44
"type": "object",
55
"properties": {
6-
"name": {
6+
"modelPath": {
77
"type": "string",
8-
"description": "",
8+
"description": "GLTF model path",
9+
"alias": "path",
910
"$default": {
1011
"$source": "argv",
1112
"index": 0
1213
},
13-
"x-prompt": "What name would you like to use?"
14+
"x-prompt": "Path to GLTF model from workspace root?"
15+
},
16+
"output": {
17+
"type": "string",
18+
"description": "Output path",
19+
"alias": "o",
20+
"x-prompt": "Where to output the component?"
21+
},
22+
"className": {
23+
"type": "string",
24+
"description": "Name to use for the generated component class",
25+
"alias": "C",
26+
"default": "Model",
27+
"x-prompt": "Enter the name of the component class to generate (default: Model)"
28+
},
29+
"selectorPrefix": {
30+
"type": "string",
31+
"description": "Prefix to use for the generated component selector",
32+
"alias": "pre",
33+
"default": "app",
34+
"x-prompt": "Enter the prefix to use for the generated component selector (default: app)"
35+
},
36+
"draco": {
37+
"type": "boolean",
38+
"description": "Use DracoLoader",
39+
"alias": "d",
40+
"default": false
41+
},
42+
"bones": {
43+
"type": "boolean",
44+
"description": "Layout Bones declaratively",
45+
"alias": "b",
46+
"default": false
47+
},
48+
"meta": {
49+
"type": "boolean",
50+
"decsription": "Include metadata (as userData)",
51+
"alias": "m",
52+
"default": false
53+
},
54+
"shadows": {
55+
"type": "boolean",
56+
"description": "Let mesh cast and receive shadows",
57+
"alias": "s",
58+
"default": false
59+
},
60+
"precision": {
61+
"type": "number",
62+
"description": "Number of fractional digits (default: 3)",
63+
"alias": "p",
64+
"default": 3
65+
},
66+
"console": {
67+
"type": "boolean",
68+
"description": "Prints to console",
69+
"alias": "c",
70+
"default": false
71+
},
72+
"instance": {
73+
"type": "boolean",
74+
"description": "Instance re-occuring geometry",
75+
"alias": "i",
76+
"default": false
77+
},
78+
"instanceAll": {
79+
"type": "boolean",
80+
"description": "Instance every geometry (for cheaper re-use)",
81+
"alias": "I",
82+
"default": false
83+
},
84+
"resolution": {
85+
"type": "number",
86+
"description": "Resolution for texture resizing (default: 1024)",
87+
"alias": "R",
88+
"default": 1024
89+
},
90+
"keepMeshes": {
91+
"type": "boolean",
92+
"description": "Do not join compatible meshes",
93+
"alias": "j",
94+
"default": false
95+
},
96+
"keepMaterials": {
97+
"type": "boolean",
98+
"description": "Do not palette join materials",
99+
"alias": "M",
100+
"default": false
101+
},
102+
"keepAttributes": {
103+
"type": "boolean",
104+
"description": "Whether to keep unused vertex attributes, such as UVs without an assigned texture",
105+
"alias": "a",
106+
"default": false
107+
},
108+
"format": {
109+
"type": "string",
110+
"description": "Texture format jpeg | png | webp | avif (default: \"webp\")",
111+
"alias": "f",
112+
"default": "webp"
113+
},
114+
"simplify": {
115+
"type": "boolean",
116+
"description": "Mesh simplification (default: false)",
117+
"alias": "S",
118+
"default": false
119+
},
120+
"ratio": {
121+
"type": "number",
122+
"description": "Simplifier ratio (default: 0)",
123+
"alias": "r",
124+
"default": 0
125+
},
126+
"error": {
127+
"type": "number",
128+
"description": "Simplifier error threshold (default: 0.0001)",
129+
"alias": "e",
130+
"default": 0.0001
131+
},
132+
"verbose": {
133+
"type": "boolean",
134+
"description": "Verbose log",
135+
"alias": "debug",
136+
"default": false
14137
}
15138
},
16-
"required": ["name"]
139+
"required": ["modelPath", "output"]
17140
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import type { Object3D } from 'three';
2+
import type { GltfGeneratorSchema } from '../gltf';
3+
4+
export class GenerateNGT {
5+
ngtTypes = new Set<string>();
6+
args = false;
7+
8+
constructor(
9+
// @ts-expect-error - type only import
10+
private analyzedGLTF: import('@rosskevin/gltfjsx').AnalyzedGLTF,
11+
private options: GltfGeneratorSchema,
12+
) {}
13+
14+
async print(obj: Object3D) {
15+
const { nodeName, isRemoved, isChildless, isTargetedLight, isInstancedMesh, sanitizeName } = await import(
16+
'@rosskevin/gltfjsx'
17+
);
18+
19+
let result = '';
20+
let children = '';
21+
22+
if (isRemoved(obj) && !isChildless(obj)) {
23+
obj.children.forEach((child) => (result += this.print(child)));
24+
return result;
25+
}
26+
27+
const { bones } = this.options;
28+
const node = nodeName(obj);
29+
const type = this.getType(obj);
30+
31+
// Bail out on bones
32+
if (!bones && type === 'bone') {
33+
this.args = true;
34+
return `<ngt-primitive *args=[gltf.${node}] />\n`;
35+
}
36+
37+
const ngtType = this.getAngularThreeElement(type);
38+
if (isTargetedLight(obj)) {
39+
this.args = true;
40+
return `<${ngtType} ${this.handleAngularInputs(obj)} [target]="gltf.${node}.target">
41+
<ngt-primitive *args="[gltf.${node}.target]" ${this.handleAngularInputs(obj.target)} />
42+
</${ngtType}>`;
43+
}
44+
45+
// Collect children
46+
if (obj.children) obj.children.forEach((child) => (children += this.print(child)));
47+
48+
// TODO: Instances are currently not supported for NGT components
49+
50+
if (isInstancedMesh(obj)) {
51+
const geo = `gltf.${node}.geometry`;
52+
const mat =
53+
'name' in obj.material ? `gltf.materials${sanitizeName(obj.material.name)}` : `gltf.${node}.material`;
54+
this.args = true;
55+
result = `<ngt-instanced-mesh *args="[${geo}, ${mat}, ${!obj.count ? `gltf.${node}.count` : obj.count}]" `;
56+
} else {
57+
if (type === 'bone') {
58+
this.args = true;
59+
result = `<ngt-primitive *args="[gltf.${node}]" `;
60+
} else {
61+
result = `<${this.getAngularThreeElement(type)} `;
62+
}
63+
}
64+
65+
if (
66+
obj.name.length &&
67+
'morphTargetDictionary' in obj &&
68+
!!obj.morphTargetDictionary &&
69+
this.analyzedGLTF.hasAnimations()
70+
) {
71+
result += `name="${obj.name}" `;
72+
}
73+
74+
const oldResult = result;
75+
result += this.handleAngularInputs(obj);
76+
return result;
77+
}
78+
79+
private handleAngularInputs(obj: Object3D) {
80+
return '';
81+
}
82+
83+
private getType(obj: Object3D) {
84+
let type = obj.type.charAt(0).toLowerCase() + obj.type.slice(1);
85+
86+
if (type === 'object3D') {
87+
type = 'group';
88+
this.ngtTypes.add('Group');
89+
}
90+
91+
if (type === 'perspectiveCamera') type = 'PerspectiveCamera';
92+
if (type === 'orthographicCamera') type = 'OrthographicCamera';
93+
94+
this.ngtTypes.add(obj.type);
95+
96+
return type;
97+
}
98+
99+
/**
100+
* Transforms a type like "mesh" into "ngt-mesh".
101+
* @param {string} type
102+
* @returns
103+
*/
104+
private getAngularThreeElement(type: string) {
105+
if (type === 'object3D') {
106+
return 'ngt-object3D';
107+
}
108+
109+
if (type === 'lOD') {
110+
return 'ngt-lod';
111+
}
112+
113+
if (type === 'perspectiveCamera') {
114+
return `ngts-perspective-camera`;
115+
}
116+
117+
if (type === 'orthographicCamera') {
118+
return `ngts-orthographic-camera`;
119+
}
120+
121+
const kebabType = type.replace(/([A-Z])/g, '-$1').toLowerCase();
122+
return `ngt-${kebabType}`;
123+
}
124+
}
125+
126+
// export async function generateNGT(
127+
// // @ts-expect-error - type only import
128+
// analyzedGLTF: import('@rosskevin/gltfjsx').AnalyzedGLTF,
129+
// ) {}
130+
//
131+
// function generate(obj: Object3D) {}

0 commit comments

Comments
 (0)
Please sign in to comment.