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
- Booster — the developer's engine, the one that can evaluate FCS from your local folder. It
looks for local sources in
fcs:LocalComponentsFolder(defaultC:\GitHub\); the--joinsetup asks for that folder. If your repositories live elsewhere, set it explicitly. - FLI — the command-line FCS interpreter. Use it to prove your model evaluates before anything goes near a server.
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
-
Push your branch. The platform reads FCS from GitHub, never from your disk (except through Booster).
-
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.
-
Add the palette item. The prototype string is
repo*fcc*branch, for examplehttps://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-itemsand save it as a backup,- append your item and
POSTthe 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
- HTTP 200 does not mean the write was saved. Inspect every
validationresult.Exceptionin the (chunked) response; null or""means saved. A rejected write can still answer 200 and change nothing. - List keys have two spellings. The platform API uses JSON-quoted keys —
"\"Rounded\""— while an FLI updater takes the FCS value —{input.inEndShape="Rounded"}. Sending the plain string to the API fails silently. - Never create components in a loop or from a headless test against a live platform — every creation is a real component. Create one test configuration, keep its key, and reuse it.
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:
-
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 isCONTRACT.mdinHiStructOrg/easy-plank. -
Publish it first from committed, pushed code:
node tools/publish-client.mjscopies 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. -
Then reference it in
Definition.fcscdm, in the same commit as the FCS it belongs to:"ClientReference": "sha256:db7eacb9eb29862d0765e20e6c4fbfa89686cb86109d3124ada4c6d92f8601d9" -
The platform tells a host which client a component needs — from the component's own pinned commit (
deve/easy-timemachinebranch 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
- Reference component:
HiStructOrg/fcs-lab-01, branchdeve/easy-plank—EasyPlank.fcc/(the FCS and itsClientReference); its client:HiStructOrg/easy-plank(module, host, publish tool, contract). TheDEV-NOTES.mdfiles in both are honest logs of agents building it from scratch. - Start Here In 5 Minutes
- FCS essentials