@aura3d/engine 1.5.0 → 1.5.1
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/README.md +77 -46
- package/dist/engine/agent-api/ApplicationKits.d.ts +307 -0
- package/dist/engine/agent-api/ApplicationKits.d.ts.map +1 -0
- package/dist/engine/agent-api/ApplicationKits.js +650 -0
- package/dist/engine/agent-api/ApplicationKits.js.map +1 -0
- package/dist/engine/agent-api/CombatFrameData.d.ts +210 -0
- package/dist/engine/agent-api/CombatFrameData.d.ts.map +1 -0
- package/dist/engine/agent-api/CombatFrameData.js +324 -0
- package/dist/engine/agent-api/CombatFrameData.js.map +1 -0
- package/dist/engine/agent-api/FocusSelection.d.ts +186 -0
- package/dist/engine/agent-api/FocusSelection.d.ts.map +1 -0
- package/dist/engine/agent-api/FocusSelection.js +436 -0
- package/dist/engine/agent-api/FocusSelection.js.map +1 -0
- package/dist/engine/agent-api/GameGenreKits.d.ts +32 -1
- package/dist/engine/agent-api/GameGenreKits.d.ts.map +1 -1
- package/dist/engine/agent-api/GameGenreKits.js +51 -7
- package/dist/engine/agent-api/GameGenreKits.js.map +1 -1
- package/dist/engine/agent-api/GameSceneGeometryBindings.d.ts +10 -0
- package/dist/engine/agent-api/GameSceneGeometryBindings.d.ts.map +1 -1
- package/dist/engine/agent-api/GameSceneGeometryBindings.js +7 -0
- package/dist/engine/agent-api/GameSceneGeometryBindings.js.map +1 -1
- package/dist/engine/agent-api/PlatformerMotion.d.ts +177 -0
- package/dist/engine/agent-api/PlatformerMotion.d.ts.map +1 -0
- package/dist/engine/agent-api/PlatformerMotion.js +207 -0
- package/dist/engine/agent-api/PlatformerMotion.js.map +1 -0
- package/dist/engine/agent-api/SceneQueries.d.ts +124 -0
- package/dist/engine/agent-api/SceneQueries.d.ts.map +1 -0
- package/dist/engine/agent-api/SceneQueries.js +221 -0
- package/dist/engine/agent-api/SceneQueries.js.map +1 -0
- package/dist/engine/agent-api/SpatialAnchoring.d.ts +232 -0
- package/dist/engine/agent-api/SpatialAnchoring.d.ts.map +1 -0
- package/dist/engine/agent-api/SpatialAnchoring.js +348 -0
- package/dist/engine/agent-api/SpatialAnchoring.js.map +1 -0
- package/dist/engine/agent-api/VehicleChassis.d.ts +192 -0
- package/dist/engine/agent-api/VehicleChassis.d.ts.map +1 -0
- package/dist/engine/agent-api/VehicleChassis.js +322 -0
- package/dist/engine/agent-api/VehicleChassis.js.map +1 -0
- package/dist/engine/agent-api/VehicleDriverAi.d.ts +123 -0
- package/dist/engine/agent-api/VehicleDriverAi.d.ts.map +1 -0
- package/dist/engine/agent-api/VehicleDriverAi.js +217 -0
- package/dist/engine/agent-api/VehicleDriverAi.js.map +1 -0
- package/dist/engine/agent-api/WorldLabelRenderer.d.ts +146 -0
- package/dist/engine/agent-api/WorldLabelRenderer.d.ts.map +1 -0
- package/dist/engine/agent-api/WorldLabelRenderer.js +331 -0
- package/dist/engine/agent-api/WorldLabelRenderer.js.map +1 -0
- package/dist/engine/agent-api/index.d.ts +30 -0
- package/dist/engine/agent-api/index.d.ts.map +1 -1
- package/dist/engine/agent-api/index.js +87 -1
- package/dist/engine/agent-api/index.js.map +1 -1
- package/dist/engine/agent-api/product-viewer-runtime.d.ts.map +1 -1
- package/package.json +8 -2
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable application kits for static and enterprise experiences.
|
|
3
|
+
*
|
|
4
|
+
* ## Why these exist
|
|
5
|
+
*
|
|
6
|
+
* The remediation's reusable *systems* -- focus, labels, asset-relative anchoring -- removed
|
|
7
|
+
* the defects that made these routes wrong. They did not remove the reason each route is
|
|
8
|
+
* 800 to 1,400 lines: every one still assembles part selection, variant state, overlay
|
|
9
|
+
* composition, camera presets and annotation placement by hand, and each assembles it
|
|
10
|
+
* slightly differently. A developer building a sixth configurator gets no help from the five
|
|
11
|
+
* that exist.
|
|
12
|
+
*
|
|
13
|
+
* A kit owns that assembly. A route declares *what* it is configuring -- these parts, these
|
|
14
|
+
* variants, these zones, these camera presets -- and the kit owns selection state, focus
|
|
15
|
+
* feedback, annotation placement, camera framing, reset and the evidence a gate needs. The
|
|
16
|
+
* route composes scene nodes from the kit's output instead of computing them.
|
|
17
|
+
*
|
|
18
|
+
* All five kits are pure: they hold state and return scene-node JSON plus intent. No
|
|
19
|
+
* renderer, no DOM, no asset loading, so each is unit-testable and reusable by any route.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately *not* included: anything a kit cannot honestly own. Measurement and section
|
|
22
|
+
* views in the architecture kit need geometry the public API does not expose, so they are
|
|
23
|
+
* reported as absent. See `unsupported` on each kit's capability report.
|
|
24
|
+
*/
|
|
25
|
+
import { clearFocus, focusCameraIntent, focusSemanticRegion } from "./FocusSelection.js";
|
|
26
|
+
import { checkSpatialInvariants, distributeInRegion, resolveBoundsAnchor, resolveSemanticRegion } from "./SpatialAnchoring.js";
|
|
27
|
+
export function createProductConfiguratorKit(options) {
|
|
28
|
+
const variants = new Map(options.variants.map((variant) => [variant.id, variant]));
|
|
29
|
+
const finishes = new Map(options.finishes.map((finish) => [finish.id, finish]));
|
|
30
|
+
const parts = new Map(options.parts.map((part) => [part.id, part]));
|
|
31
|
+
const presets = new Map((options.cameraPresets ?? []).map((preset) => [preset.id, preset]));
|
|
32
|
+
const initial = {
|
|
33
|
+
variantId: options.variants[0]?.id ?? "",
|
|
34
|
+
finishId: options.finishes[0]?.id ?? "",
|
|
35
|
+
partId: undefined,
|
|
36
|
+
exploded: false,
|
|
37
|
+
cameraPresetId: options.cameraPresets?.[0]?.id
|
|
38
|
+
};
|
|
39
|
+
let state = initial;
|
|
40
|
+
const capabilities = {
|
|
41
|
+
kind: "aura-application-kit-capabilities",
|
|
42
|
+
kit: "product-configurator",
|
|
43
|
+
supported: [
|
|
44
|
+
"part selection", "focus feedback", "variants", "materials/finishes",
|
|
45
|
+
"exploded view", "annotations", "reset", "camera presets", "price data binding",
|
|
46
|
+
"spatial invariants"
|
|
47
|
+
],
|
|
48
|
+
unsupported: [
|
|
49
|
+
{
|
|
50
|
+
capability: "material authoring",
|
|
51
|
+
reason: "the kit selects among finishes a route supplies; authoring new material graphs belongs to the materials surface, not to a configurator"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
};
|
|
55
|
+
const buildFrame = () => {
|
|
56
|
+
const variant = variants.get(state.variantId);
|
|
57
|
+
const finish = finishes.get(state.finishId);
|
|
58
|
+
const part = state.partId ? parts.get(state.partId) : undefined;
|
|
59
|
+
const focus = part
|
|
60
|
+
? focusSemanticRegion(options.bounds, part, {
|
|
61
|
+
color: variant?.accent ?? "#fde68a",
|
|
62
|
+
indicators: options.indicators ?? ["ring", "halo"],
|
|
63
|
+
callout: true,
|
|
64
|
+
leaderLine: true,
|
|
65
|
+
// The kit reports camera intent through `camera`; it does not seize the camera.
|
|
66
|
+
cameraFocus: false,
|
|
67
|
+
namePrefix: `${part.id} focus`,
|
|
68
|
+
aspect: options.aspect
|
|
69
|
+
})
|
|
70
|
+
: clearFocus();
|
|
71
|
+
/*
|
|
72
|
+
* Exploded placements are derived from the product's bounds and each part's own
|
|
73
|
+
* normalized region, so the explosion direction follows the part rather than a literal.
|
|
74
|
+
*/
|
|
75
|
+
const explodedPlacements = state.exploded
|
|
76
|
+
? options.parts.map((entry) => {
|
|
77
|
+
const region = resolveSemanticRegion(options.bounds, entry);
|
|
78
|
+
// Push each part outward from the product centre along its own offset direction.
|
|
79
|
+
const offsetX = region.center[0] - options.bounds.center[0];
|
|
80
|
+
const offsetY = region.center[1] - options.bounds.center[1];
|
|
81
|
+
const spread = 1.6;
|
|
82
|
+
return {
|
|
83
|
+
partId: entry.id,
|
|
84
|
+
position: [
|
|
85
|
+
options.bounds.center[0] + offsetX * spread,
|
|
86
|
+
options.bounds.center[1] + offsetY * spread,
|
|
87
|
+
region.center[2]
|
|
88
|
+
],
|
|
89
|
+
size: (region.size[0] > 0 ? region.size : [
|
|
90
|
+
options.bounds.size[0] * 0.2,
|
|
91
|
+
options.bounds.size[1] * 0.2,
|
|
92
|
+
options.bounds.size[2] * 0.2
|
|
93
|
+
])
|
|
94
|
+
};
|
|
95
|
+
})
|
|
96
|
+
: [];
|
|
97
|
+
// Camera: an explicit preset wins; otherwise a selected part is framed.
|
|
98
|
+
const preset = state.cameraPresetId ? presets.get(state.cameraPresetId) : undefined;
|
|
99
|
+
const partRegion = part ? resolveSemanticRegion(options.bounds, part) : undefined;
|
|
100
|
+
const camera = preset?.camera ?? (partRegion
|
|
101
|
+
? cameraSpecFromIntent(focusCameraIntent(partRegion.center, sizeOrFraction(partRegion.size, options.bounds), { aspect: options.aspect }))
|
|
102
|
+
: undefined);
|
|
103
|
+
const price = options.basePrice === undefined
|
|
104
|
+
? undefined
|
|
105
|
+
: options.basePrice + (variant?.price ?? 0) + (finish?.price ?? 0)
|
|
106
|
+
+ (state.exploded ? 0 : 0)
|
|
107
|
+
+ (part?.price ?? 0);
|
|
108
|
+
const claims = [
|
|
109
|
+
...options.parts.map((entry) => ({
|
|
110
|
+
id: `${entry.id} region`,
|
|
111
|
+
position: resolveSemanticRegion(options.bounds, entry).center,
|
|
112
|
+
relation: "inside"
|
|
113
|
+
})),
|
|
114
|
+
/*
|
|
115
|
+
* An exploded part's claim depends on whether it actually moved.
|
|
116
|
+
*
|
|
117
|
+
* A part at the product's centre has no offset direction, so multiplying its offset by
|
|
118
|
+
* the spread leaves it where it was -- correctly, since a central component is revealed
|
|
119
|
+
* in place rather than displaced. Claiming every exploded part is `outside` is therefore
|
|
120
|
+
* a false claim, not a lenient one, and it fails the spatial gate on correct geometry.
|
|
121
|
+
* This is the same mistake the configurator route made with its driver discs.
|
|
122
|
+
*/
|
|
123
|
+
...explodedPlacements.map((placement) => {
|
|
124
|
+
const displacement = Math.hypot(placement.position[0] - options.bounds.center[0], placement.position[1] - options.bounds.center[1], placement.position[2] - options.bounds.center[2]);
|
|
125
|
+
// "Moved" means displaced by more than a tenth of the product's largest extent.
|
|
126
|
+
const displaced = displacement > Math.max(...options.bounds.size) * 0.1;
|
|
127
|
+
return displaced
|
|
128
|
+
? {
|
|
129
|
+
id: `${placement.partId} exploded`,
|
|
130
|
+
position: placement.position,
|
|
131
|
+
relation: "outside",
|
|
132
|
+
maxDistance: Math.max(...options.bounds.size) * 2.5
|
|
133
|
+
}
|
|
134
|
+
: { id: `${placement.partId} exploded`, position: placement.position, relation: "inside" };
|
|
135
|
+
})
|
|
136
|
+
];
|
|
137
|
+
return {
|
|
138
|
+
kind: "aura-product-configurator-frame",
|
|
139
|
+
state,
|
|
140
|
+
nodes: focus.nodes,
|
|
141
|
+
explodedPlacements,
|
|
142
|
+
camera,
|
|
143
|
+
focus,
|
|
144
|
+
price,
|
|
145
|
+
accessibilityLabel: part
|
|
146
|
+
? `${part.label ?? part.id} selected on ${variant?.label ?? state.variantId} in ${finish?.label ?? state.finishId}`
|
|
147
|
+
: `${variant?.label ?? state.variantId} in ${finish?.label ?? state.finishId}, no part selected`,
|
|
148
|
+
spatialInvariants: checkSpatialInvariants(options.bounds, claims)
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
kind: "aura-product-configurator-kit",
|
|
153
|
+
capabilities,
|
|
154
|
+
selectVariant(variantId) {
|
|
155
|
+
if (variants.has(variantId))
|
|
156
|
+
state = { ...state, variantId };
|
|
157
|
+
return buildFrame();
|
|
158
|
+
},
|
|
159
|
+
selectFinish(finishId) {
|
|
160
|
+
if (finishes.has(finishId))
|
|
161
|
+
state = { ...state, finishId };
|
|
162
|
+
return buildFrame();
|
|
163
|
+
},
|
|
164
|
+
selectPart(partId) {
|
|
165
|
+
// Selecting the current part deselects it, which is what a user expects from a toggle.
|
|
166
|
+
const next = partId !== undefined && partId === state.partId ? undefined : partId;
|
|
167
|
+
state = { ...state, partId: next !== undefined && parts.has(next) ? next : undefined };
|
|
168
|
+
return buildFrame();
|
|
169
|
+
},
|
|
170
|
+
toggleExploded() {
|
|
171
|
+
state = { ...state, exploded: !state.exploded };
|
|
172
|
+
return buildFrame();
|
|
173
|
+
},
|
|
174
|
+
selectCameraPreset(presetId) {
|
|
175
|
+
state = { ...state, cameraPresetId: presetId !== undefined && presets.has(presetId) ? presetId : undefined };
|
|
176
|
+
return buildFrame();
|
|
177
|
+
},
|
|
178
|
+
reset() {
|
|
179
|
+
state = initial;
|
|
180
|
+
return buildFrame();
|
|
181
|
+
},
|
|
182
|
+
frame: buildFrame
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export function createDigitalTwinKit(options) {
|
|
186
|
+
const equipment = new Map(options.equipment.map((entry) => [entry.id, entry]));
|
|
187
|
+
const timelineSteps = 8;
|
|
188
|
+
const initial = {
|
|
189
|
+
selectedEquipmentId: options.equipment[0]?.id,
|
|
190
|
+
mode: "normal",
|
|
191
|
+
focused: false,
|
|
192
|
+
alarms: [],
|
|
193
|
+
step: 0
|
|
194
|
+
};
|
|
195
|
+
let state = { ...initial, alarms: [...initial.alarms] };
|
|
196
|
+
const capabilities = {
|
|
197
|
+
kind: "aura-application-kit-capabilities",
|
|
198
|
+
kit: "digital-twin",
|
|
199
|
+
supported: [
|
|
200
|
+
"semantic equipment", "state overlays", "sensor values", "alarms",
|
|
201
|
+
"asset-relative markers", "workcell bounds", "camera focus", "timeline",
|
|
202
|
+
"state simulation", "spatial invariants"
|
|
203
|
+
],
|
|
204
|
+
unsupported: [
|
|
205
|
+
{
|
|
206
|
+
capability: "live facility data",
|
|
207
|
+
reason: "the kit carries sensor values a route supplies; connecting to a PLC or historian is integration work outside the engine"
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
};
|
|
211
|
+
const buildFrame = () => {
|
|
212
|
+
const selected = state.selectedEquipmentId ? equipment.get(state.selectedEquipmentId) : undefined;
|
|
213
|
+
const focus = selected
|
|
214
|
+
? focusSemanticRegion(options.bounds, selected, {
|
|
215
|
+
color: state.alarms.length > 0 ? "#f2715c" : "#7ee8c4",
|
|
216
|
+
indicators: ["ring"],
|
|
217
|
+
callout: true,
|
|
218
|
+
cameraFocus: false,
|
|
219
|
+
namePrefix: `${selected.id} selection`,
|
|
220
|
+
aspect: options.aspect
|
|
221
|
+
})
|
|
222
|
+
: clearFocus();
|
|
223
|
+
const alarmAnchor = resolveBoundsAnchor(options.bounds, options.alarmAnchor ?? "top-left", {
|
|
224
|
+
offset: Math.max(...options.bounds.size) * 0.12
|
|
225
|
+
});
|
|
226
|
+
const flow = options.flowRegion ? resolveSemanticRegion(options.bounds, options.flowRegion) : undefined;
|
|
227
|
+
const markerPlacements = flow
|
|
228
|
+
? distributeInRegion({ min: [flow.min[0], flow.center[1], flow.center[2]], max: [flow.max[0], flow.center[1], flow.center[2]] }, { count: options.markerCount ?? 4, seed: 17 }).map((placement) => placement.position)
|
|
229
|
+
: [];
|
|
230
|
+
const selectedRegion = selected ? resolveSemanticRegion(options.bounds, selected) : undefined;
|
|
231
|
+
const camera = state.focused && selectedRegion
|
|
232
|
+
? cameraSpecFromIntent(focusCameraIntent(selectedRegion.center, sizeOrFraction(selectedRegion.size, options.bounds), { aspect: options.aspect }))
|
|
233
|
+
: undefined;
|
|
234
|
+
const sensorReadout = {};
|
|
235
|
+
for (const entry of options.equipment) {
|
|
236
|
+
for (const [key, value] of Object.entries(entry.sensors ?? {})) {
|
|
237
|
+
// Timeline advances readings deterministically, so a replay reproduces them.
|
|
238
|
+
sensorReadout[`${entry.id}.${key}`] = Number((value * (1 + Math.sin(state.step * 0.7) * 0.06)).toFixed(4));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const claims = [
|
|
242
|
+
...options.equipment.map((entry) => ({
|
|
243
|
+
id: `${entry.id} equipment`,
|
|
244
|
+
position: resolveSemanticRegion(options.bounds, entry).center,
|
|
245
|
+
relation: "inside"
|
|
246
|
+
})),
|
|
247
|
+
...markerPlacements.map((position, index) => ({
|
|
248
|
+
id: `flow marker ${index + 1}`,
|
|
249
|
+
position,
|
|
250
|
+
relation: "inside"
|
|
251
|
+
})),
|
|
252
|
+
{
|
|
253
|
+
id: "alarm beacon",
|
|
254
|
+
position: alarmAnchor.position,
|
|
255
|
+
relation: "outside",
|
|
256
|
+
maxDistance: Math.max(...options.bounds.size) * 0.6
|
|
257
|
+
}
|
|
258
|
+
];
|
|
259
|
+
return {
|
|
260
|
+
kind: "aura-digital-twin-frame",
|
|
261
|
+
selectedEquipmentId: state.selectedEquipmentId,
|
|
262
|
+
focused: state.focused,
|
|
263
|
+
mode: state.mode,
|
|
264
|
+
nodes: focus.nodes,
|
|
265
|
+
markerPlacements,
|
|
266
|
+
alarmPosition: alarmAnchor.position,
|
|
267
|
+
camera,
|
|
268
|
+
alarms: [...state.alarms],
|
|
269
|
+
sensorReadout,
|
|
270
|
+
timeline: { step: state.step, steps: timelineSteps },
|
|
271
|
+
spatialInvariants: checkSpatialInvariants(options.bounds, claims),
|
|
272
|
+
accessibilityLabel: selected
|
|
273
|
+
? `${selected.label ?? selected.id} selected, mode ${state.mode}, ${state.alarms.length} alarm(s)`
|
|
274
|
+
: `no equipment selected, mode ${state.mode}`
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
return {
|
|
278
|
+
kind: "aura-digital-twin-kit",
|
|
279
|
+
capabilities,
|
|
280
|
+
selectEquipment(equipmentId) {
|
|
281
|
+
state = { ...state, selectedEquipmentId: equipmentId !== undefined && equipment.has(equipmentId) ? equipmentId : undefined };
|
|
282
|
+
return buildFrame();
|
|
283
|
+
},
|
|
284
|
+
setMode(mode) {
|
|
285
|
+
state = { ...state, mode };
|
|
286
|
+
return buildFrame();
|
|
287
|
+
},
|
|
288
|
+
toggleFocus() {
|
|
289
|
+
state = { ...state, focused: !state.focused };
|
|
290
|
+
return buildFrame();
|
|
291
|
+
},
|
|
292
|
+
raiseAlarm(alarm) {
|
|
293
|
+
state = { ...state, alarms: [...state.alarms, alarm], mode: alarm.severity === "critical" ? "incident" : state.mode };
|
|
294
|
+
return buildFrame();
|
|
295
|
+
},
|
|
296
|
+
clearAlarms() {
|
|
297
|
+
state = { ...state, alarms: [], mode: "normal" };
|
|
298
|
+
return buildFrame();
|
|
299
|
+
},
|
|
300
|
+
advanceTimeline() {
|
|
301
|
+
state = { ...state, step: (state.step + 1) % timelineSteps };
|
|
302
|
+
return buildFrame();
|
|
303
|
+
},
|
|
304
|
+
reset() {
|
|
305
|
+
state = { ...initial, alarms: [] };
|
|
306
|
+
return buildFrame();
|
|
307
|
+
},
|
|
308
|
+
frame: buildFrame
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
export function createArchitectureKit(options) {
|
|
312
|
+
const spaces = new Map(options.spaces.map((space) => [space.id, space]));
|
|
313
|
+
const moods = new Map(options.moods.map((mood) => [mood.id, mood]));
|
|
314
|
+
const variants = new Map((options.materialVariants ?? []).map((variant) => [variant.id, variant]));
|
|
315
|
+
const initial = {
|
|
316
|
+
floor: undefined,
|
|
317
|
+
spaceId: undefined,
|
|
318
|
+
moodId: options.moods[0]?.id ?? "",
|
|
319
|
+
materialVariantId: options.materialVariants?.[0]?.id
|
|
320
|
+
};
|
|
321
|
+
let state = initial;
|
|
322
|
+
const capabilities = {
|
|
323
|
+
kind: "aura-application-kit-capabilities",
|
|
324
|
+
kit: "architecture",
|
|
325
|
+
supported: [
|
|
326
|
+
"navigation between spaces", "floor focus", "room focus", "annotations",
|
|
327
|
+
"sun/light controls", "material variants", "camera framing", "spatial invariants"
|
|
328
|
+
],
|
|
329
|
+
unsupported: [
|
|
330
|
+
{
|
|
331
|
+
capability: "measurement",
|
|
332
|
+
reason: "point-to-point measurement needs picked world positions from the renderer, which the safe public API does not expose; a kit returning invented distances would be worse than none"
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
capability: "clipping/section views",
|
|
336
|
+
reason: "clip planes are not surfaced through the root safe API, so a kit cannot honestly produce a section view"
|
|
337
|
+
}
|
|
338
|
+
]
|
|
339
|
+
};
|
|
340
|
+
const buildFrame = () => {
|
|
341
|
+
const space = state.spaceId ? spaces.get(state.spaceId) : undefined;
|
|
342
|
+
const mood = moods.get(state.moodId);
|
|
343
|
+
const focus = space
|
|
344
|
+
? focusSemanticRegion(options.bounds, space, {
|
|
345
|
+
indicators: ["bounding-box"],
|
|
346
|
+
callout: true,
|
|
347
|
+
cameraFocus: false,
|
|
348
|
+
namePrefix: `${space.id} focus`,
|
|
349
|
+
aspect: options.aspect
|
|
350
|
+
})
|
|
351
|
+
: clearFocus();
|
|
352
|
+
const visibleSpaceIds = state.floor === undefined
|
|
353
|
+
? options.spaces.map((entry) => entry.id)
|
|
354
|
+
: options.spaces.filter((entry) => entry.floor === state.floor).map((entry) => entry.id);
|
|
355
|
+
// Camera frames the focused space, or the focused floor's combined extent.
|
|
356
|
+
const spaceRegion = space ? resolveSemanticRegion(options.bounds, space) : undefined;
|
|
357
|
+
const floorSpaces = state.floor === undefined
|
|
358
|
+
? []
|
|
359
|
+
: options.spaces.filter((entry) => entry.floor === state.floor);
|
|
360
|
+
const camera = spaceRegion
|
|
361
|
+
? cameraSpecFromIntent(focusCameraIntent(spaceRegion.center, sizeOrFraction(spaceRegion.size, options.bounds), { aspect: options.aspect }))
|
|
362
|
+
: floorSpaces.length > 0
|
|
363
|
+
? cameraSpecFromIntent(focusCameraIntent(combinedCentre(floorSpaces.map((entry) => resolveSemanticRegion(options.bounds, entry).center)), [options.bounds.size[0], options.bounds.size[1] / Math.max(1, floorCount(options.spaces)), options.bounds.size[2]], { aspect: options.aspect }))
|
|
364
|
+
: undefined;
|
|
365
|
+
/*
|
|
366
|
+
* Sun direction from elevation and azimuth. Derived rather than a per-mood literal, so a
|
|
367
|
+
* new mood is two angles rather than a hand-picked vector that may not match its name.
|
|
368
|
+
*/
|
|
369
|
+
const elevation = (mood?.sunElevation ?? 45) * (Math.PI / 180);
|
|
370
|
+
const azimuth = (mood?.sunAzimuth ?? 135) * (Math.PI / 180);
|
|
371
|
+
const sunDirection = [
|
|
372
|
+
Math.cos(elevation) * Math.sin(azimuth),
|
|
373
|
+
Math.sin(elevation),
|
|
374
|
+
Math.cos(elevation) * Math.cos(azimuth)
|
|
375
|
+
];
|
|
376
|
+
return {
|
|
377
|
+
kind: "aura-architecture-frame",
|
|
378
|
+
floor: state.floor,
|
|
379
|
+
spaceId: state.spaceId,
|
|
380
|
+
moodId: state.moodId,
|
|
381
|
+
materialVariantId: state.materialVariantId,
|
|
382
|
+
nodes: focus.nodes,
|
|
383
|
+
camera,
|
|
384
|
+
sunDirection,
|
|
385
|
+
visibleSpaceIds,
|
|
386
|
+
spatialInvariants: checkSpatialInvariants(options.bounds, options.spaces.map((entry) => ({
|
|
387
|
+
id: `${entry.id} space`,
|
|
388
|
+
position: resolveSemanticRegion(options.bounds, entry).center,
|
|
389
|
+
relation: "inside"
|
|
390
|
+
}))),
|
|
391
|
+
accessibilityLabel: space
|
|
392
|
+
? `${space.label ?? space.id} focused on floor ${space.floor}, ${mood?.label ?? state.moodId} lighting`
|
|
393
|
+
: state.floor !== undefined
|
|
394
|
+
? `floor ${state.floor} focused, ${mood?.label ?? state.moodId} lighting`
|
|
395
|
+
: `${mood?.label ?? state.moodId} lighting, no focus`
|
|
396
|
+
};
|
|
397
|
+
};
|
|
398
|
+
return {
|
|
399
|
+
kind: "aura-architecture-kit",
|
|
400
|
+
capabilities,
|
|
401
|
+
focusFloor(floor) {
|
|
402
|
+
state = { ...state, floor, spaceId: undefined };
|
|
403
|
+
return buildFrame();
|
|
404
|
+
},
|
|
405
|
+
focusSpace(spaceId) {
|
|
406
|
+
const resolved = spaceId !== undefined && spaces.has(spaceId) ? spaceId : undefined;
|
|
407
|
+
// Focusing a space also focuses its floor, so the two cannot disagree.
|
|
408
|
+
const floor = resolved === undefined ? state.floor : spaces.get(resolved)?.floor;
|
|
409
|
+
state = { ...state, spaceId: resolved, floor };
|
|
410
|
+
return buildFrame();
|
|
411
|
+
},
|
|
412
|
+
setMood(moodId) {
|
|
413
|
+
if (moods.has(moodId))
|
|
414
|
+
state = { ...state, moodId };
|
|
415
|
+
return buildFrame();
|
|
416
|
+
},
|
|
417
|
+
setMaterialVariant(variantId) {
|
|
418
|
+
state = { ...state, materialVariantId: variantId !== undefined && variants.has(variantId) ? variantId : undefined };
|
|
419
|
+
return buildFrame();
|
|
420
|
+
},
|
|
421
|
+
reset() {
|
|
422
|
+
state = initial;
|
|
423
|
+
return buildFrame();
|
|
424
|
+
},
|
|
425
|
+
frame: buildFrame
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
export function createSmartCityKit(options) {
|
|
429
|
+
const districts = new Map(options.districts.map((district) => [district.id, district]));
|
|
430
|
+
const layers = new Map(options.layers.map((layer) => [layer.id, layer]));
|
|
431
|
+
/**
|
|
432
|
+
* Density budget: overlay markers drawn at full detail before reduction kicks in.
|
|
433
|
+
*
|
|
434
|
+
* Stated rather than tuned. Above this, the kit reports which districts drop to reduced
|
|
435
|
+
* detail instead of silently drawing thousands of markers.
|
|
436
|
+
*/
|
|
437
|
+
const DENSITY_BUDGET = 24;
|
|
438
|
+
const initial = {
|
|
439
|
+
districtId: undefined,
|
|
440
|
+
activeLayerIds: options.layers.slice(0, 1).map((layer) => layer.id),
|
|
441
|
+
temporalState: options.temporalStates?.[0]
|
|
442
|
+
};
|
|
443
|
+
let state = { ...initial, activeLayerIds: [...initial.activeLayerIds] };
|
|
444
|
+
const capabilities = {
|
|
445
|
+
kind: "aura-application-kit-capabilities",
|
|
446
|
+
kit: "smart-city",
|
|
447
|
+
supported: [
|
|
448
|
+
"layer toggles", "data overlays", "camera focus", "district selection",
|
|
449
|
+
"temporal state", "density handling", "labels", "spatial invariants"
|
|
450
|
+
],
|
|
451
|
+
unsupported: [
|
|
452
|
+
{
|
|
453
|
+
capability: "real GIS data",
|
|
454
|
+
reason: "the kit places overlays for values a route supplies; ingesting geospatial sources is integration work outside the engine"
|
|
455
|
+
}
|
|
456
|
+
]
|
|
457
|
+
};
|
|
458
|
+
const buildFrame = () => {
|
|
459
|
+
const district = state.districtId ? districts.get(state.districtId) : undefined;
|
|
460
|
+
const focus = district
|
|
461
|
+
? focusSemanticRegion(options.bounds, district, {
|
|
462
|
+
color: district.color,
|
|
463
|
+
indicators: ["ring"],
|
|
464
|
+
callout: true,
|
|
465
|
+
cameraFocus: false,
|
|
466
|
+
namePrefix: `${district.id} district focus`,
|
|
467
|
+
aspect: options.aspect
|
|
468
|
+
})
|
|
469
|
+
: clearFocus();
|
|
470
|
+
const overlays = [];
|
|
471
|
+
for (const layerId of state.activeLayerIds) {
|
|
472
|
+
const layer = layers.get(layerId);
|
|
473
|
+
if (!layer)
|
|
474
|
+
continue;
|
|
475
|
+
for (const entry of options.districts) {
|
|
476
|
+
const value = layer.values[entry.id];
|
|
477
|
+
if (value === undefined)
|
|
478
|
+
continue;
|
|
479
|
+
const region = resolveSemanticRegion(options.bounds, entry);
|
|
480
|
+
overlays.push({
|
|
481
|
+
layerId,
|
|
482
|
+
districtId: entry.id,
|
|
483
|
+
// Overlay height scales with the value, so a reading is legible as height.
|
|
484
|
+
position: [region.center[0], options.bounds.floorY + options.bounds.size[1] * (0.1 + value * 0.4), region.center[2]],
|
|
485
|
+
value
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// Density handling: report reduction rather than drawing past the budget.
|
|
490
|
+
const reducedDetailDistrictIds = overlays.length > DENSITY_BUDGET
|
|
491
|
+
? [...new Set(overlays.slice(DENSITY_BUDGET).map((overlay) => overlay.districtId))]
|
|
492
|
+
: [];
|
|
493
|
+
const districtRegion = district ? resolveSemanticRegion(options.bounds, district) : undefined;
|
|
494
|
+
const camera = districtRegion
|
|
495
|
+
? cameraSpecFromIntent(focusCameraIntent(districtRegion.center, sizeOrFraction(districtRegion.size, options.bounds), { aspect: options.aspect }))
|
|
496
|
+
: undefined;
|
|
497
|
+
return {
|
|
498
|
+
kind: "aura-smart-city-frame",
|
|
499
|
+
districtId: state.districtId,
|
|
500
|
+
activeLayerIds: [...state.activeLayerIds],
|
|
501
|
+
temporalState: state.temporalState,
|
|
502
|
+
nodes: focus.nodes,
|
|
503
|
+
camera,
|
|
504
|
+
overlays,
|
|
505
|
+
reducedDetailDistrictIds,
|
|
506
|
+
spatialInvariants: checkSpatialInvariants(options.bounds, options.districts.map((entry) => ({
|
|
507
|
+
id: `${entry.id} district`,
|
|
508
|
+
position: resolveSemanticRegion(options.bounds, entry).center,
|
|
509
|
+
relation: "inside"
|
|
510
|
+
}))),
|
|
511
|
+
accessibilityLabel: district
|
|
512
|
+
? `${district.label ?? district.id} district selected, ${state.activeLayerIds.length} layer(s) active`
|
|
513
|
+
: `no district selected, ${state.activeLayerIds.length} layer(s) active`
|
|
514
|
+
};
|
|
515
|
+
};
|
|
516
|
+
return {
|
|
517
|
+
kind: "aura-smart-city-kit",
|
|
518
|
+
capabilities,
|
|
519
|
+
selectDistrict(districtId) {
|
|
520
|
+
state = { ...state, districtId: districtId !== undefined && districts.has(districtId) ? districtId : undefined };
|
|
521
|
+
return buildFrame();
|
|
522
|
+
},
|
|
523
|
+
toggleLayer(layerId) {
|
|
524
|
+
if (!layers.has(layerId))
|
|
525
|
+
return buildFrame();
|
|
526
|
+
const active = state.activeLayerIds.includes(layerId);
|
|
527
|
+
state = {
|
|
528
|
+
...state,
|
|
529
|
+
activeLayerIds: active
|
|
530
|
+
? state.activeLayerIds.filter((id) => id !== layerId)
|
|
531
|
+
: [...state.activeLayerIds, layerId]
|
|
532
|
+
};
|
|
533
|
+
return buildFrame();
|
|
534
|
+
},
|
|
535
|
+
setTemporalState(temporalState) {
|
|
536
|
+
state = { ...state, temporalState };
|
|
537
|
+
return buildFrame();
|
|
538
|
+
},
|
|
539
|
+
reset() {
|
|
540
|
+
state = { ...initial, activeLayerIds: [...initial.activeLayerIds] };
|
|
541
|
+
return buildFrame();
|
|
542
|
+
},
|
|
543
|
+
frame: buildFrame
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
export function createCinematicKit(options) {
|
|
547
|
+
const shots = options.shots.length > 0
|
|
548
|
+
? options.shots
|
|
549
|
+
: [{ id: "default", seconds: 1, from: [0, 1, 4], to: [0, 1, 4], target: [0, 0, 0] }];
|
|
550
|
+
const boundaries = [];
|
|
551
|
+
let cursor = 0;
|
|
552
|
+
for (const shot of shots) {
|
|
553
|
+
const seconds = Math.max(0.01, shot.seconds);
|
|
554
|
+
boundaries.push({ shotId: shot.id, start: cursor, end: cursor + seconds });
|
|
555
|
+
cursor += seconds;
|
|
556
|
+
}
|
|
557
|
+
const totalSeconds = cursor;
|
|
558
|
+
const fov = options.fov ?? 40;
|
|
559
|
+
const capabilities = {
|
|
560
|
+
kind: "aura-application-kit-capabilities",
|
|
561
|
+
kit: "cinematic",
|
|
562
|
+
supported: [
|
|
563
|
+
"camera paths", "shot sequencing", "timing", "animation coordination",
|
|
564
|
+
"transitions", "deterministic export/replay plan"
|
|
565
|
+
],
|
|
566
|
+
unsupported: [
|
|
567
|
+
{
|
|
568
|
+
capability: "video encoding",
|
|
569
|
+
reason: "the kit produces a deterministic frame plan; encoding belongs to the frame-encoder surface, which already owns it"
|
|
570
|
+
}
|
|
571
|
+
]
|
|
572
|
+
};
|
|
573
|
+
return {
|
|
574
|
+
kind: "aura-cinematic-kit",
|
|
575
|
+
capabilities,
|
|
576
|
+
totalSeconds,
|
|
577
|
+
sampleAt(seconds) {
|
|
578
|
+
// Sequence loops, so a sample past the end is a valid frame rather than an error.
|
|
579
|
+
const time = totalSeconds > 0 ? ((seconds % totalSeconds) + totalSeconds) % totalSeconds : 0;
|
|
580
|
+
let index = boundaries.findIndex((boundary) => time >= boundary.start && time < boundary.end);
|
|
581
|
+
if (index === -1)
|
|
582
|
+
index = boundaries.length - 1;
|
|
583
|
+
const boundary = boundaries[index];
|
|
584
|
+
const shot = shots[index];
|
|
585
|
+
const span = Math.max(1e-6, boundary.end - boundary.start);
|
|
586
|
+
const raw = (time - boundary.start) / span;
|
|
587
|
+
const progress = Math.min(1, Math.max(0, raw));
|
|
588
|
+
// Easing is applied to the path parameter, not to time, so shot durations stay exact.
|
|
589
|
+
const eased = shot.transition === "cut"
|
|
590
|
+
? 1
|
|
591
|
+
: shot.transition === "linear"
|
|
592
|
+
? progress
|
|
593
|
+
: progress * progress * (3 - 2 * progress);
|
|
594
|
+
const position = [
|
|
595
|
+
shot.from[0] + (shot.to[0] - shot.from[0]) * eased,
|
|
596
|
+
shot.from[1] + (shot.to[1] - shot.from[1]) * eased,
|
|
597
|
+
shot.from[2] + (shot.to[2] - shot.from[2]) * eased
|
|
598
|
+
];
|
|
599
|
+
return {
|
|
600
|
+
kind: "aura-cinematic-frame",
|
|
601
|
+
time: Number(time.toFixed(6)),
|
|
602
|
+
shotId: shot.id,
|
|
603
|
+
shotIndex: index,
|
|
604
|
+
shotProgress: Number(progress.toFixed(6)),
|
|
605
|
+
camera: { mode: "perspective", position, target: shot.target, fov },
|
|
606
|
+
activeClip: shot.clip,
|
|
607
|
+
// A cut resolves instantly; an eased or linear move is transitioning until it lands.
|
|
608
|
+
transitioning: shot.transition !== "cut" && progress < 1,
|
|
609
|
+
totalSeconds
|
|
610
|
+
};
|
|
611
|
+
},
|
|
612
|
+
shotBoundaries() {
|
|
613
|
+
return boundaries.map((boundary) => ({ ...boundary }));
|
|
614
|
+
},
|
|
615
|
+
exportPlan(fps) {
|
|
616
|
+
const rate = Math.max(1, Math.floor(fps));
|
|
617
|
+
const frames = Math.max(1, Math.round(totalSeconds * rate));
|
|
618
|
+
return Array.from({ length: frames }, (_, frame) => {
|
|
619
|
+
const time = frame / rate;
|
|
620
|
+
const boundary = boundaries.find((entry) => time >= entry.start && time < entry.end) ?? boundaries[boundaries.length - 1];
|
|
621
|
+
return { frame, time: Number(time.toFixed(6)), shotId: boundary.shotId };
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
// ---------------------------------------------------------------------------
|
|
627
|
+
// Shared helpers
|
|
628
|
+
// ---------------------------------------------------------------------------
|
|
629
|
+
/** Camera spec from a focus intent, so kits return something a scene can consume directly. */
|
|
630
|
+
function cameraSpecFromIntent(intent) {
|
|
631
|
+
return { mode: "perspective", position: intent.position, target: intent.target, fov: intent.fov };
|
|
632
|
+
}
|
|
633
|
+
/** A region's own size, or a readable fraction of the subject when the region is a point. */
|
|
634
|
+
function sizeOrFraction(size, bounds) {
|
|
635
|
+
return [
|
|
636
|
+
size[0] > 0 ? size[0] : bounds.size[0] * 0.3,
|
|
637
|
+
size[1] > 0 ? size[1] : bounds.size[1] * 0.3,
|
|
638
|
+
size[2] > 0 ? size[2] : bounds.size[2] * 0.3
|
|
639
|
+
];
|
|
640
|
+
}
|
|
641
|
+
function combinedCentre(points) {
|
|
642
|
+
if (points.length === 0)
|
|
643
|
+
return [0, 0, 0];
|
|
644
|
+
const sum = points.reduce((total, point) => [total[0] + point[0], total[1] + point[1], total[2] + point[2]], [0, 0, 0]);
|
|
645
|
+
return [sum[0] / points.length, sum[1] / points.length, sum[2] / points.length];
|
|
646
|
+
}
|
|
647
|
+
function floorCount(spaces) {
|
|
648
|
+
return new Set(spaces.map((space) => space.floor)).size;
|
|
649
|
+
}
|
|
650
|
+
//# sourceMappingURL=ApplicationKits.js.map
|