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
- Use
:=for top-level variable declarations - Use
=insideparameters {}clauses - In object literals, both may work but
=is more common
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
- Check that each curve's endpoint matches the next curve's startpoint
- The last curve must connect back to the first curve's startpoint
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:
- Parameters/variables
- Geometry (vertices, curves, areas)
- Model type declaration
- Mesh control
- Materials and sections
- Members
- Supports
- 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
- Top-level scripts: Use keyword-first imperative syntax (
vertex {name} xyz ...) - Inside GClass/GBlock bodies: Use declarative assignment syntax (
name := Expression(...))
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
- GClass instances with geometry
- GBlock instances
- Objects that have a visual representation
Invalid Expressions for 3JS Export
- Primitive values (numbers, booleans, strings)
- Lists/arrays
- Raw vertices, curves (unless wrapped in renderable container)
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
- Some newer features may not work
- Error messages may be less helpful
- Performance may differ
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. distribution — lcs 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.jsonwith{"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(), orPow().
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 addcount (N)whenithparametersindexes 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):
- local X = (0, 1, 0) = global +Y
- local Y = (−1, 0, 0) = global −X
- local Z = (0, 0, 1) = global +Z
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":
- First decide the tilt / slope (Ry, Rx) while axes match global.
- 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:
- [ ] Using
:=for variables,=in parameters/objects - [ ] Expressions in xyz/lcs have parentheses
- [ ] Curve boundaries form a closed, connected loop
- [ ] Array indices don't go out of bounds
- [ ] Model type declared after geometry, before analysis entities
- [ ] DOF strings have correct spelling (uxuyuzrxryrz)
- [ ] File paths use forward slashes
- [ ] Parameter names in gblock match gclass exactly
- [ ] Transformation order is intentional (right-to-left)
- [ ] Using correct syntax for context (top-level vs GClass body)
- [ ] Exporting renderable objects for 3JS/HiScene output
- [ ] Curves use
vertexnotlinekeyword for straight edges - [ ]
.Sum/.Count/.Min/.Maxhave NO parentheses (properties, not methods) - [ ]
.Select,.Where,.Any,.AllDO have parentheses (methods taking a lambda) - [ ] Use
.SelectIterate(idx, elem => ...)when you need both index and value - [ ] list-of-int
.Select(i => expr)—iis each element value, not an index position - [ ]
distributionlcsclause uses(GCS)in parentheses, not bareGCS - [ ] Exponentiation uses
**orSqrt()/Pow()— never^ - [ ]
repetitions spacings (array)createsarray.Count + 1instances (fencepost); usecount (N)to limit - [ ] Chained rotations are LOCAL: do slope/tilt (Ry/Rx) before orientation (Rz) to keep them global-aligned
- [ ]
InputItemArraydefines an array schema, not runtime rows; do not build it with data iteration such asgarages.SelectIterate(...)