@@ -103,6 +103,29 @@ function writeCollaborationSignalingChannels(channels: readonly WebRtcTabCollabo
103103 }
104104}
105105
106+ function readCollaborationRtcConfiguration ( ) : RTCConfiguration | undefined {
107+ const params = new URLSearchParams ( window . location . search ) ;
108+ const serializedConfiguration = params . get ( 'collabRtcConfiguration' ) ;
109+ if ( serializedConfiguration ) {
110+ try {
111+ const parsedConfiguration = JSON . parse ( serializedConfiguration ) as RTCConfiguration ;
112+ if ( parsedConfiguration && typeof parsedConfiguration === 'object' )
113+ return parsedConfiguration ;
114+ } catch {
115+ }
116+ }
117+
118+ const iceServerUrls = params . getAll ( 'collabIceServer' )
119+ . map ( value => value . trim ( ) )
120+ . filter ( Boolean ) ;
121+ if ( iceServerUrls . length === 0 )
122+ return undefined ;
123+
124+ return {
125+ iceServers : iceServerUrls . map ( url => ( { urls : url } ) )
126+ } ;
127+ }
128+
106129import { DockManager , DockSpawnTsWebcomponent } from 'dock-spawn-ts' ;
107130import { BaseCustomWebComponentConstructorAppend , css , Disposable , html , LazyLoader } from '@node-projects/base-custom-webcomponent' ;
108131import { CommandHandling } from './CommandHandling.js'
@@ -274,6 +297,7 @@ export class AppShell extends BaseCustomWebComponentConstructorAppend {
274297 private _npmPackageLoader = new NpmPackageLoader ( ) ;
275298 private _collaborationPeerBaseId = getOrCreateCollaborationPeerBaseId ( ) ;
276299 private _collaborationSignalingChannels = readCollaborationSignalingChannels ( ) ;
300+ private _collaborationRtcConfiguration = readCollaborationRtcConfiguration ( ) ;
277301 private _collaborationSessionOverrides = new WeakMap < DocumentContainer , string > ( ) ;
278302 private _collaborationTransports = new WeakMap < DocumentContainer , WebRtcTabCollaborationTransport > ( ) ;
279303 private _closeCollaborationHelpPopup ?: ( ) => void ;
@@ -569,7 +593,10 @@ export class AppShell extends BaseCustomWebComponentConstructorAppend {
569593 private _getOrCreateCollaborationTransport ( documentContainer : DocumentContainer ) {
570594 let transport = this . _collaborationTransports . get ( documentContainer ) ;
571595 if ( ! transport ) {
572- transport = new WebRtcTabCollaborationTransport ( { enabledSignalingChannels : this . _collaborationSignalingChannels } ) ;
596+ transport = new WebRtcTabCollaborationTransport ( {
597+ enabledSignalingChannels : this . _collaborationSignalingChannels ,
598+ rtcConfiguration : this . _collaborationRtcConfiguration ,
599+ } as any ) ;
573600 this . _collaborationTransports . set ( documentContainer , transport ) ;
574601 } else {
575602 transport . setEnabledSignalingChannels ( this . _collaborationSignalingChannels ) ;
@@ -670,6 +697,7 @@ export class AppShell extends BaseCustomWebComponentConstructorAppend {
670697 <li>Copy the new signaling bundle in client B and paste it back in client A.</li>
671698 <li>If a client still shows a newer bundle, copy that one back once more.</li>
672699 </ol>
700+ <div style="margin-bottom: 12px;">Cross-machine WebRTC often needs STUN or TURN servers. Add <code>collabIceServer</code> query parameters, for example <code>?collabIceServer=stun:stun.l.google.com:19302</code>, or pass a full JSON config in <code>collabRtcConfiguration</code>.</div>
673701 <div style="margin-bottom: 12px;">The paste action reads from the clipboard directly when the browser allows it.</div>
674702 ` ;
675703
@@ -774,6 +802,11 @@ export class AppShell extends BaseCustomWebComponentConstructorAppend {
774802 this . exportData ( 'html' ) ;
775803 }
776804 } ,
805+ {
806+ title : 'export as EMF' , action : async ( ) => {
807+ this . exportData ( 'emf' ) ;
808+ }
809+ } ,
777810 { title : '-' } ,
778811 {
779812 title : 'export overlay' , checked : this . _exportOverlays , checkable : true , action : ( ) => {
@@ -790,6 +823,56 @@ export class AppShell extends BaseCustomWebComponentConstructorAppend {
790823 ] , e ) ;
791824 }
792825
826+ async exportData ( format : 'dxf' | 'pdf' | 'png' | 'svg' | 'html' | 'emf' ) {
827+ const { extractIR, renderIR, DXFWriter, PDFWriter, PNGWriter, SVGWriter, HTMLWriter, EMFWriter } = await import ( "@node-projects/layout2vector" ) ;
828+
829+ const doc = < DocumentContainer > this . _dockManager . activeDocument . resolvedElementContent ;
830+
831+ const source = this . _exportOverlays ? [
832+ doc . designerView . designerCanvas . rootDesignItem . element ,
833+ doc . designerView . designerCanvas . overlayLayer
834+ ] : doc . designerView . designerCanvas . rootDesignItem . element ;
835+
836+ const ir = await extractIR ( source , {
837+ boxType : "border" , // "border" | "content"
838+ includeText : true , // extract text node geometry
839+ includeInvisible : false , // skip display:none / visibility:hidden
840+ includeImages : true ,
841+ zoom : 1 / doc . designerView . designerCanvas . zoomFactor ,
842+ convertFormControls : true , // convert form controls (input, select, textarea) to rectangles with text
843+ walkIframes : true , // recursively extract iframes
844+ } ) ;
845+ if ( format === 'dxf' ) {
846+ const dxfWriter = new DXFWriter ( document . documentElement . scrollHeight ) ;
847+ const dxfString = await renderIR ( ir , dxfWriter ) ;
848+ await saveData ( dxfString , "dxfFile" , 'dxf' ) ;
849+ } else if ( format === 'pdf' ) {
850+ const pdfWriter = new PDFWriter ( 1000 , 1000 ) ;
851+ const pdfDoc = await renderIR ( ir , pdfWriter ) ;
852+ await pdfDoc . finalize ( ) ;
853+ const pdfBytes = pdfDoc . toBytes ( ) ;
854+ await saveData ( pdfBytes , 'pdfFile' , 'pdf' ) ;
855+ } else if ( format === 'png' ) {
856+ const pngWriter = new PNGWriter ( document . documentElement . scrollWidth , document . documentElement . scrollHeight ) ;
857+ const pngResult = await renderIR ( ir , pngWriter ) ;
858+ await pngResult . finalize ( ) ;
859+ const pngBytes = pngResult . toBytes ( ) ;
860+ await saveData ( pngBytes , 'pngFile' , 'png' ) ;
861+ } else if ( format === 'svg' ) {
862+ const svgWriter = new SVGWriter ( 2000 , 1000 ) ;
863+ const svgString = await renderIR ( ir , svgWriter ) ;
864+ await saveData ( svgString , 'svgFile' , 'svg' ) ;
865+ } else if ( format === 'html' ) {
866+ const htmlWriter = new HTMLWriter ( 1000 , 2000 ) ;
867+ const htmlContent = await renderIR ( ir , htmlWriter ) ;
868+ await saveData ( htmlContent , 'htmlFile' , 'html' ) ;
869+ } else if ( format === 'emf' ) {
870+ const emfWriter = new EMFWriter ( 1000 , 2000 ) ;
871+ const emfContent = await renderIR ( ir , emfWriter ) ;
872+ await saveData ( emfContent , 'emfFile' , 'emf' ) ;
873+ }
874+ }
875+
793876 showCollaborationContextMenu ( e : MouseEvent ) {
794877 const documentContainer = this . _getActiveDocumentContainer ( ) ;
795878 const collaborationService = documentContainer ?. instanceServiceContainer . collaborationService ;
@@ -907,50 +990,6 @@ export class AppShell extends BaseCustomWebComponentConstructorAppend {
907990 ] , e ) ;
908991 }
909992
910- async exportData ( format : 'dxf' | 'pdf' | 'png' | 'svg' | 'html' ) {
911- const { extractIR, renderIR, DXFWriter, PDFWriter, PNGWriter, SVGWriter, HTMLWriter } = await import ( "@node-projects/layout2vector" ) ;
912-
913- const doc = < DocumentContainer > this . _dockManager . activeDocument . resolvedElementContent ;
914-
915- const source = this . _exportOverlays ? [
916- doc . designerView . designerCanvas . rootDesignItem . element ,
917- doc . designerView . designerCanvas . overlayLayer
918- ] : doc . designerView . designerCanvas . rootDesignItem . element ;
919-
920- const ir = await extractIR ( source , {
921- boxType : "border" , // "border" | "content"
922- includeText : true , // extract text node geometry
923- includeInvisible : false , // skip display:none / visibility:hidden
924- includeImages : true ,
925- zoom : 1 / doc . designerView . designerCanvas . zoomFactor
926- } ) ;
927- if ( format === 'dxf' ) {
928- const dxfWriter = new DXFWriter ( document . documentElement . scrollHeight ) ;
929- const dxfString = await renderIR ( ir , dxfWriter ) ;
930- await saveData ( dxfString , "dxfFile" , 'dxf' ) ;
931- } else if ( format === 'pdf' ) {
932- const pdfWriter = new PDFWriter ( 1000 , 1000 ) ;
933- const pdfDoc = await renderIR ( ir , pdfWriter ) ;
934- await pdfDoc . finalize ( ) ;
935- const pdfBytes = pdfDoc . toBytes ( ) ;
936- await saveData ( pdfBytes , 'pdfFile' , 'pdf' ) ;
937- } else if ( format === 'png' ) {
938- const pngWriter = new PNGWriter ( document . documentElement . scrollWidth , document . documentElement . scrollHeight ) ;
939- const pngResult = await renderIR ( ir , pngWriter ) ;
940- await pngResult . finalize ( ) ;
941- const pngBytes = pngResult . toBytes ( ) ;
942- await saveData ( pngBytes , 'pngFile' , 'png' ) ;
943- } else if ( format === 'svg' ) {
944- const svgWriter = new SVGWriter ( 2000 , 1000 ) ;
945- const svgString = await renderIR ( ir , svgWriter ) ;
946- await saveData ( svgString , 'svgFile' , 'svg' ) ;
947- } else if ( format === 'html' ) {
948- const htmlWriter = new HTMLWriter ( 1000 , 2000 ) ;
949- const htmlContent = await renderIR ( ir , htmlWriter ) ;
950- await saveData ( htmlContent , 'htmlFile' , 'html' ) ;
951- }
952- }
953-
954993 engine : webllmType . MLCEngine ;
955994 async LLM ( ) {
956995 let op = this . _getDomElement < HTMLTextAreaElement > ( 'llmOutput' ) ;
0 commit comments