ProductBuilder Engine
The ProductBuilder Engine is the core runtime for online 3D product configurators. It loads product packages, builds configurable 3D scenes, and exposes the cut / split / undo / redo and material-assignment workflows that the ~50 owned configurators are built on top of.
Engine consumers are served versioned, parallel bundles, so a configurator can adopt a new engine version incrementally. The frozen r127 line is kept as a deprecated baseline; this repository is the canonical home of the consolidated, modern engine.
Current state
- Engine version:
3.0.0(@productbuilder/engineinpackage.json) - Three.js: r185 (
three@^0.185.1) - Bundle:
build/pb-engine.3.0.0.js(and a minified.min.jsalongside the older, deprecated r127 bundles) - Language/module system: ES modules (
"type": "module")
Directory structure
engine/
build/ Bundled engine outputs (versioned)
coverage/ Generated coverage report (npm run coverage; not committed)
docs/ Generated API reference (npm run doc; not committed)
examples/ Runnable examples (npm run examples)
issues/ Tracked issue notes (e.g. memory-leaks)
src/ Engine source
test/ Browser/Mocha + Chai test suite (npm run test:headless)
utils/ Helper apps/viewers, build tooling
website/ Project website
.forgejo/ CI/workflows (test + deploy)
Getting started
npm install
Build
npm run build
Rollup bundles src/main.js into build/pb-engine.<version>.js (plus a minified
.min.js). Three.js is externalised so there is one shared instance across the
host page and the engine.
Tests
The test suite is browser-based (Mocha + Chai) and driven headlessly through Puppeteer, which serves the repo with correct ES-module MIME types and launches Chrome:
npm run test:headless
To also produce an Istanbul coverage report (HTML in coverage/ plus lcov):
npm run coverage
To run the suite interactively in your own browser:
npm test # live-server --cors --open=/test/
There is also an end-to-end smoke test against the real axel package on the
r185 bundle:
npm run smoke
Current results (see the section below for the remaining coverage work):
passes: 525
failures: 0
pending: 63
Line coverage of src/: ~82% (statements/lines), ~70% functions, ~81%
branches across 127 files.
Examples
npm run examples
This serves the repo and opens examples/index.html. See examples/README.md
for details, including how to load the shipped bundle versus the source.
Documentation
npm run doc
jsdoc generates the API reference into docs/.
Source sections (src/)
actor—Actor,Configurator,SingleBlockInstancecomponent—Component,ComponentTreeconfigurator—Configuration, block instances, connections, categoriesdefault_settings— camera/renderer defaultsexport— image / GLB / USDZ / PDF exporterspackage— blocks, materials, meshes, themes, connectors, loadersreporter— logging and information sourcesscene— composer, environment, contact shadow, space,scene_managerserver—ServerConnection(WebSocket RPC)transform—Transform,DefaultSelectionTransformutilities— geometry/dimension/placement helpersview—View,DefaultView,MoooiView, markers, tweens
Utils (utils/)
Helper apps and viewers built on top of the engine (standard-viewer, simple-viewer, generator, glb2usdz, android-viewer, and several experimental variants). These are standalone experiments and are not part of the engine bundle.
Reaching 100% coverage
This section is the on-ramp for driving line coverage from the current ~82% to the remaining target. The test infrastructure for this is already in place; the work below is about exercising the correct code paths, not building new tooling.
Useful existing test infrastructure
test/helpers/make_test_block.js—makeTestBlock,makeTestBlockInstance,makeConnectorTypes: builds a fully meshed block (PositionedMesh→WrappedMesh→GLTF→MaterialSet→WrappedMaterial). This is the fixture needed to driveConfiguration._build('main').test/package/make_component_test_set.js—makeComponentTestSet,makeMesh.test/package/mock_component.js—MockComponent.test/configurator/create_mock_constructs.js—createConfiguration,createTestSetup(used throughout the configuration suite).test/resources/testdata.js— real GLB/image data (box003p4MeshFileData,boxpMeshFileData, texture data, etc.).test/integration/helpers.js— shared integration harness (waitInitialBody,countBlockBodies,danglingConnections,findOption,verifyConfigurationInvariants,makeMultiBlockChain).test/resources/packages/axel— a real production package with meshes, used bytest/integration/test.integration_axel.jsand testing the WebGL view.test/integration/— three integration specs:test.integration_axel.js— real production package end-to-end.test.integration_multiblock.js— synthetic multi-block chain driven through a parameterized mutation matrix (extend/insert/cut/split + undo/redo).test.integration_render.js— WebGL rendering of the axel configurator viaproject.addView().
Remaining gaps, by file
The four remaining large files and where their uncovered lines live:
src/configurator/configuration.js (~90% covered)
The uncovered lines are all in _build(part, quality, dependencies):
- The
_build('UI')thumbnail branch (covered whensettings.thumbnailexists). - The "assign from default materials" loop (
defaultMaterials) — needs a config instantiated with a non-emptydefaultMaterials. - The
WrappedMesh._normalMap→buildMeshMaterialbranch. - The texture-map clone + tiling loop (needs a default material that carries a
Texturemap). - The
aoMap instanceof TextureneedsUpdateline.
To cover: build a meshed configuration using makeTestBlockInstance/makeMesh
whose default material has a map texture (and optionally a normal map), set
defaultMaterials, and call configuration.build() (or _build('main', 'medium')).
The axel integration test already covers the main loop; these fixtures target the
remaining branches.
src/project.js (~62% covered)
Uncovered methods are the networked/actor paths:
_addPackage/addPackagefull path (already partly covered by the axel test).save,load,share,buildProjectFromExport.addActor,selectPreset(happy path),setDefaultMaterial(happy path).messageApiRequestbranchesset-locale,save,load-project,list-available-packages,screenshot,configuration-summary,export-project.
To cover: stub fetch (and/or a ServerConnection) to return a package
index.json, then call addPackage, save, load, and share. The axel package
under test/resources/packages/axel is a ready-made payload.
src/view/view.js (~75% covered) and src/view/default_view.js (~42% covered)
The uncovered lines are the interactive handlers and camera-fit methods:
zoomExtents,zoomScene,zoomTop,showDimensions/hideDimensions/showBoundingBoxes/toggleDimensions(these currently reference an undefined globalproject— see known issues).- The drag/touch lifecycle in
default_view.js(onTouch*,onPointer*,onDragEnter/Over/Drop/Leave/End,expectComponentDrag,checkDrag).
To cover: either build a Configurator + View fixture and dispatch real
pointer/drag events, or first fix the global project reference so the
camera-fit/dimension methods can be exercised with a stub scene at the unit level.
Known issues surfaced while writing tests (left unchanged)
src/view/view.jszoomExtents/zoomScene/zoomTop/showDimensionsreference an undefined globalproject(lowercase). They crash when called outside a global scope that happens to defineproject.src/project.jsmessageApiRequest('set-locale')references an undefined globalmakeResponse(and an undeclaredresponse) when there is no server, so the graceful "not connected" fallback is currently unreachable.src/utilities/getDimensionsBoundingBox.jsreturnscenter/basevia a sharedVector3thatgetSize()later mutates, socenter(andbase.x/base.z) reflect the box size rather than the geometric center.src/utilities/nodeMaterialBuilder.js(andwrapped_material.js) assignsmaterial.needsUpdate = true, which is a no-op on three.js r185 (MeshStandardMaterial.needsUpdatestaysundefined).src/state_tracker.jschecks the overflow beforepush, so the state register can transiently holdmaxStateCount + 1entries.src/server/server_connection.jsusessettings.reconnectTime || 30, soreconnectTime: 0cannot disable auto-reconnect (it is coerced to 30).
Branch disposition
Remote-tracking branches mirror across two remotes that carry the same work:
github (productbuilder/pb-frt-lib-engine.git) and origin
(git.targetscanner.com/pb/engine.git). A third remote, westminster
(git.westminsteralpha.com/pb/engine.git), carries a single main.
refs/heads/main (commit 601cd82) is the merge-base of main and
refs/heads/v2; all main history is therefore already contained in v2.
Branches already merged into v2 have had their local remote-tracking refs
pruned (66 refs — 33 on each of github/origin). Their work is accounted
for in the canonical tree and they can be re-fetched or deleted remotely at any
time without losing content.
The table below records the disposition of the branches that are not merged
into v2 (20 unique branches × 2 mirrors = 40 refs). These are the candidates
for Step 2 porting. "ahead" is the commit count on top of v2's tip for that
branch; "touches" lists the source files it changes (build/test/docs/utility
noise omitted).
| Branch | ahead | Touches (src/) |
Disposition |
|---|---|---|---|
getGltfData |
56 | actor, component/*, configurator/*, view/view.js |
PORT — GltfData/image exporter + memory work (Step 2) |
feature-main-with-updates-from-getGTLFData |
14 | actor, configurator/*, view/*, contact_shadow, project |
PORT — layers/zoom/marker + contact-shadow updates |
feature-main-with-updates-from-getGTLFData-kevin-mem-fix |
22 | gltf, project, contact_shadow, view/* (mem-fix fork) |
REVIEW — overlaps the above + memory fix |
main-with-updates-and-memory-check |
22 | gltf, project, view/* (+ de-closured material) |
REVIEW — memory de-closure line |
inactive-state-exported |
6 | project, view/* (inactive-state handling) |
REVIEW — inactive config export |
getGltfData-memory-research |
1 | component, configurator/* (memory research) |
SUPERSEDED — research snapshot |
usdz_lighting_test |
2 | export/USDZExporterPB.js |
PORT/REVIEW — USDZ lighting tweak (Step 2 exporter) |
mapUuid |
1 | main.js (mapUuid export) |
REVIEW — one-line API addition |
load-fix |
1 | project.js (quaternion xyzw prop name) |
PORT — likely a real bug fix |
feat/multi-material |
3 | wrapped_multi_material.js (new), wrapped_material, package |
PORT — multi-material feature |
grid-connector |
3 | connector_set.js (new), connector/_CONNECTOR.md |
PORT/REVIEW — connector-set feature |
feature/theme-on-instantiation |
1 | configuration, configurator |
PORT — theme-on-instantiation (Step 2 theme) |
feature/ggd-texture-tweak |
1 | component_tree, block_instance, configuration |
REVIEW — texture tweaks |
feature/materials-base |
1 | project.js |
REVIEW — 8-line change |
long-task |
2 | main, material_category*, wrapped_material |
REVIEW — long-task scheduling |
filter-visible-configs-for-price |
2 | project, state_tracker |
REVIEW — price-visible filtering |
main-moooi |
3 | project.js |
SUPERSEDED — moooi app line |
productMaps |
13 | view/moooi_view.js |
SUPERSEDED — moooi app line |
generator-old |
18 | (generator/util data, no engine src/) |
SUPERSEDED — old generator |
touchscreen |
6 | view/default_view.js |
REVIEW — touch pointer handling |
example/draggable-materials |
2 | block_instance, configuration, positioned_*, view |
SUPERSEDED — drag-and-drop example |
Legend:
- PORT — carries unique work that Step 2 ("port the unique app work from pb-frt-lib-engine-v2") should reconcile into the canonical tree.
- REVIEW — likely superseded or minor, but not yet confirmed; needs a human to verify before deletion.
- SUPERSEDED — redundant app/example snapshots that predate the consolidation and are safe to delete once confirmed.
No remote branches have been deleted yet; only local remote-tracking refs
for the already-merged branches have been pruned (reversible via git fetch).
Version history
- 3.0.0 — consolidated modern engine on three.js r185; parallel bundle alongside the frozen r127 baseline.
- r127 (deprecated baseline) — tagged
legacy/prod-r127; kept serving, rollback-only.
Older, pre-consolidation release notes can be found in the git history and
VERSIONS.MD.