Build a Component and Its Easy Client

A practical path from an empty folder to a parametric component with a hand-written browser client, served by the HiStruct "time machine". Every command and request body on this page was run against a real platform on 2026-10-07 while building the reference component EasyPlank (HiStructOrg/fcs-lab-01, branch deve/easy-plank, folder EasyPlank.fcc).

Written for developers and for their coding agents. If you are an agent: read the whole page before writing code — most of the time lost so far went into rediscovering what is written here.

Status. Sections 1–5 work on today's platform. Section 6 (the time machine) uses two endpoints that exist only on the experimental platform branch deve/easy-timemachine — not on any deployment yet.

1. Your Toolbox

Running FLI non-interactively (important for agents)

fli.exe with no arguments — and even fli.exe --help — opens an interactive console. In a shell without a real console (every coding agent) it crashes with "The handle is invalid". Always pass a model and an expression:

# evaluate one expression of a model, run from inside the .fcc folder
fli.exe main.fcs "outVolume" --td Info

# override inputs with an UPDATER as the third argument
fli.exe main.fcs "outVolume" "{input.inLength=4.0, input.inWidth=0.3, input.inThickness=0.05}" --td Info

# list values take the FCS value in an updater (Git Bash quoting shown)
fli.exe main.fcs "outVolume" '{input.inEndShape="Rounded"}' --td Info

Without an updater FLI prints the plain value; with one it prints a JSON envelope {"Content": …, "Exception": null}. Scripts that parse the output should always pass an updater ("{}" works as a no-op). Fcs.Geometry.BoundingBox(gbYourBlock) is the quickest way to prove a geometry follows its inputs.

2. Component Structure — the Current Style

Inputs are generated by the engine from FCS. Declaring input items statically in the .fcscdm (ParameterDescriptions, as some old lab examples still do) is an abandoned approach. Think of input items as static, read-only constants evaluated at runtime: produced by FCS code, never depending on the user's values.

A minimal component (EasyPlank.fcc):

EasyPlank.fcc/
  Definition.fcscdm      references only - no ParameterDescriptions
  InputPages.fcscdx      the pages; each names an FCS identifier that GENERATES its input items
  PlankInput.fcs         the input class: in* inputs with defaults, computed values
  PlankInputType.fcs     the input UI definition (Fcs.Parameter.ItemClass, lists, ranges)
  main.fcs               ties it together: input instance, page items, scene
  Plank.fcs              geometry
  Views_metric.fcscdx    views; view3D uses view3D.fcsdrv (camera)

Definition.fcscdm holds only references:

{
  "HumanName": "Easy Plank",
  "NameFcc": "EasyPlank.fcc",
  "FcsScript": "main.fcs",
  "ParameterDescriptions": null,
  "SharedTypeDescriptionsIdentifier": "sharedTypeDescriptions",
  "ViewsFile": "Views_metric.fcscdx",
  "ComponentPagesFile": "InputPages.fcscdx",
  "StartUpPageName": "Plank"
}

InputPages.fcscdx points a page at the FCS identifier that produces its items:

[ { "Name": "Plank", "HumanName": "Plank",
    "ParameterDescriptionsIdentifier": "inputPagePlank_ParameterItemClasses",
    "DefaultViewName": "view3D", "AdditionalViewNames": [] } ]

and main.fcs defines it:

input := PlankInput

inputPagePlank_ParameterItemClasses := [
   PlankInputType{ NameSpace := "input." }.typeDefinition
]

sharedTypeDescriptions = {
   DropListTypeConversions = True
}

The generated items are addressed by their full path — input.inLength, input.inMaterial — and that path is what a client reads and writes. Lists: keep each option's Key equal to its Value (with DropListTypeConversions = True). See also InputItemArray. For real-world conventions, production components live in fcs-construct-2 — heavier than you need, but authoritative.

3. Colours in the 3D Scene

Give the layer a material — the current, documented way (Material Visualization from Layers). With the default draw settings, a layer that has a material { … } is painted with that material; a layer with only a color is not — its solids come out as a fixed translucent grey (opacity ~0.16). No draw-settings (.fcsdrs) file is needed. SurfaceColor accepts a computed value, so the colour can follow the inputs:

# PlankInput.fcs - plankArgb is picked from inMaterial / inColor
plankRgb := { R := plankArgb.R / 255.0, G := plankArgb.G / 255.0, B := plankArgb.B / 255.0 }

# main.fcs
layer {lPlank} color (input.plankArgb) material {
   SurfaceColor := input.plankRgb,
   Roughness := 0.65,
   Metalness := 0.0,
   Side := "Front"
}
gblock {gbPlank} gclass (plankModel)

The scene then carries the material on the face: colorDiffuse plus materialData: {side, colorDiffuse, roughness, metalness} for the client to use.

Put every face-producing entity on the layer — areas too. A solid built as volume … prism 1 2 layer (lPlank) between two outline areas still shows those two areas as separate grey faces if the areas themselves have no layer:

area 1 boundary curve +1 +2 +3 +4 layer (lPlank)
area 2 boundary curve +5 +6 +7 +8 layer (lPlank)
volume {vPlank} prism 1 2 layer (lPlank)

Check in the scene, not by eye: every face of hi-scene-update's threeJsModel should point at an opaque material. (An older workaround — copying production's SolidsColorByLayer.fcsdrs, which sets geometry_solids.brushColorBy = 2, and referencing it from the view — also works, but costs a 68 KB file and two references. Do not start there.)

4. Getting a Component onto a Palette

  1. Push your branch. The platform reads FCS from GitHub, never from your disk (except through Booster).

  2. Make the platform fetch it. A new branch is not visible until a palette admin "looks at it": the error is "Could not resolve reference … Was the branch pushed and fetched by admin?". The developer's equivalent is one authenticated call:

    GET /api/static/{space}/{lang}/prototype-branch-info?prototype=<url-encoded prototype string>
    Authorization: Bearer <personal access token with palette rights in that space>
    

    Call it again after every push you want new configurations to pick up.

  3. Add the palette item. The prototype string is repo*fcc*branch, for example https://github.com/HiStructClient*fcs-lab-01*EasyPlank.fcc*deve/easy-plank (slashes in branch names are fine). Writing the palette replaces the whole palette — so always:

    • GET /api/static/{space}/raw-palette-items and save it as a backup,
    • append your item and POST the full array back to the same URL,
    • read it again and check that every previous item is byte-identical and in order.

    Each new configuration created from the item is pinned to the commit the branch points at that moment; components created earlier keep their commit.

5. The Client API (what a hand-written client calls)

All component calls: POST /api/spaces/{space}/components/{componentId}/{endpoint}?privateKey=… with Content-Type: application/json and Content-Language: <lang>. HBC Easy's src/api/histruct.ts is the reference implementation and its comments are worth reading.

Create a configuration (anonymous guest flow):

POST /{space}/{lang}/inquiry/new-configuration
{"FccRef":"<prototype string>","ProjectName":"…","Tags":"…","Note":"…","PvcJsonString":null}
→ {"componentId":…,"projectId":…,"privateKey":"…"}

Read one input — load-properties, body {"contextPath":"input.inLength"}. Returns ParamValueDto.Value with Min, Max, Step, DefaultUnit, Value, Identifier; lists also carry Resources.ListOptionsEntries. A whole input class ("input") cannot be loaded — ask per item.

Write — update-properties:

{"vm":{"ComponentId":"490055","ComponentSubRevision":null,"ComponentPrivateKey":"<key>",
  "ParamValueDto":{"Value":{"TypeName":"ParamValueDoubleVM","Identifier":"input.inLength",
  "ParameterPath":"","IsEditable":true,"FromUser":true,"Value":2.5}}},
 "validationTrigger":null,"observedContexts":null,"navigationContexts":null}

For a list item use ParamValueListVM and the option Key exactly as load-properties lists it.

Render — hi-scene-update:

{"viewName":"view3D","viewPvcUpdater":"{}","ownedScenes":""}

viewPvcUpdater is a JSON string and ownedScenes is a string. The answer's scene3Djson holds classes[] (each with threeJsModel.vertices), faceMaterials, lineMaterials; the geometry is Z-up.

Pitfalls that cost the most time

6. The Easy Client and the Time Machine (experimental)

A hand-written client is only correct for the FCS commit it was written for, and components are pinned to their commit. So the client lives in the same commit as the FCS. The design — what belongs to the constant host and what to the pinned module, and why — is on Easy Clients: Host and Module. The steps:

  1. Build the client (any tool — Vite here) into an ES module that implements the host contract: export const hostApi = "1", mount(el, ctx), unmount(). The contract text is CONTRACT.md in HiStructOrg/easy-plank.

  2. Publish it first from committed, pushed code: node tools/publish-client.mjs copies the build into a content-addressed store and prints its address, e.g. sha256:db7eacb9…. The same build always gets the same address; unchanged chunks are stored once.

  3. Then reference it in Definition.fcscdm, in the same commit as the FCS it belongs to:

    "ClientReference": "sha256:db7eacb9eb29862d0765e20e6c4fbfa89686cb86109d3124ada4c6d92f8601d9"
    
  4. The platform tells a host which client a component needs — from the component's own pinned commit (deve/easy-timemachine branch only):

    GET /{space}/{lang}/Model/GetComponentClient?id=<componentId>&privateKey=<key>
    GET /api/static/{space}/prototype-client?footprint=<palette footprint>
    → {"Prototype":"…EasyPlank.fcc*<commit sha>","ClientReference":"sha256:…" | null}
    

The host (host/ in HiStructOrg/easy-plank) loads exactly that version and mounts it. Verified with four EasyPlank generations in one page: a component from before any client shows "no client"; components from older commits keep client v1 and stay fully editable; new ones get client v2 with the new end-shape control; switching back to a version already loaded downloads nothing.

Where to Look Next