@@ -146,6 +146,81 @@ function extractFromZip(zipData: Uint8Array, targetName: string): Uint8Array | n
146146 return null ;
147147}
148148
149+ // ── Synthetic connector factory ──
150+
151+ /**
152+ * Create a lightweight `ModelicaClassInstance` with `classKind = CONNECTOR`
153+ * and a triangular `Icon` annotation. Used for FMU input/output ports so
154+ * that diagram renderers (which filter for `CONNECTOR`) display them.
155+ *
156+ * Input connectors: solid‐filled blue triangle (matches MSL `RealInput`).
157+ * Output connectors: unfilled blue triangle (matches MSL `RealOutput`).
158+ */
159+ function createSyntheticConnector ( parent : ModelicaClassInstance , isInput : boolean ) : ModelicaClassInstance {
160+ const connector = new ModelicaClassInstance ( parent ) ;
161+ connector . classKind = ModelicaClassKind . CONNECTOR ;
162+ connector . name = isInput ? "RealInput" : "RealOutput" ;
163+ connector . instantiated = true ;
164+ connector . declaredElements = [ ] ;
165+
166+ // Triangle polygon points (Modelica coordinate system, -100..100)
167+ // Input: filled blue triangle pointing right
168+ // Output: unfilled blue triangle pointing right
169+ const trianglePoints : [ number , number ] [ ] = isInput
170+ ? [
171+ [ - 100 , 100 ] ,
172+ [ 100 , 0 ] ,
173+ [ - 100 , - 100 ] ,
174+ [ - 100 , 100 ] ,
175+ ]
176+ : [
177+ [ - 100 , 100 ] ,
178+ [ 100 , 0 ] ,
179+ [ - 100 , - 100 ] ,
180+ [ - 100 , 100 ] ,
181+ ] ;
182+
183+ const iconData = {
184+ "@type" : "Icon" as const ,
185+ coordinateSystem : {
186+ extent : [
187+ [ - 100 , - 100 ] ,
188+ [ 100 , 100 ] ,
189+ ] as [ [ number , number ] , [ number , number ] ] ,
190+ preserveAspectRatio : true ,
191+ initialScale : 0.2 ,
192+ "@type" : "CoordinateSystem" as const ,
193+ } ,
194+ graphics : [
195+ {
196+ visible : true ,
197+ origin : [ 0 , 0 ] as [ number , number ] ,
198+ rotation : 0 ,
199+ lineColor : [ 0 , 0 , 255 ] as [ number , number , number ] ,
200+ fillColor : isInput
201+ ? ( [ 0 , 0 , 255 ] as [ number , number , number ] ) // filled for input
202+ : ( [ 255 , 255 , 255 ] as [ number , number , number ] ) , // unfilled for output
203+ pattern : "Solid" ,
204+ fillPattern : isInput ? "Solid" : "Solid" ,
205+ lineThickness : 0.25 ,
206+ points : trianglePoints ,
207+ smooth : "None" ,
208+ "@type" : "Polygon" as const ,
209+ } ,
210+ ] ,
211+ } ;
212+
213+ // Override annotation() to return the synthetic Icon
214+ connector . annotation = function < T > ( name : string , annotations ?: ModelicaNamedElement [ ] | null ) : T | null {
215+ if ( name === "Icon" && ( ! annotations || annotations === this . annotations ) ) {
216+ return iconData as unknown as T ;
217+ }
218+ return ModelicaClassInstance . prototype . annotation . call ( this , name , annotations ) as T | null ;
219+ } ;
220+
221+ return connector ;
222+ }
223+
149224// ── ModelicaFmuEntity ──
150225
151226/**
@@ -235,6 +310,107 @@ export class ModelicaFmuEntity extends ModelicaClassInstance {
235310 return super . resolveSimpleName ( identifier , global , encapsulated ) ;
236311 }
237312
313+ /** Build the graphics array for Icon/Diagram annotations (rectangle + name + port labels). */
314+ #buildFmuGraphics( ) : unknown [ ] {
315+ const graphics : unknown [ ] = [
316+ {
317+ visible : true ,
318+ origin : [ 0 , 0 ] ,
319+ rotation : 0 ,
320+ lineColor : [ 0 , 0 , 255 ] ,
321+ fillColor : [ 255 , 255 , 255 ] ,
322+ pattern : "Solid" ,
323+ lineThickness : 0.25 ,
324+ borderPattern : "None" ,
325+ extent : [
326+ [ - 100 , - 100 ] ,
327+ [ 100 , 100 ] ,
328+ ] ,
329+ radius : 0 ,
330+ "@type" : "Rectangle" ,
331+ } ,
332+ {
333+ visible : true ,
334+ origin : [ 0 , 0 ] ,
335+ rotation : 0 ,
336+ extent : [
337+ [ - 100 , 20 ] ,
338+ [ 100 , - 20 ] ,
339+ ] ,
340+ textString : "%name" ,
341+ fontSize : 0 ,
342+ textStyle : [ ] ,
343+ textColor : [ 0 , 0 , 0 ] ,
344+ horizontalAlignment : "Center" ,
345+ "@type" : "Text" ,
346+ } ,
347+ ] ;
348+
349+ // Add port name labels inside the block
350+ const inputs = this . fmuVariables . filter ( ( v ) => v . causality === "input" ) ;
351+ const outputs = this . fmuVariables . filter ( ( v ) => v . causality === "output" ) ;
352+
353+ for ( let i = 0 ; i < inputs . length ; i ++ ) {
354+ const y = 100 - ( ( i + 1 ) * 200 ) / ( inputs . length + 1 ) ;
355+ graphics . push ( {
356+ visible : true ,
357+ origin : [ 0 , 0 ] ,
358+ rotation : 0 ,
359+ extent : [
360+ [ - 98 , y - 8 ] ,
361+ [ - 30 , y + 8 ] ,
362+ ] ,
363+ textString : inputs [ i ] ?. name ?? "" ,
364+ fontSize : 0 ,
365+ textStyle : [ ] ,
366+ textColor : [ 0 , 0 , 0 ] ,
367+ horizontalAlignment : "Left" ,
368+ "@type" : "Text" ,
369+ } ) ;
370+ }
371+
372+ for ( let i = 0 ; i < outputs . length ; i ++ ) {
373+ const y = 100 - ( ( i + 1 ) * 200 ) / ( outputs . length + 1 ) ;
374+ graphics . push ( {
375+ visible : true ,
376+ origin : [ 0 , 0 ] ,
377+ rotation : 0 ,
378+ extent : [
379+ [ 30 , y - 8 ] ,
380+ [ 98 , y + 8 ] ,
381+ ] ,
382+ textString : outputs [ i ] ?. name ?? "" ,
383+ fontSize : 0 ,
384+ textStyle : [ ] ,
385+ textColor : [ 0 , 0 , 0 ] ,
386+ horizontalAlignment : "Right" ,
387+ "@type" : "Text" ,
388+ } ) ;
389+ }
390+
391+ return graphics ;
392+ }
393+
394+ override annotation < T > ( name : string , annotations ?: ModelicaNamedElement [ ] | null ) : T | null {
395+ if ( ( name === "Icon" || name === "Diagram" ) && ( ! annotations || annotations === this . annotations ) ) {
396+ if ( ! this . #loaded) this . load ( ) ;
397+ return {
398+ "@type" : name ,
399+ coordinateSystem : {
400+ extent : [
401+ [ - 100 , - 100 ] ,
402+ [ 100 , 100 ] ,
403+ ] ,
404+ preserveAspectRatio : true ,
405+ initialScale : 0.1 ,
406+ "@type" : "CoordinateSystem" ,
407+ } ,
408+ graphics : this . #buildFmuGraphics( ) ,
409+ } as unknown as T ;
410+ }
411+ return super . annotation ( name , annotations ) ;
412+ }
413+
238414 override instantiate ( ) : void {
239415 if ( this . instantiated ) return ;
240416 if ( this . instantiating ) return ;
@@ -244,25 +420,82 @@ export class ModelicaFmuEntity extends ModelicaClassInstance {
244420 this . declaredElements = [ ] ;
245421 this . #syntheticComponents = [ ] ;
246422
247- // Resolve the predefined Real type for component class instances
423+ // Resolve the predefined Real type for non-port component class instances
248424 const realType = this . root ?. resolveSimpleName ( "Real" ) as ModelicaClassInstance | null ;
249425
250- for ( const v of this . fmuVariables ) {
426+ // Segregate by causality to calculate port spacing
427+ const inputs = this . fmuVariables . filter ( ( v ) => v . causality === "input" ) ;
428+ const outputs = this . fmuVariables . filter ( ( v ) => v . causality === "output" ) ;
429+ const others = this . fmuVariables . filter ( ( v ) => v . causality !== "input" && v . causality !== "output" ) ;
430+
431+ let inputCount = 0 ;
432+ let outputCount = 0 ;
433+
434+ for ( const v of [ ...inputs , ...outputs , ...others ] ) {
251435 // Create a synthetic component instance with null AST node
252436 const comp = new ModelicaComponentInstance ( this , null ) ;
253437 comp . name = v . name ;
254438 comp . description = v . description || null ;
255439
256440 // Set causality from FMU variable
257- if ( v . causality === "input" ) {
441+ const isInput = v . causality === "input" ;
442+ const isOutput = v . causality === "output" ;
443+ if ( isInput ) {
258444 comp . causality = ModelicaCausality . INPUT ;
259- } else if ( v . causality === "output" ) {
445+ } else if ( isOutput ) {
260446 comp . causality = ModelicaCausality . OUTPUT ;
261447 }
262448
263- // Set the class instance to Real (all FMU 2.0 continuous variables are Real)
264- if ( realType ) {
265- comp . classInstance = realType . clone ( ) ;
449+ if ( isInput || isOutput ) {
450+ // Create a synthetic CONNECTOR class instance so diagram renderers
451+ // recognise this component as a port (they filter for classKind === CONNECTOR).
452+ comp . classInstance = createSyntheticConnector ( this , isInput ) ;
453+
454+ let y = 0 ;
455+ if ( isInput ) {
456+ y = 100 - ( ( inputCount + 1 ) * 200 ) / ( inputs . length + 1 ) ;
457+ inputCount ++ ;
458+ } else {
459+ y = 100 - ( ( outputCount + 1 ) * 200 ) / ( outputs . length + 1 ) ;
460+ outputCount ++ ;
461+ }
462+
463+ const extent : [ [ number , number ] , [ number , number ] ] = isInput
464+ ? [
465+ [ - 120 , y - 10 ] ,
466+ [ - 100 , y + 10 ] ,
467+ ]
468+ : [
469+ [ 100 , y - 10 ] ,
470+ [ 120 , y + 10 ] ,
471+ ] ;
472+
473+ comp . annotation = function < T > ( name : string , annotations ?: ModelicaNamedElement [ ] | null ) : T | null {
474+ if ( name === "Placement" && ( ! annotations || annotations === this . annotations ) ) {
475+ return {
476+ "@type" : "Placement" ,
477+ visible : true ,
478+ transformation : {
479+ extent : extent ,
480+ rotation : 0 ,
481+ origin : [ 0 , 0 ] ,
482+ "@type" : "Transformation" ,
483+ } ,
484+ iconTransformation : {
485+ extent : extent ,
486+ rotation : 0 ,
487+ origin : [ 0 , 0 ] ,
488+ "@type" : "Transformation" ,
489+ } ,
490+ } as unknown as T ;
491+ }
492+ return ModelicaComponentInstance . prototype . annotation . call ( this , name , annotations ) as T | null ;
493+ } ;
494+ } else {
495+ // Non-port variables: use Real type
496+ if ( realType ) {
497+ comp . classInstance = realType . clone ( ) ;
498+ }
266499 }
267500
268501 comp . instantiated = true ;
0 commit comments