Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@

Browser CAD with editable solids, parametric sketches, and local project files.

SolidFrame is a CAD application developed and maintained by **Nazeeh Abdul-Hadi**. It brings solid modeling, parametric sketches, feature history, geometry exchange, and local document storage into a focused browser workspace. Nazeeh’s work includes the project hub, editable workholding fixture, workspace styling, keyboard-accessible modeling controls, startup safeguards, and deployment improvements.
SolidFrame is a CAD application developed and maintained by **Nazeeh Abdul-Hadi**. It brings solid modeling, parametric sketches, feature history, geometry exchange, and local document storage into a focused browser workspace. Nazeeh’s work includes an editable mechanical part builder, the project hub and workholding fixture, workspace styling, keyboard-accessible modeling controls, startup safeguards, and deployment improvements.

[Open SolidFrame](https://nazeeh111.github.io/SolidFrame/) · [Source and license](LICENSE)

## Design a drilled part

The mechanical part builder creates a rectangular mounting plate or a sharp right-angle bracket from dimensions in millimetres. Set the stock dimensions and centered round through-hole grids; brackets have separate horizontal and vertical flange layouts. The editor checks the complete configuration before rebuilding the native solid, including hole spacing, edge clearance and the inside corner of a bracket.

Edit the saved part through the dedicated part editor. Apply commits the configuration together, and undo/redo restores parameter changes. Invalid or cancelled edits keep the previous valid part. Native `.cd` documents retain the dimensions and hole layouts; STEP and STL exports contain geometry rather than the editable builder settings.

This workflow models ideal geometry. It does not calculate sheet-metal bend allowance, thread or fastener standards, structural strength, or fabrication approval. See [mechanical parts](docs/mechanical-parts.md) for the controls, supported ranges and verification.

## Start with real geometry

Choose **Open editable sample** on the home screen. The workholding fixture contains seven native, editable solids: a base plate, two jaws, a workpiece, two locator pins, and a reference boss. It is an illustrative CAD exercise, not a fabrication-approved fixture.
Expand Down Expand Up @@ -47,6 +55,7 @@ The production site is in `dist/`. Serve it through HTTP, rather than opening it

## Project development

- Original drilled-plate and right-angle-bracket builder with coupled hole-layout validation, atomic parameter editing and native-document persistence.
- SolidFrame project hub, matching light/dark workspace palettes, original mark and fixture illustration, branded loading and recoverable startup errors.
- A compact modeling ribbon with distinct command/menu targets, visible keyboard focus, and keyboard navigation through tool menus.
- One-click native fixture with editable Box/Cylinder nodes, serialization support, and one undoable creation transaction.
Expand Down
35 changes: 35 additions & 0 deletions docs/mechanical-parts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Mechanical parts

SolidFrame’s mechanical part builder creates one editable native solid from a compact set of dimensions. It adds a focused part-design workflow to the existing modeling workspace.

## Create and revise

1. Choose **Create mechanical part** on the home screen or in the modeling ribbon.
2. Choose a plate or bracket, then enter stock dimensions and hole layouts. For a bracket, configure the horizontal and vertical flanges separately.
3. Choose **Create part** to add one part. Opening or cancelling the editor does not create a document.
4. Select the part and choose **Edit mechanical part** to revise the complete configuration. Use **Apply changes**, then the document’s **Undo** and **Redo** controls as needed.
5. Save the project in this browser or download a native `.cd` file. Reopen a saved project from Recent projects to continue editing.

Defaults are a 100 × 80 × 6 mm plate with four Ø8 mm holes at 60 × 40 mm pitch. The bracket adds a 60 mm total height and four vertical-flange Ø8 mm holes at 60 × 30 mm pitch. These are starting dimensions, not a standardized or approved part.

## Geometry

A plate is rectangular stock with a centered grid of round through holes. A bracket is an ideal sharp right-angle solid with separate hole grids on its horizontal and vertical flanges. The bracket height includes the base thickness. Grids are centered in the usable flange area, clear of the inside corner.

Stock dimensions, hole diameter and pitch use millimetres. Rows and columns are integer counts. The complete configuration is validated before a native geometry operation: dimensions must be finite and positive, holes must not overlap or touch stock edges, and bracket holes must clear the adjoining flange. Stock and residual web must be at least 0.5 mm; dimensions are limited to 2,000 mm. A part may have at most 32 holes across its enabled grids. These limits bound work in the browser; they are not material or manufacturing requirements.

## Editing and files

The editor applies the whole configuration together. It builds candidate geometry before replacing the current valid parameters. A rejected edit leaves the old part intact. Creation and accepted edits participate in document undo/redo. No construction cylinders appear as additional project nodes.

The native `.cd` document stores the builder parameters. Reopening it reconstructs the part with the same editable values. STEP and STL use the existing export commands and preserve geometry, not these builder settings.

The part has parameter editing and document undo history. It does not expose separate blank/cut rows in the inherited parametric feature timeline. General sketch-based feature history remains available for other models.

## Limits

Supported shapes are rectangular plates and sharp right-angle brackets with regular round through-hole grids. There are no countersinks, threads, slots, corner fillets or arbitrary picked-face hole placement in this builder.

A sharp bracket is an ideal geometric model. It is not a sheet-metal flat pattern and does not calculate bend radius, bend allowance, material properties, strength or manufacturing tolerances. Validate a design separately before fabrication.

The implementation reuses Chili3D’s node, transaction, serialization, selection and export infrastructure and the existing OpenCascade geometry kernel. The part configuration, validation, construction workflow and dedicated editor are the new SolidFrame contribution.
10 changes: 10 additions & 0 deletions docs/solidframe-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,13 @@ The ribbon retains its commands and compact height, with softer tool surfaces, c
The focused ribbon, dropdown and viewport checks passed **54 tests across six files**. The three new activation and disabled tests failed before implementation. Production application and bundled-plugin builds passed with the existing three bundle-size warnings. `npm run check` passed without applying fixes; inherited warnings and suggestions remain. `git diff --check` passed.

Actual browser checks created a box, selected and edited the fixture base from 100 to 110 mm, launched Move using Enter and completed a two-point transform. Save reported success; a page reload and Recent-project reopen retained the new box, edited length and transform. The primitive dropdown opened and closed with Escape; the End snap checkbox switched off and on. Both themes were inspected in the running workspace. These checks cover the exercised controls and local-project workflow, not every CAD operation or exchange format.

## Mechanical part builder (2026-10-07)

The dedicated builder adds plates and sharp right-angle brackets with independent horizontal and vertical through-hole grids. The complete configuration is checked before kernel operations, and candidate geometry is built before replacing saved parameters. Shape wrappers are regenerated during undo/redo; undo records retain parameter JSON. This is parameter editing, not an extension of the inherited operation timeline.

The focused 34-test run passed across four files, covering the new geometry, modal and commands plus the existing native fixture. Real OpenCascade tests check analytical volume, hole-center containment, closed shells and valid solids; reject invalid layouts before native execution; exercise edit failure preservation and undo/redo; reconstruct a nondefault bracket from serialized parameters; roundtrip STEP geometry; validate binary STL structure; and construct a 32-hole part. TypeScript and the application plus bundled-plugin builds passed. Existing bundle-size warnings remain.

Native browser checks created a bracket with six horizontal Ø8 mm holes and four vertical Ø10 mm holes, then edited length from 95 to 110 mm. Undo and Redo restored each length in the editor. A fresh application page reopened the saved project from Recent projects with width 120, length 110, total height 75, thickness 6 mm, both enabled layouts and their different dimensions intact. The model tree showed one part. The 390-pixel editor fit without horizontal overflow, showed a rejected layout inline and allowed cancellation.

The native browser reached the existing STEP export confirmation and reported Downloading. Save To File also reported Downloading, but the current in-app browser did not provide completed download artifacts. These are export-flow observations, not verified browser file downloads or imports. STEP roundtrip, STL bytes and native-node serialization were verified separately through the real local kernel tests. No third-party model, manufacturing process, material strength, or every-browser behavior is certified by these fixtures.
178 changes: 178 additions & 0 deletions packages/app/src/bodys/fabricationParameters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Part of the Chili3d Project, under the AGPL-3.0 License.
// See LICENSE file in the project root for full license information.

import { Result } from "@chili3d/core";

export interface HoleGrid {
enabled: boolean;
columns: number;
rows: number;
pitchX: number;
pitchY: number;
diameter: number;
}

export interface FabricationParameters {
kind: "plate" | "bracket";
width: number;
length: number;
height: number;
thickness: number;
horizontal: HoleGrid;
vertical: HoleGrid;
}

export const FABRICATION_MIN_WEB = 0.5;
export const FABRICATION_MAX_DIMENSION = 2000;
export const FABRICATION_MAX_HOLES = 32;

export function defaultFabricationParameters(kind: "plate" | "bracket" = "plate"): FabricationParameters {
return {
kind,
width: 100,
length: 80,
height: 60,
thickness: 6,
horizontal: { enabled: true, columns: 2, rows: 2, pitchX: 60, pitchY: 40, diameter: 8 },
vertical: { enabled: true, columns: 2, rows: 2, pitchX: 60, pitchY: 30, diameter: 8 },
};
}

function record(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

function dimension(value: unknown): value is number {
return (
typeof value === "number" &&
Number.isFinite(value) &&
value >= FABRICATION_MIN_WEB &&
value <= FABRICATION_MAX_DIMENSION
);
}

function readGrid(value: unknown, fallback: HoleGrid, active: boolean): Result<HoleGrid> {
if (!record(value) || typeof value["enabled"] !== "boolean") {
return Result.err("A hole layout must include an enabled setting.");
}
if (!active || !value["enabled"]) {
const count = (key: "columns" | "rows") =>
typeof value[key] === "number" &&
Number.isInteger(value[key]) &&
value[key] >= 1 &&
value[key] <= FABRICATION_MAX_HOLES
? value[key]
: fallback[key];
return Result.ok({
enabled: value["enabled"],
columns: count("columns"),
rows: count("rows"),
pitchX: dimension(value["pitchX"]) ? value["pitchX"] : fallback.pitchX,
pitchY: dimension(value["pitchY"]) ? value["pitchY"] : fallback.pitchY,
diameter: dimension(value["diameter"]) ? value["diameter"] : fallback.diameter,
});
}
for (const key of ["columns", "rows"] as const) {
if (
typeof value[key] !== "number" ||
!Number.isInteger(value[key]) ||
value[key] < 1 ||
value[key] > FABRICATION_MAX_HOLES
) {
return Result.err("Hole rows and columns must be whole numbers from 1 to 32.");
}
}
for (const key of ["pitchX", "pitchY", "diameter"] as const) {
if (!dimension(value[key])) return Result.err("Hole diameter and pitches must be 0.5 to 2,000 mm.");
}
return Result.ok({
enabled: true,
columns: value["columns"] as number,
rows: value["rows"] as number,
pitchX: value["pitchX"] as number,
pitchY: value["pitchY"] as number,
diameter: value["diameter"] as number,
});
}

/** Validate the complete coupled layout before any native kernel call. Also strips unknown saved fields. */
export function validateFabricationParameters(value: unknown): Result<FabricationParameters> {
if (!record(value) || (value["kind"] !== "plate" && value["kind"] !== "bracket")) {
return Result.err("Choose a rectangular plate or right-angle bracket.");
}
for (const key of ["width", "length", "thickness"] as const) {
if (!dimension(value[key])) return Result.err("Stock dimensions must be 0.5 to 2,000 mm.");
}
const defaults = defaultFabricationParameters(value["kind"]);
const height =
value["kind"] === "plate" && !dimension(value["height"]) ? defaults.height : value["height"];
if (!dimension(height)) return Result.err("Bracket height must be 0.5 to 2,000 mm.");
const width = value["width"] as number;
const length = value["length"] as number;
const thickness = value["thickness"] as number;
if (
value["kind"] === "bracket" &&
(length - thickness < FABRICATION_MIN_WEB || height - thickness < FABRICATION_MIN_WEB)
) {
return Result.err("Bracket length and total height must exceed thickness by at least 0.5 mm.");
}
const horizontal = readGrid(value["horizontal"], defaults.horizontal, true);
if (!horizontal.isOk) return Result.err(horizontal.error);
const vertical = readGrid(value["vertical"], defaults.vertical, value["kind"] === "bracket");
if (!vertical.isOk) return Result.err(vertical.error);
const parameters: FabricationParameters = {
kind: value["kind"],
width,
length,
height,
thickness,
horizontal: horizontal.value,
vertical: vertical.value,
};
let holes = 0;
for (const flange of ["horizontal", "vertical"] as const) {
if (flange === "vertical" && parameters.kind === "plate") continue;
const grid = parameters[flange];
if (!grid.enabled) continue;
holes += grid.columns * grid.rows;
const span =
(flange === "horizontal" ? length : height) - (parameters.kind === "bracket" ? thickness : 0);
if (
(grid.columns > 1 && grid.pitchX - grid.diameter < FABRICATION_MIN_WEB) ||
(grid.rows > 1 && grid.pitchY - grid.diameter < FABRICATION_MIN_WEB)
) {
return Result.err("Leave at least 0.5 mm of stock between neighboring holes.");
}
if (
(grid.columns - 1) * grid.pitchX + grid.diameter + 2 * FABRICATION_MIN_WEB > width ||
(grid.rows - 1) * grid.pitchY + grid.diameter + 2 * FABRICATION_MIN_WEB > span
) {
return Result.err(
`${flange === "horizontal" ? "Horizontal" : "Vertical"} holes need at least 0.5 mm clearance from every edge and the bracket corner.`,
);
}
}
if (holes > FABRICATION_MAX_HOLES) return Result.err("Use at most 32 holes across both flange layouts.");
return Result.ok(parameters);
}

/** Coordinates in the flange's own drawing: x across width, y along length or total height. */
export function fabricationHoleCenters(
parameters: FabricationParameters,
flange: "horizontal" | "vertical",
): { x: number; y: number }[] {
if (flange === "vertical" && parameters.kind === "plate") return [];
const grid = parameters[flange];
if (!grid.enabled) return [];
const offset = parameters.kind === "bracket" ? parameters.thickness : 0;
const span = (flange === "horizontal" ? parameters.length : parameters.height) - offset;
const startX = (parameters.width - (grid.columns - 1) * grid.pitchX) / 2;
const startY = offset + (span - (grid.rows - 1) * grid.pitchY) / 2;
const centers: { x: number; y: number }[] = [];
for (let row = 0; row < grid.rows; row++) {
for (let column = 0; column < grid.columns; column++) {
centers.push({ x: startX + column * grid.pitchX, y: startY + row * grid.pitchY });
}
}
return centers;
}
Loading
Loading