@editframe/react 0.45.2 → 0.45.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"TimelineRoot.js","names":["TimelineRoot: React.FC<TimelineRootProps>"],"sources":["../../src/components/TimelineRoot.tsx"],"sourcesContent":["import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { flushSync } from \"react-dom\";\nimport { useEffect, useRef } from \"react\";\nimport {\n registerCloneFactory,\n unregisterCloneFactory,\n type EFTimegroup,\n} from \"@editframe/elements\";\n\ninterface TimelineRootProps {\n /**\n * Unique identifier used for playback control targeting (Preview, Scrubber, TogglePlay).\n * Also passed as `id` prop to the component for clone rendering.\n */\n id: string;\n /**\n * React component that renders the timeline content (must include a Timegroup at root).\n */\n component: React.ComponentType<Record<string, unknown>>;\n /** Optional CSS class name for the container */\n className?: string;\n /** Optional inline styles for the container */\n style?: React.CSSProperties;\n /** Optional children to render alongside the component (e.g., Configuration wrapper) */\n children?: React.ReactNode;\n}\n\n/**\n * TimelineRoot - Factory wrapper for React-based timelines.\n *\n * This component enables proper clone rendering by registering a clone factory\n * for the managed ef-timegroup element. When render clones are needed\n * (for exports, thumbnails, etc.), the factory mounts a fresh React component\n * tree — producing a fully functional second instance with all hooks, state,\n * and effects running.\n *\n * This is necessary because React DOM cannot be cloned via cloneNode() —\n * cloned elements are dead HTML without React's fiber tree behind them.\n * The factory pattern ensures each clone is a real React mount.\n *\n * @example\n * ```tsx\n * const MyTimeline = () => (\n * <Timegroup mode=\"sequence\">\n * <MyScenes />\n * </Timegroup>\n * );\n *\n * <TimelineRoot id=\"root\" component={MyTimeline} />\n * ```\n */\nexport const TimelineRoot: React.FC<TimelineRootProps> = ({\n id,\n component: Component,\n className,\n style,\n children,\n}) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const componentRef = useRef(Component);\n componentRef.current = Component;\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n // Find the root timegroup rendered by Component\n const timegroup = container.querySelector(\"ef-timegroup\") as EFTimegroup;\n if (!timegroup) {\n throw new Error(\n \"[TimelineRoot] No ef-timegroup found in component. \" +\n \"Ensure your component renders a Timegroup.\",\n );\n }\n\n // Register a clone factory for this element.\n // When createRenderClone is called, it will use this factory\n // to mount a fresh React tree instead of cloning dead DOM.\n registerCloneFactory(timegroup, (cloneContainer: HTMLElement) => {\n const root = ReactDOM.createRoot(cloneContainer);\n const Comp = componentRef.current;\n flushSync(() => {\n root.render(<Comp id={id} />);\n });\n\n const newTimegroup = cloneContainer.querySelector(\n \"ef-timegroup\",\n ) as EFTimegroup | null;\n if (!newTimegroup) {\n throw new Error(\n \"[TimelineRoot] Clone factory did not produce an ef-timegroup. \" +\n \"Ensure your component renders a Timegroup.\",\n );\n }\n\n return {\n timegroup: newTimegroup,\n cleanup: () => {\n root.unmount();\n },\n };\n });\n\n return () => {\n unregisterCloneFactory(timegroup);\n };\n }, [id]);\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={{ display: \"contents\", ...style }}\n >\n {children}\n <Component id={id} />\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,MAAaA,gBAA6C,EACxD,IACA,WAAW,WACX,WACA,OACA,eACI;CACJ,MAAM,eAAe,OAAuB,KAAK;CACjD,MAAM,eAAe,OAAO,UAAU;AACtC,cAAa,UAAU;AAEvB,iBAAgB;EACd,MAAM,YAAY,aAAa;AAC/B,MAAI,CAAC,UAAW;EAGhB,MAAM,YAAY,UAAU,cAAc,eAAe;AACzD,MAAI,CAAC,UACH,OAAM,IAAI,MACR,gGAED;AAMH,uBAAqB,YAAY,mBAAgC;GAC/D,MAAM,OAAO,SAAS,WAAW,eAAe;GAChD,MAAM,OAAO,aAAa;AAC1B,mBAAgB;AACd,SAAK,OAAO,oBAAC,QAAS,KAAM,CAAC;KAC7B;GAEF,MAAM,eAAe,eAAe,cAClC,eACD;AACD,OAAI,CAAC,aACH,OAAM,IAAI,MACR,2GAED;AAGH,UAAO;IACL,WAAW;IACX,eAAe;AACb,UAAK,SAAS;;IAEjB;IACD;AAEF,eAAa;AACX,0BAAuB,UAAU;;IAElC,CAAC,GAAG,CAAC;AAER,QACE,qBAAC;EACC,KAAK;EACM;EACX,OAAO;GAAE,SAAS;GAAY,GAAG;GAAO;aAEvC,UACD,oBAAC,aAAc,KAAM;GACjB"}
1
+ {"version":3,"file":"TimelineRoot.js","names":["TimelineRoot: React.FC<TimelineRootProps>"],"sources":["../../src/components/TimelineRoot.tsx"],"sourcesContent":["import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { flushSync } from \"react-dom\";\nimport { useEffect, useRef } from \"react\";\nimport {\n registerCloneFactory,\n unregisterCloneFactory,\n type EFTimegroup,\n} from \"@editframe/elements\";\n\ninterface TimelineRootProps {\n /**\n * Unique identifier used for playback control targeting (Preview, Scrubber, TogglePlay).\n * Also passed as `id` prop to the component for clone rendering.\n */\n id: string;\n /**\n * React component that renders the timeline content (must include a Timegroup at root).\n */\n component: React.ComponentType<Record<string, unknown>>;\n /** Optional CSS class name for the container */\n className?: string;\n /** Optional inline styles for the container */\n style?: React.CSSProperties;\n /** Optional children to render alongside the component (e.g., Configuration wrapper) */\n children?: React.ReactNode;\n}\n\n/**\n * TimelineRoot - Factory wrapper for React-based timelines.\n *\n * This component enables proper clone rendering by registering a clone factory\n * for the managed ef-timegroup element. When render clones are needed\n * (for exports, thumbnails, etc.), the factory mounts a fresh React component\n * tree — producing a fully functional second instance with all hooks, state,\n * and effects running.\n *\n * This is necessary because React DOM cannot be cloned via cloneNode() —\n * cloned elements are dead HTML without React's fiber tree behind them.\n * The factory pattern ensures each clone is a real React mount.\n *\n * @example\n * ```tsx\n * const MyTimeline = () => (\n * <Timegroup mode=\"sequence\">\n * <MyScenes />\n * </Timegroup>\n * );\n *\n * <TimelineRoot id=\"root\" component={MyTimeline} />\n * ```\n */\nexport const TimelineRoot: React.FC<TimelineRootProps> = ({\n id,\n component: Component,\n className,\n style,\n children,\n}) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const componentRef = useRef(Component);\n componentRef.current = Component;\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n // Find the root timegroup rendered by Component\n const timegroup = container.querySelector(\"ef-timegroup\") as EFTimegroup;\n if (!timegroup) {\n throw new Error(\n \"[TimelineRoot] No ef-timegroup found in component. \" +\n \"Ensure your component renders a Timegroup.\",\n );\n }\n\n // Register a clone factory for this element.\n // When createRenderClone is called, it will use this factory\n // to mount a fresh React tree instead of cloning dead DOM.\n registerCloneFactory(timegroup, (cloneContainer: HTMLElement) => {\n const root = ReactDOM.createRoot(cloneContainer);\n const Comp = componentRef.current;\n flushSync(() => {\n root.render(<Comp id={id} />);\n });\n\n const newTimegroup = cloneContainer.querySelector(\"ef-timegroup\") as EFTimegroup | null;\n if (!newTimegroup) {\n throw new Error(\n \"[TimelineRoot] Clone factory did not produce an ef-timegroup. \" +\n \"Ensure your component renders a Timegroup.\",\n );\n }\n\n return {\n timegroup: newTimegroup,\n cleanup: () => {\n root.unmount();\n },\n };\n });\n\n return () => {\n unregisterCloneFactory(timegroup);\n };\n }, [id]);\n\n return (\n <div ref={containerRef} className={className} style={{ display: \"contents\", ...style }}>\n {children}\n <Component id={id} />\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,MAAaA,gBAA6C,EACxD,IACA,WAAW,WACX,WACA,OACA,eACI;CACJ,MAAM,eAAe,OAAuB,KAAK;CACjD,MAAM,eAAe,OAAO,UAAU;AACtC,cAAa,UAAU;AAEvB,iBAAgB;EACd,MAAM,YAAY,aAAa;AAC/B,MAAI,CAAC,UAAW;EAGhB,MAAM,YAAY,UAAU,cAAc,eAAe;AACzD,MAAI,CAAC,UACH,OAAM,IAAI,MACR,gGAED;AAMH,uBAAqB,YAAY,mBAAgC;GAC/D,MAAM,OAAO,SAAS,WAAW,eAAe;GAChD,MAAM,OAAO,aAAa;AAC1B,mBAAgB;AACd,SAAK,OAAO,oBAAC,QAAS,KAAM,CAAC;KAC7B;GAEF,MAAM,eAAe,eAAe,cAAc,eAAe;AACjE,OAAI,CAAC,aACH,OAAM,IAAI,MACR,2GAED;AAGH,UAAO;IACL,WAAW;IACX,eAAe;AACb,UAAK,SAAS;;IAEjB;IACD;AAEF,eAAa;AACX,0BAAuB,UAAU;;IAElC,CAAC,GAAG,CAAC;AAER,QACE,qBAAC;EAAI,KAAK;EAAyB;EAAW,OAAO;GAAE,SAAS;GAAY,GAAG;GAAO;aACnF,UACD,oBAAC,aAAc,KAAM;GACjB"}
@@ -1 +1 @@
1
- {"version":3,"file":"OverlayItem.js","names":["EFOverlayItemElement"],"sources":["../../src/gui/OverlayItem.tsx"],"sourcesContent":["import { EFOverlayItem as EFOverlayItemElement } from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent } from \"../hooks/create-element\";\n\nexport interface OverlayItemProps {\n /** Element ID - automatically resolves to [data-element-id] or [data-timegroup-id] selector */\n elementId?: string;\n /** Target element or selector - use for custom targeting when elementId doesn't work */\n target?: HTMLElement | string;\n /** Called when position changes. Receives CustomEvent with OverlayItemPosition as detail. */\n onPositionChanged?: (e: Event) => void;\n}\n\nexport const OverlayItem = createComponent<\n EFOverlayItemElement,\n { onPositionChanged: \"position-changed\" }\n>({\n tagName: \"ef-overlay-item\",\n elementClass: EFOverlayItemElement,\n react: React,\n displayName: \"OverlayItem\",\n events: {\n onPositionChanged: \"position-changed\",\n },\n}) as React.ForwardRefExoticComponent<\n OverlayItemProps & React.RefAttributes<EFOverlayItemElement>\n>;\n"],"mappings":";;;;;AAaA,MAAa,cAAc,gBAGzB;CACA,SAAS;CACT,cAAcA;CACd,OAAO;CACP,aAAa;CACb,QAAQ,EACN,mBAAmB,oBACpB;CACF,CAAC"}
1
+ {"version":3,"file":"OverlayItem.js","names":["EFOverlayItemElement"],"sources":["../../src/gui/OverlayItem.tsx"],"sourcesContent":["import { EFOverlayItem as EFOverlayItemElement } from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent } from \"../hooks/create-element\";\n\nexport interface OverlayItemProps {\n /** Element ID - automatically resolves to [data-element-id] or [data-timegroup-id] selector */\n elementId?: string;\n /** Target element or selector - use for custom targeting when elementId doesn't work */\n target?: HTMLElement | string;\n /** Called when position changes. Receives CustomEvent with OverlayItemPosition as detail. */\n onPositionChanged?: (e: Event) => void;\n}\n\nexport const OverlayItem = createComponent<\n EFOverlayItemElement,\n { onPositionChanged: \"position-changed\" }\n>({\n tagName: \"ef-overlay-item\",\n elementClass: EFOverlayItemElement,\n react: React,\n displayName: \"OverlayItem\",\n events: {\n onPositionChanged: \"position-changed\",\n },\n}) as React.ForwardRefExoticComponent<OverlayItemProps & React.RefAttributes<EFOverlayItemElement>>;\n"],"mappings":";;;;;AAaA,MAAa,cAAc,gBAGzB;CACA,SAAS;CACT,cAAcA;CACd,OAAO;CACP,aAAa;CACb,QAAQ,EACN,mBAAmB,oBACpB;CACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"Scrubber.js","names":[],"sources":["../../src/gui/Scrubber.tsx"],"sourcesContent":["import { EFScrubber } from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent } from \"../hooks/create-element\";\n\nexport interface ScrubberProps {\n target?: string;\n orientation?: \"horizontal\" | \"vertical\";\n currentTimeMs?: number;\n durationMs?: number;\n zoomScale?: number;\n containerWidth?: number;\n fps?: number;\n rawScrubTimeMs?: number | null;\n scrollContainerRef?: React.RefObject<HTMLElement>;\n isScrubbingRef?: React.MutableRefObject<boolean>;\n onSeek?: (time: number) => void;\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst BaseScrubber = createComponent<EFScrubber, { onSeek: \"seek\" }>({\n tagName: \"ef-scrubber\",\n elementClass: EFScrubber,\n react: React,\n displayName: \"Scrubber\",\n events: {\n onSeek: \"seek\",\n },\n}) as React.ForwardRefExoticComponent<\n ScrubberProps & React.RefAttributes<EFScrubber>\n>;\n\nexport const Scrubber = React.forwardRef<EFScrubber, ScrubberProps>(\n (props, ref) => {\n const { scrollContainerRef, isScrubbingRef, ...restProps } = props;\n const elementRef = React.useRef<EFScrubber | null>(null);\n\n React.useLayoutEffect(() => {\n if (elementRef.current) {\n if (scrollContainerRef?.current) {\n elementRef.current.scrollContainerRef = scrollContainerRef;\n } else {\n elementRef.current.scrollContainerRef = undefined;\n }\n if (isScrubbingRef) {\n elementRef.current.isScrubbingRef = isScrubbingRef;\n } else {\n elementRef.current.isScrubbingRef = undefined;\n }\n }\n }, [scrollContainerRef?.current, isScrubbingRef]);\n\n return (\n <BaseScrubber\n {...restProps}\n ref={(node) => {\n elementRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n }}\n />\n );\n },\n);\n\nScrubber.displayName = \"Scrubber\";\n"],"mappings":";;;;;;AAoBA,MAAM,eAAe,gBAAgD;CACnE,SAAS;CACT,cAAc;CACd,OAAO;CACP,aAAa;CACb,QAAQ,EACN,QAAQ,QACT;CACF,CAAC;AAIF,MAAa,WAAW,MAAM,YAC3B,OAAO,QAAQ;CACd,MAAM,EAAE,oBAAoB,eAAgB,GAAG,cAAc;CAC7D,MAAM,aAAa,MAAM,OAA0B,KAAK;AAExD,OAAM,sBAAsB;AAC1B,MAAI,WAAW,SAAS;AACtB,OAAI,oBAAoB,QACtB,YAAW,QAAQ,qBAAqB;OAExC,YAAW,QAAQ,qBAAqB;AAE1C,OAAI,eACF,YAAW,QAAQ,iBAAiB;OAEpC,YAAW,QAAQ,iBAAiB;;IAGvC,CAAC,oBAAoB,SAAS,eAAe,CAAC;AAEjD,QACE,oBAAC;EACC,GAAI;EACJ,MAAM,SAAS;AACb,cAAW,UAAU;AACrB,OAAI,OAAO,QAAQ,WAAY,KAAI,KAAK;YAC/B,IAAK,KAAI,UAAU;;GAE9B;EAGP;AAED,SAAS,cAAc"}
1
+ {"version":3,"file":"Scrubber.js","names":[],"sources":["../../src/gui/Scrubber.tsx"],"sourcesContent":["import { EFScrubber } from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent } from \"../hooks/create-element\";\n\nexport interface ScrubberProps {\n target?: string;\n orientation?: \"horizontal\" | \"vertical\";\n currentTimeMs?: number;\n durationMs?: number;\n zoomScale?: number;\n containerWidth?: number;\n fps?: number;\n rawScrubTimeMs?: number | null;\n scrollContainerRef?: React.RefObject<HTMLElement>;\n isScrubbingRef?: React.MutableRefObject<boolean>;\n onSeek?: (time: number) => void;\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst BaseScrubber = createComponent<EFScrubber, { onSeek: \"seek\" }>({\n tagName: \"ef-scrubber\",\n elementClass: EFScrubber,\n react: React,\n displayName: \"Scrubber\",\n events: {\n onSeek: \"seek\",\n },\n}) as React.ForwardRefExoticComponent<ScrubberProps & React.RefAttributes<EFScrubber>>;\n\nexport const Scrubber = React.forwardRef<EFScrubber, ScrubberProps>((props, ref) => {\n const { scrollContainerRef, isScrubbingRef, ...restProps } = props;\n const elementRef = React.useRef<EFScrubber | null>(null);\n\n React.useLayoutEffect(() => {\n if (elementRef.current) {\n if (scrollContainerRef?.current) {\n elementRef.current.scrollContainerRef = scrollContainerRef;\n } else {\n elementRef.current.scrollContainerRef = undefined;\n }\n if (isScrubbingRef) {\n elementRef.current.isScrubbingRef = isScrubbingRef;\n } else {\n elementRef.current.isScrubbingRef = undefined;\n }\n }\n }, [scrollContainerRef?.current, isScrubbingRef]);\n\n return (\n <BaseScrubber\n {...restProps}\n ref={(node) => {\n elementRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n }}\n />\n );\n});\n\nScrubber.displayName = \"Scrubber\";\n"],"mappings":";;;;;;AAoBA,MAAM,eAAe,gBAAgD;CACnE,SAAS;CACT,cAAc;CACd,OAAO;CACP,aAAa;CACb,QAAQ,EACN,QAAQ,QACT;CACF,CAAC;AAEF,MAAa,WAAW,MAAM,YAAuC,OAAO,QAAQ;CAClF,MAAM,EAAE,oBAAoB,eAAgB,GAAG,cAAc;CAC7D,MAAM,aAAa,MAAM,OAA0B,KAAK;AAExD,OAAM,sBAAsB;AAC1B,MAAI,WAAW,SAAS;AACtB,OAAI,oBAAoB,QACtB,YAAW,QAAQ,qBAAqB;OAExC,YAAW,QAAQ,qBAAqB;AAE1C,OAAI,eACF,YAAW,QAAQ,iBAAiB;OAEpC,YAAW,QAAQ,iBAAiB;;IAGvC,CAAC,oBAAoB,SAAS,eAAe,CAAC;AAEjD,QACE,oBAAC;EACC,GAAI;EACJ,MAAM,SAAS;AACb,cAAW,UAAU;AACrB,OAAI,OAAO,QAAQ,WAAY,KAAI,KAAK;YAC/B,IAAK,KAAI,UAAU;;GAE9B;EAEJ;AAEF,SAAS,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"TimelineRuler.js","names":[],"sources":["../../src/gui/TimelineRuler.tsx"],"sourcesContent":["import { EFTimelineRuler } from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent } from \"../hooks/create-element\";\n\nexport interface TimelineRulerProps {\n durationMs?: number;\n zoomScale?: number;\n containerWidth?: number;\n fps?: number;\n scrollContainerSelector?: string;\n scrollContainerRef?: React.RefObject<HTMLElement>;\n}\n\nconst BaseTimelineRuler = createComponent<EFTimelineRuler, {}>({\n tagName: \"ef-timeline-ruler\",\n elementClass: EFTimelineRuler,\n react: React,\n displayName: \"TimelineRuler\",\n}) as React.ForwardRefExoticComponent<\n TimelineRulerProps & React.RefAttributes<EFTimelineRuler>\n>;\n\nexport const TimelineRuler = React.forwardRef<\n EFTimelineRuler,\n TimelineRulerProps\n>((props, ref) => {\n const { scrollContainerRef, ...restProps } = props;\n const elementRef = React.useRef<EFTimelineRuler | null>(null);\n\n React.useLayoutEffect(() => {\n if (elementRef.current && scrollContainerRef?.current) {\n (elementRef.current as any).scrollContainerElement =\n scrollContainerRef.current;\n } else if (elementRef.current) {\n (elementRef.current as any).scrollContainerElement = null;\n }\n }, [scrollContainerRef?.current]);\n\n return (\n <BaseTimelineRuler\n {...(restProps as any)}\n ref={(node) => {\n elementRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n }}\n />\n );\n});\n\nTimelineRuler.displayName = \"TimelineRuler\";\n"],"mappings":";;;;;;AAaA,MAAM,oBAAoB,gBAAqC;CAC7D,SAAS;CACT,cAAc;CACd,OAAO;CACP,aAAa;CACd,CAAC;AAIF,MAAa,gBAAgB,MAAM,YAGhC,OAAO,QAAQ;CAChB,MAAM,EAAE,mBAAoB,GAAG,cAAc;CAC7C,MAAM,aAAa,MAAM,OAA+B,KAAK;AAE7D,OAAM,sBAAsB;AAC1B,MAAI,WAAW,WAAW,oBAAoB,QAC5C,CAAC,WAAW,QAAgB,yBAC1B,mBAAmB;WACZ,WAAW,QACpB,CAAC,WAAW,QAAgB,yBAAyB;IAEtD,CAAC,oBAAoB,QAAQ,CAAC;AAEjC,QACE,oBAAC;EACC,GAAK;EACL,MAAM,SAAS;AACb,cAAW,UAAU;AACrB,OAAI,OAAO,QAAQ,WAAY,KAAI,KAAK;YAC/B,IAAK,KAAI,UAAU;;GAE9B;EAEJ;AAEF,cAAc,cAAc"}
1
+ {"version":3,"file":"TimelineRuler.js","names":[],"sources":["../../src/gui/TimelineRuler.tsx"],"sourcesContent":["import { EFTimelineRuler } from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent } from \"../hooks/create-element\";\n\nexport interface TimelineRulerProps {\n durationMs?: number;\n zoomScale?: number;\n containerWidth?: number;\n fps?: number;\n scrollContainerSelector?: string;\n scrollContainerRef?: React.RefObject<HTMLElement>;\n}\n\nconst BaseTimelineRuler = createComponent<EFTimelineRuler, {}>({\n tagName: \"ef-timeline-ruler\",\n elementClass: EFTimelineRuler,\n react: React,\n displayName: \"TimelineRuler\",\n}) as React.ForwardRefExoticComponent<TimelineRulerProps & React.RefAttributes<EFTimelineRuler>>;\n\nexport const TimelineRuler = React.forwardRef<EFTimelineRuler, TimelineRulerProps>((props, ref) => {\n const { scrollContainerRef, ...restProps } = props;\n const elementRef = React.useRef<EFTimelineRuler | null>(null);\n\n React.useLayoutEffect(() => {\n if (elementRef.current && scrollContainerRef?.current) {\n (elementRef.current as any).scrollContainerElement = scrollContainerRef.current;\n } else if (elementRef.current) {\n (elementRef.current as any).scrollContainerElement = null;\n }\n }, [scrollContainerRef?.current]);\n\n return (\n <BaseTimelineRuler\n {...(restProps as any)}\n ref={(node) => {\n elementRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n }}\n />\n );\n});\n\nTimelineRuler.displayName = \"TimelineRuler\";\n"],"mappings":";;;;;;AAaA,MAAM,oBAAoB,gBAAqC;CAC7D,SAAS;CACT,cAAc;CACd,OAAO;CACP,aAAa;CACd,CAAC;AAEF,MAAa,gBAAgB,MAAM,YAAiD,OAAO,QAAQ;CACjG,MAAM,EAAE,mBAAoB,GAAG,cAAc;CAC7C,MAAM,aAAa,MAAM,OAA+B,KAAK;AAE7D,OAAM,sBAAsB;AAC1B,MAAI,WAAW,WAAW,oBAAoB,QAC5C,CAAC,WAAW,QAAgB,yBAAyB,mBAAmB;WAC/D,WAAW,QACpB,CAAC,WAAW,QAAgB,yBAAyB;IAEtD,CAAC,oBAAoB,QAAQ,CAAC;AAEjC,QACE,oBAAC;EACC,GAAK;EACL,MAAM,SAAS;AACb,cAAW,UAAU;AACrB,OAAI,OAAO,QAAQ,WAAY,KAAI,KAAK;YAC/B,IAAK,KAAI,UAAU;;GAE9B;EAEJ;AAEF,cAAc,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"TrimHandles.js","names":["EFTrimHandlesElement"],"sources":["../../src/gui/TrimHandles.ts"],"sourcesContent":["import {\n EFTrimHandles as EFTrimHandlesElement,\n type TrimChangeDetail,\n} from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent, type EventName } from \"../hooks/create-element\";\n\nexport const TrimHandles = createComponent({\n tagName: \"ef-trim-handles\",\n elementClass: EFTrimHandlesElement,\n react: React,\n events: {\n onTrimChange: \"trim-change\" as EventName<CustomEvent<TrimChangeDetail>>,\n onTrimChangeEnd: \"trim-change-end\" as EventName<\n CustomEvent<TrimChangeDetail>\n >,\n },\n});\n"],"mappings":";;;;;AAOA,MAAa,cAAc,gBAAgB;CACzC,SAAS;CACT,cAAcA;CACd,OAAO;CACP,QAAQ;EACN,cAAc;EACd,iBAAiB;EAGlB;CACF,CAAC"}
1
+ {"version":3,"file":"TrimHandles.js","names":["EFTrimHandlesElement"],"sources":["../../src/gui/TrimHandles.ts"],"sourcesContent":["import { EFTrimHandles as EFTrimHandlesElement, type TrimChangeDetail } from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent, type EventName } from \"../hooks/create-element\";\n\nexport const TrimHandles = createComponent({\n tagName: \"ef-trim-handles\",\n elementClass: EFTrimHandlesElement,\n react: React,\n events: {\n onTrimChange: \"trim-change\" as EventName<CustomEvent<TrimChangeDetail>>,\n onTrimChangeEnd: \"trim-change-end\" as EventName<CustomEvent<TrimChangeDetail>>,\n },\n});\n"],"mappings":";;;;;AAIA,MAAa,cAAc,gBAAgB;CACzC,SAAS;CACT,cAAcA;CACd,OAAO;CACP,QAAQ;EACN,cAAc;EACd,iBAAiB;EAClB;CACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"create-element.js","names":["React","reactProps: Record<string, unknown>","elementProps: Record<string, unknown>"],"sources":["../../src/hooks/create-element.ts"],"sourcesContent":["import React from \"react\";\n\nlet isomorphicEffect =\n typeof window !== \"undefined\" ? React.useLayoutEffect : React.useEffect;\n\nexport function setIsomorphicEffect(\n effect: typeof React.useLayoutEffect | typeof React.useEffect,\n) {\n isomorphicEffect = effect;\n}\n\nconst reservedReactProperties = new Set([\n \"children\",\n \"localName\",\n \"ref\",\n \"style\",\n \"className\",\n]);\nconst listenedEvents = new WeakMap<Element, Map<string, EventListenerObject>>();\n\ntype Constructor<T> = { new (): T };\n\n/**\n * Branded string that carries the DOM event type at the type level.\n * At runtime it's just a string (the DOM event name).\n */\nexport type EventName<T extends Event = Event> = string & { __eventType?: T };\n\ntype EventNames = Record<string, EventName>;\n\ntype EventListeners<E extends EventNames> = {\n [K in keyof E]?: (e: E[K] extends EventName<infer T> ? T : Event) => void;\n};\n\ntype ElementProps<I> = Partial<Omit<I, keyof HTMLElement>>;\ntype ComponentProps<I, E extends EventNames = {}> = Omit<\n React.HTMLAttributes<I>,\n keyof E | keyof ElementProps<I>\n> &\n EventListeners<E> &\n ElementProps<I>;\n\nexport type ReactWebComponent<\n I extends HTMLElement,\n E extends EventNames = {},\n> = React.ForwardRefExoticComponent<\n ComponentProps<I, E> & React.RefAttributes<I>\n>;\n\nexport interface Options<I extends HTMLElement, E extends EventNames = {}> {\n react: typeof React;\n tagName: string;\n elementClass: Constructor<I>;\n events?: E;\n displayName?: string;\n}\n\nfunction addOrUpdateEventListener(\n node: Element,\n event: string,\n listener?: (e?: Event) => void,\n) {\n let events = listenedEvents.get(node);\n if (!events) {\n events = new Map();\n listenedEvents.set(node, events);\n }\n let handler = events.get(event);\n\n if (listener) {\n if (!handler) {\n handler = { handleEvent: listener };\n events.set(event, handler);\n node.addEventListener(event, handler);\n } else {\n handler.handleEvent = listener;\n }\n } else if (handler) {\n events.delete(event);\n node.removeEventListener(event, handler);\n }\n}\n\nfunction setProperty<E extends Element>(\n node: E,\n name: string,\n value: unknown,\n old: unknown,\n events?: EventNames,\n) {\n const event = events?.[name];\n if (event) {\n if (value !== old)\n addOrUpdateEventListener(node, event, value as (e?: Event) => void);\n return;\n }\n node[name as keyof E] = value as E[keyof E];\n if (\n (value === undefined || value === null) &&\n name in HTMLElement.prototype\n ) {\n node.removeAttribute(name);\n }\n}\n\nexport function createComponent<\n I extends HTMLElement,\n E extends EventNames = {},\n>({\n react: React,\n tagName,\n elementClass,\n events,\n displayName,\n}: Options<I, E>): ReactWebComponent<I, E> {\n const eventProps = new Set(Object.keys(events ?? {}));\n\n const ReactComponent = React.forwardRef<I, ComponentProps<I, E>>(\n (props, ref) => {\n const elementRef = React.useRef<I | null>(null);\n const prevPropsRef = React.useRef(new Map<string, unknown>());\n\n const reactProps: Record<string, unknown> = {\n suppressHydrationWarning: true,\n };\n const elementProps: Record<string, unknown> = {};\n\n for (const [k, v] of Object.entries(props)) {\n if (reservedReactProperties.has(k)) {\n reactProps[k === \"className\" ? \"class\" : k] = v;\n continue;\n }\n if (eventProps.has(k) || k in elementClass.prototype) {\n elementProps[k] = v;\n continue;\n }\n reactProps[k] = v;\n }\n\n isomorphicEffect(() => {\n if (!elementRef.current) return;\n const newProps = new Map<string, unknown>();\n for (const key in elementProps) {\n setProperty(\n elementRef.current,\n key,\n props[key as keyof typeof props],\n prevPropsRef.current.get(key),\n events,\n );\n prevPropsRef.current.delete(key);\n newProps.set(key, props[key as keyof typeof props]);\n }\n for (const [key, value] of prevPropsRef.current) {\n setProperty(elementRef.current, key, undefined, value, events);\n }\n prevPropsRef.current = newProps;\n\n // Remove defer-hydration if present\n elementRef.current.removeAttribute(\"defer-hydration\");\n }, [props]);\n\n return React.createElement(tagName, {\n ...reactProps,\n ref: (node: I) => {\n elementRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n },\n });\n },\n );\n\n ReactComponent.displayName = displayName ?? elementClass.name;\n return ReactComponent as ReactWebComponent<I, E>;\n}\n"],"mappings":";;;AAEA,IAAI,mBACF,OAAO,WAAW,cAAc,MAAM,kBAAkB,MAAM;AAQhE,MAAM,0BAA0B,IAAI,IAAI;CACtC;CACA;CACA;CACA;CACA;CACD,CAAC;AACF,MAAM,iCAAiB,IAAI,SAAoD;AAuC/E,SAAS,yBACP,MACA,OACA,UACA;CACA,IAAI,SAAS,eAAe,IAAI,KAAK;AACrC,KAAI,CAAC,QAAQ;AACX,2BAAS,IAAI,KAAK;AAClB,iBAAe,IAAI,MAAM,OAAO;;CAElC,IAAI,UAAU,OAAO,IAAI,MAAM;AAE/B,KAAI,SACF,KAAI,CAAC,SAAS;AACZ,YAAU,EAAE,aAAa,UAAU;AACnC,SAAO,IAAI,OAAO,QAAQ;AAC1B,OAAK,iBAAiB,OAAO,QAAQ;OAErC,SAAQ,cAAc;UAEf,SAAS;AAClB,SAAO,OAAO,MAAM;AACpB,OAAK,oBAAoB,OAAO,QAAQ;;;AAI5C,SAAS,YACP,MACA,MACA,OACA,KACA,QACA;CACA,MAAM,QAAQ,SAAS;AACvB,KAAI,OAAO;AACT,MAAI,UAAU,IACZ,0BAAyB,MAAM,OAAO,MAA6B;AACrE;;AAEF,MAAK,QAAmB;AACxB,MACG,UAAU,UAAa,UAAU,SAClC,QAAQ,YAAY,UAEpB,MAAK,gBAAgB,KAAK;;AAI9B,SAAgB,gBAGd,EACA,OAAOA,SACP,SACA,cACA,QACA,eACyC;CACzC,MAAM,aAAa,IAAI,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC,CAAC;CAErD,MAAM,iBAAiBA,QAAM,YAC1B,OAAO,QAAQ;EACd,MAAM,aAAaA,QAAM,OAAiB,KAAK;EAC/C,MAAM,eAAeA,QAAM,uBAAO,IAAI,KAAsB,CAAC;EAE7D,MAAMC,aAAsC,EAC1C,0BAA0B,MAC3B;EACD,MAAMC,eAAwC,EAAE;AAEhD,OAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAM,EAAE;AAC1C,OAAI,wBAAwB,IAAI,EAAE,EAAE;AAClC,eAAW,MAAM,cAAc,UAAU,KAAK;AAC9C;;AAEF,OAAI,WAAW,IAAI,EAAE,IAAI,KAAK,aAAa,WAAW;AACpD,iBAAa,KAAK;AAClB;;AAEF,cAAW,KAAK;;AAGlB,yBAAuB;AACrB,OAAI,CAAC,WAAW,QAAS;GACzB,MAAM,2BAAW,IAAI,KAAsB;AAC3C,QAAK,MAAM,OAAO,cAAc;AAC9B,gBACE,WAAW,SACX,KACA,MAAM,MACN,aAAa,QAAQ,IAAI,IAAI,EAC7B,OACD;AACD,iBAAa,QAAQ,OAAO,IAAI;AAChC,aAAS,IAAI,KAAK,MAAM,KAA2B;;AAErD,QAAK,MAAM,CAAC,KAAK,UAAU,aAAa,QACtC,aAAY,WAAW,SAAS,KAAK,QAAW,OAAO,OAAO;AAEhE,gBAAa,UAAU;AAGvB,cAAW,QAAQ,gBAAgB,kBAAkB;KACpD,CAAC,MAAM,CAAC;AAEX,SAAOF,QAAM,cAAc,SAAS;GAClC,GAAG;GACH,MAAM,SAAY;AAChB,eAAW,UAAU;AACrB,QAAI,OAAO,QAAQ,WAAY,KAAI,KAAK;aAC/B,IAAK,KAAI,UAAU;;GAE/B,CAAC;GAEL;AAED,gBAAe,cAAc,eAAe,aAAa;AACzD,QAAO"}
1
+ {"version":3,"file":"create-element.js","names":["React","reactProps: Record<string, unknown>","elementProps: Record<string, unknown>"],"sources":["../../src/hooks/create-element.ts"],"sourcesContent":["import React from \"react\";\n\nlet isomorphicEffect = typeof window !== \"undefined\" ? React.useLayoutEffect : React.useEffect;\n\nexport function setIsomorphicEffect(effect: typeof React.useLayoutEffect | typeof React.useEffect) {\n isomorphicEffect = effect;\n}\n\nconst reservedReactProperties = new Set([\"children\", \"localName\", \"ref\", \"style\", \"className\"]);\nconst listenedEvents = new WeakMap<Element, Map<string, EventListenerObject>>();\n\ntype Constructor<T> = { new (): T };\n\n/**\n * Branded string that carries the DOM event type at the type level.\n * At runtime it's just a string (the DOM event name).\n */\nexport type EventName<T extends Event = Event> = string & { __eventType?: T };\n\ntype EventNames = Record<string, EventName>;\n\ntype EventListeners<E extends EventNames> = {\n [K in keyof E]?: (e: E[K] extends EventName<infer T> ? T : Event) => void;\n};\n\ntype ElementProps<I> = Partial<Omit<I, keyof HTMLElement>>;\ntype ComponentProps<I, E extends EventNames = {}> = Omit<\n React.HTMLAttributes<I>,\n keyof E | keyof ElementProps<I>\n> &\n EventListeners<E> &\n ElementProps<I>;\n\nexport type ReactWebComponent<\n I extends HTMLElement,\n E extends EventNames = {},\n> = React.ForwardRefExoticComponent<ComponentProps<I, E> & React.RefAttributes<I>>;\n\nexport interface Options<I extends HTMLElement, E extends EventNames = {}> {\n react: typeof React;\n tagName: string;\n elementClass: Constructor<I>;\n events?: E;\n displayName?: string;\n}\n\nfunction addOrUpdateEventListener(node: Element, event: string, listener?: (e?: Event) => void) {\n let events = listenedEvents.get(node);\n if (!events) {\n events = new Map();\n listenedEvents.set(node, events);\n }\n let handler = events.get(event);\n\n if (listener) {\n if (!handler) {\n handler = { handleEvent: listener };\n events.set(event, handler);\n node.addEventListener(event, handler);\n } else {\n handler.handleEvent = listener;\n }\n } else if (handler) {\n events.delete(event);\n node.removeEventListener(event, handler);\n }\n}\n\nfunction setProperty<E extends Element>(\n node: E,\n name: string,\n value: unknown,\n old: unknown,\n events?: EventNames,\n) {\n const event = events?.[name];\n if (event) {\n if (value !== old) addOrUpdateEventListener(node, event, value as (e?: Event) => void);\n return;\n }\n node[name as keyof E] = value as E[keyof E];\n if ((value === undefined || value === null) && name in HTMLElement.prototype) {\n node.removeAttribute(name);\n }\n}\n\nexport function createComponent<I extends HTMLElement, E extends EventNames = {}>({\n react: React,\n tagName,\n elementClass,\n events,\n displayName,\n}: Options<I, E>): ReactWebComponent<I, E> {\n const eventProps = new Set(Object.keys(events ?? {}));\n\n const ReactComponent = React.forwardRef<I, ComponentProps<I, E>>((props, ref) => {\n const elementRef = React.useRef<I | null>(null);\n const prevPropsRef = React.useRef(new Map<string, unknown>());\n\n const reactProps: Record<string, unknown> = {\n suppressHydrationWarning: true,\n };\n const elementProps: Record<string, unknown> = {};\n\n for (const [k, v] of Object.entries(props)) {\n if (reservedReactProperties.has(k)) {\n reactProps[k === \"className\" ? \"class\" : k] = v;\n continue;\n }\n if (eventProps.has(k) || k in elementClass.prototype) {\n elementProps[k] = v;\n continue;\n }\n reactProps[k] = v;\n }\n\n isomorphicEffect(() => {\n if (!elementRef.current) return;\n const newProps = new Map<string, unknown>();\n for (const key in elementProps) {\n setProperty(\n elementRef.current,\n key,\n props[key as keyof typeof props],\n prevPropsRef.current.get(key),\n events,\n );\n prevPropsRef.current.delete(key);\n newProps.set(key, props[key as keyof typeof props]);\n }\n for (const [key, value] of prevPropsRef.current) {\n setProperty(elementRef.current, key, undefined, value, events);\n }\n prevPropsRef.current = newProps;\n\n // Remove defer-hydration if present\n elementRef.current.removeAttribute(\"defer-hydration\");\n }, [props]);\n\n return React.createElement(tagName, {\n ...reactProps,\n ref: (node: I) => {\n elementRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n },\n });\n });\n\n ReactComponent.displayName = displayName ?? elementClass.name;\n return ReactComponent as ReactWebComponent<I, E>;\n}\n"],"mappings":";;;AAEA,IAAI,mBAAmB,OAAO,WAAW,cAAc,MAAM,kBAAkB,MAAM;AAMrF,MAAM,0BAA0B,IAAI,IAAI;CAAC;CAAY;CAAa;CAAO;CAAS;CAAY,CAAC;AAC/F,MAAM,iCAAiB,IAAI,SAAoD;AAqC/E,SAAS,yBAAyB,MAAe,OAAe,UAAgC;CAC9F,IAAI,SAAS,eAAe,IAAI,KAAK;AACrC,KAAI,CAAC,QAAQ;AACX,2BAAS,IAAI,KAAK;AAClB,iBAAe,IAAI,MAAM,OAAO;;CAElC,IAAI,UAAU,OAAO,IAAI,MAAM;AAE/B,KAAI,SACF,KAAI,CAAC,SAAS;AACZ,YAAU,EAAE,aAAa,UAAU;AACnC,SAAO,IAAI,OAAO,QAAQ;AAC1B,OAAK,iBAAiB,OAAO,QAAQ;OAErC,SAAQ,cAAc;UAEf,SAAS;AAClB,SAAO,OAAO,MAAM;AACpB,OAAK,oBAAoB,OAAO,QAAQ;;;AAI5C,SAAS,YACP,MACA,MACA,OACA,KACA,QACA;CACA,MAAM,QAAQ,SAAS;AACvB,KAAI,OAAO;AACT,MAAI,UAAU,IAAK,0BAAyB,MAAM,OAAO,MAA6B;AACtF;;AAEF,MAAK,QAAmB;AACxB,MAAK,UAAU,UAAa,UAAU,SAAS,QAAQ,YAAY,UACjE,MAAK,gBAAgB,KAAK;;AAI9B,SAAgB,gBAAkE,EAChF,OAAOA,SACP,SACA,cACA,QACA,eACyC;CACzC,MAAM,aAAa,IAAI,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC,CAAC;CAErD,MAAM,iBAAiBA,QAAM,YAAqC,OAAO,QAAQ;EAC/E,MAAM,aAAaA,QAAM,OAAiB,KAAK;EAC/C,MAAM,eAAeA,QAAM,uBAAO,IAAI,KAAsB,CAAC;EAE7D,MAAMC,aAAsC,EAC1C,0BAA0B,MAC3B;EACD,MAAMC,eAAwC,EAAE;AAEhD,OAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAM,EAAE;AAC1C,OAAI,wBAAwB,IAAI,EAAE,EAAE;AAClC,eAAW,MAAM,cAAc,UAAU,KAAK;AAC9C;;AAEF,OAAI,WAAW,IAAI,EAAE,IAAI,KAAK,aAAa,WAAW;AACpD,iBAAa,KAAK;AAClB;;AAEF,cAAW,KAAK;;AAGlB,yBAAuB;AACrB,OAAI,CAAC,WAAW,QAAS;GACzB,MAAM,2BAAW,IAAI,KAAsB;AAC3C,QAAK,MAAM,OAAO,cAAc;AAC9B,gBACE,WAAW,SACX,KACA,MAAM,MACN,aAAa,QAAQ,IAAI,IAAI,EAC7B,OACD;AACD,iBAAa,QAAQ,OAAO,IAAI;AAChC,aAAS,IAAI,KAAK,MAAM,KAA2B;;AAErD,QAAK,MAAM,CAAC,KAAK,UAAU,aAAa,QACtC,aAAY,WAAW,SAAS,KAAK,QAAW,OAAO,OAAO;AAEhE,gBAAa,UAAU;AAGvB,cAAW,QAAQ,gBAAgB,kBAAkB;KACpD,CAAC,MAAM,CAAC;AAEX,SAAOF,QAAM,cAAc,SAAS;GAClC,GAAG;GACH,MAAM,SAAY;AAChB,eAAW,UAAU;AACrB,QAAI,OAAO,QAAQ,WAAY,KAAI,KAAK;aAC/B,IAAK,KAAI,UAAU;;GAE/B,CAAC;GACF;AAEF,gBAAe,cAAc,eAAe,aAAa;AACzD,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"useMediaInfo.js","names":["host: MediaElement","setMediaInfo: React.Dispatch<React.SetStateAction<MediaInfo>>","#isConnected","#lastIntrinsicDurationMs","#updateReactState"],"sources":["../../src/hooks/useMediaInfo.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from \"lit\";\nimport { useEffect, useState } from \"react\";\n\nexport interface MediaInfo {\n intrinsicDurationMs: number | undefined;\n loading: boolean;\n}\n\ntype MediaElement = ReactiveControllerHost & {\n intrinsicDurationMs: number | undefined;\n isConnected: boolean;\n};\n\nexport class MediaInfoController implements ReactiveController {\n #isConnected = false;\n #lastIntrinsicDurationMs: number | undefined = undefined;\n\n constructor(\n private host: MediaElement,\n private setMediaInfo: React.Dispatch<React.SetStateAction<MediaInfo>>,\n ) {\n this.host.addController(this);\n }\n\n hostConnected(): void {\n this.#isConnected = true;\n }\n\n hostDisconnected(): void {\n this.#isConnected = false;\n this.host.removeController(this);\n }\n\n hostUpdated(): void {\n if (!this.#isConnected) return;\n\n const intrinsicDurationMs = this.host.intrinsicDurationMs;\n\n if (intrinsicDurationMs !== this.#lastIntrinsicDurationMs) {\n this.#lastIntrinsicDurationMs = intrinsicDurationMs;\n this.#updateReactState();\n }\n }\n\n #updateReactState(): void {\n const intrinsicDurationMs = this.host.intrinsicDurationMs;\n this.setMediaInfo({\n intrinsicDurationMs,\n loading: intrinsicDurationMs === undefined,\n });\n }\n\n syncNow(): void {\n this.#updateReactState();\n }\n}\n\nexport const useMediaInfo = (\n mediaRef: React.RefObject<MediaElement | null>,\n) => {\n const [mediaInfo, setMediaInfo] = useState<MediaInfo>({\n intrinsicDurationMs: undefined,\n loading: true,\n });\n\n useEffect(() => {\n if (!mediaRef.current) return;\n\n const controller = new MediaInfoController(mediaRef.current, setMediaInfo);\n\n if (mediaRef.current.isConnected) {\n controller.hostConnected();\n controller.syncNow();\n }\n\n return () => {\n controller.hostDisconnected();\n };\n }, [mediaRef.current]);\n\n return mediaInfo;\n};\n"],"mappings":";;;AAaA,IAAa,sBAAb,MAA+D;CAC7D,eAAe;CACf,2BAA+C;CAE/C,YACE,AAAQA,MACR,AAAQC,cACR;EAFQ;EACA;AAER,OAAK,KAAK,cAAc,KAAK;;CAG/B,gBAAsB;AACpB,QAAKC,cAAe;;CAGtB,mBAAyB;AACvB,QAAKA,cAAe;AACpB,OAAK,KAAK,iBAAiB,KAAK;;CAGlC,cAAoB;AAClB,MAAI,CAAC,MAAKA,YAAc;EAExB,MAAM,sBAAsB,KAAK,KAAK;AAEtC,MAAI,wBAAwB,MAAKC,yBAA0B;AACzD,SAAKA,0BAA2B;AAChC,SAAKC,kBAAmB;;;CAI5B,oBAA0B;EACxB,MAAM,sBAAsB,KAAK,KAAK;AACtC,OAAK,aAAa;GAChB;GACA,SAAS,wBAAwB;GAClC,CAAC;;CAGJ,UAAgB;AACd,QAAKA,kBAAmB;;;AAI5B,MAAa,gBACX,aACG;CACH,MAAM,CAAC,WAAW,gBAAgB,SAAoB;EACpD,qBAAqB;EACrB,SAAS;EACV,CAAC;AAEF,iBAAgB;AACd,MAAI,CAAC,SAAS,QAAS;EAEvB,MAAM,aAAa,IAAI,oBAAoB,SAAS,SAAS,aAAa;AAE1E,MAAI,SAAS,QAAQ,aAAa;AAChC,cAAW,eAAe;AAC1B,cAAW,SAAS;;AAGtB,eAAa;AACX,cAAW,kBAAkB;;IAE9B,CAAC,SAAS,QAAQ,CAAC;AAEtB,QAAO"}
1
+ {"version":3,"file":"useMediaInfo.js","names":["host: MediaElement","setMediaInfo: React.Dispatch<React.SetStateAction<MediaInfo>>","#isConnected","#lastIntrinsicDurationMs","#updateReactState"],"sources":["../../src/hooks/useMediaInfo.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from \"lit\";\nimport { useEffect, useState } from \"react\";\n\nexport interface MediaInfo {\n intrinsicDurationMs: number | undefined;\n loading: boolean;\n}\n\ntype MediaElement = ReactiveControllerHost & {\n intrinsicDurationMs: number | undefined;\n isConnected: boolean;\n};\n\nexport class MediaInfoController implements ReactiveController {\n #isConnected = false;\n #lastIntrinsicDurationMs: number | undefined = undefined;\n\n constructor(\n private host: MediaElement,\n private setMediaInfo: React.Dispatch<React.SetStateAction<MediaInfo>>,\n ) {\n this.host.addController(this);\n }\n\n hostConnected(): void {\n this.#isConnected = true;\n }\n\n hostDisconnected(): void {\n this.#isConnected = false;\n this.host.removeController(this);\n }\n\n hostUpdated(): void {\n if (!this.#isConnected) return;\n\n const intrinsicDurationMs = this.host.intrinsicDurationMs;\n\n if (intrinsicDurationMs !== this.#lastIntrinsicDurationMs) {\n this.#lastIntrinsicDurationMs = intrinsicDurationMs;\n this.#updateReactState();\n }\n }\n\n #updateReactState(): void {\n const intrinsicDurationMs = this.host.intrinsicDurationMs;\n this.setMediaInfo({\n intrinsicDurationMs,\n loading: intrinsicDurationMs === undefined,\n });\n }\n\n syncNow(): void {\n this.#updateReactState();\n }\n}\n\nexport const useMediaInfo = (mediaRef: React.RefObject<MediaElement | null>) => {\n const [mediaInfo, setMediaInfo] = useState<MediaInfo>({\n intrinsicDurationMs: undefined,\n loading: true,\n });\n\n useEffect(() => {\n if (!mediaRef.current) return;\n\n const controller = new MediaInfoController(mediaRef.current, setMediaInfo);\n\n if (mediaRef.current.isConnected) {\n controller.hostConnected();\n controller.syncNow();\n }\n\n return () => {\n controller.hostDisconnected();\n };\n }, [mediaRef.current]);\n\n return mediaInfo;\n};\n"],"mappings":";;;AAaA,IAAa,sBAAb,MAA+D;CAC7D,eAAe;CACf,2BAA+C;CAE/C,YACE,AAAQA,MACR,AAAQC,cACR;EAFQ;EACA;AAER,OAAK,KAAK,cAAc,KAAK;;CAG/B,gBAAsB;AACpB,QAAKC,cAAe;;CAGtB,mBAAyB;AACvB,QAAKA,cAAe;AACpB,OAAK,KAAK,iBAAiB,KAAK;;CAGlC,cAAoB;AAClB,MAAI,CAAC,MAAKA,YAAc;EAExB,MAAM,sBAAsB,KAAK,KAAK;AAEtC,MAAI,wBAAwB,MAAKC,yBAA0B;AACzD,SAAKA,0BAA2B;AAChC,SAAKC,kBAAmB;;;CAI5B,oBAA0B;EACxB,MAAM,sBAAsB,KAAK,KAAK;AACtC,OAAK,aAAa;GAChB;GACA,SAAS,wBAAwB;GAClC,CAAC;;CAGJ,UAAgB;AACd,QAAKA,kBAAmB;;;AAI5B,MAAa,gBAAgB,aAAmD;CAC9E,MAAM,CAAC,WAAW,gBAAgB,SAAoB;EACpD,qBAAqB;EACrB,SAAS;EACV,CAAC;AAEF,iBAAgB;AACd,MAAI,CAAC,SAAS,QAAS;EAEvB,MAAM,aAAa,IAAI,oBAAoB,SAAS,SAAS,aAAa;AAE1E,MAAI,SAAS,QAAQ,aAAa;AAChC,cAAW,eAAe;AAC1B,cAAW,SAAS;;AAGtB,eAAa;AACX,cAAW,kBAAkB;;IAE9B,CAAC,SAAS,QAAQ,CAAC;AAEtB,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"usePanZoomTransform.js","names":[],"sources":["../../src/hooks/usePanZoomTransform.ts"],"sourcesContent":["import { type PanZoomTransform } from \"@editframe/elements\";\nimport React, { useEffect, useState } from \"react\";\n\n/**\n * Hook to get PanZoom transform values from a PanZoom element ref.\n * Listens to transform-changed events to keep transform values in sync.\n *\n * @param panZoomRef - Ref to the PanZoom element\n * @returns Current transform values (scale, x, y)\n *\n * @example\n * const panZoomRef = useRef<EFPanZoom | null>(null);\n * const transform = usePanZoomTransform(panZoomRef);\n * // transform.scale, transform.x, transform.y\n */\nexport function usePanZoomTransform(\n panZoomRef: React.RefObject<\n (EventTarget & { scale: number; x: number; y: number }) | null\n >,\n): PanZoomTransform {\n const [transform, setTransform] = useState<PanZoomTransform>({\n scale: 1,\n x: 0,\n y: 0,\n });\n\n useEffect(() => {\n const panZoom = panZoomRef.current;\n if (!panZoom) {\n return;\n }\n\n // Initialize with current values\n setTransform({\n scale: panZoom.scale ?? 1,\n x: panZoom.x ?? 0,\n y: panZoom.y ?? 0,\n });\n\n // Listen for transform changes\n const handleTransformChanged = (e: Event) => {\n const customEvent = e as CustomEvent<PanZoomTransform>;\n if (customEvent.detail) {\n setTransform(customEvent.detail);\n } else {\n // Fallback to reading from element if detail is not available\n setTransform({\n scale: panZoom.scale ?? 1,\n x: panZoom.x ?? 0,\n y: panZoom.y ?? 0,\n });\n }\n };\n\n panZoom.addEventListener(\"transform-changed\", handleTransformChanged);\n\n return () => {\n panZoom.removeEventListener(\"transform-changed\", handleTransformChanged);\n };\n }, [panZoomRef]);\n\n return transform;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAgB,oBACd,YAGkB;CAClB,MAAM,CAAC,WAAW,gBAAgB,SAA2B;EAC3D,OAAO;EACP,GAAG;EACH,GAAG;EACJ,CAAC;AAEF,iBAAgB;EACd,MAAM,UAAU,WAAW;AAC3B,MAAI,CAAC,QACH;AAIF,eAAa;GACX,OAAO,QAAQ,SAAS;GACxB,GAAG,QAAQ,KAAK;GAChB,GAAG,QAAQ,KAAK;GACjB,CAAC;EAGF,MAAM,0BAA0B,MAAa;GAC3C,MAAM,cAAc;AACpB,OAAI,YAAY,OACd,cAAa,YAAY,OAAO;OAGhC,cAAa;IACX,OAAO,QAAQ,SAAS;IACxB,GAAG,QAAQ,KAAK;IAChB,GAAG,QAAQ,KAAK;IACjB,CAAC;;AAIN,UAAQ,iBAAiB,qBAAqB,uBAAuB;AAErE,eAAa;AACX,WAAQ,oBAAoB,qBAAqB,uBAAuB;;IAEzE,CAAC,WAAW,CAAC;AAEhB,QAAO"}
1
+ {"version":3,"file":"usePanZoomTransform.js","names":[],"sources":["../../src/hooks/usePanZoomTransform.ts"],"sourcesContent":["import { type PanZoomTransform } from \"@editframe/elements\";\nimport React, { useEffect, useState } from \"react\";\n\n/**\n * Hook to get PanZoom transform values from a PanZoom element ref.\n * Listens to transform-changed events to keep transform values in sync.\n *\n * @param panZoomRef - Ref to the PanZoom element\n * @returns Current transform values (scale, x, y)\n *\n * @example\n * const panZoomRef = useRef<EFPanZoom | null>(null);\n * const transform = usePanZoomTransform(panZoomRef);\n * // transform.scale, transform.x, transform.y\n */\nexport function usePanZoomTransform(\n panZoomRef: React.RefObject<(EventTarget & { scale: number; x: number; y: number }) | null>,\n): PanZoomTransform {\n const [transform, setTransform] = useState<PanZoomTransform>({\n scale: 1,\n x: 0,\n y: 0,\n });\n\n useEffect(() => {\n const panZoom = panZoomRef.current;\n if (!panZoom) {\n return;\n }\n\n // Initialize with current values\n setTransform({\n scale: panZoom.scale ?? 1,\n x: panZoom.x ?? 0,\n y: panZoom.y ?? 0,\n });\n\n // Listen for transform changes\n const handleTransformChanged = (e: Event) => {\n const customEvent = e as CustomEvent<PanZoomTransform>;\n if (customEvent.detail) {\n setTransform(customEvent.detail);\n } else {\n // Fallback to reading from element if detail is not available\n setTransform({\n scale: panZoom.scale ?? 1,\n x: panZoom.x ?? 0,\n y: panZoom.y ?? 0,\n });\n }\n };\n\n panZoom.addEventListener(\"transform-changed\", handleTransformChanged);\n\n return () => {\n panZoom.removeEventListener(\"transform-changed\", handleTransformChanged);\n };\n }, [panZoomRef]);\n\n return transform;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAgB,oBACd,YACkB;CAClB,MAAM,CAAC,WAAW,gBAAgB,SAA2B;EAC3D,OAAO;EACP,GAAG;EACH,GAAG;EACJ,CAAC;AAEF,iBAAgB;EACd,MAAM,UAAU,WAAW;AAC3B,MAAI,CAAC,QACH;AAIF,eAAa;GACX,OAAO,QAAQ,SAAS;GACxB,GAAG,QAAQ,KAAK;GAChB,GAAG,QAAQ,KAAK;GACjB,CAAC;EAGF,MAAM,0BAA0B,MAAa;GAC3C,MAAM,cAAc;AACpB,OAAI,YAAY,OACd,cAAa,YAAY,OAAO;OAGhC,cAAa;IACX,OAAO,QAAQ,SAAS;IACxB,GAAG,QAAQ,KAAK;IAChB,GAAG,QAAQ,KAAK;IACjB,CAAC;;AAIN,UAAQ,iBAAiB,qBAAqB,uBAAuB;AAErE,eAAa;AACX,WAAQ,oBAAoB,qBAAqB,uBAAuB;;IAEzE,CAAC,WAAW,CAAC;AAEhB,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"useTimingInfo.js","names":["host: {\n ownCurrentTimeMs: number;\n durationMs: number;\n } & ReactiveControllerHost","setCurrentTime: React.Dispatch<React.SetStateAction<TimeInfo>>","#isConnected","#lastOwnCurrentTimeMs","#lastDurationMs","#updateReactState"],"sources":["../../src/hooks/useTimingInfo.ts"],"sourcesContent":["import type { EFTimegroup } from \"@editframe/elements\";\nimport type { ReactiveController, ReactiveControllerHost } from \"lit\";\nimport { useEffect, useRef, useState } from \"react\";\n\ninterface TimeInfo {\n ownCurrentTimeMs: number;\n durationMs: number;\n percentComplete: number;\n}\n\nclass CurrentTimeController implements ReactiveController {\n #isConnected = false;\n #lastOwnCurrentTimeMs = Number.NaN;\n #lastDurationMs = Number.NaN;\n\n constructor(\n private host: {\n ownCurrentTimeMs: number;\n durationMs: number;\n } & ReactiveControllerHost,\n private setCurrentTime: React.Dispatch<React.SetStateAction<TimeInfo>>,\n ) {\n this.host.addController(this);\n }\n\n hostConnected(): void {\n this.#isConnected = true;\n }\n\n hostDisconnected(): void {\n this.#isConnected = false;\n this.host.removeController(this);\n }\n\n hostUpdated(): void {\n if (!this.#isConnected) return;\n\n const { ownCurrentTimeMs, durationMs } = this.host;\n\n if (\n ownCurrentTimeMs !== this.#lastOwnCurrentTimeMs ||\n durationMs !== this.#lastDurationMs\n ) {\n this.#lastOwnCurrentTimeMs = ownCurrentTimeMs;\n this.#lastDurationMs = durationMs;\n this.#updateReactState();\n }\n }\n\n #updateReactState(): void {\n const { ownCurrentTimeMs, durationMs } = this.host;\n this.setCurrentTime({\n ownCurrentTimeMs,\n durationMs,\n percentComplete: durationMs > 0 ? ownCurrentTimeMs / durationMs : 0,\n });\n }\n\n syncNow(): void {\n this.#updateReactState();\n }\n}\n\nexport const useTimingInfo = (\n timegroupRef: React.RefObject<EFTimegroup | null> = useRef<EFTimegroup>(null),\n) => {\n const [timeInfo, setTimeInfo] = useState<TimeInfo>({\n ownCurrentTimeMs: 0,\n durationMs: 0,\n percentComplete: 0,\n });\n\n useEffect(() => {\n if (!timegroupRef.current) {\n throw new Error(\"Timegroup ref not set\");\n }\n\n const controller = new CurrentTimeController(\n timegroupRef.current,\n setTimeInfo,\n );\n\n if (timegroupRef.current.isConnected) {\n controller.hostConnected();\n controller.syncNow();\n }\n\n return () => {\n controller.hostDisconnected();\n };\n }, [timegroupRef.current]);\n\n return { ...timeInfo, ref: timegroupRef };\n};\n"],"mappings":";;;AAUA,IAAM,wBAAN,MAA0D;CACxD,eAAe;CACf,wBAAwB;CACxB,kBAAkB;CAElB,YACE,AAAQA,MAIR,AAAQC,gBACR;EALQ;EAIA;AAER,OAAK,KAAK,cAAc,KAAK;;CAG/B,gBAAsB;AACpB,QAAKC,cAAe;;CAGtB,mBAAyB;AACvB,QAAKA,cAAe;AACpB,OAAK,KAAK,iBAAiB,KAAK;;CAGlC,cAAoB;AAClB,MAAI,CAAC,MAAKA,YAAc;EAExB,MAAM,EAAE,kBAAkB,eAAe,KAAK;AAE9C,MACE,qBAAqB,MAAKC,wBAC1B,eAAe,MAAKC,gBACpB;AACA,SAAKD,uBAAwB;AAC7B,SAAKC,iBAAkB;AACvB,SAAKC,kBAAmB;;;CAI5B,oBAA0B;EACxB,MAAM,EAAE,kBAAkB,eAAe,KAAK;AAC9C,OAAK,eAAe;GAClB;GACA;GACA,iBAAiB,aAAa,IAAI,mBAAmB,aAAa;GACnE,CAAC;;CAGJ,UAAgB;AACd,QAAKA,kBAAmB;;;AAI5B,MAAa,iBACX,eAAoD,OAAoB,KAAK,KAC1E;CACH,MAAM,CAAC,UAAU,eAAe,SAAmB;EACjD,kBAAkB;EAClB,YAAY;EACZ,iBAAiB;EAClB,CAAC;AAEF,iBAAgB;AACd,MAAI,CAAC,aAAa,QAChB,OAAM,IAAI,MAAM,wBAAwB;EAG1C,MAAM,aAAa,IAAI,sBACrB,aAAa,SACb,YACD;AAED,MAAI,aAAa,QAAQ,aAAa;AACpC,cAAW,eAAe;AAC1B,cAAW,SAAS;;AAGtB,eAAa;AACX,cAAW,kBAAkB;;IAE9B,CAAC,aAAa,QAAQ,CAAC;AAE1B,QAAO;EAAE,GAAG;EAAU,KAAK;EAAc"}
1
+ {"version":3,"file":"useTimingInfo.js","names":["host: {\n ownCurrentTimeMs: number;\n durationMs: number;\n } & ReactiveControllerHost","setCurrentTime: React.Dispatch<React.SetStateAction<TimeInfo>>","#isConnected","#lastOwnCurrentTimeMs","#lastDurationMs","#updateReactState"],"sources":["../../src/hooks/useTimingInfo.ts"],"sourcesContent":["import type { EFTimegroup } from \"@editframe/elements\";\nimport type { ReactiveController, ReactiveControllerHost } from \"lit\";\nimport { useEffect, useRef, useState } from \"react\";\n\ninterface TimeInfo {\n ownCurrentTimeMs: number;\n durationMs: number;\n percentComplete: number;\n}\n\nclass CurrentTimeController implements ReactiveController {\n #isConnected = false;\n #lastOwnCurrentTimeMs = Number.NaN;\n #lastDurationMs = Number.NaN;\n\n constructor(\n private host: {\n ownCurrentTimeMs: number;\n durationMs: number;\n } & ReactiveControllerHost,\n private setCurrentTime: React.Dispatch<React.SetStateAction<TimeInfo>>,\n ) {\n this.host.addController(this);\n }\n\n hostConnected(): void {\n this.#isConnected = true;\n }\n\n hostDisconnected(): void {\n this.#isConnected = false;\n this.host.removeController(this);\n }\n\n hostUpdated(): void {\n if (!this.#isConnected) return;\n\n const { ownCurrentTimeMs, durationMs } = this.host;\n\n if (ownCurrentTimeMs !== this.#lastOwnCurrentTimeMs || durationMs !== this.#lastDurationMs) {\n this.#lastOwnCurrentTimeMs = ownCurrentTimeMs;\n this.#lastDurationMs = durationMs;\n this.#updateReactState();\n }\n }\n\n #updateReactState(): void {\n const { ownCurrentTimeMs, durationMs } = this.host;\n this.setCurrentTime({\n ownCurrentTimeMs,\n durationMs,\n percentComplete: durationMs > 0 ? ownCurrentTimeMs / durationMs : 0,\n });\n }\n\n syncNow(): void {\n this.#updateReactState();\n }\n}\n\nexport const useTimingInfo = (\n timegroupRef: React.RefObject<EFTimegroup | null> = useRef<EFTimegroup>(null),\n) => {\n const [timeInfo, setTimeInfo] = useState<TimeInfo>({\n ownCurrentTimeMs: 0,\n durationMs: 0,\n percentComplete: 0,\n });\n\n useEffect(() => {\n if (!timegroupRef.current) {\n throw new Error(\"Timegroup ref not set\");\n }\n\n const controller = new CurrentTimeController(timegroupRef.current, setTimeInfo);\n\n if (timegroupRef.current.isConnected) {\n controller.hostConnected();\n controller.syncNow();\n }\n\n return () => {\n controller.hostDisconnected();\n };\n }, [timegroupRef.current]);\n\n return { ...timeInfo, ref: timegroupRef };\n};\n"],"mappings":";;;AAUA,IAAM,wBAAN,MAA0D;CACxD,eAAe;CACf,wBAAwB;CACxB,kBAAkB;CAElB,YACE,AAAQA,MAIR,AAAQC,gBACR;EALQ;EAIA;AAER,OAAK,KAAK,cAAc,KAAK;;CAG/B,gBAAsB;AACpB,QAAKC,cAAe;;CAGtB,mBAAyB;AACvB,QAAKA,cAAe;AACpB,OAAK,KAAK,iBAAiB,KAAK;;CAGlC,cAAoB;AAClB,MAAI,CAAC,MAAKA,YAAc;EAExB,MAAM,EAAE,kBAAkB,eAAe,KAAK;AAE9C,MAAI,qBAAqB,MAAKC,wBAAyB,eAAe,MAAKC,gBAAiB;AAC1F,SAAKD,uBAAwB;AAC7B,SAAKC,iBAAkB;AACvB,SAAKC,kBAAmB;;;CAI5B,oBAA0B;EACxB,MAAM,EAAE,kBAAkB,eAAe,KAAK;AAC9C,OAAK,eAAe;GAClB;GACA;GACA,iBAAiB,aAAa,IAAI,mBAAmB,aAAa;GACnE,CAAC;;CAGJ,UAAgB;AACd,QAAKA,kBAAmB;;;AAI5B,MAAa,iBACX,eAAoD,OAAoB,KAAK,KAC1E;CACH,MAAM,CAAC,UAAU,eAAe,SAAmB;EACjD,kBAAkB;EAClB,YAAY;EACZ,iBAAiB;EAClB,CAAC;AAEF,iBAAgB;AACd,MAAI,CAAC,aAAa,QAChB,OAAM,IAAI,MAAM,wBAAwB;EAG1C,MAAM,aAAa,IAAI,sBAAsB,aAAa,SAAS,YAAY;AAE/E,MAAI,aAAa,QAAQ,aAAa;AACpC,cAAW,eAAe;AAC1B,cAAW,SAAS;;AAGtB,eAAa;AACX,cAAW,kBAAkB;;IAE9B,CAAC,aAAa,QAAQ,CAAC;AAE1B,QAAO;EAAE,GAAG;EAAU,KAAK;EAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"CompositionCanvas.js","names":[],"sources":["../../src/r3f/CompositionCanvas.tsx"],"sourcesContent":["/**\n * CompositionCanvas — R3F Canvas that automatically bridges\n * Editframe composition time into the 3D scene.\n *\n * Handles: addFrameTask → React state, preserveDrawingBuffer,\n * gl.finish(), frameloop=\"demand\", and invalidation.\n *\n * Usage:\n * ```tsx\n * <Timegroup mode=\"fixed\" duration=\"14s\">\n * <CompositionCanvas shadows>\n * <MyScene />\n * </CompositionCanvas>\n * </Timegroup>\n * ```\n *\n * Inside scene components, use `useCompositionTime()` to read the\n * current composition time in milliseconds.\n */\n\nimport * as React from \"react\";\nimport {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport { Canvas, useThree, useFrame } from \"@react-three/fiber\";\nimport { flushSync } from \"react-dom\";\nimport type { CanvasProps } from \"@react-three/fiber\";\n\n/* ━━ Context for composition time ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nconst CompositionTimeContext = createContext<{\n timeMs: number;\n durationMs: number;\n}>({ timeMs: 0, durationMs: 0 });\n\n/**\n * Hook to read the current composition time inside an R3F scene.\n * Must be used within a `<CompositionCanvas>`.\n *\n * @returns { timeMs, durationMs } — current time and total duration in ms\n */\nexport function useCompositionTime() {\n return useContext(CompositionTimeContext);\n}\n\n/* ━━ Internal: GL sync for renderToVideo ━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nfunction GLSync() {\n const { gl } = useThree();\n useFrame(() => {\n gl.getContext().finish();\n });\n return null;\n}\n\n/* ━━ Internal: invalidate on time change ━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nfunction InvalidateOnTimeChange({ timeMs }: { timeMs: number }) {\n const { invalidate } = useThree();\n // useLayoutEffect fires synchronously during flushSync, ensuring\n // invalidate() runs before the addFrameTask callback returns.\n useLayoutEffect(() => {\n invalidate();\n }, [timeMs, invalidate]);\n return null;\n}\n\n/* ━━ CompositionCanvas ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nexport interface CompositionCanvasProps extends Omit<CanvasProps, \"frameloop\"> {\n /** Extra styles for the container div */\n containerStyle?: React.CSSProperties;\n /** Extra className for the container div */\n containerClassName?: string;\n}\n\nexport function CompositionCanvas({\n children,\n containerStyle,\n containerClassName,\n gl: glProp,\n ...canvasProps\n}: CompositionCanvasProps) {\n const [timeMs, setTimeMs] = useState(0);\n const [durationMs, setDurationMs] = useState(0);\n const containerRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n\n // Walk up to find the ef-timegroup ancestor\n const tg = el.closest(\"ef-timegroup\") as\n | (HTMLElement & {\n addFrameTask?: (\n cb: (info: {\n ownCurrentTimeMs: number;\n durationMs: number;\n }) => void,\n ) => () => void;\n durationMs?: number;\n })\n | null;\n\n if (!tg) {\n console.warn(\n \"[CompositionCanvas] No ef-timegroup ancestor found. \" +\n \"Wrap CompositionCanvas inside a <Timegroup>.\",\n );\n return;\n }\n\n if (tg.durationMs) setDurationMs(tg.durationMs);\n\n const cleanup = tg.addFrameTask?.(\n ({ ownCurrentTimeMs, durationMs: dur }) => {\n // flushSync commits the state update synchronously so the\n // useLayoutEffect → invalidate() fires before we return.\n // R3F's demand render then runs useFrame subscribers (which\n // update instancedMesh matrices, cameras, etc.) and gl.render\n // in a single pass — no duplicate GPU work.\n flushSync(() => {\n setTimeMs(ownCurrentTimeMs);\n setDurationMs(dur);\n });\n },\n );\n\n return cleanup;\n }, []);\n\n // Merge user gl options with required defaults\n const mergedGl =\n typeof glProp === \"object\"\n ? { preserveDrawingBuffer: true, ...glProp }\n : (glProp ?? { preserveDrawingBuffer: true });\n\n return (\n <div\n ref={containerRef}\n className={containerClassName}\n style={{\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n ...containerStyle,\n }}\n >\n <Canvas\n frameloop=\"demand\"\n gl={mergedGl}\n {...canvasProps}\n style={{ width: \"100%\", height: \"100%\", ...canvasProps.style }}\n >\n <CompositionTimeContext.Provider value={{ timeMs, durationMs }}>\n <GLSync />\n <InvalidateOnTimeChange timeMs={timeMs} />\n {children}\n </CompositionTimeContext.Provider>\n </Canvas>\n </div>\n );\n}\n"],"mappings":";;;;;;AAmCA,MAAM,yBAAyB,cAG5B;CAAE,QAAQ;CAAG,YAAY;CAAG,CAAC;;;;;;;AAQhC,SAAgB,qBAAqB;AACnC,QAAO,WAAW,uBAAuB;;AAK3C,SAAS,SAAS;CAChB,MAAM,EAAE,OAAO,UAAU;AACzB,gBAAe;AACb,KAAG,YAAY,CAAC,QAAQ;GACxB;AACF,QAAO;;AAKT,SAAS,uBAAuB,EAAE,UAA8B;CAC9D,MAAM,EAAE,eAAe,UAAU;AAGjC,uBAAsB;AACpB,cAAY;IACX,CAAC,QAAQ,WAAW,CAAC;AACxB,QAAO;;AAYT,SAAgB,kBAAkB,EAChC,UACA,gBACA,oBACA,IAAI,OACJ,GAAG,eACsB;CACzB,MAAM,CAAC,QAAQ,aAAa,SAAS,EAAE;CACvC,MAAM,CAAC,YAAY,iBAAiB,SAAS,EAAE;CAC/C,MAAM,eAAe,OAAuB,KAAK;AAEjD,iBAAgB;EACd,MAAM,KAAK,aAAa;AACxB,MAAI,CAAC,GAAI;EAGT,MAAM,KAAK,GAAG,QAAQ,eAAe;AAYrC,MAAI,CAAC,IAAI;AACP,WAAQ,KACN,mGAED;AACD;;AAGF,MAAI,GAAG,WAAY,eAAc,GAAG,WAAW;AAgB/C,SAdgB,GAAG,gBAChB,EAAE,kBAAkB,YAAY,UAAU;AAMzC,mBAAgB;AACd,cAAU,iBAAiB;AAC3B,kBAAc,IAAI;KAClB;IAEL;IAGA,EAAE,CAAC;CAGN,MAAM,WACJ,OAAO,WAAW,WACd;EAAE,uBAAuB;EAAM,GAAG;EAAQ,GACzC,UAAU,EAAE,uBAAuB,MAAM;AAEhD,QACE,oBAAC;EACC,KAAK;EACL,WAAW;EACX,OAAO;GACL,UAAU;GACV,OAAO;GACP,OAAO;GACP,QAAQ;GACR,GAAG;GACJ;YAED,oBAAC;GACC,WAAU;GACV,IAAI;GACJ,GAAI;GACJ,OAAO;IAAE,OAAO;IAAQ,QAAQ;IAAQ,GAAG,YAAY;IAAO;aAE9D,qBAAC,uBAAuB;IAAS,OAAO;KAAE;KAAQ;KAAY;;KAC5D,oBAAC,WAAS;KACV,oBAAC,0BAA+B,SAAU;KACzC;;KAC+B;IAC3B;GACL"}
1
+ {"version":3,"file":"CompositionCanvas.js","names":[],"sources":["../../src/r3f/CompositionCanvas.tsx"],"sourcesContent":["/**\n * CompositionCanvas — R3F Canvas that automatically bridges\n * Editframe composition time into the 3D scene.\n *\n * Handles: addFrameTask → React state, preserveDrawingBuffer,\n * gl.finish(), frameloop=\"demand\", and invalidation.\n *\n * Usage:\n * ```tsx\n * <Timegroup mode=\"fixed\" duration=\"14s\">\n * <CompositionCanvas shadows>\n * <MyScene />\n * </CompositionCanvas>\n * </Timegroup>\n * ```\n *\n * Inside scene components, use `useCompositionTime()` to read the\n * current composition time in milliseconds.\n */\n\nimport * as React from \"react\";\nimport { createContext, useContext, useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { Canvas, useThree, useFrame } from \"@react-three/fiber\";\nimport { flushSync } from \"react-dom\";\nimport type { CanvasProps } from \"@react-three/fiber\";\n\n/* ━━ Context for composition time ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nconst CompositionTimeContext = createContext<{\n timeMs: number;\n durationMs: number;\n}>({ timeMs: 0, durationMs: 0 });\n\n/**\n * Hook to read the current composition time inside an R3F scene.\n * Must be used within a `<CompositionCanvas>`.\n *\n * @returns { timeMs, durationMs } — current time and total duration in ms\n */\nexport function useCompositionTime() {\n return useContext(CompositionTimeContext);\n}\n\n/* ━━ Internal: GL sync for renderToVideo ━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nfunction GLSync() {\n const { gl } = useThree();\n useFrame(() => {\n gl.getContext().finish();\n });\n return null;\n}\n\n/* ━━ Internal: invalidate on time change ━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nfunction InvalidateOnTimeChange({ timeMs }: { timeMs: number }) {\n const { invalidate } = useThree();\n // useLayoutEffect fires synchronously during flushSync, ensuring\n // invalidate() runs before the addFrameTask callback returns.\n useLayoutEffect(() => {\n invalidate();\n }, [timeMs, invalidate]);\n return null;\n}\n\n/* ━━ CompositionCanvas ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nexport interface CompositionCanvasProps extends Omit<CanvasProps, \"frameloop\"> {\n /** Extra styles for the container div */\n containerStyle?: React.CSSProperties;\n /** Extra className for the container div */\n containerClassName?: string;\n}\n\nexport function CompositionCanvas({\n children,\n containerStyle,\n containerClassName,\n gl: glProp,\n ...canvasProps\n}: CompositionCanvasProps) {\n const [timeMs, setTimeMs] = useState(0);\n const [durationMs, setDurationMs] = useState(0);\n const containerRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n\n // Walk up to find the ef-timegroup ancestor\n const tg = el.closest(\"ef-timegroup\") as\n | (HTMLElement & {\n addFrameTask?: (\n cb: (info: { ownCurrentTimeMs: number; durationMs: number }) => void,\n ) => () => void;\n durationMs?: number;\n })\n | null;\n\n if (!tg) {\n console.warn(\n \"[CompositionCanvas] No ef-timegroup ancestor found. \" +\n \"Wrap CompositionCanvas inside a <Timegroup>.\",\n );\n return;\n }\n\n if (tg.durationMs) setDurationMs(tg.durationMs);\n\n const cleanup = tg.addFrameTask?.(({ ownCurrentTimeMs, durationMs: dur }) => {\n // flushSync commits the state update synchronously so the\n // useLayoutEffect → invalidate() fires before we return.\n // R3F's demand render then runs useFrame subscribers (which\n // update instancedMesh matrices, cameras, etc.) and gl.render\n // in a single pass — no duplicate GPU work.\n flushSync(() => {\n setTimeMs(ownCurrentTimeMs);\n setDurationMs(dur);\n });\n });\n\n return cleanup;\n }, []);\n\n // Merge user gl options with required defaults\n const mergedGl =\n typeof glProp === \"object\"\n ? { preserveDrawingBuffer: true, ...glProp }\n : (glProp ?? { preserveDrawingBuffer: true });\n\n return (\n <div\n ref={containerRef}\n className={containerClassName}\n style={{\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n ...containerStyle,\n }}\n >\n <Canvas\n frameloop=\"demand\"\n gl={mergedGl}\n {...canvasProps}\n style={{ width: \"100%\", height: \"100%\", ...canvasProps.style }}\n >\n <CompositionTimeContext.Provider value={{ timeMs, durationMs }}>\n <GLSync />\n <InvalidateOnTimeChange timeMs={timeMs} />\n {children}\n </CompositionTimeContext.Provider>\n </Canvas>\n </div>\n );\n}\n"],"mappings":";;;;;;AA4BA,MAAM,yBAAyB,cAG5B;CAAE,QAAQ;CAAG,YAAY;CAAG,CAAC;;;;;;;AAQhC,SAAgB,qBAAqB;AACnC,QAAO,WAAW,uBAAuB;;AAK3C,SAAS,SAAS;CAChB,MAAM,EAAE,OAAO,UAAU;AACzB,gBAAe;AACb,KAAG,YAAY,CAAC,QAAQ;GACxB;AACF,QAAO;;AAKT,SAAS,uBAAuB,EAAE,UAA8B;CAC9D,MAAM,EAAE,eAAe,UAAU;AAGjC,uBAAsB;AACpB,cAAY;IACX,CAAC,QAAQ,WAAW,CAAC;AACxB,QAAO;;AAYT,SAAgB,kBAAkB,EAChC,UACA,gBACA,oBACA,IAAI,OACJ,GAAG,eACsB;CACzB,MAAM,CAAC,QAAQ,aAAa,SAAS,EAAE;CACvC,MAAM,CAAC,YAAY,iBAAiB,SAAS,EAAE;CAC/C,MAAM,eAAe,OAAuB,KAAK;AAEjD,iBAAgB;EACd,MAAM,KAAK,aAAa;AACxB,MAAI,CAAC,GAAI;EAGT,MAAM,KAAK,GAAG,QAAQ,eAAe;AASrC,MAAI,CAAC,IAAI;AACP,WAAQ,KACN,mGAED;AACD;;AAGF,MAAI,GAAG,WAAY,eAAc,GAAG,WAAW;AAc/C,SAZgB,GAAG,gBAAgB,EAAE,kBAAkB,YAAY,UAAU;AAM3E,mBAAgB;AACd,cAAU,iBAAiB;AAC3B,kBAAc,IAAI;KAClB;IACF;IAGD,EAAE,CAAC;CAGN,MAAM,WACJ,OAAO,WAAW,WACd;EAAE,uBAAuB;EAAM,GAAG;EAAQ,GACzC,UAAU,EAAE,uBAAuB,MAAM;AAEhD,QACE,oBAAC;EACC,KAAK;EACL,WAAW;EACX,OAAO;GACL,UAAU;GACV,OAAO;GACP,OAAO;GACP,QAAQ;GACR,GAAG;GACJ;YAED,oBAAC;GACC,WAAU;GACV,IAAI;GACJ,GAAI;GACJ,OAAO;IAAE,OAAO;IAAQ,QAAQ;IAAQ,GAAG,YAAY;IAAO;aAE9D,qBAAC,uBAAuB;IAAS,OAAO;KAAE;KAAQ;KAAY;;KAC5D,oBAAC,WAAS;KACV,oBAAC,0BAA+B,SAAU;KACzC;;KAC+B;IAC3B;GACL"}
@@ -1 +1 @@
1
- {"version":3,"file":"OffscreenCompositionCanvas.js","names":["OffscreenCanvas"],"sources":["../../src/r3f/OffscreenCompositionCanvas.tsx"],"sourcesContent":["/**\n * OffscreenCompositionCanvas — R3F Canvas that renders in a web worker via OffscreenCanvas.\n *\n * This component integrates with Editframe's timeline system by:\n * - Registering an addFrameTask that sends time updates to the worker\n * - Receiving rendered frames (ImageBitmap) from the worker\n * - Drawing frames onto a hidden capture canvas for video export\n *\n * The worker handles all R3F rendering, keeping the main thread free and enabling\n * rendering to continue even when the browser tab is hidden.\n *\n * Usage:\n * ```tsx\n * const worker = new Worker(new URL('./scene-worker.ts', import.meta.url), { type: 'module' });\n *\n * <Timegroup mode=\"fixed\" duration=\"14s\">\n * <OffscreenCompositionCanvas\n * worker={worker}\n * fallback={<MainThreadFallback />}\n * canvasProps={{ shadows: true, dpr: [1, 2] }}\n * />\n * </Timegroup>\n * ```\n */\n\nimport * as React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { Canvas as OffscreenCanvas } from \"@react-three/offscreen\";\nimport type { CanvasProps } from \"@react-three/fiber\";\nimport type { EFTimegroup } from \"@editframe/elements\";\n\n/* ━━ Types ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nexport interface OffscreenCompositionCanvasProps {\n /** Web worker that will handle R3F rendering */\n worker: Worker;\n /** Fallback content for browsers without OffscreenCanvas support (Safari) */\n fallback?: React.ReactNode;\n /** Extra styles for the container div */\n containerStyle?: React.CSSProperties;\n /** Extra className for the container div */\n containerClassName?: string;\n /** Canvas props to forward to @react-three/offscreen Canvas (shadows, dpr, gl, camera, scene, etc.) */\n canvasProps?: Omit<CanvasProps, \"frameloop\">;\n}\n\n/* ━━ Helper: Wait for bitmap from worker ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nfunction waitForBitmap(\n worker: Worker,\n requestId: number,\n): Promise<ImageBitmap> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n worker.removeEventListener(\"message\", handler);\n reject(\n new Error(\n `[OffscreenCompositionCanvas] Timeout waiting for frame ${requestId}`,\n ),\n );\n }, 5000); // 5 second timeout\n\n const handler = (e: MessageEvent) => {\n if (e.data.type === \"frameRendered\" && e.data.requestId === requestId) {\n clearTimeout(timeout);\n worker.removeEventListener(\"message\", handler);\n resolve(e.data.bitmap);\n } else if (e.data.type === \"error\") {\n clearTimeout(timeout);\n worker.removeEventListener(\"message\", handler);\n reject(\n new Error(\n `[OffscreenCompositionCanvas] Worker error: ${e.data.message}`,\n ),\n );\n }\n };\n\n worker.addEventListener(\"message\", handler);\n });\n}\n\n/* ━━ Component ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nexport function OffscreenCompositionCanvas({\n worker,\n fallback,\n containerStyle,\n containerClassName,\n canvasProps,\n}: OffscreenCompositionCanvasProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const captureCanvasRef = useRef<HTMLCanvasElement>(null);\n const [dimensions, setDimensions] = useState({ width: 0, height: 0 });\n\n /* ━━ Resize observer to keep capture canvas in sync ━━━━━━━━━━━━━━━━━ */\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const { width, height } = entry.contentRect;\n setDimensions({ width, height });\n }\n });\n\n observer.observe(container);\n return () => observer.disconnect();\n }, []);\n\n /* ━━ addFrameTask integration ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n // Walk up to find the parent ef-timegroup\n const tg = container.closest(\"ef-timegroup\") as\n | (HTMLElement & EFTimegroup)\n | null;\n\n if (!tg) {\n console.warn(\n \"[OffscreenCompositionCanvas] No ef-timegroup ancestor found. \" +\n \"Wrap OffscreenCompositionCanvas inside a <Timegroup>.\",\n );\n return;\n }\n\n if (!tg.addFrameTask) {\n console.warn(\n \"[OffscreenCompositionCanvas] ef-timegroup does not have addFrameTask method\",\n );\n return;\n }\n\n let nextRequestId = 0;\n\n const cleanup = tg.addFrameTask(\n async ({ ownCurrentTimeMs, durationMs }) => {\n const requestId = nextRequestId++;\n\n // Send render request to worker\n worker.postMessage({\n type: \"renderFrame\",\n timeMs: ownCurrentTimeMs,\n durationMs,\n requestId,\n });\n\n try {\n // Wait for worker to finish rendering and return pixels\n const bitmap = await waitForBitmap(worker, requestId);\n\n // Draw onto capture canvas so serialization pipeline can read pixels\n const captureCanvas = captureCanvasRef.current;\n if (captureCanvas) {\n const ctx = captureCanvas.getContext(\"2d\");\n if (ctx) {\n // Resize capture canvas to match bitmap\n if (\n captureCanvas.width !== bitmap.width ||\n captureCanvas.height !== bitmap.height\n ) {\n captureCanvas.width = bitmap.width;\n captureCanvas.height = bitmap.height;\n }\n\n // Draw the bitmap\n ctx.drawImage(bitmap, 0, 0);\n }\n\n // Close bitmap to free memory\n bitmap.close();\n }\n } catch (error) {\n console.error(\n \"[OffscreenCompositionCanvas] Frame render error:\",\n error,\n );\n }\n },\n );\n\n return cleanup;\n }, [worker]);\n\n /* ━━ Render ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n return (\n <div\n ref={containerRef}\n className={containerClassName}\n style={{\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n ...containerStyle,\n }}\n >\n {/* Display canvas - handled by @react-three/offscreen */}\n <OffscreenCanvas\n worker={worker}\n fallback={fallback}\n {...canvasProps}\n style={{ width: \"100%\", height: \"100%\", ...canvasProps?.style }}\n />\n\n {/* Hidden capture canvas for video export */}\n <canvas\n ref={captureCanvasRef}\n data-offscreen-capture=\"true\"\n style={{ display: \"none\" }}\n width={dimensions.width}\n height={dimensions.height}\n />\n </div>\n );\n}\n"],"mappings":";;;;;AAgDA,SAAS,cACP,QACA,WACsB;AACtB,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,UAAU,iBAAiB;AAC/B,UAAO,oBAAoB,WAAW,QAAQ;AAC9C,0BACE,IAAI,MACF,0DAA0D,YAC3D,CACF;KACA,IAAK;EAER,MAAM,WAAW,MAAoB;AACnC,OAAI,EAAE,KAAK,SAAS,mBAAmB,EAAE,KAAK,cAAc,WAAW;AACrE,iBAAa,QAAQ;AACrB,WAAO,oBAAoB,WAAW,QAAQ;AAC9C,YAAQ,EAAE,KAAK,OAAO;cACb,EAAE,KAAK,SAAS,SAAS;AAClC,iBAAa,QAAQ;AACrB,WAAO,oBAAoB,WAAW,QAAQ;AAC9C,2BACE,IAAI,MACF,8CAA8C,EAAE,KAAK,UACtD,CACF;;;AAIL,SAAO,iBAAiB,WAAW,QAAQ;GAC3C;;AAKJ,SAAgB,2BAA2B,EACzC,QACA,UACA,gBACA,oBACA,eACkC;CAClC,MAAM,eAAe,OAAuB,KAAK;CACjD,MAAM,mBAAmB,OAA0B,KAAK;CACxD,MAAM,CAAC,YAAY,iBAAiB,SAAS;EAAE,OAAO;EAAG,QAAQ;EAAG,CAAC;AAIrE,iBAAgB;EACd,MAAM,YAAY,aAAa;AAC/B,MAAI,CAAC,UAAW;EAEhB,MAAM,WAAW,IAAI,gBAAgB,YAAY;AAC/C,QAAK,MAAM,SAAS,SAAS;IAC3B,MAAM,EAAE,OAAO,WAAW,MAAM;AAChC,kBAAc;KAAE;KAAO;KAAQ,CAAC;;IAElC;AAEF,WAAS,QAAQ,UAAU;AAC3B,eAAa,SAAS,YAAY;IACjC,EAAE,CAAC;AAIN,iBAAgB;EACd,MAAM,YAAY,aAAa;AAC/B,MAAI,CAAC,UAAW;EAGhB,MAAM,KAAK,UAAU,QAAQ,eAAe;AAI5C,MAAI,CAAC,IAAI;AACP,WAAQ,KACN,qHAED;AACD;;AAGF,MAAI,CAAC,GAAG,cAAc;AACpB,WAAQ,KACN,8EACD;AACD;;EAGF,IAAI,gBAAgB;AAgDpB,SA9CgB,GAAG,aACjB,OAAO,EAAE,kBAAkB,iBAAiB;GAC1C,MAAM,YAAY;AAGlB,UAAO,YAAY;IACjB,MAAM;IACN,QAAQ;IACR;IACA;IACD,CAAC;AAEF,OAAI;IAEF,MAAM,SAAS,MAAM,cAAc,QAAQ,UAAU;IAGrD,MAAM,gBAAgB,iBAAiB;AACvC,QAAI,eAAe;KACjB,MAAM,MAAM,cAAc,WAAW,KAAK;AAC1C,SAAI,KAAK;AAEP,UACE,cAAc,UAAU,OAAO,SAC/B,cAAc,WAAW,OAAO,QAChC;AACA,qBAAc,QAAQ,OAAO;AAC7B,qBAAc,SAAS,OAAO;;AAIhC,UAAI,UAAU,QAAQ,GAAG,EAAE;;AAI7B,YAAO,OAAO;;YAET,OAAO;AACd,YAAQ,MACN,oDACA,MACD;;IAGN;IAGA,CAAC,OAAO,CAAC;AAIZ,QACE,qBAAC;EACC,KAAK;EACL,WAAW;EACX,OAAO;GACL,UAAU;GACV,OAAO;GACP,OAAO;GACP,QAAQ;GACR,GAAG;GACJ;aAGD,oBAACA;GACS;GACE;GACV,GAAI;GACJ,OAAO;IAAE,OAAO;IAAQ,QAAQ;IAAQ,GAAG,aAAa;IAAO;IAC/D,EAGF,oBAAC;GACC,KAAK;GACL,0BAAuB;GACvB,OAAO,EAAE,SAAS,QAAQ;GAC1B,OAAO,WAAW;GAClB,QAAQ,WAAW;IACnB;GACE"}
1
+ {"version":3,"file":"OffscreenCompositionCanvas.js","names":["OffscreenCanvas"],"sources":["../../src/r3f/OffscreenCompositionCanvas.tsx"],"sourcesContent":["/**\n * OffscreenCompositionCanvas — R3F Canvas that renders in a web worker via OffscreenCanvas.\n *\n * This component integrates with Editframe's timeline system by:\n * - Registering an addFrameTask that sends time updates to the worker\n * - Receiving rendered frames (ImageBitmap) from the worker\n * - Drawing frames onto a hidden capture canvas for video export\n *\n * The worker handles all R3F rendering, keeping the main thread free and enabling\n * rendering to continue even when the browser tab is hidden.\n *\n * Usage:\n * ```tsx\n * const worker = new Worker(new URL('./scene-worker.ts', import.meta.url), { type: 'module' });\n *\n * <Timegroup mode=\"fixed\" duration=\"14s\">\n * <OffscreenCompositionCanvas\n * worker={worker}\n * fallback={<MainThreadFallback />}\n * canvasProps={{ shadows: true, dpr: [1, 2] }}\n * />\n * </Timegroup>\n * ```\n */\n\nimport * as React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { Canvas as OffscreenCanvas } from \"@react-three/offscreen\";\nimport type { CanvasProps } from \"@react-three/fiber\";\nimport type { EFTimegroup } from \"@editframe/elements\";\n\n/* ━━ Types ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nexport interface OffscreenCompositionCanvasProps {\n /** Web worker that will handle R3F rendering */\n worker: Worker;\n /** Fallback content for browsers without OffscreenCanvas support (Safari) */\n fallback?: React.ReactNode;\n /** Extra styles for the container div */\n containerStyle?: React.CSSProperties;\n /** Extra className for the container div */\n containerClassName?: string;\n /** Canvas props to forward to @react-three/offscreen Canvas (shadows, dpr, gl, camera, scene, etc.) */\n canvasProps?: Omit<CanvasProps, \"frameloop\">;\n}\n\n/* ━━ Helper: Wait for bitmap from worker ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nfunction waitForBitmap(worker: Worker, requestId: number): Promise<ImageBitmap> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n worker.removeEventListener(\"message\", handler);\n reject(new Error(`[OffscreenCompositionCanvas] Timeout waiting for frame ${requestId}`));\n }, 5000); // 5 second timeout\n\n const handler = (e: MessageEvent) => {\n if (e.data.type === \"frameRendered\" && e.data.requestId === requestId) {\n clearTimeout(timeout);\n worker.removeEventListener(\"message\", handler);\n resolve(e.data.bitmap);\n } else if (e.data.type === \"error\") {\n clearTimeout(timeout);\n worker.removeEventListener(\"message\", handler);\n reject(new Error(`[OffscreenCompositionCanvas] Worker error: ${e.data.message}`));\n }\n };\n\n worker.addEventListener(\"message\", handler);\n });\n}\n\n/* ━━ Component ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\nexport function OffscreenCompositionCanvas({\n worker,\n fallback,\n containerStyle,\n containerClassName,\n canvasProps,\n}: OffscreenCompositionCanvasProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const captureCanvasRef = useRef<HTMLCanvasElement>(null);\n const [dimensions, setDimensions] = useState({ width: 0, height: 0 });\n\n /* ━━ Resize observer to keep capture canvas in sync ━━━━━━━━━━━━━━━━━ */\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const { width, height } = entry.contentRect;\n setDimensions({ width, height });\n }\n });\n\n observer.observe(container);\n return () => observer.disconnect();\n }, []);\n\n /* ━━ addFrameTask integration ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n // Walk up to find the parent ef-timegroup\n const tg = container.closest(\"ef-timegroup\") as (HTMLElement & EFTimegroup) | null;\n\n if (!tg) {\n console.warn(\n \"[OffscreenCompositionCanvas] No ef-timegroup ancestor found. \" +\n \"Wrap OffscreenCompositionCanvas inside a <Timegroup>.\",\n );\n return;\n }\n\n if (!tg.addFrameTask) {\n console.warn(\"[OffscreenCompositionCanvas] ef-timegroup does not have addFrameTask method\");\n return;\n }\n\n let nextRequestId = 0;\n\n const cleanup = tg.addFrameTask(async ({ ownCurrentTimeMs, durationMs }) => {\n const requestId = nextRequestId++;\n\n // Send render request to worker\n worker.postMessage({\n type: \"renderFrame\",\n timeMs: ownCurrentTimeMs,\n durationMs,\n requestId,\n });\n\n try {\n // Wait for worker to finish rendering and return pixels\n const bitmap = await waitForBitmap(worker, requestId);\n\n // Draw onto capture canvas so serialization pipeline can read pixels\n const captureCanvas = captureCanvasRef.current;\n if (captureCanvas) {\n const ctx = captureCanvas.getContext(\"2d\");\n if (ctx) {\n // Resize capture canvas to match bitmap\n if (captureCanvas.width !== bitmap.width || captureCanvas.height !== bitmap.height) {\n captureCanvas.width = bitmap.width;\n captureCanvas.height = bitmap.height;\n }\n\n // Draw the bitmap\n ctx.drawImage(bitmap, 0, 0);\n }\n\n // Close bitmap to free memory\n bitmap.close();\n }\n } catch (error) {\n console.error(\"[OffscreenCompositionCanvas] Frame render error:\", error);\n }\n });\n\n return cleanup;\n }, [worker]);\n\n /* ━━ Render ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n return (\n <div\n ref={containerRef}\n className={containerClassName}\n style={{\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n ...containerStyle,\n }}\n >\n {/* Display canvas - handled by @react-three/offscreen */}\n <OffscreenCanvas\n worker={worker}\n fallback={fallback}\n {...canvasProps}\n style={{ width: \"100%\", height: \"100%\", ...canvasProps?.style }}\n />\n\n {/* Hidden capture canvas for video export */}\n <canvas\n ref={captureCanvasRef}\n data-offscreen-capture=\"true\"\n style={{ display: \"none\" }}\n width={dimensions.width}\n height={dimensions.height}\n />\n </div>\n );\n}\n"],"mappings":";;;;;AAgDA,SAAS,cAAc,QAAgB,WAAyC;AAC9E,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,UAAU,iBAAiB;AAC/B,UAAO,oBAAoB,WAAW,QAAQ;AAC9C,0BAAO,IAAI,MAAM,0DAA0D,YAAY,CAAC;KACvF,IAAK;EAER,MAAM,WAAW,MAAoB;AACnC,OAAI,EAAE,KAAK,SAAS,mBAAmB,EAAE,KAAK,cAAc,WAAW;AACrE,iBAAa,QAAQ;AACrB,WAAO,oBAAoB,WAAW,QAAQ;AAC9C,YAAQ,EAAE,KAAK,OAAO;cACb,EAAE,KAAK,SAAS,SAAS;AAClC,iBAAa,QAAQ;AACrB,WAAO,oBAAoB,WAAW,QAAQ;AAC9C,2BAAO,IAAI,MAAM,8CAA8C,EAAE,KAAK,UAAU,CAAC;;;AAIrF,SAAO,iBAAiB,WAAW,QAAQ;GAC3C;;AAKJ,SAAgB,2BAA2B,EACzC,QACA,UACA,gBACA,oBACA,eACkC;CAClC,MAAM,eAAe,OAAuB,KAAK;CACjD,MAAM,mBAAmB,OAA0B,KAAK;CACxD,MAAM,CAAC,YAAY,iBAAiB,SAAS;EAAE,OAAO;EAAG,QAAQ;EAAG,CAAC;AAIrE,iBAAgB;EACd,MAAM,YAAY,aAAa;AAC/B,MAAI,CAAC,UAAW;EAEhB,MAAM,WAAW,IAAI,gBAAgB,YAAY;AAC/C,QAAK,MAAM,SAAS,SAAS;IAC3B,MAAM,EAAE,OAAO,WAAW,MAAM;AAChC,kBAAc;KAAE;KAAO;KAAQ,CAAC;;IAElC;AAEF,WAAS,QAAQ,UAAU;AAC3B,eAAa,SAAS,YAAY;IACjC,EAAE,CAAC;AAIN,iBAAgB;EACd,MAAM,YAAY,aAAa;AAC/B,MAAI,CAAC,UAAW;EAGhB,MAAM,KAAK,UAAU,QAAQ,eAAe;AAE5C,MAAI,CAAC,IAAI;AACP,WAAQ,KACN,qHAED;AACD;;AAGF,MAAI,CAAC,GAAG,cAAc;AACpB,WAAQ,KAAK,8EAA8E;AAC3F;;EAGF,IAAI,gBAAgB;AAwCpB,SAtCgB,GAAG,aAAa,OAAO,EAAE,kBAAkB,iBAAiB;GAC1E,MAAM,YAAY;AAGlB,UAAO,YAAY;IACjB,MAAM;IACN,QAAQ;IACR;IACA;IACD,CAAC;AAEF,OAAI;IAEF,MAAM,SAAS,MAAM,cAAc,QAAQ,UAAU;IAGrD,MAAM,gBAAgB,iBAAiB;AACvC,QAAI,eAAe;KACjB,MAAM,MAAM,cAAc,WAAW,KAAK;AAC1C,SAAI,KAAK;AAEP,UAAI,cAAc,UAAU,OAAO,SAAS,cAAc,WAAW,OAAO,QAAQ;AAClF,qBAAc,QAAQ,OAAO;AAC7B,qBAAc,SAAS,OAAO;;AAIhC,UAAI,UAAU,QAAQ,GAAG,EAAE;;AAI7B,YAAO,OAAO;;YAET,OAAO;AACd,YAAQ,MAAM,oDAAoD,MAAM;;IAE1E;IAGD,CAAC,OAAO,CAAC;AAIZ,QACE,qBAAC;EACC,KAAK;EACL,WAAW;EACX,OAAO;GACL,UAAU;GACV,OAAO;GACP,OAAO;GACP,QAAQ;GACR,GAAG;GACJ;aAGD,oBAACA;GACS;GACE;GACV,GAAI;GACJ,OAAO;IAAE,OAAO;IAAQ,QAAQ;IAAQ,GAAG,aAAa;IAAO;IAC/D,EAGF,oBAAC;GACC,KAAK;GACL,0BAAuB;GACvB,OAAO,EAAE,SAAS,QAAQ;GAC1B,OAAO,WAAW;GAClB,QAAQ,WAAW;IACnB;GACE"}
@@ -255,21 +255,21 @@ function renderOffscreen(children) {
255
255
  THREE.ImageLoader.prototype.load = function(url, onLoad, _onProgress, onError) {
256
256
  if (this.path !== void 0) url = this.path + url;
257
257
  url = this.manager.resolveURL(url);
258
- const scope = this;
259
258
  const cached = THREE.Cache.get(url);
260
259
  if (cached !== void 0) {
261
- scope.manager.itemStart(url);
260
+ this.manager.itemStart(url);
262
261
  if (onLoad) onLoad(cached);
263
- scope.manager.itemEnd(url);
262
+ this.manager.itemEnd(url);
264
263
  return cached;
265
264
  }
265
+ const manager = this.manager;
266
266
  fetch(url).then((res) => res.blob()).then((res) => createImageBitmap(res, {
267
267
  premultiplyAlpha: "none",
268
268
  colorSpaceConversion: "none"
269
269
  })).then((bitmap) => {
270
270
  THREE.Cache.add(url, bitmap);
271
271
  if (onLoad) onLoad(bitmap);
272
- scope.manager.itemEnd(url);
272
+ manager.itemEnd(url);
273
273
  }).catch(onError);
274
274
  return {};
275
275
  };
@@ -1 +1 @@
1
- {"version":3,"file":"renderOffscreen.js","names":["timeStore: TimeStore","root: ReconcilerRoot<HTMLCanvasElement> | null","offscreenCanvas: OffscreenCanvas | null","size: Size","emitter: Emitter<Record<any, unknown>>","state","e: any"],"sources":["../../src/r3f/renderOffscreen.ts"],"sourcesContent":["/**\n * Worker-side entry point for offscreen R3F rendering.\n *\n * This extends @react-three/offscreen's render() function with:\n * - Time synchronization store for composition time\n * - Frame rendering + pixel capture pipeline\n * - Support for deterministic frame-by-frame rendering\n *\n * Based on @react-three/offscreen render.ts but extended for Editframe's needs.\n */\n\nimport * as React from \"react\";\nimport { useSyncExternalStore } from \"react\";\n// @ts-ignore\nimport * as THREE from \"three\";\nimport type { Emitter } from \"mitt\";\nimport {\n createRoot,\n createEvents,\n ReconcilerRoot,\n Size,\n RootState,\n EventManager,\n Events,\n RootStore,\n} from \"@react-three/fiber\";\nimport type { DomEvent } from \"@react-three/fiber/dist/declarations/src/core/events\";\nimport type { RenderFramePayload } from \"./worker-protocol\";\n\n/* ━━ Event handling (from @react-three/offscreen) ━━━━━━━━━━━━━━━━━━━━━ */\n\nconst EVENTS = {\n onClick: [\"click\", false],\n onContextMenu: [\"contextmenu\", false],\n onDoubleClick: [\"dblclick\", false],\n onWheel: [\"wheel\", true],\n onPointerDown: [\"pointerdown\", true],\n onPointerUp: [\"pointerup\", true],\n onPointerLeave: [\"pointerleave\", true],\n onPointerMove: [\"pointermove\", true],\n onPointerCancel: [\"pointercancel\", true],\n onLostPointerCapture: [\"lostpointercapture\", true],\n} as const;\n\nfunction createPointerEvents(emitter: Emitter<Record<any, unknown>>) {\n return (store: RootStore): EventManager<HTMLElement> => {\n const { handlePointer } = createEvents(store);\n\n return {\n priority: 1,\n enabled: true,\n compute(event, state) {\n state.pointer.set(\n (event.offsetX / state.size.width) * 2 - 1,\n -(event.offsetY / state.size.height) * 2 + 1,\n );\n state.raycaster.setFromCamera(state.pointer, state.camera);\n },\n\n connected: undefined,\n handlers: Object.keys(EVENTS).reduce(\n (acc, key) => ({ ...acc, [key]: handlePointer(key) }),\n {},\n ) as unknown as Events,\n connect: (target) => {\n const { set, events } = store.getState();\n events.disconnect?.();\n set((state) => ({ events: { ...state.events, connected: target } }));\n Object.entries(events?.handlers ?? []).forEach(([name, event]) => {\n const [eventName] = EVENTS[name as keyof typeof EVENTS];\n emitter.on(eventName as any, event as any);\n });\n },\n disconnect: () => {\n const { set, events } = store.getState();\n if (events.connected) {\n Object.entries(events.handlers ?? []).forEach(([name, event]) => {\n const [eventName] = EVENTS[name as keyof typeof EVENTS];\n emitter.off(eventName as any, event as any);\n });\n set((state) => ({\n events: { ...state.events, connected: undefined },\n }));\n }\n },\n };\n };\n}\n\n/* ━━ Time synchronization store ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\ninterface TimeStore {\n timeMs: number;\n durationMs: number;\n listeners: Set<() => void>;\n update(timeMs: number, durationMs: number): void;\n}\n\nconst timeStore: TimeStore = {\n timeMs: 0,\n durationMs: 0,\n listeners: new Set(),\n update(timeMs: number, durationMs: number) {\n this.timeMs = timeMs;\n this.durationMs = durationMs;\n this.listeners.forEach((l) => l());\n },\n};\n\n/**\n * Hook to read composition time inside R3F scenes running in a worker.\n * Must be used within a scene rendered by renderOffscreen().\n */\nexport function useCompositionTime() {\n const timeMs = useSyncExternalStore(\n (callback) => {\n timeStore.listeners.add(callback);\n return () => timeStore.listeners.delete(callback);\n },\n () => timeStore.timeMs,\n );\n\n const durationMs = useSyncExternalStore(\n (callback) => {\n timeStore.listeners.add(callback);\n return () => timeStore.listeners.delete(callback);\n },\n () => timeStore.durationMs,\n );\n\n return { timeMs, durationMs };\n}\n\n/* ━━ Worker entry point ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n/**\n * Render a React Three Fiber scene in a web worker with offscreen canvas.\n *\n * This extends @react-three/offscreen's render() with Editframe-specific features:\n * - Time synchronization via timeStore and useCompositionTime hook\n * - Frame-by-frame rendering on demand (renderFrame message)\n * - Pixel capture and transfer back to main thread via ImageBitmap\n *\n * @param children - React node containing the R3F scene\n *\n * @example\n * ```typescript\n * // worker.ts\n * import { renderOffscreen } from '@editframe/react/r3f';\n * import { MyScene } from './MyScene';\n *\n * renderOffscreen(<MyScene />);\n * ```\n */\nexport function renderOffscreen(children: React.ReactNode) {\n let root: ReconcilerRoot<HTMLCanvasElement> | null = null;\n let offscreenCanvas: OffscreenCanvas | null = null;\n let size: Size = { width: 0, height: 0, top: 0, left: 0 };\n let dpr = 1;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const handlers = new Map<any, Set<any>>();\n const emitter: Emitter<Record<any, unknown>> = {\n all: new Map(),\n on(type: any, handler: any) {\n const s = handlers.get(type) ?? new Set();\n s.add(handler);\n handlers.set(type, s);\n },\n off(type: any, handler: any) {\n handlers.get(type)?.delete(handler);\n },\n emit(type: any, event?: any) {\n handlers.get(type)?.forEach((h: any) => h(event));\n handlers.get(\"*\")?.forEach((h: any) => h(type, event));\n },\n };\n\n /* ━━ Init handler ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n const handleInit = (payload: any) => {\n const {\n props,\n drawingSurface: canvas,\n width,\n top,\n left,\n height,\n pixelRatio,\n } = payload;\n\n console.log(\"[renderOffscreen] Init received\", {\n width,\n height,\n pixelRatio,\n });\n\n try {\n // Unmount root if already mounted\n if (root) {\n root.unmount();\n }\n\n offscreenCanvas = canvas;\n\n // Shim the canvas into a fake window/document\n Object.assign(canvas, {\n pageXOffset: left,\n pageYOffset: top,\n clientLeft: left,\n clientTop: top,\n clientWidth: width,\n clientHeight: height,\n style: { touchAction: \"none\" },\n ownerDocument: canvas,\n documentElement: canvas,\n getBoundingClientRect() {\n return size;\n },\n setAttribute() {},\n setPointerCapture() {},\n releasePointerCapture() {},\n addEventListener(event: string, callback: () => void) {\n emitter.on(event, callback);\n },\n removeEventListener(event: string, callback: () => void) {\n emitter.off(event, callback);\n },\n });\n\n // Create react-three-fiber root\n root = createRoot(canvas);\n\n // Configure root\n root.configure({\n events: createPointerEvents(emitter),\n size: (size = { width, height, top, left }),\n dpr: (dpr = Math.min(Math.max(1, pixelRatio), 2)),\n frameloop: \"demand\", // Critical: only render when invalidated\n ...props,\n onCreated: (state: RootState) => {\n if (props.eventPrefix) {\n state.setEvents({\n compute: (event: DomEvent, state: RootState) => {\n const x = event[\n (props.eventPrefix + \"X\") as keyof DomEvent\n ] as number;\n const y = event[\n (props.eventPrefix + \"Y\") as keyof DomEvent\n ] as number;\n state.pointer.set(\n (x / state.size.width) * 2 - 1,\n -(y / state.size.height) * 2 + 1,\n );\n state.raycaster.setFromCamera(state.pointer, state.camera);\n },\n });\n }\n },\n });\n\n // Render children once\n console.log(\"[renderOffscreen] Rendering children\");\n root.render(children);\n console.log(\"[renderOffscreen] Init complete\");\n } catch (e: any) {\n console.error(\"[renderOffscreen] Init error:\", e);\n postMessage({ type: \"error\", payload: e?.message });\n }\n\n // Shim window to the canvas from here on\n (self as any).window = canvas;\n };\n\n /* ━━ Resize handler ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n const handleResize = ({ width, height, top, left }: Size) => {\n if (!root) return;\n root.configure({ size: (size = { width, height, top, left }), dpr });\n };\n\n /* ━━ Event handler ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n const handleEvents = (payload: any) => {\n emitter.emit(payload.eventName, {\n ...payload,\n preventDefault() {},\n stopPropagation() {},\n });\n };\n\n /* ━━ Props handler ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n const handleProps = (payload: any) => {\n if (!root) return;\n if (payload.dpr) dpr = payload.dpr;\n root.configure({ size, dpr, ...payload });\n };\n\n /* ━━ Frame rendering + pixel capture ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n const handleRenderFrame = async ({\n timeMs,\n durationMs,\n requestId,\n }: RenderFramePayload) => {\n console.log(\"[renderOffscreen] Render frame\", { timeMs, requestId });\n\n try {\n // 1. Update time store (triggers React re-render via useSyncExternalStore)\n timeStore.update(timeMs, durationMs);\n\n // 2. Force R3F to process the state change and render synchronously\n const state = (root as any)?.store?.getState?.();\n if (!state) {\n throw new Error(\"[renderOffscreen] No R3F root state available\");\n }\n\n if (state?.gl && state?.scene && state?.camera) {\n // Mark as needing render\n state.invalidate();\n\n // Synchronous render (bypasses RAF)\n state.gl.render(state.scene, state.camera);\n\n // GPU sync - ensure all WebGL commands complete\n state.gl.getContext().finish();\n } else {\n throw new Error(\"[renderOffscreen] Missing gl/scene/camera in state\");\n }\n\n // 3. Capture pixels without clearing the canvas\n if (!offscreenCanvas) {\n throw new Error(\"[renderOffscreen] No offscreen canvas available\");\n }\n\n const bitmap = await createImageBitmap(offscreenCanvas);\n console.log(\"[renderOffscreen] Bitmap created\", {\n width: bitmap.width,\n height: bitmap.height,\n });\n\n // 4. Transfer back to main thread (zero-copy transfer)\n (self as any).postMessage({ type: \"frameRendered\", requestId, bitmap }, [\n bitmap,\n ]);\n } catch (e: any) {\n console.error(\"[renderOffscreen] Frame render error:\", e);\n postMessage({\n type: \"error\",\n message: e?.message || \"Unknown error in handleRenderFrame\",\n });\n }\n };\n\n /* ━━ Message routing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n const handlerMap = {\n resize: handleResize,\n init: handleInit,\n dom_events: handleEvents,\n props: handleProps,\n renderFrame: handleRenderFrame,\n };\n\n self.onmessage = (event) => {\n const { type, payload } = event.data;\n const handler = handlerMap[type as keyof typeof handlerMap];\n if (handler) handler(payload);\n };\n\n /* ━━ Three.js shims for worker environment ━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n // Override ImageLoader to use fetch + createImageBitmap instead of DOM Image\n (THREE.ImageLoader.prototype as any).load = function (\n url: string,\n onLoad: (img: ImageBitmap) => void,\n _onProgress: () => void,\n onError: (e: Error) => void,\n ) {\n if (this.path !== undefined) url = this.path + url;\n url = this.manager.resolveURL(url);\n const scope = this;\n const cached = THREE.Cache.get(url);\n\n if (cached !== undefined) {\n scope.manager.itemStart(url);\n if (onLoad) onLoad(cached);\n scope.manager.itemEnd(url);\n return cached;\n }\n\n fetch(url)\n .then((res) => res.blob())\n .then((res) =>\n createImageBitmap(res, {\n premultiplyAlpha: \"none\",\n colorSpaceConversion: \"none\",\n }),\n )\n .then((bitmap) => {\n THREE.Cache.add(url, bitmap);\n if (onLoad) onLoad(bitmap);\n scope.manager.itemEnd(url);\n })\n .catch(onError);\n\n return {};\n };\n\n /* ━━ DOM shims for worker environment ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n (self as any).window = {};\n (self as any).document = {};\n (self as any).Image = class {\n height = 1;\n width = 1;\n set onload(callback: any) {\n callback(true);\n }\n };\n}\n"],"mappings":";;;;;AA+BA,MAAM,SAAS;CACb,SAAS,CAAC,SAAS,MAAM;CACzB,eAAe,CAAC,eAAe,MAAM;CACrC,eAAe,CAAC,YAAY,MAAM;CAClC,SAAS,CAAC,SAAS,KAAK;CACxB,eAAe,CAAC,eAAe,KAAK;CACpC,aAAa,CAAC,aAAa,KAAK;CAChC,gBAAgB,CAAC,gBAAgB,KAAK;CACtC,eAAe,CAAC,eAAe,KAAK;CACpC,iBAAiB,CAAC,iBAAiB,KAAK;CACxC,sBAAsB,CAAC,sBAAsB,KAAK;CACnD;AAED,SAAS,oBAAoB,SAAwC;AACnE,SAAQ,UAAgD;EACtD,MAAM,EAAE,kBAAkB,aAAa,MAAM;AAE7C,SAAO;GACL,UAAU;GACV,SAAS;GACT,QAAQ,OAAO,OAAO;AACpB,UAAM,QAAQ,IACX,MAAM,UAAU,MAAM,KAAK,QAAS,IAAI,GACzC,EAAE,MAAM,UAAU,MAAM,KAAK,UAAU,IAAI,EAC5C;AACD,UAAM,UAAU,cAAc,MAAM,SAAS,MAAM,OAAO;;GAG5D,WAAW;GACX,UAAU,OAAO,KAAK,OAAO,CAAC,QAC3B,KAAK,SAAS;IAAE,GAAG;KAAM,MAAM,cAAc,IAAI;IAAE,GACpD,EAAE,CACH;GACD,UAAU,WAAW;IACnB,MAAM,EAAE,KAAK,WAAW,MAAM,UAAU;AACxC,WAAO,cAAc;AACrB,SAAK,WAAW,EAAE,QAAQ;KAAE,GAAG,MAAM;KAAQ,WAAW;KAAQ,EAAE,EAAE;AACpE,WAAO,QAAQ,QAAQ,YAAY,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,WAAW;KAChE,MAAM,CAAC,aAAa,OAAO;AAC3B,aAAQ,GAAG,WAAkB,MAAa;MAC1C;;GAEJ,kBAAkB;IAChB,MAAM,EAAE,KAAK,WAAW,MAAM,UAAU;AACxC,QAAI,OAAO,WAAW;AACpB,YAAO,QAAQ,OAAO,YAAY,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,WAAW;MAC/D,MAAM,CAAC,aAAa,OAAO;AAC3B,cAAQ,IAAI,WAAkB,MAAa;OAC3C;AACF,UAAK,WAAW,EACd,QAAQ;MAAE,GAAG,MAAM;MAAQ,WAAW;MAAW,EAClD,EAAE;;;GAGR;;;AAaL,MAAMA,YAAuB;CAC3B,QAAQ;CACR,YAAY;CACZ,2BAAW,IAAI,KAAK;CACpB,OAAO,QAAgB,YAAoB;AACzC,OAAK,SAAS;AACd,OAAK,aAAa;AAClB,OAAK,UAAU,SAAS,MAAM,GAAG,CAAC;;CAErC;;;;;;;;;;;;;;;;;;;;AA+CD,SAAgB,gBAAgB,UAA2B;CACzD,IAAIC,OAAiD;CACrD,IAAIC,kBAA0C;CAC9C,IAAIC,OAAa;EAAE,OAAO;EAAG,QAAQ;EAAG,KAAK;EAAG,MAAM;EAAG;CACzD,IAAI,MAAM;CAEV,MAAM,2BAAW,IAAI,KAAoB;CACzC,MAAMC,UAAyC;EAC7C,qBAAK,IAAI,KAAK;EACd,GAAG,MAAW,SAAc;GAC1B,MAAM,IAAI,SAAS,IAAI,KAAK,oBAAI,IAAI,KAAK;AACzC,KAAE,IAAI,QAAQ;AACd,YAAS,IAAI,MAAM,EAAE;;EAEvB,IAAI,MAAW,SAAc;AAC3B,YAAS,IAAI,KAAK,EAAE,OAAO,QAAQ;;EAErC,KAAK,MAAW,OAAa;AAC3B,YAAS,IAAI,KAAK,EAAE,SAAS,MAAW,EAAE,MAAM,CAAC;AACjD,YAAS,IAAI,IAAI,EAAE,SAAS,MAAW,EAAE,MAAM,MAAM,CAAC;;EAEzD;CAID,MAAM,cAAc,YAAiB;EACnC,MAAM,EACJ,OACA,gBAAgB,QAChB,OACA,KACA,MACA,QACA,eACE;AAEJ,UAAQ,IAAI,mCAAmC;GAC7C;GACA;GACA;GACD,CAAC;AAEF,MAAI;AAEF,OAAI,KACF,MAAK,SAAS;AAGhB,qBAAkB;AAGlB,UAAO,OAAO,QAAQ;IACpB,aAAa;IACb,aAAa;IACb,YAAY;IACZ,WAAW;IACX,aAAa;IACb,cAAc;IACd,OAAO,EAAE,aAAa,QAAQ;IAC9B,eAAe;IACf,iBAAiB;IACjB,wBAAwB;AACtB,YAAO;;IAET,eAAe;IACf,oBAAoB;IACpB,wBAAwB;IACxB,iBAAiB,OAAe,UAAsB;AACpD,aAAQ,GAAG,OAAO,SAAS;;IAE7B,oBAAoB,OAAe,UAAsB;AACvD,aAAQ,IAAI,OAAO,SAAS;;IAE/B,CAAC;AAGF,UAAO,WAAW,OAAO;AAGzB,QAAK,UAAU;IACb,QAAQ,oBAAoB,QAAQ;IACpC,MAAO,OAAO;KAAE;KAAO;KAAQ;KAAK;KAAM;IAC1C,KAAM,MAAM,KAAK,IAAI,KAAK,IAAI,GAAG,WAAW,EAAE,EAAE;IAChD,WAAW;IACX,GAAG;IACH,YAAY,UAAqB;AAC/B,SAAI,MAAM,YACR,OAAM,UAAU,EACd,UAAU,OAAiB,YAAqB;MAC9C,MAAM,IAAI,MACP,MAAM,cAAc;MAEvB,MAAM,IAAI,MACP,MAAM,cAAc;AAEvB,cAAM,QAAQ,IACX,IAAIC,QAAM,KAAK,QAAS,IAAI,GAC7B,EAAE,IAAIA,QAAM,KAAK,UAAU,IAAI,EAChC;AACD,cAAM,UAAU,cAAcA,QAAM,SAASA,QAAM,OAAO;QAE7D,CAAC;;IAGP,CAAC;AAGF,WAAQ,IAAI,uCAAuC;AACnD,QAAK,OAAO,SAAS;AACrB,WAAQ,IAAI,kCAAkC;WACvCC,GAAQ;AACf,WAAQ,MAAM,iCAAiC,EAAE;AACjD,eAAY;IAAE,MAAM;IAAS,SAAS,GAAG;IAAS,CAAC;;AAIrD,EAAC,KAAa,SAAS;;CAKzB,MAAM,gBAAgB,EAAE,OAAO,QAAQ,KAAK,WAAiB;AAC3D,MAAI,CAAC,KAAM;AACX,OAAK,UAAU;GAAE,MAAO,OAAO;IAAE;IAAO;IAAQ;IAAK;IAAM;GAAG;GAAK,CAAC;;CAKtE,MAAM,gBAAgB,YAAiB;AACrC,UAAQ,KAAK,QAAQ,WAAW;GAC9B,GAAG;GACH,iBAAiB;GACjB,kBAAkB;GACnB,CAAC;;CAKJ,MAAM,eAAe,YAAiB;AACpC,MAAI,CAAC,KAAM;AACX,MAAI,QAAQ,IAAK,OAAM,QAAQ;AAC/B,OAAK,UAAU;GAAE;GAAM;GAAK,GAAG;GAAS,CAAC;;CAK3C,MAAM,oBAAoB,OAAO,EAC/B,QACA,YACA,gBACwB;AACxB,UAAQ,IAAI,kCAAkC;GAAE;GAAQ;GAAW,CAAC;AAEpE,MAAI;AAEF,aAAU,OAAO,QAAQ,WAAW;GAGpC,MAAM,QAAS,MAAc,OAAO,YAAY;AAChD,OAAI,CAAC,MACH,OAAM,IAAI,MAAM,gDAAgD;AAGlE,OAAI,OAAO,MAAM,OAAO,SAAS,OAAO,QAAQ;AAE9C,UAAM,YAAY;AAGlB,UAAM,GAAG,OAAO,MAAM,OAAO,MAAM,OAAO;AAG1C,UAAM,GAAG,YAAY,CAAC,QAAQ;SAE9B,OAAM,IAAI,MAAM,qDAAqD;AAIvE,OAAI,CAAC,gBACH,OAAM,IAAI,MAAM,kDAAkD;GAGpE,MAAM,SAAS,MAAM,kBAAkB,gBAAgB;AACvD,WAAQ,IAAI,oCAAoC;IAC9C,OAAO,OAAO;IACd,QAAQ,OAAO;IAChB,CAAC;AAGF,GAAC,KAAa,YAAY;IAAE,MAAM;IAAiB;IAAW;IAAQ,EAAE,CACtE,OACD,CAAC;WACKA,GAAQ;AACf,WAAQ,MAAM,yCAAyC,EAAE;AACzD,eAAY;IACV,MAAM;IACN,SAAS,GAAG,WAAW;IACxB,CAAC;;;CAMN,MAAM,aAAa;EACjB,QAAQ;EACR,MAAM;EACN,YAAY;EACZ,OAAO;EACP,aAAa;EACd;AAED,MAAK,aAAa,UAAU;EAC1B,MAAM,EAAE,MAAM,YAAY,MAAM;EAChC,MAAM,UAAU,WAAW;AAC3B,MAAI,QAAS,SAAQ,QAAQ;;AAM/B,CAAC,MAAM,YAAY,UAAkB,OAAO,SAC1C,KACA,QACA,aACA,SACA;AACA,MAAI,KAAK,SAAS,OAAW,OAAM,KAAK,OAAO;AAC/C,QAAM,KAAK,QAAQ,WAAW,IAAI;EAClC,MAAM,QAAQ;EACd,MAAM,SAAS,MAAM,MAAM,IAAI,IAAI;AAEnC,MAAI,WAAW,QAAW;AACxB,SAAM,QAAQ,UAAU,IAAI;AAC5B,OAAI,OAAQ,QAAO,OAAO;AAC1B,SAAM,QAAQ,QAAQ,IAAI;AAC1B,UAAO;;AAGT,QAAM,IAAI,CACP,MAAM,QAAQ,IAAI,MAAM,CAAC,CACzB,MAAM,QACL,kBAAkB,KAAK;GACrB,kBAAkB;GAClB,sBAAsB;GACvB,CAAC,CACH,CACA,MAAM,WAAW;AAChB,SAAM,MAAM,IAAI,KAAK,OAAO;AAC5B,OAAI,OAAQ,QAAO,OAAO;AAC1B,SAAM,QAAQ,QAAQ,IAAI;IAC1B,CACD,MAAM,QAAQ;AAEjB,SAAO,EAAE;;AAKX,CAAC,KAAa,SAAS,EAAE;AACzB,CAAC,KAAa,WAAW,EAAE;AAC3B,CAAC,KAAa,QAAQ,MAAM;;iBACjB;gBACD;;EACR,IAAI,OAAO,UAAe;AACxB,YAAS,KAAK"}
1
+ {"version":3,"file":"renderOffscreen.js","names":["timeStore: TimeStore","root: ReconcilerRoot<HTMLCanvasElement> | null","offscreenCanvas: OffscreenCanvas | null","size: Size","emitter: Emitter<Record<any, unknown>>","state","e: any"],"sources":["../../src/r3f/renderOffscreen.ts"],"sourcesContent":["/**\n * Worker-side entry point for offscreen R3F rendering.\n *\n * This extends @react-three/offscreen's render() function with:\n * - Time synchronization store for composition time\n * - Frame rendering + pixel capture pipeline\n * - Support for deterministic frame-by-frame rendering\n *\n * Based on @react-three/offscreen render.ts but extended for Editframe's needs.\n */\n\nimport * as React from \"react\";\nimport { useSyncExternalStore } from \"react\";\n// @ts-ignore\nimport * as THREE from \"three\";\nimport type { Emitter } from \"mitt\";\nimport {\n createRoot,\n createEvents,\n ReconcilerRoot,\n Size,\n RootState,\n EventManager,\n Events,\n} from \"@react-three/fiber\";\nimport type { RenderFramePayload } from \"./worker-protocol\";\n\ntype R3FStore = Parameters<typeof createEvents>[0];\ntype DomEvent = PointerEvent | MouseEvent | WheelEvent;\n\n/* ━━ Event handling (from @react-three/offscreen) ━━━━━━━━━━━━━━━━━━━━━ */\n\nconst EVENTS = {\n onClick: [\"click\", false],\n onContextMenu: [\"contextmenu\", false],\n onDoubleClick: [\"dblclick\", false],\n onWheel: [\"wheel\", true],\n onPointerDown: [\"pointerdown\", true],\n onPointerUp: [\"pointerup\", true],\n onPointerLeave: [\"pointerleave\", true],\n onPointerMove: [\"pointermove\", true],\n onPointerCancel: [\"pointercancel\", true],\n onLostPointerCapture: [\"lostpointercapture\", true],\n} as const;\n\nfunction createPointerEvents(emitter: Emitter<Record<any, unknown>>) {\n return (store: R3FStore): EventManager<HTMLElement> => {\n const { handlePointer } = createEvents(store);\n\n return {\n priority: 1,\n enabled: true,\n compute(event, state) {\n state.pointer.set(\n (event.offsetX / state.size.width) * 2 - 1,\n -(event.offsetY / state.size.height) * 2 + 1,\n );\n state.raycaster.setFromCamera(state.pointer, state.camera);\n },\n\n connected: undefined,\n handlers: Object.keys(EVENTS).reduce(\n (acc, key) => ({ ...acc, [key]: handlePointer(key) }),\n {},\n ) as unknown as Events,\n connect: (target) => {\n const { set, events } = store.getState();\n events.disconnect?.();\n set((state) => ({ events: { ...state.events, connected: target } }));\n Object.entries(events?.handlers ?? []).forEach(([name, event]) => {\n const [eventName] = EVENTS[name as keyof typeof EVENTS];\n emitter.on(eventName as any, event as any);\n });\n },\n disconnect: () => {\n const { set, events } = store.getState();\n if (events.connected) {\n Object.entries(events.handlers ?? []).forEach(([name, event]) => {\n const [eventName] = EVENTS[name as keyof typeof EVENTS];\n emitter.off(eventName as any, event as any);\n });\n set((state) => ({\n events: { ...state.events, connected: undefined },\n }));\n }\n },\n };\n };\n}\n\n/* ━━ Time synchronization store ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\ninterface TimeStore {\n timeMs: number;\n durationMs: number;\n listeners: Set<() => void>;\n update(timeMs: number, durationMs: number): void;\n}\n\nconst timeStore: TimeStore = {\n timeMs: 0,\n durationMs: 0,\n listeners: new Set(),\n update(timeMs: number, durationMs: number) {\n this.timeMs = timeMs;\n this.durationMs = durationMs;\n this.listeners.forEach((l) => l());\n },\n};\n\n/**\n * Hook to read composition time inside R3F scenes running in a worker.\n * Must be used within a scene rendered by renderOffscreen().\n */\nexport function useCompositionTime() {\n const timeMs = useSyncExternalStore(\n (callback) => {\n timeStore.listeners.add(callback);\n return () => timeStore.listeners.delete(callback);\n },\n () => timeStore.timeMs,\n );\n\n const durationMs = useSyncExternalStore(\n (callback) => {\n timeStore.listeners.add(callback);\n return () => timeStore.listeners.delete(callback);\n },\n () => timeStore.durationMs,\n );\n\n return { timeMs, durationMs };\n}\n\n/* ━━ Worker entry point ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n/**\n * Render a React Three Fiber scene in a web worker with offscreen canvas.\n *\n * This extends @react-three/offscreen's render() with Editframe-specific features:\n * - Time synchronization via timeStore and useCompositionTime hook\n * - Frame-by-frame rendering on demand (renderFrame message)\n * - Pixel capture and transfer back to main thread via ImageBitmap\n *\n * @param children - React node containing the R3F scene\n *\n * @example\n * ```typescript\n * // worker.ts\n * import { renderOffscreen } from '@editframe/react/r3f';\n * import { MyScene } from './MyScene';\n *\n * renderOffscreen(<MyScene />);\n * ```\n */\nexport function renderOffscreen(children: React.ReactNode) {\n let root: ReconcilerRoot<HTMLCanvasElement> | null = null;\n let offscreenCanvas: OffscreenCanvas | null = null;\n let size: Size = { width: 0, height: 0, top: 0, left: 0 };\n let dpr = 1;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const handlers = new Map<any, Set<any>>();\n const emitter: Emitter<Record<any, unknown>> = {\n all: new Map(),\n on(type: any, handler: any) {\n const s = handlers.get(type) ?? new Set();\n s.add(handler);\n handlers.set(type, s);\n },\n off(type: any, handler: any) {\n handlers.get(type)?.delete(handler);\n },\n emit(type: any, event?: any) {\n handlers.get(type)?.forEach((h: any) => h(event));\n handlers.get(\"*\")?.forEach((h: any) => h(type, event));\n },\n };\n\n /* ━━ Init handler ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n const handleInit = (payload: any) => {\n const { props, drawingSurface: canvas, width, top, left, height, pixelRatio } = payload;\n\n console.log(\"[renderOffscreen] Init received\", {\n width,\n height,\n pixelRatio,\n });\n\n try {\n // Unmount root if already mounted\n if (root) {\n root.unmount();\n }\n\n offscreenCanvas = canvas;\n\n // Shim the canvas into a fake window/document\n Object.assign(canvas, {\n pageXOffset: left,\n pageYOffset: top,\n clientLeft: left,\n clientTop: top,\n clientWidth: width,\n clientHeight: height,\n style: { touchAction: \"none\" },\n ownerDocument: canvas,\n documentElement: canvas,\n getBoundingClientRect() {\n return size;\n },\n setAttribute() {},\n setPointerCapture() {},\n releasePointerCapture() {},\n addEventListener(event: string, callback: () => void) {\n emitter.on(event, callback);\n },\n removeEventListener(event: string, callback: () => void) {\n emitter.off(event, callback);\n },\n });\n\n // Create react-three-fiber root\n root = createRoot(canvas);\n\n // Configure root\n root.configure({\n events: createPointerEvents(emitter),\n size: (size = { width, height, top, left }),\n dpr: (dpr = Math.min(Math.max(1, pixelRatio), 2)),\n frameloop: \"demand\", // Critical: only render when invalidated\n ...props,\n onCreated: (state: RootState) => {\n if (props.eventPrefix) {\n state.setEvents({\n compute: (event: DomEvent, state: RootState) => {\n const x = event[(props.eventPrefix + \"X\") as keyof DomEvent] as number;\n const y = event[(props.eventPrefix + \"Y\") as keyof DomEvent] as number;\n state.pointer.set((x / state.size.width) * 2 - 1, -(y / state.size.height) * 2 + 1);\n state.raycaster.setFromCamera(state.pointer, state.camera);\n },\n });\n }\n },\n });\n\n // Render children once\n console.log(\"[renderOffscreen] Rendering children\");\n root.render(children);\n console.log(\"[renderOffscreen] Init complete\");\n } catch (e: any) {\n console.error(\"[renderOffscreen] Init error:\", e);\n postMessage({ type: \"error\", payload: e?.message });\n }\n\n // Shim window to the canvas from here on\n (self as any).window = canvas;\n };\n\n /* ━━ Resize handler ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n const handleResize = ({ width, height, top, left }: Size) => {\n if (!root) return;\n root.configure({ size: (size = { width, height, top, left }), dpr });\n };\n\n /* ━━ Event handler ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n const handleEvents = (payload: any) => {\n emitter.emit(payload.eventName, {\n ...payload,\n preventDefault() {},\n stopPropagation() {},\n });\n };\n\n /* ━━ Props handler ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n const handleProps = (payload: any) => {\n if (!root) return;\n if (payload.dpr) dpr = payload.dpr;\n root.configure({ size, dpr, ...payload });\n };\n\n /* ━━ Frame rendering + pixel capture ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n const handleRenderFrame = async ({ timeMs, durationMs, requestId }: RenderFramePayload) => {\n console.log(\"[renderOffscreen] Render frame\", { timeMs, requestId });\n\n try {\n // 1. Update time store (triggers React re-render via useSyncExternalStore)\n timeStore.update(timeMs, durationMs);\n\n // 2. Force R3F to process the state change and render synchronously\n const state = (root as any)?.store?.getState?.();\n if (!state) {\n throw new Error(\"[renderOffscreen] No R3F root state available\");\n }\n\n if (state?.gl && state?.scene && state?.camera) {\n // Mark as needing render\n state.invalidate();\n\n // Synchronous render (bypasses RAF)\n state.gl.render(state.scene, state.camera);\n\n // GPU sync - ensure all WebGL commands complete\n state.gl.getContext().finish();\n } else {\n throw new Error(\"[renderOffscreen] Missing gl/scene/camera in state\");\n }\n\n // 3. Capture pixels without clearing the canvas\n if (!offscreenCanvas) {\n throw new Error(\"[renderOffscreen] No offscreen canvas available\");\n }\n\n const bitmap = await createImageBitmap(offscreenCanvas);\n console.log(\"[renderOffscreen] Bitmap created\", {\n width: bitmap.width,\n height: bitmap.height,\n });\n\n // 4. Transfer back to main thread (zero-copy transfer)\n (self as any).postMessage({ type: \"frameRendered\", requestId, bitmap }, [bitmap]);\n } catch (e: any) {\n console.error(\"[renderOffscreen] Frame render error:\", e);\n postMessage({\n type: \"error\",\n message: e?.message || \"Unknown error in handleRenderFrame\",\n });\n }\n };\n\n /* ━━ Message routing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n const handlerMap = {\n resize: handleResize,\n init: handleInit,\n dom_events: handleEvents,\n props: handleProps,\n renderFrame: handleRenderFrame,\n };\n\n self.onmessage = (event) => {\n const { type, payload } = event.data;\n const handler = handlerMap[type as keyof typeof handlerMap];\n if (handler) handler(payload);\n };\n\n /* ━━ Three.js shims for worker environment ━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n // Override ImageLoader to use fetch + createImageBitmap instead of DOM Image\n (THREE.ImageLoader.prototype as any).load = function (\n url: string,\n onLoad: (img: ImageBitmap) => void,\n _onProgress: () => void,\n onError: (e: Error) => void,\n ) {\n if (this.path !== undefined) url = this.path + url;\n url = this.manager.resolveURL(url);\n const cached = THREE.Cache.get(url);\n\n if (cached !== undefined) {\n this.manager.itemStart(url);\n if (onLoad) onLoad(cached);\n this.manager.itemEnd(url);\n return cached;\n }\n\n const manager = this.manager;\n fetch(url)\n .then((res) => res.blob())\n .then((res) =>\n createImageBitmap(res, {\n premultiplyAlpha: \"none\",\n colorSpaceConversion: \"none\",\n }),\n )\n .then((bitmap) => {\n THREE.Cache.add(url, bitmap);\n if (onLoad) onLoad(bitmap);\n manager.itemEnd(url);\n })\n .catch(onError);\n\n return {};\n };\n\n /* ━━ DOM shims for worker environment ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n (self as any).window = {};\n (self as any).document = {};\n (self as any).Image = class {\n height = 1;\n width = 1;\n set onload(callback: any) {\n callback(true);\n }\n };\n}\n"],"mappings":";;;;;AAgCA,MAAM,SAAS;CACb,SAAS,CAAC,SAAS,MAAM;CACzB,eAAe,CAAC,eAAe,MAAM;CACrC,eAAe,CAAC,YAAY,MAAM;CAClC,SAAS,CAAC,SAAS,KAAK;CACxB,eAAe,CAAC,eAAe,KAAK;CACpC,aAAa,CAAC,aAAa,KAAK;CAChC,gBAAgB,CAAC,gBAAgB,KAAK;CACtC,eAAe,CAAC,eAAe,KAAK;CACpC,iBAAiB,CAAC,iBAAiB,KAAK;CACxC,sBAAsB,CAAC,sBAAsB,KAAK;CACnD;AAED,SAAS,oBAAoB,SAAwC;AACnE,SAAQ,UAA+C;EACrD,MAAM,EAAE,kBAAkB,aAAa,MAAM;AAE7C,SAAO;GACL,UAAU;GACV,SAAS;GACT,QAAQ,OAAO,OAAO;AACpB,UAAM,QAAQ,IACX,MAAM,UAAU,MAAM,KAAK,QAAS,IAAI,GACzC,EAAE,MAAM,UAAU,MAAM,KAAK,UAAU,IAAI,EAC5C;AACD,UAAM,UAAU,cAAc,MAAM,SAAS,MAAM,OAAO;;GAG5D,WAAW;GACX,UAAU,OAAO,KAAK,OAAO,CAAC,QAC3B,KAAK,SAAS;IAAE,GAAG;KAAM,MAAM,cAAc,IAAI;IAAE,GACpD,EAAE,CACH;GACD,UAAU,WAAW;IACnB,MAAM,EAAE,KAAK,WAAW,MAAM,UAAU;AACxC,WAAO,cAAc;AACrB,SAAK,WAAW,EAAE,QAAQ;KAAE,GAAG,MAAM;KAAQ,WAAW;KAAQ,EAAE,EAAE;AACpE,WAAO,QAAQ,QAAQ,YAAY,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,WAAW;KAChE,MAAM,CAAC,aAAa,OAAO;AAC3B,aAAQ,GAAG,WAAkB,MAAa;MAC1C;;GAEJ,kBAAkB;IAChB,MAAM,EAAE,KAAK,WAAW,MAAM,UAAU;AACxC,QAAI,OAAO,WAAW;AACpB,YAAO,QAAQ,OAAO,YAAY,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,WAAW;MAC/D,MAAM,CAAC,aAAa,OAAO;AAC3B,cAAQ,IAAI,WAAkB,MAAa;OAC3C;AACF,UAAK,WAAW,EACd,QAAQ;MAAE,GAAG,MAAM;MAAQ,WAAW;MAAW,EAClD,EAAE;;;GAGR;;;AAaL,MAAMA,YAAuB;CAC3B,QAAQ;CACR,YAAY;CACZ,2BAAW,IAAI,KAAK;CACpB,OAAO,QAAgB,YAAoB;AACzC,OAAK,SAAS;AACd,OAAK,aAAa;AAClB,OAAK,UAAU,SAAS,MAAM,GAAG,CAAC;;CAErC;;;;;;;;;;;;;;;;;;;;AA+CD,SAAgB,gBAAgB,UAA2B;CACzD,IAAIC,OAAiD;CACrD,IAAIC,kBAA0C;CAC9C,IAAIC,OAAa;EAAE,OAAO;EAAG,QAAQ;EAAG,KAAK;EAAG,MAAM;EAAG;CACzD,IAAI,MAAM;CAEV,MAAM,2BAAW,IAAI,KAAoB;CACzC,MAAMC,UAAyC;EAC7C,qBAAK,IAAI,KAAK;EACd,GAAG,MAAW,SAAc;GAC1B,MAAM,IAAI,SAAS,IAAI,KAAK,oBAAI,IAAI,KAAK;AACzC,KAAE,IAAI,QAAQ;AACd,YAAS,IAAI,MAAM,EAAE;;EAEvB,IAAI,MAAW,SAAc;AAC3B,YAAS,IAAI,KAAK,EAAE,OAAO,QAAQ;;EAErC,KAAK,MAAW,OAAa;AAC3B,YAAS,IAAI,KAAK,EAAE,SAAS,MAAW,EAAE,MAAM,CAAC;AACjD,YAAS,IAAI,IAAI,EAAE,SAAS,MAAW,EAAE,MAAM,MAAM,CAAC;;EAEzD;CAID,MAAM,cAAc,YAAiB;EACnC,MAAM,EAAE,OAAO,gBAAgB,QAAQ,OAAO,KAAK,MAAM,QAAQ,eAAe;AAEhF,UAAQ,IAAI,mCAAmC;GAC7C;GACA;GACA;GACD,CAAC;AAEF,MAAI;AAEF,OAAI,KACF,MAAK,SAAS;AAGhB,qBAAkB;AAGlB,UAAO,OAAO,QAAQ;IACpB,aAAa;IACb,aAAa;IACb,YAAY;IACZ,WAAW;IACX,aAAa;IACb,cAAc;IACd,OAAO,EAAE,aAAa,QAAQ;IAC9B,eAAe;IACf,iBAAiB;IACjB,wBAAwB;AACtB,YAAO;;IAET,eAAe;IACf,oBAAoB;IACpB,wBAAwB;IACxB,iBAAiB,OAAe,UAAsB;AACpD,aAAQ,GAAG,OAAO,SAAS;;IAE7B,oBAAoB,OAAe,UAAsB;AACvD,aAAQ,IAAI,OAAO,SAAS;;IAE/B,CAAC;AAGF,UAAO,WAAW,OAAO;AAGzB,QAAK,UAAU;IACb,QAAQ,oBAAoB,QAAQ;IACpC,MAAO,OAAO;KAAE;KAAO;KAAQ;KAAK;KAAM;IAC1C,KAAM,MAAM,KAAK,IAAI,KAAK,IAAI,GAAG,WAAW,EAAE,EAAE;IAChD,WAAW;IACX,GAAG;IACH,YAAY,UAAqB;AAC/B,SAAI,MAAM,YACR,OAAM,UAAU,EACd,UAAU,OAAiB,YAAqB;MAC9C,MAAM,IAAI,MAAO,MAAM,cAAc;MACrC,MAAM,IAAI,MAAO,MAAM,cAAc;AACrC,cAAM,QAAQ,IAAK,IAAIC,QAAM,KAAK,QAAS,IAAI,GAAG,EAAE,IAAIA,QAAM,KAAK,UAAU,IAAI,EAAE;AACnF,cAAM,UAAU,cAAcA,QAAM,SAASA,QAAM,OAAO;QAE7D,CAAC;;IAGP,CAAC;AAGF,WAAQ,IAAI,uCAAuC;AACnD,QAAK,OAAO,SAAS;AACrB,WAAQ,IAAI,kCAAkC;WACvCC,GAAQ;AACf,WAAQ,MAAM,iCAAiC,EAAE;AACjD,eAAY;IAAE,MAAM;IAAS,SAAS,GAAG;IAAS,CAAC;;AAIrD,EAAC,KAAa,SAAS;;CAKzB,MAAM,gBAAgB,EAAE,OAAO,QAAQ,KAAK,WAAiB;AAC3D,MAAI,CAAC,KAAM;AACX,OAAK,UAAU;GAAE,MAAO,OAAO;IAAE;IAAO;IAAQ;IAAK;IAAM;GAAG;GAAK,CAAC;;CAKtE,MAAM,gBAAgB,YAAiB;AACrC,UAAQ,KAAK,QAAQ,WAAW;GAC9B,GAAG;GACH,iBAAiB;GACjB,kBAAkB;GACnB,CAAC;;CAKJ,MAAM,eAAe,YAAiB;AACpC,MAAI,CAAC,KAAM;AACX,MAAI,QAAQ,IAAK,OAAM,QAAQ;AAC/B,OAAK,UAAU;GAAE;GAAM;GAAK,GAAG;GAAS,CAAC;;CAK3C,MAAM,oBAAoB,OAAO,EAAE,QAAQ,YAAY,gBAAoC;AACzF,UAAQ,IAAI,kCAAkC;GAAE;GAAQ;GAAW,CAAC;AAEpE,MAAI;AAEF,aAAU,OAAO,QAAQ,WAAW;GAGpC,MAAM,QAAS,MAAc,OAAO,YAAY;AAChD,OAAI,CAAC,MACH,OAAM,IAAI,MAAM,gDAAgD;AAGlE,OAAI,OAAO,MAAM,OAAO,SAAS,OAAO,QAAQ;AAE9C,UAAM,YAAY;AAGlB,UAAM,GAAG,OAAO,MAAM,OAAO,MAAM,OAAO;AAG1C,UAAM,GAAG,YAAY,CAAC,QAAQ;SAE9B,OAAM,IAAI,MAAM,qDAAqD;AAIvE,OAAI,CAAC,gBACH,OAAM,IAAI,MAAM,kDAAkD;GAGpE,MAAM,SAAS,MAAM,kBAAkB,gBAAgB;AACvD,WAAQ,IAAI,oCAAoC;IAC9C,OAAO,OAAO;IACd,QAAQ,OAAO;IAChB,CAAC;AAGF,GAAC,KAAa,YAAY;IAAE,MAAM;IAAiB;IAAW;IAAQ,EAAE,CAAC,OAAO,CAAC;WAC1EA,GAAQ;AACf,WAAQ,MAAM,yCAAyC,EAAE;AACzD,eAAY;IACV,MAAM;IACN,SAAS,GAAG,WAAW;IACxB,CAAC;;;CAMN,MAAM,aAAa;EACjB,QAAQ;EACR,MAAM;EACN,YAAY;EACZ,OAAO;EACP,aAAa;EACd;AAED,MAAK,aAAa,UAAU;EAC1B,MAAM,EAAE,MAAM,YAAY,MAAM;EAChC,MAAM,UAAU,WAAW;AAC3B,MAAI,QAAS,SAAQ,QAAQ;;AAM/B,CAAC,MAAM,YAAY,UAAkB,OAAO,SAC1C,KACA,QACA,aACA,SACA;AACA,MAAI,KAAK,SAAS,OAAW,OAAM,KAAK,OAAO;AAC/C,QAAM,KAAK,QAAQ,WAAW,IAAI;EAClC,MAAM,SAAS,MAAM,MAAM,IAAI,IAAI;AAEnC,MAAI,WAAW,QAAW;AACxB,QAAK,QAAQ,UAAU,IAAI;AAC3B,OAAI,OAAQ,QAAO,OAAO;AAC1B,QAAK,QAAQ,QAAQ,IAAI;AACzB,UAAO;;EAGT,MAAM,UAAU,KAAK;AACrB,QAAM,IAAI,CACP,MAAM,QAAQ,IAAI,MAAM,CAAC,CACzB,MAAM,QACL,kBAAkB,KAAK;GACrB,kBAAkB;GAClB,sBAAsB;GACvB,CAAC,CACH,CACA,MAAM,WAAW;AAChB,SAAM,MAAM,IAAI,KAAK,OAAO;AAC5B,OAAI,OAAQ,QAAO,OAAO;AAC1B,WAAQ,QAAQ,IAAI;IACpB,CACD,MAAM,QAAQ;AAEjB,SAAO,EAAE;;AAKX,CAAC,KAAa,SAAS,EAAE;AACzB,CAAC,KAAa,WAAW,EAAE;AAC3B,CAAC,KAAa,QAAQ,MAAM;;iBACjB;gBACD;;EACR,IAAI,OAAO,UAAe;AACxB,YAAS,KAAK"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/react",
3
- "version": "0.45.2",
3
+ "version": "0.45.4",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,22 +40,39 @@
40
40
  "license": "SEE LICENSE IN LICENSE-FULL.md",
41
41
  "dependencies": {
42
42
  "@editframe/elements": "0.43.0",
43
- "@lit/task": "^1.0.1",
44
- "@react-three/fiber": "^9.5.0",
45
- "@react-three/offscreen": "^0.0.8",
46
43
  "lit": "^3.3.1",
47
- "mitt": "^3.0.1",
48
- "react": "^19.0.0",
49
- "react-dom": "^19.0.0",
50
- "three": "^0.182.0"
44
+ "mitt": "^3.0.1"
45
+ },
46
+ "peerDependencies": {
47
+ "@react-three/fiber": ">=8.0.0",
48
+ "@react-three/offscreen": ">=0.0.8",
49
+ "react": ">=18.0.0",
50
+ "react-dom": ">=18.0.0",
51
+ "three": ">=0.133"
52
+ },
53
+ "peerDependenciesMeta": {
54
+ "@react-three/fiber": {
55
+ "optional": true
56
+ },
57
+ "@react-three/offscreen": {
58
+ "optional": true
59
+ },
60
+ "three": {
61
+ "optional": true
62
+ }
51
63
  },
52
64
  "devDependencies": {
65
+ "@react-three/fiber": "^9.5.0",
66
+ "@react-three/offscreen": "^0.0.8",
53
67
  "@types/node": "^22.0.0",
54
68
  "@types/react": "^19.0.0",
55
69
  "@types/react-dom": "^19.0.0",
56
70
  "autoprefixer": "^10.4.19",
57
71
  "postcss": "^8.4.38",
72
+ "react": "^19.0.0",
73
+ "react-dom": "^19.0.0",
58
74
  "tailwindcss": "^3.4.3",
75
+ "three": "^0.182.0",
59
76
  "typescript": "^5.9.3",
60
77
  "vitest": "^4.0.0"
61
78
  },
package/tsdown.config.ts CHANGED
@@ -12,10 +12,7 @@ const inlineCssPlugin = (): Plugin => ({
12
12
  name: "inline-css",
13
13
  resolveId(source, importer) {
14
14
  if (source.endsWith(".css?inline") && importer) {
15
- const resolved = path.resolve(
16
- path.dirname(importer),
17
- source.replace("?inline", ""),
18
- );
15
+ const resolved = path.resolve(path.dirname(importer), source.replace("?inline", ""));
19
16
  return { id: `${resolved}?inline`, external: false };
20
17
  }
21
18
  return null;