@gigabuddy/gadgets 0.1.8 → 0.1.10
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/index.js +464 -2
- package/index.js.map +3 -3
- package/package.json +1 -1
- package/react.js +211 -3
- package/react.js.map +2 -2
- package/src/index.d.ts +4 -0
- package/src/lib/createGadgetRenderer.d.ts +1 -0
- package/src/lib/react/GadgetFrame.d.ts +3 -1
- package/src/lib/sceneGraph.d.ts +90 -0
- package/src/lib/setupGadgetAwareness.d.ts +64 -0
package/index.js
CHANGED
|
@@ -5,6 +5,7 @@ function createGadgetRenderer(componentCode, data = {}, options) {
|
|
|
5
5
|
const stateEnabled = options?.stateEnabled ?? false;
|
|
6
6
|
const chatEnabled = options?.chatEnabled ?? false;
|
|
7
7
|
const contextEnabled = options?.contextEnabled ?? false;
|
|
8
|
+
const roomEnabled = options?.roomEnabled ?? false;
|
|
8
9
|
const escapedCode = componentCode.replace(/<\/script>/g, "<\\/script>");
|
|
9
10
|
const serializedAssets = JSON.stringify(options?.assets ?? {});
|
|
10
11
|
const stateBridgeScript = stateEnabled ? `
|
|
@@ -150,10 +151,183 @@ function createGadgetRenderer(componentCode, data = {}, options) {
|
|
|
150
151
|
window.__rerenderGadget();
|
|
151
152
|
}
|
|
152
153
|
});` : "";
|
|
154
|
+
const roomBridgeScript = roomEnabled ? `
|
|
155
|
+
// \u2500\u2500 Room presence \u2500\u2500
|
|
156
|
+
window.gadget.room = {
|
|
157
|
+
peers: [],
|
|
158
|
+
userId: null,
|
|
159
|
+
displayName: null,
|
|
160
|
+
_peersCallbacks: [],
|
|
161
|
+
_playerJoinedCallbacks: [],
|
|
162
|
+
_playerLeftCallbacks: [],
|
|
163
|
+
|
|
164
|
+
setCursor: function(pos) {
|
|
165
|
+
window.parent.postMessage({ type: 'gadget-presence', cursor: pos }, '*');
|
|
166
|
+
},
|
|
167
|
+
setSelection: function(sel) {
|
|
168
|
+
window.parent.postMessage({ type: 'gadget-presence', selection: sel }, '*');
|
|
169
|
+
},
|
|
170
|
+
onPeersChange: function(cb) {
|
|
171
|
+
window.gadget.room._peersCallbacks.push(cb);
|
|
172
|
+
cb(window.gadget.room.peers);
|
|
173
|
+
return function() {
|
|
174
|
+
var i = window.gadget.room._peersCallbacks.indexOf(cb);
|
|
175
|
+
if (i !== -1) window.gadget.room._peersCallbacks.splice(i, 1);
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
onPlayerJoined: function(cb) {
|
|
179
|
+
window.gadget.room._playerJoinedCallbacks.push(cb);
|
|
180
|
+
return function() {
|
|
181
|
+
var i = window.gadget.room._playerJoinedCallbacks.indexOf(cb);
|
|
182
|
+
if (i !== -1) window.gadget.room._playerJoinedCallbacks.splice(i, 1);
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
onPlayerLeft: function(cb) {
|
|
186
|
+
window.gadget.room._playerLeftCallbacks.push(cb);
|
|
187
|
+
return function() {
|
|
188
|
+
var i = window.gadget.room._playerLeftCallbacks.indexOf(cb);
|
|
189
|
+
if (i !== -1) window.gadget.room._playerLeftCallbacks.splice(i, 1);
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// \u2500\u2500 Room chat \u2500\u2500
|
|
195
|
+
window.gadget.roomChat = {
|
|
196
|
+
messages: [],
|
|
197
|
+
_messageCallbacks: [],
|
|
198
|
+
|
|
199
|
+
send: function(text) {
|
|
200
|
+
window.parent.postMessage({ type: 'gadget-chat-send', text: text }, '*');
|
|
201
|
+
},
|
|
202
|
+
onMessage: function(cb) {
|
|
203
|
+
window.gadget.roomChat._messageCallbacks.push(cb);
|
|
204
|
+
return function() {
|
|
205
|
+
var i = window.gadget.roomChat._messageCallbacks.indexOf(cb);
|
|
206
|
+
if (i !== -1) window.gadget.roomChat._messageCallbacks.splice(i, 1);
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// \u2500\u2500 Gestures \u2500\u2500
|
|
212
|
+
window.gadget.gestures = {
|
|
213
|
+
active: [],
|
|
214
|
+
_spawnedCallbacks: [],
|
|
215
|
+
_dismissedCallbacks: [],
|
|
216
|
+
|
|
217
|
+
spawn: function(gadgetId, anchor, opts) {
|
|
218
|
+
opts = opts || {};
|
|
219
|
+
window.parent.postMessage({
|
|
220
|
+
type: 'gadget-gesture-send',
|
|
221
|
+
gadgetId: gadgetId,
|
|
222
|
+
anchor: anchor,
|
|
223
|
+
ttl: opts.ttl,
|
|
224
|
+
size: opts.size,
|
|
225
|
+
rotation: opts.rotation,
|
|
226
|
+
}, '*');
|
|
227
|
+
},
|
|
228
|
+
dismiss: function(gestureId) {
|
|
229
|
+
window.parent.postMessage({ type: 'gadget-gesture-dismiss', gestureId: gestureId }, '*');
|
|
230
|
+
},
|
|
231
|
+
reportAnchorRect: function(selector, rect) {
|
|
232
|
+
window.parent.postMessage({ type: 'gadget-anchor-rect', selector: selector, rect: rect }, '*');
|
|
233
|
+
},
|
|
234
|
+
onSpawned: function(cb) {
|
|
235
|
+
window.gadget.gestures._spawnedCallbacks.push(cb);
|
|
236
|
+
return function() {
|
|
237
|
+
var i = window.gadget.gestures._spawnedCallbacks.indexOf(cb);
|
|
238
|
+
if (i !== -1) window.gadget.gestures._spawnedCallbacks.splice(i, 1);
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
onDismissed: function(cb) {
|
|
242
|
+
window.gadget.gestures._dismissedCallbacks.push(cb);
|
|
243
|
+
return function() {
|
|
244
|
+
var i = window.gadget.gestures._dismissedCallbacks.indexOf(cb);
|
|
245
|
+
if (i !== -1) window.gadget.gestures._dismissedCallbacks.splice(i, 1);
|
|
246
|
+
};
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// \u2500\u2500 Buddy attraction \u2500\u2500
|
|
251
|
+
window.gadget.buddy = {
|
|
252
|
+
attract: function(anchor, interest) {
|
|
253
|
+
window.parent.postMessage({
|
|
254
|
+
type: 'gadget-buddy-attract',
|
|
255
|
+
anchor: anchor,
|
|
256
|
+
interest: interest || 'medium',
|
|
257
|
+
}, '*');
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
window.addEventListener('message', function(event) {
|
|
262
|
+
var d = event.data;
|
|
263
|
+
if (!d) return;
|
|
264
|
+
|
|
265
|
+
// Presence updates
|
|
266
|
+
if (d.type === 'gadget-presence-update') {
|
|
267
|
+
window.gadget.room.peers = d.peers || [];
|
|
268
|
+
window.gadget.room._peersCallbacks.forEach(function(cb) { try { cb(d.peers || []); } catch(e) {} });
|
|
269
|
+
window.__rerenderGadget && window.__rerenderGadget();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Player join/leave
|
|
273
|
+
if (d.type === 'gadget-player-joined') {
|
|
274
|
+
window.gadget.room._playerJoinedCallbacks.forEach(function(cb) { try { cb(d); } catch(e) {} });
|
|
275
|
+
}
|
|
276
|
+
if (d.type === 'gadget-player-left') {
|
|
277
|
+
window.gadget.room.peers = window.gadget.room.peers.filter(function(p) { return p.userId !== d.userId; });
|
|
278
|
+
window.gadget.room._playerLeftCallbacks.forEach(function(cb) { try { cb(d.userId); } catch(e) {} });
|
|
279
|
+
window.__rerenderGadget && window.__rerenderGadget();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Chat
|
|
283
|
+
if (d.type === 'gadget-chat-message') {
|
|
284
|
+
var msg = d.message || d;
|
|
285
|
+
window.gadget.roomChat.messages.push(msg);
|
|
286
|
+
if (window.gadget.roomChat.messages.length > 100) window.gadget.roomChat.messages.shift();
|
|
287
|
+
window.gadget.roomChat._messageCallbacks.forEach(function(cb) { try { cb(msg); } catch(e) {} });
|
|
288
|
+
window.__rerenderGadget && window.__rerenderGadget();
|
|
289
|
+
}
|
|
290
|
+
if (d.type === 'gadget-chat-history') {
|
|
291
|
+
window.gadget.roomChat.messages = d.messages || [];
|
|
292
|
+
window.__rerenderGadget && window.__rerenderGadget();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Gestures
|
|
296
|
+
if (d.type === 'gadget-gesture-spawned') {
|
|
297
|
+
window.gadget.gestures.active.push(d.gesture);
|
|
298
|
+
window.gadget.gestures._spawnedCallbacks.forEach(function(cb) { try { cb(d.gesture); } catch(e) {} });
|
|
299
|
+
window.__rerenderGadget && window.__rerenderGadget();
|
|
300
|
+
}
|
|
301
|
+
if (d.type === 'gadget-gesture-dismissed') {
|
|
302
|
+
window.gadget.gestures.active = window.gadget.gestures.active.filter(function(g) { return g.id !== d.gestureId; });
|
|
303
|
+
window.gadget.gestures._dismissedCallbacks.forEach(function(cb) { try { cb(d.gestureId); } catch(e) {} });
|
|
304
|
+
window.__rerenderGadget && window.__rerenderGadget();
|
|
305
|
+
}
|
|
306
|
+
if (d.type === 'gadget-gesture-sync') {
|
|
307
|
+
window.gadget.gestures.active = d.gestures || [];
|
|
308
|
+
window.__rerenderGadget && window.__rerenderGadget();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Anchor rect request from host
|
|
312
|
+
if (d.type === 'gadget-request-anchor-rect' && d.selector) {
|
|
313
|
+
try {
|
|
314
|
+
var el = document.querySelector(d.selector);
|
|
315
|
+
if (el) {
|
|
316
|
+
var rect = el.getBoundingClientRect();
|
|
317
|
+
window.parent.postMessage({
|
|
318
|
+
type: 'gadget-anchor-rect',
|
|
319
|
+
selector: d.selector,
|
|
320
|
+
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
321
|
+
}, '*');
|
|
322
|
+
}
|
|
323
|
+
} catch(e) {}
|
|
324
|
+
}
|
|
325
|
+
});` : "";
|
|
153
326
|
const chatPropFragment = chatEnabled ? ", chat: window.gadget.chat" : "";
|
|
154
327
|
const contextPropFragment = contextEnabled ? ", context: window.gadget.context" : "";
|
|
328
|
+
const roomPropFragment = roomEnabled ? ", room: window.gadget.room, roomChat: window.gadget.roomChat, gestures: window.gadget.gestures" : "";
|
|
155
329
|
const breakoutPropFragment = ", breakout: { active: window.gadget.breakout._active, originalRect: window.gadget.breakout._originalRect, request: window.gadget.breakout.request, exit: window.gadget.breakout.exit }";
|
|
156
|
-
const componentProps = stateEnabled ? `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__, state: window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined, userId: window.gadget.state ? window.gadget.state.userId : undefined${chatPropFragment}${contextPropFragment}${breakoutPropFragment} }` : `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__${chatPropFragment}${contextPropFragment}${breakoutPropFragment} }`;
|
|
330
|
+
const componentProps = stateEnabled ? `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__, state: window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined, userId: window.gadget.state ? window.gadget.state.userId : undefined${chatPropFragment}${contextPropFragment}${breakoutPropFragment}${roomPropFragment} }` : `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__${chatPropFragment}${contextPropFragment}${breakoutPropFragment}${roomPropFragment} }`;
|
|
157
331
|
return `<!DOCTYPE html>
|
|
158
332
|
<html lang="en">
|
|
159
333
|
<head>
|
|
@@ -236,6 +410,7 @@ function createGadgetRenderer(componentCode, data = {}, options) {
|
|
|
236
410
|
${stateBridgeScript}
|
|
237
411
|
${chatBridgeScript}
|
|
238
412
|
${contextBridgeScript}
|
|
413
|
+
${roomBridgeScript}
|
|
239
414
|
|
|
240
415
|
window.__gadgetRoot = null;
|
|
241
416
|
window.__gadgetComponent = null;
|
|
@@ -317,7 +492,7 @@ ${contextBridgeScript}
|
|
|
317
492
|
window.__gadgetRoot = root;
|
|
318
493
|
root.render(
|
|
319
494
|
<ErrorBoundary>
|
|
320
|
-
<ComponentToRender data={window.__GADGET_DATA__} viewport={window.__GADGET_VIEWPORT__}${stateEnabled ? " state={window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined} userId={window.gadget.state ? window.gadget.state.userId : undefined}" : ""}${chatEnabled ? " chat={window.gadget.chat}" : ""}${contextEnabled ? " context={window.gadget.context}" : ""} breakout={{ active: window.gadget.breakout._active, originalRect: window.gadget.breakout._originalRect, request: window.gadget.breakout.request, exit: window.gadget.breakout.exit }} />
|
|
495
|
+
<ComponentToRender data={window.__GADGET_DATA__} viewport={window.__GADGET_VIEWPORT__}${stateEnabled ? " state={window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined} userId={window.gadget.state ? window.gadget.state.userId : undefined}" : ""}${chatEnabled ? " chat={window.gadget.chat}" : ""}${contextEnabled ? " context={window.gadget.context}" : ""}${roomEnabled ? " room={window.gadget.room} roomChat={window.gadget.roomChat} gestures={window.gadget.gestures}" : ""} breakout={{ active: window.gadget.breakout._active, originalRect: window.gadget.breakout._originalRect, request: window.gadget.breakout.request, exit: window.gadget.breakout.exit }} />
|
|
321
496
|
</ErrorBoundary>
|
|
322
497
|
);
|
|
323
498
|
} else {
|
|
@@ -497,8 +672,295 @@ function setupGadgetBreakout(iframe, options) {
|
|
|
497
672
|
window.removeEventListener("message", handler);
|
|
498
673
|
};
|
|
499
674
|
}
|
|
675
|
+
|
|
676
|
+
// libs/gadgets/src/lib/setupGadgetAwareness.ts
|
|
677
|
+
function setupGadgetAwareness(iframe, options) {
|
|
678
|
+
const {
|
|
679
|
+
gadgetId,
|
|
680
|
+
sceneGraph,
|
|
681
|
+
getAwarenessStates,
|
|
682
|
+
onAwarenessPublish,
|
|
683
|
+
onSceneGraphChange,
|
|
684
|
+
throttleMs = 100
|
|
685
|
+
} = options;
|
|
686
|
+
let filter = "focused";
|
|
687
|
+
let throttleTimer = null;
|
|
688
|
+
let pendingUpdate = false;
|
|
689
|
+
function getIframeBounds() {
|
|
690
|
+
return iframe.getBoundingClientRect();
|
|
691
|
+
}
|
|
692
|
+
function roomToGadget(rx, ry, bounds) {
|
|
693
|
+
const gx = (rx - bounds.left) / bounds.width;
|
|
694
|
+
const gy = (ry - bounds.top) / bounds.height;
|
|
695
|
+
if (gx < 0 || gx > 1 || gy < 0 || gy > 1)
|
|
696
|
+
return null;
|
|
697
|
+
return { x: gx, y: gy };
|
|
698
|
+
}
|
|
699
|
+
function gadgetToRoom(gx, gy, bounds) {
|
|
700
|
+
return {
|
|
701
|
+
x: gx * bounds.width + bounds.left,
|
|
702
|
+
y: gy * bounds.height + bounds.top
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
function buildAwarenessState() {
|
|
706
|
+
const states = getAwarenessStates();
|
|
707
|
+
const bounds = getIframeBounds();
|
|
708
|
+
const allParticipants = [];
|
|
709
|
+
const focused = [];
|
|
710
|
+
for (const [, entry] of states) {
|
|
711
|
+
const s = entry.state;
|
|
712
|
+
if (!s || typeof s !== "object")
|
|
713
|
+
continue;
|
|
714
|
+
const participant = {
|
|
715
|
+
actorId: s["actorId"] ?? s["displayName"] ?? "unknown",
|
|
716
|
+
displayName: s["displayName"],
|
|
717
|
+
intent: s["intent"],
|
|
718
|
+
focus: s["focus"]
|
|
719
|
+
};
|
|
720
|
+
const cursorVal = s["cursor"];
|
|
721
|
+
if (cursorVal && typeof cursorVal === "object") {
|
|
722
|
+
const rc = cursorVal;
|
|
723
|
+
const gc = roomToGadget(rc.x, rc.y, bounds);
|
|
724
|
+
if (gc) {
|
|
725
|
+
participant.cursor = gc;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
allParticipants.push(participant);
|
|
729
|
+
const focusVal = s["focus"];
|
|
730
|
+
if (focusVal === gadgetId || focusVal === `gadget:${gadgetId}`) {
|
|
731
|
+
focused.push(participant);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
const gadgetAnchors = sceneGraph.getGadgetAnchors(gadgetId).map((a) => ({
|
|
735
|
+
id: a.anchorId,
|
|
736
|
+
bounds: a.bounds,
|
|
737
|
+
occupant: a.occupant,
|
|
738
|
+
attention: a.attention
|
|
739
|
+
}));
|
|
740
|
+
return {
|
|
741
|
+
participants: filter === "focused" ? focused : allParticipants,
|
|
742
|
+
anchors: gadgetAnchors,
|
|
743
|
+
focused
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function scheduleForward() {
|
|
747
|
+
if (throttleTimer) {
|
|
748
|
+
pendingUpdate = true;
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
forwardAwareness();
|
|
752
|
+
throttleTimer = setTimeout(() => {
|
|
753
|
+
throttleTimer = null;
|
|
754
|
+
if (pendingUpdate) {
|
|
755
|
+
pendingUpdate = false;
|
|
756
|
+
forwardAwareness();
|
|
757
|
+
}
|
|
758
|
+
}, throttleMs);
|
|
759
|
+
}
|
|
760
|
+
function forwardAwareness() {
|
|
761
|
+
if (!iframe.contentWindow)
|
|
762
|
+
return;
|
|
763
|
+
const state = buildAwarenessState();
|
|
764
|
+
iframe.contentWindow.postMessage({ type: "gadget-awareness-state", ...state }, "*");
|
|
765
|
+
}
|
|
766
|
+
function handleMessage(event) {
|
|
767
|
+
if (event.source !== iframe.contentWindow)
|
|
768
|
+
return;
|
|
769
|
+
const msg = event.data;
|
|
770
|
+
if (!msg || typeof msg.type !== "string")
|
|
771
|
+
return;
|
|
772
|
+
switch (msg.type) {
|
|
773
|
+
case "gadget-anchor-register": {
|
|
774
|
+
const anchors = msg.anchors;
|
|
775
|
+
if (Array.isArray(anchors)) {
|
|
776
|
+
sceneGraph.registerAnchors(gadgetId, anchors);
|
|
777
|
+
onSceneGraphChange?.();
|
|
778
|
+
}
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
case "gadget-anchor-update": {
|
|
782
|
+
const { id, occupant, attention } = msg;
|
|
783
|
+
if (typeof id === "string") {
|
|
784
|
+
sceneGraph.updateAnchor(gadgetId, id, { occupant, attention });
|
|
785
|
+
onSceneGraphChange?.();
|
|
786
|
+
}
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
case "gadget-awareness-publish": {
|
|
790
|
+
const state = msg.state;
|
|
791
|
+
if (state && typeof state === "object") {
|
|
792
|
+
if (state["cursor"] && typeof state["cursor"] === "object") {
|
|
793
|
+
const gc = state["cursor"];
|
|
794
|
+
const bounds = getIframeBounds();
|
|
795
|
+
state["cursor"] = gadgetToRoom(gc.x, gc.y, bounds);
|
|
796
|
+
}
|
|
797
|
+
onAwarenessPublish?.(gadgetId, state);
|
|
798
|
+
}
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
case "gadget-awareness-subscribe": {
|
|
802
|
+
const newFilter = msg.filter;
|
|
803
|
+
if (newFilter === "focused" || newFilter === "room" || newFilter === "all") {
|
|
804
|
+
filter = newFilter;
|
|
805
|
+
forwardAwareness();
|
|
806
|
+
}
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
window.addEventListener("message", handleMessage);
|
|
812
|
+
const onLoad = () => forwardAwareness();
|
|
813
|
+
iframe.addEventListener("load", onLoad);
|
|
814
|
+
return () => {
|
|
815
|
+
window.removeEventListener("message", handleMessage);
|
|
816
|
+
iframe.removeEventListener("load", onLoad);
|
|
817
|
+
if (throttleTimer)
|
|
818
|
+
clearTimeout(throttleTimer);
|
|
819
|
+
sceneGraph.removeGadget(gadgetId);
|
|
820
|
+
onSceneGraphChange?.();
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// libs/gadgets/src/lib/sceneGraph.ts
|
|
825
|
+
var SceneGraph = class {
|
|
826
|
+
anchors = /* @__PURE__ */ new Map();
|
|
827
|
+
onChange = null;
|
|
828
|
+
/**
|
|
829
|
+
* Set a callback for when the scene graph changes.
|
|
830
|
+
*/
|
|
831
|
+
setOnChange(cb) {
|
|
832
|
+
this.onChange = cb;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Register anchors for a gadget. Replaces any existing anchors
|
|
836
|
+
* from the same gadget with the same IDs.
|
|
837
|
+
*/
|
|
838
|
+
registerAnchors(gadgetId, anchors) {
|
|
839
|
+
for (const a of anchors) {
|
|
840
|
+
const qualifiedId = `${gadgetId}:${a.id}`;
|
|
841
|
+
const parentQualified = a.parent ? `${gadgetId}:${a.parent}` : void 0;
|
|
842
|
+
this.anchors.set(qualifiedId, {
|
|
843
|
+
qualifiedId,
|
|
844
|
+
anchorId: a.id,
|
|
845
|
+
gadgetId,
|
|
846
|
+
bounds: a.bounds,
|
|
847
|
+
parent: parentQualified,
|
|
848
|
+
occupant: this.anchors.get(qualifiedId)?.occupant,
|
|
849
|
+
attention: this.anchors.get(qualifiedId)?.attention ?? []
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
this.onChange?.();
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Update an anchor's occupancy or attention.
|
|
856
|
+
*/
|
|
857
|
+
updateAnchor(gadgetId, anchorId, update) {
|
|
858
|
+
const qualifiedId = `${gadgetId}:${anchorId}`;
|
|
859
|
+
const anchor = this.anchors.get(qualifiedId);
|
|
860
|
+
if (!anchor)
|
|
861
|
+
return;
|
|
862
|
+
if (update.occupant !== void 0) {
|
|
863
|
+
anchor.occupant = update.occupant ?? void 0;
|
|
864
|
+
}
|
|
865
|
+
if (update.attention !== void 0) {
|
|
866
|
+
anchor.attention = update.attention;
|
|
867
|
+
}
|
|
868
|
+
this.onChange?.();
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Remove all anchors for a gadget (cleanup on iframe removal).
|
|
872
|
+
*/
|
|
873
|
+
removeGadget(gadgetId) {
|
|
874
|
+
const toRemove = [];
|
|
875
|
+
for (const [id, anchor] of this.anchors) {
|
|
876
|
+
if (anchor.gadgetId === gadgetId) {
|
|
877
|
+
toRemove.push(id);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
for (const id of toRemove) {
|
|
881
|
+
this.anchors.delete(id);
|
|
882
|
+
}
|
|
883
|
+
if (toRemove.length > 0)
|
|
884
|
+
this.onChange?.();
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Get all anchors as a flat map.
|
|
888
|
+
*/
|
|
889
|
+
getAnchors() {
|
|
890
|
+
return this.anchors;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Get anchors for a specific gadget.
|
|
894
|
+
*/
|
|
895
|
+
getGadgetAnchors(gadgetId) {
|
|
896
|
+
const result = [];
|
|
897
|
+
for (const anchor of this.anchors.values()) {
|
|
898
|
+
if (anchor.gadgetId === gadgetId) {
|
|
899
|
+
result.push(anchor);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return result;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Get the anchor tree, optionally rooted at a specific anchor.
|
|
906
|
+
*/
|
|
907
|
+
getAnchorTree(rootId) {
|
|
908
|
+
const childMap = /* @__PURE__ */ new Map();
|
|
909
|
+
for (const anchor of this.anchors.values()) {
|
|
910
|
+
const parentKey = anchor.parent ?? void 0;
|
|
911
|
+
if (!childMap.has(parentKey))
|
|
912
|
+
childMap.set(parentKey, []);
|
|
913
|
+
childMap.get(parentKey).push(anchor);
|
|
914
|
+
}
|
|
915
|
+
const buildNode = (anchor) => {
|
|
916
|
+
const children = (childMap.get(anchor.qualifiedId) ?? []).map(buildNode);
|
|
917
|
+
return { ...anchor, children };
|
|
918
|
+
};
|
|
919
|
+
if (rootId) {
|
|
920
|
+
const root = this.anchors.get(rootId);
|
|
921
|
+
if (!root)
|
|
922
|
+
return [];
|
|
923
|
+
return [buildNode(root)];
|
|
924
|
+
}
|
|
925
|
+
return (childMap.get(void 0) ?? []).map(buildNode);
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Find the anchor at a given point (in room coordinates).
|
|
929
|
+
* Returns the deepest (most specific) anchor containing the point.
|
|
930
|
+
*/
|
|
931
|
+
findAnchorAt(x, y) {
|
|
932
|
+
let best = null;
|
|
933
|
+
let bestArea = Infinity;
|
|
934
|
+
for (const anchor of this.anchors.values()) {
|
|
935
|
+
const b = anchor.bounds;
|
|
936
|
+
if (x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) {
|
|
937
|
+
const area = b.w * b.h;
|
|
938
|
+
if (area < bestArea) {
|
|
939
|
+
best = anchor;
|
|
940
|
+
bestArea = area;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return best;
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Get a serializable snapshot of all anchors.
|
|
948
|
+
*/
|
|
949
|
+
toJSON() {
|
|
950
|
+
return Array.from(this.anchors.values()).map((a) => ({
|
|
951
|
+
id: a.qualifiedId,
|
|
952
|
+
gadgetId: a.gadgetId,
|
|
953
|
+
bounds: a.bounds,
|
|
954
|
+
parent: a.parent,
|
|
955
|
+
occupant: a.occupant,
|
|
956
|
+
attention: a.attention
|
|
957
|
+
}));
|
|
958
|
+
}
|
|
959
|
+
};
|
|
500
960
|
export {
|
|
961
|
+
SceneGraph,
|
|
501
962
|
createGadgetRenderer,
|
|
963
|
+
setupGadgetAwareness,
|
|
502
964
|
setupGadgetBreakout
|
|
503
965
|
};
|
|
504
966
|
//# sourceMappingURL=index.js.map
|