# Custom components

New parts are added to MyBenchLab by registering a **kind** with the engine. A kind is a self-describing plugin: it declares its parameters, ports, propagation rule, and 3D geometry. The engine itself does not need to change to accept new kinds — this is what keeps the simulator extensible.

There are two registries: `registerComponentKind` (for parts like shafts, gears, motors) and `registerConnectionKind` (for edges like gear mesh, shaft coupling, belt).

## Plugin API version

Every registration must declare the API version it targets:

```js
registerComponentKind({
  apiVersion: 1,
  id: 'my-gear',
  ...
});
```

The engine refuses any `apiVersion` outside the range `[MIN_SUPPORTED_API_VERSION, ENGINE_API_VERSION]`. When the contract evolves, older versions are kept working via compatibility shims — you bump `apiVersion` only when you opt in to new features. Benches record the engine version they were last saved under, so we can migrate them forward without breaking.

## Component kind contract

```js
registerComponentKind({
  apiVersion: 1,
  id: 'my-gear',              // globally unique id
  category: 'mech',           // domain namespace (mech, elec, hydraulic, ...)
  dslName: 'myGear',          // identifier exposed in the DSL (default: id)
  parametersSchema: {         // SimpleSchema-like shape
    teeth: { type: Number, defaultValue: 20, min: 4 },
    module: { type: Number, defaultValue: 2 },
  },
  ports: [                    // connection points on the component
    { id: 'axis', type: 'rotational' },
  ],
  buildGraphNode: ({ params }) => ({
    teeth: params.teeth,
    module: params.module,
  }),
  dimensions: ({ params }) => ({           // optional, for the layout solver
    pitchRadius: (params.module * params.teeth) / 2,
  }),
  build3D: ({ params, transform, theta, selected }) => <MyGearMesh ... />,
  ParamsPanel: null,          // optional React component for the inspector
  doc: {                      // optional but strongly recommended
    title: 'My gear',
    summary: 'A custom spur-gear-like part.',
    paramsDoc: { teeth: '...', module: '...' },
    portsDoc: { axis: '...' },
    kinematics: '...',
    example: "mech.myGear({ id: 'G1', teeth: 20, module: 2 })",
  },
});
```

Returning `build3D` node types: anything valid inside a `<SceneUnits>` subtree. The viewer scales mm → scene meters once at the root, so your geometry should be authored in mm directly.

## Connection kind contract

```js
registerConnectionKind({
  apiVersion: 1,
  id: 'my-belt',
  category: 'mech',
  dslName: 'belt',
  acceptedPorts: [
    { from: { kind: 'pulley', port: 'axis' }, to: { kind: 'pulley', port: 'axis' } },
  ],
  validate: ({ fromParams, toParams, connectionParams }) => {
    // return { ok: true } or { ok: false, error: '...' }
  },
  propagate: ({ fromState, fromParams, toParams }) => ({
    omega: fromState.omega * (fromParams.radius / toParams.radius),
  }),
  paramsSchema: { ratio: { type: Number, defaultValue: 1 } },
  solveLayout: ({ anchor, driven, connectionParams }) => ({
    position: [...], rotation: [...],   // or null to skip layout
  }),
  doc: { title: 'Belt drive', ... },
});
```

## Port types

Ports reference a **port type** registered via `registerPortType`. A connection's `acceptedPorts` list defines which port combinations it accepts — the validator rejects mismatches at compile time. The built-in port type is `rotational`; new domains will add `linear`, `electrical`, etc.

## Where plugin code lives

Built-in component kinds are bundled with the app at `app/app/components/`. Every new one adds an import to `app/app/components/index.js::bootstrapComponents` so it registers on startup.

### Inline `defineComponent` in a bench

You don't need to ship code with the app to add a new kind. Any bench source can register a kind inline via `defineComponent(...)`. The kind lives only for that compile — it is not stored in the shared registry, it does not leak to other benches, and a second compile of the same source re-runs the definition.

```js
export default defineBench(({ defineComponent, mech }) => {
  // Register a custom component kind. Same contract as registerComponentKind,
  // just scoped to this single bench. The returned `{ kindId }` is optional
  // — once registered, the kind is available via `mech.wobblyDisc(...)`.
  defineComponent({
    apiVersion: 1,
    id: 'wobbly-disc',
    category: 'mech',
    dslName: 'wobblyDisc',
    parametersSchema: {
      radius: { type: Number, defaultValue: 30, min: 1 },
    },
    ports: [{ id: 'axis', type: 'rotational' }],
    dimensions: ({ params }) => ({ outerRadius: params.radius }),
    buildGraphNode: ({ params }) => ({ ...params }),
    build3D: ({ params, transform, theta }) => (
      <group
        position={transform.position}
        rotation={[
          transform.rotation[0],
          transform.rotation[1],
          transform.rotation[2] + theta,
        ]}
      >
        <mesh>
          <cylinderGeometry args={[params.radius, params.radius, 4, 32]} />
          <meshStandardMaterial color="#8ab4f8" />
        </mesh>
      </group>
    ),
  });

  // Use it just like a built-in kind: `mech.wobblyDisc(...)`
  const disc = mech.wobblyDisc({ id: 'D1', radius: 40 });
  mech.drive(disc, { rpm: 60 });
});
```

Rules that the compiler enforces:

- `apiVersion` must be within the supported range (same check as `registerComponentKind`).
- `id` must be unique within the bench. Re-declaring the same id in the same source errors loudly — silent overrides would make the source order-dependent.
- `category`, `parametersSchema`, and `ports[]` are required; `build3D`, `buildGraphNode`, and `dimensions` default to no-ops if omitted.
- Anything the DSL would resolve against the global registry (component lookup, port type lookup, layout solver) falls back to the local kind first — so your custom kind participates in validation, layout, and rendering exactly like a built-in one.

### External sources (roadmap)

The roadmap includes loading user-authored kinds from external sources: git repositories and a curated hub in addition to the inline `defineComponent` above. The plugin API on this page is the ABI the loader enforces — writing to this contract today means your kind will be loadable via the future mechanisms too, versioning included.

## Safety considerations

When inline or git-sourced plugins ship, `propagate`, `build3D`, and `validate` will execute in a restricted context:

- No access to `window`, `document`, `fetch`, `eval`, or dynamic `import()`.
- Only pure JavaScript + a whitelist of Three.js APIs for geometry.
- Parameter values are copied (not referenced) between the DSL and the hooks so authors cannot mutate engine state.

Built-in kinds are trusted — they are part of the app bundle — but the contract above is already compatible with the restricted execution model, so porting a built-in kind to a user-authored one will not require refactoring.
