Skip to content
Draft
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
136 changes: 136 additions & 0 deletions docs/proposals/assembly-explosion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Assembly Explosion Stage and Move Elements

`assembly.explosion.stage` names an explosion step, and
`assembly.explosion.move` declares one straight movement. Stages are direct
children of their owning device or subassembly. There is no explosion container,
`run`, `detach`, `translate`, or explicit collision-check element.

```tsx
<assembly.device name="instrument">
<assembly.part name="base" modelUrl="./base.step" />

<assembly.subassembly name="cover">
<assembly.part name="shell" modelUrl="./shell.step" />
<assembly.part name="window" modelUrl="./window.step" />

<assembly.explosion.stage name="remove-window">
<assembly.explosion.move targetRef="window" offZ="15mm" />
</assembly.explosion.stage>
</assembly.subassembly>

<assembly.explosion.stage name="lift-cover">
<assembly.explosion.move targetRef="cover" offZ="12mm" />
</assembly.explosion.stage>

<assembly.explosion.stage
name="remove-cover"
nextStageRef="cover.remove-window"
>
<assembly.explosion.move targetRef="cover" offX="60mm" />
</assembly.explosion.stage>
</assembly.device>
```

The intended sequence lifts the intact cover, moves it sideways, then removes
the window at the cover's new location.

## Accepted props and canonical output

`AssemblyExplosionStageProps` / `assemblyExplosionStageProps` accept:

- `name`: required nonblank string; stable identity unique within its assembly.
- `nextStageRef`: optional nonblank string identifying a stage that executes later.
- `prevStageRef`: optional nonblank string identifying a stage that executes earlier.
- `children`: optional React nodes, preserved unchanged and in authored order.

Identity and reference strings are trimmed. Omitted optional props remain omitted;
no ordering references or children are synthesized.

`AssemblyExplosionMoveProps` / `assemblyExplosionMoveProps` accept:

- `targetRef`: required nonblank string identifying a part or subassembly relative
to the stage owner. Qualified names can address descendants. React reserves
`ref`, so this API uses an ordinary prop named `targetRef`.
- `offX`, `offY`, `offZ`: optional signed distances, with at least one required.
Numbers mean millimeters; unit strings use the existing distance parser.
Parsed offsets are finite numbers in millimeters. Negative and explicit zero
offsets are accepted. Omitted axes stay omitted and mean zero in the consumer.

All supplied axes move together along one straight segment, incrementally from
the current pose. Displacements use the owning assembly's coordinate frame,
captured when the move begins. Sequential moves accumulate. An L-shaped path
uses two moves rather than one diagonal move:

```tsx
<assembly.explosion.stage name="extract-module">
<assembly.explosion.move targetRef="module" offZ="10mm" />
<assembly.explosion.move targetRef="module" offX="40mm" />
</assembly.explosion.stage>
```

These are additive APIs with no aliases or migration of existing assembly props.
Unknown fields are stripped, matching existing assembly Zod objects; TypeScript
rejects fields outside the declared interfaces. Empty references, invalid or
nonfinite distances, and moves without offsets fail props validation.

Validators are exported individually and as `assemblyProps.explosion.stage` and
`assemblyProps.explosion.move`. `assemblyExplosionProps` exposes the same nested
namespace. The namespace is a registry, not a validator for a container element.

## Intended core behavior

This package supplies types and props validation. Core must supply the actual
namespaced JSX elements, stage scheduling, movement, and assembly checks. The
example syntax requires that corresponding core support; this props change does
not execute or verify a disassembly.

Within a root assembly, collect all stages after TSX expansion, including stages
inside subassemblies. Nesting defines ownership and coordinate frames, not an
automatic execution boundary. A stage named `remove-window` in `cover` can be
referenced from its parent as `cover.remove-window`. Short references resolve
within the owning assembly; qualified references traverse named subassemblies.
Do not guess between ambiguous references.

`prevStageRef` adds an edge from the referenced stage to this stage;
`nextStageRef` adds an edge from this stage to the referenced stage. These are
precedence constraints, not a requirement that stages be immediately adjacent.
Use a stable topological sort, selecting the earliest stage in depth-first TSX
document order among currently eligible stages. Document order is a fallback,
not an additional edge. Without references, stages follow document order.
Operations inside each stage execute sequentially in their authored order.

Core should report unresolved or ambiguous references, duplicate names within a
scope, self-references, cycles, and invalid move targets as check errors. Props
validation cannot resolve these relationships without the assembly tree.

Moves retain their resulting poses across stages. Moving a subassembly carries
all its descendants in their current relative poses. A removed subassembly stays
in the scene, and its child stages execute relative to its displaced frame.
Logical containment is preserved; removal does not hide geometry or require a
detach operation. Evaluation starts from the assembled pose, and viewer scrubbing
must reconstruct this state deterministically rather than repeatedly applying
offsets to a previously evaluated scene.

## Intended automatic assembly checks

Declaring explosion stages should include motion checks in the normal check
pipeline, without a collision-check element. The viewer and checker should use
the same resolved sequence and transforms.

Check each move's full swept path, not only its endpoints or animation frames.
Previously displaced geometry remains an obstacle. Moving a rigid subassembly
checks its constituent solids against external geometry; moving a child also
checks its path against its siblings and other surrounding solids. Validate the
initial geometry as well. Bounding boxes can accelerate candidate selection but
must not fill holes or cavities for the final collision decision.

Intentional mating contact needs support through assembly relationships and
tolerances; it must not exempt an entire part pair from path collision checks.
Missing collision geometry or unsupported constraints must produce incomplete
verification rather than a successful result.

Motion errors should identify the qualified stage, move index, moving and blocking
parts, and position along the segment so the viewer can replay the failing pose.
A verified reversible rigid-motion sequence establishes a geometric assembly
path within the modeled constraints. Fastening, cable behavior, tool access,
deformation, and manufacturing tolerances need additional modeling and checks.
28 changes: 28 additions & 0 deletions generated/PROPS_OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,34 @@ export interface AssemblyDeviceProps {
}


export interface AssemblyExplosionMoveProps {
/** Part or subassembly identity relative to the stage owner. Uses targetRef
* because React reserves ref rather than forwarding it as an ordinary prop.
*/
targetRef: string
/** Signed X displacement in the stage owner's frame, in mm or a unit string.
* Omitted axes mean zero in the consumer; parsed props remain omitted.
*/
offX?: Distance
/** Signed Y displacement in the stage owner's frame, in mm or a unit string. */
offY?: Distance
/** Signed Z displacement in the stage owner's frame, in mm or a unit string. */
offZ?: Distance
}


export interface AssemblyExplosionStageProps {
/** Stable stage identity, unique within the owning assembly. */
name: string
/** Stage that must execute after this one; qualified names may cross assembly scopes. */
nextStageRef?: string
/** Stage that must execute before this one; qualified names may cross assembly scopes. */
prevStageRef?: string
/** Ordered explosion moves; preserved unchanged for the consumer. */
children?: ReactNode
}


export interface AssemblyFastenerProps extends AssemblyPartProps {
/** Qualified reference surface, e.g. "FRAME.boltHole". Implies fastening.
* Without mountFace, use the fastener's authored local mounting origin.
Expand Down
11 changes: 11 additions & 0 deletions lib/assembly/explosion/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { assemblyExplosionMoveProps } from "./move"
import { assemblyExplosionStageProps } from "./stage"

export * from "./move"
export * from "./stage"

/** Explosion elements; no separate explosion container is needed. */
export const assemblyExplosionProps = {
stage: assemblyExplosionStageProps,
move: assemblyExplosionMoveProps,
} as const
47 changes: 47 additions & 0 deletions lib/assembly/explosion/move.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { type Distance, distance } from "lib/common/distance"
import { expectTypesMatch } from "lib/typecheck"
import { z } from "zod"

/** One straight, incremental movement within an explosion stage. */
export interface AssemblyExplosionMoveProps {
/** Part or subassembly identity relative to the stage owner. Uses targetRef
* because React reserves ref rather than forwarding it as an ordinary prop.
*/
targetRef: string
/** Signed X displacement in the stage owner's frame, in mm or a unit string.
* Omitted axes mean zero in the consumer; parsed props remain omitted.
*/
offX?: Distance
/** Signed Y displacement in the stage owner's frame, in mm or a unit string. */
offY?: Distance
/** Signed Z displacement in the stage owner's frame, in mm or a unit string. */
offZ?: Distance
}

const offset = distance.pipe(z.number().finite())

export const assemblyExplosionMoveProps = z
.object({
targetRef: z.string().trim().min(1),
offX: offset.optional(),
offY: offset.optional(),
offZ: offset.optional(),
})
.refine(
(move) =>
move.offX !== undefined ||
move.offY !== undefined ||
move.offZ !== undefined,
{
message: "Provide at least one of offX, offY, or offZ",
path: ["offX"],
},
)

export type AssemblyExplosionMovePropsInput = z.input<
typeof assemblyExplosionMoveProps
>

expectTypesMatch<AssemblyExplosionMoveProps, AssemblyExplosionMovePropsInput>(
true,
)
30 changes: 30 additions & 0 deletions lib/assembly/explosion/stage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { expectTypesMatch } from "lib/typecheck"
import type { ReactNode } from "react"
import { z } from "zod"

/** A named explosion stage owned by its containing device or subassembly. */
export interface AssemblyExplosionStageProps {
/** Stable stage identity, unique within the owning assembly. */
name: string
/** Stage that must execute after this one; qualified names may cross assembly scopes. */
nextStageRef?: string
/** Stage that must execute before this one; qualified names may cross assembly scopes. */
prevStageRef?: string
/** Ordered explosion moves; preserved unchanged for the consumer. */
children?: ReactNode
}

export const assemblyExplosionStageProps = z.object({
name: z.string().trim().min(1),
nextStageRef: z.string().trim().min(1).optional(),
prevStageRef: z.string().trim().min(1).optional(),
children: z.custom<ReactNode>().optional(),
})

export type AssemblyExplosionStagePropsInput = z.input<
typeof assemblyExplosionStageProps
>

expectTypesMatch<AssemblyExplosionStageProps, AssemblyExplosionStagePropsInput>(
true,
)
3 changes: 3 additions & 0 deletions lib/assembly/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { assemblyPrintedPartProps } from "./printedpart"
import { assemblyMotorProps } from "./motor"
import { assemblyScreenProps } from "./screen"
import { assemblySubassemblyProps } from "./subassembly"
import { assemblyExplosionProps } from "./explosion"

export * from "./board-mounting"
export * from "./bolt"
Expand All @@ -22,8 +23,10 @@ export * from "./printedpart"
export * from "./screen"
export * from "./subassembly"
export * from "./cadassembly"
export * from "./explosion"

export const assemblyProps = {
explosion: assemblyExplosionProps,
bolt: assemblyBoltProps,
fastener: assemblyFastenerProps,
cable: assemblyCableProps,
Expand Down
Loading
Loading