Skip to main content

How To Create Components

Components in DynaMaker are typically known as Parts and Assemblies in other CAD softwares. The advantage of using them over simple Visualization Models is that they include certain functionalities that can make the development of your complex product configurators easier.

Syntax​

When creating a component, you can start from the template of the cube:

  • Go into Studio > + New Building Block > Component.
  • Choose e.g. CubeComponent as name, and Width, depth and height as input.
  • You will see a component, which has its own "visualization model" connected already to some inputs or properties.
  • Then you will see the following class essentially containing:
    • properties that work as the input for the whole component.
    • generateGeometry as the only visualization model for the whole component.
export interface CubeComponentProperties {
width: number
depth: number
height: number
}
export class CubeComponent extends STUDIO.BaseComponent<CubeComponentProperties> {
constructor() {
super()

this.properties = {
width: 100,
depth: 100,
height: 100,
}
}

generateGeometry() {
const width = this.properties.width
const depth = this.properties.depth
const height = this.properties.height

const geometryGroup = new SKYCAD.GeometryGroup()

const model = new SKYCAD.ParametricModel()
const sketch = SKYCAD.generateRectangleSketch(0, 0, width, depth)
const plane = new SKYCAD.Plane()
model.addExtrude(sketch, plane, height)
geometryGroup.addGeometry(model)

return geometryGroup
}
}

Component Basics​

A. Blocks​

A component consists of some default blocks but additional ones can be added.

A1. Default blocks​

A component comes with 3 class functions or sub-blocks as default that are ready to be used:

  • generateGeometry() as the visualization model.
  • update() meant for assemblies with subcomponents.
  • getConnectors() that must return a list of connectors meant for easier placement in assemblies.

An example with these 3 (skipping the logic inside) would look like these:

export class CubeComponent extends STUDIO.BaseComponent<CubeComponentProperties> {
constructor() {
super()

this.properties = {
// properties
}
}

getConnectors() {
// list of connectors
}

update() {
// logic for sub-components
}

generateGeometry() {
// visualization model
}
}
note

See that the template only comes with generateGeometry as it's usual to have a visualization while not always connectors or sub-components commonly found in assemblies. Although these 3 are optional to use, they help to put together the product configurator with other built-in functions, that are explained later in this guide.

A2. Custom blocks​

Additional blocks can be added to any component with whatever purpose that easily wraps certain functionality. Some examples are those that include certain calculations with the properties, generation of BOM lists and so on. Simply add them as it is done with the default blocks, see getBom() and getVolume() below:

export class CubeComponent extends STUDIO.BaseComponent<CubeComponentProperties> {
constructor() {
super()

this.properties = {
// properties
}
}

getConnectors() {
// list of connectors
}

update() {
// logic for sub-components
}

generateGeometry() {
// visualization model
}

getBom() {
// logic for generating a BOM
}

getVolume() {
const volume = this.properties.width * this.properties.depth * this.properties.height
return volume
}
}

A3. Using blocks within each other​

Naturally you can always call any block within each other, just make sure there's not recursion causing infinite loops.
In the example below, the height is calculated in getHeight() which is used within generateGeometry():

export class CubeComponent extends STUDIO.BaseComponent<CubeComponentProperties> {
constructor() {
super()

this.properties = {
size: 50,
}
}

getHeight() {
const cubeSize = this.properties.size
const cubeHeight = 3 * cubeSize
return cubeHeight
}

generateGeometry() {
const cubeSize = this.properties.size
const cubeHeight = this.getHeight()

const geometryGroup = new SKYCAD.GeometryGroup()

const model = new SKYCAD.ParametricModel()
const sketch = SKYCAD.generateRectangleSketch(0, 0, cubeSize, cubeSize)
const plane = new SKYCAD.Plane()
model.addExtrude(sketch, plane, cubeHeight)
geometryGroup.addGeometry(model)

return geometryGroup
}
}

B. Properties​

As you have probably seen in the template, there are 3 properties by default. Naturally, these properties can be changed or removed. In order to add a new one, let's say cornerRadius for the extrusion:

  • under CubeComponentProperties, add cornerRadius: number.
  • within the constructor, add cornerRadius: 5 as the default value, making it ready to use.
  • then in e.g. generateGeometry(), assign the property to a variable like const cornerRadius = this.properties.cornerRadius.
  • and simply use it e.g. as the radius of the sketch used for the extrusion in this case as in the example below:
tip

Although you can retrieved any property with this.properties, you can also use:

  • getProperty() by giving the property as string (e.g. const cubeWidth = this.getProperty('width')).
  • getProperties() which gives the whole object (e.g. const { width, depth } = this.getProperties()).

Component Into Components​

It is possible and recommended in most cases, that instead of having one that does too much, it's best to split its logic into separate components, specially when these are complex on their own. You can do this in two ways:

  • a single package with multiple components.
  • a main package, containing multiple packages with one or more components each.

Either way, you can always switch to the other when you see fit in favour of cleaner code. To show the difference we take a staircase made of side railing and steps as product like the following:

A. Single package​

Using a single package is recommended when the product is simple enough that everything can be built within the same package.

Let's start by building the step and railing components with simple geometries. Try to build the geometry yourself first and compare with the solutions below. Remember to keep all component blocks within the same package.

Component Step:
note

Notice STEP_WIDTH is left outside since it will be also used later in the upcoming component Staircase.

const STEP_WIDTH = 200

interface StepProperties {
depth: number
}
export class Step extends STUDIO.BaseComponent<StepProperties> {
constructor() {
super()

this.properties = {
depth: 500,
}
}

generateGeometry() {
const { depth } = this.getProperties()
const HEIGHT = 20

const model = new SKYCAD.ParametricModel()
const baseSketch = SKYCAD.generateRectangleSketch(0, 0, STEP_WIDTH, depth)
const plane = new SKYCAD.Plane()
model.addExtrude(baseSketch, plane, HEIGHT)

const group = new SKYCAD.GeometryGroup()
group.addGeometry(model, {
materials: [new SKYCAD.Material({ color: new SKYCAD.RgbColor(196, 164, 132) })],
})

return group
}
}
Component Railing:
interface RailingProperties {
height: number
width: number
railingHeight: number
}
export class Railing extends STUDIO.BaseComponent<RailingProperties> {
constructor() {
super()
this.properties = {
height: 3000,
width: 5000,
railingHeight: 1200,
}
}
generateGeometry() {
const { height, width, railingHeight } = this.properties
const group = new SKYCAD.GeometryGroup()

const model = new SKYCAD.ParametricModel()

const sketch = new SKYCAD.Sketch()
sketch.moveTo(0, 0)
sketch.lineTo(0, railingHeight)
sketch.lineTo(width, height + railingHeight)
sketch.lineTo(width, height)
sketch.lineToId(0)

const plane = new SKYCAD.Plane(0, -1, 0)

const GLASS_THICKNESS = 20
model.addExtrude(sketch, plane, GLASS_THICKNESS)

group.addGeometry(model, {
materials: [
new SKYCAD.Material({
color: new SKYCAD.RgbColor(173, 216, 230),
opacity: 0.5,
}),
],
})
return group
}
}

As for the staircase we would need to import these components into a new one somehow. For this, the componentHandler of the component is crucial to use

tip

Good to know about the componentHandler API:

  • this.componentHandler.add() adds a component as subcomponent.
  • this.componentHandler.clear() removes a specific component type (i.e. step, railing, etc) previously added.
  • this.componentHandler.clearAll() removes all components previously added.
  • this.componentHandler.generateAllGeometry() creates the geometry of all the components added, as long as their component block generateGeometry() is used as visualization model.

Therefore the component block update() becomes quite relevant since it's automatically triggered for every change in their properties. Here it's the place to add/update/remove the subcomponents.

Component Staircase:
interface StaircaseProperties {
width: number
height: number
depth: number
railingHeight: number
}
export class Staircase extends STUDIO.BaseComponent<StaircaseProperties> {
constructor() {
super()

this.properties = {
width: 5000,
height: 3000,
depth: 500,
railingHeight: 1200,
}

this.update()
}

update() {
const { width, depth, height, railingHeight } = this.getProperties()

this.componentHandler.clear(Railing)
const railingComponent = new Railing()
railingComponent.setProperties({ height, railingHeight, width })
this.componentHandler.add(railingComponent)

this.componentHandler.clear(Step)
const stepComponent = new Step()
stepComponent.setProperties({ depth })

const MAX_STEP_RISE = 220

const nrSteps = Math.max(Math.floor(height / MAX_STEP_RISE), Math.floor(width / STEP_WIDTH))

const stepRise = height / nrSteps // z-space between steps
const stepRun = width / nrSteps // x-space between steps

for (let i = 0; i < nrSteps; i++) {
this.componentHandler.add(stepComponent, {
position: new SKYMATH.Vector3D(i * stepRun, 0, (i + 1) * stepRise),
})
}
}

generateGeometry() {
const group = this.componentHandler.generateAllGeometry()
return group
}
}

Finally the single package ASSEMBLY would look like this:

Doublecheck the full code of the package ASSEMBLY with all 3 components together:
const STEP_WIDTH = 200

interface StepProperties {
depth: number
}
export class Step extends STUDIO.BaseComponent<StepProperties> {
constructor() {
super()

this.properties = {
depth: 500,
}
}

generateGeometry() {
const { depth } = this.getProperties()
const HEIGHT = 20

const model = new SKYCAD.ParametricModel()
const baseSketch = SKYCAD.generateRectangleSketch(0, 0, STEP_WIDTH, depth)
const plane = new SKYCAD.Plane()
model.addExtrude(baseSketch, plane, HEIGHT)

const geometryGroup = new SKYCAD.GeometryGroup()
geometryGroup.addGeometry(model, {
materials: [new SKYCAD.Material({ color: new SKYCAD.RgbColor(196, 164, 132) })],
})

return geometryGroup
}
}

interface RailingProperties {
height: number
width: number
railingHeight: number
}
export class Railing extends STUDIO.BaseComponent<RailingProperties> {
constructor() {
super()
this.properties = {
height: 3000,
width: 5000,
railingHeight: 1200,
}
}
generateGeometry() {
const { height, width, railingHeight } = this.properties
const geometryGroup = new SKYCAD.GeometryGroup()

const model = new SKYCAD.ParametricModel()

const sketch = new SKYCAD.Sketch()
sketch.moveTo(0, 0)
sketch.lineTo(0, railingHeight)
sketch.lineTo(width, height + railingHeight)
sketch.lineTo(width, height)
sketch.lineToId(0)

const plane = new SKYCAD.Plane(0, -1, 0)

const GLASS_THICKNESS = 20
model.addExtrude(sketch, plane, GLASS_THICKNESS)

geometryGroup.addGeometry(model, {
materials: [
new SKYCAD.Material({
color: new SKYCAD.RgbColor(173, 216, 230),
opacity: 0.5,
}),
],
})
return geometryGroup
}
}

interface StaircaseProperties {
width: number
height: number
depth: number
railingHeight: number
}
export class Staircase extends STUDIO.BaseComponent<StaircaseProperties> {
constructor() {
super()

this.properties = {
width: 5000,
height: 3000,
depth: 500,
railingHeight: 1200,
}

this.update()
}

update() {
const { width, depth, height, railingHeight } = this.getProperties()
this.componentHandler.clearAll()

const railingComponent = new Railing()
railingComponent.setProperties({ height, railingHeight, width })
this.componentHandler.add(railingComponent)

// StairSteps
this.componentHandler.clear(Step)
const stepComponent = new Step()
stepComponent.setProperties({ depth })

const MAX_STEP_RISE = 220

const nrSteps = Math.max(Math.floor(height / MAX_STEP_RISE), Math.floor(width / STEP_WIDTH))

const stepRise = height / nrSteps // z-space between steps
const stepRun = width / nrSteps // x-space between steps

for (let i = 0; i < nrSteps; i++) {
this.componentHandler.add(stepComponent, {
position: new SKYMATH.Vector3D(i * stepRun, 0, (i + 1) * stepRise),
})
}
}

generateGeometry() {
const geometryGroup = this.componentHandler.generateAllGeometry()
return geometryGroup
}
}

B. Multiple packages​

Using multiple packages is recommended when the product is complex enough that a package with at least one component helps with its development. We will take the same staircase as product example to see the difference with the single-package approach described before.

  • STAIRSSTEP package:

    • create component block with name Step and package name STAIRSSTEP.
    • copy the code of the component from the previous section.
    • add export before the constant STEP_WIDTH to be able to use it later in other packages.
  • RAILING package:

    • create component block with name Railing and package name RAILING.
    • copy the code of the component from the previous section.
  • STAIRCASE package:

    • create component block with name Staircase and package name STAIRCASE.
    • copy the code of the component from the previous section.
    • next to Save & Run, click ... > Edit imports and check both STAIRSSTEP and RAILING.
    • replace Railing with RAILING.Railing, Step with STAIRSSTEP.Step, and STEP_WIDTH with STAIRSSTEP.STEP_WIDTH.
tip

When the product configurator gets complex, especially when using multiple packages with more than one export per package, don't forget to check the interactive Structure Graph that shows the dependencies (i.e. what's used where):

  • go into the relevant package/block, click its ... > Show Structure Graph.

C. Instances​

When using this.componentHandler.add, a copy or instance of the component is added, which is convenient to reuse due to the built-in API it brings with it.

C1. Adding, updating and removing​

For the case that a subcomponent (e.g. railing) that always exists within another component (e.g. staircase), it's best to add it in its constructor since this part is only triggered once only when created.

export class Staircase extends STUDIO.BaseComponent<StaircaseProperties> {
constructor() {
super()

this.properties = {
width: 5000,
height: 3000,
depth: 500,
railingHeight: 1200,
}

const railingComponent = new RAILING.Railing()
this.componentHandler.add(railingComponent)

this.update()
}

update() {
const { width, depth, height, railingHeight } = this.getProperties()

const railingComponents = this.componentHandler.getComponents(RAILING.Railing) // gives list with 1 component here
const railingComponent = railingComponents[0]!
railingComponent.setProperties({ height, railingHeight, width })

// ...other logic other subcomponents (e.g. steps)
}

generateGeometry() {
const geometryGroup = this.componentHandler.generateAllGeometry()
return geometryGroup
}
}
tip

Thanks to the componentHandler, you can also:

  • get specific components and instances by class type:

    const allComponents = this.componentHandler.getAllComponents() // railing and step components
    const railingComponents = this.componentHandler.getComponents(RAILING.Railing)

    const allInstances = this.componentHandler.getAllInstances() // railing and step instances
    const railingInstances = this.componentHandler.getInstances(RAILING.Railing)
  • remove (all) components/ instances:

    this.componentHandler.clearAll() // removes all instances
    this.componentHandler.clear(RAILING.Railing) // removes all railing instances

    this.componentHandler.removeComponent(railingComponentToRemove)
    this.componentHandler.removeInstances([railingInstance1, railingInstance2])

C2. Repositioning​

Position and rotation only lies within the instance, i.e. the component doesn't know anything about its position in the visualization. Let's take an example of a staircase with a railing on its left side:

export class Staircase extends STUDIO.BaseComponent<StaircaseProperties> {
constructor() {
super()

this.properties = {
width: 5000,
height: 3000,
depth: 500,
railingHeight: 1200,
}

const railingComponent = new RAILING.Railing()
this.componentHandler.add(railingComponent)

this.update()
}

update() {
const { width, depth, height, railingHeight } = this.getProperties()

const railings = this.componentHandler.getInstances(RAILING.Railing)
const railing = railings[0]!
railing.setPosition(new SKYMATH.Vector3D(0, depth, 0))
// railing.setRotation(new SKYMATH.Vector3D(0, 0, 0))

// ...other logic other subcomponents (e.g. steps)
}

generateGeometry() {
const geometryGroup = this.componentHandler.generateAllGeometry()
return geometryGroup
}
}
tip

You can also fetched the position and rotation of an instance like:

const railings = this.componentHandler.getInstances(RAILING.Railing)
const railing = railings[0]!
const leftRailingPosition = railing.getPosition()
const leftRailingRotation = railing.getRotation()

C3. Tags​

Tagging instances allow you to add some distinction between instances, despite perhaps sharing the same component:

Let's take an example of a staircase with a railing with 2 railings, one at each side:

export class Staircase extends STUDIO.BaseComponent<StaircaseProperties> {
constructor() {
super()

this.properties = {
width: 5000,
height: 3000,
depth: 500,
railingHeight: 1200,
}

const railingComponent = new RAILING.Railing()
this.componentHandler.add(railingComponent, { withTags: ['left'] })
this.componentHandler.add(railingComponent, { withTags: ['right'] })

this.update()
}

update() {
const { width, depth, height, railingHeight } = this.getProperties()

const leftRailings = this.componentHandler.getInstances(RAILING.Railing, { withTags: ['left'] })
const leftRailing = leftRailings[0]!
leftRailing.setPosition(new SKYMATH.Vector3D(0, 0, 0))

const rightRailings = this.componentHandler.getInstances(RAILING.Railing, { withTags: ['right'] })
const rightRailing = rightRailings[0]!
rightRailing.setPosition(new SKYMATH.Vector3D(0, depth, 0))

// ...other logic other subcomponents (e.g. steps)
}

generateGeometry() {
const geometryGroup = this.componentHandler.generateAllGeometry()
return geometryGroup
}
}
tip

You can also:

  • remove a specific instance if you tagged them accordingly:
this.componentHandler.clear(RAILING.Railing, { withTags: ['left-side'] }) // removes all railing instances with specified tags
  • have control of the instance tags like:
firstRailingInstance.addTag('glass') // adds new tag
const hasGlassTag = firstRailingInstance.hasTag('glass') // returns true or false
const instanceTags = firstRailingInstance.getTags() // returns tags, e.g. ['left', 'glass']