---
status: landed
summary: The FNS operator family, built 2026-09-17: FNS_OpFamily (TDFam) in the toolkit, seven members declaring TDFam's FamManifest, a derived `family` manifest block, the updater mirroring members from the store into the family folder, commands without dialogs, a guard for the toolkit's own copies, and a CMS section that edits the manifest in the tool. The research below it is kept as the record.
since: 2026-09-10
---
2026-09-18: the palette folder is now
FNSToolsand the family folder is flat:<user palette>/FNSTools/FNS/<Name>.toxwith the sidecar beside it, nofamily/tree, no category subfolders, the category in the sidecar'sop_group. The layout below is the record of what was built on 2026-09-17; the contract in force is docs/PaletteFolderContract.md.
Door B only; Door A (rows in TD's own COMP tab) stays researched and unbuilt.
- The family is a package.
FNS_OpFamily, free, Surfaces: the TDFam family namedFNSmoved from/TDFam_createinto the toolkit, colour black, Install On Start on, its registry promoting to/sysas TDFam designs.Opfolderis an expression on<palette>/FNStools_ext/family/FNS. TDFam's updater file bindings, which pointed at a folder this repo does not have, are severed.LICENSEin it is TDFam's NOTICE;LICENSE_Apacheholds the full Apache-2.0 text. - Members declare themselves. RandomCHOP (new package, Base tier),
SimpleSceneChanger, ProSceneChanger, SwitchTools, MixSequencer,
OpSequencer and CamSequencer carry TDFam's
FamManifest.op_typeis a lowercase word andop_namethe readable spelling (next section). - The manifest derives a
familyblock (build_manifest.FamilyFor):op_type,op_name,op_label,op_group,is_filter,compatible_types,search_words,summary, andpar_retain,state_retain,shortcutswhen non-empty. A foreign or hostless entry may curate it (CuratedFamily); a member impliesplacement: pane.CatalogProblemsrefuses a curated block beside a live FamManifest, a member at the root, a bad type or group; preflight blocks onFamilyProblems(an unparseable DAT, a type with capitals, a bad group). - The updater keeps the family folder (
FNS_Updater.SyncFamilyFolder): every member the store holds lands atfamily/FNS/<op_group>/<name>_<version>.toxwith a TDFam sidecar built from the row; stale versions and non-members are removed; TDFam re-reads the folder throughop.FAMREGISTRY. It runs from_afterStoreComplete, after a scoped refresh (a picker install) and after an update pass. The store's own rules decide presence, so a gated member is on the tab only for an account that holds it. TDFam's default naming regex has nov. - Commands without dialogs on
FNS_OpFamily: Stub Family Operators, Replace Family Stubs, Update Family Operators, Sync Family From Store. They call TDFam's batch methods, which its own Stubs pulses wrap in modal dialogs. - The CMS edits the facts where they live. A live package's family
section reads and writes its FamManifest through FNS_CMS
(
/api/familyread,/api/familywrite, PI-saved, validated by build_manifest's rules) and says why when TD is not connected; a foreign package's block rides the catalog save.
Owner decision: the family, its TDFam registry and the FNS tab do not
appear unless a family operator is installed. FNS_OpFamily is a
companion: family package in the catalog:
- The picker never offers it. One
pickable()test excludes companions from every list a pick can come from (the tool list, installed, last setup, wanted, the starter set, bundles, stored picks), and it carries nofits, so the questionnaire cannot choose it. - The installer decides.
ResolvePlandrops companions from whatever a selection says and adds them exactly when the selection holds a package with afamilyblock, minimal requests included. An installed family with no member left is then an ordinary removal candidate. - No folder without it.
SyncFamilyFolderskips, creating nothing, in a project where the FNS family is not registered; the family asks for a sync when it initialises, so installing it fills the folder. CatalogProblemsrefuses an unknown companion kind and a companion that is itself a member.
Owner decision: some users want the family operators in their palette without a new tab in the OP Create dialog. A member no longer forces the family.
- The picker asks. While a family operator is ticked, the selection bar
shows "Add FNS tab to OP Create", on by default. The selection always
carries
familywhen the release has a companion. Served, the switch starts from the project: members installed without the family read as off. Turning it off (or unticking the last member) counts as a change, so Apply removes the family. - The installer honours it.
ResolvePlanadds the companion only when a member is selected ANDFamilyWantedagrees. A boolfamilydecides. A selection that does not say (an agent's minimal request, an older selection.json) keeps the project as it is, so adding a member never brings back a tab someone removed. A project with no member yet gets it. - It is remembered. The root's
last_installrecord carriesfamily(was FNS_OpFamily installed), so "Set up like last time" and Always Set Up Like Last Time repeat it. The site's paste script adds the family with a free family pick unless the switch is off. - Turning it off later is the same switch in Pick Tools, or the family's
Remove Operator Family command, which calls the installer's
RemoveFamily(). That refuses the source checkout, removes throughRemoveToolslike any picker removal, and runs a frame after the command returns because it destroys the component the command lives on. - A root toggle. The toolkit root's FNSTools page has FNS Tab in OP
Create (
Opfamily,build_installer.ROOT_PROJECT_TOGGLES). It mirrors the project:InstallerExt.SyncFamilyToggleruns after every install and removal and on the installer's init. Flipping it callsSetFamily(on)through the root forwarder, deferred a frame. Off isRemoveFamily. On installs the family beside the family operators already there (a minimal plan withfamily: true), downloads it first when the store lacks it and finishes when the store job ends (familyWhenFetched), and refuses with no family operator installed. A refusal puts the toggle back. The root's config host excludes it (Excludepars), so it never roams: restoring settings in another project must not add or remove anything there. - The tab leaves at once. TDFam never prunes a destroyed owner, so
RemoveToolsunregisters every family a component owns before it destroys it (_releaseFamilies). The/sysTDFam registry itself stays until TouchDesigner restarts; with no family registered it shows nothing.
-
Never read a promoted member of the owner in
onDestroyTD. The first cut released the family from TDFam in the family's ownonDestroyTDand readself.ownerComp.Propertiesthere. A reinit runs that hook too, and the promoted lookup waited on the extensions being rebuilt: TouchDesigner's main thread parked on that line with flat CPU (py-spy, 2026-09-17) and never came back. The release moved into the installer, before the destroy. -
A type with capitals never comes back from a stub. TDFam keys its folder cache by the lowercased type, and
replace_stuband the updater look the manifest'sop_typeup verbatim.switchToolsplaced, stubbed, and thenreplace_stubs_batchreturned nothing. Withswitchtoolsthe round trip worked: 4 connectors as a stub, 32 children back,Indexkept. -
TDFam finds placed operators across the whole project by manifest tag, so it found the member masters in the toolkit container. Its own Create Stubs (All) would have stubbed them and Update (All) replaced them from the family folder. The family's callbacks DAT now returns False from
onPreStub,onPreReplaceandonPreUpdatefor anything inside the container that holds the family (masters and doorstep copies belong to the FNS updater), and the commands filter the same way. -
A copy of the family initialises while the old one still holds the name, so it neither registers nor installs; unregister the old owner (
UnregisterFamily(owner), which takes the COMP, not the name), thenRegisterFamily(new)andInstall(True). -
One boot-path modal remains in TDFam: two TDFamRegistry copies whose major versions differ prompt "Registry Version Conflict" during promotion. Not changed here (TDFam is upstream); a project that mixes TDFam majors will see it.
- ParRetain and StateRetain are empty on every member: which parameters and state survive a stub or an update is the owner's to curate in the CMS.
- The FNS tab is empty until a release publishes the family blocks: the store's manifest (v3.2.25) predates them, and the sync mirrors only rows that declare one. RandomCHOP has never been released.
- A cold boot with the family in the toolkit has not been run.
- Whether the Hub should offer "stub all" before a project is handed over.
Owner's question, 2026-09-10: "similarly to how we register a tool to install as an OpTemplate from disk, we could dedicate some tools as part of a new operator family" -- TDFam, dotsimulate/TDFam, folder-based families with manifests.
Source: the README and docs/{concepts,manifest-reference,callbacks-and-api, licensing}.md of dotsimulate/TDFam, plus the live /TDFam_create (package
1.0.1) and /T3D_1_13_2 in this project.
- One
TDFam_createCOMP per family. It carries the family name, colour,Index(tab order in the OP Create dialog), the operator sources, a callbacks DAT and a nestedTDFamRegistrytemplate that promotes itself to/sys/TDFamRegistry(op.FAMREGISTRY); highest registry version wins. - Two operator sources, either or both. Embedded: COMPs inside the
Opcompfolder. File-based:.toxfiles inOpfolder, named by a configurable regex whose default is(.+)_v(\d+\.\d+\.\d+)\.tox$; a subfolder is a category. When both sources carry the same op, the higher version wins; ties go to embedded. - Manifests. Every op carries a
FamManifestbase with four DATs:OpInfo(identity and menu:op_type,op_name,op_label,op_version,fam_version,op_fam,op_group,summary,doc_url,op_color,isFilter,compatible_types,search_words,pop_menu),ParRetain(which pars survive a stub or an update),StateRetain(extension storage, COMP storage, DAT contents that survive),Shortcuts({"ctrl.shift.b": "Bypass", ...}). File-based ops carry the same as a sidecar<tox name>.json({"OpInfo": {...}, "ParRetain": {...}, ...}) or a folder-levelmanifest.jsonkeyed by normalised op name. - Surface.
Installon: the family registers, andGlobalUIInjectorgives it a tab in the OP Create dialog (inject_opfam_registrysits at the head of the node-table chain, before ourfamiliesanchor -- the two injections coexist today, T3D is installed here).PlaceOp(target, op_type, name, x, y)prepares a clone, validates the manifest, applies colour and shortcuts, places. - Lifecycle we do not have.
StubOpturns a placed op into a lightweight placeholder that keeps wiring, position and the retained pars and state, so a project can travel without the family;UpdateOpupgrades a placed op in place to the newest version withParRetain/StateRetainapplied. Hooks:onPlaceOp/onPostPlaceOp, pre/post stub, replace, update, install,onDeployManifest, and manifest-declared pop-menu callbacks. - Licence. Apache-2.0, copyright Lyell Hintz and Dan Molnar. Redistribution
inside a third-party toolkit, commercially, is allowed provided the
Apache-2.0 notices and the
NOTICEfile ship, with the suggested visible credit "Built with TDFam".
- A family named
FNSalready exists at/TDFam_createin this project: installed, UI installed,Index -1, zero operators (OpcompandOpfolderempty), tagged<FAM>. Its tab is live and empty in the OP Create dialog now. - The store (
Palette/FNStools_ext/store/<name>.tox, flat) plus the cached manifest withname,version,title,description,category,placement,alternatives_for,help_url. Presence on disk is the availability rule for alternatives (docs/OpAlternatives.md) and would be for family membership. - A derivation rail.
build_manifestreads facts off the live package at build (surfaces,alternatives_forvia a callbacks hook); a hostless or foreign package curates the same key in the catalog and the content CMS edits it. Family membership fits the identical pattern. - A placement class.
placement: panealready means "an operator-like component spawned into the working network, frozen at its spawn version". A family member is that class of tool, with a better answer to the frozen version (TDFam'sUpdateOp).
- Membership is declared in the tool, derived at build. A member tool
carries TDFam's own
FamManifest/OpInfo(TDFam'sDeployManifestswrites it; the fields are TDFam's, not ours).build_manifestreads it and writes afamilyblock into our manifest:{op_type, op_label, op_group, isFilter, compatible_types, summary}. A foreign or hostless package may curate the block in the catalog, edited in the CMS, under the same "derived beats curated, never both" rule asalternatives_for. One source of truth per package. - The updater maintains the family folder. For every member package
whose tox is in the store,
FNS_Updatermirrors it intoPalette/FNStools_ext/family/FNS/<op_group>/<name>_v<version>.toxwith a sidecar<name>_v<version>.jsonbuilt from the manifest'sfamilyblock (OpInfo) and anyParRetain/StateRetain/Shortcutsthe tool ships. Download, update and removal keep the mirror in step and drop stale versions. Only members are mirrored, so no other store tox ever appears as an operator (TDFam lists every tox it finds inOpfolder). The store stays flat and untouched. - The family ships as a package.
FNS_OpFamily: theTDFam_createCOMP namedFNS, network-root placement,Opfolderan expression on the user palette folder,Installonstarton, its nestedTDFamRegistrypromoting as TDFam designed.NOTICEand the Apache-2.0 text ride inside it; the About and the docs page carry the "Built with TDFam" credit. - Alternatives and family are two doors to one tox. A member is its own
type in the FNS tab; it may also declare
alternatives_forand be offered when a stock type is created. Neither mechanism needs the other; both read the same store.
- Embedded members (
Opcomp): copying member tools into the family COMP makes every member resident in every project that has the family. The point of the store is placement from disk. - Pointing
Opfolderat the store itself with a permissive naming regex: TDFam would list every store tox as an operator, and versions live in our manifest, not in file names. A derived mirror keeps both models whole.
- Which tools. Only operator-like components qualify: something that reads as one node with inputs and outputs (the whole-COMP alternative convention, In/Out ops of one family). Naming candidates here would be guessing.
- Family name and colour.
FNSexists; keep it, or a product name. - Whether membership implies
placement: pane. It probably should: a family op is placed into the working network by definition. - What
ParRetain/StateRetaineach member declares. Without them an update keeps nothing; with them TDFam's in-place update is the answer to "instances stay at their spawn version". - Stubs in shipped projects. Whether the FNS Hub should offer "stub all FNS family ops" before a project is handed to someone without the toolkit.
Before building the family, the owner asked what it would take to list tools
inside the COMP tab itself. Read off /ui/dialogs/menu_op in 2025.33070:
- Rows are a table.
nodetable/families(a Script DAT fed by TD's built-in op-type list, objects with.type .label .OPType .isFilter .isCustom .licenseType ...) emits one row per type for the current family:name, label, type, subtype, mininputs, maxinputs, ordering, level, lictype, os, score, family, opType.typeis the layout (layouts/COMP/defGenerator,defFilter,...Disable). Our registry's chain stages (onChainNodes,script_inject) already rewrite this table downstream offamilies; appending rows for familyCOMPis the same mechanism, with the dialog's search string applied by the stage since the native scoring ran upstream. - A click runs a Tscript, and it has a hook for exactly this.
node_script(panel execute onnodetable,cellselectid) switches tocreate_node, which reads the clicked row fromnode_detailand, when the row'sorderingiscustom, doesrun $controlpanel/customCOMPs/$operator $menu_paneand exits, never touchingopadd.customCOMPsstill exists in the dialog with one legacy script (camwlookat, TD's old Camera+Lookat custom COMP, Tscript ending inopplace -p $pane). A Python DAT there receives the pane asargs[0], so a registry-ownedcustomCOMPs/<name>script can load the tool (live COMP copy or store tox, the alternatives engine's two placements) and hand it toui.panes[pane].placeOPs, then close the dialog (parent.OPCREATE.par.winclose), which the custom branch does not do itself. - Hover help is a table too.
summaries[family + ' ' + label]feeds the help strip; rows can be added for ours. - TDFam does not use this hook. It substitutes the whole table for its
own families, patches
create_node(# OPFAM_START ... exit) to keepopaddout, and places from its own panel execute. Nothing in it appends to a native family; adding that would mean teaching its interceptor about COMP clicks, which is more invasive than the dormant native hook.
So a COMP-family listing is a registry contribution, not a family: an
onCompRows() hook (or the store's manifest block) naming {label, source, isFilter, help}, a stage that appends the rows under COMP with
ordering=custom, one registry-managed script per row in customCOMPs
(tagged and pruned like chain stages), and a summaries row. Placement
reuses _placeComp / _placeTox; the watcher re-baselines after. About a
day, no TDFam involved, no manifests beyond the catalog. What it does not
give: stubs and in-place updates, which are TDFam's. The two are
complementary, and one tool can be both a COMP-tab row and a family member.
The updater mirror is the only real code: a folder writer keyed off the
manifest's family block, run at download, update and remove (about a day
with tests). The build derivation and the CMS field are the alternatives_for
pattern again, an hour each. The package is a TDFam_create with three
parameters set. Nothing in TDFam needs changing.
Changing TDFam itself; migrating T3D; making every FNS tool a family op.
docs/StoreCompleteness.md: with the updater's Keepstore toggle (default
on) every install and update pass ends by mirroring the rest of the release
into the machine's store, and a full mirror's completion calls one hook,
ExtUpdater._afterStoreComplete(status). Two things follow for this plan:
criterion 4's "every member whose tox is in the store" becomes every
member on any machine with the default on, so the FNS tab is complete
offline; and the family folder sync has one place to run from (that hook)
instead of three (after download, update and remove), since a remove pass
does not change what the store holds. Nothing here is built yet; the hook
only logs.