From 06c3711ebcc59bab56a3c9eb2f4a4ab9d9f8cba4 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Wed, 15 Apr 2026 09:07:24 +0200 Subject: [PATCH] Add functions for cone and torus geometry calculations in whitebox-topology.ts, update dialogue handling in runtime-host.ts --- src/geometry/whitebox-topology.ts | 73 +++++++++++++++++- src/runtime-three/runtime-host.ts | 121 +++++++++++++++++++++++++++++- 2 files changed, 189 insertions(+), 5 deletions(-) diff --git a/src/geometry/whitebox-topology.ts b/src/geometry/whitebox-topology.ts index 319ec869..f421f63f 100644 --- a/src/geometry/whitebox-topology.ts +++ b/src/geometry/whitebox-topology.ts @@ -196,11 +196,14 @@ function getRadialPrismFaceVertexIds( faceId: RadialPrismFaceId ): WhiteboxVertexId[] { if (faceId === "top") { - return Array.from({ length: brush.sideCount }, (_, index) => `top-${brush.sideCount - 1 - index}` as const); + return Array.from({ length: brush.sideCount }, (_, index) => `top-${index}` as const); } if (faceId === "bottom") { - return Array.from({ length: brush.sideCount }, (_, index) => `bottom-${index}` as const); + return Array.from( + { length: brush.sideCount }, + (_, index) => `bottom-${brush.sideCount - 1 - index}` as const + ); } const sideIndex = Number(faceId.slice(5)); @@ -234,6 +237,72 @@ function getRadialPrismEdgeVertexIds( return [`bottom-${index}`, `bottom-${nextIndex}`]; } +function getConeFaceVertexIds( + brush: ConeBrush, + faceId: ConeFaceId +): WhiteboxVertexId[] { + if (faceId === "bottom") { + return Array.from( + { length: brush.sideCount }, + (_, index) => `bottom-${brush.sideCount - 1 - index}` as const + ); + } + + const sideIndex = Number(faceId.slice(5)); + const nextIndex = (sideIndex + 1) % brush.sideCount; + return [`bottom-${sideIndex}`, `bottom-${nextIndex}`, "apex"]; +} + +function getConeEdgeVertexIds( + edgeId: ConeEdgeId, + sideCount: number +): [ConeVertexId, ConeVertexId] { + if (edgeId.startsWith("bottom-")) { + const index = Number(edgeId.slice(7)); + const nextIndex = (index + 1) % sideCount; + return [`bottom-${index}`, `bottom-${nextIndex}`]; + } + + const index = Number(edgeId.slice(5)); + return [`bottom-${index}`, "apex"]; +} + +function getTorusFaceVertexIds( + brush: TorusBrush, + faceId: TorusFaceId +): WhiteboxVertexId[] { + const [majorIndex, tubeIndex] = parseIndexedWhiteboxId(faceId, "face-"); + const nextMajorIndex = (majorIndex + 1) % brush.majorSegmentCount; + const nextTubeIndex = (tubeIndex + 1) % brush.tubeSegmentCount; + return [ + `vertex-${majorIndex}-${tubeIndex}`, + `vertex-${majorIndex}-${nextTubeIndex}`, + `vertex-${nextMajorIndex}-${nextTubeIndex}`, + `vertex-${nextMajorIndex}-${tubeIndex}` + ]; +} + +function getTorusEdgeVertexIds( + brush: TorusBrush, + edgeId: TorusEdgeId +): [TorusVertexId, TorusVertexId] { + if (edgeId.startsWith("major-")) { + const [majorIndex, tubeIndex] = parseIndexedWhiteboxId(edgeId, "major-"); + const nextMajorIndex = (majorIndex + 1) % brush.majorSegmentCount; + return [ + `vertex-${majorIndex}-${tubeIndex}`, + `vertex-${nextMajorIndex}-${tubeIndex}` + ]; + } + + const [majorIndex, tubeIndex] = parseIndexedWhiteboxId(edgeId, "tube-"); + const nextTubeIndex = (tubeIndex + 1) % brush.tubeSegmentCount; + return [ + `vertex-${majorIndex}-${tubeIndex}`, + `vertex-${majorIndex}-${nextTubeIndex}` + ]; +} + export function getBrushFaceIds(brush: Brush): WhiteboxFaceId[] { switch (brush.kind) { case "box": diff --git a/src/runtime-three/runtime-host.ts b/src/runtime-three/runtime-host.ts index 92cc3593..4d53725a 100644 --- a/src/runtime-three/runtime-host.ts +++ b/src/runtime-three/runtime-host.ts @@ -263,6 +263,7 @@ export interface RuntimeSceneTransitionRequest { } export interface RuntimeDialogueState { + npcEntityId: string; dialogueId: string; title: string; lineId: string; @@ -712,9 +713,14 @@ export class RuntimeHost { return; } + const npc = + this.runtimeScene.entities.npcs.find( + (candidate) => candidate.entityId === this.currentDialogue?.npcEntityId + ) ?? null; const dialogue = - this.runtimeScene.dialogues.dialogues[this.currentDialogue.dialogueId] ?? - null; + npc?.dialogues.find( + (candidate) => candidate.id === this.currentDialogue?.dialogueId + ) ?? null; if (dialogue === null) { this.setRuntimeDialogue(null); @@ -729,7 +735,8 @@ export class RuntimeHost { } this.setRuntimeDialogue( - this.createRuntimeDialogueState( + this.createRuntimeNpcDialogueState( + this.currentDialogue.npcEntityId, dialogue.id, nextLineIndex, this.currentDialogue.source @@ -3287,6 +3294,9 @@ export class RuntimeHost { stopSound: (soundEmitterId) => { this.audioSystem.stopSound(soundEmitterId); }, + startNpcDialogue: (npcEntityId, dialogueId, source) => { + this.openRuntimeNpcDialogue(npcEntityId, dialogueId, source); + }, startDialogue: (dialogueId, source) => { this.openRuntimeDialogue(dialogueId, source); }, @@ -3311,6 +3321,50 @@ export class RuntimeHost { this.interactionPromptHandler?.(prompt); } + private createRuntimeNpcDialogueState( + npcEntityId: string, + dialogueId: string, + lineIndex: number, + source: RuntimeDialogueStartSource + ): RuntimeDialogueState | null { + if (this.runtimeScene === null) { + return null; + } + + const npc = + this.runtimeScene.entities.npcs.find( + (candidate) => candidate.entityId === npcEntityId + ) ?? null; + + if (npc === null) { + return null; + } + + const dialogue = npc.dialogues.find((candidate) => candidate.id === dialogueId); + + if (dialogue === undefined) { + return null; + } + + const line = dialogue.lines[lineIndex]; + + if (line === undefined) { + return null; + } + + return { + npcEntityId, + dialogueId, + title: dialogue.title, + lineId: line.id, + lineIndex, + lineCount: dialogue.lines.length, + speakerName: line.speakerName, + text: line.text, + source + }; + } + private createRuntimeDialogueState( dialogueId: string, lineIndex: number, @@ -3333,6 +3387,7 @@ export class RuntimeHost { } return { + npcEntityId: source.sourceEntityId ?? "legacy-dialogue-source", dialogueId, title: dialogue.title, lineId: line.id, @@ -3346,6 +3401,7 @@ export class RuntimeHost { private setRuntimeDialogue(dialogue: RuntimeDialogueState | null) { if ( + this.currentDialogue?.npcEntityId === dialogue?.npcEntityId && this.currentDialogue?.dialogueId === dialogue?.dialogueId && this.currentDialogue?.lineId === dialogue?.lineId && this.currentDialogue?.lineIndex === dialogue?.lineIndex && @@ -3389,6 +3445,65 @@ export class RuntimeHost { this.setRuntimeDialogue(dialogue); } + private openRuntimeNpcDialogue( + npcEntityId: string, + dialogueId: string | null, + source: RuntimeDialogueStartSource = { + kind: "direct", + sourceEntityId: null, + linkId: null, + trigger: null + } + ) { + if (this.runtimeScene === null) { + return; + } + + const npc = + this.runtimeScene.entities.npcs.find( + (candidate) => candidate.entityId === npcEntityId + ) ?? null; + + if (npc === null) { + console.warn(`dialogue: missing npc ${npcEntityId}`); + return; + } + + const resolvedDialogueId = + dialogueId ?? + npc.defaultDialogueId ?? + npc.dialogues[0]?.id ?? + null; + + if (resolvedDialogueId === null) { + console.warn(`dialogue: npc ${npcEntityId} has no dialogue to speak`); + return; + } + + if ( + this.currentDialogue?.npcEntityId === npcEntityId && + this.currentDialogue?.dialogueId === resolvedDialogueId + ) { + return; + } + + const dialogue = this.createRuntimeNpcDialogueState( + npcEntityId, + resolvedDialogueId, + 0, + source + ); + + if (dialogue === null) { + console.warn( + `dialogue: npc ${npcEntityId} is missing dialogue ${resolvedDialogueId}` + ); + return; + } + + this.setRuntimeDialogue(dialogue); + } + private resolveInteractionPrompt(): RuntimeInteractionPrompt | null { if ( this.runtimeScene === null ||