@aura3d/editor-runtime 1.0.6

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.
Files changed (149) hide show
  1. package/README.md +20 -0
  2. package/dist/Command.d.ts +11 -0
  3. package/dist/Command.d.ts.map +1 -0
  4. package/dist/Command.js +2 -0
  5. package/dist/Command.js.map +1 -0
  6. package/dist/CommandHistory.d.ts +20 -0
  7. package/dist/CommandHistory.d.ts.map +1 -0
  8. package/dist/CommandHistory.js +124 -0
  9. package/dist/CommandHistory.js.map +1 -0
  10. package/dist/DiagnosticsOverlayModel.d.ts +30 -0
  11. package/dist/DiagnosticsOverlayModel.d.ts.map +1 -0
  12. package/dist/DiagnosticsOverlayModel.js +57 -0
  13. package/dist/DiagnosticsOverlayModel.js.map +1 -0
  14. package/dist/EditorPluginHost.d.ts +46 -0
  15. package/dist/EditorPluginHost.d.ts.map +1 -0
  16. package/dist/EditorPluginHost.js +57 -0
  17. package/dist/EditorPluginHost.js.map +1 -0
  18. package/dist/EditorRuntime.d.ts +95 -0
  19. package/dist/EditorRuntime.d.ts.map +1 -0
  20. package/dist/EditorRuntime.js +277 -0
  21. package/dist/EditorRuntime.js.map +1 -0
  22. package/dist/EditorStateModel.d.ts +93 -0
  23. package/dist/EditorStateModel.d.ts.map +1 -0
  24. package/dist/EditorStateModel.js +251 -0
  25. package/dist/EditorStateModel.js.map +1 -0
  26. package/dist/Gizmo.d.ts +43 -0
  27. package/dist/Gizmo.d.ts.map +1 -0
  28. package/dist/Gizmo.js +60 -0
  29. package/dist/Gizmo.js.map +1 -0
  30. package/dist/HierarchyModel.d.ts +21 -0
  31. package/dist/HierarchyModel.d.ts.map +1 -0
  32. package/dist/HierarchyModel.js +32 -0
  33. package/dist/HierarchyModel.js.map +1 -0
  34. package/dist/InspectorModel.d.ts +16 -0
  35. package/dist/InspectorModel.d.ts.map +1 -0
  36. package/dist/InspectorModel.js +53 -0
  37. package/dist/InspectorModel.js.map +1 -0
  38. package/dist/LocalizationAccessibilityFixtures.d.ts +70 -0
  39. package/dist/LocalizationAccessibilityFixtures.d.ts.map +1 -0
  40. package/dist/LocalizationAccessibilityFixtures.js +275 -0
  41. package/dist/LocalizationAccessibilityFixtures.js.map +1 -0
  42. package/dist/MaterialVariantWorkflow.d.ts +19 -0
  43. package/dist/MaterialVariantWorkflow.d.ts.map +1 -0
  44. package/dist/MaterialVariantWorkflow.js +82 -0
  45. package/dist/MaterialVariantWorkflow.js.map +1 -0
  46. package/dist/PickingService.d.ts +58 -0
  47. package/dist/PickingService.d.ts.map +1 -0
  48. package/dist/PickingService.js +119 -0
  49. package/dist/PickingService.js.map +1 -0
  50. package/dist/PlayModeBridge.d.ts +14 -0
  51. package/dist/PlayModeBridge.d.ts.map +1 -0
  52. package/dist/PlayModeBridge.js +23 -0
  53. package/dist/PlayModeBridge.js.map +1 -0
  54. package/dist/PrefabRegistry.d.ts +41 -0
  55. package/dist/PrefabRegistry.d.ts.map +1 -0
  56. package/dist/PrefabRegistry.js +119 -0
  57. package/dist/PrefabRegistry.js.map +1 -0
  58. package/dist/ProjectSerializer.d.ts +82 -0
  59. package/dist/ProjectSerializer.d.ts.map +1 -0
  60. package/dist/ProjectSerializer.js +131 -0
  61. package/dist/ProjectSerializer.js.map +1 -0
  62. package/dist/RotateGizmo.d.ts +5 -0
  63. package/dist/RotateGizmo.d.ts.map +1 -0
  64. package/dist/RotateGizmo.js +37 -0
  65. package/dist/RotateGizmo.js.map +1 -0
  66. package/dist/ScaleGizmo.d.ts +5 -0
  67. package/dist/ScaleGizmo.d.ts.map +1 -0
  68. package/dist/ScaleGizmo.js +42 -0
  69. package/dist/ScaleGizmo.js.map +1 -0
  70. package/dist/Selection.d.ts +20 -0
  71. package/dist/Selection.d.ts.map +1 -0
  72. package/dist/Selection.js +46 -0
  73. package/dist/Selection.js.map +1 -0
  74. package/dist/ShaderGraphModel.d.ts +69 -0
  75. package/dist/ShaderGraphModel.d.ts.map +1 -0
  76. package/dist/ShaderGraphModel.js +172 -0
  77. package/dist/ShaderGraphModel.js.map +1 -0
  78. package/dist/StaticExportRuntime.d.ts +13 -0
  79. package/dist/StaticExportRuntime.d.ts.map +1 -0
  80. package/dist/StaticExportRuntime.js +307 -0
  81. package/dist/StaticExportRuntime.js.map +1 -0
  82. package/dist/TimelineModel.d.ts +175 -0
  83. package/dist/TimelineModel.d.ts.map +1 -0
  84. package/dist/TimelineModel.js +335 -0
  85. package/dist/TimelineModel.js.map +1 -0
  86. package/dist/TimelineRuntimeBridge.d.ts +98 -0
  87. package/dist/TimelineRuntimeBridge.d.ts.map +1 -0
  88. package/dist/TimelineRuntimeBridge.js +186 -0
  89. package/dist/TimelineRuntimeBridge.js.map +1 -0
  90. package/dist/TranslateGizmo.d.ts +5 -0
  91. package/dist/TranslateGizmo.d.ts.map +1 -0
  92. package/dist/TranslateGizmo.js +41 -0
  93. package/dist/TranslateGizmo.js.map +1 -0
  94. package/dist/commands/CreateNodeCommand.d.ts +14 -0
  95. package/dist/commands/CreateNodeCommand.d.ts.map +1 -0
  96. package/dist/commands/CreateNodeCommand.js +16 -0
  97. package/dist/commands/CreateNodeCommand.js.map +1 -0
  98. package/dist/commands/DeleteNodeCommand.d.ts +13 -0
  99. package/dist/commands/DeleteNodeCommand.d.ts.map +1 -0
  100. package/dist/commands/DeleteNodeCommand.js +55 -0
  101. package/dist/commands/DeleteNodeCommand.js.map +1 -0
  102. package/dist/commands/ReparentNodeCommand.d.ts +24 -0
  103. package/dist/commands/ReparentNodeCommand.d.ts.map +1 -0
  104. package/dist/commands/ReparentNodeCommand.js +39 -0
  105. package/dist/commands/ReparentNodeCommand.js.map +1 -0
  106. package/dist/commands/SetPropertyCommand.d.ts +14 -0
  107. package/dist/commands/SetPropertyCommand.d.ts.map +1 -0
  108. package/dist/commands/SetPropertyCommand.js +43 -0
  109. package/dist/commands/SetPropertyCommand.js.map +1 -0
  110. package/dist/commands/TransformCommand.d.ts +42 -0
  111. package/dist/commands/TransformCommand.d.ts.map +1 -0
  112. package/dist/commands/TransformCommand.js +72 -0
  113. package/dist/commands/TransformCommand.js.map +1 -0
  114. package/dist/index.d.ts +49 -0
  115. package/dist/index.d.ts.map +1 -0
  116. package/dist/index.js +29 -0
  117. package/dist/index.js.map +1 -0
  118. package/package.json +17 -0
  119. package/src/Command.ts +11 -0
  120. package/src/CommandHistory.ts +136 -0
  121. package/src/DiagnosticsOverlayModel.ts +85 -0
  122. package/src/EditorPluginHost.ts +106 -0
  123. package/src/EditorRuntime.ts +349 -0
  124. package/src/EditorStateModel.ts +336 -0
  125. package/src/Gizmo.ts +101 -0
  126. package/src/HierarchyModel.ts +57 -0
  127. package/src/InspectorModel.ts +71 -0
  128. package/src/LocalizationAccessibilityFixtures.ts +368 -0
  129. package/src/MaterialVariantWorkflow.ts +101 -0
  130. package/src/PickingService.ts +174 -0
  131. package/src/PlayModeBridge.ts +27 -0
  132. package/src/PrefabRegistry.ts +164 -0
  133. package/src/ProjectSerializer.ts +200 -0
  134. package/src/RotateGizmo.ts +35 -0
  135. package/src/ScaleGizmo.ts +40 -0
  136. package/src/Selection.ts +63 -0
  137. package/src/ShaderGraphModel.ts +254 -0
  138. package/src/StaticExportRuntime.ts +322 -0
  139. package/src/TimelineModel.ts +466 -0
  140. package/src/TimelineRuntimeBridge.ts +280 -0
  141. package/src/TranslateGizmo.ts +39 -0
  142. package/src/commands/CreateNodeCommand.ts +23 -0
  143. package/src/commands/DeleteNodeCommand.ts +74 -0
  144. package/src/commands/ReparentNodeCommand.ts +55 -0
  145. package/src/commands/SetPropertyCommand.ts +47 -0
  146. package/src/commands/TransformCommand.ts +107 -0
  147. package/src/index.ts +85 -0
  148. package/tests/editor-runtime.test.ts +597 -0
  149. package/tsconfig.json +7 -0
@@ -0,0 +1,254 @@
1
+ export type ShaderGraphValueType = "float" | "vec2" | "vec3" | "vec4" | "color" | "texture2d" | "surface";
2
+
3
+ export interface ShaderGraphPort {
4
+ readonly name: string;
5
+ readonly type: ShaderGraphValueType;
6
+ readonly required?: boolean;
7
+ readonly defaultValue?: number | readonly number[] | string;
8
+ }
9
+
10
+ export interface ShaderGraphNode {
11
+ readonly id: string;
12
+ readonly type: string;
13
+ readonly category: "Math" | "Vector" | "Texture" | "UV" | "Color" | "PBR" | "Utility";
14
+ readonly inputs: readonly ShaderGraphPort[];
15
+ readonly outputs: readonly ShaderGraphPort[];
16
+ }
17
+
18
+ export interface ShaderGraphEdge {
19
+ readonly id: string;
20
+ readonly from: {
21
+ readonly nodeId: string;
22
+ readonly output: string;
23
+ };
24
+ readonly to: {
25
+ readonly nodeId: string;
26
+ readonly input: string;
27
+ };
28
+ }
29
+
30
+ export interface ShaderGraphDiagnostic {
31
+ readonly severity: "error" | "warning" | "info";
32
+ readonly message: string;
33
+ readonly nodeId?: string;
34
+ readonly edgeId?: string;
35
+ readonly port?: string;
36
+ }
37
+
38
+ export interface ShaderGraphFixture {
39
+ readonly id: "external-parity-old-branch-shader-graph-fixture";
40
+ readonly source: "origin-master-shader-graph-adapted";
41
+ readonly sourceFiles: readonly [
42
+ "origin/master:src/shaders/graph/ShaderGraph.ts",
43
+ "origin/master:src/shaders/graph/NodeLibrary.ts",
44
+ "origin/master:src/shaders/graph/GraphValidator.ts",
45
+ "origin/master:src/shaders/graph/GraphSerializer.ts",
46
+ "origin/master:src/shaders/graph/ShaderNode.ts"
47
+ ];
48
+ readonly nodes: readonly ShaderGraphNode[];
49
+ readonly edges: readonly ShaderGraphEdge[];
50
+ readonly validation: {
51
+ readonly valid: boolean;
52
+ readonly outputNodePresent: boolean;
53
+ readonly acyclic: boolean;
54
+ readonly connectedRequiredInputs: number;
55
+ readonly typedConnections: number;
56
+ readonly warnings: readonly ShaderGraphDiagnostic[];
57
+ readonly errors: readonly ShaderGraphDiagnostic[];
58
+ };
59
+ readonly codegen: {
60
+ readonly targets: readonly ["glsl", "wgsl"];
61
+ readonly uniformCount: number;
62
+ readonly textureBindingCount: number;
63
+ readonly generatedExpressionCount: number;
64
+ readonly glslHash: string;
65
+ readonly wgslHash: string;
66
+ readonly previewFragmentLines: readonly string[];
67
+ };
68
+ readonly categories: readonly string[];
69
+ readonly blockedClaims: readonly string[];
70
+ readonly hash: string;
71
+ readonly claimBoundary: string;
72
+ }
73
+
74
+ const sourceFiles = [
75
+ "origin/master:src/shaders/graph/ShaderGraph.ts",
76
+ "origin/master:src/shaders/graph/NodeLibrary.ts",
77
+ "origin/master:src/shaders/graph/GraphValidator.ts",
78
+ "origin/master:src/shaders/graph/GraphSerializer.ts",
79
+ "origin/master:src/shaders/graph/ShaderNode.ts"
80
+ ] as const;
81
+
82
+ const blockedClaims = [
83
+ "Unity Shader Graph parity",
84
+ "Unreal Material Editor parity",
85
+ "live visual node editor parity",
86
+ "full material graph serialization parity",
87
+ "shader compiler optimization parity"
88
+ ] as const;
89
+
90
+ export function createOldBranchShaderGraphFixture(): ShaderGraphFixture {
91
+ const nodes: readonly ShaderGraphNode[] = [
92
+ node("uv0", "uv.texcoord", "UV", [], [{ name: "uv", type: "vec2" }]),
93
+ node("albedo-texture", "texture.sample2d", "Texture", [
94
+ { name: "uv", type: "vec2", required: true },
95
+ { name: "texture", type: "texture2d", defaultValue: "procedural-carbon-fiber" }
96
+ ], [{ name: "color", type: "color" }]),
97
+ node("tint", "color.rgb", "Color", [], [{ name: "color", type: "color" }]),
98
+ node("multiply", "math.multiply", "Math", [
99
+ { name: "a", type: "color", required: true },
100
+ { name: "b", type: "color", required: true }
101
+ ], [{ name: "value", type: "color" }]),
102
+ node("normal", "vector.normal", "Vector", [], [{ name: "normal", type: "vec3" }]),
103
+ node("pbr", "pbr.surface", "PBR", [
104
+ { name: "baseColor", type: "color", required: true },
105
+ { name: "normal", type: "vec3", required: true },
106
+ { name: "metallic", type: "float", defaultValue: 0.2 },
107
+ { name: "roughness", type: "float", defaultValue: 0.38 }
108
+ ], [{ name: "surface", type: "surface" }]),
109
+ node("output", "utility.output", "Utility", [{ name: "surface", type: "surface", required: true }], [])
110
+ ];
111
+ const edges: readonly ShaderGraphEdge[] = [
112
+ edge("e0", "uv0", "uv", "albedo-texture", "uv"),
113
+ edge("e1", "albedo-texture", "color", "multiply", "a"),
114
+ edge("e2", "tint", "color", "multiply", "b"),
115
+ edge("e3", "multiply", "value", "pbr", "baseColor"),
116
+ edge("e4", "normal", "normal", "pbr", "normal"),
117
+ edge("e5", "pbr", "surface", "output", "surface")
118
+ ];
119
+ const validation = validateShaderGraph(nodes, edges);
120
+ const previewFragmentLines = [
121
+ "vec2 uv = a3d_texcoord0;",
122
+ "vec3 sampled = texture(u_proceduralCarbonFiber, uv).rgb;",
123
+ "vec3 baseColor = sampled * vec3(0.88, 0.14, 0.08);",
124
+ "a3dSurface.baseColor = baseColor;",
125
+ "a3dSurface.metallic = 0.2;",
126
+ "a3dSurface.roughness = 0.38;"
127
+ ] as const;
128
+ const glslHash = hashStrings(previewFragmentLines);
129
+ const wgslHash = hashStrings(previewFragmentLines.map((line) => line.replaceAll("vec", "vec").replaceAll("texture(", "textureSample(")));
130
+ return {
131
+ id: "external-parity-old-branch-shader-graph-fixture",
132
+ source: "origin-master-shader-graph-adapted",
133
+ sourceFiles,
134
+ nodes,
135
+ edges,
136
+ validation,
137
+ codegen: {
138
+ targets: ["glsl", "wgsl"],
139
+ uniformCount: 3,
140
+ textureBindingCount: 1,
141
+ generatedExpressionCount: previewFragmentLines.length,
142
+ glslHash,
143
+ wgslHash,
144
+ previewFragmentLines
145
+ },
146
+ categories: [...new Set(nodes.map((entry) => entry.category))].sort(),
147
+ blockedClaims,
148
+ hash: hashStrings([
149
+ ...nodes.map((entry) => `${entry.id}:${entry.type}:${entry.category}`),
150
+ ...edges.map((entry) => `${entry.from.nodeId}.${entry.from.output}->${entry.to.nodeId}.${entry.to.input}`),
151
+ glslHash,
152
+ wgslHash
153
+ ]),
154
+ claimBoundary: "Deterministic bounded shader-graph evidence adapted from the old node library, graph validation, and graph codegen concepts. It proves typed node/edge validation, required-input coverage, acyclic graph checks, category metadata, and GLSL/WGSL preview codegen hashes; it does not claim a live visual node editor, full material graph serialization, Unity Shader Graph parity, Unreal Material Editor parity, or shader compiler optimization parity."
155
+ };
156
+ }
157
+
158
+ function validateShaderGraph(nodes: readonly ShaderGraphNode[], edges: readonly ShaderGraphEdge[]): ShaderGraphFixture["validation"] {
159
+ const errors: ShaderGraphDiagnostic[] = [];
160
+ const warnings: ShaderGraphDiagnostic[] = [];
161
+ const nodeById = new Map(nodes.map((entry) => [entry.id, entry]));
162
+ const connectedInputs = new Set(edges.map((entry) => `${entry.to.nodeId}.${entry.to.input}`));
163
+ let typedConnections = 0;
164
+
165
+ for (const edgeEntry of edges) {
166
+ const fromNode = nodeById.get(edgeEntry.from.nodeId);
167
+ const toNode = nodeById.get(edgeEntry.to.nodeId);
168
+ const fromPort = fromNode?.outputs.find((entry) => entry.name === edgeEntry.from.output);
169
+ const toPort = toNode?.inputs.find((entry) => entry.name === edgeEntry.to.input);
170
+ if (!fromNode || !fromPort || !toNode || !toPort) {
171
+ errors.push({ severity: "error", message: "Edge references a missing node or port.", edgeId: edgeEntry.id });
172
+ continue;
173
+ }
174
+ if (fromPort.type !== toPort.type) {
175
+ errors.push({ severity: "error", message: `Type mismatch: ${fromPort.type} cannot connect to ${toPort.type}.`, edgeId: edgeEntry.id });
176
+ continue;
177
+ }
178
+ typedConnections += 1;
179
+ }
180
+
181
+ let connectedRequiredInputs = 0;
182
+ for (const graphNode of nodes) {
183
+ for (const input of graphNode.inputs) {
184
+ if (input.required && connectedInputs.has(`${graphNode.id}.${input.name}`)) {
185
+ connectedRequiredInputs += 1;
186
+ } else if (input.required) {
187
+ errors.push({ severity: "error", message: `Required input '${input.name}' is not connected.`, nodeId: graphNode.id, port: input.name });
188
+ } else if (!connectedInputs.has(`${graphNode.id}.${input.name}`) && input.defaultValue === undefined) {
189
+ warnings.push({ severity: "warning", message: `Input '${input.name}' has no connection and no default value.`, nodeId: graphNode.id, port: input.name });
190
+ }
191
+ }
192
+ }
193
+
194
+ const acyclic = !hasCycle(nodes, edges);
195
+ if (!acyclic) errors.push({ severity: "error", message: "Cycle detected in shader graph." });
196
+ const outputNodePresent = nodes.some((entry) => entry.type === "utility.output");
197
+ if (!outputNodePresent) errors.push({ severity: "error", message: "Graph must have a utility.output node." });
198
+
199
+ return {
200
+ valid: errors.length === 0,
201
+ outputNodePresent,
202
+ acyclic,
203
+ connectedRequiredInputs,
204
+ typedConnections,
205
+ warnings,
206
+ errors
207
+ };
208
+ }
209
+
210
+ function hasCycle(nodes: readonly ShaderGraphNode[], edges: readonly ShaderGraphEdge[]): boolean {
211
+ const adjacency = new Map(nodes.map((entry) => [entry.id, [] as string[]]));
212
+ for (const graphEdge of edges) {
213
+ adjacency.get(graphEdge.from.nodeId)?.push(graphEdge.to.nodeId);
214
+ }
215
+ const visited = new Set<string>();
216
+ const visiting = new Set<string>();
217
+ const visit = (id: string): boolean => {
218
+ if (visiting.has(id)) return true;
219
+ if (visited.has(id)) return false;
220
+ visiting.add(id);
221
+ for (const next of adjacency.get(id) ?? []) {
222
+ if (visit(next)) return true;
223
+ }
224
+ visiting.delete(id);
225
+ visited.add(id);
226
+ return false;
227
+ };
228
+ return nodes.some((entry) => visit(entry.id));
229
+ }
230
+
231
+ function node(
232
+ id: string,
233
+ type: ShaderGraphNode["type"],
234
+ category: ShaderGraphNode["category"],
235
+ inputs: readonly ShaderGraphPort[],
236
+ outputs: readonly ShaderGraphPort[]
237
+ ): ShaderGraphNode {
238
+ return { id, type, category, inputs, outputs };
239
+ }
240
+
241
+ function edge(id: string, fromNode: string, fromOutput: string, toNode: string, toInput: string): ShaderGraphEdge {
242
+ return { id, from: { nodeId: fromNode, output: fromOutput }, to: { nodeId: toNode, input: toInput } };
243
+ }
244
+
245
+ function hashStrings(values: readonly string[]): string {
246
+ let hash = 0x811c9dc5;
247
+ for (const value of values) {
248
+ for (let index = 0; index < value.length; index += 1) {
249
+ hash ^= value.charCodeAt(index);
250
+ hash = Math.imul(hash, 0x01000193) >>> 0;
251
+ }
252
+ }
253
+ return hash.toString(16).padStart(8, "0");
254
+ }
@@ -0,0 +1,322 @@
1
+ export interface StaticExportRuntimeOptions {
2
+ readonly globalName?: string;
3
+ readonly canvasSelector?: string;
4
+ readonly statusSelector?: string;
5
+ readonly projectPath?: string;
6
+ }
7
+
8
+ export interface StaticExportHtmlOptions {
9
+ readonly title: string;
10
+ readonly runtimePath?: string;
11
+ }
12
+
13
+ const defaultGlobalName = "__AURA3D_EXPORTED_PROJECT__";
14
+ const defaultCanvasSelector = "#aura3d-export";
15
+ const defaultStatusSelector = "#aura3d-export-status";
16
+ const defaultProjectPath = "./project.json";
17
+
18
+ export function createStaticExportRuntime(options: StaticExportRuntimeOptions = {}): string {
19
+ const globalName = options.globalName ?? defaultGlobalName;
20
+ const canvasSelector = options.canvasSelector ?? defaultCanvasSelector;
21
+ const statusSelector = options.statusSelector ?? defaultStatusSelector;
22
+ const projectPath = options.projectPath ?? defaultProjectPath;
23
+ return `const status = document.querySelector(${JSON.stringify(statusSelector)});
24
+ const canvas = document.querySelector(${JSON.stringify(canvasSelector)});
25
+ const context = canvas.getContext("2d");
26
+ const project = await fetch(${JSON.stringify(projectPath)}).then((response) => response.json());
27
+ const provenanceOperations = project.metadata.provenance?.operations?.map((operation) => operation.id) ?? [];
28
+ const startedAt = performance.now();
29
+ let selectedIndex = 0;
30
+ let interactions = 0;
31
+ let scriptTickCount = 0;
32
+ function nodePosition(node, index) {
33
+ const elapsed = (performance.now() - startedAt) / 1000;
34
+ const scriptedBounce = node.script?.enabled && /bounce/i.test(node.script.behavior ?? "") ? Math.sin(elapsed * 3.2) * 16 : 0;
35
+ return {
36
+ x: canvas.width / 2 + node.transform.position[0] * 120 + index * 22,
37
+ y: canvas.height / 2 - node.transform.position[1] * 90 + index * 18 + scriptedBounce
38
+ };
39
+ }
40
+ function featureEvidence() {
41
+ return {
42
+ importedAssetNames: project.assets.map((asset) => asset.name),
43
+ importedAssetUris: project.assets.map((asset) => asset.uri),
44
+ editedMaterials: project.scene.nodes.map((node) => ({
45
+ node: node.name,
46
+ name: node.material?.name ?? "",
47
+ baseColor: node.material?.baseColor ?? "",
48
+ metallic: node.material?.metallic ?? 0,
49
+ roughness: node.material?.roughness ?? 0,
50
+ textureSlots: node.material?.textureSlots ?? {}
51
+ })),
52
+ lights: project.scene.nodes.filter((node) => node.light?.kind && node.light.kind !== "none").map((node) => ({ node: node.name, kind: node.light.kind, intensity: node.light.intensity })),
53
+ cameras: project.scene.nodes.filter((node) => node.camera?.enabled).map((node) => ({ node: node.name, fov: node.camera.fov })),
54
+ physicsBodies: project.scene.nodes.filter((node) => node.physics?.body && node.physics.body !== "none").map((node) => ({ node: node.name, body: node.physics.body, collider: node.physics.collider })),
55
+ configuredBehaviors: project.scene.nodes.filter((node) => node.script?.enabled && node.script.behavior).map((node) => ({ node: node.name, behavior: node.script.behavior })),
56
+ particleEmitters: project.scene.nodes.filter((node) => node.particleEmitter?.enabled).map((node) => ({ node: node.name, preset: node.particleEmitter.preset, emissionRate: node.particleEmitter.emissionRate })),
57
+ playBehavior: project.scene.nodes.some((node) => node.script?.enabled || node.animation?.enabled || node.particleEmitter?.enabled),
58
+ usesEditorCode: false,
59
+ exportProvenance: provenanceOperations.includes("static-export") && provenanceOperations.includes("static-export-runtime")
60
+ };
61
+ }
62
+ function publish() {
63
+ const evidence = featureEvidence();
64
+ window[${JSON.stringify(globalName)}] = {
65
+ id: "external-editor-authored-app",
66
+ status: "ready",
67
+ renderer: "canvas2d-static-export",
68
+ visualClaim: "Browser-first local authoring and static export workflow for the shown ExternalParity app.",
69
+ knownLimits: [
70
+ "This exported app proves a browser editor author/export workflow, not a Unity or Unreal replacement.",
71
+ "The static export visualizes project-authored model metadata with canvas2D shapes; it does not load the editor shell.",
72
+ "Imported glTF asset references, material edits, lights, cameras, physics, scripts, and play behavior are preserved in project data and runtime evidence."
73
+ ],
74
+ claimBoundary: {
75
+ allowed: "browser-first local authoring workflow for the shown ExternalParity app",
76
+ blocked: ["Unity replacement", "Unreal replacement", "broad Unity/Unreal for the web"]
77
+ },
78
+ errors: [],
79
+ diagnostics: {
80
+ drawCalls: project.scene.nodes.length,
81
+ lastError: null,
82
+ frameTimeMs: Number((performance.now() - startedAt).toFixed(2)),
83
+ scriptTickCount
84
+ },
85
+ screenshotPath: "tests/reports/external-parity-example-screenshots/editor-authoring-external-parity-export.png",
86
+ nodeCount: project.scene.nodes.length,
87
+ assetCount: project.assets.length,
88
+ projectName: project.metadata.name,
89
+ provenanceHash: project.metadata.provenance?.evidenceHash ?? null,
90
+ importedAssetNames: evidence.importedAssetNames,
91
+ editedMaterials: evidence.editedMaterials,
92
+ configuredBehaviors: evidence.configuredBehaviors,
93
+ usesPlayExportPath: evidence.exportProvenance,
94
+ physicsOrScripting: evidence.physicsBodies.length > 0 || evidence.configuredBehaviors.length > 0,
95
+ playBehaviorActive: evidence.playBehavior,
96
+ featureEvidence: evidence,
97
+ selectedNodeName: project.scene.nodes[selectedIndex]?.name ?? null,
98
+ interactions,
99
+ interactive: true
100
+ };
101
+ }
102
+ function draw() {
103
+ scriptTickCount += 1;
104
+ const gradient = context.createLinearGradient(0, 0, 0, canvas.height);
105
+ gradient.addColorStop(0, "#101827");
106
+ gradient.addColorStop(0.62, "#151f2e");
107
+ gradient.addColorStop(1, "#0f1722");
108
+ context.fillStyle = gradient;
109
+ context.fillRect(0, 0, canvas.width, canvas.height);
110
+ context.fillStyle = "#1e293b";
111
+ context.fillRect(0, canvas.height - 150, canvas.width, 150);
112
+ context.strokeStyle = "#263244";
113
+ context.lineWidth = 1;
114
+ for (let x = 40; x < canvas.width; x += 40) {
115
+ context.beginPath();
116
+ context.moveTo(x, 0);
117
+ context.lineTo(x, canvas.height);
118
+ context.stroke();
119
+ }
120
+ for (let y = 40; y < canvas.height; y += 40) {
121
+ context.beginPath();
122
+ context.moveTo(0, y);
123
+ context.lineTo(canvas.width, y);
124
+ context.stroke();
125
+ }
126
+ context.fillStyle = "rgba(15, 23, 42, 0.72)";
127
+ context.fillRect(24, 24, 300, 112);
128
+ context.fillStyle = "#e5edf8";
129
+ context.font = "700 20px ui-sans-serif, system-ui, sans-serif";
130
+ context.fillText(project.metadata.name, 42, 58);
131
+ context.font = "14px ui-sans-serif, system-ui, sans-serif";
132
+ context.fillStyle = "#a7b8cc";
133
+ context.fillText(\`\${project.assets.length} imported assets | \${project.scene.nodes.length} authored nodes\`, 42, 84);
134
+ context.fillText("Static export runtime: editor code absent", 42, 110);
135
+ for (const [index, node] of project.scene.nodes.entries()) {
136
+ const selected = index === selectedIndex;
137
+ const color = node.material?.baseColor ?? "#38d99f";
138
+ const { x, y } = nodePosition(node, index);
139
+ if (node.light?.kind !== "none") {
140
+ context.fillStyle = "rgba(250, 204, 21, 0.2)";
141
+ context.beginPath();
142
+ context.arc(x, y, Math.max(36, node.light.intensity * 54), 0, Math.PI * 2);
143
+ context.fill();
144
+ }
145
+ if (node.mesh?.primitive === "imported" || node.mesh?.assetId) {
146
+ drawImportedModelNode(node, x, y, color);
147
+ } else if (node.camera?.enabled) {
148
+ drawCameraNode(x, y, color);
149
+ } else {
150
+ context.fillStyle = color;
151
+ context.fillRect(x - 42, y - 42, 84, 84);
152
+ }
153
+ context.strokeStyle = selected ? "#facc15" : "#edf2f7";
154
+ context.lineWidth = selected ? 5 : 3;
155
+ context.strokeRect(x - 56, y - 50, 112, 100);
156
+ if (node.physics?.collider && node.physics.collider !== "none") {
157
+ context.setLineDash([7, 5]);
158
+ context.strokeStyle = "#fb7185";
159
+ context.lineWidth = 2;
160
+ context.strokeRect(x - 66, y - 60, 132, 120);
161
+ context.setLineDash([]);
162
+ }
163
+ if (node.camera?.enabled) {
164
+ context.strokeStyle = "#67e8f9";
165
+ context.lineWidth = 2;
166
+ context.beginPath();
167
+ context.moveTo(x + 60, y - 28);
168
+ context.lineTo(x + 112, y - 56);
169
+ context.lineTo(x + 112, y + 56);
170
+ context.lineTo(x + 60, y + 28);
171
+ context.closePath();
172
+ context.stroke();
173
+ }
174
+ if (node.particleEmitter?.enabled) {
175
+ context.fillStyle = "rgba(125, 211, 252, 0.78)";
176
+ for (let particle = 0; particle < 10; particle += 1) {
177
+ const angle = particle * 0.9 + scriptTickCount * 0.04;
178
+ context.beginPath();
179
+ context.arc(x + Math.cos(angle) * (22 + particle * 2), y - 52 - particle * 5, 3, 0, Math.PI * 2);
180
+ context.fill();
181
+ }
182
+ }
183
+ context.fillStyle = selected ? "#facc15" : "#edf2f7";
184
+ context.font = "15px ui-sans-serif, system-ui, sans-serif";
185
+ context.fillText(node.name, x - 56, y + 76);
186
+ }
187
+ status.textContent = \`Loaded \${project.metadata.name}: \${project.scene.nodes.length} nodes | selected \${project.scene.nodes[selectedIndex]?.name ?? "none"} | interactions \${interactions}\`;
188
+ publish();
189
+ }
190
+ function drawImportedModelNode(node, x, y, color) {
191
+ context.save();
192
+ context.translate(x, y);
193
+ context.fillStyle = "rgba(0,0,0,0.28)";
194
+ context.beginPath();
195
+ context.ellipse(0, 54, 58, 14, 0, 0, Math.PI * 2);
196
+ context.fill();
197
+ context.fillStyle = color;
198
+ context.beginPath();
199
+ context.ellipse(-8, 0, 46, 28, -0.12, 0, Math.PI * 2);
200
+ context.fill();
201
+ context.beginPath();
202
+ context.arc(38, -18, 22, 0, Math.PI * 2);
203
+ context.fill();
204
+ context.beginPath();
205
+ context.moveTo(26, -36);
206
+ context.lineTo(36, -64);
207
+ context.lineTo(48, -36);
208
+ context.fill();
209
+ context.beginPath();
210
+ context.moveTo(46, -34);
211
+ context.lineTo(58, -58);
212
+ context.lineTo(64, -26);
213
+ context.fill();
214
+ context.strokeStyle = "#fed7aa";
215
+ context.lineWidth = 8;
216
+ context.beginPath();
217
+ context.moveTo(-48, -2);
218
+ context.quadraticCurveTo(-86, -44, -118, -12);
219
+ context.stroke();
220
+ context.strokeStyle = "#111827";
221
+ context.lineWidth = 6;
222
+ for (const legX of [-30, -10, 12, 28]) {
223
+ context.beginPath();
224
+ context.moveTo(legX, 22);
225
+ context.lineTo(legX - 8, 54);
226
+ context.stroke();
227
+ }
228
+ context.fillStyle = "#111827";
229
+ context.beginPath();
230
+ context.arc(47, -24, 3, 0, Math.PI * 2);
231
+ context.fill();
232
+ context.restore();
233
+ }
234
+ function drawCameraNode(x, y, color) {
235
+ context.fillStyle = color;
236
+ context.fillRect(x - 40, y - 26, 70, 52);
237
+ context.beginPath();
238
+ context.moveTo(x + 30, y - 20);
239
+ context.lineTo(x + 68, y - 40);
240
+ context.lineTo(x + 68, y + 40);
241
+ context.lineTo(x + 30, y + 20);
242
+ context.closePath();
243
+ context.fill();
244
+ }
245
+ canvas.tabIndex = 0;
246
+ canvas.addEventListener("click", (event) => {
247
+ const bounds = canvas.getBoundingClientRect();
248
+ const scaleX = canvas.width / Math.max(1, bounds.width);
249
+ const scaleY = canvas.height / Math.max(1, bounds.height);
250
+ const x = (event.clientX - bounds.left) * scaleX;
251
+ const y = (event.clientY - bounds.top) * scaleY;
252
+ const hitIndex = project.scene.nodes.findIndex((node, index) => {
253
+ const position = nodePosition(node, index);
254
+ return Math.abs(x - position.x) <= 48 && Math.abs(y - position.y) <= 48;
255
+ });
256
+ selectedIndex = hitIndex >= 0 ? hitIndex : (selectedIndex + 1) % Math.max(1, project.scene.nodes.length);
257
+ interactions += 1;
258
+ canvas.focus();
259
+ draw();
260
+ });
261
+ canvas.addEventListener("keydown", (event) => {
262
+ if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
263
+ const direction = event.key === "ArrowRight" ? 1 : -1;
264
+ selectedIndex = (selectedIndex + direction + project.scene.nodes.length) % Math.max(1, project.scene.nodes.length);
265
+ interactions += 1;
266
+ event.preventDefault();
267
+ draw();
268
+ });
269
+ function frame() {
270
+ draw();
271
+ requestAnimationFrame(frame);
272
+ }
273
+ frame();
274
+ `;
275
+ }
276
+
277
+ export function createStaticExportHtml(options: StaticExportHtmlOptions): string {
278
+ const runtimePath = options.runtimePath ?? "./runtime.js";
279
+ return `<!doctype html>
280
+ <html lang="en">
281
+ <head>
282
+ <meta charset="utf-8">
283
+ <meta name="viewport" content="width=device-width, initial-scale=1">
284
+ <title>${escapeHtml(options.title)}</title>
285
+ <style>
286
+ html, body { margin: 0; min-height: 100%; background: #10151f; color: #edf2f7; font-family: Inter, ui-sans-serif, system-ui, sans-serif; }
287
+ main { display: grid; min-height: 100vh; place-items: center; padding: 24px; }
288
+ canvas { width: min(920px, 100%); height: 520px; border: 1px solid #334155; background: #121a26; }
289
+ .hud { position: fixed; left: 20px; top: 16px; padding: 10px 12px; background: rgba(15, 23, 42, 0.86); border: 1px solid #334155; border-radius: 6px; }
290
+ </style>
291
+ </head>
292
+ <body>
293
+ <main>
294
+ <canvas id="aura3d-export" width="920" height="520"></canvas>
295
+ <div class="hud" id="aura3d-export-status">Loading export...</div>
296
+ </main>
297
+ <script type="module" src="${escapeAttribute(runtimePath)}"></script>
298
+ </body>
299
+ </html>
300
+ `;
301
+ }
302
+
303
+ function escapeHtml(value: string): string {
304
+ return value.replace(/[&<>"']/g, (character) => {
305
+ switch (character) {
306
+ case "&":
307
+ return "&amp;";
308
+ case "<":
309
+ return "&lt;";
310
+ case ">":
311
+ return "&gt;";
312
+ case '"':
313
+ return "&quot;";
314
+ default:
315
+ return "&#39;";
316
+ }
317
+ });
318
+ }
319
+
320
+ function escapeAttribute(value: string): string {
321
+ return escapeHtml(value);
322
+ }