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
7 changes: 7 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ 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

### 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.
- 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 Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ This section is a compact reference for writing OpenGeometry 3.0 code, by hand o
| `SystemAssembly` | `new SystemAssembly({ ogId?, parent? })`. `addChild(children, { keepWorld? })`, `removeChild(child, { keepWorld? })`, `getChildren()`, `getParent()`, `getBounds()`, `transform`, `dispose()`. Bodies have the same tree methods. |
| Queries | `getBounds()` (`[minX, minY, minZ, maxX, maxY, maxZ]`, or `null`), `getBrep()`, `getWorldBrep()` (the BRep placed in the world), `faceNormalAt(faceId, [x, y, z])` (the face's unit normal in the body's own coordinates), `classifyPoint([x, y, z])` (`'Inside'`, `'Outside'`, `'Boundary'` or `'Unknown'`, in the body's own coordinates), `measure({ deflection? })` (`{ volume, area, errorBound, areaErrorBound, deflection }`; the volume is within `errorBound` of the exact one, the area within `areaErrorBound`), `validity()` (`{ valid, closed, bodyType, error? }`), `solid.getReport()` (the last boolean's report, or `null`). |
| Appearance | `appearance` in the options, or `body.setAppearance({ color?, opacity?, outline?, pickOutline?, deflection? })`. |
| Display | `OpenGeometry.settled({ deflection? })` resolves to `{ failed }`; `OpenGeometry.flush({ geometry?: 'sync', bodies? })` (with `bodies`, meshes and places only those bodies now; hidden bodies, bodies with no shape and bodies in limbo (disposed while a mark is live) are skipped; a fully disposed body or one from a reset runtime throws `Disposed` before anything is meshed; `bodies` without `geometry: 'sync'`, a non-array `bodies` or an item that is not a Wire, Sheet or Solid throws `InvalidParameter`); `OpenGeometry.setDisplayDeflection(world?)`; `OpenGeometry.setCameraMotion(moving)`; `OpenGeometry.releaseIdleGeometry()` disposes the display geometry cached for bodies not showing it and returns `{ records, bytes }`. |
| Display | `OpenGeometry.settled({ deflection? })` resolves to `{ failed }`; `OpenGeometry.flush({ geometry?: 'sync', bodies? })` (with `bodies`, meshes and places only those bodies now; hidden bodies, bodies with no shape and bodies in limbo (disposed while a mark is live) are skipped; a fully disposed body or one from a reset runtime throws `Disposed` before anything is meshed; `bodies` without `geometry: 'sync'`, a non-array `bodies` or an item that is not a Wire, Sheet or Solid throws `InvalidParameter`); `OpenGeometry.setDisplayDeflection(world?)`; `OpenGeometry.setCameraMotion(moving)`; `OpenGeometry.observeCamera(renderer, camera)` observes the first camera seen per renderer frame (`renderer.info.render.frame`) for level of detail; a `render()` nested inside an `onBeforeRender` (Reflector style) starts a new frame, and an `ArrayCamera` is observed through its first sub-camera (a host whose drawables are not kernel bodies, such as instanced or batched layers, calls it from their `onBeforeRender`); `OpenGeometry.releaseIdleGeometry()` disposes the display geometry cached for bodies not showing it and returns `{ records, bytes }`; `OpenGeometry.markDisplayed(bodies)` counts those bodies as displayed for the current render pass (a host that draws bodies through its own instanced or batched layers calls it from that layer's `onBeforeRender`; it works for a body hidden by layer, skips a body with `visible = false`, and validates `bodies` as `flush({ bodies })` does); `OpenGeometry.setHostDrawn(bodies, hostDrawn)` hands the drawing of those bodies to the host (`true`) or gives it back (`false`): while host-drawn, a body's `visible` is `false`, so three.js skips it and everything under it, and the kernel keeps the body's own visibility apart, so `markDisplayed`, a walk, `flush({ geometry: 'sync', bodies })`, level of detail and record swaps treat it as shown, and a dispose, a rollback or a reset changes that visibility instead of `visible`; giving it back sets `visible` to that visibility (a host calls it when a body joins or leaves its instanced or batched layers; do not set `visible` on a host-drawn body; it validates `bodies` as `flush({ bodies })` does, except that giving bodies back ignores a disposed or finalized body, and a `hostDrawn` that is not a boolean throws `InvalidParameter`); `OpenGeometry.hostGroup(bodies)` returns a `HostGroup` that stamps all its bodies at once: `group.drawn()` counts every member as displayed for the current render pass exactly as `markDisplayed` does, at a cost that does not grow with the number of members (a host calls it from each instanced or batched layer's `onBeforeRender` in place of `markDisplayed`), `group.set(bodies)` replaces the members and `group.dispose()` ends the group, after which `drawn()` does nothing and its members no longer count through it; a body may be in several groups, a hidden or host-drawn member keeps its own visibility and gets its geometry when it is shown again through `setHostDrawn` or a rollback, a member that is not host-drawn and has `visible` set to `true` later gets it when it is drawn or walked, a member disposed later leaves its groups, a group that is dropped without `dispose()` is ended when it is garbage collected, `hostGroup` and `set` validate `bodies` once as `markDisplayed` does, and `set` on a disposed group or one from a reset runtime throws `Disposed`. A body is displayed when it is drawn (its own `onBeforeRender` or `markDisplayed`) or walked by `updateMatrixWorld`; a draw installs new display geometry after the render, a walk at once. Each body dispatches `geometrychanged` after it installs new display geometry and `placementchanged` when a flush moves it (`BodyEventMap`); their listeners run synchronously, after the whole flush is applied or at the end of the render pass, and a listener that throws is reported through `OpenGeometry.on('error', …)` with the thrown value while the flush and the pass complete. |
| Picking | `OpenGeometry.resolveHit(intersection)` takes a Three.js raycast hit and returns `{ ogId, shapeRevision, faceId?, edgeId? }` or `undefined`. |
| `OGMark` | `const mark = OpenGeometry.mark()`, then `mark.rollback()` and `mark.release()`. `OpenGeometry.markStats()` returns `{ liveMarks, retainedRevisions }`. `OpenGeometry.transaction(fn, { dryRun? })` runs a synchronous `fn` and rolls back if it throws. |
| Events | `OpenGeometry.on(event, handler)` with `'geometry'`, `'warning'`, `'error'` or `'fatal'`; it returns a function that unsubscribes. |
Expand Down
2 changes: 2 additions & 0 deletions main/opengeometry-three/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export { OpenGeometry } from './src/opengeometry';
export { OGMark } from './src/marks/og-mark';
export type { HostGroup } from './src/rendering/geometry/host-group';
export { SystemAssembly } from './src/bodies/system-assembly';
export { Wire } from './src/bodies/wire';
export { Solid } from './src/bodies/solid';
export { Sheet } from './src/bodies/sheet';
export type { BodyEventMap } from './src/bodies/body';
export type { PointClassification } from './src/bodies/node-methods';
export type { Measure, Validity } from './src/dto/measure';
export type { MirrorPlane } from './src/dto/mirror';
Expand Down
2 changes: 2 additions & 0 deletions main/opengeometry-three/src/bodies/body-registry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { dropMember } from '../rendering/lod/host-groups';
import { bodyKey, reindexShape, releaseShapeIfEmpty, runtime } from '../runtime/runtime-state';
import type { Body } from './body';

Expand All @@ -11,6 +12,7 @@ export function register(body: Body): void {
export function unregister(body: Body): void {
const state = runtime();
state.displayed.delete(body);
dropMember(state, body);
state.displayBounds.delete(body);
state.lod.stale = true;
state.limbo.delete(body);
Expand Down
27 changes: 18 additions & 9 deletions main/opengeometry-three/src/bodies/body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import type { Placement } from '../dto/placement';
import { OGError } from '../errors';
import { call } from '../kernel/kernel-session';
import { enterLimbo, revive } from '../marks/limbo';
import { flush, flushForDispose, wanted } from '../rendering/geometry/geometry-scheduler';
import { noteDisplayed } from '../rendering/geometry/render-pass';
import { flush, flushForDispose, notifyBody, wanted } from '../rendering/geometry/geometry-scheduler';
import { noteDisplayed, noteDrawn } from '../rendering/geometry/render-pass';
import { observeCamera } from '../rendering/lod/camera-observer';
import { isDisplayed, overrideChanged } from '../rendering/lod/lod-controller';
import { shown } from '../rendering/lod/host-drawn';
import { isDisplayed } from '../rendering/lod/displayed';
import { overrideChanged } from '../rendering/lod/lod-controller';
import {
lineMaterial, releaseLine, releaseSurface, surfaceMaterial, type MaterialAppearance,
} from '../rendering/materials/material-pool';
Expand All @@ -24,7 +26,7 @@ import { register, unregister } from './body-registry';
import { DisplayClone } from './display-clone';
import {
addChild, checkNode, classifyPoint, faceNormalAt, getBounds, getBrep, getChildren, getInstanceCount, getParent,
getPlacement, getWorldBrep, getWorldPlacement, localBounds, measure, mirror, removeChild, validity,
getPlacement, getWorldBrep, getWorldPlacement, localBounds, measure, mirror, removeChild, transformNode, validity,
} from './node-methods';
import type { PointClassification } from './node-methods';
import type { SystemAssembly } from './system-assembly';
Expand All @@ -38,7 +40,9 @@ function lineStyle(bodyType: 'Wire' | 'Sheet' | 'Solid', appearance: Appearance)
return { color: bodyType === 'Wire' ? appearance.color : 0x243040, opacity: appearance.opacity };
}

export abstract class Body extends THREE.Group {
export type BodyEventMap = THREE.Object3DEventMap & { geometrychanged: object; placementchanged: object };

export abstract class Body extends THREE.Group<BodyEventMap> {
readonly ogId: string;
readonly handle: number;
readonly generation: number;
Expand Down Expand Up @@ -74,15 +78,17 @@ export abstract class Body extends THREE.Group {
const surfaceStyle = surfaceMaterial(this.appearance);
this.surfaceKey = surfaceStyle.key;
this.surface = new THREE.Mesh(placeholder ?? new THREE.BufferGeometry(), surfaceStyle.material);
this.surface.matrixAutoUpdate = false;
this.surface.visible = Boolean(placeholder) && bodyType !== 'Wire';
const outlineStyle = lineMaterial(lineStyle(bodyType, this.appearance));
this.lineKey = outlineStyle.key;
this.outline = new THREE.LineSegments(placeholder ?? new THREE.BufferGeometry(), outlineStyle.material);
this.outline.matrixAutoUpdate = false;
this.outline.visible = Boolean(placeholder) && this.appearance.outline;
if (!this.appearance.pickOutline) this.outline.raycast = THREE.Object3D.prototype.raycast.bind(this.outline);
this.add(this.surface, this.outline);
this.surface.onBeforeRender = (renderer, _scene, camera) => { observeCamera(renderer, camera); };
this.outline.onBeforeRender = (renderer, _scene, camera) => { observeCamera(renderer, camera); };
this.surface.onBeforeRender = (renderer, _scene, camera) => { observeCamera(renderer, camera); noteDrawn(this); };
this.outline.onBeforeRender = (renderer, _scene, camera) => { observeCamera(renderer, camera); noteDrawn(this); };
register(this);
this.applyWorldMatrix(call(`${bodyType}.constructor`, () => worldGraph().worldMatrix(ogId)));
}
Expand All @@ -97,7 +103,7 @@ export abstract class Body extends THREE.Group {

override updateMatrixWorld(force?: boolean): void {
keepPose(this.pose, this, this.ogId);
if (this.visible) noteDisplayed(this);
if (shown(runtime(), this)) noteDisplayed(this);
super.updateMatrixWorld(force);
}

Expand All @@ -121,10 +127,13 @@ export abstract class Body extends THREE.Group {
this.outline.geometry = record.outline;
this.surface.position.copy(record.origin);
this.outline.position.copy(record.origin);
this.surface.updateMatrix();
this.outline.updateMatrix();
this.surface.updateMatrixWorld();
this.outline.updateMatrixWorld();
this.surface.visible = this.bodyType !== 'Wire';
this.outline.visible = this.appearance.outline;
notifyBody(this, 'geometrychanged');
}

setAppearance(partial: Partial<Appearance>): void {
Expand Down Expand Up @@ -153,7 +162,7 @@ export abstract class Body extends THREE.Group {

transform(kind: string, params: Record<string, unknown>): void {
this.check(`${this.bodyType}.transform`);
call(`${this.bodyType}.transform`, () => worldGraph().transform(this.ogId, encode({ kind, ...params })));
transformNode(this.bodyType, this.ogId, kind, params);
}

getPlacement(): Placement {
Expand Down
7 changes: 7 additions & 0 deletions main/opengeometry-three/src/bodies/node-methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,19 @@ export function getWorldPlacement(owner: string, ogId: string): Placement {
return decodePlacement(call(label, () => worldGraph().worldPlacement(ogId)), label);
}

export function transformNode(owner: string, ogId: string, kind: string, params: Record<string, unknown>): void {
call(`${owner}.transform`, () => worldGraph().transform(ogId, encode({ kind, ...params })));
runtime().graphMoved = true;
}

export function addChild(owner: string, ogId: string, children: { ogId: string }[], keepWorld: boolean): void {
call(`${owner}.addChild`, () => worldGraph().addChild(ogId, encode(children.map((child) => child.ogId)), keepWorld));
runtime().graphMoved = true;
}

export function removeChild(owner: string, ogId: string, childId: string, keepWorld: boolean): void {
call(`${owner}.removeChild`, () => worldGraph().removeChild(ogId, childId, keepWorld));
runtime().graphMoved = true;
}

export function getChildren(owner: string, ogId: string): string[] {
Expand Down
4 changes: 2 additions & 2 deletions main/opengeometry-three/src/bodies/system-assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { encode } from '../world-graph/codec';
import { creation, node, worldGraph } from '../world-graph/world-graph-client';
import type { Body } from './body';
import {
addChild, checkNode, getBounds, getChildren, getParent, getPlacement, getWorldPlacement, removeChild,
addChild, checkNode, getBounds, getChildren, getParent, getPlacement, getWorldPlacement, removeChild, transformNode,
} from './node-methods';

export class SystemAssembly {
Expand All @@ -28,7 +28,7 @@ export class SystemAssembly {

transform(kind: string, params: Record<string, unknown>): void {
this.check('SystemAssembly.transform');
call('SystemAssembly.transform', () => worldGraph().transform(this.ogId, encode({ kind, ...params })));
transformNode('SystemAssembly', this.ogId, kind, params);
}

getPlacement(): Placement {
Expand Down
7 changes: 4 additions & 3 deletions main/opengeometry-three/src/marks/limbo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Body } from '../bodies/body';
import { call } from '../kernel/kernel-session';
import { setShown } from '../rendering/lod/host-drawn';
import { bodyKey, reindexShape, runtime, type Runtime } from '../runtime/runtime-state';
import { node, worldGraph } from '../world-graph/world-graph-client';

Expand Down Expand Up @@ -29,12 +30,12 @@ export function revive(body: Body): void {
call('OGMark.rollback', () => worldGraph().nodeByHandle(body.handle, body.generation));
state.limbo.delete(body);
body.inLimbo = false;
body.visible = true;
setShown(state, body, true);
entry.parent?.add(body);
const previous = body.lastInfo.shapeId;
body.lastInfo = node(body.ogId, 'OGMark.rollback');
reindexShape(state, body, previous);
} catch { body.visible = false; }
} catch { setShown(state, body, false); }
}

export function finalizeNow(body: Body): void {
Expand All @@ -43,7 +44,7 @@ export function finalizeNow(body: Body): void {
}

function hide(state: Runtime, body: Body): void {
body.visible = false;
setShown(state, body, false);
state.displayed.delete(body);
const parent = body.parent;
if (state.flushing || state.renderPassActive) {
Expand Down
8 changes: 8 additions & 0 deletions main/opengeometry-three/src/opengeometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import { offsetPolyline, offsetPolylineRegions, offsetRingVariable } from './ope
import { booleanCurvedRegions2D, booleanRegions2D, insetCurvedRegion2D } from './operations/regions';
import { resolveHit } from './picking/resolve-hit';
import { flush } from './rendering/geometry/geometry-scheduler';
import { HostGroup } from './rendering/geometry/host-group';
import { markDisplayed, setHostDrawn } from './rendering/geometry/render-pass';
import { settled } from './rendering/geometry/settle';
import { observeCamera } from './rendering/lod/camera-observer';
import { motionHint, overrideChanged } from './rendering/lod/lod-controller';
import { createRuntimeOnce } from './runtime/create-runtime';
import { on } from './runtime/event-bus';
Expand All @@ -36,6 +39,9 @@ export class OpenGeometry {

static on(event: Events, handler: Listener): () => boolean { return on(event, handler); }
static flush(options: { geometry?: 'sync'; bodies?: readonly Body[] } = {}): void { flush(options); }
static markDisplayed(bodies: readonly Body[]): void { markDisplayed(bodies); }
static setHostDrawn(bodies: readonly Body[], hostDrawn: boolean): void { setHostDrawn(bodies, hostDrawn); }
static hostGroup(bodies: readonly Body[]): HostGroup { return new HostGroup(bodies); }
static settled(options: { deflection?: number } = {}): Promise<{ failed: { ogId: string; error: unknown }[] }> {
return settled(options);
}
Expand All @@ -55,6 +61,8 @@ export class OpenGeometry {
if (moving) motionHint(state, Date.now());
}

static observeCamera(renderer: THREE.WebGLRenderer, camera: THREE.Camera): void { observeCamera(renderer, camera); }

static resolveHit(
hit: THREE.Intersection,
): { ogId: string; shapeRevision: number; faceId?: number; edgeId?: number } | undefined {
Expand Down
Loading
Loading