Home

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/engine in package.json)
  • Three.js: r185 (three@^0.185.1)
  • Bundle: build/pb-engine.3.0.0.js (and a minified .min.js alongside 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/)

  • actorActor, Configurator, SingleBlockInstance
  • componentComponent, ComponentTree
  • configuratorConfiguration, block instances, connections, categories
  • default_settings — camera/renderer defaults
  • export — image / GLB / USDZ / PDF exporters
  • package — blocks, materials, meshes, themes, connectors, loaders
  • reporter — logging and information sources
  • scene — composer, environment, contact shadow, space, scene_manager
  • serverServerConnection (WebSocket RPC)
  • transformTransform, DefaultSelectionTransform
  • utilities — geometry/dimension/placement helpers
  • viewView, 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.jsmakeTestBlock, makeTestBlockInstance, makeConnectorTypes: builds a fully meshed block (PositionedMeshWrappedMeshGLTFMaterialSetWrappedMaterial). This is the fixture needed to drive Configuration._build('main').
  • test/package/make_component_test_set.jsmakeComponentTestSet, makeMesh.
  • test/package/mock_component.jsMockComponent.
  • test/configurator/create_mock_constructs.jscreateConfiguration, 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 by test/integration/test.integration_axel.js and 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 via project.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 when settings.thumbnail exists).
  • The "assign from default materials" loop (defaultMaterials) — needs a config instantiated with a non-empty defaultMaterials.
  • The WrappedMesh._normalMapbuildMeshMaterial branch.
  • The texture-map clone + tiling loop (needs a default material that carries a Texture map).
  • The aoMap instanceof Texture needsUpdate line.

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 / addPackage full path (already partly covered by the axel test).
  • save, load, share, buildProjectFromExport.
  • addActor, selectPreset (happy path), setDefaultMaterial (happy path).
  • messageApiRequest branches set-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 global project — 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)

  1. src/view/view.js zoomExtents/zoomScene/zoomTop/showDimensions reference an undefined global project (lowercase). They crash when called outside a global scope that happens to define project.
  2. src/project.js messageApiRequest('set-locale') references an undefined global makeResponse (and an undeclared response) when there is no server, so the graceful "not connected" fallback is currently unreachable.
  3. src/utilities/getDimensionsBoundingBox.js returns center/base via a shared Vector3 that getSize() later mutates, so center (and base.x/base.z) reflect the box size rather than the geometric center.
  4. src/utilities/nodeMaterialBuilder.js (and wrapped_material.js) assigns material.needsUpdate = true, which is a no-op on three.js r185 (MeshStandardMaterial.needsUpdate stays undefined).
  5. src/state_tracker.js checks the overflow before push, so the state register can transiently hold maxStateCount + 1 entries.
  6. src/server/server_connection.js uses settings.reconnectTime || 30, so reconnectTime: 0 cannot 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.