@scenoco-three/compiler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +373 -0
- package/dist/cli/templates.d.ts +11 -0
- package/dist/cli/templates.js +401 -0
- package/dist/components.d.ts +43 -0
- package/dist/components.js +181 -0
- package/dist/dsl/SceneParser.d.ts +30 -0
- package/dist/dsl/SceneParser.js +59 -0
- package/dist/dsl/Validator.d.ts +15 -0
- package/dist/dsl/Validator.js +331 -0
- package/dist/dsl/bundle.d.ts +9 -0
- package/dist/dsl/bundle.js +151 -0
- package/dist/dsl/compile.d.ts +36 -0
- package/dist/dsl/compile.js +308 -0
- package/dist/dsl/diagnostics.d.ts +21 -0
- package/dist/dsl/diagnostics.js +38 -0
- package/dist/dsl/prefab.d.ts +30 -0
- package/dist/dsl/prefab.js +255 -0
- package/dist/dsl/serialize.d.ts +14 -0
- package/dist/dsl/serialize.js +157 -0
- package/dist/dsl/types.d.ts +45 -0
- package/dist/dsl/types.js +120 -0
- package/dist/exporters/docs.d.ts +16 -0
- package/dist/exporters/docs.js +190 -0
- package/dist/exporters/xsd.d.ts +13 -0
- package/dist/exporters/xsd.js +199 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +23 -0
- package/dist/watch.d.ts +33 -0
- package/dist/watch.js +112 -0
- package/package.json +35 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { getNodeDef, getAttachmentDef, isScalarType, Registry, TRANSFORM_ATTRS, valueEquals, } from '@scenoco-three/core/internal';
|
|
2
|
+
import { codecs } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Serialize a CompiledScene back to canonical, re-compilable XML — the inverse
|
|
5
|
+
* of the compiler (minus validation). Used to write programmatic/agent edits
|
|
6
|
+
* back to file form.
|
|
7
|
+
*
|
|
8
|
+
* The output is the **flat** form: prefab instances and material `src=` files
|
|
9
|
+
* have already been expanded/inlined by the compiler, so they appear as ordinary
|
|
10
|
+
* nodes and inline materials. Only non-default attributes are emitted. A
|
|
11
|
+
* reference (`{ node|component: index }`) is written as `#id`; if its target had
|
|
12
|
+
* no id, a stable unique one is synthesized so the reference survives the round
|
|
13
|
+
* trip.
|
|
14
|
+
*/
|
|
15
|
+
export function serializeScene(scene) {
|
|
16
|
+
const ids = assignIds(scene);
|
|
17
|
+
const lines = [];
|
|
18
|
+
emitNode(scene, scene.root, 0, ids, lines);
|
|
19
|
+
return lines.join('\n') + '\n';
|
|
20
|
+
}
|
|
21
|
+
/** Resolve every node/component to an id, synthesizing unique ones for referenced-but-unnamed targets. */
|
|
22
|
+
function assignIds(scene) {
|
|
23
|
+
const refNodes = new Set();
|
|
24
|
+
const refComponents = new Set();
|
|
25
|
+
for (const c of scene.components) {
|
|
26
|
+
for (const v of Object.values(c.props)) {
|
|
27
|
+
if (!isRef(v))
|
|
28
|
+
continue;
|
|
29
|
+
if ('node' in v)
|
|
30
|
+
refNodes.add(v.node);
|
|
31
|
+
else if ('component' in v)
|
|
32
|
+
refComponents.add(v.component);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const taken = new Set();
|
|
36
|
+
for (const n of scene.nodes)
|
|
37
|
+
if (n.id !== undefined)
|
|
38
|
+
taken.add(n.id);
|
|
39
|
+
for (const c of scene.components)
|
|
40
|
+
if (c.id !== undefined)
|
|
41
|
+
taken.add(c.id);
|
|
42
|
+
const synth = (base) => {
|
|
43
|
+
let id = base;
|
|
44
|
+
let i = 1;
|
|
45
|
+
while (taken.has(id))
|
|
46
|
+
id = `${base}_${i++}`;
|
|
47
|
+
taken.add(id);
|
|
48
|
+
return id;
|
|
49
|
+
};
|
|
50
|
+
const nodeIds = new Map();
|
|
51
|
+
scene.nodes.forEach((n, i) => {
|
|
52
|
+
if (n.id !== undefined)
|
|
53
|
+
nodeIds.set(i, n.id);
|
|
54
|
+
else if (refNodes.has(i))
|
|
55
|
+
nodeIds.set(i, synth(n.tag.toLowerCase()));
|
|
56
|
+
});
|
|
57
|
+
const componentIds = new Map();
|
|
58
|
+
scene.components.forEach((c, i) => {
|
|
59
|
+
if (c.id !== undefined)
|
|
60
|
+
componentIds.set(i, c.id);
|
|
61
|
+
else if (refComponents.has(i))
|
|
62
|
+
componentIds.set(i, synth(c.type.toLowerCase()));
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
node: (i) => nodeIds.get(i),
|
|
66
|
+
component: (i) => componentIds.get(i),
|
|
67
|
+
nodeHas: (i) => nodeIds.has(i),
|
|
68
|
+
componentHas: (i) => componentIds.has(i),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function emitNode(scene, index, depth, ids, out) {
|
|
72
|
+
const node = scene.nodes[index];
|
|
73
|
+
const pad = ' '.repeat(depth);
|
|
74
|
+
const attrs = nodeAttrs(node, index, ids);
|
|
75
|
+
const hasAttachments = node.attachments.length > 0;
|
|
76
|
+
const hasComponents = node.components.length > 0;
|
|
77
|
+
const hasChildren = node.children.length > 0;
|
|
78
|
+
if (!hasAttachments && !hasComponents && !hasChildren) {
|
|
79
|
+
out.push(`${pad}<${node.tag}${attrs} />`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
out.push(`${pad}<${node.tag}${attrs}>`);
|
|
83
|
+
const inner = ' '.repeat(depth + 1);
|
|
84
|
+
for (const ai of node.attachments)
|
|
85
|
+
out.push(`${inner}${attachmentTag(scene.attachments[ai])}`);
|
|
86
|
+
if (hasComponents) {
|
|
87
|
+
out.push(`${inner}<Components>`);
|
|
88
|
+
for (const ci of node.components)
|
|
89
|
+
out.push(`${inner} ${componentTag(scene.components[ci], ci, ids)}`);
|
|
90
|
+
out.push(`${inner}</Components>`);
|
|
91
|
+
}
|
|
92
|
+
for (const child of node.children)
|
|
93
|
+
emitNode(scene, child, depth + 1, ids, out);
|
|
94
|
+
out.push(`${pad}</${node.tag}>`);
|
|
95
|
+
}
|
|
96
|
+
// ---- attribute builders -----------------------------------------------------
|
|
97
|
+
function nodeAttrs(node, index, ids) {
|
|
98
|
+
const parts = [];
|
|
99
|
+
if (ids.nodeHas(index))
|
|
100
|
+
parts.push(attr('id', ids.node(index)));
|
|
101
|
+
const t = node.transform;
|
|
102
|
+
if (!isZero(t.position))
|
|
103
|
+
parts.push(attr('position', codecs.vec3.serialize(t.position)));
|
|
104
|
+
if (!isZero(t.rotation))
|
|
105
|
+
parts.push(attr('rotation', codecs.euler.serialize(t.rotation)));
|
|
106
|
+
if (!isOne(t.scale))
|
|
107
|
+
parts.push(attr('scale', codecs.vec3.serialize(t.scale)));
|
|
108
|
+
if (!t.visible)
|
|
109
|
+
parts.push(attr('visible', 'false'));
|
|
110
|
+
appendDefaultedAttrs(parts, getNodeDef(node.tag)?.attrs ?? {}, node.attrs);
|
|
111
|
+
return parts.length > 0 ? ' ' + parts.join(' ') : '';
|
|
112
|
+
}
|
|
113
|
+
function attachmentTag(asset) {
|
|
114
|
+
const parts = [];
|
|
115
|
+
appendDefaultedAttrs(parts, getAttachmentDef(asset.tag)?.attrs ?? {}, asset.attrs);
|
|
116
|
+
return `<${asset.tag}${parts.length > 0 ? ' ' + parts.join(' ') : ''} />`;
|
|
117
|
+
}
|
|
118
|
+
function componentTag(component, index, ids) {
|
|
119
|
+
const meta = Registry.getComponent(component.type).meta;
|
|
120
|
+
const parts = [];
|
|
121
|
+
if (ids.componentHas(index))
|
|
122
|
+
parts.push(attr('id', ids.component(index)));
|
|
123
|
+
for (const [name, value] of Object.entries(component.props)) {
|
|
124
|
+
const propMeta = meta.properties.get(name);
|
|
125
|
+
if (!propMeta)
|
|
126
|
+
continue;
|
|
127
|
+
if (isRef(value)) {
|
|
128
|
+
// A prefab prop embeds the compiled prefab; its original file path is not
|
|
129
|
+
// retained, so it cannot be reconstructed as a `prefab="…"` attribute here.
|
|
130
|
+
if ('prefab' in value)
|
|
131
|
+
continue;
|
|
132
|
+
const target = 'node' in value ? ids.node(value.node) : ids.component(value.component);
|
|
133
|
+
parts.push(attr(name, `#${target}`));
|
|
134
|
+
}
|
|
135
|
+
else if (isScalarType(propMeta.type)) {
|
|
136
|
+
parts.push(attr(name, codecs[propMeta.type].serialize(value)));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return `<${component.type}${parts.length > 0 ? ' ' + parts.join(' ') : ''} />`;
|
|
140
|
+
}
|
|
141
|
+
/** Emit each attr whose value differs from the registry default. */
|
|
142
|
+
function appendDefaultedAttrs(parts, defs, values) {
|
|
143
|
+
for (const [name, meta] of Object.entries(defs)) {
|
|
144
|
+
const value = values[name];
|
|
145
|
+
if (value === undefined || valueEquals(value, meta.default))
|
|
146
|
+
continue;
|
|
147
|
+
parts.push(attr(name, codecs[meta.type].serialize(value)));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// ---- small helpers ----------------------------------------------------------
|
|
151
|
+
const attr = (name, value) => `${name}="${escapeXml(value)}"`;
|
|
152
|
+
const isRef = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
153
|
+
const isZero = (v) => v.every((x) => x === 0);
|
|
154
|
+
const isOne = (v) => v.every((x) => x === 1);
|
|
155
|
+
function escapeXml(s) {
|
|
156
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
157
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical wire formats for SceNoCo attribute values — the single place that
|
|
3
|
+
* defines how each scalar/math property is written in XML, parsed into a
|
|
4
|
+
* JSON-serializable compiled value, serialized back, and described in XSD.
|
|
5
|
+
*
|
|
6
|
+
* Compiled representation (what flows through compile → instantiate, and what
|
|
7
|
+
* ships as JSON in production builds) is deliberately plain and fast:
|
|
8
|
+
* float/int → number
|
|
9
|
+
* bool → boolean
|
|
10
|
+
* string → string
|
|
11
|
+
* vec2/vec3 → number[] (length 2 / 3)
|
|
12
|
+
* euler → number[] in RADIANS (XML is degrees; converted at parse time)
|
|
13
|
+
* color → number (0xRRGGBB)
|
|
14
|
+
*
|
|
15
|
+
* Reference types (`node` and typed component references) are not scalar codecs;
|
|
16
|
+
* they need the scene-wide name/id tables and are resolved by the compiler.
|
|
17
|
+
*/
|
|
18
|
+
export type ScalarType = 'float' | 'int' | 'bool' | 'string' | 'vec2' | 'vec3' | 'euler' | 'color';
|
|
19
|
+
export type CompiledScalar = number | boolean | string | number[];
|
|
20
|
+
export interface ParseOk<T> {
|
|
21
|
+
ok: true;
|
|
22
|
+
value: T;
|
|
23
|
+
}
|
|
24
|
+
export interface ParseErr {
|
|
25
|
+
ok: false;
|
|
26
|
+
/** LLM-actionable: says what was expected and shows a valid example. */
|
|
27
|
+
message: string;
|
|
28
|
+
}
|
|
29
|
+
export type ParseResult<T> = ParseOk<T> | ParseErr;
|
|
30
|
+
export interface XsdType {
|
|
31
|
+
/** Underlying XSD simple type. */
|
|
32
|
+
base: string;
|
|
33
|
+
/** Optional restriction pattern for string-encoded math types. */
|
|
34
|
+
pattern?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface TypeCodec<T extends CompiledScalar = CompiledScalar> {
|
|
37
|
+
/** Human label used in error messages, e.g. "a vec3". */
|
|
38
|
+
label: string;
|
|
39
|
+
/** A valid example value, shown in errors and docs. */
|
|
40
|
+
example: string;
|
|
41
|
+
parse(raw: string): ParseResult<T>;
|
|
42
|
+
serialize(value: T): string;
|
|
43
|
+
xsd: XsdType;
|
|
44
|
+
}
|
|
45
|
+
export declare const codecs: Record<ScalarType, TypeCodec>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Color } from 'three';
|
|
2
|
+
const ok = (value) => ({ ok: true, value });
|
|
3
|
+
const err = (message) => ({ ok: false, message });
|
|
4
|
+
// A single finite number: 1, -2.5, .5, 3e2, +4. Used for runtime parsing (JS regex).
|
|
5
|
+
const NUM_SRC = '[-+]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][-+]?\\d+)?';
|
|
6
|
+
const NUM_RE = new RegExp(`^${NUM_SRC}$`);
|
|
7
|
+
// XML-Schema-flavored equivalents for the generated XSD: patterns are implicitly
|
|
8
|
+
// anchored and forbid the `(?:…)` non-capturing group, so we use plain groups.
|
|
9
|
+
const XSD_NUM = '[+-]?(\\d+(\\.\\d*)?|\\.\\d+)([eE][+-]?\\d+)?';
|
|
10
|
+
const xsdVecPattern = (n) => `\\s*${XSD_NUM}(\\s+${XSD_NUM}){${n - 1}}\\s*`;
|
|
11
|
+
function parseFiniteNumber(raw) {
|
|
12
|
+
const t = raw.trim();
|
|
13
|
+
if (!NUM_RE.test(t))
|
|
14
|
+
return err(`expected a number (e.g. "1.5"), got "${raw}"`);
|
|
15
|
+
const n = Number(t);
|
|
16
|
+
return Number.isFinite(n) ? ok(n) : err(`expected a finite number, got "${raw}"`);
|
|
17
|
+
}
|
|
18
|
+
function parseVec(raw, n, label) {
|
|
19
|
+
const parts = raw.trim().split(/\s+/);
|
|
20
|
+
if (parts.length !== n) {
|
|
21
|
+
return err(`expected ${label} — ${n} space-separated numbers (e.g. "${vecExample(n)}"), got "${raw}"`);
|
|
22
|
+
}
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const p of parts) {
|
|
25
|
+
if (!NUM_RE.test(p)) {
|
|
26
|
+
return err(`expected ${label} — ${n} space-separated numbers (e.g. "${vecExample(n)}"), got "${raw}"`);
|
|
27
|
+
}
|
|
28
|
+
out.push(Number(p));
|
|
29
|
+
}
|
|
30
|
+
return ok(out);
|
|
31
|
+
}
|
|
32
|
+
const vecExample = (n) => (n === 2 ? '0 1' : '0 1.5 -3');
|
|
33
|
+
const DEG2RAD = Math.PI / 180;
|
|
34
|
+
const RAD2DEG = 180 / Math.PI;
|
|
35
|
+
const HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
36
|
+
export const codecs = {
|
|
37
|
+
float: {
|
|
38
|
+
label: 'a number',
|
|
39
|
+
example: '1.5',
|
|
40
|
+
parse: parseFiniteNumber,
|
|
41
|
+
serialize: (v) => String(v),
|
|
42
|
+
xsd: { base: 'xs:float' },
|
|
43
|
+
},
|
|
44
|
+
int: {
|
|
45
|
+
label: 'an integer',
|
|
46
|
+
example: '32',
|
|
47
|
+
parse: (raw) => {
|
|
48
|
+
const r = parseFiniteNumber(raw);
|
|
49
|
+
if (!r.ok)
|
|
50
|
+
return r;
|
|
51
|
+
return Number.isInteger(r.value) ? r : err(`expected an integer (e.g. "32"), got "${raw}"`);
|
|
52
|
+
},
|
|
53
|
+
serialize: (v) => String(v),
|
|
54
|
+
xsd: { base: 'xs:int' },
|
|
55
|
+
},
|
|
56
|
+
bool: {
|
|
57
|
+
label: 'a boolean',
|
|
58
|
+
example: 'true',
|
|
59
|
+
parse: (raw) => {
|
|
60
|
+
const t = raw.trim();
|
|
61
|
+
if (t === 'true')
|
|
62
|
+
return ok(true);
|
|
63
|
+
if (t === 'false')
|
|
64
|
+
return ok(false);
|
|
65
|
+
return err(`expected "true" or "false", got "${raw}"`);
|
|
66
|
+
},
|
|
67
|
+
serialize: (v) => String(v),
|
|
68
|
+
xsd: { base: 'xs:boolean' },
|
|
69
|
+
},
|
|
70
|
+
string: {
|
|
71
|
+
label: 'a string',
|
|
72
|
+
example: 'hello',
|
|
73
|
+
parse: (raw) => ok(raw),
|
|
74
|
+
serialize: (v) => String(v),
|
|
75
|
+
xsd: { base: 'xs:string' },
|
|
76
|
+
},
|
|
77
|
+
vec2: {
|
|
78
|
+
label: 'a vec2',
|
|
79
|
+
example: '0 1',
|
|
80
|
+
parse: (raw) => parseVec(raw, 2, 'a vec2'),
|
|
81
|
+
serialize: (v) => v.join(' '),
|
|
82
|
+
xsd: { base: 'xs:string', pattern: xsdVecPattern(2) },
|
|
83
|
+
},
|
|
84
|
+
vec3: {
|
|
85
|
+
label: 'a vec3',
|
|
86
|
+
example: '0 1.5 -3',
|
|
87
|
+
parse: (raw) => parseVec(raw, 3, 'a vec3'),
|
|
88
|
+
serialize: (v) => v.join(' '),
|
|
89
|
+
xsd: { base: 'xs:string', pattern: xsdVecPattern(3) },
|
|
90
|
+
},
|
|
91
|
+
euler: {
|
|
92
|
+
// XML carries degrees (Unity/Blender/A-Frame convention); compiled is radians.
|
|
93
|
+
label: 'an euler (degrees)',
|
|
94
|
+
example: '0 90 0',
|
|
95
|
+
parse: (raw) => {
|
|
96
|
+
const r = parseVec(raw, 3, 'an euler (degrees)');
|
|
97
|
+
return r.ok ? ok(r.value.map((d) => d * DEG2RAD)) : r;
|
|
98
|
+
},
|
|
99
|
+
serialize: (v) => v.map((rad) => round(rad * RAD2DEG)).join(' '),
|
|
100
|
+
xsd: { base: 'xs:string', pattern: xsdVecPattern(3) },
|
|
101
|
+
},
|
|
102
|
+
color: {
|
|
103
|
+
label: 'a color',
|
|
104
|
+
example: '#ffcc00',
|
|
105
|
+
parse: (raw) => {
|
|
106
|
+
const t = raw.trim();
|
|
107
|
+
if (HEX_RE.test(t))
|
|
108
|
+
return ok(new Color(t).getHex());
|
|
109
|
+
if (t.toLowerCase() in Color.NAMES)
|
|
110
|
+
return ok(new Color(t.toLowerCase()).getHex());
|
|
111
|
+
return err(`expected a hex color (e.g. "#ffcc00") or a CSS color name (e.g. "tomato"), got "${raw}"`);
|
|
112
|
+
},
|
|
113
|
+
serialize: (v) => `#${v.toString(16).padStart(6, '0')}`,
|
|
114
|
+
xsd: { base: 'xs:string', pattern: `#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})|[a-zA-Z]+` },
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
// Trim floating-point dust when serializing degrees back from radians.
|
|
118
|
+
function round(n) {
|
|
119
|
+
return Math.round(n * 1e6) / 1e6;
|
|
120
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface DocsOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Compact mode for small-context local models: drops prose descriptions,
|
|
4
|
+
* the prefab section, and the value-format table in favour of one-liners.
|
|
5
|
+
* Roughly halves the token count while keeping every tag, attribute, type,
|
|
6
|
+
* default and a worked example. Same registries, so it never drifts.
|
|
7
|
+
*/
|
|
8
|
+
compact?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Generates `component-api.md` — the compact reference an agent loads into
|
|
12
|
+
* context (CLAUDE.md / .cursorrules / AGENTS.md). Built from the same registries
|
|
13
|
+
* as the runtime and the XSD, so it never drifts. Kept deliberately terse:
|
|
14
|
+
* tags, attributes, types, defaults, one example each.
|
|
15
|
+
*/
|
|
16
|
+
export declare function generateDocs(options?: DocsOptions): string;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Registry, allNodeDefs, allAttachmentDefs, TRANSFORM_ATTRS, } from '@scenoco-three/core/internal';
|
|
2
|
+
import { codecs } from '../dsl/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Generates `component-api.md` — the compact reference an agent loads into
|
|
5
|
+
* context (CLAUDE.md / .cursorrules / AGENTS.md). Built from the same registries
|
|
6
|
+
* as the runtime and the XSD, so it never drifts. Kept deliberately terse:
|
|
7
|
+
* tags, attributes, types, defaults, one example each.
|
|
8
|
+
*/
|
|
9
|
+
export function generateDocs(options = {}) {
|
|
10
|
+
const compact = options.compact ?? false;
|
|
11
|
+
const defs = allNodeDefs();
|
|
12
|
+
const byKind = (k) => defs.filter((d) => d.kind === k);
|
|
13
|
+
const md = [];
|
|
14
|
+
md.push('# SceNoCo Scene API');
|
|
15
|
+
md.push('');
|
|
16
|
+
md.push('Scenes are XML files. The root element is `<Scene>`. Rules:');
|
|
17
|
+
md.push('');
|
|
18
|
+
md.push('- An object is a `<Mesh>` with exactly one geometry child and an optional material child.');
|
|
19
|
+
md.push('- Behaviour goes in a single `<Components>` block per node; each child is a component.');
|
|
20
|
+
md.push('- `id` (optional, unique) labels any node or component; reference one elsewhere as `"#id"`.');
|
|
21
|
+
md.push('- Angles are **degrees**; positions/vectors are space-separated; colors are hex or CSS names.');
|
|
22
|
+
md.push('');
|
|
23
|
+
md.push('## Value formats');
|
|
24
|
+
md.push('');
|
|
25
|
+
if (compact) {
|
|
26
|
+
const types = ['float', 'int', 'bool', 'string', 'vec2', 'vec3', 'euler', 'color']
|
|
27
|
+
.map((t) => `${t} \`${codecs[t].example}\``)
|
|
28
|
+
.join(' · ');
|
|
29
|
+
md.push(`${types} · reference \`"#someId"\``);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
md.push('| Type | Example |');
|
|
33
|
+
md.push('| --- | --- |');
|
|
34
|
+
for (const t of ['float', 'int', 'bool', 'string', 'vec2', 'vec3', 'euler', 'color']) {
|
|
35
|
+
md.push(`| ${t} | \`${codecs[t].example}\` |`);
|
|
36
|
+
}
|
|
37
|
+
md.push('| reference | `"#someId"` |');
|
|
38
|
+
}
|
|
39
|
+
md.push('');
|
|
40
|
+
md.push('## Nodes');
|
|
41
|
+
md.push('');
|
|
42
|
+
md.push('All spatial nodes also accept: ' + transformLine() + '.');
|
|
43
|
+
md.push('');
|
|
44
|
+
for (const def of [...byKind('scene'), ...byKind('object3d')]) {
|
|
45
|
+
md.push(`### \`<${def.tag}>\`${def.description && !compact ? ` — ${def.description}` : ''}`);
|
|
46
|
+
const attrs = nodeAttrLines(def.attrs, compact);
|
|
47
|
+
if (attrs.length > 0)
|
|
48
|
+
md.push(...attrs);
|
|
49
|
+
md.push('');
|
|
50
|
+
}
|
|
51
|
+
// Attachments — leaves that configure a node (geometry/material on a mesh,
|
|
52
|
+
// fog/background on the scene), grouped by their cardinality group.
|
|
53
|
+
for (const [group, group_defs] of attachmentsByGroup()) {
|
|
54
|
+
md.push(`## ${capitalize(group)} (attaches to ${targetSummary(group_defs)})`);
|
|
55
|
+
md.push('');
|
|
56
|
+
if (!compact && group === 'material') {
|
|
57
|
+
md.push('Any attachment may set `src="file.material.xml"` to inherit attributes from a');
|
|
58
|
+
md.push('resource file; inline attributes override it, and identical attachments are shared.');
|
|
59
|
+
md.push('');
|
|
60
|
+
}
|
|
61
|
+
for (const def of group_defs) {
|
|
62
|
+
md.push(`### \`<${def.tag}>\`${def.description && !compact ? ` — ${def.description}` : ''}`);
|
|
63
|
+
md.push(...nodeAttrLines(def.attrs, compact));
|
|
64
|
+
md.push('');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
md.push('## Components (inside `<Components>`)');
|
|
68
|
+
md.push('');
|
|
69
|
+
for (const name of Registry.componentNames()) {
|
|
70
|
+
const meta = Registry.getComponent(name).meta;
|
|
71
|
+
md.push(`### \`<${name}>\`${meta.description && !compact ? ` — ${meta.description}` : ''}`);
|
|
72
|
+
for (const [attr, prop] of meta.properties)
|
|
73
|
+
md.push(`- \`${attr}\` ${propertyLine(prop, compact)}`);
|
|
74
|
+
md.push('');
|
|
75
|
+
}
|
|
76
|
+
if (!compact) {
|
|
77
|
+
md.push('## Prefabs');
|
|
78
|
+
md.push('');
|
|
79
|
+
md.push('A `.prefab.xml` file has a `<Prefab>` root wrapping exactly one node; it must be');
|
|
80
|
+
md.push('self-contained (every `#ref` resolves inside the file). Place one in a scene with a');
|
|
81
|
+
md.push('`<PrefabInstance>` (a prefab file may also contain `<PrefabInstance>` children, so');
|
|
82
|
+
md.push('prefabs nest):');
|
|
83
|
+
md.push('');
|
|
84
|
+
md.push('### `<PrefabInstance>` — instance of a prefab file');
|
|
85
|
+
md.push('- `src` (string) *(required)* — the `.prefab.xml` file to expand');
|
|
86
|
+
md.push('- `id` (string) *(required)* — instance id; internals are addressed as `"#<id>/<inner>"`');
|
|
87
|
+
md.push('- plus the transform attributes (override the prefab root): ' + transformLine());
|
|
88
|
+
md.push('');
|
|
89
|
+
md.push('Children: `<Override target="#<inner-id>" …/>` patches an internal node/component;');
|
|
90
|
+
md.push('a `<Components>` block adds components to the instance root. Example:');
|
|
91
|
+
md.push('');
|
|
92
|
+
md.push('```xml');
|
|
93
|
+
md.push(prefabExample());
|
|
94
|
+
md.push('```');
|
|
95
|
+
md.push('');
|
|
96
|
+
}
|
|
97
|
+
md.push('## Example');
|
|
98
|
+
md.push('');
|
|
99
|
+
md.push('```xml');
|
|
100
|
+
md.push(example());
|
|
101
|
+
md.push('```');
|
|
102
|
+
md.push('');
|
|
103
|
+
return md.join('\n');
|
|
104
|
+
}
|
|
105
|
+
/** Group attachment defs by their cardinality group (geometry/material/fog/…). */
|
|
106
|
+
function attachmentsByGroup() {
|
|
107
|
+
const groups = new Map();
|
|
108
|
+
for (const d of allAttachmentDefs()) {
|
|
109
|
+
(groups.get(d.group) ?? groups.set(d.group, []).get(d.group)).push(d);
|
|
110
|
+
}
|
|
111
|
+
return groups;
|
|
112
|
+
}
|
|
113
|
+
/** A short description of the node(s) a group of attachments attaches to. */
|
|
114
|
+
function targetSummary(defs) {
|
|
115
|
+
const t = defs[0].target;
|
|
116
|
+
if (typeof t === 'string')
|
|
117
|
+
return `\`<${t}>\``;
|
|
118
|
+
const classes = Array.isArray(t) ? t : [t];
|
|
119
|
+
return classes.map((c) => `\`<${c.name}>\``).join('/');
|
|
120
|
+
}
|
|
121
|
+
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
122
|
+
function transformLine() {
|
|
123
|
+
return Object.entries(TRANSFORM_ATTRS)
|
|
124
|
+
.map(([name, meta]) => `\`${name}\` (${typeLabel(meta.type)})`)
|
|
125
|
+
.join(', ');
|
|
126
|
+
}
|
|
127
|
+
function nodeAttrLines(attrs, compact = false) {
|
|
128
|
+
return Object.entries(attrs).map(([name, meta]) => {
|
|
129
|
+
const dflt = meta.default !== undefined ? ` = \`${codecs[meta.type].serialize(meta.default)}\`` : '';
|
|
130
|
+
const desc = meta.description && !compact ? ` — ${meta.description}` : '';
|
|
131
|
+
const req = meta.required ? ' *(required)*' : '';
|
|
132
|
+
return `- \`${name}\` (${typeLabel(meta.type)})${dflt}${req}${desc}`;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function propertyLine(meta, compact = false) {
|
|
136
|
+
const desc = meta.description && !compact ? ` — ${meta.description}` : '';
|
|
137
|
+
const req = meta.required ? ' *(required)*' : '';
|
|
138
|
+
if (typeof meta.type !== 'string')
|
|
139
|
+
return `(reference → \`<${meta.type.name}>\`, as \`"#id"\`)${req}${desc}`;
|
|
140
|
+
if (meta.type === 'node')
|
|
141
|
+
return `(reference → node, as \`"#id"\`)${req}${desc}`;
|
|
142
|
+
if (meta.type === 'prefab')
|
|
143
|
+
return `(prefab file → \`"name.prefab.xml"\`, spawnable at runtime)${req}${desc}`;
|
|
144
|
+
const dflt = meta.default !== undefined ? ` = \`${meta.default}\`` : '';
|
|
145
|
+
return `(${typeLabel(meta.type)})${dflt}${req}${desc}`;
|
|
146
|
+
}
|
|
147
|
+
const typeLabel = (type) => (type === 'euler' ? 'degrees' : type);
|
|
148
|
+
function example() {
|
|
149
|
+
return [
|
|
150
|
+
'<Scene>',
|
|
151
|
+
' <AmbientLight intensity="0.3" />',
|
|
152
|
+
' <DirectionalLight intensity="2" position="3 5 2" />',
|
|
153
|
+
'',
|
|
154
|
+
' <Mesh id="Hero" position="0 1 0" rotation="0 45 0">',
|
|
155
|
+
' <BoxGeometry width="1.5" />',
|
|
156
|
+
' <MeshStandardMaterial color="#4f8ef7" metalness="0.6" />',
|
|
157
|
+
' <Components>',
|
|
158
|
+
' <Rotator speed="2" />',
|
|
159
|
+
' <Oscillator id="bob" amplitude="0.5" />',
|
|
160
|
+
' </Components>',
|
|
161
|
+
' </Mesh>',
|
|
162
|
+
'',
|
|
163
|
+
' <Mesh id="Chaser" position="3 1 0">',
|
|
164
|
+
' <SphereGeometry radius="0.3" />',
|
|
165
|
+
' <Components>',
|
|
166
|
+
' <Follow source="#bob" scale="2" />',
|
|
167
|
+
' <LookAt target="#Hero" />',
|
|
168
|
+
' </Components>',
|
|
169
|
+
' </Mesh>',
|
|
170
|
+
'</Scene>',
|
|
171
|
+
].join('\n');
|
|
172
|
+
}
|
|
173
|
+
function prefabExample() {
|
|
174
|
+
return [
|
|
175
|
+
'<!-- turbine.prefab.xml -->',
|
|
176
|
+
'<Prefab>',
|
|
177
|
+
' <Mesh id="body" position="0 1 0">',
|
|
178
|
+
' <CylinderGeometry height="2" />',
|
|
179
|
+
' <Components><Oscillator id="osc" amplitude="0.5" /></Components>',
|
|
180
|
+
' </Mesh>',
|
|
181
|
+
'</Prefab>',
|
|
182
|
+
'',
|
|
183
|
+
'<!-- in a scene: two instances, one overridden -->',
|
|
184
|
+
'<PrefabInstance src="turbine.prefab.xml" id="t1" position="0 0 0" />',
|
|
185
|
+
'<PrefabInstance src="turbine.prefab.xml" id="t2" position="5 0 0">',
|
|
186
|
+
' <Override target="#osc" amplitude="1.5" />',
|
|
187
|
+
'</PrefabInstance>',
|
|
188
|
+
'<!-- reach an instance internal from elsewhere: target="#t2/osc" -->',
|
|
189
|
+
].join('\n');
|
|
190
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a no-namespace XSD 1.0 from the live registries (node tags +
|
|
3
|
+
* components). Point an editor's XML language server at it (via
|
|
4
|
+
* `xsi:noNamespaceSchemaLocation="scene.xsd"`) to get attribute autocomplete and
|
|
5
|
+
* inline validation for free — the same data the runtime Validator checks, in a
|
|
6
|
+
* form stock IDE tooling understands.
|
|
7
|
+
*
|
|
8
|
+
* The structure favors editor assistance over strict validation (e.g. it does
|
|
9
|
+
* not enforce "exactly one geometry per Mesh" — that's the Validator's job and
|
|
10
|
+
* would fight XSD 1.0's Unique Particle Attribution rule). Every element still
|
|
11
|
+
* carries a precise, typed attribute list.
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateXsd(): string;
|