Scene output scripts

Create scripts that output native CADit shapes like rectangles, circles, stars, and polygons with full 3D features including twist, sculpt, and revolve.

Scene output scripts

Scripts in Code Mode normally produce a single 3D mesh (a Manifold or GLTFNode). Scene output scripts instead produce native CADit shapes — rectangles, circles, stars, polygons, and more. Each shape appears as a separate object in the scene that you can select, move, and edit individually.

This is useful when your script needs to:

  • Generate multiple independent objects at once
  • Produce shapes that use CADit-specific features like twist, sculpt, and revolve
  • Take existing 2D shapes as input and output modified versions
  • Create layouts, patterns, or arrangements of standard shapes

Basic structure

A scene output script uses createSceneOutput to return an array of shapes:

import { defineParams, createSceneOutput, rect } from '@cadit-app/script-params';

export default defineParams({
  params: {
    width: { type: 'number', default: 30 },
    height: { type: 'number', default: 10 },
  },
  main: (p) => createSceneOutput([
    rect(p.width, 20, { height: p.height }),
  ]),
});

The key difference from a normal Code Mode script: instead of returning a Manifold object, you return createSceneOutput([...shapes]).

Shape helper functions

Import these from @cadit-app/script-params to create shapes:

FunctionArgumentsDescription
rect(width, height, options?)width, height in mmRectangle
circle(radius, options?)radius in mmCircle
triangle(width, height, options?)base width, height in mmTriangle
star(outerRadius, innerRadius, rays, options?)outer/inner radius, number of raysStar
regularPolygon(numPoints, radius, options?)number of sides, radiusRegular polygon (hexagon, pentagon, etc.)
heart(width, height, options?)width, height in mmHeart shape
polygon(points, options?)array of {x, y} pointsCustom polygon

Every helper accepts an optional options object with the shape properties described below.

Note: a text() helper exists in the API but text rendering in scene outputs is not yet implemented. Use the visual Text tool in the drawing toolbar instead.

Shape properties

All shapes support these optional properties:

Position and rotation

rect(20, 20, {
  position: { x: 30, y: -10 },  // Center position in mm
  rotation: Math.PI / 4,         // Rotation in radians
})

Color

Color can be specified in three formats:

circle(10, { color: 0xff6b35 })        // Hex number
circle(10, { color: '#32cd32' })       // CSS hex string
circle(10, { color: [255, 107, 53] })  // RGB array [0-255]

If omitted, the default color is used.

3D extrusion height

rect(20, 20, { height: 15 })  // Extrude 15mm tall
rect(20, 20, { z: 5 })        // Start 5mm above the ground

Twist

Twist rotates the shape along its extrusion height:

rect(20, 20, {
  height: 40,
  twist: 0.3,  // Twist in radians
})

Sculpt

Sculpt varies the cross-section scale along the extrusion height. Each point has a pos (0 = bottom, 1 = top) and a scale factor:

circle(15, {
  height: 30,
  sculpt: [
    { pos: 0, scale: 0.5 },    // Narrow at bottom
    { pos: 0.5, scale: 1.2 },  // Wide in the middle
    { pos: 1, scale: 0.5 },    // Narrow at top
  ],
})

Revolve

Revolve spins a 2D profile around an axis to create shapes of revolution like vases, donuts, and bowls:

circle(8, {
  extrusionMode: 'revolve',
  revolveAngle: 360,           // Degrees (1-360)
  revolveAxisOffset: 10,       // Distance from profile to axis
  revolveSegments: 32,         // Smoothness
})
PropertyDefaultDescription
extrusionMode'linear'Set to 'revolve' for revolution
revolveAngle360Angle of revolution in degrees
revolveAxisOffset0Distance from the shape's left edge to the axis
revolveSegments32Number of segments (higher = smoother)

Fill

rect(20, 20, { fill: false })  // Outline only (no fill)

Custom polygons with bezier curves

For organic shapes, use polygon() with bezier control points:

import { polygon } from '@cadit-app/script-params';
import type { BezierPoint2D } from '@cadit-app/script-params';

const points: BezierPoint2D[] = [
  { x: 0, y: 20, handleIn: { x: -8, y: -4 }, handleOut: { x: 8, y: -4 } },
  { x: 15, y: 0, handleIn: { x: 0, y: 8 }, handleOut: { x: 0, y: -8 } },
  { x: 0, y: -20, handleIn: { x: 8, y: 4 }, handleOut: { x: -8, y: 4 } },
  { x: -15, y: 0, handleIn: { x: 0, y: -8 }, handleOut: { x: 0, y: 8 } },
];

polygon(points, { height: 10 })

Each point has x and y coordinates plus optional handleIn and handleOut offsets (relative to the point) that define bezier curves between points.

Polygons also support holes:

polygon(outerPoints, {
  holes: [holePoints],  // Array of point arrays
  height: 10,
})

Taking shapes as input

Scripts can accept existing 2D shapes from the canvas using the shape2d parameter type. The selected shape's path data becomes available for processing.

import { defineParams, createSceneOutput, polygon } from '@cadit-app/script-params';
import type { Shape2dValue, PathPoint2D } from '@cadit-app/script-params';

export default defineParams({
  params: {
    inputShape: { type: 'shape2d', label: 'Select a shape' },
    height: { type: 'number', default: 5 },
  },
  main: (p) => {
    const shape = p.inputShape as Shape2dValue | null;

    if (!shape?.shapeData?.doodlePaths?.length) {
      return createSceneOutput([]);  // No shape selected
    }

    // Process each path from the input shape
    const outputs = shape.shapeData.doodlePaths.map(path => {
      const points: PathPoint2D[] = path.points.map(pt => {
        if ('point' in pt) {
          // PathSegment format with handles
          const seg = pt as { point: { x: number; y: number }; handleIn?: { x: number; y: number }; handleOut?: { x: number; y: number } };
          return { x: seg.point.x, y: seg.point.y, handleIn: seg.handleIn, handleOut: seg.handleOut };
        }
        return { x: (pt as any).x, y: (pt as any).y };
      });

      return polygon(points, { height: p.height });
    });

    return createSceneOutput(outputs);
  },
});

The shape data includes:

  • shapeData.doodlePaths — array of paths, each with points and optional holes
  • Each point has x, y coordinates and optional bezier handleIn/handleOut

You can also use the samplePathTo2D utility from @cadit-app/script-params to convert bezier paths into sampled [x, y] arrays, which is useful when passing paths to Manifold's CrossSection for operations like offset.

Session behavior

Scene output scripts follow a session lifecycle:

  1. Running — Each run replaces the previous output shapes
  2. Editing parameters — Adjusting parameter values re-runs the script, replacing outputs
  3. Closing — When you close Code Mode or Maker Mode, the session finalizes and the output shapes become regular scene objects

After finalization, the shapes are independent objects. The script is saved to storage and can be reopened later.


Example scripts

The following complete scripts demonstrate different scene output features. You can paste any of these into Code Mode to try them.

Mixed shapes with colors

Creates a rectangle, circle, and triangle side by side with different colors.

import { defineParams, createSceneOutput, rect, circle, triangle } from '@cadit-app/script-params';
import type { ShapeDefinition } from '@cadit-app/script-params';

export default defineParams({
  params: {
    spread: { type: 'float', default: 40, min: 20, max: 100, caption: 'Spread Distance' },
    height: { type: 'float', default: 10, min: 1, max: 30, caption: '3D Height' },
  },
  main: (p) => {
    const objects: ShapeDefinition[] = [
      // Rectangle with hex number color
      rect(25, 20, {
        height: p.height,
        position: { x: -p.spread, y: 0 },
        color: 0xff6b35,
      }),
      // Circle with CSS hex string color
      circle(12, {
        height: p.height,
        position: { x: 0, y: 0 },
        color: '#32cd32',
      }),
      // Triangle with no color (uses default)
      triangle(26, 25, {
        height: p.height,
        position: { x: p.spread, y: 0 },
      }),
    ];

    return createSceneOutput(objects);
  },
});

Parametric grid

Generates a grid of rectangles with configurable rows, columns, and spacing.

import { defineParams, createSceneOutput, rect } from '@cadit-app/script-params';
import type { RectInput } from '@cadit-app/script-params';

export default defineParams({
  params: {
    rows: { type: 'int', default: 3, min: 1, max: 10, caption: 'Rows' },
    cols: { type: 'int', default: 3, min: 1, max: 10, caption: 'Columns' },
    size: { type: 'float', default: 15, min: 5, max: 50, caption: 'Rectangle Size' },
    spacing: { type: 'float', default: 25, min: 10, max: 100, caption: 'Spacing' },
    height: { type: 'float', default: 10, min: 1, max: 50, caption: '3D Height' },
  },
  main: (p) => {
    const objects: RectInput[] = [];
    const totalWidth = (p.cols - 1) * p.spacing;
    const totalHeight = (p.rows - 1) * p.spacing;
    const offsetX = -totalWidth / 2;
    const offsetY = -totalHeight / 2;

    for (let row = 0; row < p.rows; row++) {
      for (let col = 0; col < p.cols; col++) {
        objects.push(
          rect(p.size, p.size, {
            height: p.height,
            position: {
              x: offsetX + col * p.spacing,
              y: offsetY + row * p.spacing,
            },
          })
        );
      }
    }

    return createSceneOutput(objects);
  },
});

Twisted columns

Demonstrates the twist property on different shape types.

import { defineParams, createSceneOutput, rect, star, regularPolygon } from '@cadit-app/script-params';
import type { ShapeDefinition } from '@cadit-app/script-params';

export default defineParams({
  params: {
    height: { type: 'float', default: 40, min: 10, max: 80, caption: 'Height' },
    twistAmount: { type: 'float', default: 0.3, min: -0.5, max: 0.5, caption: 'Twist' },
    spread: { type: 'float', default: 50, min: 30, max: 100, caption: 'Spacing' },
  },
  main: (p) => {
    const objects: ShapeDefinition[] = [
      // Twisted square column
      rect(20, 20, {
        height: p.height,
        twist: p.twistAmount,
        position: { x: -p.spread, y: 0 },
        color: 0xe74c3c,
      }),
      // Twisted star
      star(18, 8, 5, {
        height: p.height,
        twist: p.twistAmount,
        position: { x: 0, y: 0 },
        color: 0xf39c12,
      }),
      // Twisted hexagon
      regularPolygon(6, 15, {
        height: p.height,
        twist: p.twistAmount,
        position: { x: p.spread, y: 0 },
        color: 0x3498db,
      }),
    ];

    return createSceneOutput(objects);
  },
});

Sculpt profiles

Uses sculpt to create organic shapes: a bulging column, a tapered cone, and a vase form.

import { defineParams, createSceneOutput, rect, circle, star } from '@cadit-app/script-params';
import type { ShapeDefinition, SculptPoint } from '@cadit-app/script-params';

export default defineParams({
  params: {
    height: { type: 'float', default: 30, min: 10, max: 80, caption: '3D Height' },
    twist: { type: 'float', default: 1, min: 0, max: 4, caption: 'Twist (rotations)' },
    taperAmount: { type: 'float', default: 0.3, min: 0, max: 1, caption: 'Taper Amount' },
    bulgePos: { type: 'float', default: 0.5, min: 0.1, max: 0.9, caption: 'Bulge Position' },
    bulgeScale: { type: 'float', default: 1.5, min: 1, max: 2.5, caption: 'Bulge Scale' },
  },
  main: (p) => {
    const sculptBulge: SculptPoint[] = [
      { pos: 0, scale: 1 - p.taperAmount },
      { pos: p.bulgePos, scale: p.bulgeScale },
      { pos: 1, scale: 1 },
    ];

    const sculptTaper: SculptPoint[] = [
      { pos: 0, scale: 0.3 },
      { pos: 1, scale: 1 },
    ];

    const sculptVase: SculptPoint[] = [
      { pos: 0, scale: 0.5 },
      { pos: 0.3, scale: 1 },
      { pos: 0.7, scale: 1 },
      { pos: 1, scale: 0.5 },
    ];

    const objects: ShapeDefinition[] = [
      // Rectangle with twist and bulge
      rect(20, 20, {
        height: p.height,
        twist: p.twist * Math.PI * 2,
        sculpt: sculptBulge,
        position: { x: -50, y: 0 },
      }),
      // Circle with taper (cone-like)
      circle(15, {
        height: p.height,
        sculpt: sculptTaper,
        position: { x: 0, y: 0 },
      }),
      // Star with vase shape and twist
      star(18, 8, 5, {
        height: p.height,
        twist: p.twist * Math.PI,
        sculpt: sculptVase,
        position: { x: 50, y: 0 },
      }),
    ];

    return createSceneOutput(objects);
  },
});

Revolved shapes

Creates shapes of revolution: a torus, an arch, and an ornamental vase.

import { defineParams, createSceneOutput, rect, circle, star } from '@cadit-app/script-params';
import type { ShapeDefinition, SculptPoint } from '@cadit-app/script-params';

export default defineParams({
  params: {
    angle: { type: 'float', default: 360, min: 15, max: 360, caption: 'Revolve Angle' },
    segments: { type: 'int', default: 32, min: 8, max: 128, caption: 'Segments' },
    axisOffset: { type: 'float', default: 5, caption: 'Axis Offset' },
  },
  main: (p) => {
    const vaseSculpt: SculptPoint[] = [
      { pos: 0, scale: 0.6 },
      { pos: 0.35, scale: 1.2 },
      { pos: 0.7, scale: 0.9 },
      { pos: 1, scale: 0.5 },
    ];

    const objects: ShapeDefinition[] = [
      // Torus from a revolved circle
      circle(6, {
        position: { x: -60, y: 0 },
        height: 2,
        extrusionMode: 'revolve',
        revolveAngle: p.angle,
        revolveSegments: p.segments,
        revolveAxisOffset: p.axisOffset,
      }),
      // Arch from a partial revolve of a rectangle
      rect(8, 20, {
        position: { x: 0, y: 0 },
        height: 2,
        extrusionMode: 'revolve',
        revolveAngle: 180,
        revolveSegments: p.segments,
      }),
      // Ornamental vase from a revolved star with sculpt
      star(12, 6, 5, {
        position: { x: 60, y: 0 },
        height: 2,
        extrusionMode: 'revolve',
        revolveAngle: p.angle,
        revolveSegments: p.segments,
        revolveAxisOffset: p.axisOffset,
        sculpt: vaseSculpt,
      }),
    ];

    return createSceneOutput(objects);
  },
});

Bezier curve shapes

Creates smooth organic shapes using bezier control points.

import { defineParams, createSceneOutput, polygon } from '@cadit-app/script-params';
import type { ShapeDefinition, BezierPoint2D } from '@cadit-app/script-params';

export default defineParams({
  params: {
    size: { type: 'float', default: 40, min: 20, max: 80, caption: 'Size' },
    height: { type: 'float', default: 15, min: 5, max: 40, caption: '3D Height' },
    smoothness: { type: 'float', default: 0.5, min: 0, max: 1, caption: 'Smoothness' },
  },
  main: (p) => {
    const s = p.size;
    const h = p.smoothness * s * 0.4;

    // Heart shape using bezier curves
    const heartPoints: BezierPoint2D[] = [
      { x: 0, y: s * 0.6, handleIn: { x: -h, y: -h * 0.5 }, handleOut: { x: h, y: -h * 0.5 } },
      { x: s * 0.5, y: 0, handleIn: { x: 0, y: h }, handleOut: { x: 0, y: -h } },
      { x: s * 0.25, y: -s * 0.3, handleIn: { x: h * 0.5, y: 0 }, handleOut: { x: -h * 0.5, y: 0 } },
      { x: 0, y: -s * 0.15, handleIn: { x: h * 0.3, y: -h * 0.3 }, handleOut: { x: -h * 0.3, y: -h * 0.3 } },
      { x: -s * 0.25, y: -s * 0.3, handleIn: { x: h * 0.5, y: 0 }, handleOut: { x: -h * 0.5, y: 0 } },
      { x: -s * 0.5, y: 0, handleIn: { x: 0, y: -h }, handleOut: { x: 0, y: h } },
    ];

    // Leaf shape using bezier curves
    const leafPoints: BezierPoint2D[] = [
      { x: 0, y: s * 0.5, handleIn: { x: -h * 0.3, y: -h * 0.5 }, handleOut: { x: h * 0.3, y: -h * 0.5 } },
      { x: s * 0.3, y: 0, handleIn: { x: 0, y: h * 0.8 }, handleOut: { x: 0, y: -h * 0.8 } },
      { x: 0, y: -s * 0.5, handleIn: { x: h * 0.3, y: h * 0.5 }, handleOut: { x: -h * 0.3, y: h * 0.5 } },
      { x: -s * 0.3, y: 0, handleIn: { x: 0, y: -h * 0.8 }, handleOut: { x: 0, y: h * 0.8 } },
    ];

    const objects: ShapeDefinition[] = [
      polygon(heartPoints, {
        height: p.height,
        position: { x: -s, y: 0 },
        color: [220, 60, 80],
      }),
      polygon(leafPoints, {
        height: p.height,
        position: { x: s, y: 0 },
        color: [60, 180, 80],
      }),
    ];

    return createSceneOutput(objects);
  },
});

Shape input: grid duplicator

Takes a 2D shape from the canvas and creates a grid of copies as native scene objects.

import { defineParams, createSceneOutput, polygon } from '@cadit-app/script-params';
import type { Shape2dValue, PathPoint, Point2D, PolygonInput, PathPoint2D, BezierPoint2D } from '@cadit-app/script-params';

function convertPathPoint(p: PathPoint): PathPoint2D {
  if ('point' in p && typeof p.point === 'object') {
    const segment = p as { point: Point2D; handleIn?: Point2D; handleOut?: Point2D };
    const result: BezierPoint2D = { x: segment.point.x, y: segment.point.y };
    if (segment.handleIn) result.handleIn = { x: segment.handleIn.x, y: segment.handleIn.y };
    if (segment.handleOut) result.handleOut = { x: segment.handleOut.x, y: segment.handleOut.y };
    return result;
  }
  return { x: (p as Point2D).x, y: (p as Point2D).y };
}

function translatePoints(points: PathPoint2D[], dx: number, dy: number): PathPoint2D[] {
  return points.map(p => {
    const result: PathPoint2D = { x: p.x + dx, y: p.y + dy };
    if ('handleIn' in p && p.handleIn) (result as BezierPoint2D).handleIn = p.handleIn;
    if ('handleOut' in p && p.handleOut) (result as BezierPoint2D).handleOut = p.handleOut;
    return result;
  });
}

export default defineParams({
  params: {
    inputShape: { type: 'shape2d' as const, label: 'Input Shape', default: null },
    rows: { type: 'int', default: 3, min: 1, max: 10, caption: 'Rows' },
    cols: { type: 'int', default: 3, min: 1, max: 10, caption: 'Columns' },
    spacingX: { type: 'float', default: 30, min: 5, max: 200, caption: 'X Spacing' },
    spacingY: { type: 'float', default: 30, min: 5, max: 200, caption: 'Y Spacing' },
    height: { type: 'float', default: 5, min: 1, max: 50, caption: '3D Height' },
  },
  main: (p) => {
    const shapeInput = p.inputShape as Shape2dValue | null;
    if (!shapeInput?.shapeData?.doodlePaths?.length) {
      return createSceneOutput([]);
    }

    const shapes: PolygonInput[] = [];
    const baseOffsetX = -((p.cols - 1) * p.spacingX) / 2;
    const baseOffsetY = -((p.rows - 1) * p.spacingY) / 2;

    for (let row = 0; row < p.rows; row++) {
      for (let col = 0; col < p.cols; col++) {
        const dx = baseOffsetX + col * p.spacingX;
        const dy = baseOffsetY + row * p.spacingY;

        for (const doodlePath of shapeInput.shapeData.doodlePaths) {
          const convertedPoints = doodlePath.points.map(convertPathPoint);
          const translatedPoints = translatePoints(convertedPoints, dx, dy);

          let translatedHoles: PathPoint2D[][] | undefined;
          if (doodlePath.holes?.length) {
            translatedHoles = doodlePath.holes.map(hole =>
              translatePoints(hole.map(convertPathPoint), dx, dy)
            );
          }

          shapes.push(polygon(translatedPoints, {
            height: p.height,
            holes: translatedHoles,
          }));
        }
      }
    }

    return createSceneOutput(shapes);
  },
});

Shape input: offset tool

Takes a 2D shape and creates an expanded or contracted version using Manifold's CrossSection offset.

import { CrossSection } from 'manifold-3d/manifoldCAD';
import { defineParams, createSceneOutput, polygon, samplePathTo2D } from '@cadit-app/script-params';
import type { Shape2dValue, PathPoint, PolygonInput, PathPoint2D } from '@cadit-app/script-params';

type JoinType = 'Round' | 'Square' | 'Miter';

function pathToCrossSectionPoints(points: PathPoint[]): [number, number][] {
  const sampled = samplePathTo2D(points);
  return sampled.map(([x, y]) => [x, -y]);  // Flip Y for CrossSection
}

function isClockwise(points: [number, number][]): boolean {
  let area = 0;
  for (let i = 0; i < points.length; i++) {
    const j = (i + 1) % points.length;
    area += points[i][0] * points[j][1] - points[j][0] * points[i][1];
  }
  return area < 0;
}

function ensureCCW(points: [number, number][]): [number, number][] {
  return isClockwise(points) ? [...points].reverse() : points;
}

export default defineParams({
  params: {
    inputShape: { type: 'shape2d' as const, label: 'Input Shape', default: null },
    offset: { type: 'float', default: 5, min: -50, max: 50, caption: 'Offset Amount', step: 0.5 },
    joinType: { type: 'choice' as const, default: 'Round', options: ['Round', 'Square', 'Miter'], caption: 'Corner Type' },
    height: { type: 'float', default: 5, min: 1, max: 50, caption: '3D Height' },
    keepOriginal: { type: 'switch', default: false, caption: 'Show Original Shape' },
  },
  main: (p) => {
    const shapeInput = p.inputShape as Shape2dValue | null;
    if (!shapeInput?.shapeData?.doodlePaths?.length) {
      return createSceneOutput([]);
    }

    const shapes: PolygonInput[] = [];

    for (const doodlePath of shapeInput.shapeData.doodlePaths) {
      const points = pathToCrossSectionPoints(doodlePath.points);
      if (points.length < 3) continue;

      const oriented = ensureCCW(points);
      let crossSection;

      if (doodlePath.holes?.length) {
        const contours: [number, number][][] = [oriented];
        for (const hole of doodlePath.holes) {
          const hp = pathToCrossSectionPoints(hole);
          contours.push(isClockwise(hp) ? hp : [...hp].reverse());
        }
        crossSection = new CrossSection(contours);
      } else {
        crossSection = new CrossSection([oriented]);
      }

      const result = crossSection.offset(p.offset, p.joinType as JoinType, 2, 16);
      const polys = result.toPolygons();

      for (const poly of polys) {
        const pathPoints: PathPoint2D[] = (poly as any[]).map((pt: any) =>
          Array.isArray(pt) ? { x: pt[0], y: pt[1] } : { x: pt.x, y: pt.y }
        );
        shapes.push(polygon(pathPoints, { height: p.height, color: [100, 150, 255] }));
      }

      if (p.keepOriginal) {
        const original = samplePathTo2D(doodlePath.points);
        shapes.push(polygon(
          original.map(([x, y]) => ({ x, y })),
          { height: p.height * 0.5, color: [255, 200, 100] }
        ));
      }
    }

    return createSceneOutput(shapes);
  },
});

Radial pattern generator

Arranges shapes in a circular pattern with configurable count and radius.

import { defineParams, createSceneOutput, rect, circle, star, triangle, heart } from '@cadit-app/script-params';
import type { ShapeDefinition } from '@cadit-app/script-params';

export default defineParams({
  params: {
    shapeType: {
      type: 'choice',
      default: 'star',
      options: ['rect', 'circle', 'star', 'triangle', 'heart'],
      label: 'Shape type',
    },
    count: { type: 'int', default: 8, min: 2, max: 24, label: 'Number of copies' },
    radius: { type: 'float', default: 40, min: 10, max: 100, label: 'Pattern radius' },
    shapeSize: { type: 'float', default: 10, min: 3, max: 30, label: 'Shape size' },
    height: { type: 'float', default: 8, min: 1, max: 40, label: '3D Height' },
    rotateWithPattern: { type: 'switch', default: true, label: 'Rotate shapes to follow ring' },
    addCenter: { type: 'switch', default: true, label: 'Add center shape' },
  },
  main: (p) => {
    const objects: ShapeDefinition[] = [];

    function makeShape(x: number, y: number, rotation: number): ShapeDefinition {
      const opts = { height: p.height, position: { x, y }, rotation };
      switch (p.shapeType) {
        case 'rect': return rect(p.shapeSize, p.shapeSize, opts);
        case 'circle': return circle(p.shapeSize / 2, opts);
        case 'star': return star(p.shapeSize / 2, p.shapeSize / 4, 5, opts);
        case 'triangle': return triangle(p.shapeSize, p.shapeSize, opts);
        case 'heart': return heart(p.shapeSize, p.shapeSize, opts);
        default: return rect(p.shapeSize, p.shapeSize, opts);
      }
    }

    // Place shapes in a circle
    for (let i = 0; i < p.count; i++) {
      const angle = (i / p.count) * Math.PI * 2;
      const x = Math.cos(angle) * p.radius;
      const y = Math.sin(angle) * p.radius;
      const rotation = p.rotateWithPattern ? angle : 0;
      objects.push(makeShape(x, y, rotation));
    }

    // Optionally add a center shape
    if (p.addCenter) {
      objects.push(makeShape(0, 0, 0));
    }

    return createSceneOutput(objects);
  },
});

Honeycomb grid

Generates a hexagonal honeycomb pattern from circles with configurable size and gap.

import { defineParams, createSceneOutput, regularPolygon } from '@cadit-app/script-params';
import type { ShapeDefinition } from '@cadit-app/script-params';

export default defineParams({
  params: {
    rings: { type: 'int', default: 3, min: 1, max: 6, label: 'Number of rings' },
    cellRadius: { type: 'float', default: 8, min: 3, max: 20, label: 'Cell radius' },
    gap: { type: 'float', default: 2, min: 0.5, max: 8, label: 'Gap between cells' },
    height: { type: 'float', default: 5, min: 1, max: 20, label: '3D Height' },
  },
  main: (p) => {
    const objects: ShapeDefinition[] = [];
    const spacing = (p.cellRadius + p.gap / 2) * Math.sqrt(3);

    // Hex grid directions for axial coordinates
    const directions = [
      [1, 0], [0, 1], [-1, 1], [-1, 0], [0, -1], [1, -1],
    ];

    // Center cell
    objects.push(regularPolygon(6, p.cellRadius, {
      height: p.height,
      position: { x: 0, y: 0 },
      color: 0xf0c040,
    }));

    // Surrounding rings
    for (let ring = 1; ring <= p.rings; ring++) {
      let q = ring;
      let r = 0;
      for (let side = 0; side < 6; side++) {
        for (let step = 0; step < ring; step++) {
          const x = spacing * (q + r * 0.5);
          const y = spacing * (r * Math.sqrt(3) / 2);
          const shade = 0.5 + 0.5 * (1 - ring / (p.rings + 1));
          objects.push(regularPolygon(6, p.cellRadius, {
            height: p.height,
            position: { x, y },
            color: [
              Math.round(240 * shade),
              Math.round(192 * shade),
              Math.round(64 * shade),
            ],
          }));
          q += directions[side][0];
          r += directions[side][1];
        }
      }
    }

    return createSceneOutput(objects);
  },
});

Staircase generator

Generates a spiral or straight staircase from rectangles with configurable step dimensions.

import { defineParams, createSceneOutput, rect } from '@cadit-app/script-params';
import type { ShapeDefinition } from '@cadit-app/script-params';

export default defineParams({
  params: {
    steps: { type: 'int', default: 10, min: 2, max: 30, label: 'Number of steps' },
    stepWidth: { type: 'float', default: 20, min: 5, max: 50, label: 'Step width' },
    stepDepth: { type: 'float', default: 12, min: 5, max: 30, label: 'Step depth' },
    stepHeight: { type: 'float', default: 3, min: 1, max: 10, label: 'Step height' },
    spiral: { type: 'switch', default: false, label: 'Spiral staircase' },
    spiralRadius: { type: 'float', default: 15, min: 10, max: 80, label: 'Spiral radius' },
  },
  main: (p) => {
    const objects: ShapeDefinition[] = [];

    for (let i = 0; i < p.steps; i++) {
      let x: number, y: number, rotation: number;

      if (p.spiral) {
        const angle = (i / p.steps) * Math.PI * 2;
        x = Math.cos(angle) * p.spiralRadius;
        y = Math.sin(angle) * p.spiralRadius;
        rotation = angle;
      } else {
        x = 0;
        y = i * p.stepDepth;
        rotation = 0;
      }

      objects.push(rect(p.stepWidth, p.stepDepth, {
        height: p.stepHeight,
        z: i * p.stepHeight,
        position: { x, y },
        rotation,
        color: i % 2 === 0 ? 0xcccccc : 0x999999,
      }));
    }

    return createSceneOutput(objects);
  },
});

Tips

  • Return createSceneOutput([]) to produce no output (useful as a fallback when no shape is selected)
  • Each shape in the array becomes an independent scene object after the session finalizes
  • Combine scene outputs with standard Manifold scripts by using two separate code objects in the same design
  • Use console.log() for debugging — output appears in the error panel
  • The ShapeDefinition type is the union of all shape input types, useful for arrays of mixed shapes

Next steps