refactor: Beute-Frontend vereinfacht (findPart, subVec3, seededRandom)
Gemeinsame Helfer statt vier Kopien der Teilsuche, zwei Vektor- Subtraktionen und zwei Zufallsgeneratoren; hasPartSheet an einer Stelle; ungenutzte setOpacity und points-Ref entfernt. Verhalten unverändert (1050 Tests). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
0b0bc3c6dc
commit
9f6ab4ea3f
13 files changed
+69
-68
No files matched your search
@@ -11,6 +11,7 @@ import { berlinCalendarDay } from "@/lib/dates";
|
|||||||
import { hiveConflictOf, hiveKeys } from "@/lib/hive/api";
|
import { hiveConflictOf, hiveKeys } from "@/lib/hive/api";
|
||||||
import { updateHiveSystem } from "@/lib/hive/catalog-api";
|
import { updateHiveSystem } from "@/lib/hive/catalog-api";
|
||||||
import { describeHiveOps } from "@/lib/hive/describe";
|
import { describeHiveOps } from "@/lib/hive/describe";
|
||||||
|
import { findPart } from "@/lib/hive/find-part";
|
||||||
import { hiveLayout } from "@/lib/hive/layout";
|
import { hiveLayout } from "@/lib/hive/layout";
|
||||||
import { partDisplayName } from "@/lib/hive/names";
|
import { partDisplayName } from "@/lib/hive/names";
|
||||||
import { hiveReasonText } from "@/lib/hive/reason-text";
|
import { hiveReasonText } from "@/lib/hive/reason-text";
|
||||||
@@ -77,10 +78,6 @@ function historyFocusTarget(used: HTMLElement | null, other: HTMLElement | null,
|
|||||||
return enabled(used) ?? enabled(other) ?? heading;
|
return enabled(used) ?? enabled(other) ?? heading;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasPart(state: HiveState, id: string): boolean {
|
|
||||||
return state.stack.some((part) => part.id === id || (part.inserts ?? []).some((insert) => insert.id === id));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** True when the key event belongs to a text control or a dialog, where Ctrl+Z means something else. */
|
/** True when the key event belongs to a text control or a dialog, where Ctrl+Z means something else. */
|
||||||
function isTypingTarget(target: EventTarget | null): boolean {
|
function isTypingTarget(target: EventTarget | null): boolean {
|
||||||
return target instanceof Element && target.closest('input, textarea, select, [contenteditable="true"], [role="dialog"]') !== null;
|
return target instanceof Element && target.closest('input, textarea, select, [contenteditable="true"], [role="dialog"]') !== null;
|
||||||
@@ -128,7 +125,7 @@ function HiveEditorBody({ colonyId, setup, catalog, canManageTemplates, onRefres
|
|||||||
|
|
||||||
const hasOps = draft.ops.length > 0;
|
const hasOps = draft.ops.length > 0;
|
||||||
// A selection ends with its part (removed or undone).
|
// A selection ends with its part (removed or undone).
|
||||||
const selectedId = pickedId !== null && hasPart(draft.state, pickedId) ? pickedId : null;
|
const selectedId = pickedId !== null && findPart(draft.state, pickedId) !== undefined ? pickedId : null;
|
||||||
// Forget a vanished selection for good, so a redo does not bring it back.
|
// Forget a vanished selection for good, so a redo does not bring it back.
|
||||||
if (pickedId !== null && selectedId === null) setPickedId(null);
|
if (pickedId !== null && selectedId === null) setPickedId(null);
|
||||||
const layout = useMemo(() => hiveLayout(draft.state, catalog), [draft.state, catalog]);
|
const layout = useMemo(() => hiveLayout(draft.state, catalog), [draft.state, catalog]);
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ import { hiveErrorCode } from "@/lib/hive/api";
|
|||||||
import { hiveReasonText } from "@/lib/hive/reason-text";
|
import { hiveReasonText } from "@/lib/hive/reason-text";
|
||||||
import { describeHiveOps } from "@/lib/hive/describe";
|
import { describeHiveOps } from "@/lib/hive/describe";
|
||||||
import { deriveHiveFacts } from "@/lib/hive/facts";
|
import { deriveHiveFacts } from "@/lib/hive/facts";
|
||||||
|
import { findPart } from "@/lib/hive/find-part";
|
||||||
import { hiveLayout } from "@/lib/hive/layout";
|
import { hiveLayout } from "@/lib/hive/layout";
|
||||||
import { partDisplayName } from "@/lib/hive/names";
|
import { partDisplayName } from "@/lib/hive/names";
|
||||||
import { applyHiveOps } from "@/lib/hive/ops";
|
import { applyHiveOps } from "@/lib/hive/ops";
|
||||||
import { withPendingOps } from "@/lib/hive/pending";
|
import { withPendingOps } from "@/lib/hive/pending";
|
||||||
import type { HiveCatalog, HiveOp, HiveSetupView, HiveState } from "@/lib/hive/types";
|
import type { CatalogPartType, HiveCatalog, HiveOp, HiveSetupView, HiveState } from "@/lib/hive/types";
|
||||||
import { useConnectivity } from "@/lib/offline/connectivity";
|
import { useConnectivity } from "@/lib/offline/connectivity";
|
||||||
import { FrameSheet } from "./frame-sheet";
|
import { FrameSheet } from "./frame-sheet";
|
||||||
import { HiveConflictSheet } from "./hive-conflict-sheet";
|
import { HiveConflictSheet } from "./hive-conflict-sheet";
|
||||||
@@ -196,12 +197,14 @@ interface PastState {
|
|||||||
occurredOn: string;
|
occurredOn: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findPartKind(state: HiveState, catalog: HiveCatalog, id: string) {
|
function findPartType(state: HiveState, catalog: HiveCatalog, id: string): CatalogPartType | undefined {
|
||||||
for (const part of state.stack) {
|
const part = findPart(state, id);
|
||||||
const hit = part.id === id ? part : part.inserts?.find((insert) => insert.id === id);
|
return part ? catalog.partTypes.get(part.partTypeId) : undefined;
|
||||||
if (hit) return catalog.partTypes.get(hit.partTypeId);
|
|
||||||
}
|
}
|
||||||
return undefined;
|
|
||||||
|
/** Parts whose state the part sheet edits (a box is changed through its frames). */
|
||||||
|
function hasPartSheet(type: CatalogPartType): boolean {
|
||||||
|
return type.kind !== "BOX" && (type.stateSchema.length > 0 || type.kind === "FLOOR");
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HiveCardBodyProps {
|
interface HiveCardBodyProps {
|
||||||
@@ -282,10 +285,9 @@ function HiveCardBody({ colonyId, setup, catalog, canEdit, mode, onOpsChange, in
|
|||||||
const partName = (id: string) => partDisplayName(shownState, catalog, id);
|
const partName = (id: string) => partDisplayName(shownState, catalog, id);
|
||||||
const focusedId = focusedIdOf(view);
|
const focusedId = focusedIdOf(view);
|
||||||
const leaveFocus = () => view.openBox(null);
|
const leaveFocus = () => view.openBox(null);
|
||||||
/** Parts whose state the part sheet edits (a box is changed through its frames). */
|
|
||||||
const hasSheet = (id: string) => {
|
const hasSheet = (id: string) => {
|
||||||
const type = findPartKind(shownState, catalog, id);
|
const type = findPartType(shownState, catalog, id);
|
||||||
return type !== undefined && type.kind !== "BOX" && (type.stateSchema.length > 0 || type.kind === "FLOOR");
|
return type !== undefined && hasPartSheet(type);
|
||||||
};
|
};
|
||||||
|
|
||||||
function editFrame(boxId: string, slot: number) {
|
function editFrame(boxId: string, slot: number) {
|
||||||
@@ -297,18 +299,18 @@ function HiveCardBody({ colonyId, setup, catalog, canEdit, mode, onOpsChange, in
|
|||||||
function editPart(id: string, intent?: "frameFeeder") {
|
function editPart(id: string, intent?: "frameFeeder") {
|
||||||
if (!editable) return;
|
if (!editable) return;
|
||||||
// The outline's add actions report their box; „Futtertasche einsetzen“ says so, the other is the evaporator.
|
// The outline's add actions report their box; „Futtertasche einsetzen“ says so, the other is the evaporator.
|
||||||
const isBox = findPartKind(workingState, catalog, id)?.kind === "BOX";
|
const isBox = findPartType(workingState, catalog, id)?.kind === "BOX";
|
||||||
setPartTarget({ id, evaporator: isBox && intent !== "frameFeeder", frameFeeder: isBox && intent === "frameFeeder" });
|
setPartTarget({ id, evaporator: isBox && intent !== "frameFeeder", frameFeeder: isBox && intent === "frameFeeder" });
|
||||||
setPartOpen(true);
|
setPartOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectPart(id: string) {
|
function selectPart(id: string) {
|
||||||
const type = findPartKind(shownState, catalog, id);
|
const type = findPartType(shownState, catalog, id);
|
||||||
if (type?.kind === "BOX") {
|
if (type?.kind === "BOX") {
|
||||||
view.openBox(id);
|
view.openBox(id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (type && (type.stateSchema.length > 0 || type.kind === "FLOOR")) editPart(id);
|
if (type && hasPartSheet(type)) editPart(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showRebuild = canEdit && !isInspection;
|
const showRebuild = canEdit && !isInspection;
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { ArrowLeft, SlidersHorizontal } from "lucide-react";
|
import { ArrowLeft, SlidersHorizontal } from "lucide-react";
|
||||||
import type { HiveCatalog, HivePart, HiveState } from "@/lib/hive/types";
|
import { findPart } from "@/lib/hive/find-part";
|
||||||
|
import type { HiveCatalog, HiveState } from "@/lib/hive/types";
|
||||||
import { GLASS_PILL, GLASS_SURFACE } from "./hive-glass";
|
import { GLASS_PILL, GLASS_SURFACE } from "./hive-glass";
|
||||||
import { formatAmount } from "./hive-scene/scene-model";
|
import { formatAmount } from "./hive-scene/scene-model";
|
||||||
|
|
||||||
@@ -15,15 +16,6 @@ export interface FocusChip {
|
|||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findPart(state: HiveState, id: string): HivePart | undefined {
|
|
||||||
for (const part of state.stack) {
|
|
||||||
if (part.id === id) return part;
|
|
||||||
const insert = part.inserts?.find((candidate) => candidate.id === id);
|
|
||||||
if (insert) return insert;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Chips for the focused part: frame count of a box, every state field with its value. */
|
/** Chips for the focused part: frame count of a box, every state field with its value. */
|
||||||
export function focusChips(state: HiveState, catalog: HiveCatalog, id: string): FocusChip[] {
|
export function focusChips(state: HiveState, catalog: HiveCatalog, id: string): FocusChip[] {
|
||||||
const part = findPart(state, id);
|
const part = findPart(state, id);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { createCausticMaterial, createLiquidMaterial } from "./materials";
|
|||||||
import { MAX_FRAME_SECONDS } from "./use-animated-node";
|
import { MAX_FRAME_SECONDS } from "./use-animated-node";
|
||||||
import { stepToward } from "./use-hive-motion";
|
import { stepToward } from "./use-hive-motion";
|
||||||
import { noRaycast, useSceneKit } from "./scene-kit";
|
import { noRaycast, useSceneKit } from "./scene-kit";
|
||||||
|
import { seededRandom } from "./seeded-random";
|
||||||
import { placeFill, setUniform } from "./scene-mutations";
|
import { placeFill, setUniform } from "./scene-mutations";
|
||||||
|
|
||||||
// Levels (spec 9.3): a glowing, half-transparent volume whose surface moves
|
// Levels (spec 9.3): a glowing, half-transparent volume whose surface moves
|
||||||
@@ -104,11 +105,7 @@ function fondantTexture(): CanvasTexture | null {
|
|||||||
canvas.height = 128;
|
canvas.height = 128;
|
||||||
const context = canvas.getContext("2d");
|
const context = canvas.getContext("2d");
|
||||||
if (!context) return null;
|
if (!context) return null;
|
||||||
let seed = 97;
|
const next = seededRandom(97);
|
||||||
const next = () => {
|
|
||||||
seed = (seed * 16807) % 2147483647;
|
|
||||||
return seed / 2147483647;
|
|
||||||
};
|
|
||||||
context.fillStyle = "#d8d8d8";
|
context.fillStyle = "#d8d8d8";
|
||||||
context.fillRect(0, 0, 128, 128);
|
context.fillRect(0, 0, 128, 128);
|
||||||
for (let dot = 0; dot < 900; dot += 1) {
|
for (let dot = 0; dot < 900; dot += 1) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { Color, MeshStandardMaterial } from "three";
|
import { Color, MeshStandardMaterial } from "three";
|
||||||
import type { Group, ShaderMaterial } from "three";
|
import type { Group, ShaderMaterial } from "three";
|
||||||
|
import { subVec3 } from "@/lib/hive/layout";
|
||||||
import type { FrameBox, Vec3 } from "@/lib/hive/layout";
|
import type { FrameBox, Vec3 } from "@/lib/hive/layout";
|
||||||
import { HoloLabel } from "./hive-labels";
|
import { HoloLabel } from "./hive-labels";
|
||||||
import { createEdgeMaterial, createGlassMaterial } from "./materials";
|
import { createEdgeMaterial, createGlassMaterial } from "./materials";
|
||||||
@@ -22,8 +23,6 @@ import type { FrameTarget } from "./use-hive-motion";
|
|||||||
// box. In an open box each frame is a clear glass plate; hovered or selected
|
// box. In an open box each frame is a clear glass plate; hovered or selected
|
||||||
// it rises and takes its type look (colour and comb pattern).
|
// it rises and takes its type look (colour and comb pattern).
|
||||||
|
|
||||||
const sub = (a: Vec3, b: Vec3): Vec3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
||||||
|
|
||||||
/** Faint top bars of all frames of a box, one merged mesh using the box's materials. */
|
/** Faint top bars of all frames of a box, one merged mesh using the box's materials. */
|
||||||
export function FrameHints({
|
export function FrameHints({
|
||||||
frames,
|
frames,
|
||||||
@@ -39,7 +38,7 @@ export function FrameHints({
|
|||||||
const geometries = useMemo(() => {
|
const geometries = useMemo(() => {
|
||||||
const hints = frames
|
const hints = frames
|
||||||
.filter((frame) => frame.type !== "NONE")
|
.filter((frame) => frame.type !== "NONE")
|
||||||
.map((frame) => frameHintSpec(frame.size, sub(frame.center, hostCenter)));
|
.map((frame) => frameHintSpec(frame.size, subVec3(frame.center, hostCenter)));
|
||||||
return {
|
return {
|
||||||
glass: buildGlassGeometry(hints.map((hint) => hint.slab)),
|
glass: buildGlassGeometry(hints.map((hint) => hint.slab)),
|
||||||
edges: buildEdgeGeometry(hints.flatMap((hint) => hint.edges)),
|
edges: buildEdgeGeometry(hints.flatMap((hint) => hint.edges)),
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import {
|
|||||||
SRGBColorSpace,
|
SRGBColorSpace,
|
||||||
ShaderMaterial,
|
ShaderMaterial,
|
||||||
} from "three";
|
} from "three";
|
||||||
import type { Points } from "three";
|
|
||||||
import { createQualityMonitor } from "./quality";
|
import { createQualityMonitor } from "./quality";
|
||||||
import type { QualityVerdict, StageTier } from "./quality";
|
import type { QualityVerdict, StageTier } from "./quality";
|
||||||
import { useSceneKit } from "./scene-kit";
|
import { noRaycast, useSceneKit } from "./scene-kit";
|
||||||
|
import { seededRandom } from "./seeded-random";
|
||||||
import { markTimerFrame, setEffectBokeh, setEffectTarget, setSceneBackground, setUniform } from "./scene-mutations";
|
import { markTimerFrame, setEffectBokeh, setEffectTarget, setSceneBackground, setUniform } from "./scene-mutations";
|
||||||
import type { HoloTheme } from "./theme";
|
import type { HoloTheme } from "./theme";
|
||||||
|
|
||||||
@@ -279,19 +279,9 @@ void main() {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/** Deterministic placement, so the stage looks the same on every visit (Park-Miller). */
|
|
||||||
function seededRandom(start: number): () => number {
|
|
||||||
let seed = start;
|
|
||||||
return () => {
|
|
||||||
seed = (seed * 16807) % 2147483647;
|
|
||||||
return seed / 2147483647;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function Particles() {
|
function Particles() {
|
||||||
const { theme } = useSceneKit();
|
const { theme } = useSceneKit();
|
||||||
const dpr = useThree((state) => state.viewport.dpr);
|
const dpr = useThree((state) => state.viewport.dpr);
|
||||||
const points = useRef<Points>(null);
|
|
||||||
const { geometry, material } = useMemo(() => {
|
const { geometry, material } = useMemo(() => {
|
||||||
const next = seededRandom(7);
|
const next = seededRandom(7);
|
||||||
const positions: number[] = [];
|
const positions: number[] = [];
|
||||||
@@ -332,7 +322,7 @@ function Particles() {
|
|||||||
setUniform(material, "uTime", state.clock.elapsedTime);
|
setUniform(material, "uTime", state.clock.elapsedTime);
|
||||||
setUniform(material, "uPixelRatio", dpr);
|
setUniform(material, "uPixelRatio", dpr);
|
||||||
});
|
});
|
||||||
return <points ref={points} geometry={geometry} material={material} renderOrder={5} raycast={() => {}} />;
|
return <points geometry={geometry} material={material} renderOrder={5} raycast={noRaycast} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Lights() {
|
function Lights() {
|
||||||
@@ -448,7 +438,7 @@ function Haze() {
|
|||||||
material={material}
|
material={material}
|
||||||
renderOrder={-1}
|
renderOrder={-1}
|
||||||
layers={OVERLAY_LAYER}
|
layers={OVERLAY_LAYER}
|
||||||
raycast={() => {}}
|
raycast={noRaycast}
|
||||||
>
|
>
|
||||||
<coneGeometry args={[6.5, HAZE_HEIGHT, 48, 1, true]} />
|
<coneGeometry args={[6.5, HAZE_HEIGHT, 48, 1, true]} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|||||||
@@ -362,9 +362,3 @@ export function createLiquidMaterial(options: LiquidOptions): ShaderMaterial {
|
|||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the per-node uniforms shared by every material here. */
|
|
||||||
export function setOpacity(material: ShaderMaterial, opacity: number): void {
|
|
||||||
const uniform = material.uniforms.uOpacity;
|
|
||||||
if (uniform) uniform.value = opacity;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/** Deterministic Park-Miller sequence in (0, 1), so procedural looks are the same on every visit. */
|
||||||
|
export function seededRandom(start: number): () => number {
|
||||||
|
let seed = start;
|
||||||
|
return () => {
|
||||||
|
seed = (seed * 16807) % 2147483647;
|
||||||
|
return seed / 2147483647;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { subVec3 } from "@/lib/hive/layout";
|
||||||
import type { FrameBox, HiveLayout, PartBox, Vec3 } from "@/lib/hive/layout";
|
import type { FrameBox, HiveLayout, PartBox, Vec3 } from "@/lib/hive/layout";
|
||||||
import type { HivePartKind } from "@/lib/hive/types";
|
import type { HivePartKind } from "@/lib/hive/types";
|
||||||
import type { HiveViewState, HoveredFrame } from "../use-hive-view";
|
import type { HiveViewState, HoveredFrame } from "../use-hive-view";
|
||||||
@@ -74,7 +75,6 @@ export interface MotionView extends HiveViewState {
|
|||||||
|
|
||||||
export const frameKey = (boxId: string, slot: number): string => `${boxId}#${slot}`;
|
export const frameKey = (boxId: string, slot: number): string => `${boxId}#${slot}`;
|
||||||
|
|
||||||
const sub = (a: Vec3, b: Vec3): Vec3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
||||||
const lift = (value: Vec3, dy: number): Vec3 => [value[0], value[1] + dy, value[2]];
|
const lift = (value: Vec3, dy: number): Vec3 => [value[0], value[1] + dy, value[2]];
|
||||||
|
|
||||||
/** Rest place of a stack part: its layout centre raised by the stack gap. */
|
/** Rest place of a stack part: its layout centre raised by the stack gap. */
|
||||||
@@ -200,10 +200,10 @@ function insertTarget(
|
|||||||
host: PartBox,
|
host: PartBox,
|
||||||
gap: number,
|
gap: number,
|
||||||
): NodeTarget {
|
): NodeTarget {
|
||||||
const local = sub(insert.center, host.center);
|
const local = subVec3(insert.center, host.center);
|
||||||
if (focus?.isInsert && focus.id === insert.id) {
|
if (focus?.isInsert && focus.id === insert.id) {
|
||||||
return {
|
return {
|
||||||
position: sub(focusPlacement(layout.bounds, insert.size), restCenter(host, gap)),
|
position: subVec3(focusPlacement(layout.bounds, insert.size), restCenter(host, gap)),
|
||||||
scale: FOCUS_SCALE,
|
scale: FOCUS_SCALE,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
glow: GLOW.focus,
|
glow: GLOW.focus,
|
||||||
@@ -241,7 +241,7 @@ function insertTarget(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function frameTarget(frame: FrameBox, host: PartBox, view: MotionView, focus: FocusState | null): FrameTarget {
|
function frameTarget(frame: FrameBox, host: PartBox, view: MotionView, focus: FocusState | null): FrameTarget {
|
||||||
const local = sub(frame.center, host.center);
|
const local = subVec3(frame.center, host.center);
|
||||||
if (frame.type === "NONE") return { position: local, opacity: 0, look: "hidden", interactive: false };
|
if (frame.type === "NONE") return { position: local, opacity: 0, look: "hidden", interactive: false };
|
||||||
const open = focus !== null && !focus.isInsert && focus.id === host.id;
|
const open = focus !== null && !focus.isInsert && focus.id === host.id;
|
||||||
if (!open) {
|
if (!open) {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { findPart } from "./find-part";
|
||||||
|
import { part, templateState } from "./test-utils";
|
||||||
|
|
||||||
|
describe("findPart", () => {
|
||||||
|
it("finds stack parts and inserts, and nothing for an unknown id", () => {
|
||||||
|
const state = templateState();
|
||||||
|
state.stack[1] = { ...state.stack[1], inserts: [part("e", "evaporator")] };
|
||||||
|
|
||||||
|
expect(findPart(state, "b")).toBe(state.stack[1]);
|
||||||
|
expect(findPart(state, "e")).toBe(state.stack[1].inserts![0]);
|
||||||
|
expect(findPart(state, "nope")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { HivePart, HiveState } from "./types";
|
||||||
|
|
||||||
|
/** A stack part or an insert by id; undefined for an unknown id. */
|
||||||
|
export function findPart(state: HiveState, id: string): HivePart | undefined {
|
||||||
|
for (const part of state.stack) {
|
||||||
|
if (part.id === id) return part;
|
||||||
|
const insert = part.inserts?.find((candidate) => candidate.id === id);
|
||||||
|
if (insert) return insert;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { FeedType } from "@/lib/feedings/types";
|
import type { FeedType } from "@/lib/feedings/types";
|
||||||
|
import { findPart } from "./find-part";
|
||||||
import type { CatalogPartType, HiveCatalog, HiveOp, HivePart, HiveState, LevelField } from "./types";
|
import type { CatalogPartType, HiveCatalog, HiveOp, HivePart, HiveState, LevelField } from "./types";
|
||||||
|
|
||||||
// Spec section 8: suggestions only. Nothing here books a feeding or treatment.
|
// Spec section 8: suggestions only. Nothing here books a feeding or treatment.
|
||||||
@@ -15,15 +16,6 @@ const ACID_KEY = "acidMl";
|
|||||||
const isFeedKind = (kind: string | undefined): boolean => kind === "FEEDER" || kind === "FRAME_FEEDER";
|
const isFeedKind = (kind: string | undefined): boolean => kind === "FEEDER" || kind === "FRAME_FEEDER";
|
||||||
const ROUNDING = 1000;
|
const ROUNDING = 1000;
|
||||||
|
|
||||||
function findPart(state: HiveState, id: string): HivePart | undefined {
|
|
||||||
for (const part of state.stack) {
|
|
||||||
if (part.id === id) return part;
|
|
||||||
const insert = part.inserts?.find((candidate) => candidate.id === id);
|
|
||||||
if (insert) return insert;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function typeOf(part: HivePart | undefined, catalog: HiveCatalog): CatalogPartType | undefined {
|
function typeOf(part: HivePart | undefined, catalog: HiveCatalog): CatalogPartType | undefined {
|
||||||
return part ? catalog.partTypes.get(part.partTypeId) : undefined;
|
return part ? catalog.partTypes.get(part.partTypeId) : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import type { CatalogPartType, FrameType, HiveCatalog, HivePart, HivePartKind, H
|
|||||||
|
|
||||||
export type Vec3 = [number, number, number];
|
export type Vec3 = [number, number, number];
|
||||||
|
|
||||||
|
/** a − b, component by component. */
|
||||||
|
export function subVec3(a: Vec3, b: Vec3): Vec3 {
|
||||||
|
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
||||||
|
}
|
||||||
|
|
||||||
export interface PartBox {
|
export interface PartBox {
|
||||||
id: string;
|
id: string;
|
||||||
partTypeId: string;
|
partTypeId: string;
|
||||||
|
|||||||
Reference in new issue
Block a user