Package examples
These examples are included from the package files that the integration suites execute. The TypeScript macros run through the published client macro runner; the Python examples run through sketchforge3d. Both suites create projects on a temporary local server and remove the projects after each run.
Use the REST API guide for server setup and endpoint semantics. For the full public client signatures, see the TypeScript and Python references.
TypeScript: bolt plate
Source: packages/sketchforge-client/examples/bolt-plate.macro.mjs.
/**
* A plate with a computed row of tapped holes.
*
* `replace` rather than `fresh`: a plate is a part someone iterates on, and
* re-running with a different hole count should give one plate with the new
* holes rather than a second plate beside the first.
*
* The holes are `kind: "thread"` with `hole: true`, which cuts the thread form
* wider by a clearance and marks the shape as a hole -- so grouping them with
* the plate in the editor taps the plate. A thread is stored as a mesh, which
* the server's exact boolean (`project.cut`, `project.group`) cannot cut yet,
* so the macro leaves them as marked holes for the editor's boolean.
*
* @type {import("@constructorfleet/sketchforge3d").MacroDefinition<{
* holes: number, spacing: number, size: string, thickness: number, margin: number,
* }>}
*/
export default {
name: "Bolt plate",
description: "A plate with a row of tapped holes, centred on it.",
project: { mode: "replace", name: "Bolt plate" },
params: {
holes: 5,
spacing: 25,
size: "M8",
thickness: 8,
/** Material left beyond the outermost hole, which sets the plate's size. */
margin: 12,
},
async run({ project, params, layout, log }) {
const boltDiameter = Number(params.size.replace(/^M/i, "")) || 8;
const width = (params.holes - 1) * params.spacing + 2 * (boltDiameter / 2 + params.margin);
const depth = boltDiameter + 2 * params.margin;
log(`plate ${layout.mm(width)} x ${depth} x ${params.thickness} mm, ${params.holes} ${params.size} holes`);
const positions = layout.row(params.holes, params.spacing);
const written = await project.addMany([
{ kind: "box", name: "Plate", width: layout.mm(width), depth, height: params.thickness },
...positions.map((position, index) => ({
kind: "thread",
name: `${params.size} hole ${index + 1}`,
size: params.size,
// Through the plate, and started below it so the first turn is complete.
length: params.thickness + 2,
hole: true,
x: layout.mm(position.x),
z: layout.mm(position.z),
elevation: -1,
})),
]);
return {
plate: { width: layout.mm(width), depth, thickness: params.thickness },
holes: positions.length,
shapesWritten: written.shapes.length,
revision: written.revision,
};
},
};TypeScript: bolt circle
Source: packages/sketchforge-client/examples/bolt-circle.macro.mjs.
/**
* N tapped holes on a ring of radius R, in a round flange.
*
* `fresh` rather than `replace`: a bolt circle is the thing someone generates
* several of to compare, so every run is its own project and no run can
* overwrite one that is being looked at.
*
* @type {import("@constructorfleet/sketchforge3d").MacroDefinition<{
* holes: number, radius: number, size: string, thickness: number, bore: number, startAngle: number,
* }>}
*/
export default {
name: "Bolt circle",
description: "A flange with N tapped holes on a ring of radius R.",
project: "fresh",
params: {
holes: 6,
radius: 30,
size: "M6",
thickness: 10,
/** A central through hole; 0 leaves the flange solid. */
bore: 20,
startAngle: 0,
},
async run({ project, params, layout, log }) {
const boltDiameter = Number(params.size.replace(/^M/i, "")) || 6;
const outerDiameter = 2 * (params.radius + boltDiameter + 6);
const positions = layout.ring(params.holes, params.radius, { startAngle: params.startAngle });
log(`flange ${outerDiameter} mm across, ${params.holes} ${params.size} holes on a ${params.radius} mm ring`);
const shapes = [
{
kind: "cylinder",
name: "Flange",
width: outerDiameter,
depth: outerDiameter,
height: params.thickness,
},
...positions.map((position, index) => ({
kind: "thread",
name: `${params.size} hole ${index + 1}`,
size: params.size,
length: params.thickness + 2,
hole: true,
x: layout.mm(position.x),
z: layout.mm(position.z),
elevation: -1,
})),
];
if (params.bore > 0) {
shapes.push({
kind: "cylinder",
name: "Bore",
width: params.bore,
depth: params.bore,
height: params.thickness + 2,
elevation: -1,
hole: true,
});
}
const written = await project.addMany(shapes);
return {
outerDiameter,
holes: positions.length,
// The angles are worth reporting: a wrong start angle is the usual way a
// bolt circle comes out unusable, and it is invisible in a shape count.
angles: positions.map((position) => layout.mm((Math.atan2(position.z, position.x) * 180) / Math.PI)),
shapesWritten: written.shapes.length,
};
},
};TypeScript: filleted box
Source: packages/sketchforge-client/examples/filleted-box.macro.mjs.
/**
* A parametric box with every roundable edge filleted, from three numbers.
*
* The fillet runs on the server with OCCT, so this is the one example that
* needs real CAD rather than parameters. Two things it demonstrates: the edge
* listing says which edges can be treated at all -- a seam in a curved surface
* cannot -- and every wanted edge goes in one request, because OCCT refuses a
* second treatment on edges adjacent to the first.
*
* `replace` keeps one box in one project however many times the numbers change.
*
* @type {import("@constructorfleet/sketchforge3d").MacroDefinition<{
* width: number, depth: number, height: number, radius: number,
* }>}
*/
export default {
name: "Filleted box",
description: "A box of three dimensions, with its edges rounded.",
project: { mode: "replace", name: "Filleted box" },
params: { width: 60, depth: 40, height: 20, radius: 4 },
async run({ project, params, log }) {
const box = await project.add({
kind: "box",
name: "Body",
width: params.width,
depth: params.depth,
height: params.height,
});
const edges = await project.edges(box.id);
const roundable = edges.filter((edge) => edge.filletable).map((edge) => edge.index);
log(`${edges.length} edges, ${roundable.length} roundable; filleting at ${params.radius} mm`);
const treated = await project.fillet(box.id, params.radius, roundable);
return {
shapeId: box.id,
edges: edges.length,
rounded: roundable.length,
radius: params.radius,
triangles: treated.triangles,
size: { width: treated.shape.width, depth: treated.shape.depth, height: treated.shape.height },
};
},
};TypeScript: tapped plate
Source: packages/sketchforge-client/examples/tap-drill-plate.macro.mjs. Cuts an M8 thread cutter through a plate with project.cut and exports the part as STL with project.export, entirely through the API. The integration test checks that material was removed, that the off-centre cut shifts the centre of mass, and that the written STL encloses the same solid.
import { writeFile } from "node:fs/promises";
/**
* A plate tapped with an M8 thread cutter, cut on the server and exported as STL.
*
* This is the whole workflow, end to end through the API: the plate and a
* M8 thread cutter is added as a shape, `project.cut` subtracts it as one
* exact boolean, and `project.export` returns
* the finished part as STL ready for a slicer. Nothing is left for the editor.
*
* The thread cutter includes its core, so it removes the bore and helical
* thread form in one operation.
*
* `replace`, because a part is something you iterate on: a second run gives
* one plate with the new hole, not a second plate beside the first.
*
* @type {import("@constructorfleet/sketchforge3d").MacroDefinition<{
* size: number, width: number, depth: number, thickness: number, offset: number, out: string,
* }>}
*/
export default {
name: "Tapped plate",
description: "A plate cut with an M8 thread cutter and exported as STL.",
project: { mode: "replace", name: "Tapped plate" },
params: {
/** Metric thread size; the cutter removes its matching thread form. */
size: 8,
width: 60,
depth: 40,
thickness: 8,
/** How far the hole sits from the plate's centre along X. */
offset: 10,
/** Where to write the STL; empty to keep it in memory only. */
out: "",
},
async run({ project, params, log }) {
const { shapes } = await project.addMany([
{ kind: "box", name: "Plate", width: params.width, depth: params.depth, height: params.thickness },
{
kind: "thread",
name: `M${params.size} tapped hole`,
size: `M${params.size}`,
// Match the plate thickness: longer helical meshes can exceed the
// server kernel's STL importer limits for boolean cutters.
length: params.thickness,
x: params.offset,
elevation: 0,
},
]);
const [plate, thread] = shapes;
if (!plate || !thread) throw new Error("Expected a plate and a thread cutter.");
// Named operands: the thread need not be marked `hole: true` to be cut away.
const cut = await project.cut({ solidIds: [plate.id], holeIds: [thread.id] });
log(`tapped M${params.size} through the plate: ${cut.triangles} triangles`);
const stl = await project.export({ format: "stl", shapeIds: [cut.shape.id], name: "tapped-plate" });
if (params.out) {
await writeFile(params.out, stl.data);
log(`wrote ${stl.bytes} bytes to ${params.out}`);
}
return {
plateId: cut.shape.id,
triangles: cut.triangles,
stl: { filename: stl.filename, bytes: stl.bytes, shapeCount: stl.shapeCount },
};
},
};Python: bolt plate
Source: packages/sketchforge-py/examples/bolt_plate.py.
"""
A plate with a computed row of tapped holes.
``replace`` rather than ``fresh``: a plate is a part someone iterates on, and
re-running with a different hole count should give one plate with the new holes
rather than a second plate beside the first.
The holes are ``kind="thread"`` with ``hole=True``, which cuts the thread form
wider by a clearance and marks the shape as a hole. Grouping these thread meshes
with the plate subtracts the bore and thread form to tap it. The REST API also
supports this through ``POST /groups`` or ``POST /booleans``.
"""
from typing import Any, Dict, List
from sketchforge3d import MacroContext, define_macro
def build(context: MacroContext) -> Dict[str, Any]:
params = context.params
layout = context.layout
bolt_diameter = float(str(params["size"]).lstrip("Mm") or 8)
margin = float(params["margin"])
thickness = float(params["thickness"])
holes = int(params["holes"])
spacing = float(params["spacing"])
width = (holes - 1) * spacing + 2 * (bolt_diameter / 2 + margin)
depth = bolt_diameter + 2 * margin
context.log(f"plate {layout.mm(width)} x {depth} x {thickness} mm, {holes} {params['size']} holes")
positions = layout.row(holes, spacing)
shapes: List[Dict[str, Any]] = [
{"kind": "box", "name": "Plate", "width": layout.mm(width), "depth": depth, "height": thickness}
]
for index, position in enumerate(positions, start=1):
shapes.append(
{
"kind": "thread",
"name": f"{params['size']} hole {index}",
"size": params["size"],
# Match the plate thickness; longer helical meshes can fail OCCT STL import.
"length": thickness,
"hole": True,
"x": layout.mm(position.x),
"z": layout.mm(position.z),
"elevation": 0,
}
)
written = context.project.add_many(shapes)
return {
"plate": {"width": layout.mm(width), "depth": depth, "thickness": thickness},
"holes": len(positions),
"shapesWritten": len(written.shapes),
"revision": written.revision,
}
MACRO = define_macro(
name="Bolt plate",
description="A plate with a row of tapped holes, centred on it.",
project=("replace", "Bolt plate (python)"),
params={
"holes": 5,
"spacing": 25.0,
"size": "M8",
"thickness": 8.0,
# Material left beyond the outermost hole, which sets the plate's size.
"margin": 12.0,
},
run=build,
)Python: bolt circle
Source: packages/sketchforge-py/examples/bolt_circle.py.
"""
N tapped holes on a ring of radius R, in a round flange.
``fresh`` rather than ``replace``: a bolt circle is the thing someone generates
several of to compare, so every run is its own project and no run can overwrite
one that is being looked at.
"""
from typing import Any, Dict, List
from sketchforge3d import MacroContext, define_macro
def build(context: MacroContext) -> Dict[str, Any]:
params = context.params
layout = context.layout
bolt_diameter = float(str(params["size"]).lstrip("Mm") or 6)
holes = int(params["holes"])
radius = float(params["radius"])
thickness = float(params["thickness"])
bore = float(params["bore"])
outer_diameter = 2 * (radius + bolt_diameter + 6)
positions = layout.ring(holes, radius, start_angle=float(params["start_angle"]))
context.log(f"flange {outer_diameter} mm across, {holes} {params['size']} holes on a {radius} mm ring")
shapes: List[Dict[str, Any]] = [
{
"kind": "cylinder",
"name": "Flange",
"width": outer_diameter,
"depth": outer_diameter,
"height": thickness,
}
]
for index, position in enumerate(positions, start=1):
shapes.append(
{
"kind": "thread",
"name": f"{params['size']} hole {index}",
"size": params["size"],
"length": thickness + 2,
"hole": True,
"x": layout.mm(position.x),
"z": layout.mm(position.z),
"elevation": -1,
}
)
if bore > 0:
shapes.append(
{
"kind": "cylinder",
"name": "Bore",
"width": bore,
"depth": bore,
"height": thickness + 2,
"elevation": -1,
"hole": True,
}
)
written = context.project.add_many(shapes)
return {
"outerDiameter": outer_diameter,
"holes": len(positions),
# The angles are worth reporting: a wrong start angle is the usual way a
# bolt circle comes out unusable, and it is invisible in a shape count.
"angles": [layout.mm(layout.angle_of(position)) for position in positions],
"shapesWritten": len(written.shapes),
}
MACRO = define_macro(
name="Bolt circle",
description="A flange with N tapped holes on a ring of radius R.",
project="fresh",
params={
"holes": 6,
"radius": 30.0,
"size": "M6",
"thickness": 10.0,
# A central through hole; 0 leaves the flange solid.
"bore": 20.0,
"start_angle": 0.0,
},
run=build,
)Python: filleted box
Source: packages/sketchforge-py/examples/filleted_box.py.
"""
A parametric box with every roundable edge filleted, from three numbers.
The fillet runs on the server with OCCT, so this is the one example that needs
real CAD rather than parameters. Two things it demonstrates: the edge listing
says which edges can be treated at all -- a seam in a curved surface cannot --
and every wanted edge goes in one request, because OCCT refuses a second
treatment on edges adjacent to the first.
"""
from typing import Any, Dict
from sketchforge3d import MacroContext, define_macro
def build(context: MacroContext) -> Dict[str, Any]:
params = context.params
box = context.project.add(
{
"kind": "box",
"name": "Body",
"width": params["width"],
"depth": params["depth"],
"height": params["height"],
}
)
edges = context.project.edges(box.id)
roundable = [edge.index for edge in edges if edge.filletable]
context.log(f"{len(edges)} edges, {len(roundable)} roundable; filleting at {params['radius']} mm")
treated = context.project.fillet(box.id, float(params["radius"]), roundable)
return {
"shapeId": box.id,
"edges": len(edges),
"rounded": len(roundable),
"radius": params["radius"],
"triangles": treated.triangles,
"size": {
"width": treated.shape.width,
"depth": treated.shape.depth,
"height": treated.shape.height,
},
}
MACRO = define_macro(
name="Filleted box",
description="A box of three dimensions, with its edges rounded.",
project=("replace", "Filleted box (python)"),
params={"width": 60.0, "depth": 40.0, "height": 20.0, "radius": 4.0},
run=build,
)Run the examples
The repository CI runs these same example files against an isolated local server:
npm run test:integration
npm run client:test:py:integrationFor local runs, start the web app with a temporary project directory and a test-only token, then set SKETCHFORGE_URL, SKETCHFORGE_PROJECTS_DIR, and SKETCHFORGE_API_TOKEN for both commands. Never point these examples at production: their project mode intentionally creates, replaces, and deletes test projects.
Package installation, macro parameters, and public API usage are documented in the TypeScript package README and Python package README. Those package READMEs remain the registry-facing source for explanatory prose; the generated references here link back to them rather than copying their guides.
Documentation version: 1.30.0 · Repository revision: 1cb9fee8dff4d394684c229dd67a21da539d167f · View source