Skip to content

Design Studio — System Overview

Platform-wide architecture summary for the Rauschmayer / DIY ROCKS jewelry configurator. Compiled from the per-component documentation in this folder. For component-level detail, follow the links in the Documentation Index.

Last compiled: 2026-06-11. Source docs verified against develop-2.0 branches.


1. Platform Summary

This is a multi-tenant jewelry configurator platform that lets end customers design custom jewelry — name chains, signet rings, bridal/engagement rings, earrings, necklaces, and wedding bands — through interactive 3D configurators embedded in e-commerce storefronts (WooCommerce, Shopify, Shopware, or custom sites). The customer-facing layer is a Vue 3 web component called Design Studio (the "container app") that orchestrates three independent designer applications, each loaded in its own iframe and communicating over window.postMessage. The three designers are the Name-chain Designer (jewelry_configurator), the Bridal Designer (engagement_configurator), and the Wedding Designer (metrix_configurator, an outsourced Metrix integration). 3D rendering is handled by an external service called Crystal Cave, hosted separately and loaded into the designer iframes at runtime. The production backend is PersnApi, a Laravel API that stores configurations, calculates prices (via the external R2 service), and converts orders into factory jobs through EDI (internal Crystal Cave factory) or the R2 external order API. A second Laravel application, PersnBackend, provides an admin panel plus e-commerce middleware that bridges Shopify and Shopware storefronts to PersnApi. For WooCommerce stores, a dedicated WordPress plugin (design-studio-woo) performs the same add-to-cart and order-sync role. The platform is multi-tenant throughout: each storefront brand is a named tenant scoped by config files in the designers and by bearer-token-scoped, customer-isolated data in the backends.


2. System Architecture Diagram

                          CUSTOMER BROWSER
 ┌──────────────────────────────────────────────────────────────────────────────┐
 │  Storefront host page (WooCommerce / Shopify / Shopware / custom site)         │
 │                                                                                │
 │  ┌──────────────────────────────────────────────────────────────────────┐    │
 │  │  Integration layer (one of):                                          │    │
 │  │   • design-studio-woo  (WordPress plugin, [design_studio] shortcode)  │    │
 │  │   • diy-event-listener.js (injected by PersnBackend on Shopify)       │    │
 │  │   • custom embed + custom triggerHostAction handler                   │    │
 │  └──────────────────────────────────────────────────────────────────────┘    │
 │                                  ▲   │ postMessage: triggerHostAction          │
 │                          jca.ready│   │ (addToCart / wishlist / print …)        │
 │                                  │   ▼                                          │
 │  ┌──────────────────────────────────────────────────────────────────────┐    │
 │  │  Design Studio = <jewelry-container-widget>  (Vue 3 Custom Element)    │    │
 │  │  container-app · ConfiguratorService.js orchestrates all postMessage  │    │
 │  │                                                                        │    │
 │  │   #mount-configurator  (≤ 3 live iframes, LRU eviction)               │    │
 │  │   ┌───────────────┐  ┌───────────────┐  ┌───────────────────────┐    │    │
 │  │   │ Name-chain    │  │ Bridal        │  │ Wedding (Metrix)      │    │    │
 │  │   │ jewelry       │  │ engagement    │  │ outsourced iframe     │    │    │
 │  │   │ PersnVueApp   │  │ configurator  │  │ (bridge.* events)     │    │    │
 │  │   └──────┬────────┘  └──────┬────────┘  └──────────┬────────────┘    │    │
 │  └──────────┼─────────────────┼──────────────────────┼─────────────────┘    │
 └─────────────┼─────────────────┼──────────────────────┼──────────────────────┘
               │ widget-loader   │ widget-loader        │ integration.js
               ▼                 ▼                      ▼
        ┌────────────────────────────────────┐      ┌─────────────────────┐
        │  WebGI 3D widgets (CrystalCave)     │      │  Metrix API         │
        │   signetring     → crystal-cave-    │      │  (snapshots,        │
        │                    signet           │      │   manufacturing)    │
        │   bridalrings    → crystal-cave-    │      │  EXTERNAL / 3rd pty │
        │                    ring (diy-bridal)│      └─────────────────────┘
        │   bridalearrings → crystal-cave-    │
        │                    earring          │
        │   bridalpendants → crystal-cave-    │
        │                    necklace         │
        │  iframe bundles · contentWindow     │
        │  .CrystalCave.world.* (no postMsg)  │
        └────────────────────────────────────┘

        ───── Configuration capture + order (server side) ───────────────────────

   Integration layer ── capture/price/order HTTP ──►  ┌────────────────────────┐
                                                      │  PersnApi (Laravel)    │
   PersnBackend (Shopify/Shopware) ──────────────────►│  configs, pricing,     │
   middleware  cron every 15 min                      │  orders, EDI, PDFs     │
                                                      └───────┬────────────────┘
                                                              │
                  ┌───────────────────────────────────────────┼───────────────────┐
                  ▼                      ▼                      ▼                   ▼
          ┌──────────────┐     ┌──────────────────┐   ┌────────────────┐  ┌──────────────┐
          │ R2 pricing + │     │ Crystal Cave     │   │ EDI filesystem │  │ AWS S3       │
          │ external     │     │ FACTORY          │   │ (factory in/   │  │ screenshots, │
          │ order API    │     │ (name-chain      │   │  out messages) │  │ PDFs, certs, │
          │ (bridal +    │     │  production via  │   │                │  │ price JSON   │
          │  metrix)     │     │ TriggerProduction│   │                │  │              │
          └──────────────┘     └──────────────────┘   └────────────────┘  └──────────────┘

  PersnBackend admin panel (AdminLTE): orders, customers, procurements, vendor invoices,
  manual orders → PersnApi.  Polls Shopify/Shopware for paid orders → PersnApi order/submit.

Data-flow summary: configuration (designer + Crystal Cave) → capture (postMessage triggerHostAction → integration layer → PersnApi /capture) → order (PersnApi /order/submit or /order/external-bulk) → factory (Crystal Cave for name-chain via EDI; R2 external order API for bridal + Metrix).


3. Component Breakdown

Component Repo Tech stack Primary responsibility
Design Studio (container app) jewelry-container-app Vue 3 Custom Element, Pinia, Vue Router (hash), Vite, Axios, vue-i18n Embeds as <jewelry-container-widget>; hosts and orchestrates the 3 designer iframes; normalizes all postMessage traffic; emits triggerHostAction to the host page
Name-chain Designer jewelry (PersnVueApp-2.0) Vue 3, Vuex, Vue Router, Vite, Axios Configures personalized jewelry (name chains, signet rings; ring/bracelet/necklace/earring); builds model data for Crystal Cave; dispatches add-to-cart by tenant cart type
Bridal Designer engagement (engagementconfigurator-2.0) Vue 3, Vuex, Vue Router (hash), Vite, Axios Configures bridal/engagement rings, earrings, necklaces; diamond search; per-jewelry-type build targets sharing src/shared/
PersnApi PersnApi PHP 8.1, Laravel 9, MySQL, Redis, S3, Sanctum, TCPDF/FPDI Stores configs + model data; routes pricing to R2; converts configs into EDI/factory orders; generates PDFs/certificates; multi-tenant by {customer} slug
PersnBackend PersnBackend PHP 8.0, Laravel 9, AdminLTE 3.8, Fortify+Sanctum, Spatie Media Library Admin panel (orders, customers, procurements, invoices) + Shopify/Shopware middleware that creates products and syncs paid orders to PersnApi

Supporting components (not standalone "main repos" but active):

Component Repo Tech stack Role
design-studio-woo design-studio-woo WordPress plugin, PHP 8.0, WooCommerce Embeds Design Studio via shortcode; add-to-cart REST endpoint; two-layer order sync to PersnApi /order/submit
Wedding Designer (Metrix) Metrix (outsourced) 3rd-party Wedding band configurator; snapshot + manufacturing API; embedded via integration.js
Crystal Cave crystalcave TypeScript/Express, Blender 2.79b + Blend4Web, Webpack 3D asset pipeline (DXF→STL/web models) and in-browser 3D widget; also the internal name-chain production factory

4. Key Data Flows

Flow 1 — Customer configures and adds to cart (WooCommerce)

  1. Storefront page renders the [design_studio] shortcode → <jewelry-container-widget> + <div id="mount-configurator">.
  2. Design Studio boots: POST /api/v2/jewelry-container-app/config/ loads offers/settings; emits jca.ready.
  3. User picks an offer; container app creates the designer iframe and sends load-configuration.
  4. The designer (Name-chain or Bridal) builds model data, POST .../modelDatamodel_hash; Crystal Cave renders the 3D view.
  5. User clicks Add to Cart inside the designer iframe → addToCart postMessage to the container app.
  6. Container app calls personalizeActiveApp() (updates localStorage/iframe id) then posts triggerHostAction { intent:'addToCart', details:{ tenant, app, r2CustomerId, configurationId, configurationUrl, imageUrl, currency } } to the host page.
  7. The plugin's event-listener.js catches it → POST /wp-json/design-studio/v1/add-to-cart (retries up to 5× with exponential backoff).
  8. The plugin REST endpoint fetches product details + price: Name-chain/Bridal → GET {diyApi}/capture/{configurationId}?configurator_type={app} (price included in capture response).
  9. A hidden WooCommerce product (SKU DS-{configurationId}, virtual, sold-individually, catalog_visibility: hidden) is created or reused and added to the cart; mini-cart fragments returned.

For Wedding (Metrix): the container app first creates a snapshot (POST /gateway/api/integration/containers/{containerId}/snapshots) to obtain a snapshotId, then emits triggerHostAction; the plugin uses /api/snapshots/{id}/manufacturing + /metrix/{id}/price.

Flow 2 — Order placed → factory

  1. WooCommerce order moves to Processing or Completed.
  2. Plugin instant hook (fast path) calls POST {diyApiUrl}/order/submit once with all Design Studio line items in a single bulk request. A WP-Cron safety net (every 15 min, last 24h, up to 10 retries/item) covers failures. HTTP 423 Locked is treated as success (idempotent re-submission).
  3. PersnApi order/submit places each item in one DB transaction and routes by app:
  4. jewelry_configuratorNameChainOrder → dispatches CrystalCave\TriggerProduction (POSTs model JSON to the Crystal Cave factory; EDI prefix APICALL_).
  5. engagement_configuratorBridalOrder and metrix_configuratorDesignerOrder → dispatched to R2 via PlaceExternalOrder (POST /api/public/externalOrder/order).
  6. Each configuration can be ordered once; a repeat returns 423 Locked (but an idempotent retry of the same customer_order_number returns the existing order).

Flow 3 — Admin management / Shopify-Shopware sync (PersnBackend)

  1. On a Shopify store, diy-event-listener.js (injected by PersnBackend when the site is added) handles the triggerHostAction and calls POST /api/products/create/{configurationId} on PersnBackend.
  2. PersnBackend looks up the ShopifySite by custom_domain, fetches capture/manufacturing data + price, creates a Shopify product, and stores a ShopifyProduct record (shopify_order_id = null).
  3. Customer pays. Scheduled app:process-orders (every 15 min) runs process-shopify-orders and process-shopware-orders: fetches paid orders from the last 12h, and per line item calls PersnApi POST order (name-chain/bridal) or POST order/metrix (Metrix).
  4. After all line items in an order succeed, one POST order/external-bulk covers all Bridal + Metrix items atomically; shopify_order_id / shopware_order_id is then set. A failed batch leaves it null and retries next run.
  5. Admin panel (AdminLTE) manages orders, customers, products, procurements, vendor invoices, manual orders (/api/v1/{tenant}/capture then /order), tokens, and audits. New-order emails go out every 10 minutes.

Flow 4 — Direct embed (non-WooCommerce / custom site)

  1. Custom site includes the Design Studio script and the <jewelry-container-widget> element with optionsid, apibaseurl, metrixapibaseurl, embedhosturl, apppublicurl.
  2. Design Studio boots and emits jca.ready.
  3. User configures; the designer posts addToCart → container app emits triggerHostAction to the host page.
  4. The host page implements its own window.addEventListener('message', …) to catch triggerHostAction, then calls PersnApi /capture/{id}, fetches the price, and adds the item to its own cart. The persn.com Shopify Liquid embed (see persn-com-embed-fix.md) is an example of this pattern, including a jca.changeLocale bridge gated on jca.ready.

5. Multi-Tenancy

Tenancy runs through every layer:

Designer tenant configs. Each designer app ships a tenant config keyed by name:

  • Name-chain: src/data/tenants.js (16 tenants), wrapped by src/classes/Tenant.js. The active tenant comes from embedConfig.tenant.
  • Bridal: src/tenantConfig.js (12 *-bridal tenants), merged into runtime config by appConfig.js; matched against configuratorConfig.tenant.

A tenant config controls branding (colors, logo, PDF footer), which options are shown (inactivePlatingGroups, inactiveStoneGroups, inactiveThemeTypes, etc.), pricing/customer id, router mode, and — critically — the cart dispatch method.

Cart / dispatch types (Name-chain Tenant.addToCart):

Type Meaning / mechanism
woocommerce Dispatches CustomEvent('diyConfigurator:addToCartButtonClicked', …) (or buyNow…). Used by persn-com, mytrauringstore, veynou, unique-jewelry, diy-rocks, vanhess-jeweller, stargold, emperor-diamond, passion-jewelry, rauschmayer
fourGradAddToCart Calls window.fourGradAddToCart(configurationId) directly. Used by christ, christ-ch, valmano, gravurschmuck, meicel-jewelry
systaro Dispatches the same CustomEvent as woocommerce; only juwelier-plus uses it
contactJeweler Dispatches CustomEvent('diyConfigurator:contactJeweler', …) instead of cart
(container-app) When inside the iframe container, the designer posts {type:'addToCart'} to window.parent — the container app then normalizes to triggerHostAction

The container app emits a single normalized contract upward regardless of the designer's internal dispatch.

PersnApi. Multi-tenant by {customer} slug in every route. Laravel Sanctum bearer tokens carry named abilities mapping to route names; an admin ability bypasses customer scoping. ApiController enforces checkAccess (ability) and checkCustomer (ownership) on every request. The Customer model's configurator_type (0 = jewelry/Persn, 1/2 = bridal, 3 = Metrix) drives pricing-path selection. Tokens may also carry an allowed_factory for factory-scoped production reads.

PersnBackend. Customer-scoped data isolation by role. Users have a customer_id (active context) and a customers JSON array of permitted customers; customer_id = -1 means all. CUSTOMER / CUSTOMER_ACCOUNTING roles are always scoped to their assigned customer; super-admin/super-user can switch context. Encrypted per-customer bearer tokens authenticate calls into PersnApi.


6. PostMessage Protocol

The container app (ConfiguratorService.js) is the central hub. A single window.addEventListener('message', …) handles all inbound messages. The seam has three boundaries: host page ↔ Design Studio ↔ designer iframes.

Design Studio → Host page (window.postMessage(msg, '*')):

  • jca.ready — boot complete; the host may now send locale/currency messages.
  • triggerHostAction — normalized user action. intent ∈ { addToCart, createWishlist, print, contactJeweler, convertOldSnapshotId }; details includes tenant, app, r2CustomerId, configurationId, configurationUrl, imageUrl, currency?. This is the primary integration contract for any storefront.

Host page → Design Studio (handled via window.addEventListener):

  • jca.changeLocale { details:{ locale } }, jca.changeCurrency { details:{ currency } } — propagated to all mounted iframes.
  • loadSnapshot { details:{ snapshotId } } — response to convertOldSnapshotId.

Designer iframes → Design Studio:

  • All: iframe-height { height } (debounced 150 ms).
  • Name-chain / Bridal: initializedConfigurator; saveConfiguration { details:{ configurationId, imageUrl } }; addToCart; createWishlist; contactJeweler.
  • Wedding (Metrix, via MetrixConfigurator.html): bridge.containerId; bridge.addToCart; bridge.wishlist; bridge.print; bridge.consultation; metrix.saveConfiguration. For all Metrix host actions, the container app first creates a snapshot via the Metrix API to obtain a configurationId (snapshot id).
  • mobileCartState — received but currently inactive (setMobileCartState not defined in the Pinia store; the Mobile Sticky Cart feature is disabled).

Design Studio → Designer iframes (iframe.contentWindow.postMessage):

  • load-configuration { config, clarityId, clarityUserId } (once after iframe load), saveConfiguration, changeCurrency, changeLocale, injectCustomCss (Name-chain/Bridal inject into <head>; Metrix injects into its shadow DOM).

Legacy DOM-event bridge. The WooCommerce plugin's event-listener.js and the designers' Tenant.addToCart also support custom DOM events (diyConfigurator:addToCartButtonClicked, engagementConfigurator:addToCartButtonClicked) for non-container embeds.


7. Deployment & Environments

Designers (jewelry, engagement). GitLab CI pipelines, triggered manually. Set $TENANT (and $APP = ring/earring/necklace for engagement) before running. Branch convention: develop* → staging, release* → production. Local: ./setupTenant.sh <tenant> (jewelry) or ./setupAppAndTenant.sh <app> <tenant> (engagement), then npm run dev. Crystal Cave is deployed separately per jewelry type (crystal-cave-ring/, etc.) and copied to the target server.

Container app. Vite build; embedded on the host page via a <script> tag loading the built JS that defines the custom element. Local dev: npm run dev (no setup script). Build-time env vars: VITE_API_URL, VITE_API_TOKEN, VITE_SCREENSHOT_URL, VITE_METRIX_API_URL, VITE_MODE.

PersnApi. Laravel app. Requires a queue worker (php artisan queue:work) and a per-minute scheduler. Deploy via bash deploy_tenant.sh (clears + re-caches config/routes, reloads PHP-FPM). APP_ENV=staging enables an extra hourly edi:force. Cache/queue on Redis in production.

PersnBackend. Laravel + Vite (HTTPS required locally to inject the Shopify script tag). Per-minute scheduler runs app:process-orders (15 min), app:delete-unused-products (daily), email:new-orders (10 min). Queue worker handles media conversions.

design-studio-woo. WordPress plugin; WP-Cron jobs (ds_woo_every_15_min, daily cleanup) — a real server cron hitting wp-cron.php is recommended on low-traffic sites.

Known stage URLs. 3D Designer API https://test-api.3d-designer.shop; Metrix https://rauschmayer.metrix-demo.com; R2 pricing https://api.r2-stage.demo-version.net/api/public/b2c/calculatePrices. Production 3D Designer API: https://api.3d-designer.shop/.


8. 3D Rendering — WebGI Widgets

The interactive 3D rendering layer is a set of four WebGI-based TypeScript widgets, each built to a UMD bundle (diamond_shader.umd.js) that exposes a single browser global, window.CrystalCave. All public surface lives on window.CrystalCave.world (an instance of a per-widget World class). Each widget is embedded as an iframe by a designer app. The signet ring widget is embedded by the Name-chain Designer (PersnVueApp / jewelry); the other three are embedded by the Bridal Designer (engagement).

No postMessage. The parent designer drives each widget by reaching into the iframe and calling methods directly: iframe.contentWindow.CrystalCave.world.setData(...). (The bridalrings integration doc also describes an optional postMessage routing layer for cross-origin use, but the same-origin direct-call pattern is the one in use.) The widgets emit no events and register no callbacks; the only callback mechanism is the per-call result of setData / getScreenshot / getProductImageAsBase64.

These WebGI widgets are distinct from the legacy crystalcave repo (the Blender/Blend4Web asset pipeline and the internal name-chain production factory described in §4 Flow 2). They share the "Crystal Cave" name and global namespace but are separate codebases with their own GLB asset pipelines.

8.1 Widget map

Widget Repo / source Embedded by Configures
Signet ring signetring Name-chain Designer (PersnVueApp) Signet ring: base style + plate shape, engraving plate (letter/font/zodiac/fingerprint/text), metal plating, surface finish (top/side), stone-texture rings, diamonds
Bridal rings bridalrings Bridal Designer Engagement ring: diamond shape, head (setting), shank (band), per-head/per-shank plating, engraving, AR try-on, lift-up
Bridal earrings bridalearrings Bridal Designer Mirrored left/right earring pair from a single model ID; metal + diamond optics; standalone stone preview
Bridal pendants bridalpendants Bridal Designer Pendant or necklace head; optional chain (1 of 2 styles); bail connector; metal + diamond color

8.2 Common API

All four widgets share a core surface:

Method Behavior
world.initialize(json?) Boots the WebGI viewer (loads scene, materials, volume/stone data). Must run once before anything else. Takes an optional { assetRoot }; defaults to ./assets/. Camera zoom/pan/rotation are locked at init. Requires a canvas element in the host page (#webgi-canvas for signetring, #webgl_drawer for the three bridal widgets).
world.setData(data) The main call. Accepts the configuration object/JSON, loads the right GLBs, applies materials, and returns weight/volume + stone data. Async (returns a Promise).
world.setBackgroundColor(val) Sets the scene background to a CSS color/hex (e.g. "#F8F8F8") so the iframe blends into the page.
world.setCameraDistance(d) Scales the camera distance from the model.
world.resetCamera() Recenters the camera on the model / default position.

What setData returns — the shape differs per widget but always carries the data the configurator forwards to PersnApi pricing (metal weight = volume × material specific weight):

Widget setData return (pricing-relevant fields)
signetring { status, weight, stones }weight in grams (from ring_data.json volume × plating specific weight, base scaled by inner diameter); stones: [{ quantity, code }]
bridalrings { status, response_number, volume: { head, shank }, stones: [{ head, shank }] } — volumes in mm³ (shank scaled to ring diameter)
bridalearrings { status, response_number, volume: { left, right }, stones: [{ left, right }] }
bridalpendants { status, response_number, volume: { head }, stones: [{ head }] }

The three bridal widgets include a response_number (monotonic counter) so the configurator can discard stale out-of-order responses. Screenshot capture is getScreenshot() (bridal widgets, returns a PNG data URL) or getProductImageAsBase64(json, cb) (signetring, callback with base64); all reset the camera before capturing.

8.3 Per-widget differences

  • signetring — Geometry is a base + engraving-plate structure. Driven by ring_style (basic / shoulder / side-circle / top-frame / combinations) × ring_shape (cushion / oval / rectangle / round / mini / small / hexagon / hexagon-stone). Setting plate_stone (onyx / lapis / malachite / tigerseye) auto-switches the ring to the Lucke "stone" style. Rich plate system: single/double/quarter letters, multiple fonts (Roboto Flex, Playfair, Courier, Libre Baskerville), zodiac symbols, fingerprint plates, and free engraving text. Plating SKU → metal ID lookup; surface finish (polished/brushed) set independently for top and side faces. Includes a standalone CrystalCave.FingerprintProcessor utility for preprocessing fingerprint images.
  • bridalringsHead + shank + diamond as separate GLBs. Head file resolved as {head_id}_ct{karat}.glb (karat bucketed to 6 tiers); setting a head forces the loose diamond to empty. Shank needs a matching cutout so head prongs fit — setData swaps the shank folder (shank/, shank_cutout_h1041/, _h4018/, _hd038/, _h5021/) by head-ID + karat rules. Supports two-tone plating (head and shank independent), text engraving, AR try-on (ijewel3d web-vto, 20 localized languages), and lift-up mode (per-shank Y offsets).
  • bridalearrings — Renders a mirrored pair from one model ID: left at (+0.5,0,0) rot -1.5z, right at (-0.5,0,0) rot +1.5z. No shank/head separation — each earring is a single self-contained GLB ({id}_ct{carat}.glb). Left and right cannot be configured independently. Carat token is passed as stone.diamond.code (e.g. "ct1000"; eb036/eb046 also have ct1500). setEmpty(shape) shows a bare stone.
  • bridalpendants — Single piece with chain attachment. chain.pendantType.type selects the loader: "pendant"setPendant() (prefix p, single chain GLB + bail_1.glb bail connector) vs "necklace"setNecklace() (prefix n, split _left/_right chain halves). Chains attach at named anchor objects (Default_chain_ap, Default_left_ap, Default_right_ap). All models rotated π/2 on X at load.

8.4 Asset pipeline

GLB files are not hand-authored. They are produced by Python pipelines from CAD source (Rhino .3dm / 3DM):

  • signetring assets come from the separate SignetRingAssets project — a 4-step pipeline (step0_clear_outputstep1_save_as_glb (Rhino export) → step2_fix_material (apply WebGI .pmat/.dmat extensions, mesh names, diamond/brushed extensions) → step3_copy_glb_to_public). Volume/diamond data per variant lives in src/ring_data.json (drives weight calc).
  • bridalrings / bridalearrings / bridalpendants assets derive from the DemoWebGI_Assets source project. Files follow strict naming conventions (heads h{id}_ct{karat}.glb, shanks s{id}_c56_w{width}_ct{karat}.glb, earrings e{id}_ct{carat}.glb, pendants/necklaces p|n{id}_ct{carat}.glb). An asset manifest (variations.config.json) plus volume/stone-count JSON files feed the viewer. Per-repo Python utility scripts audit completeness and regenerate dropdowns (list_files.py, list_missing_carats.py, fix_bad_shanks.py, etc.).

WebGI versions differ across widgets: signetring and bridalrings use 0.17.0; bridalearrings and bridalpendants use 0.9.13.

8.5 Deploy paths

Each widget builds (npm run b) to dist/ and deploys via a per-repo rsync_*.py script (paramiko SSH/SFTP, SHA-256 hash diffing, SSH key at %USERPROFILE%\.ssh\id_rsa). Both servers run on port 1022: staging 49.12.38.120 (test-api3d-designershop), production 23.88.47.146 (api3d-designershop). Each widget deploys to a different CDN path:

Widget Remote path / served URL
signetring …/crystal-cave-signet
bridalrings …/crystal-cave-ring (served at /diy-bridal/ring/)
bridalearrings …/crystal-cave-earring
bridalpendants …/crystal-cave-necklace

Production hosts under https://api.3d-designer.shop/, staging under https://test-api.3d-designer.shop/.

8.6 Known quirks

  • signetring setColor does not exist. The README shows setColor(1, 1, 1, …), but no such method exists in src/index.ts. It is a stale/removed reference — do not use it. (The three bridal widgets do have a real setColor(r,g,b,absorption,refractiveIndex,reflectivity,dispersion) for diamond optics.)
  • bridalpendants setData always resets metal to gold. setPendant/setNecklace end by calling setMetal(0). The configurator must re-apply the desired metal with setMetal(metalId) after every setData().
  • bridalpendants 400ms chain-positioning workaround. Chain anchors are positioned via an await timeout(400) before chain placement, to ensure anchor objects are settled after model load. Chain offsets are hardcoded (-0.07 Y for pendants; +0.02 X / +0.03 Y for necklaces) rather than read from the anchors.
  • bridalpendants inherited e-prefix keys. Volume lookups use earrings_volume_data.json keyed by e<headId> and stone counts use heads_stone_count.json keyed by h<headId>, both inherited from the earrings/rings projects (the code rewrites the eh prefix where needed).
  • bridalrings shank width matching. Width is not transmitted in the API payload; the shank GLB is resolved by regex against variations.config.json, with a width-agnostic fallback if the cutout variant was built at a different width.

9. Out of Scope / Prototype

original-configurator is a prototype and is not yet integrated into the production platform. Its only documentation is a minimal README describing a git-submodule-based client app run with yarn install && yarn start — it appears to be an early standalone configurator client, kept for reference but not part of the active Design Studio → PersnApi → factory flow.


10. Documentation Index

Area File Description
Top level design-studio/ONBOARDING.md Onboarding: repos, how the apps talk, add-to-cart summary, stage URLs, gotchas
design-studio/add-to-cart-integration-docs.md External IT integration guide: triggerHostAction, capture, prices, /order/submit
design-studio/persn-com-embed-fix.md persn.com Shopify Liquid embed + locale-bridge fix
design-studio/persn-skus/products-changes-report.md SKU/plating-suffix changes; stone-plate splits into shape variants
design-studio/persn-skus/skus-price-update-21052026.md Filtered SKU price table (2026-05-21 update)
Container app design-studio/container-app/README.md Container app index + quick facts
design-studio/container-app/architecture.md Stack, boot sequence, ConfiguratorService, Pinia store, component tree
design-studio/container-app/api-endpoints.md 3D Designer + Metrix API endpoints used by the container app
design-studio/container-app/designers.md The three designer iframes: config shapes, ids, settings merge
design-studio/container-app/embedding.md Embed attributes, inline options, custom CSS/theming, locales
design-studio/container-app/features.md Feature inventory (multi-config, sharing, QR, deep links, Clarity)
design-studio/container-app/postmessage-protocol.md Full postMessage reference, all directions
design-studio/container-app/routing-and-deep-links.md Hash routing, route table, deep-link params
Jewelry (Name-chain) design-studio/jewelry/project-overview.md Stack, categories/themes, router, embed config, deploy
design-studio/jewelry/tenant-config.md 16 tenants, cart types, config fields, per-tenant notes
design-studio/jewelry/store-state.md Vuex state shape, mutations, session persistence
design-studio/jewelry/model-data-structure.md storedInputData payload sent to createModelData
design-studio/jewelry/shared-reference-data.md Platings, stones, enamelings, ring sizes
design-studio/jewelry/signet-ring-data-structure.md Signet 3-product structure, theme types, stone plates, surfaces
design-studio/jewelry/demo-pages.md Demo/widget-loader page locations
Engagement (Bridal) design-studio/engagement/project-overview.md Build targets (ring/earring/necklace), steps, routes, deploy
design-studio/engagement/tenant-config.md 12 *-bridal tenants, config fields, embed overrides
design-studio/engagement/store-state.md Vuex modules (configuration, diamond, canvasMenu)
design-studio/engagement/model-data-structure.md Ring/earring/necklace model data; stone modes
design-studio/engagement/reference-data.md Alloys, colors, diamonds, heads, shanks, chains
design-studio/engagement/api-and-services.md API config, BaseDesignerApiService, messaging plugin
WooCommerce plugin design-studio-woo/README.md Install, config, add-to-cart flow, order sync, cleanup, REST endpoint
design-studio-woo/technical-reference.md Hooks, product/order meta, cron, payloads, capture mapping
PersnApi persn-api/README.md What it does, configurator types, scheduled tasks, Artisan commands
persn-api/api-endpoints.md Full V1/V2 endpoint reference + error codes
persn-api/architecture.md Controllers, models, services, jobs, middleware, enums, schema
persn-api/configuration.md All .env variables (DB, Redis, S3, R2, Crystal Cave, Metrix, EDI)
PersnBackend persn-backend/README.md Admin panel sections, API endpoint, scheduled tasks, docs feature
persn-backend/ARCHITECTURE.md System context, layers, data model, order lifecycle, security
persn-backend/CONFIGURATION.md All .env variables + custom config files
persn-backend/LOGGING.md Log channels, Sentry, what gets logged, per-env setup
persn-backend/ROLES-AND-ACCESS.md Roles, access matrix, customer scoping, 2FA, tokens
persn-backend/SCHEDULED-TASKS.md Cron schedule, process-orders detail, one-off commands
3D widget: signet ring 3d-widgets/signetring/README.md WebGI signet-ring widget: what it renders, tech stack, structure, build/deploy
3d-widgets/signetring/api-reference.md window.CrystalCave.world API: setData shape, ring/plate/plating tables, capture
3d-widgets/signetring/integration.md Iframe embedding (direct contentWindow), modelData, plating/finish, weight/stones
3d-widgets/signetring/asset-pipeline.md SignetRingAssets 4-step Rhino→GLB pipeline, file naming, ring_data.json, rsync
3D widget: bridal rings 3d-widgets/bridalrings/README.md WebGI engagement-ring widget: diamond/head/shank/plating, AR, build/deploy
3d-widgets/bridalrings/api-reference.md API: setData/getVolume, selection JSON, karat buckets, plating codes, AR, engraving
3d-widgets/bridalrings/integration.md Iframe embed + postMessage routing, modelData, return shape, screenshot, AR
3d-widgets/bridalrings/asset-pipeline.md Head/shank/diamond naming, cutout rules, lift-up offsets, variations.config, rsync
3D widget: bridal earrings 3d-widgets/bridalearrings/README.md WebGI earring-pair widget: mirrored pair, materials, structure, build/deploy
3d-widgets/bridalearrings/api-reference.md API: setData (left/right), plating codes, setMetal/setColor/setEmpty, screenshot
3d-widgets/bridalearrings/integration.md Embed, modelData, return shape, diffs from bridalrings (pair vs single, no shank)
3D widget: bridal pendants 3d-widgets/bridalpendants/README.md WebGI pendant/necklace widget: chain/bail, carat buckets, structure, build/deploy
3d-widgets/bridalpendants/api-reference.md API: setData, setPendant/setNecklace, plating codes, setColor, modelData shape
3d-widgets/bridalpendants/integration.md Embed, pendant vs necklace, metal-reset quirk, diffs from rings/earrings
External / context crystalcave/README.md Legacy Crystal Cave pipeline (Blender/Blend4Web), exporters, name-chain factory
original-configurator/README.md Prototype client (not integrated)
This file SYSTEM-OVERVIEW.md Platform-wide architecture summary (you are here)