Simplio3D

Option Blocks

Option blocks are the core configurator building blocks. Each block type represents a different UI control that drives 3D model changes, material swaps, visibility toggles, and more. Supports 15 block types for headless integration.

Supported Block Types

dropdown
select-material
thumbnail-selector
checkbox
toggle-switch
carousel
text-input
number-input
file-upload
pattern-designer
design-canvas
hotspot
fixed-material
section-header
scenery

Select Material — authoring modes

The select-material block exposes two authoring modes via a top-level materialSource field:

  • Manual (materialSource: 'manual', default) — variants are authored individually in dropdownVariants, each with its own target, material, and pricing. Supports Price Group / Variable / Price Table linking.
  • From Category (materialSource: 'category') — runtime variants are auto-populated from every material inmaterialCategoryId. The block uses a shared target that can span multiple objects — categoryTargetObjectIds (string[]) and per-model categoryTargetParts (Record<modelId, string[]>) — applied to every synthesized variant (legacy single-object categoryTargetObjectId /categoryTargetPartNames are still read as a fallback). Pricing flows through the standard Pricing system (Price Group / Variable / Price Table / Unique Price) — link a pricing block to the category-mode Select Material block and author one price per material. Variant IDs at runtime are auto:{materialId}.

Public share viewers fetch referenced category materials at first paint via GET /share/:projectId/:token/category-materials?ids=cat1,cat2, with GET /share/:projectId/:token/category-materials/:categoryId still available for a single category.

List Option Blocks

GET/projects/:id/option-blocks
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// {
//   "success": true,
//   "blocks": [
//     {
//       "id": "blk_001",
//       "type": "dropdown",
//       "name": "Frame Color",
//       "visible": true,
//       "enabled": true,
//       "dropdownVariants": [
//         {
//           "id": "var_a",
//           "label": "Black",
//           "value": "black",
//           "materialData": { "baseColor": "#1a1a1a", "metallic": 0.8, "roughness": 0.2 },
//           "targetPartNames": ["frame_body", "frame_legs"]
//         },
//         {
//           "id": "var_b",
//           "label": "Silver",
//           "value": "silver",
//           "materialData": { "baseColor": "#C0C0C0", "metallic": 0.95, "roughness": 0.1 },
//           "targetPartNames": ["frame_body", "frame_legs"]
//         }
//       ],
//       "defaultDropdownValue": "black"
//     },
//     {
//       "id": "blk_002",
//       "type": "thumbnail-selector",
//       "name": "Fabric Type",
//       "visible": true,
//       "enabled": true,
//       "thumbnailStyle": "grid",
//       "thumbnailSize": "60x60",
//       "thumbnailShape": "rounded",
//       "dropdownVariants": [
//         {
//           "id": "var_c",
//           "label": "Linen",
//           "value": "linen",
//           "thumbnailUrl": "https://...",
//           "materialData": { "baseColor": "#f0e6d3", "roughness": 0.9 }
//         }
//       ]
//     },
//     {
//       "id": "blk_003",
//       "type": "toggle-switch",
//       "name": "Armrests",
//       "visible": true,
//       "enabled": true,
//       "dropdownVariants": [
//         { "id": "var_d", "label": "Without", "value": "without",
//           "visibilityConfig": { "targetObjectIds": ["model_armrest"], "action": "hide" } },
//         { "id": "var_e", "label": "With Armrests", "value": "with",
//           "visibilityConfig": { "targetObjectIds": ["model_armrest"], "action": "show" } }
//       ]
//     }
//   ]
// }

Create Option Block

POST/projects/:id/option-blocks
// Create a Dropdown block with material-swapping variants
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    type: 'dropdown',
    name: 'Wood Finish',
    dropdownVariants: [
      {
        id: crypto.randomUUID(),
        label: 'Oak',
        value: 'oak',
        targetPartNames: ['desk_top', 'desk_drawer'],
        materialData: {
          baseColor: '#8B6F47',
          metallic: 0.0,
          roughness: 0.8,
          textureMap: 'https://cdn.example.com/textures/oak.jpg',
          normalMap: 'https://cdn.example.com/textures/oak_normal.jpg'
        }
      },
      {
        id: crypto.randomUUID(),
        label: 'Walnut',
        value: 'walnut',
        targetPartNames: ['desk_top', 'desk_drawer'],
        materialData: {
          baseColor: '#5C3D2E',
          metallic: 0.0,
          roughness: 0.75,
          textureMap: 'https://cdn.example.com/textures/walnut.jpg'
        }
      }
    ],
    defaultDropdownValue: 'oak'
  })
});

// { "success": true, "block": { "id": "blk_new", ... } }

Create Checkbox Block

POST/projects/:id/option-blocks
// Checkbox — multiple selections, each toggling 3D object visibility
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    type: 'checkbox',
    name: 'Optional Accessories',
    checkboxAlignment: 'vertical',
    showSelectAll: true,
    dropdownVariants: [
      {
        id: crypto.randomUUID(),
        label: 'Cup Holder',
        value: 'cup-holder',
        visibilityConfig: { targetObjectIds: ['model_cup_holder'], action: 'show' }
      },
      {
        id: crypto.randomUUID(),
        label: 'Monitor Stand',
        value: 'monitor-stand',
        visibilityConfig: { targetObjectIds: ['model_monitor_arm'], action: 'show' }
      },
      {
        id: crypto.randomUUID(),
        label: 'Cable Tray',
        value: 'cable-tray',
        visibilityConfig: { targetObjectIds: ['model_cable_tray'], action: 'show' }
      }
    ],
    defaultCheckboxValues: ['cup-holder']
  })
});

Create Carousel Block

POST/projects/:id/option-blocks
// Carousel — swipe/arrow navigation through variants
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    type: 'carousel',
    name: 'Upholstery',
    carouselArrowStyle: 'filled',
    carouselAnimation: 'slide',
    carouselLoop: true,
    carouselShowCounter: true,
    carouselShowLabel: true,
    dropdownVariants: [
      { id: crypto.randomUUID(), label: 'Velvet Red', value: 'velvet-red',
        thumbnailUrl: 'https://...', materialData: { baseColor: '#8B0000', roughness: 0.9 } },
      { id: crypto.randomUUID(), label: 'Leather Brown', value: 'leather-brown',
        thumbnailUrl: 'https://...', materialData: { baseColor: '#654321', roughness: 0.6, metallic: 0.1 } }
    ]
  })
});

Create Text Input Block

POST/projects/:id/option-blocks
// Text Input — user-typed text rendered as a texture on a 3D part
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    type: 'text-input',
    name: 'Engraving',
    textInputTargets: [{
      id: crypto.randomUUID(),
      label: 'Custom Text',
      targetPartName: 'engraving_plate',
      placeholder: 'Enter up to 20 characters',
      maxLength: 20,
      fontFamily: 'Roboto',
      fontSize: 48,
      fontColor: '#1a1a1a',
      textAlign: 'center',
      canvasWidth: 1024,
      canvasHeight: 256,
      backgroundColor: '',
      // Restrict consumer input. 'numbers' triggers the mobile numeric keypad
      // and strips any non-digit characters on every change. Use 'any' (or
      // omit the field) for unrestricted text. For decimal math / pricing,
      // use the dedicated number-input option block instead.
      contentType: 'numbers',
      // Let consumers swap the font, but only between these three.
      // Omit (or set to undefined) to expose every built-in font.
      allowConsumerFontFamily: true,
      consumerAllowedFonts: ['Roboto', 'Montserrat', 'Playfair Display']
    }],
    textInputShowLivePreview: true
  })
});

Create Hotspot Block

POST/projects/:id/option-blocks
// Hotspot — interactive 3D marker with popup content
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    type: 'hotspot',
    name: 'Adjustable Mechanism',
    hotspotPosition: { x: 0.5, y: 1.2, z: 0.3 },
    hotspotTitle: 'Tilt Mechanism',
    hotspotDescription: 'Multi-angle tilt lock supports 5 positions from 90-135 degrees.',
    hotspotIcon: 'info',
    hotspotTrigger: 'click',
    hotspotAnimationStyle: 'pulse',
    hotspotImageUrl: 'https://cdn.example.com/mechanism.jpg',
    hotspotLinkUrl: 'https://example.com/specs',
    hotspotLinkLabel: 'View Specifications'
  })
});

Update Option Block

PUT/projects/:id/option-blocks/:blockId
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks/blk_001', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Updated Block Name',
    visible: false,
    dropdownVariants: [/* updated variants */]
  })
});

Delete Option Block

DELETE/projects/:id/option-blocks/:blockId
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks/blk_001', {
  method: 'DELETE',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Reorder Option Blocks

POST/projects/:id/option-blocks/reorder
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks/reorder', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    blockIds: ['blk_003', 'blk_001', 'blk_002'] // New order
  })
});

// { "success": true, "blocks": [/* blocks with updated order */] }

Continue reading