tscircuit/ti

ti tscircuit library

Version
1.0.198
License
unset
Stars
0

system-block-ui/src/rendering/automotive-window-module-render.test.tsx

import { expect, test } from "bun:test";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { getSchematicElementBounds } from "@tscircuit/circuit-json-util";
import type { AnyCircuitElement } from "circuit-json";
import { unzipSync } from "fflate";
import {
  createSystemBlockExamples,
  generateSystemDesignArtifacts,
  SUBCIRCUIT_CATALOG,
} from "../model";
import {
  evaluateGeneratedTsx,
  getCircuitJsonErrors,
} from "./evaluate-schematic";
import { createKicadProjectZipBlob } from "./export-kicad-project";
import { getGeneratedSystemEvaluationFsMap } from "./generated-source-files";
import { createLocalTiPackageEvaluationFsMap } from "./local-ti-package-files";

const automotiveWindowDefinitions = [
  {
    componentName: "PowerSupply_WindowModule",
    sourcePath: "lib/subcircuits/PowerSupply_WindowModule.circuit.tsx",
  },
  {
    componentName: "CommunicationInterface_LIN_TLIN1028",
    sourcePath:
      "lib/subcircuits/CommunicationInterface_LIN_TLIN1028.circuit.tsx",
  },
  {
    componentName: "Microcontroller_MSP430FR6007",
    sourcePath: "lib/subcircuits/Microcontroller_MSP430FR6007.circuit.tsx",
  },
  {
    componentName: "MotorDriver_DRV8703",
    sourcePath: "lib/subcircuits/MotorDriver_DRV8703.circuit.tsx",
  },
  {
    componentName: "PinchDetection_INA240_TLV2316_LMV7275",
    sourcePath:
      "lib/subcircuits/PinchDetection_INA240_TLV2316_LMV7275.circuit.tsx",
  },
  {
    componentName: "MotorThermalProtection_TMP390",
    sourcePath: "lib/subcircuits/MotorThermalProtection_TMP390.circuit.tsx",
  },
  {
    componentName: "PositionFeedback_DRV5013_TIDA01389",
    sourcePath:
      "lib/subcircuits/PositionFeedback_DRV5013_TIDA01389.circuit.tsx",
  },
] as const;

const loadRepositorySources = async (
  repositoryRoot: string,
): Promise<Record<string, string>> => {
  const sources: Record<string, string> = {};
  const glob = new Bun.Glob("lib/**/*.{ts,tsx}");
  for await (const path of glob.scan({
    cwd: repositoryRoot,
    onlyFiles: true,
  })) {
    sources[path] = await Bun.file(join(repositoryRoot, path)).text();
  }
  return sources;
};

const expectWindowSheetSizes = (circuitJson: AnyCircuitElement[]) => {
  const dimensionsBySheet = {
    communication_interface: [297, 210],
    motor_driver: [297, 210],
    motor_thermal_protection: [297, 210],
    position_feedback: [297, 210],
    watchdog_and_vref: [297, 210],
    main_supply: [431.8, 279.4],
    reference_full: [500, 330],
    pinch_detection: [430, 280],
  } as const;

  // Native sheet rendering maps the 1.1-unit resistor span to KiCad's 10.16 mm
  // and reserves a 5 mm inner margin on each side of the physical page.
  const schematicUnitToMm = 10.16 / 1.1;
  for (const [name, [width, height]] of Object.entries(dimensionsBySheet)) {
    const sheet = circuitJson.find(
      (element) => element.type === "schematic_sheet" && element.name === name,
    );
    expect(sheet).toMatchObject({ sheet_width: width, sheet_height: height });
    if (sheet?.type !== "schematic_sheet") throw new Error(`Missing ${name}`);
    if (width === 297 && height === 210) {
      expect(sheet.sheet_size).toBe("a4");
    }

    const bounds = circuitJson.flatMap((element) => {
      if (
        (element.type !== "schematic_component" &&
          element.type !== "schematic_trace" &&
          element.type !== "schematic_net_label") ||
        element.schematic_sheet_id !== sheet.schematic_sheet_id
      ) {
        return [];
      }
      const bounds = getSchematicElementBounds(element);
      return bounds ? [bounds] : [];
    });
    expect(bounds.length).toBeGreaterThan(0);
    const extentX = Math.max(
      ...bounds.flatMap(({ minX, maxX }) => [Math.abs(minX), Math.abs(maxX)]),
    );
    const extentY = Math.max(
      ...bounds.flatMap(({ minY, maxY }) => [Math.abs(minY), Math.abs(maxY)]),
    );
    expect(extentX).toBeLessThanOrEqual((width / 2 - 5) / schematicUnitToMm);
    expect(extentY).toBeLessThanOrEqual((height / 2 - 5) / schematicUnitToMm);
  }
};

test("Automotive Window Module renders every real schematic sheet", async () => {
  const repositoryRoot = fileURLToPath(new URL("../../..", import.meta.url));
  const sourceModules = await loadRepositorySources(repositoryRoot);
  const example = createSystemBlockExamples(SUBCIRCUIT_CATALOG).find(
    ({ id }) => id === "automotive-window-module",
  );
  if (!example) throw new Error("Missing Automotive Window Module example");

  const artifacts = generateSystemDesignArtifacts({
    blocks: example.graph.blocks,
    connections: example.graph.connections,
    catalog: SUBCIRCUIT_CATALOG,
    boardName: "automotive_window_module",
  });
  const evaluated = await evaluateGeneratedTsx(artifacts.tsx, {
    fsMap: {
      ...createLocalTiPackageEvaluationFsMap(
        automotiveWindowDefinitions,
        sourceModules,
      ),
      ...getGeneratedSystemEvaluationFsMap(artifacts),
    },
    mainComponentPath: "GeneratedSystem.circuit.tsx",
    timeoutMs: 120_000,
  });

  expect(evaluated.sheets).toHaveLength(9);
  expect(evaluated.sheets.map(({ title }) => title)).toEqual(
    expect.arrayContaining([
      "System Diagram",
      "Main Supply",
      "Watchdog and Vref",
      "MSP-TS430PZ100E Figure B-78",
      "TLIN1028-Q1 LIN Interface",
      "TIDA-01389 DRV8703 Motor Driver",
      "TIDA-01421 Pinch Detection",
      "TMP390-Q1 Motor Thermal Protection",
      "TIDA-01389 Position Feedback",
    ]),
  );
  expect(evaluated.sheets.map(({ title }) => title)).not.toContain(
    "Automotive Window Power Supply",
  );
  expect(evaluated.sheets.map(({ title }) => title)).not.toContain(
    "MSP430FR6007 Microcontroller",
  );

  expectWindowSheetSizes(evaluated.circuitJson);

  const errorTypes = getCircuitJsonErrors(evaluated.circuitJson).map(
    ({ error_type }) => error_type,
  );
  expect(new Set(errorTypes)).toEqual(
    new Set(["external_footprint_load_error", "pcb_missing_footprint_error"]),
  );

  const kicadBlob = await createKicadProjectZipBlob(evaluated.circuitJson, {
    projectName: "automotive-window-module",
  });
  const kicadArchive = unzipSync(new Uint8Array(await kicadBlob.arrayBuffer()));
  expect(Object.keys(kicadArchive)).toContain(
    "automotive-window-module.kicad_pro",
  );
  expect(Object.keys(kicadArchive)).toContain(
    "automotive-window-module.kicad_pcb",
  );
  expect(
    Object.keys(kicadArchive).filter((fileName) =>
      fileName.endsWith(".kicad_sch"),
    ),
  ).toHaveLength(9);
}, 120_000);

test("standalone window example uses A4 unless the circuit needs a larger sheet", async () => {
  const repositoryRoot = fileURLToPath(new URL("../../..", import.meta.url));
  const sourceModules = await loadRepositorySources(repositoryRoot);
  const source = await Bun.file(
    join(repositoryRoot, "examples/AutomotiveWindowModule.circuit.tsx"),
  ).text();
  const evaluated = await evaluateGeneratedTsx(source, {
    fsMap: createLocalTiPackageEvaluationFsMap(
      automotiveWindowDefinitions,
      sourceModules,
    ),
    mainComponentPath: "AutomotiveWindowModule.circuit.tsx",
    timeoutMs: 120_000,
  });

  expect(evaluated.sheets).toHaveLength(8);
  expectWindowSheetSizes(evaluated.circuitJson);
}, 120_000);