https://github.com/mmiscool/breppluginexample
Example plugin for BREP CAD
https://github.com/mmiscool/breppluginexample
Last synced: 8 months ago
JSON representation
Example plugin for BREP CAD
- Host: GitHub
- URL: https://github.com/mmiscool/breppluginexample
- Owner: mmiscool
- License: mit
- Created: 2025-09-23T23:20:59.000Z (9 months ago)
- Default Branch: master
- Last Pushed: 2025-09-25T00:08:27.000Z (9 months ago)
- Last Synced: 2025-10-10T03:37:20.161Z (8 months ago)
- Language: JavaScript
- Size: 59.6 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Plugin Development Guide
This document explains how to build third‑party plugins for the BREP CAD application. It covers the plugin entrypoint, the `app` object you receive (including `app.BREP`), how to register new features, the structure of a feature class, the supported parameter schema types, and the small UI hooks you can use.
If you just want the TL;DR: your repo must contain a `plugin.js` ES module that exports a function. In that function, call `app.registerFeature(YourFeatureClass)` and optionally add UI via `app.addToolbarButton(...)` or `app.addSidePanel(...)`.
You can fork this repo as a starting point for your plugin. After you have forked it simply edit `plugin.js`.
See also: the BREP application README: https://github.com/mmiscool/BREP/blob/master/README.md
## Overview
- Plugins are ES modules fetched directly from GitHub.
- The loader expects an entry file named `plugin.js` in the repo (or subdirectory when the URL includes it).
- The entry is executed with a single argument: `app`, an object that provides:
- `BREP`: access to BREP modeling classes/utilities (solids/primitives/CSG helpers/THREE instance).
- `viewer`: the live viewer instance (scene, part history, toolbar, etc.).
- `registerFeature(FeatureClass)`: register a new modeling feature.
- `addToolbarButton(label, title, onClick)`: add a toolbar button.
- `addSidePanel(title, content)`: add a side panel section to the sidebar.
Where these come from in the app: see [src/plugins/pluginManager.js](https://github.com/mmiscool/BREP/blob/master/src/plugins/pluginManager.js) (builds the `app` object) and [src/UI/viewer.js](https://github.com/mmiscool/BREP/blob/master/src/UI/viewer.js) (viewer API).
## How Plugins Are Loaded
- Add your GitHub repo URL in the in‑app Plugins panel. Supported forms:
- `https://github.com/USER/REPO`
- `https://github.com/USER/REPO/tree/BRANCH`
- `https://github.com/USER/REPO/tree/BRANCH/sub/dir`
- The loader looks for `plugin.js` at that path, tries GitHub Raw on `ref` (or `main`/`master`), and falls back to jsDelivr CDN.
- Relative imports inside `plugin.js` are rewritten to absolute URLs against your repo base, so you can structure your plugin across multiple files.
- Bare specifiers (e.g., `import x from 'some-npm-package'`) are NOT resolved. Use relative imports within your repo, or explicit URL imports if you need an external dependency.
## Entrypoint Contract
Place a `plugin.js` at the repo path. Export either a default function or an `install` function. Both receive `app`.
Example minimal entrypoint (from this repo):
```js
// plugin.js
import { PrimitiveSphereFeaturePlugin } from './exampleFeature.js';
export default function install(app) {
// Make the app (and BREP) available to the feature class
PrimitiveSphereFeaturePlugin.setup(app);
// Optional: small UI hooks
app.addToolbarButton('🧩', 'Hello', () => {
console.log('Hello from plugin');
console.log('This is the context passed in to the plugin.', app);
alert('Yay');
});
app.addSidePanel('Plugin Panel', () => {
const div = document.createElement('div');
div.textContent = 'This was added by a plugin.';
return div;
});
// Register your feature so users can add it
app.registerFeature(PrimitiveSphereFeaturePlugin);
}
```
## The `app` Object
`app` is constructed by the application when your plugin loads:
- `app.BREP` — The modeling toolkit and a shared THREE instance. This includes:
- `THREE`, `Solid`, `Face`, `Edge`, `Vertex`
- primitives: `Cube`, `Sphere`, `Cylinder`, `Cone`, `Torus`, `Pyramid`
- operations: `Sweep`, `ExtrudeSolid`, `ChamferSolid`, `FilletSolid`
- utilities: `applyBooleanOperation(partHistory, baseSolid, booleanParam, featureID)`, `MeshToBrep`, `MeshRepairer`
- `app.viewer` — The live viewer. Useful bits:
- `viewer.scene` — A Three.js scene containing the model objects.
- `viewer.partHistory` — The history and feature pipeline (see below).
- `viewer.addToolbarButton(label, title, onClick)` — Add a custom button.
- `viewer.addPluginSidePanel(title, content)` — Add a side panel; `content` can be an element, a function that returns an element, or a string.
- `app.registerFeature(FeatureClass)` — Register a feature so users can add it from the UI/history. The app marks it as plugin‑provided and prefixes names with a plug icon.
## Registering a Feature
Call `app.registerFeature(YourFeatureClass)`. The app will:
- Flag the class as plugin‑provided (`fromPlugin = true`).
- Prefix `featureShortName` and `featureName` with a plug icon in UI.
- Make it available to the Feature Registry so it can be created via the History UI or programmatically with `viewer.partHistory.newFeature('Your Name or ShortName')`.
## Feature Class Anatomy
Every feature is a small class with three static fields and two instance fields, plus a `run()` method.
- Static fields:
- `featureShortName` — Short code shown in menus (e.g., `E`, `P.CU`).
- `featureName` — Human friendly name.
- `inputParamsSchema` — Describes inputs; the UI is auto‑generated from this.
- Instance fields:
- `this.inputParams` — Filled with sanitized values before `run()` is called.
- `this.persistentData` — Your scratchpad; survives across runs of this feature.
- Method:
- `async run(partHistory)` — Build/update geometry. Return either:
- An array of objects to add to the scene, OR
- `{ added: [...], removed: [...] }` for fine‑grained control.
Typical flow inside `run()` for geometry‑creating features:
1) Construct a BREP solid/face using `app.BREP` classes.
2) Apply any transform (`bakeTRS`) and call `visualize()` to create display geometry and helper edges.
3) Apply optional boolean with `BREP.applyBooleanOperation(partHistory, base, this.inputParams.boolean, this.inputParams.featureID)`.
4) Return the result from step 3 (additions/removals). The app removes flagged items and adds new ones.
Important conventions:
- Name your output objects with `this.inputParams.featureID` (e.g., pass `name: featureID` to constructors). This enables referencing them in later features.
- You can persist heavy intermediate data in `this.persistentData` to speed up subsequent runs.
## Parameter Schema Cookbook
Define `static inputParamsSchema = { ... }`. The app uses this to:
- Provide default values when the feature is created.
- Render an editable UI.
- Sanitize/resolve values before calling `run()`.
Supported field types and options:
- `string`
- `default_value: string`
- `hint?: string`
- `number`
- `default_value: number | string` — Users can type math expressions. Numbers are evaluated against the global Expressions manager, so `x * 2` is allowed if `x` is defined.
- `min?`, `max?`, `step?`: number or string
- `hint?: string`
- `boolean`
- `default_value: boolean`
- `options`
- `options: string[]` — Fixed list of values
- `default_value: string`
- `button`
- `label?: string`
- `actionFunction?: (ctx) => void | Promise` — Called on click. `ctx` includes `{ featureID, key, viewer, partHistory, feature, params, schemaDef }`.
- `file`
- `accept?: string` — e.g., `.png,image/png`
- Value is a Data URL string of the selected file after UI selection.
- `transform`
- `default_value: { position: [x,y,z], rotationEuler: [rx,ry,rz], scale?: [sx,sy,sz] }`
- UI provides interactive 3D gizmos; commit updates into `inputParams`.
- `reference_selection`
- Lets users pick scene objects.
- `selectionFilter: ["SOLID" | "FACE" | "EDGE" | "SKETCH" | "PLANE", ...]`
- `multiple: boolean` — single value (string) or array of strings.
- `default_value: string | string[] | null`
- Before `run()`, the app resolves the names into actual objects where possible; `this.inputParams[key]` will be an array of objects for `multiple: true` or a single‑element array for single select.
- `boolean_operation`
- Object with shape `{ operation: 'NONE'|'UNION'|'SUBTRACT'|'INTERSECT', targets: (string|object)[] }`.
- Optional: `biasDistance`, `offsetCoplanarCap`, `offsetDistance` for advanced subtract behavior.
- Pass directly to `BREP.applyBooleanOperation(...)` for robust, normalized handling.
## Using `app.BREP`
`app.BREP` is already imported from the application, so you must NOT import your own copy of Three.js or the BREP library. Using the shared instance avoids duplicate constructors and broken `instanceof` checks.
Common patterns:
```js
const { BREP } = app;
// Primitives
const sphere = new BREP.Sphere({ r: 5, resolution: 32, name: 'MySphere' });
sphere.visualize();
// Sweeps/Extrudes
const sweep = new BREP.Sweep({ face: someFace, distance: 10, name: 'MyExtrude' });
sweep.visualize();
// CSG helper
const result = await BREP.applyBooleanOperation(partHistory, sphere, { operation: 'UNION', targets: [otherSolid] }, 'Feat123');
// → { added: [Solid], removed: [Solid, ...] }
```
### Accessing `app.BREP` exactly
This repo’s example passes the `app` object into the feature class via a static `setup(app)` method, and the feature reads the shared BREP instance from there:
```js
// exampleFeature.js
export class PrimitiveSphereFeaturePlugin {
static app = null;
static setup(app) { this.app = app; }
async run(partHistory) {
const BREP = this.constructor.app.BREP; // shared instance from the host app
const sphere = new BREP.Sphere({ r: 5, resolution: 32, name: 'FeatureID' });
sphere.visualize();
return await BREP.applyBooleanOperation(partHistory, sphere, this.inputParams.boolean, this.inputParams.featureID);
}
}
```
Alternative patterns also work, such as capturing once at module scope (`let BREP; export default (app) => { BREP = app.BREP; }`) or destructuring on demand (`const { BREP } = app`). Always use the provided `app.BREP` rather than importing your own copy.
## Complete Example: Toolbar + Side Panel + Feature
This section shows the actual files in this repo and how the feature gets access to `app.BREP`.
```js
// plugin.js
import { PrimitiveSphereFeaturePlugin } from './exampleFeature.js';
export default (app) => {
// Provide the app (which contains BREP) to the feature class
PrimitiveSphereFeaturePlugin.setup(app);
// Optional UI examples
app.addToolbarButton('🧩', 'Hello', () => {
console.log('Hello from plugin');
console.log('This is the context passed in to the plugin.', app);
alert('Yay');
});
app.addSidePanel('Plugin Panel', () => {
const div = document.createElement('div');
div.textContent = 'This was added by a plugin.';
return div;
});
// Register the feature
app.registerFeature(PrimitiveSphereFeaturePlugin);
};
```
```js
// exampleFeature.js
const inputParamsSchema = {
featureID: { type: 'string', default_value: null, hint: 'Unique identifier for the feature' },
radius: { type: 'number', default_value: 5, hint: 'Radius of the sphere' },
resolution: { type: 'number', default_value: 32, hint: 'Base segment count (longitude). Latitude segments are derived from this.' },
transform: { type: 'transform', default_value: { position: [0, 0, 0], rotationEuler: [0, 0, 0], scale: [1, 1, 1] }, hint: 'Position, rotation, and scale' },
boolean: { type: 'boolean_operation', default_value: { targets: [], operation: 'NONE' }, hint: 'Optional boolean operation with selected solids' },
};
export class PrimitiveSphereFeaturePlugin {
static featureShortName = 'S.p';
static featureName = 'Primitive Sphere';
static inputParamsSchema = inputParamsSchema;
static app = null;
static setup(app) { this.app = app; }
constructor() {
this.inputParams = {};
this.persistentData = {};
}
async run(partHistory) {
const { radius, resolution, featureID } = this.inputParams;
const BREP = this.constructor.app.BREP; // Access BREP from the app provided during setup
const sphere = await new BREP.Sphere({ r: radius, resolution, name: featureID });
try {
if (this.inputParams.transform) {
sphere.bakeTRS(this.inputParams.transform);
}
} catch (_) {}
sphere.visualize();
return await BREP.applyBooleanOperation(partHistory || {}, sphere, this.inputParams.boolean, featureID);
}
}
```
Note: the loader will prefix plugin‑provided names in the UI with a plug icon. To avoid confusion with built‑in features, pick a unique `featureShortName`.
## Programmatic Feature Creation
From toolbar buttons or side panels you can add features directly:
```js
const ph = app.viewer.partHistory;
// Either the long name or short name works; match your class statics.
const feature = await ph.newFeature('Primitive Sphere'); // or 'S.p'
feature.inputParams.radius = 20;
await ph.runHistory();
```
Internally, this uses the Feature Registry ([src/FeatureRegistry.js](https://github.com/mmiscool/BREP/blob/master/src/FeatureRegistry.js)) to find your class and apply default values from `inputParamsSchema`.
## Side Panels and Toolbar
- `app.addToolbarButton(label, title, onClick)` — Adds a small labeled button to the main toolbar.
- `app.addSidePanel(title, content)` — Inserts a collapsible section in the sidebar. `content` may be:
- An `HTMLElement`
- A function returning an `HTMLElement` (async allowed)
- A string (rendered in a `
`)
Plugin side panels appear before the Display Settings panel.
## Recommended Repo Layout
```
your-plugin-repo/
plugin.js # entrypoint (required)
exampleFeature.js # a feature class
```
`plugin.js` can import your own files via relative paths:
```js
import { PrimitiveSphereFeaturePlugin } from './exampleFeature.js';
export default (app) => {
PrimitiveSphereFeaturePlugin.setup(app);
app.registerFeature(PrimitiveSphereFeaturePlugin);
};
```
## Best Practices
- Always call `.visualize()` on solids you generate before returning them.
- Name your outputs with the feature’s `featureID` for easy referencing.
- Use `this.persistentData` for caches/intermediate results to speed reruns.
- Prefer `app.BREP.THREE` over importing your own Three.js.
- Keep imports relative to your repo, or use absolute URL imports when needed.
- For boolean operations, prefer the provided helper: `BREP.applyBooleanOperation(...)`.
## Troubleshooting
- “Cannot find plugin.js” — Ensure the file exists at the path your URL points to. If you use a subdirectory, include it in the GitHub URL (see above).
- “Import failed” — Avoid bare specifiers. Use relative paths or absolute URLs.
- “TypeError or geometry not visible” — Did you call `.visualize()` on your BREP solids/faces? Are you naming outputs correctly?
- “Selections don’t resolve” — For `reference_selection` fields, the app resolves names to objects at run time. Ensure the referenced objects exist and are named.
## References (source)
- App plugin surface and loader: [src/plugins/pluginManager.js](https://github.com/mmiscool/BREP/blob/master/src/plugins/pluginManager.js)
- Feature registry and history execution: [src/FeatureRegistry.js](https://github.com/mmiscool/BREP/blob/master/src/FeatureRegistry.js), [src/PartHistory.js](https://github.com/mmiscool/BREP/blob/master/src/PartHistory.js)
- Example features (good templates): [src/features](https://github.com/mmiscool/BREP/tree/master/src/features)
- Boolean helper: [src/BREP/applyBooleanOperation.js](https://github.com/mmiscool/BREP/blob/master/src/BREP/applyBooleanOperation.js)