FCS Common Pitfalls & Gotchas

This document covers common mistakes and confusing aspects of FCS that can trip up new users (including AI agents).


1. Assignment Operators: = vs :=

The Confusion

FCS uses two different assignment operators with subtly different meanings:

# Variable binding (mutable, evaluated lazily)
x := 10

# Parameter binding in objects/gblocks (immediate, often used in object literals)
gblock {gb} gclass {gc} parameters {x = 10}

# Object literal properties
obj := { a = 1, b = 2 }

Pitfall

Using the wrong operator in the wrong context:

# WRONG - may not work in some contexts
gblock {gb} gclass {gc} parameters {x := 10}  

# CORRECT for parameters clause
gblock {gb} gclass {gc} parameters {x = 10}

Rule of Thumb


2. Self-Reference Timing in GBlocks

The Pattern

GBlocks can reference their own properties for positioning:

gblock {gbCss1} gclass {gCss} lcs {Origin={gbCss1.h/2,0,0}} parameters {h:=0.3}

Pitfall

The self-reference relies on lazy evaluation. If you reference a property that depends on another property not yet resolved, you get circular dependency errors.

# RISKY - depends on evaluation order
gblock {gb1} gclass {gc} lcs {Origin={gb1.width/2, 0, 0}} parameters {width := gb1.height * 2}
#                                                                              ^^^^^^^^^^^
#                                                     height not defined here!

Solution

Ensure referenced properties are defined independently:

height := 0.5
gblock {gb1} gclass {gc} lcs {Origin={gb1.width/2, 0, 0}} parameters {height := height, width := height * 2}

3. Transformation Order Matters

The Issue

Coordinate system transformations are applied right-to-left (like matrix multiplication):

# These are DIFFERENT:
GCS.Tx(5).Rz(PI/2)    # First rotate, then translate
GCS.Rz(PI/2).Tx(5)    # First translate, then rotate

Pitfall

# Intending to: move 5 units in X, then rotate 90°
lcs (GCS.Rz(PI/2).Tx(5))   # WRONG - rotates first, then translates in rotated X
lcs (GCS.Tx(5).Rz(PI/2))   # CORRECT - translates, then rotates

Visualization

GCS.Tx(5).Rz(PI/2):
  Start at origin → Move to (5,0,0) → Rotate around origin → End at (0,5,0)

GCS.Rz(PI/2).Tx(5):
  Start at origin → Rotate (axes now rotated) → Move 5 in new X direction → End at (0,5,0)? No! The axes are rotated, so Tx moves in the new X which was old -Y.

4. Curve Boundary Order for Areas

The Issue

When defining an area with boundary curve, the curves must form a closed loop in the correct order:

# Curves must connect: end of one → start of next
area 1 boundary curve {c1} {c2} {c3} {c4}

Pitfall

If curves don't connect properly, meshing fails silently or produces garbage:

# If c1 goes A→B, c2 should start at B
# WRONG if c2 goes D→C instead of B→C
area 1 boundary curve {c1} {c2} {c3} {c4}  # May fail to mesh

Debugging


5. Numeric IDs vs Named Identifiers

The Issue

Many keywords accept both numeric IDs and named identifiers:

vertex 1 xyz 0 0 0           # Numeric ID
vertex {v1} xyz 0 0 0        # Named identifier

curve 1 vertex 1 2           # Reference by numeric ID
curve {c1} vertex {v1} {v2}  # Reference by name

Pitfall

Mixing styles inconsistently leads to reference errors:

vertex 1 xyz 0 0 0
vertex {v2} xyz 1 0 0

# WRONG - mixing numeric and named references
curve {c1} vertex 1 {v2}     # May work
curve {c1} vertex {1} {v2}   # WRONG - {1} is not valid syntax

Best Practice

Choose one style per script and stick to it. Named identifiers are clearer for complex models.


6. DOF String Format for Supports

The Syntax

Support DOF strings must be exact concatenations:

support 1 curve {c1} fixed "uxuyuzrxryrz"   # All 6 DOFs fixed
support 2 curve {c2} fixed "uxuz"           # Only ux and uz fixed

Pitfall

Typos are easy and produce silent failures or unexpected behavior:

# WRONG - various typos
fixed "ux uy uz"      # Spaces not allowed
fixed "UxUyUz"        # Wrong case (may be case-sensitive)
fixed "uxuyrz"        # Missing 'uz' - probably not intended
fixed "rxryrzuxuyuz"  # Order doesn't matter, but confusing

Valid DOF Names

Only these exact names: ux, uy, uz, rx, ry, rz


7. Parentheses Required in Certain Contexts

The Issue

Expressions embedded in keyword arguments sometimes require parentheses:

# Direct value - no parentheses needed
vertex {v1} xyz 0 0 10

# Expression - MUST have parentheses
vertex {v1} xyz 0 0 (height + offset)
vertex {v2} xyz (span/2) 0 (h1 + h2)

Pitfall

# WRONG - parser may misinterpret
vertex {v1} xyz 0 0 height + offset     # "height" is interpreted as Z, rest is garbage

# CORRECT
vertex {v1} xyz 0 0 (height + offset)

Rule

Always parenthesize expressions in geometry keyword arguments.


8. Array Indexing is 0-Based

The Issue

FCS uses 0-based array indexing (like C#/JavaScript, unlike MATLAB/Lua):

arr := [10, 20, 30]
arr[0]   # → 10
arr[1]   # → 20
arr[2]   # → 30
arr[3]   # → Error: index out of range

Pitfall with ithparameters

heights := [0.5, 0.4, 0.3]   # 3 elements, indices 0-2

distribution {d} gclass {gc} 
    repetitions count (heights.Count)   # count = 3
    specialization ithparameters {
        h := heights[ith.i],        # ith.i goes 0, 1, 2 ✓
        nextH := heights[ith.i+1]   # WRONG on last iteration: ith.i=2, index=3 → error!
    }

Solution

Use one more element than repetitions for "fence-post" patterns:

heights := [0.5, 0.4, 0.3, 0.2]   # 4 elements for 3 segments

9. File Path Separators

The Issue

Windows uses backslashes, but FCS may handle them differently:

# Both may work on Windows:
gclass {gc} filename "css\I_profile.fcs"     # Backslash
gclass {gc} filename "css/I_profile.fcs"     # Forward slash

# For dynamic paths, be careful:
gclass {gc} filename ("css\" + profileName + ".fcs")   # Backslash in string

Pitfall

Backslash may be interpreted as escape character in some contexts:

# RISKY - \I might be interpreted as escape sequence
filename "css\I_profile.fcs"

# SAFER - use forward slashes
filename "css/I_profile.fcs"

Best Practice

Use forward slashes / consistently for cross-platform compatibility.


10. Model Type Declaration Order

The Issue

The model_shell3d (or similar) declaration must appear before certain keywords:

# Define geometry FIRST
vertex {v1} xyz 0 0 0
curve {c1} ...
area 1 ...

# THEN declare model type
model_shell3d

# THEN define analysis entities
mesharea 1 ...
material {mat} ...
planestress 1 ...

Pitfall

# WRONG ORDER - may cause errors
model_shell3d
material {mat} ...    # Before geometry - risky
vertex {v1} xyz ...   # Model type already declared

Best Practice

Follow this order:

  1. Parameters/variables
  2. Geometry (vertices, curves, areas)
  3. Model type declaration
  4. Mesh control
  5. Materials and sections
  6. Members
  7. Supports
  8. Loads

11. Lazy Evaluation Surprises

The Issue

FCS uses lazy evaluation - expressions are computed when needed, not when defined:

x := 10
y := x * 2    # y is NOT 20 yet - it's "x * 2" as an expression
x := 5        # Redefine x
# Now evaluating y gives 10, not 20!

Pitfall

# Counter-intuitive behavior
baseHeight := 10
roof := { height := baseHeight + 5 }

baseHeight := 20   # Change base height

# roof.height is now 25, not 15!

When This Helps

Dynamic/parametric models where changes propagate automatically.

When This Hurts

When you expect "snapshot" behavior at definition time.


12. Object Property Access vs Method Calls

The Issue

Some things look like properties but behave differently:

arr := [1, 2, 3]
arr.Count      # Property - no parentheses
arr.Sum        # Also property-like
arr.Select(...)  # Method - needs parentheses and arguments

Pitfall

# WRONG
arr.Select     # Missing function and arguments
arr.Count()    # Count is not a method

# CORRECT
arr.Select(x => x * 2)
arr.Count

13. GClass Parameter vs Internal Variable

The Issue

Parameters passed to a gclass shadow internal definitions:

# In MyComponent.fcs:
height := 100    # Default value

# When instantiating:
gblock {gb} gclass {MyComponent} parameters {height := 50}
# gb.height is 50, not 100

Pitfall

If you misspell a parameter name, it creates a new variable instead of overriding:

# In MyComponent.fcs:
height := 100

# WRONG - typo, doesn't override 'height'
gblock {gb} gclass {MyComponent} parameters {heigth := 50}
# gb.height is still 100!

14. Conditional GBlock Still Evaluates Class

The Issue

Even with if (False), the gclass file is still loaded and parsed:

gblock {optional} gclass {ExpensiveComponent} lcs GCS if (False)
# ExpensiveComponent.fcs is still loaded and parsed (just not instantiated)

Pitfall

If the gclass file has errors, you get errors even when the condition is false.


16. Top-Level vs GClass Syntax Are Different

The Issue

FCS has two different syntax contexts: top-level scripts and GClass bodies. They use different syntax!

Top-level (imperative style):

vertex {v1} xyz 0 0 0
vertex {v2} xyz 1 0 0
curve {c1} vertex {v1} {v2}
area 1 boundary curve {c1} {c2} {c3} {c4}

Inside GClass (declarative style with :=):

MyShape := GClass {
    v1 := Vertex(0, 0, 0)
    v2 := Vertex(1, 0, 0)
    c1 := Line(v1, v2)
}

Pitfall

# WRONG - mixing top-level syntax inside GClass
BadShape := GClass {
    vertex {v1} xyz 0 0 0    # ERROR: "Expected :="
    curve {c1} vertex {v1} {v2}    # ERROR: "Expected :="
}

# CORRECT - use declarative assignment syntax
GoodShape := GClass {
    v1 := Vertex(0, 0, 0)
    v2 := Vertex(1, 0, 0)
    c1 := Line(v1, v2)
}

Rule


17. HiScene/3JS Export Requires Renderable Objects

The Issue

The --t 3JS export option only works with objects that implement IFcsImageRenderer. Not every expression can be exported.

# This works - Main returns a renderable GClass instance
fli.exe model.fcs "Main" --t 3JS --o output.hiscene.json

# This FAILS - "True" is a boolean, not renderable
fli.exe model.fcs "True" --t 3JS --o output.hiscene.json

# This FAILS - arithmetic result is not renderable  
fli.exe model.fcs "2+2" --t 3JS --o output.hiscene.json

Pitfall

# You wrote a geometry script and want to export it
fli.exe model.fcs "vertices" --t 3JS --o output.json
# ERROR: vertices is a list, not an IFcsImageRenderer

# You need to export the containing class/block
fli.exe model.fcs "Main" --t 3JS --o output.json

Valid Expressions for 3JS Export

Invalid Expressions for 3JS Export


18. Legacy Engine Warning and .fcsconfig.json

The Issue

When running fli.exe, you may see:

Warning: No '.fcsconfig.json' found, so running legacy engine EngineVersion:'e3.emulation' !

This means the script uses the old E3 engine emulation mode.

Impact

Solution

For new projects, create .fcsconfig.json in your project root:

{
    "EngineVersion": "e4"
}

When to Ignore

For simple scripts and learning purposes, the legacy engine warning is harmless. The functionality is the same for core features.


19. There Is No "line" Keyword for Curves

The Issue

When defining straight line edges (curves), there is NO line type keyword. Curves are defined only by their endpoint vertices.

Wrong - Using "line" keyword

vertex {v1} xyz 0 0 0
vertex {v2} xyz 1 0 0
curve {c1} line v1 v2     # ERROR: Expected <NEWLINE>
curve {c2} line {v1} {v2} # ERROR: Same problem

Correct - Just use "vertex"

vertex {v1} xyz 0 0 0
vertex {v2} xyz 1 0 0
curve {c1} vertex {v1} {v2}   # Correct: straight line from v1 to v2

Curve Types Summary

Type Syntax Description
Straight line curve {name} vertex {v1} {v2} Line between 2 vertices
Arc curve {name} arc vertex {v1} {v2} {v3} Arc through 3 points
Polyline curve {name} vertex {v1} {v2} {v3} ... Multi-segment line

Key Point

The word line is not used in curve definitions. Curves are implicitly straight when given exactly 2 vertices. Use arc keyword only when you need an arc through 3 points.


20. .Sum / .Count / .Max etc. Are Properties, Not Methods

Confirmed by live fli.exe testing

FCS list aggregation accessors are properties, not callable methods.
Calling them with parentheses causes '[...].Sum' is not ICallableType but 'System.Double'.

arr := [1.0, 2.0, 3.0]

# CORRECT (no parentheses)
arr.Sum    # → 6
arr.Count  # → 3
arr.Min    # → 1
arr.Max    # → 3

# WRONG (parentheses cause runtime error)
arr.Sum()   # ERROR: not ICallableType
arr.Count() # ERROR: not ICallableType

In contrast, LINQ-style methods with lambdas DO need parentheses:

arr.Select(x => x * 2)   # Correct — takes a lambda argument
arr.Where(x => x > 1)    # Correct — takes a lambda argument
arr.Any(x => x > 2)      # Correct — takes a lambda argument

21. .Select vs .SelectIterate — Getting Indices

.Select(elem => ...) — Element Only

.Select passes each element value to the lambda. There is no index parameter:

[10, 20, 30].Select(x => x * 2)   # → [20, 40, 60]

# If you pass an integer list, the element IS the number:
[0,1,2,3].Select(i => i * 10)     # → [0, 10, 20, 30]

.SelectIterate(index, elem => ...) — Index AND Element

When you need both the 0-based position and the element value, use SelectIterate. The lambda takes two parameters: index first, then element value.

# Signature:  list.SelectIterate( index, value => expression )

[1, 1, 2, 1].SelectIterate( idx, x => x + idx )
# idx=0, x=1 → 1+0=1
# idx=1, x=1 → 1+1=2
# idx=2, x=2 → 2+2=4
# idx=3, x=1 → 1+3=4
# Result: [1, 2, 4, 4]

Common Patterns

# Tag each element with its position
items.SelectIterate( idx, item => { index = idx, value = item } )

# Conditional logic using index
spacings.SelectIterate( idx, val => (idx == 0) ? val * 0.5 : val )

# Use index to look up related data from another array
widths.SelectIterate( idx, w => { width = w, height = heights[idx] } )

Pitfall: Confusing Select and SelectIterate

# WRONG — Select gives NO index; this anonymous "idx" is just the element itself
[10,20,30].Select( idx, x => x + idx )   # May error or misbehave

# CORRECT — use SelectIterate when you need the index
[10,20,30].SelectIterate( idx, x => x + idx )  # → [10, 21, 32]

Preferred Pattern for "repeat N times" uniform lists

# Explicit literal list (clear, safe)
spacings := [6.0, 6.0, 6.0, 6.0]

# Or via Select on [0..n-1]
spacings := [0,1,2,3].Select(ignored => 6.0)

22. distributionlcs Clause Needs Parentheses in Some Forms

Observed Behavior (needs further testing)

In a distribution block, using bare lcs GCS (without parentheses) on its own line has been observed to cause a parse error in the e3.emulation engine. The safe form is always to use parentheses:

# RISKY — may parse-fail in e3.emulation
distribution {dCols} gclass {gCol}
    lcs GCS
    ...

# SAFE — works in both engines
distribution {dCols} gclass {gCol}
    lcs (GCS)
    ...

This mirrors the GBlock rule: bare lcs GCS works for gblock, but distributions may behave differently. Always use lcs (GCS) or lcs (GCS.Tx(...)) in distributions until this is confirmed.

Note: This was observed running e3.emulation (no .fcsconfig.json). Create .fcsconfig.json with {"EngineVersion":"e4.trans"} to use the current engine when in a real project directory.


23. Exponentiation Uses **, Not ^

The Confusion

Many languages use ^ for power (e.g., 2^3 = 8). FCS does not support the caret operator — it causes a BindBinaryOperator crash at parse time.

What Happens

# WRONG — fails with "The method or operation is not implemented"
a := 9 ^ 0.5

# CORRECT — use ** (Python-style exponentiation)
a := 9 ** 0.5          # → 3

# ALSO CORRECT — use Sqrt for square roots
a := Sqrt(9)           # → 3

# ALSO CORRECT — use Pow for general power
a := Pow(2, 3)         # → 8

Available Math Functions in FCS

Function Description Example
Sqrt(x) Square root Sqrt(9) → 3
Pow(x,y) Power (x^y) Pow(2,3) → 8
Atan(x) Arctangent Atan(1) → 0.785…
Atan2(y,x) Two-argument arctangent Atan2(1,1) → π/4
Sin(x) Sine Sin(PI/2) → 1
Cos(x) Cosine Cos(0) → 1
Ceiling(x) Round up Ceiling(2.3) → 3
** Exponent operator 9**0.5 → 3

Rule: Never use ^ for power. Use **, Sqrt(), or Pow().


24. repetitions spacings (array) Creates array.Count + 1 Instances

The Confusion

repetitions spacings (array) uses a fencepost convention: N spacings produce N + 1 positions (like fence posts between fence panels).

baySpacings := [6.0, 6.0, 6.0, 6.0, 6.0]   # 5 elements

# This creates 6 instances (indices 0–5), NOT 5!
distribution {d} gclass {gc} lcs (GCS) transformation translation direction {0,1,0}
  repetitions spacings (baySpacings) specialization ithparameters {
    width := baySpacings[ith.i]   # BOOM at ith.i = 5 → out of range
  }

Why It Matters

If your ithparameters index into the spacings array with array[ith.i], the last instance will be out of bounds because ith.i reaches array.Count but valid indices are 0 .. array.Count - 1.

Fix

Add an explicit count to limit the number of instances:

# Correct: explicitly limit to 5 instances
distribution {d} gclass {gc} lcs (GCS) transformation translation direction {0,1,0}
  repetitions count (baySpacings.Count) spacings (baySpacings) specialization ithparameters {
    width := baySpacings[ith.i]   # ith.i = 0..4, all valid
  }

When You DO Want N+1

For elements placed at every fence post (e.g. portal frames at every gridline):

# 5 spacings → 6 frames → use count (bayCount + 1)
distribution {dFrames} gclass {gcFrame} lcs (GCS) transformation translation direction {0,1,0}
  repetitions count (bayCount + 1) spacings (baySpacings) specialization ithparameters {
    span := span   # does NOT index baySpacings, so no OOB risk
  }

Rule: repetitions spacings (A) = A.Count + 1 instances. Always add count (N) when ithparameters indexes into the spacings array.


25. Chained Rotations Are in LOCAL Axes, Not Global

The Confusion

GCS.Axes.Rz(PI/2).Ry(-roofAngle) looks like it should rotate around global Z then around global Y. But FCS applies the second rotation around the local Y that resulted from the first rotation.

What Happens

After Rz(PI/2):

Then .Ry(-roofAngle) rotates around local Y = (−1, 0, 0), which tilts in the global YZ plane — not in the global XZ plane where the roof slope lives.

Concrete Example (the roof-panel bug)

# WRONG — Ry tilts around local Y = (-1,0,0), tilt in YZ not XZ
#   local Z ≈ (0, -0.119, 0.993) — nearly vertical, not along rafter!
lcs {Origin={0,0,wallHeight}, Axes=GCS.Axes.Rz(PI/2).Ry(-roofAngle)}

# CORRECT — Ry tilts while axes are still global-aligned (Y = (0,1,0)),
#   so tilt lands in the XZ plane;  Rz(PI/2) THEN swings X to Y.
#   local Z ≈ (0.993, 0, 0.119) — along the rafter slope ✓
lcs {Origin={0,0,wallHeight}, Axes=GCS.Axes.Ry(PI/2 - roofAngle).Rz(PI/2)}

The Rule

Think of the rotation chain right-to-left for "which global effect do I want":

  1. First decide the tilt / slope (Ry, Rx) while axes match global.
  2. Then orient the panel (Rz) to swing width along the building.
# LEFT roof:  local Z → up the left rafter  (+X, +Z)
Axes=GCS.Axes.Ry(PI/2 - roofAngle).Rz(PI/2)

# RIGHT roof: local Z → up the right rafter (-X, +Z)
Axes=GCS.Axes.Ry(roofAngle - PI/2).Rz(PI/2)

Rule: Rotation methods chain in LOCAL axes. Do slope/tilt rotations before orientation rotations so they operate in the global-aligned frame.


Summary: Quick Checklist

Before running your FCS script, verify: