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
43 changes: 43 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Documentation

on:
pull_request:
paths:
- "docs/**"
- "main/opengeometry-three/index.ts"
- "main/opengeometry-three/src/constants/**"
- ".github/workflows/docs.yml"
push:
branches:
- main
paths:
- "docs/**"
- "main/opengeometry-three/index.ts"
- "main/opengeometry-three/src/constants/**"
- ".github/workflows/docs.yml"

permissions:
contents: read

concurrency:
group: docs-${{ github.ref }}
cancel-in-progress: true

jobs:
docs:
name: Format, coverage, build and links
runs-on: ubuntu-latest
defaults:
run:
working-directory: docs
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: docs/package-lock.json
- name: Install documentation tools
run: npm ci
- name: Check documentation
run: npm run check
2 changes: 2 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ on:
- 'main/**'
- 'README.md'
- 'MIGRATION.md'
- 'docs/opengeometry/**'
- 'LICENSE.md'
- '.github/workflows/verify.yml'
pull_request:
paths:
- 'main/**'
- 'README.md'
- 'MIGRATION.md'
- 'docs/opengeometry/**'
- 'LICENSE.md'
- '.github/workflows/verify.yml'
workflow_dispatch:
Expand Down
79 changes: 79 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Changelog

All notable changes to the `opengeometry` package are in this file. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the package follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.0.0] - 2026-11-09

3.0 is a new API. Code written for 2.0 does not run without changes. Version 2.5 was not
published. Follow [MIGRATION.md](./MIGRATION.md): it gives every change with code before and after.

### Changed

- **Breaking:** bodies are made with three classes and a kind constant: `new Wire(kind, params)`,
`new Solid(kind, params)` and `new Sheet(OG_OPERATION_SWEEP, params)`. The 2.0 classes
(`Cuboid`, `Cylinder`, `Polygon`, `Opening`, `Sweep`, `AnalyticSolid` and the others) are gone.
See "Replace this with that" in MIGRATION.md.
- **Breaking:** points and vectors are `[x, y, z]` arrays. `Vector3` is gone.
- **Breaking:** bodies move with `body.transform(kind, params)` and `OG_TRANSFORM_*`. Setting
`position`, `rotation` or `scale` is reverted with a `PlacementOverwritten` warning.
- **Breaking:** start the kernel with `await OpenGeometry.create(input, options)` before the first
body. `workerURL` goes in the second argument.
- **Breaking:** the 2D region and offset functions are statics of `OpenGeometry`, such as
`OpenGeometry.booleanRegions2D` and `OpenGeometry.offsetRingVariable`. The offset calls return
`CurvedRegion2D` line and arc edges, not `{ x, y, z }` points.
- **Breaking:** `solid.prepareIfcExchange()` is `OpenGeometry.exportIfcBody(solid)`.
- **Breaking:** failures throw `OGError` with a `code`, a `call`, a `message` and `details`.
- **Breaking:** `classifyPoint` takes a point in the body's own coordinates and returns
`'Inside'`, `'Outside'`, `'Boundary'` or `'Unknown'`.
- **Breaking:** the shell hollows a solid in place:
`solid.operate(OG_OPERATION_SHELL, { thickness })`. To keep the first solid, shell
`solid.duplicate()`.
- **Breaking:** a body is displayed when it is drawn or walked by `updateMatrixWorld`. Its new
mesh is installed after the render that draws it, so the next frame shows it.
- **Breaking:** a body's `surface` and `outline` meshes have `matrixAutoUpdate` set to `false`.
Code that writes their `position`, `rotation`, `quaternion` or `scale` must call
`updateMatrix()` on that mesh.
- The outline leaves out smooth edges, and planar faces average their normals across them, so a
polygon that approximates a curve shades as a curve. The smooth angle is 20° by default; set
`appearance.smoothAngle` to `0` for the 2.0 look.

### Added

- Solid primitives: cuboid, cylinder, cylinder sector, sphere, cone or frustum, ring torus and
planar polyhedron. Wire primitives: rectangle, circle, polyline, line-and-arc profile and arc.
Each goes on any plane through the `plane` option.
- Extrude with holes, from any closed wire, including a line-and-arc profile.
- Sweep along lines and arcs, with one exact bend per arc, into a capped `Solid`, an open-ended
`Sheet` (`caps: false`), or a closed ring.
- Revolve a closed line-and-arc profile a full turn about an axis in its plane.
- Union, subtract and intersect with one to a hundred tool solids in one `operate` call, and a
report of the result with `solid.getReport()`.
- `body.rebuild(kind, params)` gives a body new parameters in place. `body.mirror(plane)` rebuilds
it as its reflection.
- `SystemAssembly` with parent and child placement; `duplicate()` for an independent copy and
`instance()` for a shared shape.
- Marks with `rollback` and `release`, and `OpenGeometry.transaction(fn, { dryRun })`.
- Queries: `getBounds`, `getBrep`, `getWorldBrep`, `faceNormalAt`, `classifyPoint`, `measure`
(volume and area within a stated error bound) and `validity`.
- Display for Three.js: every body is a `THREE.Group`, with level of detail that follows the
camera, tessellation in a web worker or inline, picking to a face or an edge with
`OpenGeometry.resolveHit`, `OpenGeometry.settled` and `OpenGeometry.flush`.
- Drawing through your own layers: `OpenGeometry.setHostDrawn`, `OpenGeometry.markDisplayed` and
`OpenGeometry.hostGroup`. `OpenGeometry.releaseIdleGeometry()` frees cached display meshes.
- `OpenGeometry.exportStep` writes STEP in millimetres or metres with Y or Z up, with an export
report.
- `OpenGeometry.insetCurvedRegion2D` and `OpenGeometry.offsetCurvedRingVariable` inset a
line-and-arc region exactly, with one distance, or one distance per edge.

### Removed

- **Breaking:** IFC file text, STL and PDF export, projection, loft, triangulation as a public
call, curves other than lines, circles and arcs, and wedges. They have no replacement in 3.0.

### Fixed

- A circular hole away from the profile centre extrudes. 2.0 refused some of these.
- Offset corners are exact arcs. 2.0 drew an inside corner as short segments outside the circle.
- A point inside one of two touching solids of a body is `Inside`. 2.0 could read it as outside.
20 changes: 6 additions & 14 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,6 @@ It is written for people and for coding assistants alike: each section says what
4. For a name the table does not list, look in "Partly replaced". Anything listed under "Removed" has no replacement: take that code out, or stay on the older version.
5. Finish with its "Check your result" steps.

## Unreleased

### Added

- `OpenGeometry.offsetCurvedRingVariable(region, distances, holeDistances = [])` insets one `CurvedRegion2D` of lines and arcs by one distance (finite, >= 0) per edge, with one list per hole ring, and returns `CurvedRegion2D[]`. An arc edge stays on its centre: a convex arc moves to `r - d`, a concave arc or a hole's arc to `r + d`. A distance of 0 keeps its edge. A distance larger than an arc's radius, a wrong count, or a negative or non-finite distance is `InvalidParameter`. A collapsed result is an empty list. `offsetRingVariable` is unchanged.

### Different behaviour

- A body counts as displayed when it is drawn, not only when `updateMatrixWorld` walks it: its surface or outline `onBeforeRender`, or `OpenGeometry.markDisplayed(bodies)`, stamps it for the current render pass, and its new display geometry is installed after that render, so the next frame shows it. A body that is neither drawn nor walked for a full pass is no longer displayed and gets no level-of-detail target or record swap. A walk still stamps and installs at once, so code that walks, then calls `OpenGeometry.flush({ geometry: 'sync' })`, gets the same meshes as before.
- Display buffers leave smooth edges out of `outline` and `edgeIds`. An edge is smooth when its two faces are tangent-continuous, or when two planes fold by less than the smooth angle, 20° by default. The outline of a polygon that approximates a curve therefore loses its fold lines, and planar faces average their vertex normals across smooth edges, so the shading no longer shows the facets. A new `smoothEdges` `Uint8Array` gives one byte per topology edge, 1 for smooth.
- `appearance.smoothAngle` sets the angle per body, in degrees from 0 up to but not including 90; `0` restores the previous outline and normals. `OGWorldGraph.buffers` and `OGTessellator.buffers` take it as an optional fourth argument.
- Rust: `SnapshotStore::buffers()`, `WorldGraph::buffers()` and `display_buffers()` now default to 20°, not 0°. `tessellate_with_smooth_angle`, `display_buffers_with_smooth_angle` and `buffers_with_smooth_angle` take an explicit angle, and `SMOOTH_ANGLE` is the default. `tessellate()` stays at 0°.
- A body's `surface` and `outline` meshes are static: their `matrixAutoUpdate` is `false`, so a world matrix update no longer recomputes their local matrix. Code that writes their `position`, `rotation`, `quaternion` or `scale` must call `updateMatrix()` on that mesh after the write.

## 2.5 to 3.0

2.5 was not published. From 2.0, follow 2.0 to 2.5, then 2.5 to 3.0.
Expand All @@ -50,6 +36,7 @@ It is written for people and for coding assistants alike: each section says what
- An arched opening is one cutter: extrude a closed `OG_PRIMITIVE_PROFILE` (jambs, sill and an arch) across a curved or straight wall and subtract it with `wall.operate(OG_OPERATION_SUBTRACT, { tools: [arch] })`. 2.0 needed a box and a half-cylinder for it, and a curved wall then failed.
- An arched door from the floor is one cutter too: when its sill is at or below the wall's bottom face, the subtraction cuts a notch out of the wall instead of a hole. A cutter through both the floor and the top of the wall still throws `CoverageGap`.
- The 2D offsets are back as statics. `OpenGeometry.offsetRingVariable(ring, distances, holes?)` insets a closed ring by one distance per edge, each hole grown by its own distances. `OpenGeometry.offsetPolylineRegions(path, width, closed, miterLimit?)` strokes a path, with mitres up to `miterLimit` (default 4) and bevels past it. Both take 2.0's flat `[x, y, z, ...]` lists or a `Float64Array` and return `CurvedRegion2D[]`, exact line and arc edges in XZ. `OpenGeometry.offsetPolyline(path, distance, options?)` replaces 2.0's `polyline.getOffset(distance, acuteThresholdDegrees, bevel)`. It returns `OffsetPath2D`, `{ edges, bevelled, closed }`, and its options are `{ acuteThresholdDegrees?, bevel?, closed? }`, with defaults 35 and `true`. When `closed` is left out, a path whose ends meet is closed. The entry exports the types `OffsetHole`, `OffsetPath2D` and `OffsetPolylineOptions`.
- `OpenGeometry.offsetCurvedRingVariable(region, distances, holeDistances = [])` insets one `CurvedRegion2D` of lines and arcs by one distance (finite, >= 0) per edge, with one list per hole ring, and returns `CurvedRegion2D[]`. An arc edge stays on its centre: a convex arc moves to `r - d`, a concave arc or a hole's arc to `r + d`. A distance of 0 keeps its edge. A distance larger than an arc's radius, a wrong count, or a negative or non-finite distance is `InvalidParameter`. A collapsed result is an empty list. `offsetRingVariable` is unchanged.
- `OG_PRIMITIVE_SPHERE` makes a solid sphere: `{ radius }`, centred on the `plane` origin.
- `OG_PRIMITIVE_CONE` makes a solid cone or frustum standing on the `plane`, its axis along the normal: `{ radiusBottom, radiusTop, height }`. One radius may be `0` for a pointed cone; radii within four times the geometric tolerance of each other make a cylinder.
- `OG_PRIMITIVE_TORUS` makes a solid ring torus lying on the `plane`, its axis along the normal: `{ majorRadius, minorRadius }`, with the minor radius below the major one.
Expand Down Expand Up @@ -605,6 +592,11 @@ console.log(pan.getReport()?.handlers, cistern.getReport()?.handlers, bowl.ogId)
- An `operate` call (subtract, union or intersect) whose intersection reaches the apex of a cone throws `CoverageGap`. `details.families` names the pair, for example `['cone or frustum', 'cuboid']` or `['cylinder', 'noncoaxial cone or frustum']`. The `message` names `'singular surface point'`.
- When that failure is `CoverageGap`, `details` also has `families`. It names the specialised gap the kernel met, such as `['cylinder', 'oblique cuboid intersection']`; for two planar bodies, that of the last box handler tried. With no specialised gap, it is the error's own pair. For most gaps the `message` names only the generic pair, `generic analytic solid` and `intersecting analytic solid`, so read `details.families` for the specialised one. The `code` is `CoverageGap`.
- `flush({ geometry: 'sync', bodies })` checks its input before it meshes anything. A body that was disposed, or that comes from a reset runtime, throws `Disposed`. `bodies` without `geometry: 'sync'`, a `bodies` that is not an array, and an item that is not a Wire, a Sheet or a Solid throw `InvalidParameter`. Hidden bodies, bodies with no shape and bodies disposed while a mark is live are skipped.
- A body counts as displayed when it is drawn, not only when `updateMatrixWorld` walks it: its surface or outline `onBeforeRender`, or `OpenGeometry.markDisplayed(bodies)`, stamps it for the current render pass, and its new display geometry is installed after that render, so the next frame shows it. A body that is neither drawn nor walked for a full pass is no longer displayed and gets no level-of-detail target or record swap. A walk still stamps and installs at once, so code that walks, then calls `OpenGeometry.flush({ geometry: 'sync' })`, gets the same meshes as before.
- Display buffers leave smooth edges out of `outline` and `edgeIds`. An edge is smooth when its two faces are tangent-continuous, or when two planes fold by less than the smooth angle, 20° by default. The outline of a polygon that approximates a curve therefore loses its fold lines, and planar faces average their vertex normals across smooth edges, so the shading no longer shows the facets. A new `smoothEdges` `Uint8Array` gives one byte per topology edge, 1 for smooth.
- `appearance.smoothAngle` sets the angle per body, in degrees from 0 up to but not including 90; `0` restores the previous outline and normals. `OGWorldGraph.buffers` and `OGTessellator.buffers` take it as an optional fourth argument.
- Rust: `SnapshotStore::buffers()`, `WorldGraph::buffers()` and `display_buffers()` now default to 20°, not 0°. `tessellate_with_smooth_angle`, `display_buffers_with_smooth_angle` and `buffers_with_smooth_angle` take an explicit angle, and `SMOOTH_ANGLE` is the default. `tessellate()` stays at 0°.
- A body's `surface` and `outline` meshes are static: their `matrixAutoUpdate` is `false`, so a world matrix update no longer recomputes their local matrix. Code that writes their `position`, `rotation`, `quaternion` or `scale` must call `updateMatrix()` on that mesh after the write.
- `OGError.details` is typed as `ErrorDetails`, which the package exports with `OperateErrorDetails`. A `details` value that is not an object becomes `{}`.
- Point classification counts each solid of a body on its own, outer shell and cavities. A body is Inside if any solid holds the point. Before, the faces of all solids were counted together, so a point inside one of two touching solids could read Outside, and a Boolean that tested such a point could go wrong.
- `classifyPoint` returns `'Boundary'` for a point within the geometric tolerance (`1e-8`) of the body's surface: on faces at any rotation, on seams, rims, edges, corners, sphere poles and a cone apex. Limit: the face test follows a circle, an ellipse or a full-turn line trim as 64 straight pieces, and an intersection curve as 128. So near such a trim the answer can be wrong. A point a little off a cylinder or cone cap near its rim (within about 1.2e-3 times the radius) can get the wrong side. A point on the surface just past a cut edge can read `'Boundary'`, up to about 7e-5 from the body on a cylinder minus a cuboid tilted 45 degrees. A point 1e-7 off a sphere pole or a cone apex can read `'Unknown'`.
Expand Down
Loading
Loading