| 483 | type SelectionHandler = (ids: string[]) => void; |
| 484 | |
| 485 | class IframePreviewAdapter implements PreviewAdapter { |
| 486 | private readonly iframe: HTMLIFrameElement; |
| 487 | private readonly _dispatch: ((op: EditOp) => void) | undefined; |
| 488 | |
| 489 | private _selection: string[] = []; |
| 490 | private _handlers: SelectionHandler[] = []; |
| 491 | |
| 492 | /** Tracked id and element for the in-progress drag. */ |
| 493 | private _draftId: string | null = null; |
| 494 | private _draftEl: HTMLElement | null = null; |
| 495 | |
| 496 | constructor(iframe: HTMLIFrameElement, dispatch?: (op: EditOp) => void) { |
| 497 | this.iframe = iframe; |
| 498 | this._dispatch = dispatch; |
| 499 | } |
| 500 | |
| 501 | /** |
| 502 | * Synchronous hit-test. Returns the nearest `[data-hf-id]` element under |
| 503 | * (x, y) in the iframe's coordinate space, or null for a transparent hit |
| 504 | * (root, opacity-0, nothing at all, or a transparent image pixel). |
| 505 | * |
| 506 | * WS-G phase 1: uses elementsFromPoint (z-stack) so a transparent-image hit |
| 507 | * falls through to the layer behind. For <img> elements, the alpha at the |
| 508 | * mapped natural pixel is sampled from an offscreen canvas. Cross-origin |
| 509 | * images that taint the canvas are treated as opaque (safe fallback). |
| 510 | * |
| 511 | * atTime: reflects the GSAP state at the playhead when this is called. |
| 512 | * Seeking to a different time to check visibility is WS-G follow-on. |
| 513 | */ |
| 514 | elementAtPoint(x: number, y: number, _opts?: { atTime?: number }): ElementAtPointResult | null { |
| 515 | const doc = this.iframe.contentDocument; |
| 516 | if (!doc) return null; |
| 517 | const win = this.iframe.contentWindow as (Window & typeof globalThis) | null; |
| 518 | if (!win) return null; |
| 519 | |
| 520 | const stack = hitStack(doc, x, y); |
| 521 | |
| 522 | for (const candidate of stack) { |
| 523 | // One opacity walk per candidate (candidate → root). An opacity:0 element |
| 524 | // is skipped, so the click falls through to the layer painted behind it. |
| 525 | if (!isOpacityVisible(candidate, win)) continue; |
| 526 | |
| 527 | // Image-alpha check: if this is an <img>, verify the pixel is opaque. |
| 528 | if (win.HTMLImageElement && candidate instanceof win.HTMLImageElement) { |
| 529 | if (!imageAlphaOpaqueAt(candidate, x, y, win)) { |
| 530 | // Transparent pixel — fall through to the next element in the stack. |
| 531 | continue; |
| 532 | } |
| 533 | } |
| 534 | |
| 535 | // The candidate's whole ancestor chain is already known visible (the walk |
| 536 | // above covers it, and the hf node is on that chain), so the resolver |
| 537 | // needs no second visibility walk. |
| 538 | const result = resolveNearestHfElement(candidate, () => true); |
| 539 | if (result !== null) return result; |
| 540 | } |
| 541 | |
| 542 | return null; |
nothing calls this directly
no outgoing calls
no test coverage detected