Skip to content
Open
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
19 changes: 19 additions & 0 deletions .changeset/architecture-doors-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@scifi-kit/registry": patch
---

Add the Axiom Relay doors group: ten procedural architecture modules covering
the shared portal, the hardened swing leaves, the powered and rolling closures,
a glazed shopfront leaf, a damage variant, and a floor hatch.

All ten are built on a new `axiom-door-kit` support item. It fixes one clear
opening — 1.06 m by 2.00 m, centred on a 1.6 m by 2.6 m plate — so every leaf in
the group hangs in the same doorway, and carries the shared portal plate, reveal
ring, threshold, jamb strips, head lamp, control plate, hinge stack, and leaf
construction. The kit also adds the lit RED-500 and CYAN-400 lamps the doors
brief asks for, at a lower emissive tier than the cargo wave's, because a door
lights an 850 mm jamb strip where a crate lights a 70 mm lens.

The hangar door widens to the brief's 2.8 m double envelope and the floor hatch
lays the same language flat on a 1.2 m deck plate; both keep the group's
clipped-corner outline and shell-over-graphite-over-ink value order.
105 changes: 105 additions & 0 deletions assets/prototypes/airlock-door/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Group } from 'three/webgpu'

import { box, type CargoPreview } from '../axiom-cargo-kit/index.ts'
import {
DOOR_KIT,
LEAF_HALF,
PANEL_FRONT,
buildPortal,
createDoorModel,
createDoorPreview,
leafHandle,
leafPanel,
leafRibs,
leafSkin,
signalLamp,
steppedSeam,
visionPort,
type DoorModel,
type DoorPreviewOptions,
} from '../axiom-door-kit/index.ts'

/**
* Axiom Relay airlock door — the pressure-rated leaf with a vision port.
*
* What separates it from the blast door is not thickness, it is *procedure*: an
* airlock is a door you are meant to look through before you open, and whose
* seal state has to be readable before you touch it. So it gets the family's
* only glazed leaf opening, an inflatable-seal bead standing proud of the skin,
* and a cyan equalisation read next to the amber lock state — two signals,
* because "pressure equal" and "door unlocked" are different facts and a crew
* that conflates them opens onto vacuum.
*/

export function createModel(): DoorModel {
return createDoorModel({
id: 'airlock-door',
condition: 0.3,
build: ({ m, bundle, part, root }) => {
const frame = part('frame')
const leaf = part('leaf')
const amber = signalLamp(bundle, 'AMBER-400', 6_140)

buildPortal(frame, m, { signal: amber })

leafSkin(leaf, m)
steppedSeam(leaf, m)
leafHandle(leaf, m)
visionPort(leaf, m, [0.3, 0.36], [-LEAF_HALF[0] * 0.34, DOOR_KIT.centreY + 0.42])
leafPanel(leaf, m, amber, [LEAF_HALF[0] * 0.66, DOOR_KIT.centreY - 0.02])
leafRibs(leaf, m)

// Inflatable seal bead around the leaf edge. Rubber, proud, and continuous
// — the one part that explains why this door is rated and the others
// are not.
for (const sy of [-1, 1]) {
box(leaf, m.rubber, [LEAF_HALF[0] * 1.72, 0.03, 0.04], [
0, DOOR_KIT.centreY + sy * (LEAF_HALF[1] - 0.045), PANEL_FRONT - 0.028,
], { chamfer: 0.01, fillet: 0.005, bevel: 0.004 })
}
for (const sx of [-1, 1]) {
box(leaf, m.rubber, [0.03, LEAF_HALF[1] * 1.68, 0.04], [
sx * (LEAF_HALF[0] - 0.045), DOOR_KIT.centreY, PANEL_FRONT - 0.028,
], { chamfer: 0.01, fillet: 0.005, bevel: 0.004 })
}

// Equalisation valve: the physical thing the cyan read is about.
box(leaf, m.graphite, [0.11, 0.11, 0.05], [-LEAF_HALF[0] * 0.34, DOOR_KIT.centreY - 0.5, PANEL_FRONT + 0.018], {
chamfer: 0.03, fillet: 0.01, bevel: 0.008,
})
box(leaf, m.steel, [0.13, 0.028, 0.028], [-LEAF_HALF[0] * 0.34, DOOR_KIT.centreY - 0.5, PANEL_FRONT + 0.05], {
chamfer: 0.008, fillet: 0.004, bevel: 0.004,
})

const swing = new Group()
swing.name = 'AXR_ARCH_AIRLOCK-DOOR_PART_SWING_CLOSED'
swing.position.set(-LEAF_HALF[0], 0, 0)
leaf.position.set(LEAF_HALF[0], 0, 0)
swing.add(leaf)
root.add(swing)

return {
assemblies: [swing],
cycleSeconds: 2.6,
apply: (blend) => {
swing.rotation.y = blend * 1.7
},
sockets: {
fx_seal_bead: [0, DOOR_KIT.centreY + LEAF_HALF[1], PANEL_FRONT],
pipe_equalise: [-LEAF_HALF[0] * 0.34, DOOR_KIT.centreY - 0.5, PANEL_FRONT],
},
tick: (elapsed) => {
amber.emissiveIntensity = 0.72 + Math.sin(elapsed * 1.2) * 0.12
},
}
},
})
}

export function createPreview(options: DoorPreviewOptions = {}): CargoPreview {
return createDoorPreview(createModel(), options)
}

export function createOpenPreview(options: DoorPreviewOptions = {}): CargoPreview {
return createDoorPreview(createModel(), { ...options, state: 'open' })
}
70 changes: 70 additions & 0 deletions assets/prototypes/axiom-door-kit/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Vec3 } from '../../../src/asset-forge/generator/index.ts'

/**
* The one set of datums every Axiom Relay door module is built on.
*
* The doors brief gives each leaf the same public envelope — 1.6 m wide, 0.30 m
* deep, 2.6 m tall — and that only buys interchangeability if the *opening*
* inside it is shared too. A hangar door and a laboratory door authored to
* private clear dimensions are two props that happen to be the same size on the
* outside; authored to these numbers they are the same doorway wearing two
* leaves, which is what a kit is.
*
* The opening is centred on the plate on purpose. A head band deeper than the
* sill band is how real doors are built, but it also means the frame can no
* longer be cut as one symmetric plate with one centred octagonal hole, and the
* reference sheets show a symmetric plate. Symmetry wins: the sill band carries
* a threshold plate on top of it, which restores the asymmetry where it reads.
*/
export const DOOR_KIT = Object.freeze({
/** Public envelope from the doors brief. */
width: 1.6,
height: 2.6,
depth: 0.3,
/** Clear structural opening, centred on the plate. */
clearWidth: 1.06,
clearHeight: 2,
/** Opening centre height; also the plate's own centre. */
centreY: 1.3,
/** Corner clip on the opening, and the larger one on the outer edge. */
clip: 0.2,
outerClip: 0.26,
/** Depth of the shell frame plate; the graphite reveal runs deeper behind it. */
platePitch: 0.24,
/** Face the operational side points at, per the brief's orientation rule. */
front: '+Z',
/** Ground contact, centred in width, on the wall plane. */
pivot: 'ground-centre',
/** Double-leaf variants widen to this. */
doubleWidth: 2.8,
} as const)

export interface DoorEnvelope {
readonly width: number
readonly depth: number
readonly height: number
}

export interface DoorMetadata {
readonly version: 1
readonly moduleId: string
readonly pivot: typeof DOOR_KIT.pivot
readonly front: typeof DOOR_KIT.front
readonly envelope: DoorEnvelope
readonly clear: { readonly width: number; readonly height: number }
}

/** Half-extents of the clear opening, the figure most builders actually want. */
export const CLEAR_HALF: readonly [number, number] = [
DOOR_KIT.clearWidth * 0.5,
DOOR_KIT.clearHeight * 0.5,
]

/** Socket positions shared by every module in the family. */
export const DOOR_SOCKETS: Readonly<Record<string, Vec3>> = Object.freeze({
door_threshold: [0, 0.06, 0],
door_head: [0, DOOR_KIT.centreY + CLEAR_HALF[1], 0],
mount_left: [-DOOR_KIT.width * 0.5, DOOR_KIT.centreY, 0],
mount_right: [DOOR_KIT.width * 0.5, DOOR_KIT.centreY, 0],
power_head: [DOOR_KIT.width * 0.5 - 0.14, DOOR_KIT.height - 0.18, -DOOR_KIT.depth * 0.4],
})
Loading