MyBenchLab Documentation Open editor

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:

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

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

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.

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:

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:

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.