diff --git a/docs-site/astro.config.mjs b/docs-site/astro.config.mjs index 719250f5..116eecdd 100644 --- a/docs-site/astro.config.mjs +++ b/docs-site/astro.config.mjs @@ -155,6 +155,7 @@ export default defineConfig({ { label: 'n,k Characterization', translations: { 'zh-CN': 'n,k 表征' }, slug: 'data-exchange/nk-characterization' }, { label: 'Spectrum File Formats', translations: { 'zh-CN': '光谱文件格式' }, slug: 'data-exchange/spectrum-file-formats' }, { label: 'Zemax Coatings', translations: { 'zh-CN': 'Zemax 膜层' }, slug: 'data-exchange/zemax-coatings' }, + { label: 'CODE V Coatings', translations: { 'zh-CN': 'CODE V 膜层' }, slug: 'data-exchange/codev-coatings' }, { label: 'Design Import', translations: { 'zh-CN': '设计导入' }, slug: 'data-exchange/design-import' }, { label: 'Process Exporter', translations: { 'zh-CN': '工艺导出' }, slug: 'simulation/process-simulator' }, ], diff --git a/docs-site/public/llms.txt b/docs-site/public/llms.txt index e3b0034a..f622fca0 100644 --- a/docs-site/public/llms.txt +++ b/docs-site/public/llms.txt @@ -67,6 +67,7 @@ Design Editor from the substrate outwards. - [Report](https://docs.tfstudio.xyz/data-exchange/report-generator/): a document assembled from blocks over one or several designs, saved as PDF or HTML. - [Zemax Coatings](https://docs.tfstudio.xyz/data-exchange/zemax-coatings/): reading and writing Zemax COATING.DAT files. +- [CODE V Coatings](https://docs.tfstudio.xyz/data-exchange/codev-coatings/): reading CODE V MUL coatings from .seq and .mul files, and writing a design as a .seq file. - [Measured Spectra](https://docs.tfstudio.xyz/data-exchange/measured-spectra/): importing spectrophotometer data for comparison against a design. - [Process Exporter](https://docs.tfstudio.xyz/simulation/process-simulator/): exporting a layer schedule for a deposition controller. diff --git a/docs-site/src/components/ribbon-icons.js b/docs-site/src/components/ribbon-icons.js index d42f7e4e..05696fb8 100644 --- a/docs-site/src/components/ribbon-icons.js +++ b/docs-site/src/components/ribbon-icons.js @@ -216,15 +216,22 @@ export const ICONS = { Cf(15,6.2,0.9), ]), - // Zemax Coatings — a data file (coating-layer lines) + bidirectional exchange arrow. + // Zemax Coatings: a file holding a table, as COATING.DAT is tables of + // materials and coatings. 'zemax-coatings': I([ - R(2.5, 3, 8, 14, 1.2), - L(4.5, 6.5, 8.5, 6.5, 1), - L(4.5, 9, 8.5, 9, 1), - L(4.5, 11.5, 8.5, 11.5, 1), - L(11.5, 10, 17.5, 10, 1.4), - P('M15 7.5 L17.8 10 L15 12.5', 1.4), - P('M14 5 L11.5 7 L14 9', 1.2), + P('M4.5 2.5H11.5L15.5 6.5V17.5H4.5Z'), + P('M11.5 2.5V6.5H15.5', 1.2), + P('M6.5 9H13.5V15.5H6.5Z', 1), + P('M6.5 11.2H13.5M6.5 13.3H13.5M10 9V15.5', 0.9), + ]), + + // CODE V Coatings: a file holding a command prompt, as a .seq is a CODE V + // command file. + 'codev-coatings': I([ + P('M4.5 2.5H11.5L15.5 6.5V17.5H4.5Z'), + P('M11.5 2.5V6.5H15.5', 1.2), + P('M7 9.3L9.6 12L7 14.7'), + P('M10.6 14.7H13.4'), ]), 'help-docs': I([ C(10,10,8), P('M7.5 7.8q0-2 2.5-2t2.5 2q0 1.5-2.5 2.5v1', 1.5), Cf(10,14.7,0.7) ]), diff --git a/docs-site/src/content/docs/data-exchange/codev-coatings.md b/docs-site/src/content/docs/data-exchange/codev-coatings.md new file mode 100644 index 00000000..b789a123 --- /dev/null +++ b/docs-site/src/content/docs/data-exchange/codev-coatings.md @@ -0,0 +1,66 @@ +--- +title: CODE V Coatings +description: Read and write CODE V MULTILAYER (MUL) coatings, importing the layers of a .seq or .mul file into a design or the Coating Library, or exporting the active design as a .seq file. +ribbonIcon: codev-coatings +--- + +The **CODE V Coatings** window moves a coating between TFStudio and CODE V. It reads a CODE V MULTILAYER (`MUL`) coating, either the `.seq` command file that enters the stack or the `.mul` file CODE V saves from it, and writes one side of the active design as a `.seq` file that CODE V runs to make the `.mul`. Import and export are on separate tabs in one row. What the last action did shows at the right-hand end of that row, beside a warning badge: on the Import tab it holds what was noted while reading the file, on the Export tab what the export noted about the preview. + +## Settings + +**Open .seq or .mul** (Import tab, in the panel on the left): reads a coating file. The panel then shows the incident medium and the substrate in the file, the wavelengths, the angles and the reference wavelength. Beside it are the file's title and its layers from the incident medium to the substrate, with their thickness in nm and a lock on each layer CODE V holds fixed (code 100). + +**Import → front coating** and **Import → back coating** (Import tab): the file's layers replace the front or the back coating of the active design, and the design keeps its incident medium, exit medium and substrate; the file's `INC` and `SUB` are shown, not applied. The materials the layers use go into a user catalog named `CODE V` and the file name. With no design selected there is nothing to import into, so both buttons are off, while **Save to Coating Library…** stays on. + +**Save to Coating Library…** (Import tab): saves the stack into My coatings, through the same dialog as **Save current coating…** in the [Coating Library](/design/coating-library/). The saved coating lies between the file's `INC` and `SUB`: an index of n 1 with no k is saved as Air, and any other index or `MIC` material as the material the import makes of it. Its layers are the ones **Import → front coating** puts on the design. The band starts at the span of the file's analysis wavelengths, or at the band [Optical Evaluation](/analysis/optical-evaluation/) is set to when the file has one wavelength, and the angle at its first `ANG`. The materials are carried in the saved coating, so it works on a computer that never opened the file, and saving adds nothing to your material catalogs. + +**Side** (Export tab): **Front** writes the design's incident medium, the front coating and the substrate. **Back** writes the exit medium as the incident medium, then the back coating from the exit side to the substrate, then the substrate, because CODE V wants air as the incident medium and the glass as the substrate on either face of an element. + +**Title (TIT)**: the coating title, up to 80 characters. It starts as the design name. + +**File name (SAV)**: the name of the `.mul` file the last command of the `.seq` writes. **Save .seq** offers the `.seq` under the same name. + +**REF (nm)**: the reference wavelength written to `REF`. It starts at the design's reference wavelength. + +**Analysis wavelengths, WL**: from, to and step in nm, with the number of wavelengths shown beside them. CODE V takes up to 100, and a larger count is refused before anything is written. + +**Angles of incidence, ANG**: up to five angles in the incident medium, each from 0 up to but not including 90 degrees, decimals allowed. An entry outside that range goes back to the angle the field held. The plus button adds one, the cross removes one. + +**Generate preview** builds the file and shows it in full; **Save .seq** writes it. A design with a material that cannot be found on this computer cannot be exported until the material is replaced. With no design selected there is nothing to export, and the Export tab shows nothing but a request to open or create one. + +## How to read it + +From a `.seq` file the import reads the stack entered in the `MDA` sub-option: `PHT`, `TIT`, `WL`, `WLG`, `REF`, `ANG`, `INC`, `COA`, `GRO` and `END` groups, `SUB` and the `MIC` catalog. The analysis, plot and optimization options and the file commands are passed over; any other command is skipped, and the warning badge names it. CODE V reads at most 21 values from one `WL`, `MWL` or `EXT` command or one `MIC` material line and ignores the rest, and the import keeps the first 21 the same way, with a warning. A `.mul` file is read as CODE V saved it, both the sample coatings that come with CODE V and the files CODE V 11.2 saves. + +A file that writes its numbers with a decimal comma (`403,03`) imports with a warning. The import reads a comma between two digits as a decimal point, but CODE V does not read such a number, so the file does not run in CODE V as it is. + +`WLG` builds its wavelengths the way CODE V does, in single precision, so a wavelength can sit slightly off the round value. The window shows the wavelengths and `REF` with only the digits that precision holds: `WLG 400 800 10` shows as 400 to 799.9997 nm, and with no `REF` in the file the reference wavelength shows as 599.9998 nm. + +A thickness entered in waves (`PHT N`) is converted to nm with the layer's n at `REF`, or at the central analysis wavelength when the file has no `REF`, which is what CODE V takes then. An imported `MIC` material is tabulated over the span of its `MWL` points, the analysis wavelengths and `REF`, with n and k that follow CODE V 11.2's interpolation of its table, between the `MWL` points and past them. When `REF` lies outside a `MIC` table that a layer entered in waves uses, a warning names the material. + +The front coating takes the layers in the order the file lists them. The back coating takes them with the file's first layer on the exit side and its last against the substrate, the order the export writes a back coating in. When [**Symmetric (back = front)**](/design/evaluation-modes/) is ticked, the back coating is the mirror of the front, so only **Import → front coating** is available. + +A layer CODE V holds fixed (code 100) is imported locked. A layer coupled to others (code 1 to 99) is imported free, since TFStudio has no coupled layers, and the report says how many there were. + +Importing the same file again uses its catalog, and a material the catalog already holds is kept as it is, edits included. Undo puts the coating back as it was before the import; the catalog and the materials added to it stay. + +What CODE V accepts, from the MUL chapter of its help, and what the export does about each limit: + +| Item | CODE V | On export | +| --- | --- | --- | +| Layers | up to 1000 | refused above 1000 | +| Analysis wavelengths, `WL` | up to 100 | refused above 100; written at most 21 to a `WL` command, the most CODE V reads from one | +| Angles, `ANG` | up to 5 | the field holds five | +| Points per `MIC` material, `MWL` | up to 21 | a material is sampled at the analysis wavelengths; above 21, the points kept are added one at a time where CODE V's own interpolation misses the material most, until it matches every wavelength to the 6 decimals written or 21 are kept, and a warning names the material | +| Material label | 6 characters | made from the material name, with a number added when two labels would be the same | +| Incident medium and substrate | n only | the k of an absorbing medium is dropped, with a warning | +| Thickness | physical nm (`PHT Y`) or waves at `REF` (`PHT N`) | physical nm, `PHT Y` | +| Fixed layer | code 100 | a locked layer is written with 100, any other with 0 | + +A material whose n and k do not change over the analysis wavelengths is written as a number on its `COA` line rather than as a `MIC` entry. k is written as a positive number. CODE V enters layers from the incident medium to the substrate, while the Design Editor numbers them from the substrate, so the Design Editor's layer 1 is the last `COA` line of the file. + +The file ends with `SAV`, so running it in CODE V writes the `.mul` that `MLT` attaches to a lens surface. Once the coating is on a lens surface, CODE V takes the substrate index from the lens, so `SUB` matters only inside `MUL`. + +## References + +- CODE V Multilayer Design Reference Manual, chapter "Multilayer Coating Design and Analysis (MUL)": *MDA Sub-Option* for the commands and their limits, *Technical Notes* for the order of the media on each kind of surface. diff --git a/docs-site/src/content/docs/data-exchange/measured-ellipsometry.md b/docs-site/src/content/docs/data-exchange/measured-ellipsometry.md index f005d6fb..4fbfa54a 100644 --- a/docs-site/src/content/docs/data-exchange/measured-ellipsometry.md +++ b/docs-site/src/content/docs/data-exchange/measured-ellipsometry.md @@ -56,7 +56,7 @@ section of the formats page. An opened file belongs to the design selected in the project explorer. Select another design and the window shows that design's curves and nothing of the file; come back and the file is where you left it. With no design selected -there is nothing to import into, and the button is off. +the window shows nothing but a request to open or create one. ### Configuring the columns diff --git a/docs-site/src/content/docs/data-exchange/measured-spectra.md b/docs-site/src/content/docs/data-exchange/measured-spectra.md index 00f80a84..48f048ac 100644 --- a/docs-site/src/content/docs/data-exchange/measured-spectra.md +++ b/docs-site/src/content/docs/data-exchange/measured-spectra.md @@ -21,7 +21,7 @@ instrument quirks it handles on its own, is on the An opened file belongs to the design selected in the project explorer. Select another design and the window shows that design's curves and nothing of the file; come back and the file is where you left it. With no design selected -there is nothing to import into, and the button is off. +the window shows nothing but a request to open or create one. ### Confirming the parse diff --git a/docs-site/src/content/docs/data-exchange/report-generator.md b/docs-site/src/content/docs/data-exchange/report-generator.md index d7129f39..9d489d63 100644 --- a/docs-site/src/content/docs/data-exchange/report-generator.md +++ b/docs-site/src/content/docs/data-exchange/report-generator.md @@ -30,7 +30,7 @@ The **Template** list holds three: Design record, the default, with everything i ## Several designs -Pick more than one design under **Designs** and the report becomes a comparison. The designs are lettered A, B, C and listed with their full names in a key under the masthead; tables, plot legends and recipe headings refer to them by letter, so a long name never widens a column. Facts, verdicts, integral values and color render as one table with a column per design, six designs to a table, the spectrum block draws every design on one plot, and the recipes sit side by side. Blocks without a comparison form render once per design. +Pick more than one design under **Designs** and the report becomes a comparison. The designs are lettered A, B, C and listed with their full names in a key under the masthead; tables, plot legends and recipe headings refer to them by letter, so a long name never widens a column. Facts, verdicts, integral values and color render as one table with a column per design, six designs to a table, the spectrum block draws every design on one plot, and the recipes sit side by side. Blocks without a comparison form render once per design. With no design open the report covers only the designs picked under **Designs**; with none picked there is no page, only a request to open or create a design. ## Export diff --git a/docs-site/src/content/docs/data-exchange/zemax-coatings.md b/docs-site/src/content/docs/data-exchange/zemax-coatings.md index 115598e6..08844d3a 100644 --- a/docs-site/src/content/docs/data-exchange/zemax-coatings.md +++ b/docs-site/src/content/docs/data-exchange/zemax-coatings.md @@ -1,51 +1,42 @@ --- title: Zemax Coatings -description: Read and write Zemax OpticStudio COATING.DAT, importing materials and coating stacks or exporting the active design. +description: Read and write Zemax OpticStudio COATING.DAT, importing materials and coating stacks, saving a coating to the Coating Library, or exporting the active design. ribbonIcon: zemax-coatings --- -The **Zemax Coatings** window reads and writes Zemax OpticStudio -`COATING.DAT` files. Import a coating stack (and its materials) from a -`COATING.DAT` into a TFStudio design, or export the active design as a `COAT` -stack plus its `MATE` material definitions for use in OpticStudio. +The **Zemax Coatings** window reads and writes Zemax OpticStudio `COATING.DAT` files. Import a coating stack (and its materials) from a `COATING.DAT` into a TFStudio design, save one to the [Coating Library](/design/coating-library/), or export the active design as a `COAT` stack plus its `MATE` material definitions for use in OpticStudio. -`COATING.DAT` is Zemax's coating database: a text file of `MATE` (material) -and `COAT` (coating-stack) records, alongside the ideal and tabular coating -models (`IDEAL`, `IDEAL2`, `TABLE`, `TAPR`, `ENCRYPTED`). The window parses the -whole file and presents it in three tabs: **Coatings**, **Materials**, and -**Export**. Load a file with the **Load** button at the top; the parsed -contents and your selections stay put while you switch between tools. +`COATING.DAT` is Zemax's coating database: a text file of `MATE` (material) and `COAT` (coating-stack) records, alongside the ideal and tabular coating models (`IDEAL`, `IDEAL2`, `TABLE`, `TAPR`, `ENCRYPTED`). The window parses the whole file and presents it in three tabs: **Coatings**, **Materials**, and **Export**. The tabs sit in one row with the reference wavelength, and what the last action did shows at the right-hand end of that row. Load a file with **Load COATING.DAT** in the panel on the left of the Coatings or the Materials tab; the parsed contents and your selections stay put while you switch between tools. ## Settings -**Reference wavelength**: λ₀ used to convert between Zemax's relative -thickness (in waves) and physical thickness in nanometres on both import and -export. +**Reference λ₀ (nm)**: the wavelength used to convert between Zemax's relative thickness (in waves) and physical thickness in nanometres on both import and export. -**Coatings tab**: lists every `COAT` stack in the file with its type and layer -count. Select a layer stack and press **Import to front** to load it as the -front design; the stack's `MATE` materials are auto-registered into a -`Zemax ` catalog so the design resolves its materials immediately. -Encrypted stacks are locked and cannot be imported. +**Coatings tab**: the panel lists every coating record in the file with its type and layer count. Select a layer stack to see its layers on the right. Only `COAT` layer stacks import; the ideal, table and encrypted records are listed with a lock. -**Materials tab**: lists every `MATE` table. Tick the ones you want and use -**Import selected** or **Import all** to add them to a catalog without touching -the design. +- **Import → front coating** loads the stack as the front coating of the active design. Its `MATE` materials are registered into a `Zemax ` catalog so the design resolves its materials immediately. With no design selected there is nothing to import into, so the button is off, while **Save to Coating Library…** stays on and puts the coating between air and BK7. +- **Save to Coating Library…** saves the stack into My coatings, through the same dialog as **Save current coating…** in the Coating Library: give it a name, a type and a use note, and set the band, angle and polarization it is meant for. A `COAT` record names no incident medium or substrate, so the saved coating takes those of the active design. Its layers are the ones **Import → front coating** would put on the design, with relative thicknesses converted at the reference wavelength, which is stored with the coating. Its materials are embedded in it, so it works on a computer that never loaded the file. Saving adds nothing to your material catalogs. -**Export tab**: generates `COAT` + `MATE` text from the current front design, -with a live preview before you save: +**Materials tab**: lists every `MATE` record, one row each. Tick the ones you want and use **Import selected** or **Import all** to add them to a catalog without touching the design. -- **Thickness mode**: write **absolute** thickness (µm) or **relative** waves. -- **Material scope**: export only the materials **used** by the design, or - **all** catalog materials. +**Export tab**: generates `COAT` + `MATE` text from the current front design, with a preview before you save: + +- **Layer thickness**: write **Absolute (µm)** physical thickness or **Relative (waves)** of λ₀. +- **Include materials**: export only the materials **Used by design**, or **All catalog materials**. - **Coating name**: the `COAT` record name. -- **Sample grid**: the wavelength range and step at which each material's - n,k is tabulated into its `MATE` record. +- **Material sampling grid**: the wavelength range and step at which each material's n,k is tabulated into its `MATE` record. + +With no design selected there is nothing to export, and the tab shows nothing but a request to open or create one. + +## A name defined twice + +A file can define one material name in more than one `MATE` record. The window matches names without regard to case, so `SiO2` and `SIO2` count as one name. The Materials tab marks each such row with which record of the name it is, for example **2 of 2**, and a notice in the window's top row lists the names. + +Each record imports on its own. The first keeps its name and a later one gets (2), (3) after it, so importing both never replaces one with the other in the catalog. A coating layer that names such a material is built with the last record of that name, on import, on Save to Coating Library and in the thicknesses the Coatings tab shows. If the records differ, check which one the file meant before relying on that coating. ## How to read it -TFStudio and Zemax differ in a few conventions, which the window handles for -you automatically: +TFStudio and Zemax differ in a few conventions, which the window handles for you automatically: | Quantity | Zemax | TFStudio | | --------------- | ------------------------- | ----------------------------- | @@ -54,14 +45,8 @@ you automatically: | Layer thickness | relative `T` (waves) | physical `d = T·λ₀ / n₀` | | Layer order | outermost → substrate | same internal storage order | -Layer order needs no reversal. Zemax's outermost-to-substrate order is exactly -how TFStudio stores the front coating (the Design Editor only displays it -reversed). Round-tripping a design out to `COATING.DAT` and back in preserves -both the layer thicknesses and the k-sign convention. Only `IDEAL`, `IDEAL2`, -and `TABLE` style material models map cleanly to TFStudio dispersion; -`ENCRYPTED` Zemax materials cannot be decoded. +Layer order needs no reversal. Zemax's outermost-to-substrate order is exactly how TFStudio stores the front coating (the Design Editor only displays it reversed). Round-tripping a design out to `COATING.DAT` and back in preserves both the layer thicknesses and the k-sign convention. `IDEAL`, `IDEAL2` and `TABLE` coatings describe a surface by its reflectance and transmittance rather than by layers, and `ENCRYPTED` ones cannot be decoded, so none of them becomes a stack. ## References -- Zemax OpticStudio Help → *The Coating Tab → Coating File Definitions* - (`MATE`, `COAT`, `IDEAL`, `TABLE`), the source of the `COATING.DAT` format. +- Zemax OpticStudio Help → *The Coating Tab → Coating File Definitions* (`MATE`, `COAT`, `IDEAL`, `TABLE`), the source of the `COATING.DAT` format. diff --git a/docs-site/src/content/docs/design/coating-library.md b/docs-site/src/content/docs/design/coating-library.md index a6075006..7a483ac4 100644 --- a/docs-site/src/content/docs/design/coating-library.md +++ b/docs-site/src/content/docs/design/coating-library.md @@ -89,13 +89,15 @@ layers are the outermost ones. The design's substrate and media are not changed; compare them with the substrate the entry was designed on before you rely on the numbers. -Applying is one undoable edit: **Undo** brings the previous stack back. +Applying is one undoable edit: **Undo** brings the previous stack back. With no design selected there is nothing to apply the coating to, so **Apply** is off. A material that a catalog on this computer holds is computed from that catalog, here and in the design. A coating can instead carry a copy from another computer's catalog of the same ID with other n,k: the library computes with that copy, and so does a design that does not use the ID yet. Whenever the coating brings a definition under an ID the design already computes with other data, the design keeps what it has and the window says so; the coating may then compute differently than it does in the library. ## Saving a coating -**Save current coating…** in this window, or **Save coating to library…** in the Design Editor's Tools menu, saves the front or back stack of the active design into My coatings. Give it a name, a type and a short use note, and set the band, angle and polarization it is meant for. Non-built-in materials are embedded in the saved entry, so it stays usable when the catalog it came from is renamed or removed. While that catalog holds the material, the coating computes with the catalog's current data. +**Save current coating…** in this window, or **Save coating to library…** in the Design Editor's Tools menu, saves the front or back stack of the active design into My coatings. Give it a name, a type and a short use note, and set the band, angle and polarization it is meant for. Non-built-in materials are embedded in the saved entry, so it stays usable when the catalog it came from is renamed or removed. While that catalog holds the material, the coating computes with the catalog's current data. With no design selected there is no stack to save, so **Save current coating…** is off. + +The [Zemax Coatings](/data-exchange/zemax-coatings/) and [CODE V Coatings](/data-exchange/codev-coatings/) windows save a coating read from a file through the same dialog, with the layers of the file instead of those of the design. Saved coatings are plain JSON files with the `.tfsc` extension in the Coatings folder (Settings, Data folder). Saving under an existing name replaces that diff --git a/docs-site/src/content/docs/index.mdx b/docs-site/src/content/docs/index.mdx index 9ae222fa..40b96162 100644 --- a/docs-site/src/content/docs/index.mdx +++ b/docs-site/src/content/docs/index.mdx @@ -104,6 +104,7 @@ what the tools do, so a page is easy to find from the window it documents. + diff --git a/docs-site/src/content/docs/zh/data-exchange/codev-coatings.md b/docs-site/src/content/docs/zh/data-exchange/codev-coatings.md new file mode 100644 index 00000000..e1087be6 --- /dev/null +++ b/docs-site/src/content/docs/zh/data-exchange/codev-coatings.md @@ -0,0 +1,66 @@ +--- +title: CODE V 镀膜 +description: 读写 CODE V MULTILAYER (MUL) 镀膜:把 .seq 或 .mul 文件中的膜层导入设计或镀膜库,或把活动设计导出为 .seq 文件。 +ribbonIcon: codev-coatings +--- + +**CODE V 镀膜** 窗口在 TFStudio 与 CODE V 之间传递镀膜。它读取 CODE V MULTILAYER(`MUL`)镀膜,可以是输入膜层堆栈的 `.seq` 命令文件,也可以是 CODE V 由其保存的 `.mul` 文件;它还把活动设计的一个面写成 `.seq` 文件,CODE V 运行该文件即生成 `.mul`。导入和导出是同一行中的两个标签页。上一次操作的结果显示在这一行的右端,旁边是警告标记:在导入标签页上列出读取文件时的提示,在导出标签页上列出导出时对预览的提示。 + +## 设置 + +**打开 .seq 或 .mul**(导入标签页,左侧面板中):读取镀膜文件。随后面板显示文件中的入射介质和基底、波长、入射角和参考波长。旁边显示文件的标题,以及从入射介质到基底的各层,含以 nm 计的厚度,CODE V 中固定的层(代码 100)带锁形标记。 + +**导入 → 正面镀膜** 和 **导入 → 背面镀膜**(导入标签页):文件中的膜层替换活动设计的正面或背面镀膜,设计保留自己的入射介质、出射介质和基底;文件中的 `INC` 和 `SUB` 只显示,不应用。这些膜层所用的材料放入一个以 `CODE V` 加文件名命名的用户目录。未选择设计时没有可导入的对象,这两个按钮不可用,而 **保存到镀膜库…** 仍可使用。 + +**保存到镀膜库…**(导入标签页):把该堆栈保存到“我的镀膜”,使用的对话框与[镀膜库](/zh/design/coating-library/)中的 **保存当前镀膜…** 相同。保存的镀膜位于文件的 `INC` 与 `SUB` 之间:n 为 1 且没有 k 的折射率保存为空气,其他折射率或 `MIC` 材料保存为导入时由它生成的材料。其各层与 **导入 → 正面镀膜** 放到设计上的各层相同。波段初始为文件分析波长的范围,文件只有一个波长时为[光学评估](/zh/analysis/optical-evaluation/)所设的波段;入射角初始为其第一个 `ANG`。材料随镀膜一起保存,因此在从未打开该文件的计算机上也能使用,保存也不会向你的材料目录添加任何内容。 + +**面**(导出标签页):**正面** 写出设计的入射介质、正面镀膜和基底。**背面** 把出射介质作为入射介质写出,然后是从出射侧到基底的背面镀膜,最后是基底,因为无论元件的哪一面,CODE V 都要求以空气为入射介质、以玻璃为基底。 + +**标题 (TIT)**:镀膜标题,最多 80 个字符。初始值为设计名称。 + +**文件名 (SAV)**:`.seq` 最后一条命令写出的 `.mul` 文件名。**保存 .seq** 以同一名称保存 `.seq`。 + +**REF (nm)**:写入 `REF` 的参考波长。初始值为设计的参考波长。 + +**分析波长,WL**:以 nm 计的起点、终点和步长,旁边显示波长个数。CODE V 最多接受 100 个,超过时在写出任何内容之前即被拒绝。 + +**入射角,ANG**:最多五个角度,在入射介质中,每个从 0 起、小于 90 度,可以带小数。超出此范围的输入会恢复为输入框原来的角度。加号按钮添加一个,叉号删除一个。 + +**生成预览** 构建文件并完整显示;**保存 .seq** 将其写出。若设计中有材料在本机上找不到,则在替换该材料之前无法导出。未选择设计时没有可导出的内容,导出标签页只提示先打开或创建设计。 + +## 如何解读 + +从 `.seq` 文件中,导入读取在 `MDA` 子选项中输入的膜层堆栈:`PHT`、`TIT`、`WL`、`WLG`、`REF`、`ANG`、`INC`、`COA`、`GRO` 与 `END` 组、`SUB` 以及 `MIC` 目录。分析、绘图和优化选项以及文件命令被略过;其他命令会被跳过,并由警告标记列出。CODE V 从一条 `WL`、`MWL` 或 `EXT` 命令或一行 `MIC` 材料中最多读取 21 个值,其余的值被忽略;导入时同样保留前 21 个,并给出警告。`.mul` 文件按 CODE V 保存的内容读取,CODE V 自带的示例镀膜和 CODE V 11.2 保存的文件都可以读取。 + +以逗号作小数点写数字(`403,03`)的文件导入时会给出警告。导入把两个数字之间的逗号读作小数点,但 CODE V 不读取这样的数字,因此该文件原样无法在 CODE V 中运行。 + +`WLG` 按 CODE V 的方式以单精度生成波长,因此某个波长可能略微偏离整数值。窗口显示波长和 `REF` 时只保留该精度所含的位数:`WLG 400 800 10` 显示为 400 到 799.9997 nm,文件中没有 `REF` 时参考波长显示为 599.9998 nm。 + +以波数输入的厚度(`PHT N`)用该层在 `REF` 处的 n 换算为 nm;文件中没有 `REF` 时,按 CODE V 的做法取中间的分析波长。导入的 `MIC` 材料在其 `MWL` 点、分析波长和 `REF` 所覆盖的范围内制表,n 和 k 遵循 CODE V 11.2 对其表的插值,包括 `MWL` 点之间和点以外的值。当以波数输入的层所用的 `MIC` 表不包含 `REF` 时,会有警告指出该材料。 + +正面镀膜按文件列出的顺序接收各层。背面镀膜接收时,文件的第一层位于出射侧,最后一层贴着基底,与导出背面镀膜时写出的顺序相同。勾选 [**对称(背面 = 正面)**](/zh/design/evaluation-modes/) 时,背面镀膜是正面的镜像,因此只能使用 **导入 → 正面镀膜**。 + +CODE V 中固定的层(代码 100)导入后为锁定。与其他层耦合的层(代码 1 到 99)以自由层导入,因为 TFStudio 没有耦合层,报告中会给出这类层的数量。 + +再次导入同一文件时使用它的目录,目录中已有的材料保持原样,包括你做过的编辑。撤销会把镀膜恢复为导入前的样子;目录以及加入其中的材料保留。 + +CODE V 接受的范围(摘自其帮助的 MUL 章节),以及导出时对每项限制的处理: + +| 项目 | CODE V | 导出时 | +| --- | --- | --- | +| 层数 | 最多 1000 | 超过 1000 时拒绝 | +| 分析波长,`WL` | 最多 100 | 超过 100 时拒绝;每条 `WL` 命令最多写 21 个,这是 CODE V 从一条命令读取的上限 | +| 角度,`ANG` | 最多 5 | 输入框最多容纳五个 | +| 每种 `MIC` 材料的点数,`MWL` | 最多 21 | 材料在分析波长上取样;超过 21 个点时,逐个保留 CODE V 自身插值偏离材料最大处的点,直到所有波长在写出的 6 位小数内一致或已满 21 个,并有警告指出该材料 | +| 材料标签 | 6 个字符 | 由材料名称生成,两个标签相同时加数字区分 | +| 入射介质和基底 | 只有 n | 吸收介质的 k 被舍去,并给出警告 | +| 厚度 | 物理厚度 nm(`PHT Y`)或 `REF` 处的波数(`PHT N`) | 物理厚度 nm,`PHT Y` | +| 固定层 | 代码 100 | 锁定的层写为 100,其余写为 0 | + +在分析波长范围内 n 和 k 不变的材料,直接以数字写在其 `COA` 行上,而不作为 `MIC` 条目。k 以正数写出。CODE V 从入射介质到基底输入各层,而设计编辑器从基底开始编号,因此设计编辑器中的第 1 层是文件中最后一条 `COA` 行。 + +文件以 `SAV` 结尾,因此在 CODE V 中运行即写出 `.mul`,由 `MLT` 将其附加到透镜表面。镀膜一旦附加到透镜表面,CODE V 就从透镜取基底折射率,因此 `SUB` 只在 `MUL` 内部起作用。 + +## 参考文献 + +- CODE V Multilayer Design Reference Manual,"Multilayer Coating Design and Analysis (MUL)" 一章:*MDA Sub-Option* 介绍命令及其限制,*Technical Notes* 介绍各类表面上介质的顺序。 diff --git a/docs-site/src/content/docs/zh/data-exchange/measured-ellipsometry.md b/docs-site/src/content/docs/zh/data-exchange/measured-ellipsometry.md index d27d2553..a8e6f73a 100644 --- a/docs-site/src/content/docs/zh/data-exchange/measured-ellipsometry.md +++ b/docs-site/src/content/docs/zh/data-exchange/measured-ellipsometry.md @@ -34,7 +34,7 @@ ribbonIcon: measured-ellipsometry 点击 **Open file…(打开文件)** 并选择文件。导入器读取哪些内容,以及哪些仪器的导出文件已与它核对过,见格式页面的[椭偏文件格式](/zh/data-exchange/spectrum-file-formats/#椭偏文件)一节。 -打开的文件属于项目浏览器中当前选中的设计。切换到另一个设计时,窗口只显示那个设计的曲线,不会带上这个文件;切回来时文件仍在原处。未选择设计时没有可导入的对象,按钮不可用。 +打开的文件属于项目浏览器中当前选中的设计。切换到另一个设计时,窗口只显示那个设计的曲线,不会带上这个文件;切回来时文件仍在原处。未选择设计时,窗口只提示先打开或创建设计。 ### 配置各列 diff --git a/docs-site/src/content/docs/zh/data-exchange/measured-spectra.md b/docs-site/src/content/docs/zh/data-exchange/measured-spectra.md index 2bbc46c5..b61e9495 100644 --- a/docs-site/src/content/docs/zh/data-exchange/measured-spectra.md +++ b/docs-site/src/content/docs/zh/data-exchange/measured-spectra.md @@ -10,7 +10,7 @@ ribbonIcon: spectrum-exchange 点击 **Import Spectrum(导入光谱)** 并选择文件。导入器接受哪些内容、以及它会自行处理的仪器格式怪癖,见[光谱文件格式](/zh/data-exchange/spectrum-file-formats/)页面。 -打开的文件属于项目浏览器中当前选中的设计。切换到另一个设计时,窗口只显示那个设计的曲线,不会带上这个文件;切回来时文件仍在原处。未选择设计时没有可导入的对象,按钮不可用。 +打开的文件属于项目浏览器中当前选中的设计。切换到另一个设计时,窗口只显示那个设计的曲线,不会带上这个文件;切回来时文件仍在原处。未选择设计时,窗口只提示先打开或创建设计。 ### 确认解析 diff --git a/docs-site/src/content/docs/zh/data-exchange/report-generator.md b/docs-site/src/content/docs/zh/data-exchange/report-generator.md index 0a8d1a60..933a2d3b 100644 --- a/docs-site/src/content/docs/zh/data-exchange/report-generator.md +++ b/docs-site/src/content/docs/zh/data-exchange/report-generator.md @@ -30,7 +30,7 @@ ribbonIcon: report-gen ## 多个设计 -在 **设计** 下选择多个设计,报告就变为对比。各设计以 A、B、C 编号,并在报告头下方的图例中列出全名;表格、图例和配方标题都用字母指代,因此过长的名称不会撑宽表格列。概况、判定、积分值和颜色以每个设计一列的表格呈现,每张表最多六个设计;光谱区块把所有设计画在同一张图上,配方并排放置。没有对比形式的区块按设计逐个呈现。 +在 **设计** 下选择多个设计,报告就变为对比。各设计以 A、B、C 编号,并在报告头下方的图例中列出全名;表格、图例和配方标题都用字母指代,因此过长的名称不会撑宽表格列。概况、判定、积分值和颜色以每个设计一列的表格呈现,每张表最多六个设计;光谱区块把所有设计画在同一张图上,配方并排放置。没有对比形式的区块按设计逐个呈现。未打开设计时,报告只涵盖在 **设计** 下选择的设计;一个也没有选择时不显示页面,只提示先打开或创建设计。 ## 导出 diff --git a/docs-site/src/content/docs/zh/data-exchange/zemax-coatings.md b/docs-site/src/content/docs/zh/data-exchange/zemax-coatings.md index 3ab2e6c1..de26abe6 100644 --- a/docs-site/src/content/docs/zh/data-exchange/zemax-coatings.md +++ b/docs-site/src/content/docs/zh/data-exchange/zemax-coatings.md @@ -1,27 +1,38 @@ --- title: Zemax 镀膜 -description: 读写 Zemax OpticStudio 的 COATING.DAT,导入材料和镀膜堆栈,或导出活动设计。 +description: 读写 Zemax OpticStudio 的 COATING.DAT,导入材料和镀膜堆栈,把镀膜保存到镀膜库,或导出活动设计。 ribbonIcon: zemax-coatings --- -**Zemax 镀膜(Zemax Coatings)** 窗口读写 Zemax OpticStudio 的 `COATING.DAT` 文件。从 `COATING.DAT` 将镀膜堆栈(及其材料)导入到 TFStudio 设计中,或将活动设计导出为 `COAT` 堆栈及其 `MATE` 材料定义,供 OpticStudio 使用。 +**Zemax 镀膜(Zemax Coatings)** 窗口读写 Zemax OpticStudio 的 `COATING.DAT` 文件。从 `COATING.DAT` 将镀膜堆栈(及其材料)导入到 TFStudio 设计中,把一个镀膜保存到[镀膜库](/zh/design/coating-library/),或将活动设计导出为 `COAT` 堆栈及其 `MATE` 材料定义,供 OpticStudio 使用。 -`COATING.DAT` 是 Zemax 的镀膜数据库:一个包含 `MATE`(材料)和 `COAT`(镀膜堆栈)记录,以及理想与列表式镀膜模型(`IDEAL`、`IDEAL2`、`TABLE`、`TAPR`、`ENCRYPTED`)的文本文件。窗口解析整个文件,并以三个标签页呈现:**Coatings(镀膜)**、**Materials(材料)** 和 **Export(导出)**。使用顶部的 **Load(加载)** 按钮载入文件;在你切换工具时,解析出的内容和你的选择会保持不变。 +`COATING.DAT` 是 Zemax 的镀膜数据库:一个包含 `MATE`(材料)和 `COAT`(镀膜堆栈)记录,以及理想与列表式镀膜模型(`IDEAL`、`IDEAL2`、`TABLE`、`TAPR`、`ENCRYPTED`)的文本文件。窗口解析整个文件,并以三个标签页呈现:**Coatings(镀膜)**、**Materials(材料)** 和 **Export(导出)**。标签页与参考波长位于同一行,上一次操作的结果显示在这一行的右端。使用 Coatings 或 Materials 标签页左侧面板中的 **Load COATING.DAT(载入 COATING.DAT)** 载入文件;在你切换工具时,解析出的内容和你的选择会保持不变。 ## 设置 -**参考波长(Reference wavelength)**:λ₀,用于在 Zemax 的相对厚度(以波为单位)与物理厚度(纳米)之间进行转换,导入和导出时均使用。 +**参考 λ₀ (nm)**:用于在 Zemax 的相对厚度(以波为单位)与物理厚度(纳米)之间进行转换,导入和导出时均使用。 -**Coatings(镀膜)标签页**:列出文件中每一个 `COAT` 堆栈及其类型和层数。选择一个层堆栈并按 **Import to front(导入到前表面)** 将其作为前表面设计加载;该堆栈的 `MATE` 材料会自动注册到一个 `Zemax ` 目录中,以便设计立即解析其材料。加密的堆栈被锁定,无法导入。 +**Coatings(镀膜)标签页**:面板列出文件中每一条镀膜记录及其类型和层数。选择一个层堆栈,其各层显示在右侧。只有 `COAT` 层堆栈可以导入;理想、表格和加密记录带锁形标记列出。 -**Materials(材料)标签页**:列出每一个 `MATE` 表格。勾选你需要的,使用 **Import selected(导入所选)** 或 **Import all(导入全部)** 将它们添加到目录中,而不影响设计。 +- **Import → front coating(导入 → 正面镀膜)** 将该堆栈作为活动设计的正面镀膜载入。其 `MATE` 材料会自动注册到一个 `Zemax ` 目录中,以便设计立即解析其材料。未选择设计时没有可导入的对象,该按钮不可用,而 **Save to Coating Library…(保存到镀膜库…)** 仍可使用,镀膜位于空气与 BK7 之间。 +- **Save to Coating Library…(保存到镀膜库…)** 把该堆栈保存到“我的镀膜”,使用的对话框与镀膜库中 **Save current coating…(保存当前镀膜…)** 相同:给它一个名称、类型和用途说明,并设置它所针对的波段、入射角和偏振。`COAT` 记录不指定入射介质和基底,因此保存的镀膜采用活动设计的入射介质和基底。其各层与 **Import → front coating** 放到设计上的各层相同,相对厚度按参考波长换算,该参考波长随镀膜一起保存。其材料嵌入镀膜中,因此在从未载入该文件的计算机上也能使用。保存不会向你的材料目录添加任何内容。 -**Export(导出)标签页**:根据当前前表面设计生成 `COAT` + `MATE` 文本,并在保存前提供实时预览: +**Materials(材料)标签页**:列出每一条 `MATE` 记录,每行一条。勾选你需要的,使用 **Import selected(导入所选)** 或 **Import all(导入全部)** 将它们添加到目录中,而不影响设计。 -- **Thickness mode(厚度模式)**:写入**绝对**厚度(µm)或**相对**波数。 -- **Material scope(材料范围)**:仅导出设计**使用**的材料,或导出**全部**目录材料。 +**Export(导出)标签页**:根据当前前表面设计生成 `COAT` + `MATE` 文本,并在保存前提供预览: + +- **Layer thickness(膜层厚度)**:写入**绝对 (µm)** 物理厚度,或以 λ₀ 波数计的**相对(波)**厚度。 +- **Include materials(包含材料)**:仅导出**设计所用**的材料,或导出**全部目录材料**。 - **Coating name(镀膜名称)**:`COAT` 记录的名称。 -- **Sample grid(采样网格)**:将每种材料的 n,k 列表化写入其 `MATE` 记录时所用的波长范围和步长。 +- **Material sampling grid(材料采样网格)**:将每种材料的 n,k 列表化写入其 `MATE` 记录时所用的波长范围和步长。 + +未选择设计时没有可导出的内容,该标签页只提示先打开或创建设计。 + +## 定义了两次的名称 + +一个文件可以在不止一条 `MATE` 记录中定义同一个材料名称。窗口匹配名称时不区分大小写,因此 `SiO2` 和 `SIO2` 算作同一个名称。Materials 标签页在每个这样的行上标出它是该名称的第几条记录,例如 **2 / 2**,窗口顶行的一条提示列出这些名称。 + +每条记录单独导入。第一条保留原名,后面的在名称后加 (2)、(3),因此同时导入两条时,目录中的一条绝不会替换另一条。引用这种材料的镀膜层使用该名称的最后一条记录,导入、保存到镀膜库以及 Coatings 标签页显示的厚度都是如此。如果这些记录不同,在依赖该镀膜之前请确认文件指的是哪一条。 ## 如何解读 @@ -34,7 +45,7 @@ TFStudio 与 Zemax 在若干约定上有所不同,窗口会自动为你处理 | 层厚度 | 相对 `T`(波数) | 物理 `d = T·λ₀ / n₀` | | 层顺序 | 最外层 → 基底 | 相同的内部存储顺序 | -层顺序无需反转。Zemax 从最外层到基底的顺序,正是 TFStudio 存储前表面镀膜的方式(设计编辑器只是将其反向显示)。将一个设计导出到 `COATING.DAT` 再导入回来,会同时保留层厚度和 k 符号约定。只有 `IDEAL`、`IDEAL2` 和 `TABLE` 类型的材料模型能干净地映射到 TFStudio 的色散;`ENCRYPTED` 的 Zemax 材料无法被解码。 +层顺序无需反转。Zemax 从最外层到基底的顺序,正是 TFStudio 存储前表面镀膜的方式(设计编辑器只是将其反向显示)。将一个设计导出到 `COATING.DAT` 再导入回来,会同时保留层厚度和 k 符号约定。`IDEAL`、`IDEAL2` 和 `TABLE` 镀膜以反射率和透射率而不是膜层描述一个表面,`ENCRYPTED` 镀膜无法解码,因此它们都不会成为膜层堆栈。 ## 参考文献 diff --git a/docs-site/src/content/docs/zh/design/coating-library.md b/docs-site/src/content/docs/zh/design/coating-library.md index 3aac02ee..1890976d 100644 --- a/docs-site/src/content/docs/zh/design/coating-library.md +++ b/docs-site/src/content/docs/zh/design/coating-library.md @@ -37,13 +37,15 @@ ribbonIcon: coating-library 选择**前表面**或**后表面**,再选择**替换膜系**或**叠加在膜系之上**,然后按**应用**。替换会把所选表面的膜系换成该镀膜。叠加会把镀膜镀在已有膜系之上,新膜层位于最外侧。设计的基底和介质不会改变;在依赖这些数字之前,请把它们与条目设计时所用的基底作比较。 -应用是一次可撤销的编辑:**撤销**会恢复原来的膜系。 +应用是一次可撤销的编辑:**撤销**会恢复原来的膜系。未选择设计时没有可应用镀膜的对象,**应用**不可用。 本机目录中已有的材料,在本窗口和设计中都按该目录计算。镀膜也可能携带来自另一台电脑上同一 ID 目录、n、k 不同的副本:库按该副本计算,尚未使用该 ID 的设计也是如此。只要镀膜带来的定义所用的 ID 已被设计以不同的数据计算,设计就保留它已有的材料,窗口会加以说明;此时该镀膜的计算结果可能与它在库中的表现不同。 ## 保存镀膜 -本窗口中的**保存当前镀膜…**,或设计编辑器"工具"菜单中的**将镀膜保存到库…**,会把活动设计的前表面或后表面膜系保存到"我的镀膜"。为它取一个名称、选择类型并写一句用途说明,然后设置它所针对的波段、入射角和偏振。非内置材料会嵌入保存的条目中,因此即使来源目录被重命名或删除,它仍然可用。只要该目录仍保存着这种材料,镀膜就按目录中的当前数据计算。 +本窗口中的**保存当前镀膜…**,或设计编辑器"工具"菜单中的**将镀膜保存到库…**,会把活动设计的前表面或后表面膜系保存到"我的镀膜"。为它取一个名称、选择类型并写一句用途说明,然后设置它所针对的波段、入射角和偏振。非内置材料会嵌入保存的条目中,因此即使来源目录被重命名或删除,它仍然可用。只要该目录仍保存着这种材料,镀膜就按目录中的当前数据计算。未选择设计时没有可保存的膜系,**保存当前镀膜…**不可用。 + +[Zemax 镀膜](/zh/data-exchange/zemax-coatings/)和 [CODE V 镀膜](/zh/data-exchange/codev-coatings/)窗口用同一个对话框保存从文件读取的镀膜,保存的是文件中的膜层,而不是设计中的膜层。 保存的镀膜是"镀膜"文件夹(设置,数据文件夹)中扩展名为 `.tfsc` 的纯 JSON 文件。用已有名称保存会替换该镀膜。**删除**会移除所选的已保存镀膜。 diff --git a/docs-site/src/content/docs/zh/index.mdx b/docs-site/src/content/docs/zh/index.mdx index 0333d938..12eab6c4 100644 --- a/docs-site/src/content/docs/zh/index.mdx +++ b/docs-site/src/content/docs/zh/index.mdx @@ -98,6 +98,7 @@ TFStudio 是一款用于设计、分析和合成多层光学薄膜镀膜的桌 + diff --git a/src/components/SpectralMonitor.js b/src/components/SpectralMonitor.js index f6dd861b..4d5bd6c4 100644 --- a/src/components/SpectralMonitor.js +++ b/src/components/SpectralMonitor.js @@ -53,12 +53,14 @@ function formatMonitorValue(m, value, strings) { return `${value.toFixed(meta.decimals)}${unitSuffix(meta.unit, strings)}`; } -function monitorScopeBadge(m, design) { +function monitorScopeBadge(m, design, hasActiveDesign) { if (m.type === 'integral') return 'total'; const meta = DIRECT_MONITOR_META[m.type]; if (meta?.frontOnly) return 'front'; - if (meta?.phase) return design.surfaceMode === 'back_only' ? 'back' : 'front'; - return null; + // A phase is read on the side the design is evaluated from; with no design + // open there is no side to name. + if (!meta?.phase || !hasActiveDesign) return null; + return design.surfaceMode === 'back_only' ? 'back' : 'front'; } function monitorLabel(m, strings = {}) { @@ -97,6 +99,18 @@ function monitorLabel(m, strings = {}) { return `⟨${qty}⟩ ${m.lambdaStart}–${m.lambdaEnd} ${strings.units.nm}${aoiStr}`; } +/** + * What each monitor reads on the design: nothing while no design is open, the + * design then being a placeholder nobody opened, nothing while a material it + * uses is unresolved, and nothing here while the cone response is computed in + * the worker. + */ +export function monitorValues({ monitors, design, hasActiveDesign, missingMaterialIds, coneActive }) { + if (!hasActiveDesign || missingMaterialIds.length > 0 || coneActive) return monitors.map(() => null); + const resolveMaterial = designMaterialLookup(design); + return monitors.map(m => computeMonitor(m, design, resolveMaterial)); +} + function genId() { return Math.random().toString(36).slice(2, 9); } // ── Add / Edit monitor form ────────────────────────────────────────────────── @@ -307,7 +321,7 @@ function AddForm({ c, t, layerCount, onAdd, onCancel, initial, mode }) { // ── Main component ──────────────────────────────────────────────────────────── export function SpectralMonitor({ c, t }) { - const { design, evalMode } = useDesign(); + const { design, evalMode, hasActiveDesign } = useDesign(); const monitorStrings = t.statusMonitors; const missingMaterialIds = useUnresolvedMaterials(design); const missingMaterialKey = missingMaterialIds.join('\u0000'); @@ -323,17 +337,8 @@ export function SpectralMonitor({ c, t }) { useEffect(() => { saveMonitors(monitors); - if (missingMaterialIds.length > 0) { - setValues(monitors.map(() => null)); - return; - } - if (coneActive) { - setValues(monitors.map(() => null)); - return; - } - const resolveMaterial = designMaterialLookup(design); - setValues(monitors.map(m => computeMonitor(m, design, resolveMaterial))); - }, [design, monitors, evalMode, missingMaterialKey, coneActive]); + setValues(monitorValues({ monitors, design, hasActiveDesign, missingMaterialIds, coneActive })); + }, [design, monitors, evalMode, missingMaterialKey, coneActive, hasActiveDesign]); useEffect(() => { if (!coneActive) return; @@ -402,7 +407,8 @@ export function SpectralMonitor({ c, t }) { } }, h('span', { style: { fontSize: 10, color: c.textDim, flexShrink: 0, letterSpacing: '0.03em' } }, monitorStrings.title), - h('span', { + // The side the open design is evaluated on; with none open, none. + hasActiveDesign && h('span', { style: { fontSize: 10, color: c.accent, flexShrink: 0, padding: '0 5px', border: `1px solid ${c.accent}33`, @@ -424,7 +430,7 @@ export function SpectralMonitor({ c, t }) { monitors.map((m, i) => { const val = values[i]; const display = formatMonitorValue(m, val, monitorStrings); - const scopeBadge = monitorScopeBadge(m, design); + const scopeBadge = monitorScopeBadge(m, design, hasActiveDesign); const isDropTarget = dragOverId === m.id; return h('span', { key: m.id, diff --git a/src/components/Toolbar.js b/src/components/Toolbar.js index 6a65a976..8cb56ad1 100644 --- a/src/components/Toolbar.js +++ b/src/components/Toolbar.js @@ -353,16 +353,22 @@ export const ICONS = { Cf(14.6, 12.4, 0.9), ]), - // Zemax coatings: a data file (page with stacked coating-layer lines) and a - // bidirectional exchange arrow (import/export COATING.DAT). + // Zemax Coatings: a file holding a table, as COATING.DAT is tables of + // materials and coatings. 'zemax-coatings': I([ - R(2.5, 3, 8, 14, 1.2), - L(4.5, 6.5, 8.5, 6.5, 1), - L(4.5, 9, 8.5, 9, 1), - L(4.5, 11.5, 8.5, 11.5, 1), - L(11.5, 10, 17.5, 10, 1.4), - P('M15 7.5 L17.8 10 L15 12.5', 1.4), - P('M14 5 L11.5 7 L14 9', 1.2), + P('M4.5 2.5H11.5L15.5 6.5V17.5H4.5Z'), + P('M11.5 2.5V6.5H15.5', 1.2), + P('M6.5 9H13.5V15.5H6.5Z', 1), + P('M6.5 11.2H13.5M6.5 13.3H13.5M10 9V15.5', 0.9), + ]), + + // CODE V Coatings: a file holding a command prompt, as a .seq is a CODE V + // command file. + 'codev-coatings': I([ + P('M4.5 2.5H11.5L15.5 6.5V17.5H4.5Z'), + P('M11.5 2.5V6.5H15.5', 1.2), + P('M7 9.3L9.6 12L7 14.7'), + P('M10.6 14.7H13.4'), ]), }; @@ -401,6 +407,7 @@ const TOOL_GROUP = { 'bbm-simulator': 'simulation', 'mono-simulator': 'simulation', 'monitor-worksheet': 'simulation', 'process-sim': 'simulation', 'zemax-coatings': 'data-exchange', 'spectrum-exchange': 'data-exchange', 'measured-ellipsometry': 'data-exchange', + 'codev-coatings': 'data-exchange', 'nk-characterization': 'data-exchange', 'report-gen': 'information', 'help-docs': 'information', 'welcome': 'information', 'tutorials': 'information', @@ -459,7 +466,7 @@ export function makeTabs(t) { key: 'production', label: tb.tabs.production, groups: [ grp('monitoring', ['bbm-simulator', 'mono-simulator', 'monitor-worksheet', 'process-sim']), grp('measured', ['spectrum-exchange', 'measured-ellipsometry', 'nk-characterization']), - grp('exchange', ['zemax-coatings', 'report-gen']), + grp('exchange', ['zemax-coatings', 'codev-coatings', 'report-gen']), ] }, { diff --git a/src/components/docking/DockingLayout.js b/src/components/docking/DockingLayout.js index 368dfba2..d59ad3ca 100644 --- a/src/components/docking/DockingLayout.js +++ b/src/components/docking/DockingLayout.js @@ -68,6 +68,7 @@ function renderNode(node, ctx) { c: ctx.c, theme: ctx.theme, t: ctx.t, setInputDialog: ctx.setInputDialog, onCreateDesign: ctx.onCreateDesign, + hasActiveDesign: ctx.hasActiveDesign, missingMaterialIds: ctx.missingMaterialIds, onReplaceMaterials: ctx.onReplaceMaterials, }), @@ -82,7 +83,7 @@ function renderNode(node, ctx) { // ── DockingLayout ───────────────────────────────────────────────────────────── export function DockingLayout({ c, theme, toolRequests, onWindowListChange, layoutRequest, t, setInputDialog, locale, ribbonStyle = 'colorful', onCreateProject, onCreateDesign }) { - const { design, updateDesign } = useDesign(); + const { design, updateDesign, hasActiveDesign } = useDesign(); const missingMaterialIds = useUnresolvedMaterials(design); const [dragActive, setDragActive] = useState(false); const [dragSrcGroupId, setDragSrcGroupId] = useState(null); @@ -157,7 +158,7 @@ export function DockingLayout({ c, theme, toolRequests, onWindowListChange, layo c, theme, t, locale, ribbonStyle, setTree, setInputDialog, onCreateDesign, dragActive, dragSrcGroupId, dragInsertRef, dropTargetRef, forcedZone, handleTabClick, handleTabClose, handleTabDragStart, handleGroupFocus, - missingMaterialIds, + hasActiveDesign, missingMaterialIds, onReplaceMaterials: () => setReplaceMaterialsOpen(true), }; diff --git a/src/components/docking/ToolContent.js b/src/components/docking/ToolContent.js index cbd372c1..0eca54a7 100644 --- a/src/components/docking/ToolContent.js +++ b/src/components/docking/ToolContent.js @@ -12,6 +12,7 @@ import { ReplaceMaterialsDialog } from '../dialogs/ReplaceMaterialsDialog.js'; import { InputDialog } from '../dialogs/InputDialog.js'; import { MaterialCalculationBlocked } from '../materials/MissingMaterialsNotice.js'; import { ErrorBoundary, WindowFailedPane } from '../ui/ErrorBoundary.js'; +import { AnalysisWindow, CenteredMessage } from '../windows/analysis/chrome/layout.js'; import { WINDOW_REGISTRY, TOOL_LABELS, windowTitle } from './windowRegistry.js'; const { createElement: h, useState } = React; @@ -22,14 +23,25 @@ const { createElement: h, useState } = React; // `theme` also get `theme`; entries flagged `dialog` also get `setInputDialog`; // entries flagged `createDesign` also get `onCreateDesign`. // An id with no component (modal/wizard/stub) falls through to the placeholder. +// +// `hasActiveDesign` is false while no design is open. A window flagged +// `requiresDesign` is then not mounted, so nothing of it computes from the +// placeholder design the provider hands out meanwhile, and a request to open +// or create a design is drawn instead. It mounts once a design is open, with +// its controls as they were: they live in its session store. This comes before +// the materials check, since with no design open there is no design to repair. export function ToolContent({ toolId, copyId = null, c, theme, t, setInputDialog, onCreateDesign, - missingMaterialIds = [], onReplaceMaterials }) { + hasActiveDesign = true, missingMaterialIds = [], onReplaceMaterials }) { // Every window, docked or torn off, is mounted here, so this is also where it // is told which of its open copies it is. Its session store keys the controls // on that, so two tabs of one tool hold two sets of them. const asCopy = body => h(WindowCopyProvider, { copyId }, body); const entry = WINDOW_REGISTRY[toolId]; + if (entry?.requiresDesign && !hasActiveDesign) { + return asCopy(h(AnalysisWindow, { c }, + h(CenteredMessage, { c, message: t.windowChrome.noDesign }))); + } if (entry?.requiresResolvedMaterials && missingMaterialIds.length > 0) { return asCopy(h(MaterialCalculationBlocked, { ids: missingMaterialIds, c, t, onRepair: onReplaceMaterials, @@ -69,12 +81,12 @@ export function ToolContent({ toolId, copyId = null, c, theme, t, setInputDialog // raised through one of those from a torn-off tool opens on the main window, // behind the tool that asked for it. export function FloatToolHost(props) { - const { design, updateDesign } = useDesign(); + const { design, updateDesign, hasActiveDesign } = useDesign(); const [inputDialog, setInputDialog] = useState(null); const [repairMaterials, setRepairMaterials] = useState(false); return h(React.Fragment, null, h(ToolContent, { - ...props, setInputDialog, onReplaceMaterials: () => setRepairMaterials(true), + ...props, hasActiveDesign, setInputDialog, onReplaceMaterials: () => setRepairMaterials(true), }), h(InputDialog, { inputDialog, c: props.c, t: props.t }), repairMaterials && h(ReplaceMaterialsDialog, { diff --git a/src/components/docking/windowRegistry.js b/src/components/docking/windowRegistry.js index b4a22104..2c974c1e 100644 --- a/src/components/docking/windowRegistry.js +++ b/src/components/docking/windowRegistry.js @@ -20,6 +20,11 @@ * Pass the `onCreateDesign` prop: add a ready-made design to the * project explorer and open it. It is absent while no folder is * selected to create one in, so a window can disable the action. + * requiresDesign + * Do not mount the window while no design is open: it works on + * the open design and has nothing of its own to show without one. + * Until one is open, the window draws a request to open or create + * a design instead of computing from the placeholder. * requiresResolvedMaterials * Do not mount the window while the active design references a * material with no embedded or catalog definition. @@ -54,6 +59,7 @@ import { DesignCleaner } from '../windows/optimization/designCleaner/DesignClean import { HistoryWindow } from '../windows/edit/HistoryWindow.js'; import { ProcessSimulator } from '../windows/dataExchange/processSimulator/ProcessSimulator.js'; import { ZemaxCoatings } from '../windows/dataExchange/zemaxCoatings/ZemaxCoatings.js'; +import { CodevCoatings } from '../windows/dataExchange/codevCoatings/CodevCoatings.js'; import { SpectrumExchange } from '../windows/dataExchange/spectrumExchange/SpectrumExchange.js'; import { MeasuredEllipsometry } from '../windows/dataExchange/measuredEllipsometry/MeasuredEllipsometry.js'; import { Variator } from '../windows/optimization/variator/Variator.js'; @@ -73,58 +79,70 @@ import { Games } from '../windows/information/games/Games.js'; export const WINDOW_REGISTRY = { // ── Design ────────────────────────────────────────────────────────────────── - 'design-editor': { component: DesignEditor, title: 'Design Editor', label: 'Design Editor: layer stack table', help: 'design/design-editor' }, + 'design-editor': { component: DesignEditor, title: 'Design Editor', label: 'Design Editor: layer stack table', help: 'design/design-editor', requiresDesign: true }, + // Browses, imports and edits the material catalogs, which belong to no design. 'material-editor': { component: MaterialEditor, title: 'Material Editor', label: 'Material Editor: n,k database', help: 'design/material-editor', dialog: true }, + // Browsing coatings needs no design; Apply and Save current coating are off without one. 'coating-library': { component: CoatingLibrary, title: 'Coating Library', label: 'Coating Library: reusable coating stacks, yours and the built-in starting designs', help: 'design/coating-library' }, - 'specification': { component: Specification, title: 'Specification', label: 'Specification: design requirements (PASS/FAIL qualifiers)', help: 'design/specification', theme: true, dialog: true, requiresResolvedMaterials: true }, - 'merit-function': { component: MeritFunctionEditor, title: 'Merit Function Editor', label: 'Merit Function Editor: operand table', help: 'design/merit-function-editor', dialog: true, requiresResolvedMaterials: true }, - 'variator': { component: Variator, title: 'Variator', label: 'Variator: live parameter slider', help: 'design/variator', theme: true, requiresResolvedMaterials: true }, - 'history': { component: HistoryWindow, title: 'History', label: 'History: design undo/redo tree', help: 'design/history', theme: true }, + 'specification': { component: Specification, title: 'Specification', label: 'Specification: design requirements (PASS/FAIL qualifiers)', help: 'design/specification', theme: true, dialog: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'merit-function': { component: MeritFunctionEditor, title: 'Merit Function Editor', label: 'Merit Function Editor: operand table', help: 'design/merit-function-editor', dialog: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'variator': { component: Variator, title: 'Variator', label: 'Variator: live parameter slider', help: 'design/variator', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + // The undo history of the open design; with none open there is no history. + 'history': { component: HistoryWindow, title: 'History', label: 'History: design undo/redo tree', help: 'design/history', theme: true, requiresDesign: true }, // ── Analysis ──────────────────────────────────────────────────────────────── - 'optical-eval': { component: OpticalEvaluation, title: 'Optical Evaluation', label: 'Optical Evaluation: T/R/A plots', help: 'analysis/optical-evaluation', theme: true, requiresResolvedMaterials: true }, - 'wavelength-angle-map': { component: WavelengthAngleMap, title: 'Wavelength vs Angle', label: 'Wavelength vs Angle: T, R or A mapped over wavelength and angle of incidence', help: 'analysis/wavelength-angle-map', theme: true, requiresResolvedMaterials: true }, - 'color-eval': { component: ColorEvaluation, title: 'Color Evaluation', label: 'Color Evaluation: CIE diagram', help: 'analysis/color-evaluation', theme: true, requiresResolvedMaterials: true }, - 'admittance': { component: AdmittanceDiagram, title: 'Admittance Diagram', label: 'Admittance Diagram: locus plot', help: 'analysis/admittance', theme: true, requiresResolvedMaterials: true }, - 'efield': { component: EFieldEvaluation, title: 'Electric Field', label: 'Electric Field: |E(z)|² vs depth', help: 'analysis/efield', theme: true, requiresResolvedMaterials: true }, - 'ellipsometry': { component: EllipsometryEvaluation, title: 'Ellipsometry', label: 'Ellipsometry: Ψ(λ) and Δ(λ)', help: 'analysis/ellipsometry', theme: true, requiresResolvedMaterials: true }, - 'gd-gdd': { component: GDGDDEvaluation, title: 'Group Delay / GDD', label: 'Group Delay / GDD: dispersion', help: 'analysis/gd-gdd', theme: true, requiresResolvedMaterials: true }, + 'optical-eval': { component: OpticalEvaluation, title: 'Optical Evaluation', label: 'Optical Evaluation: T/R/A plots', help: 'analysis/optical-evaluation', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'wavelength-angle-map': { component: WavelengthAngleMap, title: 'Wavelength vs Angle', label: 'Wavelength vs Angle: T, R or A mapped over wavelength and angle of incidence', help: 'analysis/wavelength-angle-map', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'color-eval': { component: ColorEvaluation, title: 'Color Evaluation', label: 'Color Evaluation: CIE diagram', help: 'analysis/color-evaluation', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'admittance': { component: AdmittanceDiagram, title: 'Admittance Diagram', label: 'Admittance Diagram: locus plot', help: 'analysis/admittance', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'efield': { component: EFieldEvaluation, title: 'Electric Field', label: 'Electric Field: |E(z)|² vs depth', help: 'analysis/efield', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'ellipsometry': { component: EllipsometryEvaluation, title: 'Ellipsometry', label: 'Ellipsometry: Ψ(λ) and Δ(λ)', help: 'analysis/ellipsometry', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'gd-gdd': { component: GDGDDEvaluation, title: 'Group Delay / GDD', label: 'Group Delay / GDD: dispersion', help: 'analysis/gd-gdd', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + // Plots a material picked from the catalogs, not the design's stack. 'material-dispersion': { component: MaterialDispersionEvaluation, title: 'Material Dispersion', label: 'Material Dispersion: bulk phase, GD, GDD, and TOD', help: 'analysis/material-dispersion', theme: true }, - 'ri-profiler': { component: RefractiveIndexProfiler, title: 'RI Profiler', label: 'RI Profiler: n(z) and k(z)', help: 'analysis/refractive-index-profile', theme: true, requiresResolvedMaterials: true }, - 'layer-thicknesses': { component: LayerThicknesses, title: 'Layer Thicknesses', label: 'Layer Thicknesses: per-layer bar diagram', help: 'analysis/layer-thicknesses', theme: true, requiresResolvedMaterials: true }, - 'sensitivity': { component: LayerSensitivity, title: 'Layer Sensitivity', label: 'Layer Sensitivity: ∂MF/∂dᵢ', help: 'analysis/layer-sensitivity', theme: true, requiresResolvedMaterials: true }, - 'error-analysis': { component: ErrorAnalysis, title: 'Monte Carlo', label: 'Monte Carlo: manufacturing-error yield simulation', help: 'analysis/error-analysis', theme: true, requiresResolvedMaterials: true }, - 'integral-values': { component: IntegralValues, title: 'Integral Values', label: 'Integral Values: Tvis/Tsol/TUV/TNIR', help: 'analysis/integral-values', theme: true, requiresResolvedMaterials: true }, - 'systematic-dev': { component: SystematicDeviations, title: 'Systematic Deviations', label: 'Systematic Deviations: global perturbation sweep', help: 'analysis/systematic-deviations', theme: true, requiresResolvedMaterials: true }, - 'inhomogeneities': { component: Inhomogeneities, title: 'Inhomogeneities & Interlayers', label: 'Inhomogeneities & Interlayers: graded interface transitions', help: 'analysis/inhomogeneities', theme: true, requiresResolvedMaterials: true }, - 'roughness': { component: RoughnessScattering, title: 'Roughness / Scattering', label: 'Interface Roughness / Scattering: TIS(λ)', help: 'analysis/roughness-scattering', theme: true, requiresResolvedMaterials: true }, - 'stress': { component: StressAnalysis, title: 'Stress', label: 'Stress: per-film stress, substrate bow, cracking and delamination margins', help: 'analysis/stress', theme: true, requiresResolvedMaterials: true }, - 'plot-engine': { component: PlotEngine, title: 'Plot Engine', label: 'Plot Engine: custom XY plot builder', help: 'analysis/plot-engine', theme: true, requiresResolvedMaterials: true }, + 'ri-profiler': { component: RefractiveIndexProfiler, title: 'RI Profiler', label: 'RI Profiler: n(z) and k(z)', help: 'analysis/refractive-index-profile', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'layer-thicknesses': { component: LayerThicknesses, title: 'Layer Thicknesses', label: 'Layer Thicknesses: per-layer bar diagram', help: 'analysis/layer-thicknesses', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'sensitivity': { component: LayerSensitivity, title: 'Layer Sensitivity', label: 'Layer Sensitivity: ∂MF/∂dᵢ', help: 'analysis/layer-sensitivity', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'error-analysis': { component: ErrorAnalysis, title: 'Monte Carlo', label: 'Monte Carlo: manufacturing-error yield simulation', help: 'analysis/error-analysis', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'integral-values': { component: IntegralValues, title: 'Integral Values', label: 'Integral Values: Tvis/Tsol/TUV/TNIR', help: 'analysis/integral-values', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'systematic-dev': { component: SystematicDeviations, title: 'Systematic Deviations', label: 'Systematic Deviations: global perturbation sweep', help: 'analysis/systematic-deviations', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'inhomogeneities': { component: Inhomogeneities, title: 'Inhomogeneities & Interlayers', label: 'Inhomogeneities & Interlayers: graded interface transitions', help: 'analysis/inhomogeneities', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'roughness': { component: RoughnessScattering, title: 'Roughness / Scattering', label: 'Interface Roughness / Scattering: TIS(λ)', help: 'analysis/roughness-scattering', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'stress': { component: StressAnalysis, title: 'Stress', label: 'Stress: per-film stress, substrate bow, cracking and delamination margins', help: 'analysis/stress', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'plot-engine': { component: PlotEngine, title: 'Plot Engine', label: 'Plot Engine: custom XY plot builder', help: 'analysis/plot-engine', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, // ── Synthesis ───────────────────────────────────────────────────────────────── - 'refinement': { component: Refinement, title: 'Refinement', label: 'Refinement: SQP (default) / DLS / CG / Newton / Newton-CG / DLS multi-start / DE / Simulated Annealing (pick method, or Try-all)', help: 'synthesis/refinement', theme: true, requiresResolvedMaterials: true }, - 'needle': { component: NeedleVariation, title: 'Needle Automatic', label: 'Needle Automatic: automatic layer insertion loop', help: 'synthesis/needle', theme: true, requiresResolvedMaterials: true }, - 'needle-manual': { component: NeedleManual, title: 'Needle Manual', label: 'Needle Manual: pick position + material by hand', help: 'synthesis/needle', theme: true, requiresResolvedMaterials: true }, - 'gradual': { component: GradualEvolution, title: 'Gradual Evolution', label: 'Gradual Evolution: layer count ramp', help: 'synthesis/gradual-evolution', theme: true, requiresResolvedMaterials: true }, - 'structural': { component: StructuralOptimizer, title: 'Structural Optimizer', label: 'Structural Optimizer: random add/remove/split/merge layer mutations + simulated-annealing accept', help: 'synthesis/structural-optimizer', theme: true, requiresResolvedMaterials: true }, - 'design-cleaner': { component: DesignCleaner, title: 'Design Cleaner', label: 'Design Cleaner: merge thin layers', help: 'synthesis/design-cleaner', theme: true, requiresResolvedMaterials: true }, + 'refinement': { component: Refinement, title: 'Refinement', label: 'Refinement: SQP (default) / DLS / CG / Newton / Newton-CG / DLS multi-start / DE / Simulated Annealing (pick method, or Try-all)', help: 'synthesis/refinement', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'needle': { component: NeedleVariation, title: 'Needle Automatic', label: 'Needle Automatic: automatic layer insertion loop', help: 'synthesis/needle', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'needle-manual': { component: NeedleManual, title: 'Needle Manual', label: 'Needle Manual: pick position + material by hand', help: 'synthesis/needle', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'gradual': { component: GradualEvolution, title: 'Gradual Evolution', label: 'Gradual Evolution: layer count ramp', help: 'synthesis/gradual-evolution', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'structural': { component: StructuralOptimizer, title: 'Structural Optimizer', label: 'Structural Optimizer: random add/remove/split/merge layer mutations + simulated-annealing accept', help: 'synthesis/structural-optimizer', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + 'design-cleaner': { component: DesignCleaner, title: 'Design Cleaner', label: 'Design Cleaner: merge thin layers', help: 'synthesis/design-cleaner', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, 'filter-design': { help: 'synthesis/wdm-wizard' }, // ── Simulation ──────────────────────────────────────────────────────────────── - 'monitor-worksheet': { component: MonitorWorksheet, title: 'Monitor Worksheet', label: 'Monitor Worksheet: per-layer signal, swing and termination error on the witness chips the run would be monitored on', help: 'simulation/monitor-worksheet', requiresResolvedMaterials: true }, + 'monitor-worksheet': { component: MonitorWorksheet, title: 'Monitor Worksheet', label: 'Monitor Worksheet: per-layer signal, swing and termination error on the witness chips the run would be monitored on', help: 'simulation/monitor-worksheet', requiresDesign: true, requiresResolvedMaterials: true }, // ── Data Exchange ────────────────────────────────────────────────────────────── - 'process-sim': { component: ProcessSimulator, title: 'Process Exporter', label: 'Process Exporter: scrub through deposition + export .res files', help: 'simulation/process-simulator', theme: true, requiresResolvedMaterials: true }, + 'process-sim': { component: ProcessSimulator, title: 'Process Exporter', label: 'Process Exporter: scrub through deposition + export .res files', help: 'simulation/process-simulator', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, + // Their Import tabs open a coating file and save it to the Coating Library + // without a design; importing it into the front or back is off without one. 'zemax-coatings': { component: ZemaxCoatings, title: 'Zemax Coatings', label: 'Zemax Coatings: import / export COATING.DAT (materials + coatings)', help: 'data-exchange/zemax-coatings', theme: true, dialog: true }, - 'spectrum-exchange': { component: SpectrumExchange, title: 'Measured Spectra', label: 'Measured Spectra: import measured R/T/A spectra (CSV/TXT/ASCII/JCAMP-DX) as overlays; export design or measured spectra to CSV/JCAMP-DX', help: 'data-exchange/measured-spectra', theme: true }, - 'measured-ellipsometry': { component: MeasuredEllipsometry, title: 'Measured Ellipsometry', label: 'Measured Ellipsometry: import measured Ψ/Δ from a spectroscopic ellipsometer; export measured or calculated Ψ/Δ to CSV', help: 'data-exchange/measured-ellipsometry', theme: true }, - 'nk-characterization': { component: NkCharacterization, title: 'n,k Characterization', label: 'n,k Characterization: derive the n, k and thickness of a film from a measured R/T spectrum or a measured Ψ/Δ pair', help: 'data-exchange/nk-characterization', theme: true, dialog: true, createDesign: true }, + 'codev-coatings': { component: CodevCoatings, title: 'CODE V Coatings', label: 'CODE V Coatings: import / export CODE V MUL coatings (.seq, .mul)', help: 'data-exchange/codev-coatings', theme: true }, + // Measured curves are kept in the design: without one there is nothing to + // import into, and nothing measured or calculated to export. + 'spectrum-exchange': { component: SpectrumExchange, title: 'Measured Spectra', label: 'Measured Spectra: import measured R/T/A spectra (CSV/TXT/ASCII/JCAMP-DX) as overlays; export design or measured spectra to CSV/JCAMP-DX', help: 'data-exchange/measured-spectra', theme: true, requiresDesign: true }, + 'measured-ellipsometry': { component: MeasuredEllipsometry, title: 'Measured Ellipsometry', label: 'Measured Ellipsometry: import measured Ψ/Δ from a spectroscopic ellipsometer; export measured or calculated Ψ/Δ to CSV', help: 'data-exchange/measured-ellipsometry', theme: true, requiresDesign: true }, + // Fits the measured curves the design holds, so it has nothing to fit without one. + 'nk-characterization': { component: NkCharacterization, title: 'n,k Characterization', label: 'n,k Characterization: derive the n, k and thickness of a film from a measured R/T spectrum or a measured Ψ/Δ pair', help: 'data-exchange/nk-characterization', theme: true, dialog: true, createDesign: true, requiresDesign: true }, + // Can report over designs picked in the explorer while none is open. 'report-gen': { component: ReportWindow, title: 'Report', label: 'Report: a document built from blocks over one or several designs, saved as PDF or HTML', help: 'data-exchange/report-generator', theme: true, requiresResolvedMaterials: true }, // ── Dev / QA (opened from the dev-only View menu; not in the user ribbon) ─────── + // Runs its own built-in cases, whatever design is open. 'optimizer-benchmark': { component: OptimizerBenchmark, title: 'Optimizer Benchmark', label: 'Optimizer Benchmark: live cross-optimizer comparison (dev/QA)', help: 'index', theme: true, requiresResolvedMaterials: true }, // Experimental: opened from the application menu until it moves to the ribbon. - 'deep-synthesis': { component: DeepSynthesis, title: 'Deep Synthesis', label: 'Deep Synthesis: gradual evolution with the deep needle, then a search over the layer structure (experimental)', help: 'index', theme: true, requiresResolvedMaterials: true }, + 'deep-synthesis': { component: DeepSynthesis, title: 'Deep Synthesis', label: 'Deep Synthesis: gradual evolution with the deep needle, then a search over the layer structure (experimental)', help: 'index', theme: true, requiresDesign: true, requiresResolvedMaterials: true }, // ── Not in any ribbon tab: offered in the application menu only after the // version number in About has been clicked seven times ──────────────────── diff --git a/src/components/ui/MaterialPicker.js b/src/components/ui/MaterialPicker.js index 764cc6ca..3eb9655f 100644 --- a/src/components/ui/MaterialPicker.js +++ b/src/components/ui/MaterialPicker.js @@ -122,13 +122,17 @@ export function currentGroupOf(design, resolvedId) { * @param {boolean} [catalogsOnly] hide the open design's own materials, for * callers that build a stack outside it and resolve ids * against the catalogs alone + * + * With no design open there are no design materials to list either: the + * placeholder the provider hands out then is nobody's design. */ export function MaterialPicker({ value, onChange, c, t, compact, catalogsOnly }) { const mp = t.materialPicker; // Read directly rather than through useDesign(): the picker is also mounted // by windows that do not sit under a design provider. const context = useContext(DesignContext); - const design = catalogsOnly ? null : (context?.design || null); + const noDesign = catalogsOnly || context?.hasActiveDesign === false; + const design = noDesign ? null : (context?.design || null); // The trigger's colour, and its name for a catalog material, come from the // catalogs, which can change while the value stays the same. useCatalogRevision(); diff --git a/src/components/windows/dataExchange/chrome/panel.js b/src/components/windows/dataExchange/chrome/panel.js index b5a65b4f..29e802da 100644 --- a/src/components/windows/dataExchange/chrome/panel.js +++ b/src/components/windows/dataExchange/chrome/panel.js @@ -7,6 +7,7 @@ */ import { ActionButton, FieldLabel } from '../../analysis/chrome/controls.js'; +import { CenteredMessage, EditorGroupTitle, SidePanel } from '../../analysis/chrome/layout.js'; import { startDividerDrag } from '../../../ui/dividerDrag.js'; const { createElement: h, useCallback, useRef } = React; @@ -123,6 +124,30 @@ export function ImportLayout({ c, panelWidth, onPanelWidthChange, children }) { ); } +/** + * An import tab of a window that reads one file and lists what is in it: the + * file panel with the tab's own section under it on the left, the listing on + * the right. + * + * file props for ImportFilePanel + * section the tab's panel section, under the file panel + * children the right-hand side + */ +export function ImportPage({ c, file, section, panelWidth, onPanelWidthChange, children }) { + return h(ImportLayout, { c, panelWidth, onPanelWidthChange }, + h(SidePanel, { c, width: '100%' }, + h(ImportFilePanel, { c, ...file }), + section, + ), + h('div', { + style: { + flex: 1, minWidth: 0, minHeight: 0, overflow: 'auto', padding: 10, + display: 'flex', flexDirection: 'column', gap: 8, + }, + }, children), + ); +} + /** * A labelled field. The label column is wide enough for the longest of them, * "Angle of incidence", so the controls line up down the panel and no label @@ -158,3 +183,49 @@ export function textInputStyle(c) { fontFamily: FONT, }; } + +/** A one-line text field `width` pixels wide at most. */ +export function TextInput({ c, value, onChange, width, maxLength }) { + return h('input', { + value, maxLength, onChange: (event) => onChange(event.target.value), + style: { ...textInputStyle(c), flex: `0 1 ${width}px` }, + }); +} + +/** A file an export tab generated, in full, taking the height the options leave. */ +export function FilePreview({ c, title, text }) { + return h('div', { style: { flex: 1, minHeight: 160, display: 'flex', flexDirection: 'column', padding: '0 10px 10px' } }, + h(EditorGroupTitle, { c }, title), + h('textarea', { + value: text, readOnly: true, spellCheck: false, + style: { + flex: 1, width: '100%', resize: 'none', boxSizing: 'border-box', + background: c.bg, color: c.text, border: `1px solid ${c.border}`, borderRadius: 4, + fontFamily: 'ui-monospace, Consolas, monospace', fontSize: 10.5, padding: 8, outline: 'none', whiteSpace: 'pre', + }, + }), + ); +} + +/** + * An export tab that writes the open design into a coating file: the window's + * options with the generated file under them. With no design open there is no + * design to write, and while the design uses a material nothing here resolves + * the file would carry wrong n,k, so either is said in place of the options. + * + * z the window's strings: exportTitle, exportBlocked(ids), preview + * children the window's options + */ +export function CoatingExportPage({ c, t, z, hasActiveDesign = true, missingMaterialIds, preview, children }) { + if (!hasActiveDesign) return h(CenteredMessage, { c, message: t.windowChrome.noDesign }); + if (missingMaterialIds.length > 0) { + return h(PanelSection, { c, title: z.exportTitle }, + h('div', { role: 'alert', style: { color: c.error, fontSize: 11.5, lineHeight: 1.5 } }, + z.exportBlocked(missingMaterialIds.join(', '))), + ); + } + return h('div', { style: { flex: 1, minHeight: 0, overflow: 'auto', display: 'flex', flexDirection: 'column' } }, + children, + h(FilePreview, { c, title: z.preview, text: preview }), + ); +} diff --git a/src/components/windows/dataExchange/codevCoatings/AnglesField.js b/src/components/windows/dataExchange/codevCoatings/AnglesField.js new file mode 100644 index 00000000..74f99df3 --- /dev/null +++ b/src/components/windows/dataExchange/codevCoatings/AnglesField.js @@ -0,0 +1,59 @@ +import { tablerIcon } from '../../../ui/tablerIcons.js'; +import { CODEV_LIMITS } from '../../../../utils/io/codevCoatingFile.js'; +import { NumInput } from '../../analysis/chrome/controls.js'; +import { InlineRow } from '../chrome/panel.js'; + +const { createElement: h, useState } = React; + +function IconButton({ c, icon, title, onClick, disabled }) { + return h('button', { + type: 'button', onClick, disabled, title, 'aria-label': title, + style: { + display: 'flex', alignItems: 'center', padding: 2, background: 'transparent', border: 'none', + color: disabled ? c.textDim : c.text, cursor: disabled ? 'not-allowed' : 'pointer', + opacity: disabled ? 0.5 : 1, + }, + }, tablerIcon(icon, 14)); +} + +/** + * Whether an angle of incidence, in degrees, can be written. It is measured + * from the normal, so it runs from 0 towards 90, and 90 itself is out: there + * the light runs along the surface and never enters the coating. Any angle + * short of 90 is taken, decimals included. + */ +export const angleAccepted = (deg) => deg >= 0 && deg < 90; + +// One angle. NumInput clamps to a bound it is given, and 90 is no bound it +// could clamp to, so an entry outside the range is refused here instead: the +// field is mounted afresh under a new key and shows the angle it holds again. +function AngleInput({ c, value, onChange }) { + const [refused, setRefused] = useState(0); + return h(NumInput, { + key: refused, value, step: 1, c, width: 56, + onChange: (deg) => (angleAccepted(deg) ? onChange(deg) : setRefused(count => count + 1)), + }); +} + +/** + * The angles of incidence for ANG, degrees in the incident medium. CODE V takes + * up to five (CODEV_LIMITS.angles), and at least one is kept. A new angle starts + * as a copy of the last one. + */ +export function AnglesField({ c, z, anglesDeg, setAnglesDeg }) { + const setAt = (at, value) => setAnglesDeg(anglesDeg.map((angle, index) => (index === at ? value : angle))); + const removeAt = (at) => setAnglesDeg(anglesDeg.filter((_, index) => index !== at)); + const full = anglesDeg.length >= CODEV_LIMITS.angles; + return h(InlineRow, { c, label: z.angles }, + anglesDeg.map((angle, index) => h('div', { key: index, style: { display: 'flex', alignItems: 'center' } }, + h(AngleInput, { c, value: angle, onChange: (value) => setAt(index, value) }), + anglesDeg.length > 1 + ? h(IconButton, { c, icon: 'x', title: z.removeAngle, onClick: () => removeAt(index) }) + : null, + )), + h(IconButton, { + c, icon: 'plus', title: full ? z.anglesFull(CODEV_LIMITS.angles) : z.addAngle, disabled: full, + onClick: () => setAnglesDeg([...anglesDeg, anglesDeg[anglesDeg.length - 1] ?? 0]), + }), + ); +} diff --git a/src/components/windows/dataExchange/codevCoatings/CodevCoatings.js b/src/components/windows/dataExchange/codevCoatings/CodevCoatings.js new file mode 100644 index 00000000..4196f4bb --- /dev/null +++ b/src/components/windows/dataExchange/codevCoatings/CodevCoatings.js @@ -0,0 +1,55 @@ +/** + * Imports and exports CODE V MULTILAYER (MUL) coatings: the .seq command file + * that enters a stack in MDA, and the .mul file CODE V saves from it. File + * format conventions and conversions live in codevCoatingFile.js. + */ + +import { useDesign } from '../../../../state/DesignContext.js'; +import { useUnresolvedMaterials } from '../../../../utils/materials/useUnresolvedMaterials.js'; +import { usePersistentNumber } from '../../../ui/usePersistentState.js'; +import { useWindowSession } from '../../windowSession.js'; +import { useActionStatus } from '../chrome/actionStatus.js'; +import { CodevLayout } from './CodevLayout.js'; +import { codevCoatingsSession } from './sessionState.js'; +import { useGenerateAction, useSaveAction } from './useExportActions.js'; +import { useImportAction, useLibraryAction, useLoadAction } from './useImportActions.js'; + +const { createElement: h, useState } = React; + +const SETTERS = { + setTab: 'tab', setSide: 'side', setTitle: 'title', setSaveName: 'saveName', + setRefNm: 'refNm', setAnglesDeg: 'anglesDeg', setPanelWidth: 'panelWidth', +}; + +export function CodevCoatings({ c, t }) { + const z = t.codevCoatings; + const { design, updateDesign, checkpoint, hasActiveDesign } = useDesign(); + const missingMaterialIds = useUnresolvedMaterials(design); + const [session, setField, patch] = useWindowSession(codevCoatingsSession, design); + const [loading, setLoading] = useState(false); + const [library, setLibrary] = useState(null); + const { status, flash, clear } = useActionStatus(); + const [gStart, setGStart] = usePersistentNumber('tfstudio-codev-gStart', 400); + const [gEnd, setGEnd] = usePersistentNumber('tfstudio-codev-gEnd', 800); + const [gStep, setGStep] = usePersistentNumber('tfstudio-codev-gStep', 10); + const setters = Object.fromEntries(Object.entries(SETTERS).map(([name, key]) => [name, value => setField(key, value)])); + + const file = { z, flash, stack: session.stack, fileName: session.fileName, filePath: session.filePath }; + const onLoad = useLoadAction({ z, flash, clear, setLoading, setFile: patch }); + const importCoating = useImportAction({ ...file, checkpoint, updateDesign, hasActiveDesign }); + const saveToLibrary = useLibraryAction({ ...file, setLibrary }); + const exportArgs = { + ...session, z, flash, design, gStart, gEnd, gStep, + setExport: (preview, exportWarnings) => patch({ preview, exportWarnings }), + }; + const onGenerate = useGenerateAction(exportArgs); + const onSave = useSaveAction(exportArgs); + const onLibrarySaved = name => flash('success', t.coatingLibrary.saveDialog.saved(name)); + + return h(CodevLayout, { + ...session, ...setters, c, t, z, design, hasActiveDesign, loading, status, missingMaterialIds, + gStart, setGStart, gEnd, setGEnd, gStep, setGStep, + onLoad, importCoating, saveToLibrary, onGenerate, onSave, + library, closeLibrary: () => setLibrary(null), onLibrarySaved, + }); +} diff --git a/src/components/windows/dataExchange/codevCoatings/CodevLayout.js b/src/components/windows/dataExchange/codevCoatings/CodevLayout.js new file mode 100644 index 00000000..cc346064 --- /dev/null +++ b/src/components/windows/dataExchange/codevCoatings/CodevLayout.js @@ -0,0 +1,38 @@ +import { AnalysisWindow, ControlRow } from '../../analysis/chrome/layout.js'; +import { ReportAndNotices } from '../chrome/actionStatus.js'; +import { TabBtn } from '../chrome/panel.js'; +import { SaveCoatingDialog } from '../../design/coatingLibrary/SaveCoatingDialog.js'; +import { ExportTab } from './ExportTab.js'; +import { ImportTab } from './ImportTab.js'; +import { warningText } from './messages.js'; + +const { createElement: h } = React; + +const TABS = [['import', 'tabImport'], ['export', 'tabExport']]; + +/** + * The window's notices: on the Import tab what the reader noted about the file + * shown there, on the Export tab what the writer noted about the preview. + */ +export function tabNotices({ z, tab, stack, exportWarnings }) { + const warnings = (tab === 'export' ? exportWarnings : stack?.warnings) || []; + return warnings.map(warning => ({ label: warningText(z, warning), tone: 'warning' })); +} + +// The tabs, then what the last action did and the notices. +function Controls(props) { + const { c, t, z, tab, setTab, status } = props; + return h(ControlRow, { c }, + TABS.map(([id, label]) => h(TabBtn, { key: id, active: tab === id, onClick: () => setTab(id), c }, z[label])), + h(ReportAndNotices, { c, t, status, notices: tabNotices(props) }), + ); +} + +export function CodevLayout(props) { + const { c, t, tab, library, closeLibrary, onLibrarySaved } = props; + return h(AnalysisWindow, { c }, + h(Controls, props), + h(tab === 'export' ? ExportTab : ImportTab, props), + library && h(SaveCoatingDialog, { coating: library, c, t, onClose: closeLibrary, onSaved: onLibrarySaved }), + ); +} diff --git a/src/components/windows/dataExchange/codevCoatings/ExportTab.js b/src/components/windows/dataExchange/codevCoatings/ExportTab.js new file mode 100644 index 00000000..1dfed6d1 --- /dev/null +++ b/src/components/windows/dataExchange/codevCoatings/ExportTab.js @@ -0,0 +1,66 @@ +import { CODEV_LIMITS } from '../../../../utils/io/codevCoatingFile.js'; +import { + ActionButton, ChoiceGroup, FieldLabel, NumInput, RangeField, +} from '../../analysis/chrome/controls.js'; +import { + CoatingExportPage, InlineRow, PanelSection, TextInput, +} from '../chrome/panel.js'; +import { AnglesField } from './AnglesField.js'; +import { analysisWavelengths, sideStack } from './exportModel.js'; + +const { createElement: h } = React; + +// The side written, and the commands named after the design or taken from it. +function StackOptions({ c, z, side, setSide, title, setTitle, saveName, setSaveName, refNm, setRefNm }) { + return h(React.Fragment, null, + h(InlineRow, { c, label: z.side }, + h(ChoiceGroup, { + c, activeId: side, onSelect: setSide, + items: [{ id: 'front', label: z.sideFront }, { id: 'back', label: z.sideBack }], + }), + ), + h(InlineRow, { c, label: z.titleField }, + h(TextInput, { c, value: title, onChange: setTitle, width: 240, maxLength: CODEV_LIMITS.title })), + h(InlineRow, { c, label: z.saveName }, + h(TextInput, { c, value: saveName, onChange: setSaveName, width: 160 })), + h(InlineRow, { c, label: z.refWavelength }, + h(NumInput, { value: refNm, onChange: setRefNm, positive: true, step: 10, c, width: 70 })), + ); +} + +// From, to and step of the WL list, with the count they give; past CODE V's +// limit the count is shown as the refusal Generate would give. +function WavelengthRow({ c, z, gStart, setGStart, gEnd, setGEnd, gStep, setGStep }) { + const { count } = analysisWavelengths(gStart, gEnd, gStep); + const tooMany = count > CODEV_LIMITS.wavelengths; + return h(InlineRow, { c, label: z.wavelengths }, + h(RangeField, { + c, + from: { value: gStart, onChange: setGStart, positive: true, step: 10 }, + to: { value: gEnd, onChange: setGEnd, positive: true, step: 10 }, + }), + h(FieldLabel, { c }, z.step), + h(NumInput, { value: gStep, onChange: setGStep, positive: true, step: 5, c, width: 56 }), + h('span', { style: { fontSize: 11, color: tooMany ? c.error : c.textDim } }, + tooMany ? z.errTooManyWavelengths(count, CODEV_LIMITS.wavelengths) : z.wavelengthCount(count)), + ); +} + +function ExportOptions(props) { + const { c, z, design, side, preview, onGenerate, onSave } = props; + return h(PanelSection, { c, title: z.exportTitle }, + h('div', { style: { fontSize: 11, color: c.textDim, lineHeight: 1.45 } }, + z.exportHint(sideStack(design, side).layers.length)), + h(StackOptions, props), + h(WavelengthRow, props), + h(AnglesField, props), + h(InlineRow, { c }, + h(ActionButton, { c, label: z.generate, onClick: onGenerate }), + h(ActionButton, { c, label: z.saveBtn, onClick: onSave, disabled: !preview }), + ), + ); +} + +export function ExportTab(props) { + return h(CoatingExportPage, props, h(ExportOptions, props)); +} diff --git a/src/components/windows/dataExchange/codevCoatings/ImportTab.js b/src/components/windows/dataExchange/codevCoatings/ImportTab.js new file mode 100644 index 00000000..f7fb9d89 --- /dev/null +++ b/src/components/windows/dataExchange/codevCoatings/ImportTab.js @@ -0,0 +1,110 @@ +import { ActionButton } from '../../analysis/chrome/controls.js'; +import { CenteredMessage, EditorGroupTitle } from '../../analysis/chrome/layout.js'; +import { ImportPage, PanelSection } from '../chrome/panel.js'; +import { LockIcon } from '../../../ui/LockIcon.js'; +import { td, th } from '../zemaxCoatings/ui.js'; +import { fileNumber } from './fileStack.js'; + +const { createElement: h } = React; + +// A MIC material by its label, a constant index as CODE V wrote it. +const indexText = (index) => { + if (index.label != null) return index.label; + return index.k > 0 ? `n ${index.n}, k ${index.k}` : `n ${index.n}`; +}; + +const rangeText = (z, wavelengthsNm) => { + if (!wavelengthsNm.length) return ''; + const shown = wavelengthsNm.map(fileNumber); + return z.wavelengthsValue(shown.length, Math.min(...shown), Math.max(...shown)); +}; + +function SummaryRow({ c, label, value }) { + return h('tr', null, + h('td', { style: { ...td(c), color: c.textDim, whiteSpace: 'nowrap', width: 1 } }, label), + h('td', { style: td(c) }, value), + ); +} + +// What the file sets around its layers. +function StackSummary({ c, z, stack }) { + const rows = [ + [z.summaryIncident, indexText(stack.incident)], + [z.summarySubstrate, indexText(stack.substrate)], + [z.summaryWavelengths, rangeText(z, stack.wavelengthsNm)], + [z.summaryAngles, stack.anglesDeg.map(angle => `${angle}°`).join(', ')], + [z.summaryRef, `${fileNumber(stack.refNm)} nm`], + ]; + return h(PanelSection, { c }, + h('table', { style: { borderCollapse: 'collapse' } }, + h('tbody', null, rows.map(([label, value]) => h(SummaryRow, { key: label, c, label, value }))))); +} + +function layerRow(layer, index, c) { + return h('tr', { key: index }, + h('td', { style: { ...td(c), color: c.textDim } }, index + 1), + h('td', { style: td(c) }, indexText(layer.index)), + h('td', { style: { ...td(c), textAlign: 'right', fontVariantNumeric: 'tabular-nums' } }, layer.thicknessNm.toFixed(2)), + h('td', { style: { ...td(c), color: c.textDim } }, + layer.code === 100 ? h(LockIcon, { locked: true, size: 11 }) : null), + ); +} + +function LayersTable({ c, z, layers }) { + return h('table', { style: { width: '100%', borderCollapse: 'collapse' } }, + h('thead', null, h('tr', null, + h('th', { style: { ...th(c), width: 30 } }, '#'), + h('th', { style: th(c) }, z.colMaterial), + h('th', { style: { ...th(c), textAlign: 'right' } }, z.colThickness), + h('th', { style: { ...th(c), width: 60 } }, z.colLocked), + )), + h('tbody', null, layers.map((layer, index) => layerRow(layer, index, c))), + ); +} + +// Why an import button is off, or undefined while it is on. With no design +// selected there is nothing to import into. In Symmetric mode the design +// replaces any back coating written to it with the mirror of the front, so +// layers imported to the back would not stay there. +function offReason(z, side, design, hasActiveDesign) { + if (hasActiveDesign === false) return z.importNoDesign; + if (side === 'back' && design?.surfaceMode === 'symmetric') return z.importBackSymmetric; + return undefined; +} + +function ImportButton({ c, z, side, label, design, hasActiveDesign, importCoating }) { + const reason = offReason(z, side, design, hasActiveDesign); + return h('span', { title: reason }, + h(ActionButton, { c, label, onClick: () => importCoating(side), disabled: reason != null })); +} + +// The stack read, its layers as the file lists them, and what can be done with it. +function StackView({ c, z, stack, fileName, design, hasActiveDesign, importCoating, saveToLibrary }) { + const importProps = { c, z, design, hasActiveDesign, importCoating }; + return h(React.Fragment, null, + h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } }, + h('div', { style: { fontWeight: 600, fontSize: 12, marginRight: 'auto' } }, stack.title || fileName), + h(ImportButton, { ...importProps, side: 'front', label: z.importToFront }), + h(ImportButton, { ...importProps, side: 'back', label: z.importToBack }), + h(ActionButton, { c, label: z.saveToLibrary, title: z.saveToLibraryTip, onClick: saveToLibrary }), + ), + h(EditorGroupTitle, { c }, z.layersHeader(stack.layers.length)), + h(LayersTable, { c, z, layers: stack.layers }), + ); +} + +/** + * The panel that opens the file and sums up what it sets, on the left; the + * layers it holds and their actions, on the right. + */ +export function ImportTab(props) { + const { c, z, stack, loading, onLoad, fileName, panelWidth, setPanelWidth } = props; + return h(ImportPage, { + c, panelWidth, onPanelWidthChange: setPanelWidth, + file: { + title: z.fileTitle, label: loading ? z.loading : z.openBtn, + onImport: onLoad, loading, fileName, hint: z.fileHint, + }, + section: stack && h(StackSummary, { c, z, stack }), + }, stack ? h(StackView, props) : h(CenteredMessage, { c, message: z.noFile })); +} diff --git a/src/components/windows/dataExchange/codevCoatings/catalogImport.js b/src/components/windows/dataExchange/codevCoatings/catalogImport.js new file mode 100644 index 00000000..0dff021b --- /dev/null +++ b/src/components/windows/dataExchange/codevCoatings/catalogImport.js @@ -0,0 +1,97 @@ +/** + * Where the materials of an imported CODE V coating go: a user catalog named + * after the file, made on the first import and merged into on every later one. + */ + +import { addCatalog, getCatalogs, materialIdTaken } from '../../../../utils/materials/catalogManager.js'; +import { randomHex } from '../../../../utils/materials/catalogManager/randomHex.js'; + +const baseName = (fileName) => String(fileName || 'coating').replace(/\.[^.]*$/, ''); +const slug = (text) => text.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'coating'; +const nameKey = (material) => String(material?.name ?? '').trim().toUpperCase(); + +/** + * The catalog an import from this file goes into. + * + * A file imported before (the same path) goes back into its own catalog. A new + * one gets the file's base name plus a random part as its id, as a new user + * catalog does: two files of one name, on one computer or on two, would + * otherwise share an id, and a design made with one would be computed with the + * other's material of the same name. + * + * @param {string} fileName base name shown to the user, e.g. `AR4.seq` + * @param {string} [filePath] full path, used to recognise a re-import + * @param {Object[]} [catalogs] the catalogs to look in + * @returns {{ id: string, name: string, existing: Object|null }} + */ +export function catalogIdFor(fileName, filePath, catalogs = getCatalogs()) { + const own = filePath && catalogs.find(cat => cat.id.startsWith('codev_') && cat.sourceFile === filePath); + if (own) return { id: own.id, name: own.name, existing: own }; + + const base = baseName(fileName); + const taken = new Set(catalogs.map(cat => cat.id)); + let id; + do id = `codev_${slug(base)}_${randomHex(4)}`; while (taken.has(id)); + return { id, name: `CODE V ${base}`, existing: null }; +} + +// `base`, or the first of `base_2`, `base_3`, … that `taken` says is free. +function freeId(taken, base) { + let id = base; + let suffix = 2; + while (taken(id)) id = `${base}_${suffix++}`; + return id; +} + +// The id under which the catalog held a material of this name before the +// import, or null. +function heldId(existing, material) { + const held = existing?.materials || {}; + return Object.keys(held).find(id => nameKey(held[id]) === nameKey(material)) || null; +} + +/** + * What an import of these materials writes to the file's catalog, without + * writing it. + * + * A material the catalog already holds under the same name is kept as it is, + * edits made in the Material Editor included, and the coating is built with + * it; any other is added under an id the catalog never gave out (see + * materialIdTaken). Nothing else of the catalog is touched. A file never + * imported gets the catalog a first import would make, whose random part (see + * catalogIdFor) is drawn anew on each call. + * + * @param {Array<{ key: string, material: Object }>} materials as codevStackToDesign returns them + * @param {string} fileName + * @param {string} [filePath] + * @returns {{ cat: Object, idOf: Object }} the catalog as it + * would be written, and each key's `catalogId:materialId` + */ +export function codevRegistration(materials, fileName, filePath) { + const { id: catId, name, existing } = catalogIdFor(fileName, filePath); + const held = { ...(existing?.materials || {}) }; + const idOf = {}; + for (const { key, material } of materials) { + let id = heldId(existing, material); + if (!id) { + id = freeId(candidate => !!held[candidate] || materialIdTaken(existing, candidate), material.id || 'material'); + held[id] = { ...material, id }; + } + idOf[key] = `${catId}:${id}`; + } + const cat = existing + ? { ...existing, materials: held } + : { id: catId, name, source: 'user', sourceFile: filePath || null, materials: held }; + return { cat, idOf }; +} + +/** + * Put the materials of one import into the file's catalog, as + * codevRegistration describes. + * + * @returns {{ catName: string, idOf: Object }} each key's `catalogId:materialId` + */ +export function registerCodevMaterials(materials, fileName, filePath) { + const { cat, idOf } = codevRegistration(materials, fileName, filePath); + return { catName: addCatalog(cat).name, idOf }; +} diff --git a/src/components/windows/dataExchange/codevCoatings/exportModel.js b/src/components/windows/dataExchange/codevCoatings/exportModel.js new file mode 100644 index 00000000..08431e99 --- /dev/null +++ b/src/components/windows/dataExchange/codevCoatings/exportModel.js @@ -0,0 +1,66 @@ +import { buildCodevSeq, CODEV_LIMITS } from '../../../../utils/io/codevCoatingFile.js'; +import { buildGrid } from '../../../../utils/io/zemaxCoatingFile.js'; +import { designMaterialLookup, isBuiltinId } from '../../../../utils/materials/designMaterials.js'; + +// A built-in material stored bare ('TiO2', as synthesis inserts it and old +// files have it) is the one the catalogs list as 'builtin:TiO2'. Both +// spellings get one MIC entry and one label. +const canonicalId = (id) => (id && !id.includes(':') && isBuiltinId(id) ? `builtin:${id}` : id); + +const codevLayer = (layer) => ({ + material: canonicalId(layer.material), thickness: layer.thickness, locked: !!layer.locked, +}); + +/** + * One side of a design as CODE V takes it: from the incident medium to the + * substrate, thickness in nm, layers of zero thickness left out. + * + * Front: the design's incident medium, then frontLayers as stored (incident + * side first), then the substrate. + * + * Back: on a glass/air surface CODE V wants air as the incident medium and the + * glass as the substrate, whichever face of the element it is (MUL Technical + * Notes, "Sequential Surfaces"). The incident medium is therefore the design's + * exit medium and the layers run from the exit side to the substrate, which is + * backLayers reversed: backLayers are stored substrate side first. + */ +export function sideStack(design, side) { + const back = side === 'back'; + const stored = back ? [...(design.backLayers || [])].reverse() : (design.frontLayers || []); + return { + incident: canonicalId(back ? design.exitMedium : design.incidentMedium), + substrate: canonicalId(design.substrate?.material), + layers: stored.filter(layer => layer.thickness > 0).map(codevLayer), + }; +} + +/** + * The analysis wavelengths of the From, To and Step fields, nm, and their + * count. Past the WL limit only the count is worked out, so a very small step + * does not build a long list just to have it refused. + */ +export function analysisWavelengths(startNm, endNm, stepNm) { + const span = Math.abs(endNm - startNm) / stepNm; + if (!(span < CODEV_LIMITS.wavelengths)) return { count: Math.ceil(span - 1e-6) + 1, wavelengthsNm: null }; + const wavelengthsNm = buildGrid(startNm, endNm, stepNm); + return { count: wavelengthsNm.length, wavelengthsNm }; +} + +/** + * The .seq text for one side of `design`. Every material is resolved through + * the design, the way the design computes it: the catalog here first, then the + * definition the design carries. An id that resolves nowhere throws + * UnresolvedDesignMaterialError rather than being written as air. + * + * @returns {{ text: string, warnings: object[] }} as buildCodevSeq returns them + * @throws {CodevExportError} past a limit of the MUL option + */ +export function buildDesignSeq(design, { side, title, saveName, wavelengthsNm, anglesDeg, refNm }) { + const lookup = designMaterialLookup(design); + return buildCodevSeq({ + ...sideStack(design, side), + title, saveName, wavelengthsNm, anglesDeg, refNm, + materialName: (id) => lookup(id).name || id, + getNK: (id, wavelengthNm) => lookup(id).getNK(wavelengthNm), + }); +} diff --git a/src/components/windows/dataExchange/codevCoatings/fileStack.js b/src/components/windows/dataExchange/codevCoatings/fileStack.js new file mode 100644 index 00000000..38193b6f --- /dev/null +++ b/src/components/windows/dataExchange/codevCoatings/fileStack.js @@ -0,0 +1,84 @@ +/** + * A stack read from a CODE V file: its numbers as the window shows them, its + * conversion to TFStudio materials and layers, and the coating Save to Coating + * Library takes from it. + */ + +import { CodevParseError, codevStackToDesign } from '../../../../utils/io/codevCoatingFile.js'; +import { shortestNear } from '../../../../utils/io/codevCoating/mulReader.js'; +import { codevRegistration } from './catalogImport.js'; +import { parseErrorText } from './messages.js'; + +// CODE V holds a wavelength as a float32 in µm: WLG adds its step in single +// precision and a .mul stores every value so. A float32 carries 24 significant +// bits, the .mul reader reads a stored value back within 2^-23 of it, and a +// value is shown the same way: WLG 400 800 10 shows 400, not 400.0000059604645. +const FLOAT32_TOLERANCE = 2 ** -23; + +/** A wavelength of the file, nm, as shown: the shortest decimal a float32 of it allows. */ +export const fileNumber = (value) => shortestNear(value, FLOAT32_TOLERANCE); + +/** + * codevStackToDesign of the stack, or `{ error }` with the text of the reader + * error that stopped it. + */ +export function convertStack(z, stack, fileName) { + try { + return codevStackToDesign(stack, { sourceName: fileName }); + } catch (err) { + if (!(err instanceof CodevParseError)) throw err; + return { error: parseErrorText(z, err) }; + } +} + +const AIR = 'builtin:Air'; +const baseName = (fileName) => String(fileName || 'coating').replace(/\.[^.]*$/, ''); + +// CODE V's default incident medium, n 1 with no k, is vacuum. TFStudio's +// built-in Air has that index at every wavelength, so an inline index of +// exactly n 1, k 0 is saved as Air rather than as a material of its own. +const isAir = (index) => !('label' in index) && index.n === 1 && index.k === 0; + +/** + * The stack as the coating the Save Coating dialog takes. + * + * The layers are the ones the import puts on the front coating: incident side + * first, thickness in nm, each material under the id the import gives it in + * the catalog of the file, carrying the record the import holds there, edits + * included for a file imported before. The incident medium and the substrate + * are the file's INC and SUB, built and named the same way, except that an + * inline n 1, k 0 is built-in Air. Nothing is written to a catalog, so the + * saved coating computes with the records it carries on a computer that never + * imported the file. + * + * The dialog opens with the band of the file's analysis wavelengths, shortest + * to longest, and the angle of its first ANG. A file with one analysis + * wavelength has no band, and the dialog opens with its own. + * + * @returns {{ coating: object } | { error: string }} + */ +export function libraryCoating({ z, stack, fileName, filePath }) { + const converted = convertStack(z, stack, fileName); + if (converted.error) return converted; + const media = [[stack.incident, converted.incidentKey], [stack.substrate, converted.substrateKey]]; + const keys = new Set([ + ...converted.layers.map(layer => layer.materialKey), + ...media.filter(([index]) => !isAir(index)).map(([, key]) => key), + ]); + const { cat, idOf } = codevRegistration(converted.materials.filter(({ key }) => keys.has(key)), fileName, filePath); + const materials = Object.fromEntries([...keys].map(key => [idOf[key], cat.materials[idOf[key].slice(cat.id.length + 1)]])); + const [incidentMedium, substrate] = media.map(([index, key]) => (isAir(index) ? AIR : idOf[key])); + const wavelengths = stack.wavelengthsNm.map(fileNumber); + const band = [Math.min(...wavelengths), Math.max(...wavelengths)]; + return { + coating: { + name: stack.title.trim() || baseName(fileName), + source: z.librarySource(fileName), + layers: converted.layers.map(({ materialKey, thickness }) => ({ material: idOf[materialKey], thickness })), + materials, incidentMedium, substrate, + referenceWavelength: fileNumber(stack.refNm), + ...(band[1] > band[0] ? { band } : {}), + aoi: stack.anglesDeg[0] ?? 0, + }, + }; +} diff --git a/src/components/windows/dataExchange/codevCoatings/messages.js b/src/components/windows/dataExchange/codevCoatings/messages.js new file mode 100644 index 00000000..0afc9277 --- /dev/null +++ b/src/components/windows/dataExchange/codevCoatings/messages.js @@ -0,0 +1,57 @@ +/** + * User-facing text for what the CODE V reader and writer report. Both report a + * kind and its values, never a sentence, so the text is chosen here from the + * window's locale namespace `z` (t.codevCoatings). + */ + +const PARSE_ERRORS = { + noStack: (z) => z.errNoStack, + unknownGroup: (z, d) => z.errUnknownGroup(d.label), + unknownMaterial: (z, d) => z.errUnknownMaterial(d.label), + badNumber: (z, d) => z.errBadNumber(d.line, d.text), + notMul: (z) => z.errNotMul, + missingCommand: (z, d) => z.errMissingCommand(d.command), + micMismatch: (z, d) => z.errMicMismatch(d.label, d.line), +}; + +const EXPORT_ERRORS = { + layers: (z, d) => z.errTooManyLayers(d.count, d.limit), + wavelengths: (z, d) => (d.count === 0 ? z.errNoWavelengths : z.errTooManyWavelengths(d.count, d.limit)), + angles: (z, d) => z.errTooManyAngles(d.count, d.limit), + noIndex: (z, d) => z.errNoIndex(d.material), +}; + +// The commands an extraValues warning names by themselves; any other name is +// the label of a MIC entry, whose values sit on a line of their own. +const VALUE_COMMANDS = new Set(['WL', 'MWL', 'EXT']); + +const WARNINGS = { + unknownCommand: (z, w) => z.warnUnknownCommand(w.command, w.line), + extraValues: (z, w) => (VALUE_COMMANDS.has(w.command) + ? z.warnExtraValues(w.command, w.line, w.count, w.limit) + : z.warnExtraMicValues(w.command, w.line, w.count, w.limit)), + decimalComma: (z, w) => z.warnDecimalComma(w.line), + refOutsideTable: (z, w) => z.warnRefOutsideTable(w.label), + coupledLayers: (z, w) => z.warnCoupledLayers(w.count), + resampled: (z, w) => z.warnResampled(w.material, w.from, w.to), + mediumAbsorbs: (z, w) => (w.role === 'incident' + ? z.warnIncidentAbsorbs(w.material) : z.warnSubstrateAbsorbs(w.material)), +}; + +/** Text for a CodevParseError. */ +export function parseErrorText(z, err) { + const text = PARSE_ERRORS[err.kind]; + return text ? text(z, err.detail || {}) : z.errParse(err.kind); +} + +/** Text for a CodevExportError. */ +export function exportErrorText(z, err) { + const text = EXPORT_ERRORS[err.kind]; + return text ? text(z, err.detail || {}) : z.errExport(err.kind); +} + +/** Text for one warning of the reader or the writer. */ +export function warningText(z, warning) { + const text = WARNINGS[warning.kind]; + return text ? text(z, warning) : z.warnOther(warning.kind); +} diff --git a/src/components/windows/dataExchange/codevCoatings/sessionState.js b/src/components/windows/dataExchange/codevCoatings/sessionState.js new file mode 100644 index 00000000..0bb228d5 --- /dev/null +++ b/src/components/windows/dataExchange/codevCoatings/sessionState.js @@ -0,0 +1,30 @@ +import { createWindowSession } from '../../windowSession.js'; + +// The CODE V file read on the Import tab belongs to the file rather than to any +// design, so it stays loaded across design switches. The export settings that +// are named after the design or taken from it (title, SAV name, REF) are +// reseeded when another design is selected, and a preview built from the +// previous design is dropped. `panelWidth` is the Import tab's panel width once +// the divider has been dragged. +export const codevCoatingsSession = createWindowSession({ + tab: 'import', + stack: null, + fileName: '', + filePath: '', + side: 'front', + title: '', + saveName: '', + refNm: 550, + anglesDeg: [0], + preview: '', + exportWarnings: [], + panelWidth: null, +}, { + onDesignChange: (design) => ({ + title: design?.name || '', + saveName: design?.name || '', + refNm: design?.referenceWavelength || 550, + preview: '', + exportWarnings: [], + }), +}); diff --git a/src/components/windows/dataExchange/codevCoatings/useExportActions.js b/src/components/windows/dataExchange/codevCoatings/useExportActions.js new file mode 100644 index 00000000..c191385c --- /dev/null +++ b/src/components/windows/dataExchange/codevCoatings/useExportActions.js @@ -0,0 +1,63 @@ +import { CodevExportError, CODEV_LIMITS } from '../../../../utils/io/codevCoatingFile.js'; +import { UnresolvedDesignMaterialError } from '../../../../utils/materials/designMaterials.js'; +import { analysisWavelengths, buildDesignSeq, sideStack } from './exportModel.js'; +import { exportErrorText } from './messages.js'; + +const { useCallback } = React; + +// The text for a build that failed on the design's data, or null for an error +// that is a fault of the program and has to surface as one. +function buildFailure(z, err) { + if (err instanceof UnresolvedDesignMaterialError) return z.exportUnresolved(err.materialId); + if (err instanceof CodevExportError) return exportErrorText(z, err); + return null; +} + +function tryBuild({ z, design, side, title, saveName, anglesDeg, refNm }, wavelengthsNm, fail) { + try { + return buildDesignSeq(design, { side, title, saveName, wavelengthsNm, anglesDeg, refNm }); + } catch (err) { + const message = buildFailure(z, err); + if (message === null) throw err; + fail(message); + return null; + } +} + +function generatePreview(args) { + const { z, flash, design, side, gStart, gEnd, gStep, setExport } = args; + const fail = (message) => { flash('error', message); setExport('', []); }; + const layerCount = sideStack(design, side).layers.length; + if (!layerCount) { fail(side === 'back' ? z.nothingToExportBack : z.nothingToExportFront); return; } + const { count, wavelengthsNm } = analysisWavelengths(gStart, gEnd, gStep); + if (!wavelengthsNm) { fail(z.errTooManyWavelengths(count, CODEV_LIMITS.wavelengths)); return; } + const built = tryBuild(args, wavelengthsNm, fail); + if (!built) return; + setExport(built.text, built.warnings); + flash('success', z.generated(layerCount, built.warnings.length)); +} + +// The name the SAV line gives the .mul, so the .seq is offered under the same one. +const savName = (text) => (/^SAV (\S+)/m.exec(text) || [])[1] || 'coating'; + +async function savePreview({ z, flash, preview }) { + if (!preview) return; + try { + const result = await window.electronAPI.codevSaveCoatingFile(preview, `${savName(preview)}.seq`); + if (result?.success) flash('success', z.savedFile(result.filePath)); + else if (!result?.canceled) flash('error', z.errSave(result?.error || '')); + } catch (error) { + flash('error', z.errSave(error.message)); + } +} + +export function useGenerateAction(args) { + const { design, side, title, saveName, gStart, gEnd, gStep, anglesDeg, refNm, z } = args; + return useCallback(() => generatePreview(args), + [design, side, title, saveName, gStart, gEnd, gStep, anglesDeg, refNm, z]); +} + +export function useSaveAction(args) { + const { preview, z } = args; + return useCallback(() => savePreview(args), [preview, z]); +} diff --git a/src/components/windows/dataExchange/codevCoatings/useImportActions.js b/src/components/windows/dataExchange/codevCoatings/useImportActions.js new file mode 100644 index 00000000..de5006e8 --- /dev/null +++ b/src/components/windows/dataExchange/codevCoatings/useImportActions.js @@ -0,0 +1,98 @@ +import { CodevParseError, parseCodevMul, parseCodevSeq } from '../../../../utils/io/codevCoatingFile.js'; +import { registerCodevMaterials } from './catalogImport.js'; +import { convertStack, libraryCoating } from './fileStack.js'; +import { parseErrorText, warningText } from './messages.js'; + +const { useCallback } = React; + +// A .mul is the file CODE V saves with SAV; anything else is read as commands. +const parserFor = (fileName) => (/\.mul$/i.test(fileName || '') ? parseCodevMul : parseCodevSeq); + +// The text for a reader error about the file, or null for any other error. +const readFailure = (z, err) => (err instanceof CodevParseError ? parseErrorText(z, err) : null); + +function readPicked(result, { z, flash, setFile }) { + const fileName = result.fileName || ''; + const stack = parserFor(fileName)(result.text); + setFile({ stack, fileName, filePath: result.filePath || '' }); + flash('success', z.loadedFile(fileName, stack.layers.length)); +} + +async function loadCodevFile({ z, flash, clear, setLoading, setFile }) { + setLoading(true); + clear(); + try { + const result = await window.electronAPI.codevPickCoatingFile(); + if (result?.success) readPicked(result, { z, flash, setFile }); + else if (!result?.canceled) flash('error', z.errLoad(result?.error || '')); + } catch (error) { + flash('error', readFailure(z, error) || z.errLoad(error.message)); + } + setLoading(false); +} + +// The layers of the stack replace one coating of the active design, as one undo +// step; the design keeps its incident medium, exit medium and substrate. The +// file lists the layers incident side first, the order frontLayers are kept in. +// backLayers are kept substrate side first, so a back import is reversed: the +// file's incident medium faces the exit side. The materials the layers use are +// registered first, in the catalog named after the file, and Undo leaves them +// there; the file's INC and SUB are not registered unless a layer uses them too. +// The report adds what the conversion noted, and is then a warning, which stays +// until the next action rather than clearing itself; what the reader noted is +// in the window's notices already. With no design selected the window has a +// placeholder that nothing keeps, so nothing is registered or written. +function importStack({ z, flash, stack, fileName, filePath, checkpoint, updateDesign, hasActiveDesign }, side) { + if (!stack) return; + if (hasActiveDesign === false) { + flash('error', z.importNoDesign); + return; + } + const converted = convertStack(z, stack, fileName); + if (converted.error) { + flash('error', converted.error); + return; + } + const used = new Set(converted.layers.map(layer => layer.materialKey)); + const { catName, idOf } = registerCodevMaterials( + converted.materials.filter(({ key }) => used.has(key)), fileName, filePath); + const layers = converted.layers.map(layer => ({ + material: idOf[layer.materialKey], thickness: layer.thickness, locked: layer.locked, + })); + const back = side === 'back'; + checkpoint(); + updateDesign(back ? { backLayers: layers.reverse() } : { frontLayers: layers }); + const notes = converted.warnings + .filter(warning => !stack.warnings.includes(warning)) + .map(warning => warningText(z, warning)); + const imported = back ? z.importedBack : z.importedFront; + flash(notes.length ? 'warning' : 'success', [imported(layers.length, catName), ...notes].join(' ')); +} + +// The Save Coating dialog opens on the stack, or the report says why it cannot. +function openLibraryDialog({ stack, flash, setLibrary, ...args }) { + if (!stack) return; + const built = libraryCoating({ stack, ...args }); + if (built.error) { + flash('error', built.error); + return; + } + setLibrary(built.coating); +} + +export function useLoadAction(args) { + return useCallback(() => loadCodevFile(args), [args.z]); +} + +/** The import as a function of the side, 'front' or 'back'. */ +export function useImportAction(args) { + const { stack, fileName, filePath, checkpoint, updateDesign, hasActiveDesign, z } = args; + return useCallback((side) => importStack(args, side), + [stack, fileName, filePath, checkpoint, updateDesign, hasActiveDesign, z]); +} + +/** Open the Save Coating dialog on the stack read, or report why it cannot be saved. */ +export function useLibraryAction(args) { + const { stack, fileName, filePath, z } = args; + return useCallback(() => openLibraryDialog(args), [stack, fileName, filePath, z]); +} diff --git a/src/components/windows/dataExchange/zemaxCoatings/CoatingsTab.js b/src/components/windows/dataExchange/zemaxCoatings/CoatingsTab.js index 4664a7b7..32ed788b 100644 --- a/src/components/windows/dataExchange/zemaxCoatings/CoatingsTab.js +++ b/src/components/windows/dataExchange/zemaxCoatings/CoatingsTab.js @@ -1,6 +1,10 @@ +import { ActionButton } from '../../analysis/chrome/controls.js'; +import { CenteredMessage, EditorGroupTitle } from '../../analysis/chrome/layout.js'; +import { PanelSection } from '../chrome/panel.js'; import { LockIcon } from '../../../ui/LockIcon.js'; +import { ImportPage } from './ImportPage.js'; import { coatLayerThkNm } from './model.js'; -import { Btn, td, th } from './ui.js'; +import { td, th } from './ui.js'; const { createElement: h } = React; @@ -23,36 +27,63 @@ function coatingListRow(coating, index, { c, z, selCoating, setSelCoating }) { }, h('td', { style: { ...td(c), display: 'flex', alignItems: 'center', gap: 5 } }, importable ? null : h('span', { style: { display: 'inline-flex', color: c.textDim }, title: z.notImportable }, h(LockIcon, { locked: true, size: 11 })), - h('span', null, coating.name || '—'), + h('span', null, coating.name), ), h('td', { style: { ...td(c), color: c.textDim } }, coatingTypeLabel(z, coating.type)), h('td', { style: { ...td(c), textAlign: 'right', color: c.textDim } }, importable ? coating.layers.length : ''), ); } +function CoatingList({ c, z, doc, selCoating, setSelCoating }) { + return h(PanelSection, { c, title: z.tabCoatings }, + h('table', { style: { width: '100%', borderCollapse: 'collapse' } }, + h('thead', null, h('tr', null, + h('th', { style: th(c) }, z.colName), + h('th', { style: th(c) }, z.colType), + h('th', { style: { ...th(c), textAlign: 'right' } }, z.colLayers), + )), + h('tbody', null, doc.coatings.map((coating, index) => coatingListRow(coating, index, { c, z, selCoating, setSelCoating }))), + ), + ); +} + +// A relative thickness for which the file's MATE tables give no index at λ₀ +// shows as "?". function coatingLayerRow(layer, index, { c, z, materialsByName, refNm }) { const thickness = coatLayerThkNm(layer, materialsByName, refNm); return h('tr', { key: index }, h('td', { style: { ...td(c), color: c.textDim } }, index + 1), h('td', { style: td(c) }, layer.material), h('td', { style: { ...td(c), textAlign: 'right', fontVariantNumeric: 'tabular-nums' } }, - Number.isFinite(thickness) ? `${thickness.toFixed(2)} nm` : '—'), + Number.isFinite(thickness) ? `${thickness.toFixed(2)} nm` : '?'), h('td', { style: { ...td(c), color: c.textDim } }, layer.isAbsolute ? `${layer.thickness} ${z.modeAbs}` : `${layer.thickness} ${z.modeRel}`), ); } -function coatingDetail(selected, { c, z, materialsByName, refNm, importCoating }) { - if (!selected) return h('div', { style: { color: c.textDim, fontSize: 12, padding: 12 } }, z.selectCoating); - if (selected.type !== 'layers') return h('div', { style: { color: c.textDim, fontSize: 12, padding: 12 } }, z.importNotStack); - return [ - h('div', { key: 'h', style: { display: 'flex', alignItems: 'center', gap: 10 } }, - h('div', { style: { fontWeight: 600, fontSize: 12 } }, selected.name), - h('div', { style: { flex: 1 } }), - h(Btn, { onClick: importCoating, c, primary: true }, z.importToFront), +// With no design selected there is nothing to import into. +function ImportButton({ c, z, hasActiveDesign, importCoating }) { + const noDesign = hasActiveDesign === false; + return h('span', { title: noDesign ? z.importNoDesign : undefined }, + h(ActionButton, { c, label: z.importToFront, onClick: importCoating, disabled: noDesign })); +} + +// The selected COAT, its layers as the file lists them, and what can be done +// with it. A layer naming a material the file defines more than once is +// converted with the last record of that name, as the import does. +function CoatingDetail({ c, z, doc, selected, refNm, hasActiveDesign, importCoating, saveToLibrary }) { + if (!selected) return h(CenteredMessage, { c, message: z.selectCoating }); + if (selected.type !== 'layers') return h(CenteredMessage, { c, message: z.importNotStack }); + const materialsByName = {}; + for (const material of doc.materials) materialsByName[material.name.toUpperCase()] = material; + return h(React.Fragment, null, + h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } }, + h('div', { style: { fontWeight: 600, fontSize: 12, marginRight: 'auto' } }, selected.name), + h(ImportButton, { c, z, hasActiveDesign, importCoating }), + h(ActionButton, { c, label: z.saveToLibrary, title: z.saveToLibraryTip, onClick: saveToLibrary }), ), - h('div', { key: 'lh', style: { fontSize: 10, color: c.textDim, textTransform: 'uppercase', letterSpacing: '0.4px' } }, z.layersHeader), - h('table', { key: 'lt', style: { width: '100%', borderCollapse: 'collapse' } }, + h(EditorGroupTitle, { c }, z.layersHeader), + h('table', { style: { width: '100%', borderCollapse: 'collapse' } }, h('thead', null, h('tr', null, h('th', { style: { ...th(c), width: 30 } }, '#'), h('th', { style: th(c) }, z.colMaterial), @@ -61,31 +92,19 @@ function coatingDetail(selected, { c, z, materialsByName, refNm, importCoating } )), h('tbody', null, selected.layers.map((layer, index) => coatingLayerRow(layer, index, { c, z, materialsByName, refNm }))), ), - h('div', { key: 'note', style: { fontSize: 10.5, color: c.textDim, marginTop: 4 } }, z.importNotStack), - ]; + h('div', { style: { fontSize: 10.5, color: c.textDim } }, z.importNotStack), + ); } -export function CoatingsTab({ c, z, doc, selCoating, setSelCoating, refNm, importCoating }) { - if (!doc) return h('div', { style: { color: c.textDim, fontSize: 12, padding: 20, textAlign: 'center' } }, z.noFile); - if (!doc.coatings.length) return h('div', { style: { color: c.textDim, fontSize: 12, padding: 20, textAlign: 'center' } }, z.noCoatings); - - const selected = doc.coatings[selCoating]; - const materialsByName = {}; - for (const material of doc.materials) materialsByName[material.name.toUpperCase()] = material; +function coatingsBody(props) { + const { c, z, doc, selCoating } = props; + if (!doc) return h(CenteredMessage, { c, message: z.noFile }); + if (!doc.coatings.length) return h(CenteredMessage, { c, message: z.noCoatings }); + return h(CoatingDetail, { ...props, selected: doc.coatings[selCoating] }); +} - return h('div', { style: { display: 'flex', gap: 12, height: '100%' } }, - h('div', { style: { flex: '0 0 300px', overflow: 'auto', border: `1px solid ${c.border}`, borderRadius: 4 } }, - h('table', { style: { width: '100%', borderCollapse: 'collapse' } }, - h('thead', null, h('tr', null, - h('th', { style: th(c) }, z.colName), - h('th', { style: th(c) }, z.colType), - h('th', { style: { ...th(c), textAlign: 'right' } }, z.colLayers), - )), - h('tbody', null, doc.coatings.map((coating, index) => coatingListRow(coating, index, { c, z, selCoating, setSelCoating }))), - ), - ), - h('div', { style: { flex: 1, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 8 } }, - coatingDetail(selected, { c, z, materialsByName, refNm, importCoating }), - ), - ); +export function CoatingsTab(props) { + const hasCoatings = props.doc?.coatings.length > 0; + return h(ImportPage, { ...props, section: hasCoatings ? h(CoatingList, props) : null }, + coatingsBody(props)); } diff --git a/src/components/windows/dataExchange/zemaxCoatings/ExportTab.js b/src/components/windows/dataExchange/zemaxCoatings/ExportTab.js index 1d127fcc..b32ac252 100644 --- a/src/components/windows/dataExchange/zemaxCoatings/ExportTab.js +++ b/src/components/windows/dataExchange/zemaxCoatings/ExportTab.js @@ -1,75 +1,52 @@ -import { Btn, Label, Num, Seg } from './ui.js'; +import { + ActionButton, ChoiceGroup, FieldLabel, NumInput, RangeField, +} from '../../analysis/chrome/controls.js'; +import { + CoatingExportPage, InlineRow, PanelSection, TextInput, +} from '../chrome/panel.js'; const { createElement: h } = React; -function ExportOptions({ c, z, thMode, setThMode, scope, setScope, coatName, setCoatName }) { - return h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 16, alignItems: 'center' } }, - h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } }, - h(Label, { c }, z.thicknessMode), - h('div', { style: { display: 'flex' } }, - h(Seg, { active: thMode === 'absolute', onClick: () => setThMode('absolute'), c, position: 'first' }, z.thicknessAbs), - h(Seg, { active: thMode === 'relative', onClick: () => setThMode('relative'), c, position: 'last' }, z.thicknessRel), - ), +function ExportOptions(props) { + const { + c, z, design, thMode, setThMode, scope, setScope, coatName, setCoatName, refNm, + gStart, setGStart, gEnd, setGEnd, gStep, setGStep, preview, onGenerate, onSave, + } = props; + return h(PanelSection, { c, title: z.exportTitle }, + h('div', { style: { fontSize: 11, color: c.textDim } }, z.frontLayerCount((design.frontLayers || []).length)), + h(InlineRow, { c, label: z.thicknessMode }, + h(ChoiceGroup, { + c, activeId: thMode, onSelect: setThMode, + items: [{ id: 'absolute', label: z.thicknessAbs }, { id: 'relative', label: z.thicknessRel }], + }), ), - h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } }, - h(Label, { c }, z.materialScope), - h('div', { style: { display: 'flex' } }, - h(Seg, { active: scope === 'used', onClick: () => setScope('used'), c, position: 'first' }, z.scopeUsed), - h(Seg, { active: scope === 'all', onClick: () => setScope('all'), c, position: 'last' }, z.scopeAll), - ), + h('div', { style: { fontSize: 10.5, color: c.textDim, lineHeight: 1.45 } }, + thMode === 'absolute' ? z.thicknessAbsHint : `${z.thicknessRelHint} (λ₀ = ${refNm} nm)`), + h(InlineRow, { c, label: z.materialScope }, + h(ChoiceGroup, { + c, activeId: scope, onSelect: setScope, + items: [{ id: 'used', label: z.scopeUsed }, { id: 'all', label: z.scopeAll }], + }), ), - h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } }, - h(Label, { c }, z.coatingName), - h('input', { - value: coatName, onChange: (event) => setCoatName(event.target.value), - style: { height: 24, width: 180, background: c.bg, color: c.text, border: `1px solid ${c.border}`, borderRadius: 3, fontSize: 11, padding: '0 6px', outline: 'none' }, + h(InlineRow, { c, label: z.coatingName }, + h(TextInput, { c, value: coatName, onChange: setCoatName, width: 200 }), + ), + h(InlineRow, { c, label: z.sampleGrid }, + h(RangeField, { + c, + from: { value: gStart, onChange: setGStart, positive: true, step: 10 }, + to: { value: gEnd, onChange: setGEnd, positive: true, step: 10 }, }), + h(FieldLabel, { c }, z.step), + h(NumInput, { value: gStep, onChange: setGStep, positive: true, step: 5, c, width: 56 }), + ), + h(InlineRow, { c }, + h(ActionButton, { c, label: z.generate, onClick: onGenerate }), + h(ActionButton, { c, label: z.saveBtn, onClick: onSave, disabled: !preview }), ), - ); -} - -function SampleGrid({ c, z, gStart, setGStart, gEnd, setGEnd, gStep, setGStep }) { - return h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } }, - h(Label, { c }, z.sampleGrid), - h(Label, { c }, z.from), h(Num, { value: gStart, onChange: setGStart, positive: true, step: 10, c, width: 64 }), - h(Label, { c }, z.to), h(Num, { value: gEnd, onChange: setGEnd, positive: true, step: 10, c, width: 64 }), - h(Label, { c }, z.step), h(Num, { value: gStep, onChange: setGStep, positive: true, step: 5, c, width: 56 }), ); } export function ExportTab(props) { - const { - c, z, design, thMode, gStart, gEnd, gStep, scope, coatName, - preview, onGenerate, onSave, refNm, missingMaterialIds, - } = props; - const layerCount = (design.frontLayers || []).length; - if (missingMaterialIds.length > 0) { - return h('div', { - role: 'alert', - style: { color: c.error, fontSize: 11.5, lineHeight: 1.5 }, - }, z.exportBlocked(missingMaterialIds.join(', '))); - } - return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 10, height: '100%' } }, - h('div', { style: { fontSize: 12, fontWeight: 600 } }, z.exportTitle), - h('div', { style: { fontSize: 10.5, color: c.textDim } }, `${layerCount} front-coating layer${layerCount === 1 ? '' : 's'}`), - h(ExportOptions, props), - h(SampleGrid, props), - h('div', { style: { fontSize: 10.5, color: c.textDim } }, - thMode === 'absolute' ? z.thicknessAbsHint : `${z.thicknessRelHint} (λ₀ = ${refNm} nm)`), - h('div', { style: { display: 'flex', gap: 8 } }, - h(Btn, { onClick: onGenerate, c, primary: true }, z.generate), - h(Btn, { onClick: onSave, c, disabled: !preview }, z.saveBtn), - ), - h('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', minHeight: 120 } }, - h(Label, { c }, z.preview), - h('textarea', { - value: preview, readOnly: true, spellCheck: false, - style: { - flex: 1, marginTop: 4, width: '100%', resize: 'none', - background: c.bg, color: c.text, border: `1px solid ${c.border}`, borderRadius: 4, - fontFamily: 'ui-monospace, Consolas, monospace', fontSize: 10.5, padding: 8, outline: 'none', whiteSpace: 'pre', - }, - }), - ), - ); + return h(CoatingExportPage, props, h(ExportOptions, props)); } diff --git a/src/components/windows/dataExchange/zemaxCoatings/ImportPage.js b/src/components/windows/dataExchange/zemaxCoatings/ImportPage.js new file mode 100644 index 00000000..6166f6e3 --- /dev/null +++ b/src/components/windows/dataExchange/zemaxCoatings/ImportPage.js @@ -0,0 +1,20 @@ +import { ImportPage as FileImportPage } from '../chrome/panel.js'; + +const { createElement: h } = React; + +/** + * A Zemax import tab. Both import tabs read the one loaded file, so each + * carries the button that opens it. + * + * section the tab's panel section, under the file panel + * children the right-hand side + */ +export function ImportPage({ c, z, loading, onLoad, fileName, panelWidth, setPanelWidth, section, children }) { + return h(FileImportPage, { + c, section, panelWidth, onPanelWidthChange: setPanelWidth, + file: { + title: z.fileTitle, label: loading ? z.loading : z.loadBtn, + onImport: onLoad, loading, fileName, hint: z.fileHint, + }, + }, children); +} diff --git a/src/components/windows/dataExchange/zemaxCoatings/MaterialsTab.js b/src/components/windows/dataExchange/zemaxCoatings/MaterialsTab.js index 9d5eb291..74cab036 100644 --- a/src/components/windows/dataExchange/zemaxCoatings/MaterialsTab.js +++ b/src/components/windows/dataExchange/zemaxCoatings/MaterialsTab.js @@ -1,52 +1,76 @@ +import { ActionButton } from '../../analysis/chrome/controls.js'; +import { CenteredMessage } from '../../analysis/chrome/layout.js'; +import { InlineRow, PanelSection } from '../chrome/panel.js'; import { Checkbox } from '../../../ui/Checkbox.js'; -import { Btn, td, th } from './ui.js'; +import { fileMaterialNames } from './catalogImport.js'; +import { ImportPage } from './ImportPage.js'; +import { td, th } from './ui.js'; const { createElement: h } = React; -function materialRow(material, index, { c, selMats, toggle }) { +// A name the file defines more than once: which record of it this row is, and +// the name the row is imported under. +function RepeatMark({ c, z, row }) { + return h('span', { + title: z.repeatedRowTip(row.name), + style: { marginLeft: 6, color: c.warning, fontSize: 10.5, whiteSpace: 'nowrap' }, + }, z.repeatedRow(row.repeat.index, row.repeat.count)); +} + +// One MATE record. The selection holds its position in the file, so two +// records of one name are checked and imported one at a time. +function materialRow(material, index, { c, z, selRows, toggle, names }) { const low = material.points.length ? material.points[0][0] : 0; const high = material.points.length ? material.points[material.points.length - 1][0] : 0; - const checked = selMats.has(material.name); + const checked = selRows.has(index); return h('tr', { - key: index, onClick: () => toggle(material.name), + key: index, onClick: () => toggle(index), style: { cursor: 'pointer', background: checked ? c.accent + '18' : 'transparent' }, }, h('td', { style: { ...td(c), textAlign: 'center' } }, h(Checkbox, { c, checked, readOnly: true })), - h('td', { style: td(c) }, material.name), + h('td', { style: td(c) }, material.name, names[index].repeat && h(RepeatMark, { c, z, row: names[index] })), h('td', { style: { ...td(c), textAlign: 'right', color: c.textDim } }, material.points.length), - h('td', { style: { ...td(c), color: c.textDim } }, material.points.length ? `${low}–${high}` : '—'), + h('td', { style: { ...td(c), color: c.textDim } }, material.points.length ? `${low}–${high}` : ''), ); } -export function MaterialsTab({ c, z, doc, selMats, setSelMats, importMaterials }) { - if (!doc) return h('div', { style: { color: c.textDim, fontSize: 12, padding: 20, textAlign: 'center' } }, z.noFile); - if (!doc.materials.length) return h('div', { style: { color: c.textDim, fontSize: 12, padding: 20, textAlign: 'center' } }, z.noMaterials); - - const toggle = (name) => { - const next = new Set(selMats); - if (next.has(name)) next.delete(name); - else next.add(name); - setSelMats(next); +function MaterialTable({ c, z, doc, selRows, setSelRows }) { + const toggle = (index) => { + const next = new Set(selRows); + if (next.has(index)) next.delete(index); + else next.add(index); + setSelRows(next); }; + const names = fileMaterialNames(doc.materials); + return h('table', { style: { width: '100%', borderCollapse: 'collapse' } }, + h('thead', null, h('tr', null, + h('th', { style: { ...th(c), width: 28 } }, ''), + h('th', { style: th(c) }, z.colName), + h('th', { style: { ...th(c), textAlign: 'right' } }, z.colPoints), + h('th', { style: th(c) }, z.colRange), + )), + h('tbody', null, doc.materials.map((material, index) => materialRow(material, index, { c, z, selRows, toggle, names }))), + ); +} - return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, height: '100%' } }, - h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } }, - h(Btn, { onClick: () => setSelMats(new Set(doc.materials.map((material) => material.name))), c }, z.selectAll), - h(Btn, { onClick: () => setSelMats(new Set()), c }, z.clearSel), - h('div', { style: { flex: 1 } }), - h(Btn, { onClick: () => importMaterials(false), c, primary: true }, z.importSelected), - h(Btn, { onClick: () => importMaterials(true), c }, z.importAll), +function SelectionSection({ c, z, doc, setSelRows, importMaterials }) { + return h(PanelSection, { c, title: z.tabMaterials }, + h(InlineRow, { c }, + h(ActionButton, { c, label: z.selectAll, onClick: () => setSelRows(new Set(doc.materials.keys())) }), + h(ActionButton, { c, label: z.clearSel, onClick: () => setSelRows(new Set()) }), ), - h('div', { style: { flex: 1, overflow: 'auto', border: `1px solid ${c.border}`, borderRadius: 4 } }, - h('table', { style: { width: '100%', borderCollapse: 'collapse' } }, - h('thead', null, h('tr', null, - h('th', { style: { ...th(c), width: 28 } }, ''), - h('th', { style: th(c) }, z.colName), - h('th', { style: { ...th(c), textAlign: 'right' } }, z.colPoints), - h('th', { style: th(c) }, z.colRange), - )), - h('tbody', null, doc.materials.map((material, index) => materialRow(material, index, { c, selMats, toggle }))), - ), + h(InlineRow, { c }, + h(ActionButton, { c, label: z.importSelected, onClick: () => importMaterials(false) }), + h(ActionButton, { c, label: z.importAll, onClick: () => importMaterials(true) }), ), ); } + +export function MaterialsTab(props) { + const { c, z, doc } = props; + const hasMaterials = doc?.materials.length > 0; + const body = !doc ? h(CenteredMessage, { c, message: z.noFile }) + : !hasMaterials ? h(CenteredMessage, { c, message: z.noMaterials }) + : h(MaterialTable, props); + return h(ImportPage, { ...props, section: hasMaterials ? h(SelectionSection, props) : null }, body); +} diff --git a/src/components/windows/dataExchange/zemaxCoatings/ZemaxCoatings.js b/src/components/windows/dataExchange/zemaxCoatings/ZemaxCoatings.js index 8945dbc1..6ff68efb 100644 --- a/src/components/windows/dataExchange/zemaxCoatings/ZemaxCoatings.js +++ b/src/components/windows/dataExchange/zemaxCoatings/ZemaxCoatings.js @@ -6,60 +6,62 @@ import { useDesign } from '../../../../state/DesignContext.js'; import { useUnresolvedMaterials } from '../../../../utils/materials/useUnresolvedMaterials.js'; import { usePersistentNumber } from '../../../ui/usePersistentState.js'; +import { useActionStatus } from '../chrome/actionStatus.js'; +import { fileMaterialNames } from './catalogImport.js'; import { zemaxCoatingsSession } from './sessionState.js'; import { useWindowSession } from '../../windowSession.js'; import { - useCoatingImportAction, useLoadAction, useMaterialImportAction, + useCoatingImportAction, useLibraryAction, useLoadAction, useMaterialImportAction, warningsSuffix, } from './useImportActions.js'; import { useGenerateAction, useSaveAction } from './useExportActions.js'; import { ZemaxLayout } from './ZemaxLayout.js'; const { createElement: h, useState } = React; +const SETTERS = { + setTab: 'tab', setSelCoating: 'selCoating', setSelRows: 'selRows', setThMode: 'thMode', + setScope: 'scope', setCoatName: 'coatName', setPreview: 'preview', setPanelWidth: 'panelWidth', +}; + +// A file that defines a material name more than once is ambiguous about which +// record a COAT layer of that name means, so the window says so. +function repeatedNameNotices(z, doc) { + const names = fileMaterialNames(doc?.materials || []) + .filter(({ repeat }) => repeat?.index === 1) + .map(({ name }) => name); + if (!names.length) return []; + return [{ label: z.repeatedNames(names.length), detail: z.repeatedNamesDetail(names.join(', ')), tone: 'warning' }]; +} + export function ZemaxCoatings({ c, t, setInputDialog }) { const z = t.zemaxCoatings; - const { design, updateDesign, checkpoint } = useDesign(); + const { design, updateDesign, checkpoint, hasActiveDesign } = useDesign(); const missingMaterialIds = useUnresolvedMaterials(design); - const [session, setField] = useWindowSession(zemaxCoatingsSession, design); - const { tab, doc, fileName, filePath, selCoating, selMats, thMode, scope, coatName, preview } = session; - const setTab = value => setField('tab', value); - const setDoc = value => setField('doc', value); - const setFileName = value => setField('fileName', value); - const setFilePath = value => setField('filePath', value); - const setSelCoating = value => setField('selCoating', value); - const setSelMats = value => setField('selMats', value); - const setThMode = value => setField('thMode', value); - const setScope = value => setField('scope', value); - const setCoatName = value => setField('coatName', value); - const setPreview = value => setField('preview', value); + const [session, setField, patch] = useWindowSession(zemaxCoatingsSession, design); const [loading, setLoading] = useState(false); - const [status, setStatus] = useState(null); + const [library, setLibraryCoating] = useState(null); + const { status, flash, clear } = useActionStatus(); const [refNm, setRefNm] = usePersistentNumber('tfstudio-zemax-refNm', 550); const [gStart, setGStart] = usePersistentNumber('tfstudio-zemax-gStart', 400); const [gEnd, setGEnd] = usePersistentNumber('tfstudio-zemax-gEnd', 800); const [gStep, setGStep] = usePersistentNumber('tfstudio-zemax-gStep', 25); - const flash = (type, msg) => setStatus({ type, msg }); + const setters = Object.fromEntries(Object.entries(SETTERS).map(([name, key]) => [name, value => setField(key, value)])); - const shared = { z, flash, doc, fileName, filePath, selCoating, selMats, refNm }; - const onLoad = useLoadAction({ - ...shared, setLoading, setStatus, setDoc, setFileName, setFilePath, setSelCoating, setSelMats, - }); - const importCoating = useCoatingImportAction({ - ...shared, checkpoint, updateDesign, - }); + const { doc, fileName, filePath, selCoating, selRows } = session; + const shared = { z, flash, doc, fileName, filePath, selCoating, selRows, refNm }; + const onLoad = useLoadAction({ z, flash, clear, setLoading, setFile: patch }); + const importCoating = useCoatingImportAction({ ...shared, checkpoint, updateDesign, hasActiveDesign }); + const saveToLibrary = useLibraryAction({ ...shared, design, setLibraryCoating }); const importMaterials = useMaterialImportAction({ ...shared, setInputDialog }); - const exportArgs = { - z, flash, design, gStart, gEnd, gStep, scope, coatName, thMode, - refNm, preview, setPreview, - }; + const exportArgs = { ...session, z, flash, design, gStart, gEnd, gStep, refNm, setPreview: setters.setPreview }; const onGenerate = useGenerateAction(exportArgs); const onSave = useSaveAction(exportArgs); + const onLibrarySaved = name => flash('success', t.coatingLibrary.saveDialog.saved(name) + warningsSuffix(z, library.warnings)); return h(ZemaxLayout, { - c, z, design, tab, setTab, doc, fileName, selCoating, setSelCoating, - selMats, setSelMats, thMode, setThMode, scope, setScope, coatName, - setCoatName, preview, loading, status, refNm, setRefNm, gStart, - setGStart, gEnd, setGEnd, gStep, setGStep, onLoad, importCoating, - importMaterials, onGenerate, onSave, missingMaterialIds, + ...session, ...setters, c, t, z, design, hasActiveDesign, loading, status, missingMaterialIds, + notices: repeatedNameNotices(z, doc), refNm, setRefNm, gStart, setGStart, gEnd, setGEnd, gStep, setGStep, + onLoad, importCoating, saveToLibrary, importMaterials, onGenerate, onSave, + library, closeLibrary: () => setLibraryCoating(null), onLibrarySaved, }); } diff --git a/src/components/windows/dataExchange/zemaxCoatings/ZemaxLayout.js b/src/components/windows/dataExchange/zemaxCoatings/ZemaxLayout.js index 2efb2d24..169a48ec 100644 --- a/src/components/windows/dataExchange/zemaxCoatings/ZemaxLayout.js +++ b/src/components/windows/dataExchange/zemaxCoatings/ZemaxLayout.js @@ -1,61 +1,34 @@ +import { Divider, FieldLabel, NumInput } from '../../analysis/chrome/controls.js'; +import { AnalysisWindow, ControlRow } from '../../analysis/chrome/layout.js'; +import { ReportAndNotices } from '../chrome/actionStatus.js'; +import { TabBtn } from '../chrome/panel.js'; +import { SaveCoatingDialog } from '../../design/coatingLibrary/SaveCoatingDialog.js'; import { CoatingsTab } from './CoatingsTab.js'; import { ExportTab } from './ExportTab.js'; import { MaterialsTab } from './MaterialsTab.js'; -import { Btn, Label, Num, TabBtn } from './ui.js'; const { createElement: h } = React; -function Header(props) { - const { c, z, onLoad, loading, fileName, refNm, setRefNm } = props; - return h('div', { style: { padding: '8px 12px', borderBottom: `1px solid ${c.border}`, background: c.panel, flexShrink: 0 } }, - h('div', { style: { fontSize: 13, fontWeight: 600, marginBottom: 2 } }, z.title), - h('div', { style: { fontSize: 10.5, color: c.textDim, lineHeight: 1.4, marginBottom: 8 } }, z.subtitle), - h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } }, - h(Btn, { onClick: onLoad, c, primary: true, disabled: loading }, loading ? z.loading : z.loadBtn), - fileName ? h('span', { style: { fontSize: 11, color: c.textDim } }, z.loadedFile(fileName)) : null, - h('div', { style: { flex: 1 } }), - h(Label, { c }, z.refWavelength), - h(Num, { value: refNm, onChange: setRefNm, positive: true, step: 10, c, width: 70 }), - ), - ); -} +const TABS = [['coatings', 'tabCoatings'], ['materials', 'tabMaterials'], ['export', 'tabExport']]; +const TAB_BODY = { coatings: CoatingsTab, materials: MaterialsTab, export: ExportTab }; -function Tabs({ c, z, tab, setTab }) { - return h('div', { style: { display: 'flex', borderBottom: `1px solid ${c.border}`, background: c.panel, flexShrink: 0 } }, - h(TabBtn, { active: tab === 'coatings', onClick: () => setTab('coatings'), c }, z.tabCoatings), - h(TabBtn, { active: tab === 'materials', onClick: () => setTab('materials'), c }, z.tabMaterials), - h(TabBtn, { active: tab === 'export', onClick: () => setTab('export'), c }, z.tabExport), +// The tabs, then λ₀, which converts relative thicknesses both ways and so +// belongs to no one tab, then what the last action did. +function Controls({ c, t, z, tab, setTab, refNm, setRefNm, status, notices }) { + return h(ControlRow, { c }, + TABS.map(([id, label]) => h(TabBtn, { key: id, active: tab === id, onClick: () => setTab(id), c }, z[label])), + h(Divider, { c }), + h(FieldLabel, { c }, z.refWavelength), + h(NumInput, { value: refNm, onChange: setRefNm, positive: true, step: 10, c, width: 64, title: z.refWaveHint }), + h(ReportAndNotices, { c, t, status, notices }), ); } -function Body(props) { - const { c, tab } = props; - const content = tab === 'coatings' ? h(CoatingsTab, props) - : tab === 'materials' ? h(MaterialsTab, props) - : h(ExportTab, props); - return h('div', { style: { flex: 1, overflow: 'auto', padding: 12 } }, content); -} - -function Status({ c, status }) { - if (!status) return null; - return h('div', { - style: { - padding: '6px 12px', fontSize: 11, flexShrink: 0, - borderTop: `1px solid ${c.border}`, - background: status.type === 'error' ? (c.error + '22') : (c.success + '22'), - color: status.type === 'error' ? c.error : c.text, - }, - }, status.msg); -} - export function ZemaxLayout(props) { - const { c, status } = props; - return h('div', { - style: { display: 'flex', flexDirection: 'column', height: '100%', background: c.bg, color: c.text, fontFamily: 'system-ui, -apple-system, sans-serif', overflow: 'hidden' }, - }, - h(Header, props), - h(Tabs, props), - h(Body, props), - h(Status, { c, status }), + const { c, t, tab, library, closeLibrary, onLibrarySaved } = props; + return h(AnalysisWindow, { c }, + h(Controls, props), + h(TAB_BODY[tab] || CoatingsTab, props), + library && h(SaveCoatingDialog, { coating: library.coating, c, t, onClose: closeLibrary, onSaved: onLibrarySaved }), ); } diff --git a/src/components/windows/dataExchange/zemaxCoatings/catalogImport.js b/src/components/windows/dataExchange/zemaxCoatings/catalogImport.js index 832873d0..98d597fa 100644 --- a/src/components/windows/dataExchange/zemaxCoatings/catalogImport.js +++ b/src/components/windows/dataExchange/zemaxCoatings/catalogImport.js @@ -45,29 +45,74 @@ function freeId(taken, base) { return id; } +// `name (n)`, or the first number up from `n` that names no record of the +// file; the name it returns is taken from then on. +function freeName(taken, name, n) { + let candidate; + do candidate = `${name} (${n++})`; while (taken.has(candidate.toUpperCase())); + taken.add(candidate.toUpperCase()); + return candidate; +} + +/** + * How the import tells the file's MATE records apart, in file order. + * + * Zemax names are case-insensitive, and a file can define one name more than + * once. Each record is imported on its own: the first of a name keeps it, and + * every later one gets ` (2)`, ` (3)`, … after it, as a material copied into a + * catalog that already holds its name does (freeMaterialName). `repeat` says + * which record of a repeated name a row is, 1-based, and how many the file has. + * + * @param {Array<{name: string}>} materials the file's MATE records + * @returns {Array<{ name: string, repeat: { index: number, count: number } | null }>} + */ +export function fileMaterialNames(materials) { + const names = materials.map(material => String(material.name ?? '').trim()); + const keys = names.map(name => name.toUpperCase()); + const count = {}; + for (const key of keys) count[key] = (count[key] || 0) + 1; + const taken = new Set(keys); + const seen = {}; + return names.map((name, row) => { + const key = keys[row]; + seen[key] = (seen[key] || 0) + 1; + if (count[key] === 1) return { name, repeat: null }; + const repeat = { index: seen[key], count: count[key] }; + return { name: repeat.index === 1 ? name : freeName(taken, name, repeat.index), repeat }; + }); +} + +/** The rows of the file whose MATE name, in any case, is in `upperNames`. */ +export function rowsNamed(materials, upperNames) { + return new Set(materials.flatMap((material, row) => (upperNames.has(material.name.toUpperCase()) ? [row] : []))); +} + // The file's materials to import, each as the record a new catalog stores, with -// the id it gets there: its sanitised name, numbered when two names sanitise alike. -function incomingMaterials(materials, fileName, onlyNames) { +// the id it gets there: its sanitised name, numbered when two names sanitise +// alike. `rows` picks records by their position in the file; null takes all. +// `zemaxName` is the name a COAT layer looks the record up by. +function incomingMaterials(materials, fileName, rows) { + const names = fileMaterialNames(materials); const usedIds = {}; const incoming = []; - for (const material of materials) { - if (onlyNames && !onlyNames.has(material.name.toUpperCase())) continue; - const record = mateToTfMaterial(material, { comment: `Imported from ${fileName}` }); + materials.forEach((material, row) => { + if (rows && !rows.has(row)) return; + const record = { ...mateToTfMaterial(material, { comment: `Imported from ${fileName}` }), name: names[row].name }; const id = freeId(candidate => usedIds[candidate], record.id || 'material'); usedIds[id] = true; incoming.push({ zemaxName: material.name.toUpperCase(), record: { ...record, id } }); - } + }); return incoming; } const nameKey = material => String(material?.name ?? '').trim().toUpperCase(); -// The id under which a catalog made from this file holds the file's material -// `zemaxName`: the material still named after it (the one at the file's own id -// first), else the one at the file's id, which an edit in the Material Editor -// may have renamed, unless that one is named after another material of the file. -function heldId(held, zemaxName, fileId, fileNames) { - const wanted = zemaxName.trim(); +// The id under which a catalog made from this file holds the record named +// `wanted` (upper-case): the material still named so (the one at the file's +// own id first), else the one at the file's id, which an edit in the Material +// Editor may have renamed, unless that one is named after another record of +// the file. +function heldId(held, wanted, fileId, fileNames) { if (held[fileId] && nameKey(held[fileId]) === wanted) return fileId; const byName = Object.keys(held).find(id => nameKey(held[id]) === wanted); if (byName) return byName; @@ -84,7 +129,7 @@ function mergeIntoExisting(existing, incoming, fileNames, replaceChanged) { const held = { ...existing.materials }; const nameMap = {}; const changed = []; - const targets = incoming.map(item => ({ ...item, id: heldId(existing.materials, item.zemaxName, item.record.id, fileNames) })); + const targets = incoming.map(item => ({ ...item, id: heldId(existing.materials, nameKey(item.record), item.record.id, fileNames) })); for (const { zemaxName, record, id: found } of targets) { let id = found; if (!id) { @@ -102,20 +147,23 @@ function mergeIntoExisting(existing, incoming, fileNames, replaceChanged) { /** * What an import from a coating file writes, without writing it. * - * `changed` names the materials the target catalog holds in a form that differs - * from the file's (an edit in the Material Editor, or a newer file): a coating - * import uses the held ones, a materials import replaces them only with - * `replaceChanged`. `fileRecords` maps each imported Zemax name to the record - * built from the file, whose index is the one Zemax used for the coating's - * relative thicknesses. + * `rows` is the set of the file's MATE records to import, by their position in + * `materials`, or null for all of them. `changed` names the materials the + * target catalog holds in a form that differs from the file's (an edit in the + * Material Editor, or a newer file): a coating import uses the held ones, a + * materials import replaces them only with `replaceChanged`. `nameMap` maps + * each imported Zemax name, upper-case, to its `catalogId:materialId`, and + * `fileRecords` to the record built from the file, whose index is the one + * Zemax used for the coating's relative thicknesses. For a name the file + * defines more than once, both hold the last of its records in `rows`. */ -export function buildMaterialRegistration(materials, fileName, onlyNames, filePath, { replaceChanged = false } = {}) { +export function buildMaterialRegistration(materials, fileName, rows, filePath, { replaceChanged = false } = {}) { const { id: catId, name: catName, existing } = catalogIdFor(fileName, filePath); - const incoming = incomingMaterials(materials, fileName, onlyNames); + const incoming = incomingMaterials(materials, fileName, rows); const fileRecords = Object.fromEntries(incoming.map(({ zemaxName, record }) => [zemaxName, record])); if (existing) { - const fileNames = new Set(materials.map(material => material.name.trim().toUpperCase())); + const fileNames = new Set(fileMaterialNames(materials).map(({ name }) => name.toUpperCase())); const merged = mergeIntoExisting(existing, incoming, fileNames, replaceChanged); return { ...merged, catId, catName, fileRecords, count: incoming.length }; } @@ -130,8 +178,8 @@ export function buildMaterialRegistration(materials, fileName, onlyNames, filePa } /** Write what buildMaterialRegistration describes; the catalog keeps its id and stamp. */ -export function registerMaterials(materials, fileName, onlyNames, filePath, options) { - const registration = buildMaterialRegistration(materials, fileName, onlyNames, filePath, options); +export function registerMaterials(materials, fileName, rows, filePath, options) { + const registration = buildMaterialRegistration(materials, fileName, rows, filePath, options); const registered = addCatalog(registration.cat); return { ...registration, catName: registered.name }; } diff --git a/src/components/windows/dataExchange/zemaxCoatings/coatingLayers.js b/src/components/windows/dataExchange/zemaxCoatings/coatingLayers.js new file mode 100644 index 00000000..e868fe77 --- /dev/null +++ b/src/components/windows/dataExchange/zemaxCoatings/coatingLayers.js @@ -0,0 +1,102 @@ +/** + * The selected COAT of a loaded COATING.DAT as TFStudio layers, for the import + * to the front coating and for Save to Coating Library. + */ + +import { getNKById } from '../../../../utils/materials/catalogManager.js'; +import { makeGetNK } from '../../../../utils/materials/catalogManager/dispersion.js'; +import { isBuiltinId, savedMaterialRecord } from '../../../../utils/materials/designMaterials.js'; +import { coatToTfLayers } from '../../../../utils/io/zemaxCoatingFile.js'; +import { buildMaterialRegistration, registerMaterials, rowsNamed } from './catalogImport.js'; + +/** + * Convert the selected COAT to the layers of a front coating: incident side + * first, as the file lists them, physical thickness in nm, each material under + * the id the import gives it in the catalog of the file. + * + * The layers use the materials the catalog holds, edits included; a relative + * thickness is converted with the index the file gives, as Zemax converts it. + * A layer naming a material the file defines more than once takes the last + * record of that name. With `write` the materials the layers name are + * registered in the catalog, as an import does. Without it nothing is written: + * a file imported before keeps the ids of its catalog, and one never imported + * gets ids in the catalog a first import would make, whose random part (see + * catalogIdFor) is drawn anew each time. + * + * @returns {{ coating, layers, warnings, registration } | { error: string }} + */ +export function convertSelectedCoating({ z, doc, selCoating, fileName, filePath, refNm }, { write }) { + const coating = doc?.coatings?.[selCoating]; + if (!coating || coating.type !== 'layers') return { error: z.importNotStack }; + + const neededNames = new Set(coating.layers.map((layer) => layer.material.toUpperCase())); + const register = write ? registerMaterials : buildMaterialRegistration; + const registration = register(doc.materials, fileName, rowsNamed(doc.materials, neededNames), filePath); + const { nameMap, fileRecords } = registration; + const resolveId = (zemaxName) => nameMap[zemaxName.toUpperCase()] || (/^AIR$/i.test(zemaxName) ? 'Air' : null); + const { layers, warnings } = coatToTfLayers(coating, { + refWavelengthUm: refNm / 1000, + materialId: resolveId, + realIndex: (zemaxName, wavelengthNm) => { + const record = fileRecords[zemaxName.toUpperCase()]; + if (record) return makeGetNK(record)(wavelengthNm)[0]; + const id = resolveId(zemaxName); + return id ? getNKById(id, wavelengthNm)[0] : 0; + }, + }); + if (!layers.length) return { error: warnings[0] || z.importNotStack }; + return { coating, layers, warnings, registration }; +} + +// The records behind the layers' ids in the file's catalog, as the import +// would hold them there. +function catalogRecords(layers, { catId, cat }) { + const prefix = `${catId}:`; + const records = {}; + for (const { material } of layers) { + if (material.startsWith(prefix)) records[material] = cat.materials[material.slice(prefix.length)]; + } + return records; +} + +// The definitions a save of `design` writes for these of its ids, the ones no +// built-in serves. +function designRecords(design, ids) { + const records = {}; + for (const id of ids) { + const record = id && !isBuiltinId(id) ? savedMaterialRecord(design, id) : null; + if (record) records[id] = record; + } + return records; +} + +/** + * The selected COAT as the coating the Save Coating dialog takes. + * + * A COAT record names no media, so the coating lies between the active + * design's incident medium and substrate. Every layer material takes its id as + * convertSelectedCoating gives it without writing, and carries the record the + * import would hold under that id, so the saved coating computes on a computer + * that never imported the file. Nothing is written to a catalog. + * + * @returns {{ coating: object, warnings: string[] } | { error: string }} + */ +export function libraryCoating(args) { + const converted = convertSelectedCoating(args, { write: false }); + if (converted.error) return converted; + const { z, design, fileName, refNm } = args; + const { coating, layers, warnings, registration } = converted; + const incidentMedium = design.incidentMedium; + const substrate = design.substrate?.material; + return { + warnings, + coating: { + name: coating.name, + source: z.librarySource(coating.name, fileName), + layers: layers.map(({ material, thickness }) => ({ material, thickness })), + materials: { ...catalogRecords(layers, registration), ...designRecords(design, [incidentMedium, substrate]) }, + incidentMedium, substrate, + referenceWavelength: refNm, + }, + }; +} diff --git a/src/components/windows/dataExchange/zemaxCoatings/sessionState.js b/src/components/windows/dataExchange/zemaxCoatings/sessionState.js index 48a99553..8756974c 100644 --- a/src/components/windows/dataExchange/zemaxCoatings/sessionState.js +++ b/src/components/windows/dataExchange/zemaxCoatings/sessionState.js @@ -2,16 +2,19 @@ import { createWindowSession } from '../../windowSession.js'; // The loaded COATING.DAT document and the browsing selections belong to the file // rather than to any design, so one slot serves every design and the document -// stays loaded until another file replaces it. +// stays loaded until another file replaces it. `selRows` holds positions in +// `doc.materials` rather than names, since a file can define one name twice. +// `panelWidth` is the import tabs' panel width once the divider has been dragged. export const zemaxCoatingsSession = createWindowSession({ doc: null, fileName: '', filePath: '', tab: 'coatings', selCoating: -1, - selMats: new Set(), + selRows: new Set(), thMode: 'absolute', scope: 'used', coatName: 'TFSTUDIO_DESIGN', preview: '', + panelWidth: null, }); diff --git a/src/components/windows/dataExchange/zemaxCoatings/ui.js b/src/components/windows/dataExchange/zemaxCoatings/ui.js index 9eeec7d2..acdc7c04 100644 --- a/src/components/windows/dataExchange/zemaxCoatings/ui.js +++ b/src/components/windows/dataExchange/zemaxCoatings/ui.js @@ -1,79 +1,2 @@ -const { createElement: h, useState } = React; - -export function TabBtn({ active, onClick, c, children }) { - return h('button', { - onClick, - style: { - padding: '7px 16px', fontSize: 12, cursor: 'pointer', outline: 'none', - border: 'none', borderBottom: `2px solid ${active ? c.accent : 'transparent'}`, - background: 'transparent', color: active ? c.accent : c.text, - fontWeight: active ? 600 : 400, - }, - }, children); -} - -export function Btn({ onClick, c, children, disabled, primary }) { - return h('button', { - onClick, disabled, - style: { - padding: '5px 12px', fontSize: 11.5, cursor: disabled ? 'not-allowed' : 'pointer', - border: `1px solid ${primary && !disabled ? c.accent : c.border}`, borderRadius: 4, - background: primary && !disabled ? c.accent + '22' : 'transparent', - color: disabled ? c.textDim : (primary ? c.accent : c.text), - fontWeight: primary ? 600 : 400, opacity: disabled ? 0.55 : 1, whiteSpace: 'nowrap', - }, - }, children); -} - -export function Seg({ active, onClick, c, position, children }) { - const radius = position === 'first' ? '4px 0 0 4px' : position === 'last' ? '0 4px 4px 0' : '0'; - return h('button', { - onClick, - style: { - padding: '4px 10px', fontSize: 11, cursor: 'pointer', outline: 'none', - border: `1px solid ${active ? c.accent : c.border}`, borderRadius: radius, - marginLeft: position === 'first' ? 0 : -1, - background: active ? c.accent + '33' : 'transparent', - color: active ? c.accent : c.text, fontWeight: active ? 600 : 400, - position: 'relative', zIndex: active ? 1 : 0, whiteSpace: 'nowrap', - }, - }, children); -} - -// `positive` is for a quantity with no bound but zero, such as a wavelength: -// zero or a negative entry reverts like unparseable text. -function commitNumber({ raw, min, max, positive, value, onChange, setRaw }) { - const parsed = parseFloat(raw); - if (!isNaN(parsed) && (!positive || parsed > 0)) { - const clamped = Math.min(Math.max(parsed, min ?? -Infinity), max ?? Infinity); - onChange(clamped); - setRaw(String(clamped)); - } else { - setRaw(String(value)); - } -} - -export function Num({ value, onChange, min, max, positive, step = 1, c, width = 72 }) { - const [raw, setRaw] = useState(String(value)); - React.useEffect(() => { setRaw(String(value)); }, [value]); - const commit = () => commitNumber({ raw, min, max, positive, value, onChange, setRaw }); - return h('input', { - type: 'number', value: raw, min, max, step, - onChange: (event) => setRaw(event.target.value), onBlur: commit, - onKeyDown: (event) => { if (event.key === 'Enter') event.currentTarget.blur(); }, - style: { - width, height: 24, background: c.bg, color: c.text, - border: `1px solid ${c.border}`, borderRadius: 3, fontSize: 11, padding: '0 5px', - outline: 'none', textAlign: 'right', - }, - }); -} - -export function Label({ children, c }) { - return h('span', { - style: { fontSize: 10, color: c.textDim, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.4px', whiteSpace: 'nowrap' }, - }, children); -} - export const th = (c) => ({ textAlign: 'left', padding: '4px 8px', fontSize: 10, color: c.textDim, textTransform: 'uppercase', letterSpacing: '0.4px', borderBottom: `1px solid ${c.border}`, position: 'sticky', top: 0, background: c.panel }); export const td = (c) => ({ padding: '3px 8px', fontSize: 11.5, color: c.text, borderBottom: `1px solid ${c.border}22` }); diff --git a/src/components/windows/dataExchange/zemaxCoatings/useExportActions.js b/src/components/windows/dataExchange/zemaxCoatings/useExportActions.js index 2421cad3..b2f16684 100644 --- a/src/components/windows/dataExchange/zemaxCoatings/useExportActions.js +++ b/src/components/windows/dataExchange/zemaxCoatings/useExportActions.js @@ -65,7 +65,7 @@ function generatePreview(args) { return; } setPreview(built.text); - flash('success', `${built.materials.length} MATE · ${built.coating.layers.length} layers`); + flash('success', z.generated(built.materials.length, built.coating.layers.length)); } async function savePreview({ z, flash, preview }) { diff --git a/src/components/windows/dataExchange/zemaxCoatings/useImportActions.js b/src/components/windows/dataExchange/zemaxCoatings/useImportActions.js index 6d6aab35..5cdaafc5 100644 --- a/src/components/windows/dataExchange/zemaxCoatings/useImportActions.js +++ b/src/components/windows/dataExchange/zemaxCoatings/useImportActions.js @@ -1,14 +1,15 @@ -import { getNKById } from '../../../../utils/materials/catalogManager.js'; -import { makeGetNK } from '../../../../utils/materials/catalogManager/dispersion.js'; -import { coatToTfLayers, parseZemaxCoating } from '../../../../utils/io/zemaxCoatingFile.js'; +import { parseZemaxCoating } from '../../../../utils/io/zemaxCoatingFile.js'; import { applyImportedLayers } from './model.js'; import { buildMaterialRegistration, registerMaterials } from './catalogImport.js'; +import { convertSelectedCoating, libraryCoating } from './coatingLayers.js'; const { useCallback } = React; -async function loadCoatingFile({ z, flash, setLoading, setStatus, setDoc, setFileName, setFilePath, setSelCoating, setSelMats }) { +// A new file replaces the old one, with its first layer stack selected on the +// Coatings tab and no row checked on the Materials tab. +async function loadCoatingFile({ z, flash, clear, setLoading, setFile }) { setLoading(true); - setStatus(null); + clear(); try { const result = await window.electronAPI.zemaxPickCoatingFile(); if (!result?.success) { @@ -22,11 +23,13 @@ async function loadCoatingFile({ z, flash, setLoading, setStatus, setDoc, setFil setLoading(false); return; } - setDoc(parsed); - setFileName(result.fileName || 'COATING.DAT'); - setFilePath(result.filePath || ''); - setSelCoating(parsed.coatings.findIndex((coating) => coating.type === 'layers')); - setSelMats(new Set()); + setFile({ + doc: parsed, + fileName: result.fileName || 'COATING.DAT', + filePath: result.filePath || '', + selCoating: parsed.coatings.findIndex((coating) => coating.type === 'layers'), + selRows: new Set(), + }); flash('success', z.loadedFile(result.fileName || '')); } catch (error) { flash('error', z.errLoad(error.message)); @@ -34,52 +37,54 @@ async function loadCoatingFile({ z, flash, setLoading, setStatus, setDoc, setFil setLoading(false); } -function importSelectedCoating({ z, flash, doc, selCoating, fileName, filePath, refNm, checkpoint, updateDesign }) { - const coating = doc?.coatings?.[selCoating]; - if (!coating || coating.type !== 'layers') { - flash('error', z.importNotStack); +/** What the conversion noted, as a count to put after a report. */ +export const warningsSuffix = (z, warnings) => (warnings.length ? ` (${z.warningsN(warnings.length)})` : ''); + +// With no design selected the window has a placeholder that nothing keeps, so +// nothing is registered or written. +function importSelectedCoating(args) { + const { z, flash, checkpoint, updateDesign, hasActiveDesign } = args; + if (hasActiveDesign === false) { + flash('error', z.importNoDesign); return; } - - // The layers use the materials the catalog holds, edits included; a relative - // thickness is converted with the index the file gives, as Zemax converts it. - const neededNames = new Set(coating.layers.map((layer) => layer.material.toUpperCase())); - const { catName, nameMap, fileRecords } = registerMaterials(doc.materials, fileName, neededNames, filePath); - const resolveId = (zemaxName) => nameMap[zemaxName.toUpperCase()] || (/^AIR$/i.test(zemaxName) ? 'Air' : null); - const { layers, warnings } = coatToTfLayers(coating, { - refWavelengthUm: refNm / 1000, - materialId: resolveId, - realIndex: (zemaxName, wavelengthNm) => { - const record = fileRecords[zemaxName.toUpperCase()]; - if (record) return makeGetNK(record)(wavelengthNm)[0]; - const id = resolveId(zemaxName); - return id ? getNKById(id, wavelengthNm)[0] : 0; - }, - }); - if (!layers.length) { - flash('error', warnings[0] || z.importNotStack); + const converted = convertSelectedCoating(args, { write: true }); + if (converted.error) { + flash('error', converted.error); return; } - + const { coating, layers, warnings, registration } = converted; applyImportedLayers(layers, checkpoint, updateDesign); - flash('success', z.importedCoating(coating.name, layers.length) + (catName ? '' : '') + (warnings.length ? ` (${z.warningsN(warnings.length)})` : '')); + flash('success', z.importedCoating(coating.name, layers.length, registration.catName) + warningsSuffix(z, warnings)); +} + +// The dialog opens on the coating; what the conversion noted is reported once +// the coating is saved. +function openLibraryDialog(args) { + const built = libraryCoating(args); + if (built.error) { + args.flash('error', built.error); + return; + } + args.setLibraryCoating(built); } // A material the catalog holds in a form that differs from the file's, most // often after an edit in the Material Editor, is replaced only when the user -// says so; Cancel imports nothing. -function importSelectedMaterials({ z, flash, doc, selMats, fileName, filePath, setInputDialog }, all) { +// says so; Cancel imports nothing. The selection is a set of rows of the +// Materials tab, so each record of a repeated name is imported on its own. +function importSelectedMaterials({ z, flash, doc, selRows, fileName, filePath, setInputDialog }, all) { if (!doc?.materials?.length) return; - const onlyNames = all ? null : selMats; - if (!all && (!onlyNames || onlyNames.size === 0)) { + const rows = all ? null : selRows; + if (!all && (!rows || rows.size === 0)) { flash('error', z.noSelection); return; } const commit = () => { - const { catName, count } = registerMaterials(doc.materials, fileName, onlyNames, filePath, { replaceChanged: true }); + const { catName, count } = registerMaterials(doc.materials, fileName, rows, filePath, { replaceChanged: true }); flash('success', z.importedMaterials(count, catName)); }; - const { changed, catName } = buildMaterialRegistration(doc.materials, fileName, onlyNames, filePath); + const { changed, catName } = buildMaterialRegistration(doc.materials, fileName, rows, filePath); if (changed.length === 0) { commit(); return; } setInputDialog({ confirm: true, danger: true, @@ -96,11 +101,18 @@ export function useLoadAction(args) { } export function useCoatingImportAction(args) { - const { doc, selCoating, fileName, filePath, refNm, checkpoint, updateDesign, z } = args; - return useCallback(() => importSelectedCoating(args), [doc, selCoating, fileName, filePath, refNm, checkpoint, updateDesign, z]); + const { doc, selCoating, fileName, filePath, refNm, checkpoint, updateDesign, hasActiveDesign, z } = args; + return useCallback(() => importSelectedCoating(args), + [doc, selCoating, fileName, filePath, refNm, checkpoint, updateDesign, hasActiveDesign, z]); +} + +/** Open the Save Coating dialog on the selected COAT, or report why it cannot be saved. */ +export function useLibraryAction(args) { + const { doc, selCoating, fileName, filePath, refNm, design, z } = args; + return useCallback(() => openLibraryDialog(args), [doc, selCoating, fileName, filePath, refNm, design, z]); } export function useMaterialImportAction(args) { - const { doc, selMats, fileName, filePath, setInputDialog, z } = args; - return useCallback((all) => importSelectedMaterials(args, all), [doc, selMats, fileName, filePath, setInputDialog, z]); + const { doc, selRows, fileName, filePath, setInputDialog, z } = args; + return useCallback((all) => importSelectedMaterials(args, all), [doc, selRows, fileName, filePath, setInputDialog, z]); } diff --git a/src/components/windows/design/coatingLibrary/CoatingLibrary.js b/src/components/windows/design/coatingLibrary/CoatingLibrary.js index 5fed755e..411c385a 100644 --- a/src/components/windows/design/coatingLibrary/CoatingLibrary.js +++ b/src/components/windows/design/coatingLibrary/CoatingLibrary.js @@ -24,7 +24,8 @@ const barStyle = (c, edge) => ({ padding: '6px 10px', background: c.panel, [edge]: `1px solid ${c.border}`, }); -function FilterBar({ session, setField, entries, visible, substrates, onSave, onShare, c, ts }) { +// `saveOff` is why saving the current coating is off, or null while it is on. +function FilterBar({ session, setField, entries, visible, substrates, saveOff, onSave, onShare, c, ts }) { const label = text => h('span', { style: { fontSize: 11, color: c.textDim } }, text); return h('div', { style: barStyle(c, 'borderBottom') }, h(Segmented, { @@ -60,7 +61,10 @@ function FilterBar({ session, setField, entries, visible, substrates, onSave, on }), label(ts.count(visible.length, entries.length)), h('span', { style: { flex: 1 } }), - h('button', { onClick: onSave, title: ts.saveCurrentTip, style: buttonStyle(c) }, ts.saveCurrent), + h('button', { + onClick: onSave, disabled: !!saveOff, title: saveOff || ts.saveCurrentTip, + style: buttonStyle(c, { disabled: !!saveOff }), + }, ts.saveCurrent), h('button', { onClick: onShare, title: ts.share.buttonTip, style: buttonStyle(c) }, ts.share.button)); } @@ -102,8 +106,10 @@ function TagBar({ session, setField, tags, toggleTag, c, ts }) { items.map(({ tag, count }) => tagChip(tag, count, false, toggleTag, c)))))); } -function ApplyBar({ session, setField, selected, onApply, onDelete, message, c, ts }) { +function ApplyBar({ session, setField, selected, hasActiveDesign, onApply, onDelete, message, c, ts }) { const canApply = !!selected; + // With no design selected there is nothing to apply the coating to. + const noDesign = hasActiveDesign === false; return h('div', { style: barStyle(c, 'borderTop') }, h('span', { style: { fontSize: 11, color: c.textDim } }, ts.applyHeading), h(Segmented, { @@ -118,8 +124,8 @@ function ApplyBar({ session, setField, selected, onApply, onDelete, message, c, h('option', { value: 'replace' }, ts.modeReplace), h('option', { value: 'append' }, ts.modeAppend)), h('button', { - onClick: onApply, disabled: !canApply, - style: buttonStyle(c, { primary: true, disabled: !canApply }), + onClick: onApply, disabled: !canApply || noDesign, title: noDesign ? ts.applyNoDesign : undefined, + style: buttonStyle(c, { primary: true, disabled: !canApply || noDesign }), }, ts.apply), session.source === 'user' && h('button', { onClick: onDelete, disabled: !canApply, @@ -133,7 +139,7 @@ function ApplyBar({ session, setField, selected, onApply, onDelete, message, c, export function CoatingLibrary({ c, t }) { const ts = t.coatingLibrary; const { - design, session, setField, entries, visible, tags, substrates, toggleTag, toggleType, + design, hasActiveDesign, session, setField, entries, visible, tags, substrates, toggleTag, toggleType, selected, message, setMessage, apply, remove, } = useCoatingLibrary(ts); const [saving, setSaving] = useState(false); @@ -151,6 +157,9 @@ export function CoatingLibrary({ c, t }) { }, h(FilterBar, { session, setField, entries, visible, substrates, + // The current coating is a side of the open design; with none open + // there is only the placeholder, which nobody opened. + saveOff: hasActiveDesign === false ? t.windowChrome.noDesign : null, onSave: () => setSaving(true), onShare: () => setSharing(true), c, ts, }), h(TagBar, { session, setField, tags, toggleTag, c, ts }), @@ -165,7 +174,7 @@ export function CoatingLibrary({ c, t }) { selected ? h(EntryDetail, { entry: selected, c, ts, summaries: t.specification.summaries, lambdaAxis: t.spectralAxis.lambdaShort }) : h('div', { style: { padding: 24, fontSize: 12, color: c.textDim, fontStyle: 'italic' } }, ts.selectHint))), - h(ApplyBar, { session, setField, selected, onApply: apply, onDelete: remove, message, c, ts }), + h(ApplyBar, { session, setField, selected, hasActiveDesign, onApply: apply, onDelete: remove, message, c, ts }), saving && h(SaveCoatingDialog, { design, c, t, onClose: () => setSaving(false), diff --git a/src/components/windows/design/coatingLibrary/SaveCoatingDialog.js b/src/components/windows/design/coatingLibrary/SaveCoatingDialog.js index df21e6fb..ea73efcc 100644 --- a/src/components/windows/design/coatingLibrary/SaveCoatingDialog.js +++ b/src/components/windows/design/coatingLibrary/SaveCoatingDialog.js @@ -1,5 +1,7 @@ import { evalParamsSession } from '../../../../state/evalParamsSession.js'; -import { COATING_TYPES, POLARIZATIONS, entryFromDesign } from '../../../../utils/coatingLibrary/entryModel.js'; +import { + COATING_TYPES, POLARIZATIONS, entryFromCoating, entryFromDesign, +} from '../../../../utils/coatingLibrary/entryModel.js'; import { validateEntry } from '../../../../utils/coatingLibrary/validateEntry.js'; import { saveUserCoating } from '../../../../utils/coatingLibrary/userCoatings.js'; import { FONT, Segmented, buttonStyle, failReason, inputStyle } from './ui.js'; @@ -12,36 +14,64 @@ function Row({ label, c, children }) { children); } +// What the fields open with. The band and angle are the coating's own when it +// states them; otherwise the band is the one Optical Evaluation was last set +// to, read once, and the angle is 0°. The fields are the user's from then on. +function initialFields(design, side, coating) { + const evalParams = evalParamsSession.peek(null); + const band = coating?.band || [evalParams?.lambdaStart ?? 400, evalParams?.lambdaEnd ?? 700]; + return { + name: coating ? coating.name : `${design.name} ${side}`, + bandStart: String(band[0]), bandEnd: String(band[1]), + aoi: String(coating?.aoi ?? 0), + }; +} + +const sideLayers = (design, side) => (side === 'back' ? design.backLayers : design.frontLayers) || []; + +// How many layers are saved: the coating's, or those of the chosen side of the +// design, with the picker for the side. +function StackRow({ coating, design, side, setSide, c, ts }) { + const sd = ts.saveDialog; + const count = (coating ? coating.layers : sideLayers(design, side)).length; + return h(Row, { label: sd.side, c }, + !coating && h(Segmented, { + value: side, onChange: setSide, c, + options: [['front', sd.front], ['back', sd.back]], + }), + h('span', { style: { color: c.textDim } }, ts.layersShort(count))); +} + /** - * Save one side of the active design into My coatings. Opened from the Design - * Editor's Tools menu and from the Coating Library window. + * Save a coating into My coatings. Opened from the Design Editor's Tools menu + * and the Coating Library window on one side of the active design (`design`, + * `side`), and from a coating import window on a coating read from a file + * (`coating`, the shape entryFromCoating takes, plus optional `band` [from, to] + * in nm and `aoi` in degrees that the fields open with). */ -export function SaveCoatingDialog({ design, side: initialSide = 'front', c, t, onClose, onSaved }) { +export function SaveCoatingDialog({ design, side: initialSide = 'front', coating = null, c, t, onClose, onSaved }) { const ts = t.coatingLibrary; const sd = ts.saveDialog; - // The band Optical Evaluation was last set to, as the band this coating is - // offered over. Read once: it seeds the fields below, which the user owns. - const evalParams = useMemo(() => evalParamsSession.peek(null), []); + const initial = useMemo(() => initialFields(design, initialSide, coating), []); const [side, setSide] = useState(initialSide); - const [name, setName] = useState(`${design.name} ${initialSide}`); + const [name, setName] = useState(initial.name); const [type, setType] = useState('other'); const [use, setUse] = useState(''); - const [bandStart, setBandStart] = useState(String(evalParams?.lambdaStart ?? 400)); - const [bandEnd, setBandEnd] = useState(String(evalParams?.lambdaEnd ?? 700)); - const [aoi, setAoi] = useState('0'); + const [bandStart, setBandStart] = useState(initial.bandStart); + const [bandEnd, setBandEnd] = useState(initial.bandEnd); + const [aoi, setAoi] = useState(initial.aoi); const [polarization, setPolarization] = useState('avg'); const [problems, setProblems] = useState([]); const [busy, setBusy] = useState(false); - const layerCount = (side === 'back' ? design.backLayers : design.frontLayers)?.length || 0; - async function save() { if (!name.trim()) { setProblems([sd.nameRequired]); return; } - if (layerCount === 0) { setProblems([sd.emptyStack]); return; } - const entry = entryFromDesign(design, side, { + if (!coating && sideLayers(design, side).length === 0) { setProblems([sd.emptyStack]); return; } + const meta = { name: name.trim(), type, use: use.trim(), band: [Number(bandStart), Number(bandEnd)], aoi: Number(aoi), polarization, - }); + }; + const entry = coating ? entryFromCoating(coating, meta) : entryFromDesign(design, side, meta); const found = validateEntry(entry); if (found.length > 0) { setProblems(found); return; } setBusy(true); @@ -70,10 +100,7 @@ export function SaveCoatingDialog({ design, side: initialSide = 'front', c, t, o }, }, h('h2', { style: { margin: '0 0 14px', fontSize: 17 } }, sd.title), - h(Row, { label: sd.side, c }, h(Segmented, { - value: side, onChange: setSide, c, - options: [['front', sd.front], ['back', sd.back]], - }), h('span', { style: { color: c.textDim } }, ts.layersShort(layerCount))), + h(StackRow, { coating, design, side, setSide, c, ts }), h(Row, { label: sd.name, c }, h('input', { value: name, onChange: event => setName(event.target.value), style: inputStyle(c, 260), autoFocus: true, })), diff --git a/src/components/windows/design/coatingLibrary/useCoatingLibrary.js b/src/components/windows/design/coatingLibrary/useCoatingLibrary.js index f06b23f3..b7469a54 100644 --- a/src/components/windows/design/coatingLibrary/useCoatingLibrary.js +++ b/src/components/windows/design/coatingLibrary/useCoatingLibrary.js @@ -12,9 +12,27 @@ const { useCallback, useEffect, useMemo, useState } = React; const numberOrNull = text => (text === '' || text == null ? null : Number(text)); +// Put the selected coating on one side of the design as one undo step, and say +// what was done. With no design selected the window has a placeholder that +// nothing keeps, so there is nothing to put the coating on. +function applyEntry({ selected, side, mode, design, hasActiveDesign, checkpoint, updateDesign, setMessage, ts }) { + if (!selected) return; + if (hasActiveDesign === false) { + setMessage(ts.applyNoDesign); + return; + } + checkpoint(); + const { patch, clashes } = applyCoatingPatch(design, selected, { side, mode }); + updateDesign(patch); + const sideLabel = side === 'back' ? ts.sideBack : ts.sideFront; + const lines = [ts.applied(selected.layers.length, sideLabel.toLowerCase())]; + if (clashes.length > 0) lines.push(ts.clashes(clashes.join(', '))); + setMessage(lines.join(' ')); +} + /** State and actions of the Coating Library window. */ export function useCoatingLibrary(ts) { - const { design, updateDesign, checkpoint } = useDesign(); + const { design, updateDesign, checkpoint, hasActiveDesign } = useDesign(); const [session, setField] = useWindowSession(coatingLibrarySession, null); const [userEntries, setUserEntries] = useState([]); const [message, setMessage] = useState(''); @@ -45,17 +63,10 @@ export function useCoatingLibrary(ts) { const toggleType = useCallback(type => setField('openTypes', current => (current.includes(type) ? current.filter(item => item !== type) : [...current, type])), [setField]); - const apply = useCallback(() => { - if (!selected) return; - const side = session.applySide; - checkpoint(); - const { patch, clashes } = applyCoatingPatch(design, selected, { side, mode: session.applyMode }); - updateDesign(patch); - const sideLabel = side === 'back' ? ts.sideBack : ts.sideFront; - const lines = [ts.applied(selected.layers.length, sideLabel.toLowerCase())]; - if (clashes.length > 0) lines.push(ts.clashes(clashes.join(', '))); - setMessage(lines.join(' ')); - }, [selected, session.applySide, session.applyMode, design, checkpoint, updateDesign, ts]); + const apply = useCallback(() => applyEntry({ + selected, side: session.applySide, mode: session.applyMode, + design, hasActiveDesign, checkpoint, updateDesign, setMessage, ts, + }), [selected, session.applySide, session.applyMode, design, hasActiveDesign, checkpoint, updateDesign, ts]); const remove = useCallback(async () => { if (!selected || session.source !== 'user') return; @@ -68,7 +79,7 @@ export function useCoatingLibrary(ts) { }, [selected, session.source, setField, ts]); return { - design, session, setField, entries, visible, tags, substrates, toggleTag, toggleType, + design, hasActiveDesign, session, setField, entries, visible, tags, substrates, toggleTag, toggleType, selected, message, setMessage, apply, remove, }; } diff --git a/src/components/windows/design/materialEditor/useMaterialEditor.js b/src/components/windows/design/materialEditor/useMaterialEditor.js index 53252d1b..bb2477e9 100644 --- a/src/components/windows/design/materialEditor/useMaterialEditor.js +++ b/src/components/windows/design/materialEditor/useMaterialEditor.js @@ -79,7 +79,7 @@ export function useMaterialEditor({ c, t, setInputDialog }) { const [fileImport, setFileImport] = useState(null); const me = t.materialEditor; - const { design, designs } = useDesign(); + const { design, designs, hasActiveDesign } = useDesign(); // The wavelengths the design is evaluated over, from whichever copy of // Optical Evaluation was changed last. A fit has to be right where the // coating is used, so this is the band the fit panel offers, and it follows @@ -113,9 +113,11 @@ export function useMaterialEditor({ c, t, setInputDialog }) { // `catalogs` stays the registry list every catalog action works against; // `browseCatalogs` is the merged list used only where materials are listed. // Rebuilt on a catalog change too: an entry a catalog holds is that - // catalog's material, which a save here replaces. + // catalog's material, which a save here replaces. With no design open there + // is none: the placeholder shown then is nobody's design. const designCatalog = useMemo( - () => buildDesignCatalog(design, me.designCatalog), [design, me.designCatalog, catalogRevision]); + () => (hasActiveDesign === false ? null : buildDesignCatalog(design, me.designCatalog)), + [design, hasActiveDesign, me.designCatalog, catalogRevision]); // Listed first: what the open design is made of is the most likely reason to // be in this window. const browseCatalogs = designCatalog ? [designCatalog, ...catalogs] : catalogs; diff --git a/src/components/windows/information/report/ReportWindow.js b/src/components/windows/information/report/ReportWindow.js index c5b6210a..a4b8cd4b 100644 --- a/src/components/windows/information/report/ReportWindow.js +++ b/src/components/windows/information/report/ReportWindow.js @@ -12,7 +12,7 @@ * window with a running header and footer. */ -import { AnalysisWindow, ControlRow } from '../../analysis/chrome/layout.js'; +import { AnalysisWindow, CenteredMessage, ControlRow } from '../../analysis/chrome/layout.js'; import { Divider, FieldLabel, ChoiceGroup, SelectField } from '../../analysis/chrome/controls.js'; import { availableLocales } from '../../../../constants/locales/index.js'; import { PAPERS } from '../../../../utils/report/template.js'; @@ -38,6 +38,8 @@ export function ReportWindow({ c, t }) { const selectedIndex = r.blocks.findIndex(b => b.id === selectedId); const selected = selectedIndex >= 0 ? r.blocks[selectedIndex] : null; const onCount = r.blocks.filter(b => b.on).length; + // None only while no design is open and none is picked. + const hasDesigns = r.chosen.length > 0; return h(AnalysisWindow, { c }, h(ControlRow, { @@ -70,7 +72,9 @@ export function ReportWindow({ c, t }) { onToggle: r.toggleBlock, onReorder: r.reorderBlock, onToggleAdd: () => { setAddOpen(open => !open); setSelectedId(null); }, }), - h(Preview, { c, W, html: r.html, error: r.error, paper: r.paper }), + hasDesigns + ? h(Preview, { c, W, html: r.html, error: r.error, paper: r.paper }) + : h(CenteredMessage, { c, message: t.windowChrome.noDesign }), selected && h(BlockSettingsPanel, { c, W, t, block: selected, index: selectedIndex, count: r.blocks.length, design: r.design, onChange: r.setBlockSettings, onMove: r.moveBlock, onRemove: r.removeBlock, @@ -79,7 +83,7 @@ export function ReportWindow({ c, t }) { addOpen && h(AddBlockMenu, { c, W, design: r.design, onAdd: r.addBlock, onClose: () => setAddOpen(false) }), ), h(ExportBar, { - c, W, summary: W.status(onCount, r.chosen.length), status: r.status, enabled: !!r.html && !r.error, + c, W, summary: W.status(onCount, r.chosen.length), status: r.status, enabled: hasDesigns && !!r.html && !r.error, onPdf: r.exportPdf, onHtml: r.exportHtml, onCopy: r.copyTables, }), ); diff --git a/src/components/windows/information/report/useReportWindow.js b/src/components/windows/information/report/useReportWindow.js index 701308fe..fe1cef33 100644 --- a/src/components/windows/information/report/useReportWindow.js +++ b/src/components/windows/information/report/useReportWindow.js @@ -188,10 +188,12 @@ function useDocument({ chosen, blocks, external, lang, tr, branding, doc, paper, () => attempt(() => chosen.map(d => ({ design: d, data: gatherDesignData(d, blocks, external) }))), [chosen, blocks, external, catalogRevision]); const items = gathered.value || []; - const composed = useMemo(() => (gathered.error - ? { value: '', error: gathered.error } - : attempt(() => composeReport({ lang, tr, brand: branding, doc, paper, blocks, designs: items, meta }))), - [gathered.error, items, lang, tr, branding, doc, paper, blocks, meta]); + // No design to cover gives no page, rather than a cover over nothing. + const composed = useMemo(() => { + if (gathered.error) return { value: '', error: gathered.error }; + if (!items.length) return { value: '', error: null }; + return attempt(() => composeReport({ lang, tr, brand: branding, doc, paper, blocks, designs: items, meta })); + }, [gathered.error, items, lang, tr, branding, doc, paper, blocks, meta]); return { items, html: composed.value || '', error: composed.error }; } @@ -203,7 +205,10 @@ function documentLanguage(stored) { export function useReportWindow({ t }) { const W = t.report.window; const designCtx = useDesign(); - const { design, preview } = useLiveDesign(); + const { design: liveDesign, preview } = useLiveDesign(); + // With no design open the provider hands out a placeholder nobody opened, + // so the current design is none and the report covers only picked designs. + const design = designCtx.hasActiveDesign ? liveDesign : null; const [state, setField, patch] = useWindowSession(reportSession, null); const branding = useBranding(); const version = useAppVersion(); diff --git a/src/components/windows/simulation/bbmWizard/BBMWizard.js b/src/components/windows/simulation/bbmWizard/BBMWizard.js index bdcbe850..a91a7b97 100644 --- a/src/components/windows/simulation/bbmWizard/BBMWizard.js +++ b/src/components/windows/simulation/bbmWizard/BBMWizard.js @@ -118,7 +118,9 @@ function presampleMaterialsFor({ design, materialIds, resolveMat, p }) { export function BBMWizard({ c, t, onClose }) { const B = t.bbmSim; - const { design } = useDesign(); + const { design: openDesign, hasActiveDesign } = useDesign(); + // With no design open the provider hands out a placeholder nobody opened. + const design = hasActiveDesign ? openDesign : null; const [step, setStep] = useState(1); const [run, setRun] = useState(null); // captured single-experiment trajectory diff --git a/src/components/windows/simulation/monoWizard/MonoWizard.js b/src/components/windows/simulation/monoWizard/MonoWizard.js index 5180412a..a7fe664d 100644 --- a/src/components/windows/simulation/monoWizard/MonoWizard.js +++ b/src/components/windows/simulation/monoWizard/MonoWizard.js @@ -163,7 +163,9 @@ function buildWizardBody({ step, p, set, materialIds, layers, c, B, t, ctx, desi export function MonoWizard({ c, t, onClose }) { const B = t.monoSim; - const { design } = useDesign(); + const { design: openDesign, hasActiveDesign } = useDesign(); + // With no design open the provider hands out a placeholder nobody opened. + const design = hasActiveDesign ? openDesign : null; const [step, setStep] = useState(1); const [run, setRun] = useState(null); diff --git a/src/constants/locales/en.js b/src/constants/locales/en.js index d8f7a861..c2b30c2e 100644 --- a/src/constants/locales/en.js +++ b/src/constants/locales/en.js @@ -94,6 +94,7 @@ export default { 'monitor-worksheet': 'Worksheet', 'process-sim': 'Process Export', 'zemax-coatings': 'Zemax', + 'codev-coatings': 'CODE V', 'spectrum-exchange': 'Spectra', 'measured-ellipsometry': 'Ψ/Δ', 'history': 'History', @@ -153,6 +154,7 @@ export default { 'monitor-worksheet': 'Monitor Worksheet: can each layer be terminated on a witness chip, and on what', 'process-sim': 'Process Exporter: scrub through deposition and export .res files for in-chamber monitoring', 'zemax-coatings': 'Zemax Coatings: import / export Zemax OpticStudio COATING.DAT (materials + coatings)', + 'codev-coatings': 'CODE V Coatings: import / export CODE V MUL coatings (.seq command file, .mul file)', 'spectrum-exchange': 'Measured Spectra: import measured R/T/A spectra (CSV / TXT / ASCII / JCAMP-DX) as overlays; export design or measured spectra to CSV / JCAMP-DX', 'measured-ellipsometry': 'Measured Ellipsometry: import a measured Ψ/Δ pair from a spectroscopic ellipsometer; export measured or calculated Ψ/Δ to CSV', 'history': 'Design History', @@ -735,6 +737,8 @@ export default { windowChrome: { help: 'Help for this window (F1)', close: 'Close', + // Drawn in place of a window that works on a design while none is open. + noDesign: 'No design selected. Open or create a design first.', }, windowError: { paneTitle: (window) => `${window} stopped with an error`, @@ -782,6 +786,7 @@ export default { 'monitor-worksheet': 'Monitor Worksheet', 'process-sim': 'Process Exporter', 'zemax-coatings': 'Zemax Coatings', + 'codev-coatings': 'CODE V Coatings', 'spectrum-exchange': 'Measured Spectra', 'measured-ellipsometry': 'Measured Ellipsometry', 'report-gen': 'Report', @@ -3652,6 +3657,7 @@ export default { sideFront: 'Front', sideBack: 'Back', modeReplace: 'Replace the stack', modeAppend: 'Add on top of the stack', apply: 'Apply', + applyNoDesign: 'No design selected. Open or create a design first.', applied: (n, side) => `${n} layers put on the ${side} coating. Undo restores the previous stack.`, clashes: (ids) => `Kept the design's own definition of ${ids}; the coating may compute differently than shown here.`, delete: 'Delete', @@ -4137,8 +4143,8 @@ export default { // ── Zemax Coatings ────────────────────────────────────────────────────────── zemaxCoatings: { - title: 'Zemax Coatings', - subtitle: 'Import / export Zemax OpticStudio COATING.DAT: material (MATE) tables and coating (COAT) layer stacks. Relative (wave) thickness converts as d = T·λ₀/n₀.', + fileTitle: 'COATING.DAT file', + fileHint: 'Reads the material (MATE) tables and coating (COAT) layer stacks of a Zemax OpticStudio COATING.DAT. A relative (wave) thickness converts as d = T·λ₀/n₀.', tabCoatings: 'Coatings', tabMaterials: 'Materials', tabExport: 'Export', @@ -4166,9 +4172,17 @@ export default { modeRel: 'wave', importToFront:'Import → front coating', importToBack: 'Import → back coating', + importNoDesign: 'No design selected. Open or create a design first.', importNotStack:'Only layer-stack (COAT) coatings import to a design. Ideal/Table/Encrypted are informational.', notImportable:'Not importable: no layer data (encrypted / ideal / table). Cannot be converted to a design.', - importedCoating:(name, n) => `Imported “${name}” → ${n} layer${n === 1 ? '' : 's'} (front). Materials added to catalog “${'Zemax'}”.`, + importedCoating:(name, n, cat) => `Imported “${name}” → ${n} layer${n === 1 ? '' : 's'} (front). Materials added to catalog “${cat}”.`, + saveToLibrary:'Save to Coating Library…', + saveToLibraryTip:'Save this coating into My coatings, between the incident medium and the substrate of the active design', + librarySource:(name, file) => `Zemax COAT “${name}” from ${file}`, + repeatedNames:(n) => `${n} material name${n === 1 ? ' is' : 's are'} defined more than once`, + repeatedNamesDetail:(names) => `${names}. Each record imports on its own, a later one with (2), (3) after its name. A coating layer that names one is built with the last record of that name.`, + repeatedRow: (index, count) => `${index} of ${count}`, + repeatedRowTip:(name) => `This name is defined more than once in the file. This record imports as ${name}.`, colPoints: 'Points', colRange: 'λ range (µm)', noMaterials: 'No materials in this file.', @@ -4180,14 +4194,13 @@ export default { importedMaterials:(n, cat) => `Imported ${n} material${n === 1 ? '' : 's'} into catalog “${cat}”.`, noSelection: 'No materials selected.', exportTitle: 'Generate COATING.DAT from the current design', + frontLayerCount:(n) => `${n} front-coating layer${n === 1 ? '' : 's'}`, thicknessMode:'Layer thickness', thicknessAbs: 'Absolute (µm)', thicknessRel: 'Relative (waves)', thicknessAbsHint:'Lossless: physical thickness in micrometres (is_absolute = 1).', thicknessRelHint:'Optical thickness in waves of λ₀ (Zemax-native, portable across wavelengths).', sampleGrid: 'Material sampling grid (nm)', - from: 'from', - to: 'to', step: 'step', coatingName: 'Coating name', materialScope:'Include materials', @@ -4195,6 +4208,7 @@ export default { scopeAll: 'All catalog materials', generate: 'Generate preview', preview: 'Preview', + generated: (materials, layers) => `${materials} MATE record${materials === 1 ? '' : 's'} and ${layers} layer${layers === 1 ? '' : 's'} written.`, saveBtn: 'Save COATING.DAT…', saving: 'Saving…', nothingToExport:'The current design has no front-coating layers to export.', @@ -4210,6 +4224,87 @@ export default { layersN: (n) => `${n}`, }, + // ── CODE V Coatings ───────────────────────────────────────────────────────── + codevCoatings: { + fileTitle: 'CODE V file', + fileHint: 'Reads a CODE V MULTILAYER (MUL) coating: the .seq command file that enters a stack, or the .mul file CODE V saves from it. Thicknesses are shown physical, in nm.', + tabImport: 'Import', + tabExport: 'Export', + openBtn: 'Open .seq or .mul…', + loading: 'Loading…', + noFile: 'No file loaded. Open a CODE V .seq command file or a .mul file saved by CODE V.', + loadedFile: (name, n) => `Read ${name}: ${n} layer${n === 1 ? '' : 's'}.`, + summaryIncident: 'Incident medium in the file', + summarySubstrate: 'Substrate in the file', + summaryWavelengths: 'Wavelengths', + wavelengthsValue: (n, min, max) => `${n}, ${min} to ${max} nm`, + summaryAngles: 'Angles of incidence', + summaryRef: 'Reference wavelength', + layersHeader: (n) => `${n} layer${n === 1 ? '' : 's'}, incident side first`, + colMaterial: 'Material', + colThickness: 'Thickness (nm)', + colLocked: 'Locked', + importToFront:'Import → front coating', + importToBack: 'Import → back coating', + importBackSymmetric: 'The back coating mirrors the front (Symmetric). Import to the front coating.', + importNoDesign: 'No design selected. Open or create a design first.', + saveToLibrary:'Save to Coating Library…', + saveToLibraryTip:'Save this coating into My coatings, between the incident medium and the substrate the file gives', + librarySource:(file) => `CODE V MUL coating from ${file}`, + importedFront: (n, cat) => `Imported ${n} layer${n === 1 ? '' : 's'} to the front coating. Materials are in catalog “${cat}”.`, + importedBack: (n, cat) => `Imported ${n} layer${n === 1 ? '' : 's'} to the back coating. Materials are in catalog “${cat}”.`, + warnUnknownCommand: (command, line) => `Line ${line}: ${command} is not a command TFStudio reads, so it was skipped.`, + warnExtraValues: (command, line, count, limit) => `Line ${line}: CODE V reads the first ${limit} values of one ${command} command and ignores the other ${count}, so they were left out here too.`, + warnExtraMicValues: (label, line, count, limit) => `Line ${line}: CODE V reads the first ${limit} values of MIC entry '${label}' and ignores the other ${count}, so they were left out here too.`, + warnDecimalComma: (line) => `Line ${line} is the first with a decimal comma. TFStudio reads a comma between two digits as a decimal point, but CODE V does not read such a number, so this file does not run in CODE V as it is.`, + warnResampled: (material, from, to) => `${material}: ${from} wavelengths cut to ${to} for its MIC entry, as CODE V takes at most 21. Points were added where CODE V's interpolation missed the material most, until it matched every wavelength to the 6 decimals written or 21 were kept.`, + warnIncidentAbsorbs: (material) => `${material} absorbs, but CODE V takes no k for the incident medium. Only its n is written.`, + warnSubstrateAbsorbs: (material) => `${material} absorbs, but CODE V takes no k for the substrate. Only its n is written.`, + warnRefOutsideTable: (label) => `REF is outside the MWL range of '${label}', so thicknesses in waves were converted with its n at REF extrapolated from the table, the way CODE V extrapolates it.`, + warnCoupledLayers: (count) => `${count} layer${count === 1 ? ' is' : 's are'} coupled to others in CODE V (code 1 to 99). TFStudio has no coupled layers, so ${count === 1 ? 'it comes' : 'they come'} in free.`, + warnOther: (kind) => `Note from the reader: ${kind}`, + errNoStack: 'No coating in this file: it has no MDA stack with a COA layer.', + errMissingCommand: (command) => `The file has no ${command}, which CODE V needs to define the stack.`, + errMicMismatch: (label, line) => `Line ${line}: MIC entry '${label}' does not have one value for each MWL wavelength.`, + errUnknownGroup: (label) => `COA calls group '${label}', which no GRO before it defines.`, + errUnknownMaterial: (label) => `'${label}' is not in the file's MIC catalog.`, + errBadNumber: (line, text) => `Line ${line}: "${text}" is not a number.`, + errNotMul: 'This is not a .mul file saved by CODE V.', + errParse: (kind) => `Could not read this file (${kind}).`, + errLoad: (err) => `Load failed: ${err}`, + exportTitle: 'Write a CODE V .seq file from the current design', + exportHint: (n) => `${n} layer${n === 1 ? '' : 's'} on this side, written from the incident medium to the substrate with physical thicknesses in nm. The file ends with SAV, so running it in CODE V writes the .mul.`, + side: 'Side', + sideFront: 'Front', + sideBack: 'Back', + titleField: 'Title (TIT)', + saveName: 'File name (SAV)', + refWavelength:'REF (nm)', + wavelengths: 'Analysis wavelengths, WL (nm)', + step: 'step', + wavelengthCount: (n) => `${n} wavelength${n === 1 ? '' : 's'}`, + angles: 'Angles of incidence, ANG (°)', + addAngle: 'Add an angle', + removeAngle: 'Remove this angle', + anglesFull: (limit) => `CODE V takes up to ${limit} angles`, + generate: 'Generate preview', + preview: 'Preview', + saveBtn: 'Save .seq…', + generated: (n, warnings) => `${n} layer${n === 1 ? '' : 's'} written${warnings ? `, ${warnings} warning${warnings === 1 ? '' : 's'}` : ''}.`, + nothingToExportFront: 'The front of the current design has no layers to export.', + nothingToExportBack: 'The back of the current design has no layers to export.', + exportBlocked: (ids) => `CODE V export is blocked until these materials are replaced: ${ids}`, + exportUnresolved: (id) => `Not exported: material ${id} has no n,k on this computer.`, + errTooManyLayers: (count, limit) => `${count} layers: CODE V takes up to ${limit}.`, + errTooManyWavelengths: (count, limit) => `${count} wavelengths: CODE V takes up to ${limit}. Use a larger step or a shorter range.`, + errNoWavelengths: 'No analysis wavelengths: CODE V needs at least one.', + errTooManyAngles: (count, limit) => `${count} angles: CODE V takes up to ${limit}.`, + errNoIndex: (material) => `${material} has no index at any of the analysis wavelengths.`, + errExport: (kind) => `Could not write the file (${kind}).`, + savedFile: (path) => `Saved ${path}`, + errSave: (err) => `Save failed: ${err}`, + }, + // ── Design import (TFCalc .tfd, Essential Macleod .dds) ──────────────────── designImport: { title: 'Import designs', diff --git a/src/constants/locales/it.js b/src/constants/locales/it.js index 36fabc4a..7247704a 100644 --- a/src/constants/locales/it.js +++ b/src/constants/locales/it.js @@ -94,6 +94,7 @@ export default { 'monitor-worksheet': 'Foglio', 'process-sim': 'Esporta processo', 'zemax-coatings': 'Zemax', + 'codev-coatings': 'CODE V', 'spectrum-exchange': 'Spettri', 'measured-ellipsometry': 'Ψ/Δ', history: 'Cronologia', @@ -153,6 +154,7 @@ export default { 'monitor-worksheet': 'Foglio di monitoraggio: se e su cosa ogni strato può essere terminato su un vetrino testimone', 'process-sim': 'Esportazione di processo: scorri la deposizione ed esporta file .res per il monitoraggio in camera', 'zemax-coatings': 'Rivestimenti Zemax: importa ed esporta COATING.DAT di Zemax OpticStudio (materiali e rivestimenti)', + 'codev-coatings': 'Rivestimenti CODE V: importa ed esporta rivestimenti CODE V MUL (file di comandi .seq, file .mul)', 'spectrum-exchange': 'Spettri misurati: importa spettri R/T/A misurati (CSV / TXT / ASCII / JCAMP-DX) come sovrapposizioni; esporta gli spettri del design o misurati in CSV / JCAMP-DX', 'measured-ellipsometry': 'Ellissometria misurata: importa una coppia Ψ/Δ misurata da un ellissometro spettroscopico; esporta in CSV i Ψ/Δ misurati o calcolati', history: 'Cronologia del design', @@ -826,6 +828,7 @@ export default { windowChrome: { help: 'Guida per questa finestra (F1)', close: 'Chiudi', + noDesign: 'Nessun design selezionato. Apri o crea prima un design.', }, windowError: { paneTitle: (window) => `La finestra «${window}» si è arrestata per un errore`, @@ -873,6 +876,7 @@ export default { 'monitor-worksheet': 'Foglio di monitoraggio', 'process-sim': 'Esporta processo', 'zemax-coatings': 'Rivestimenti Zemax', + 'codev-coatings': 'Rivestimenti CODE V', 'spectrum-exchange': 'Spettri misurati', 'measured-ellipsometry': 'Ellissometria misurata', 'report-gen': 'Report', @@ -4098,6 +4102,7 @@ export default { modeReplace: 'Sostituisci lo stack', modeAppend: 'Aggiungi sopra lo stack', apply: 'Applica', + applyNoDesign: 'Nessun design selezionato. Apri o crea prima un design.', applied: (n, side) => `${n} strati applicati al rivestimento ${side}. Annulla ripristina lo stack precedente.`, clashes: (ids) => `È stata mantenuta la definizione propria del design per ${ids}; il rivestimento può risultare diverso da quanto mostrato qui.`, delete: 'Elimina', @@ -4577,8 +4582,8 @@ export default { rateHint: 'Le velocità servono solo a scalare la linea del tempo. Predefinita 1 nm/s.', }, zemaxCoatings: { - title: 'Rivestimenti Zemax', - subtitle: 'Importa ed esporta COATING.DAT di Zemax OpticStudio: tabelle di materiali (MATE) e stack di strati dei rivestimenti (COAT). Lo spessore relativo (in onde) si converte come d = T·λ₀/n₀.', + fileTitle: 'File COATING.DAT', + fileHint: 'Legge le tabelle di materiali (MATE) e gli stack di strati dei rivestimenti (COAT) di un COATING.DAT di Zemax OpticStudio. Lo spessore relativo (in onde) si converte come d = T·λ₀/n₀.', tabCoatings: 'Rivestimenti', tabMaterials: 'Materiali', tabExport: 'Esporta', @@ -4606,9 +4611,17 @@ export default { modeRel: 'onde', importToFront: 'Importa → rivestimento anteriore', importToBack: 'Importa → rivestimento posteriore', + importNoDesign: 'Nessun design selezionato. Apri o crea prima un design.', importNotStack: 'Solo i rivestimenti a stack di strati (COAT) si importano in un design. Ideale, Tabella e Cifrato sono solo informativi.', notImportable: 'Non importabile: nessun dato sugli strati (cifrato / ideale / tabella). Non è convertibile in un design.', - importedCoating: (name, n) => `Importato “${name}” → ${n} strat${n === 1 ? 'o' : 'i'} (anteriore). Materiali aggiunti al catalogo “${'Zemax'}”.`, + importedCoating: (name, n, cat) => `Importato “${name}” → ${n} strat${n === 1 ? 'o' : 'i'} (anteriore). Materiali aggiunti al catalogo “${cat}”.`, + saveToLibrary: 'Salva nella libreria dei rivestimenti…', + saveToLibraryTip: 'Salva questo rivestimento in I miei rivestimenti, tra il mezzo incidente e il substrato del design attivo', + librarySource: (name, file) => `COAT Zemax “${name}” da ${file}`, + repeatedNames: (n) => `${n} nom${n === 1 ? 'e' : 'i'} di materiale definit${n === 1 ? 'o' : 'i'} più di una volta`, + repeatedNamesDetail: (names) => `${names}. Ogni voce si importa a sé, quelle successive con (2), (3) dopo il nome. Uno strato del rivestimento che nomina uno di essi usa l'ultima voce con quel nome.`, + repeatedRow: (index, count) => `${index} di ${count}`, + repeatedRowTip: (name) => `Questo nome è definito più di una volta nel file. Questa voce si importa come ${name}.`, colPoints: 'Punti', colRange: 'Intervallo λ (µm)', noMaterials: 'Nessun materiale in questo file.', @@ -4620,14 +4633,13 @@ export default { importedMaterials: (n, cat) => `Importati ${n} material${n === 1 ? 'e' : 'i'} nel catalogo “${cat}”.`, noSelection: 'Nessun materiale selezionato.', exportTitle: 'Genera COATING.DAT dal design corrente', + frontLayerCount: (n) => `${n} strat${n === 1 ? 'o' : 'i'} del rivestimento anteriore`, thicknessMode: 'Spessore degli strati', thicknessAbs: 'Assoluto (µm)', thicknessRel: 'Relativo (onde)', thicknessAbsHint: 'Senza perdite: spessore fisico in micrometri (is_absolute = 1).', thicknessRelHint: "Spessore ottico in onde di λ₀ (nativo di Zemax, trasferibile tra lunghezze d'onda).", sampleGrid: 'Griglia di campionamento dei materiali (nm)', - from: 'da', - to: 'a', step: 'passo', coatingName: 'Nome del rivestimento', materialScope: 'Materiali da includere', @@ -4635,6 +4647,7 @@ export default { scopeAll: 'Tutti i materiali del catalogo', generate: "Genera l'anteprima", preview: 'Anteprima', + generated: (materials, layers) => `Scritto: ${materials} voc${materials === 1 ? 'e' : 'i'} MATE, ${layers} strat${layers === 1 ? 'o' : 'i'}.`, saveBtn: 'Salva COATING.DAT…', saving: 'Salvataggio in corso…', nothingToExport: 'Il design corrente non ha strati del rivestimento anteriore da esportare.', @@ -4649,6 +4662,85 @@ export default { errParse: 'Non è stato possibile leggere questo file come un COATING.DAT di Zemax.', layersN: (n) => `${n}`, }, + codevCoatings: { + fileTitle: 'File CODE V', + fileHint: 'Legge un rivestimento CODE V MULTILAYER (MUL): il file di comandi .seq che inserisce uno stack, o il file .mul che CODE V ne salva. Gli spessori sono mostrati fisici, in nm.', + tabImport: 'Importa', + tabExport: 'Esporta', + openBtn: 'Apri .seq o .mul…', + loading: 'Caricamento in corso…', + noFile: 'Nessun file caricato. Apri un file di comandi CODE V .seq o un file .mul salvato da CODE V.', + loadedFile: (name, n) => `Letto ${name}: ${n} strat${n === 1 ? 'o' : 'i'}.`, + summaryIncident: 'Mezzo incidente nel file', + summarySubstrate: 'Substrato nel file', + summaryWavelengths: "Lunghezze d'onda", + wavelengthsValue: (n, min, max) => `${n}, da ${min} a ${max} nm`, + summaryAngles: 'Angoli di incidenza', + summaryRef: "Lunghezza d'onda di riferimento", + layersHeader: (n) => `${n} strat${n === 1 ? 'o' : 'i'}, dal lato incidente`, + colMaterial: 'Materiale', + colThickness: 'Spessore (nm)', + colLocked: 'Bloccato', + importToFront: 'Importa → rivestimento anteriore', + importToBack: 'Importa → rivestimento posteriore', + importBackSymmetric: "Il rivestimento posteriore rispecchia l'anteriore (Simmetrico). Importa nel rivestimento anteriore.", + importNoDesign: 'Nessun design selezionato. Apri o crea prima un design.', + saveToLibrary: 'Salva nella libreria dei rivestimenti…', + saveToLibraryTip: 'Salva questo rivestimento in I miei rivestimenti, tra il mezzo incidente e il substrato del file', + librarySource: (file) => `Rivestimento CODE V MUL da ${file}`, + importedFront: (n, cat) => `${n} strat${n === 1 ? 'o importato' : 'i importati'} nel rivestimento anteriore. I materiali sono nel catalogo “${cat}”.`, + importedBack: (n, cat) => `${n} strat${n === 1 ? 'o importato' : 'i importati'} nel rivestimento posteriore. I materiali sono nel catalogo “${cat}”.`, + warnUnknownCommand: (command, line) => `Riga ${line}: ${command} non è un comando che TFStudio legge, quindi è stato saltato.`, + warnExtraValues: (command, line, count, limit) => `Riga ${line}: CODE V legge i primi ${limit} valori di un comando ${command} e ignora gli altri ${count}, quindi sono stati esclusi anche qui.`, + warnExtraMicValues: (label, line, count, limit) => `Riga ${line}: CODE V legge i primi ${limit} valori della voce MIC '${label}' e ignora gli altri ${count}, quindi sono stati esclusi anche qui.`, + warnDecimalComma: (line) => `La riga ${line} è la prima con una virgola decimale. TFStudio legge una virgola tra due cifre come punto decimale, ma CODE V non legge un numero così, quindi questo file, così com'è, non si esegue in CODE V.`, + warnResampled: (material, from, to) => `${material}: ${from} lunghezze d'onda ridotte a ${to} nella voce MIC (CODE V ne accetta al massimo 21). I punti sono stati aggiunti dove l'interpolazione di CODE V si scostava di più dal materiale, finché non coincideva con tutte le lunghezze d'onda alla sesta cifra decimale o i punti erano 21.`, + warnIncidentAbsorbs: (material) => `${material} assorbe, ma CODE V non accetta k per il mezzo incidente. Viene scritto solo n.`, + warnSubstrateAbsorbs: (material) => `${material} assorbe, ma CODE V non accetta k per il substrato. Viene scritto solo n.`, + warnRefOutsideTable: (label) => `REF è fuori dall'intervallo MWL di '${label}', quindi gli spessori in onde sono stati convertiti con il suo n a REF estrapolato dalla tabella, come lo estrapola CODE V.`, + warnCoupledLayers: (count) => `${count} strat${count === 1 ? 'o è accoppiato' : 'i sono accoppiati'} ad altri in CODE V (codice da 1 a 99). TFStudio non ha strati accoppiati, quindi ${count === 1 ? 'entra libero' : 'entrano liberi'}.`, + warnOther: (kind) => `Nota dalla lettura del file: ${kind}`, + errNoStack: 'Nessun rivestimento in questo file: non ha uno stack MDA con uno strato COA.', + errMissingCommand: (command) => `Il file non ha il comando ${command}, che serve a CODE V per definire lo stack.`, + errMicMismatch: (label, line) => `Riga ${line}: la voce MIC '${label}' non ha un valore per ogni lunghezza d'onda MWL.`, + errUnknownGroup: (label) => `COA richiama il gruppo '${label}', che nessun GRO precedente definisce.`, + errUnknownMaterial: (label) => `'${label}' non è nel catalogo MIC del file.`, + errBadNumber: (line, text) => `Riga ${line}: “${text}” non è un numero.`, + errNotMul: 'Questo non è un file .mul salvato da CODE V.', + errParse: (kind) => `Non è stato possibile leggere questo file (${kind}).`, + errLoad: (err) => `Caricamento non riuscito: ${err}`, + exportTitle: 'Scrivi un file CODE V .seq dal design corrente', + exportHint: (n) => `${n} strat${n === 1 ? 'o' : 'i'} su questo lato, scritti dal mezzo incidente al substrato con spessori fisici in nm. Il file termina con SAV, quindi eseguendolo in CODE V si scrive il .mul.`, + side: 'Lato', + sideFront: 'Anteriore', + sideBack: 'Posteriore', + titleField: 'Titolo (TIT)', + saveName: 'Nome del file (SAV)', + refWavelength: 'REF (nm)', + wavelengths: "Lunghezze d'onda di analisi, WL (nm)", + step: 'passo', + wavelengthCount: (n) => `${n} lunghezz${n === 1 ? 'a' : 'e'} d'onda`, + angles: 'Angoli di incidenza, ANG (°)', + addAngle: 'Aggiungi un angolo', + removeAngle: 'Rimuovi questo angolo', + anglesFull: (limit) => `CODE V accetta fino a ${limit} angoli`, + generate: "Genera l'anteprima", + preview: 'Anteprima', + saveBtn: 'Salva .seq…', + generated: (n, warnings) => `${n} strat${n === 1 ? 'o scritto' : 'i scritti'}${warnings ? `, ${warnings} avvis${warnings === 1 ? 'o' : 'i'}` : ''}.`, + nothingToExportFront: 'Il lato anteriore del design corrente non ha strati da esportare.', + nothingToExportBack: 'Il lato posteriore del design corrente non ha strati da esportare.', + exportBlocked: (ids) => `L'esportazione per CODE V è bloccata finché questi materiali non vengono sostituiti: ${ids}`, + exportUnresolved: (id) => `Non esportato: il materiale ${id} non ha n,k su questo computer.`, + errTooManyLayers: (count, limit) => `${count} strati: CODE V ne accetta fino a ${limit}.`, + errTooManyWavelengths: (count, limit) => `${count} lunghezze d'onda: CODE V ne accetta fino a ${limit}. Usa un passo più grande o un intervallo più corto.`, + errNoWavelengths: "Nessuna lunghezza d'onda di analisi: CODE V ne vuole almeno una.", + errTooManyAngles: (count, limit) => `${count} angoli: CODE V ne accetta fino a ${limit}.`, + errNoIndex: (material) => `${material} non ha indice a nessuna delle lunghezze d'onda di analisi.`, + errExport: (kind) => `Non è stato possibile scrivere il file (${kind}).`, + savedFile: (path) => `Salvato ${path}`, + errSave: (err) => `Salvataggio non riuscito: ${err}`, + }, designImport: { title: 'Importa design', summary: (n, failed) => `${n} design${failed ? `, ${failed} file non leggibil${failed === 1 ? 'e' : 'i'}` : ''}`, diff --git a/src/constants/locales/ru.js b/src/constants/locales/ru.js index a0efa368..04cf95b9 100644 --- a/src/constants/locales/ru.js +++ b/src/constants/locales/ru.js @@ -93,6 +93,7 @@ export default { 'monitor-worksheet': 'Лист', 'process-sim': 'Экспорт процесса', 'zemax-coatings': 'Zemax', + 'codev-coatings': 'CODE V', 'spectrum-exchange': 'Спектры', 'measured-ellipsometry': 'Ψ/Δ', 'history': 'История', @@ -152,6 +153,7 @@ export default { 'monitor-worksheet': 'Лист мониторинга: можно ли остановить каждый слой на свидетеле и по какому сигналу', 'process-sim': 'Экспорт процесса: таймлайн напыления и экспорт .res-файлов для мониторинга в камере', 'zemax-coatings': 'Покрытия Zemax: импорт / экспорт файла Zemax OpticStudio COATING.DAT (материалы + покрытия)', + 'codev-coatings': 'Покрытия CODE V: импорт / экспорт покрытий CODE V MUL (командный файл .seq, файл .mul)', 'spectrum-exchange': 'Измеренные спектры: импорт измеренных R/T/A (CSV / TXT / ASCII / JCAMP-DX) как накладок; экспорт спектра проекта или измеренного в CSV / JCAMP-DX', 'measured-ellipsometry': 'Измеренная эллипсометрия: импорт пары Ψ/Δ со спектрального эллипсометра; экспорт измеренных или расчётных Ψ/Δ в CSV', 'history': 'История проекта', @@ -725,6 +727,7 @@ export default { windowChrome: { help: 'Справка по этому окну (F1)', close: 'Закрыть', + noDesign: 'Не выбран проект. Откройте или создайте проект.', }, windowError: { paneTitle: (window) => `Окно «${window}» остановлено из-за ошибки`, @@ -772,6 +775,7 @@ export default { 'monitor-worksheet': 'Лист мониторинга', 'process-sim': 'Экспорт процесса', 'zemax-coatings': 'Покрытия Zemax', + 'codev-coatings': 'Покрытия CODE V', 'spectrum-exchange': 'Измеренные спектры', 'measured-ellipsometry': 'Измеренная эллипсометрия', 'report-gen': 'Отчёт', @@ -1796,7 +1800,7 @@ export default { mfLabel: 'MF:', omfLabel: 'OMF:', omfTip: 'Оптическая MF — без ограничений по толщине (MNT/MXT/TT)', - noDesign: 'Нет выбранного проекта.', + noDesign: 'Не выбран проект. Откройте или создайте проект.', noOperands: 'Нет операндов. Используйте мастер выше или "+ Добавить".', evaluationError: 'Ошибка', residualSlack: 'Отклонение', @@ -3601,6 +3605,7 @@ export default { sideFront: 'Переднее', sideBack: 'Заднее', modeReplace: 'Заменить покрытие', modeAppend: 'Добавить поверх', apply: 'Применить', + applyNoDesign: 'Не выбран проект. Откройте или создайте проект.', applied: (n, side) => `Слоёв добавлено: ${n} (${side} покрытие). «Отменить» вернёт прежнюю структуру.`, clashes: (ids) => `Сохранено собственное определение материалов проекта: ${ids}. Покрытие может рассчитываться иначе, чем показано здесь.`, delete: 'Удалить', @@ -4083,8 +4088,8 @@ export default { // ── Покрытия Zemax ────────────────────────────────────────────────────────── zemaxCoatings: { - title: 'Покрытия Zemax', - subtitle: 'Импорт / экспорт файла Zemax OpticStudio COATING.DAT — таблицы материалов (MATE) и покрытия (COAT). Относительная толщина (в волнах) пересчитывается как d = T·λ₀/n₀.', + fileTitle: 'Файл COATING.DAT', + fileHint: 'Читает таблицы материалов (MATE) и покрытия (COAT) из файла Zemax OpticStudio COATING.DAT. Относительная толщина (в волнах) пересчитывается как d = T·λ₀/n₀.', tabCoatings: 'Покрытия', tabMaterials: 'Материалы', tabExport: 'Экспорт', @@ -4112,9 +4117,17 @@ export default { modeRel: 'волна', importToFront:'Импорт → переднее покрытие', importToBack: 'Импорт → заднее покрытие', - importNotStack:'В проект импортируются только покрытия (COAT). Идеальные/табличные/зашифрованные — справочные.', - notImportable:'Недоступно для импорта — нет данных о слоях (зашифровано / идеальное / таблица). Нельзя преобразовать в проект.', - importedCoating:(name, n) => `Импортировано «${name}» → ${n} сло${n === 1 ? 'й' : 'ёв'} (переднее). Материалы добавлены в каталог «${'Zemax'}».`, + importNoDesign: 'Не выбран проект. Откройте или создайте проект.', + importNotStack:'В проект импортируются только покрытия (COAT). Идеальные, табличные и зашифрованные показаны для справки.', + notImportable:'Недоступно для импорта: нет данных о слоях (зашифровано / идеальное / таблица). Нельзя преобразовать в проект.', + importedCoating:(name, n, cat) => `Импортировано «${name}» → ${n} сло${n === 1 ? 'й' : 'ёв'} (переднее). Материалы добавлены в каталог «${cat}».`, + saveToLibrary:'Сохранить в библиотеку покрытий…', + saveToLibraryTip:'Сохранить это покрытие в «Мои покрытия», между средой падения и подложкой активного проекта', + librarySource:(name, file) => `Покрытие Zemax COAT «${name}» из ${file}`, + repeatedNames:(n) => `Имена материалов, заданные в файле больше одного раза: ${n}`, + repeatedNamesDetail:(names) => `${names}. Каждая запись импортируется отдельно, следующие с (2), (3) после имени. Слой покрытия с таким именем строится по последней записи с этим именем.`, + repeatedRow: (index, count) => `${index} из ${count}`, + repeatedRowTip:(name) => `Это имя задано в файле больше одного раза. Эта запись импортируется как ${name}.`, colPoints: 'Точек', colRange: 'Диапазон λ (мкм)', noMaterials: 'В этом файле нет материалов.', @@ -4126,14 +4139,13 @@ export default { importedMaterials:(n, cat) => `Импортировано ${n} материал${n === 1 ? '' : 'ов'} в каталог «${cat}».`, noSelection: 'Материалы не выбраны.', exportTitle: 'Сгенерировать COATING.DAT из текущего проекта', + frontLayerCount:(n) => `Слоёв переднего покрытия: ${n}`, thicknessMode:'Толщина слоёв', thicknessAbs: 'Абсолютная (мкм)', thicknessRel: 'Относительная (волны)', - thicknessAbsHint:'Без потерь — физическая толщина в микрометрах (is_absolute = 1).', + thicknessAbsHint:'Без потерь: физическая толщина в микрометрах (is_absolute = 1).', thicknessRelHint:'Оптическая толщина в волнах λ₀ (родная для Zemax, переносима между длинами волн).', sampleGrid: 'Сетка дискретизации материалов (нм)', - from: 'от', - to: 'до', step: 'шаг', coatingName: 'Имя покрытия', materialScope:'Включить материалы', @@ -4141,6 +4153,7 @@ export default { scopeAll: 'Все материалы каталогов', generate: 'Сгенерировать предпросмотр', preview: 'Предпросмотр', + generated: (materials, layers) => `Записано записей MATE: ${materials}, слоёв: ${layers}.`, saveBtn: 'Сохранить COATING.DAT…', saving: 'Сохранение…', nothingToExport:'В текущем проекте нет слоёв переднего покрытия для экспорта.', @@ -4156,6 +4169,87 @@ export default { layersN: (n) => `${n}`, }, + // ── Покрытия CODE V ───────────────────────────────────────────────────────── + codevCoatings: { + fileTitle: 'Файл CODE V', + fileHint: 'Читает покрытие CODE V MULTILAYER (MUL): командный файл .seq, который задаёт покрытие, или файл .mul, который CODE V сохраняет из него. Толщины показаны физические, в нм.', + tabImport: 'Импорт', + tabExport: 'Экспорт', + openBtn: 'Открыть .seq или .mul…', + loading: 'Загрузка…', + noFile: 'Файл не загружен. Откройте командный файл CODE V .seq или файл .mul, сохранённый в CODE V.', + loadedFile: (name, n) => `Прочитан ${name}, слоёв: ${n}.`, + summaryIncident: 'Среда падения в файле', + summarySubstrate: 'Подложка в файле', + summaryWavelengths: 'Длины волн', + wavelengthsValue: (n, min, max) => `${n}, от ${min} до ${max} нм`, + summaryAngles: 'Углы падения', + summaryRef: 'Опорная длина волны', + layersHeader: (n) => `Слоёв: ${n}, первым слой со стороны среды`, + colMaterial: 'Материал', + colThickness: 'Толщина (нм)', + colLocked: 'Зафиксирован', + importToFront:'Импорт → переднее покрытие', + importToBack: 'Импорт → заднее покрытие', + importBackSymmetric: 'Заднее покрытие зеркально переднему (симметрично). Импортируйте в переднее покрытие.', + importNoDesign: 'Не выбран проект. Откройте или создайте проект.', + saveToLibrary:'Сохранить в библиотеку покрытий…', + saveToLibraryTip:'Сохранить это покрытие в «Мои покрытия», между средой падения и подложкой из файла', + librarySource:(file) => `Покрытие CODE V MUL из ${file}`, + importedFront: (n, cat) => `В переднее покрытие импортировано слоёв: ${n}. Материалы в каталоге «${cat}».`, + importedBack: (n, cat) => `В заднее покрытие импортировано слоёв: ${n}. Материалы в каталоге «${cat}».`, + warnUnknownCommand: (command, line) => `Строка ${line}: команду ${command} TFStudio не читает, она пропущена.`, + warnExtraValues: (command, line, count, limit) => `Строка ${line}: из одной команды ${command} CODE V читает первые ${limit} значений, а остальные (${count}) пропускает, поэтому здесь они тоже пропущены.`, + warnExtraMicValues: (label, line, count, limit) => `Строка ${line}: из записи MIC '${label}' CODE V читает первые ${limit} значений, а остальные (${count}) пропускает, поэтому здесь они тоже пропущены.`, + warnDecimalComma: (line) => `Строка ${line}: первая с десятичной запятой. TFStudio читает запятую между двумя цифрами как десятичную точку, а CODE V такое число не читает, поэтому в таком виде этот файл в CODE V не запустится.`, + warnResampled: (material, from, to) => `${material}: таблица MIC сокращена (было ${from}, стало ${to}; CODE V принимает не больше 21). Точки добавлялись там, где интерполяция CODE V дальше всего от материала, пока она не совпала со всеми длинами волн до 6-го знака или точек не стало 21.`, + warnIncidentAbsorbs: (material) => `${material} поглощает, но для среды падения CODE V не принимает k. Записан только n.`, + warnSubstrateAbsorbs: (material) => `${material} поглощает, но для подложки CODE V не принимает k. Записан только n.`, + warnRefOutsideTable: (label) => `REF лежит вне диапазона MWL таблицы '${label}', поэтому толщины в волнах пересчитаны по n в REF, экстраполированному по таблице так же, как это делает CODE V.`, + warnCoupledLayers: (count) => `Слоёв, связанных в CODE V с другими (код от 1 до 99): ${count}. В TFStudio связанных слоёв нет, поэтому они импортированы свободными.`, + warnOther: (kind) => `Замечание при чтении: ${kind}`, + errNoStack: 'В файле нет покрытия: в MDA нет ни одного слоя COA.', + errMissingCommand: (command) => `В файле нет команды ${command}, а без неё CODE V не задаёт покрытие.`, + errMicMismatch: (label, line) => `Строка ${line}: в записи MIC '${label}' число значений не совпадает с числом длин волн MWL.`, + errUnknownGroup: (label) => `COA вызывает группу '${label}', но ни одна команда GRO выше её не задаёт.`, + errUnknownMaterial: (label) => `'${label}' нет в каталоге MIC этого файла.`, + errBadNumber: (line, text) => `Строка ${line}: «${text}» не число.`, + errNotMul: 'Это не файл .mul, сохранённый в CODE V.', + errParse: (kind) => `Не удалось прочитать файл (${kind}).`, + errLoad: (err) => `Ошибка загрузки: ${err}`, + exportTitle: 'Записать файл CODE V .seq из текущего проекта', + exportHint: (n) => `Слоёв на этой стороне: ${n}. Они записываются от среды падения к подложке, толщины физические, в нм. Файл заканчивается командой SAV, поэтому при запуске в CODE V записывается .mul.`, + side: 'Сторона', + sideFront: 'Передняя', + sideBack: 'Задняя', + titleField: 'Название (TIT)', + saveName: 'Имя файла (SAV)', + refWavelength:'REF (нм)', + wavelengths: 'Длины волн расчёта, WL (нм)', + step: 'шаг', + wavelengthCount: (n) => `длин волн: ${n}`, + angles: 'Углы падения, ANG (°)', + addAngle: 'Добавить угол', + removeAngle: 'Убрать этот угол', + anglesFull: (limit) => `Больше углов CODE V не принимает (предел ${limit})`, + generate: 'Сгенерировать предпросмотр', + preview: 'Предпросмотр', + saveBtn: 'Сохранить .seq…', + generated: (n, warnings) => `Записано слоёв: ${n}${warnings ? `, предупреждений: ${warnings}` : ''}.`, + nothingToExportFront: 'У переднего покрытия текущего проекта нет слоёв для экспорта.', + nothingToExportBack: 'У заднего покрытия текущего проекта нет слоёв для экспорта.', + exportBlocked: (ids) => `Экспорт в CODE V заблокирован до замены материалов: ${ids}`, + exportUnresolved: (id) => `Не экспортировано: для материала ${id} на этом компьютере нет n,k.`, + errTooManyLayers: (count, limit) => `Слоёв ${count}, а CODE V принимает не больше ${limit}.`, + errTooManyWavelengths: (count, limit) => `Длин волн ${count}, а CODE V принимает не больше ${limit}. Увеличьте шаг или сузьте диапазон.`, + errNoWavelengths: 'Нет ни одной длины волны расчёта, а CODE V нужна хотя бы одна.', + errTooManyAngles: (count, limit) => `Углов ${count}, а CODE V принимает не больше ${limit}.`, + errNoIndex: (material) => `У материала ${material} нет показателя преломления ни на одной длине волны расчёта.`, + errExport: (kind) => `Не удалось записать файл (${kind}).`, + savedFile: (path) => `Сохранено: ${path}`, + errSave: (err) => `Ошибка сохранения: ${err}`, + }, + // ── Импорт проектов (TFCalc .tfd, Essential Macleod .dds) ────────────────── designImport: { title: 'Импорт проектов', diff --git a/src/constants/locales/zh.js b/src/constants/locales/zh.js index 3e681513..525e9321 100644 --- a/src/constants/locales/zh.js +++ b/src/constants/locales/zh.js @@ -93,6 +93,7 @@ export default { 'monitor-worksheet': '工作表', 'process-sim': '工艺导出', 'zemax-coatings': 'Zemax', + 'codev-coatings': 'CODE V', 'spectrum-exchange': '光谱', 'measured-ellipsometry': 'Ψ/Δ', history: '历史', @@ -152,6 +153,7 @@ export default { 'monitor-worksheet': '监控工作表:每层能否在监控片上停切,以及依据什么信号', 'process-sim': '工艺导出器:逐层回放沉积过程并导出 .res 文件用于腔内监控', 'zemax-coatings': 'Zemax 镀膜: 导入/导出 Zemax OpticStudio COATING.DAT(材料 + 镀膜)', + 'codev-coatings': 'CODE V 镀膜: 导入/导出 CODE V MUL 镀膜(.seq 命令文件、.mul 文件)', 'spectrum-exchange': '实测光谱: 导入实测 R/T/A 光谱(CSV / TXT / ASCII / JCAMP-DX)作为叠加;导出设计或实测光谱为 CSV / JCAMP-DX', 'measured-ellipsometry': '实测椭偏: 从椭偏仪导入一对 Ψ/Δ 曲线;导出实测或计算的 Ψ/Δ 为 CSV', history: '设计历史', @@ -823,6 +825,7 @@ export default { windowChrome: { help: '此窗口的帮助 (F1)', close: '关闭', + noDesign: '未选择设计。请先打开或创建设计。', }, windowError: { paneTitle: (window) => `${window} 因错误已停止`, @@ -870,6 +873,7 @@ export default { 'monitor-worksheet': '监控工作表', 'process-sim': '工艺导出器', 'zemax-coatings': 'Zemax 镀膜', + 'codev-coatings': 'CODE V 镀膜', 'spectrum-exchange': '实测光谱', 'measured-ellipsometry': '实测椭偏', 'report-gen': '报告', @@ -4052,6 +4056,7 @@ export default { sideFront: '前表面', sideBack: '后表面', modeReplace: '替换膜系', modeAppend: '叠加在膜系之上', apply: '应用', + applyNoDesign: '未选择设计。请先打开或创建设计。', applied: (n, side) => `已将 ${n} 层放到${side}镀膜上。撤销可恢复原膜系。`, clashes: (ids) => `保留了设计自身对 ${ids} 的定义;该镀膜的计算结果可能与此处显示的不同。`, delete: '删除', @@ -4518,8 +4523,8 @@ export default { rateHint: '速率仅用于缩放时间轴。默认 1 nm/s。', }, zemaxCoatings: { - title: 'Zemax 镀膜', - subtitle: '导入/导出 Zemax OpticStudio COATING.DAT — 材料 (MATE) 表和镀膜 (COAT) 膜层堆栈。相对(波)厚度按 d = T·λ₀/n₀ 换算。', + fileTitle: 'COATING.DAT 文件', + fileHint: '读取 Zemax OpticStudio COATING.DAT 中的材料 (MATE) 表和镀膜 (COAT) 膜层堆栈。相对(波)厚度按 d = T·λ₀/n₀ 换算。', tabCoatings: '镀膜', tabMaterials: '材料', tabExport: '导出', @@ -4547,9 +4552,17 @@ export default { modeRel: '波', importToFront: '导入 → 正面镀膜', importToBack: '导入 → 背面镀膜', + importNoDesign: '未选择设计。请先打开或创建设计。', importNotStack: '只有膜层堆栈 (COAT) 镀膜可导入设计。理想/表格/加密仅作参考。', - notImportable: '不可导入 — 无膜层数据(加密/理想/表格)。无法转换为设计。', - importedCoating: (name, n) => `已导入“${name}” → ${n} 层(正面)。材料已添加到目录“Zemax”。`, + notImportable: '不可导入:无膜层数据(加密/理想/表格)。无法转换为设计。', + importedCoating: (name, n, cat) => `已导入“${name}” → ${n} 层(正面)。材料已添加到目录“${cat}”。`, + saveToLibrary: '保存到镀膜库…', + saveToLibraryTip: '将此镀膜保存到“我的镀膜”,置于活动设计的入射介质与基底之间', + librarySource: (name, file) => `来自 ${file} 的 Zemax COAT“${name}”`, + repeatedNames: (n) => `${n} 个材料名称在文件中定义了不止一次`, + repeatedNamesDetail: (names) => `${names}。每个条目单独导入,后面的条目在名称后加 (2)、(3)。引用该名称的镀膜层使用同名的最后一个条目。`, + repeatedRow: (index, count) => `${index} / ${count}`, + repeatedRowTip: (name) => `此名称在文件中定义了不止一次。此条目导入为 ${name}。`, colPoints: '点数', colRange: 'λ 范围 (µm)', noMaterials: '此文件中无材料。', @@ -4561,14 +4574,13 @@ export default { importedMaterials: (n, cat) => `已将 ${n} 种材料导入目录“${cat}”。`, noSelection: '未选择材料。', exportTitle: '从当前设计生成 COATING.DAT', + frontLayerCount: (n) => `正面镀膜 ${n} 层`, thicknessMode: '膜层厚度', thicknessAbs: '绝对 (µm)', thicknessRel: '相对(波)', - thicknessAbsHint: '无损 — 以微米计的物理厚度 (is_absolute = 1)。', + thicknessAbsHint: '无损:以微米计的物理厚度 (is_absolute = 1)。', thicknessRelHint: '以 λ₀ 波数计的光学厚度(Zemax 原生,跨波长可移植)。', sampleGrid: '材料采样网格 (nm)', - from: '从', - to: '到', step: '步长', coatingName: '镀膜名称', materialScope: '包含材料', @@ -4576,6 +4588,7 @@ export default { scopeAll: '全部目录材料', generate: '生成预览', preview: '预览', + generated: (materials, layers) => `已写出 ${materials} 个 MATE 条目、${layers} 层。`, saveBtn: '保存 COATING.DAT…', saving: '保存中…', nothingToExport: '当前设计没有可导出的正面镀膜层。', @@ -4590,6 +4603,85 @@ export default { errParse: '无法将此文件作为 Zemax COATING.DAT 读取。', layersN: (n) => `${n}`, }, + codevCoatings: { + fileTitle: 'CODE V 文件', + fileHint: '读取 CODE V MULTILAYER (MUL) 镀膜:输入膜层堆栈的 .seq 命令文件,或 CODE V 由其保存的 .mul 文件。厚度显示为物理厚度,单位 nm。', + tabImport: '导入', + tabExport: '导出', + openBtn: '打开 .seq 或 .mul…', + loading: '载入中…', + noFile: '未载入文件。请打开 CODE V 的 .seq 命令文件或由 CODE V 保存的 .mul 文件。', + loadedFile: (name, n) => `已读取 ${name}:${n} 层。`, + summaryIncident: '文件中的入射介质', + summarySubstrate: '文件中的基底', + summaryWavelengths: '波长', + wavelengthsValue: (n, min, max) => `${n} 个,${min} 到 ${max} nm`, + summaryAngles: '入射角', + summaryRef: '参考波长', + layersHeader: (n) => `${n} 层,入射侧在前`, + colMaterial: '材料', + colThickness: '厚度 (nm)', + colLocked: '已锁定', + importToFront: '导入 → 正面镀膜', + importToBack: '导入 → 背面镀膜', + importBackSymmetric: '背面镀膜镜像正面(对称)。请导入到正面镀膜。', + importNoDesign: '未选择设计。请先打开或创建设计。', + saveToLibrary: '保存到镀膜库…', + saveToLibraryTip: '将此镀膜保存到“我的镀膜”,置于文件中的入射介质与基底之间', + librarySource: (file) => `来自 ${file} 的 CODE V MUL 镀膜`, + importedFront: (n, cat) => `已将 ${n} 层导入正面镀膜。材料位于目录“${cat}”。`, + importedBack: (n, cat) => `已将 ${n} 层导入背面镀膜。材料位于目录“${cat}”。`, + warnUnknownCommand: (command, line) => `第 ${line} 行:TFStudio 不读取命令 ${command},已跳过。`, + warnExtraValues: (command, line, count, limit) => `第 ${line} 行:一条 ${command} 命令 CODE V 只读取前 ${limit} 个值,其余 ${count} 个被忽略,因此这里也略去。`, + warnExtraMicValues: (label, line, count, limit) => `第 ${line} 行:CODE V 只读取 MIC 条目 '${label}' 的前 ${limit} 个值,其余 ${count} 个被忽略,因此这里也略去。`, + warnDecimalComma: (line) => `第 ${line} 行是第一个以逗号作小数点的行。TFStudio 把两个数字之间的逗号读作小数点,但 CODE V 不读取这样的数字,因此此文件原样无法在 CODE V 中运行。`, + warnResampled: (material, from, to) => `${material}:MIC 表从 ${from} 个波长减为 ${to} 个(CODE V 最多接受 21 个)。在 CODE V 的插值偏离材料最大处逐个加点,直到所有波长在写出的 6 位小数内一致,或已满 21 个。`, + warnIncidentAbsorbs: (material) => `${material} 有吸收,但 CODE V 的入射介质不接受 k。只写入 n。`, + warnSubstrateAbsorbs: (material) => `${material} 有吸收,但 CODE V 的基底不接受 k。只写入 n。`, + warnRefOutsideTable: (label) => `REF 超出 '${label}' 的 MWL 范围,因此以波数表示的厚度按该表外推到 REF 的 n 换算,外推方式与 CODE V 相同。`, + warnCoupledLayers: (count) => `${count} 层在 CODE V 中与其他层耦合(代码 1 到 99)。TFStudio 没有耦合层,因此这些层以自由层导入。`, + warnOther: (kind) => `读取时的提示:${kind}`, + errNoStack: '文件中没有镀膜:MDA 中没有 COA 层。', + errMissingCommand: (command) => `文件中没有 ${command} 命令,而 CODE V 需要它来定义膜层堆栈。`, + errMicMismatch: (label, line) => `第 ${line} 行:MIC 条目 '${label}' 的数值个数与 MWL 波长个数不一致。`, + errUnknownGroup: (label) => `COA 调用了组 '${label}',但前面没有 GRO 定义它。`, + errUnknownMaterial: (label) => `'${label}' 不在文件的 MIC 目录中。`, + errBadNumber: (line, text) => `第 ${line} 行:“${text}” 不是数字。`, + errNotMul: '这不是 CODE V 保存的 .mul 文件。', + errParse: (kind) => `无法读取此文件(${kind})。`, + errLoad: (err) => `载入失败:${err}`, + exportTitle: '从当前设计写出 CODE V .seq 文件', + exportHint: (n) => `此面共 ${n} 层,从入射介质到基底写出,厚度为物理厚度,单位 nm。文件以 SAV 结尾,在 CODE V 中运行即写出 .mul。`, + side: '面', + sideFront: '正面', + sideBack: '背面', + titleField: '标题 (TIT)', + saveName: '文件名 (SAV)', + refWavelength: 'REF (nm)', + wavelengths: '分析波长,WL (nm)', + step: '步长', + wavelengthCount: (n) => `${n} 个波长`, + angles: '入射角,ANG (°)', + addAngle: '添加角度', + removeAngle: '删除此角度', + anglesFull: (limit) => `CODE V 最多接受 ${limit} 个角度`, + generate: '生成预览', + preview: '预览', + saveBtn: '保存 .seq…', + generated: (n, warnings) => `已写出 ${n} 层${warnings ? `,${warnings} 条警告` : ''}。`, + nothingToExportFront: '当前设计的正面没有可导出的膜层。', + nothingToExportBack: '当前设计的背面没有可导出的膜层。', + exportBlocked: (ids) => `CODE V 导出被阻止,直到替换这些材料:${ids}`, + exportUnresolved: (id) => `未导出:本机上材料 ${id} 没有 n,k 数据。`, + errTooManyLayers: (count, limit) => `${count} 层:CODE V 最多接受 ${limit} 层。`, + errTooManyWavelengths: (count, limit) => `${count} 个波长:CODE V 最多接受 ${limit} 个。请加大步长或缩小范围。`, + errNoWavelengths: '没有分析波长:CODE V 至少需要一个。', + errTooManyAngles: (count, limit) => `${count} 个角度:CODE V 最多接受 ${limit} 个。`, + errNoIndex: (material) => `${material} 在所有分析波长上都没有折射率。`, + errExport: (kind) => `无法写出文件(${kind})。`, + savedFile: (path) => `已保存 ${path}`, + errSave: (err) => `保存失败:${err}`, + }, designImport: { title: '导入设计', summary: (n, failed) => `${n} 个设计${failed ? `,${failed} 个文件无法读取` : ''}`, diff --git a/src/main/ipc/codev.js b/src/main/ipc/codev.js new file mode 100644 index 00000000..588f99c1 --- /dev/null +++ b/src/main/ipc/codev.js @@ -0,0 +1,48 @@ +// IPC: CODE V MULTILAYER (MUL) coating import / export. +// codev:pick-coating-file: open dialog for a .seq command file or a .mul file +// saved by CODE V, return its text for the renderer +// to parse. +// codev:save-coating-file: save dialog, write generated .seq text (UTF-8). +// +// Parsing and generation live in the renderer (utils/io/codevCoatingFile.js); +// these handlers only do file I/O. CommonJS, Electron-free (deps via ctx). + +const { saveTextFile } = require('./saveTextFile'); + +function register(ipcMain, ctx) { + ipcMain.handle('codev:pick-coating-file', async () => handlePickCoatingFile(ctx)); + ipcMain.handle('codev:save-coating-file', async (event, text, suggestedName) => handleSaveCoatingFile(ctx, text, suggestedName)); +} + +async function handlePickCoatingFile(ctx) { + const { dialog, getMainWindow, path, log, readTextAuto } = ctx; + try { + const result = await dialog.showOpenDialog(getMainWindow(), { + title: 'Import CODE V Coating (.seq, .mul)', + filters: [ + { name: 'CODE V Coating', extensions: ['seq', 'mul', 'SEQ', 'MUL'] }, + { name: 'CODE V Sequence File', extensions: ['seq', 'SEQ'] }, + { name: 'CODE V MUL File', extensions: ['mul', 'MUL'] }, + { name: 'All Files', extensions: ['*'] }, + ], + properties: ['openFile'], + }); + if (result.canceled || result.filePaths.length === 0) return { success: false, canceled: true }; + const filePath = result.filePaths[0]; + const text = readTextAuto(filePath); + return { success: true, text, fileName: path.basename(filePath), filePath }; + } catch (err) { + log(`codev:pick-coating-file error: ${err.message}`); + return { success: false, error: err.message }; + } +} + +function handleSaveCoatingFile(ctx, text, suggestedName) { + return saveTextFile(ctx, 'codev:save-coating-file', text, { + title: 'Export CODE V Coating', + defaultPath: suggestedName || 'coating.seq', + filters: [{ name: 'CODE V Sequence File', extensions: ['seq'] }], + }); +} + +module.exports = { register }; diff --git a/src/main/ipc/index.js b/src/main/ipc/index.js index 3f110f66..42b0a20b 100644 --- a/src/main/ipc/index.js +++ b/src/main/ipc/index.js @@ -16,6 +16,7 @@ const report = require('./report'); const processFiles = require('./process'); const rii = require('./rii'); const zemax = require('./zemax'); +const codev = require('./codev'); const spectrum = require('./spectrum'); const paths = require('./paths'); const preferences = require('./preferences'); @@ -35,6 +36,7 @@ function registerAllIpc(ipcMain, ctx) { processFiles.register(ipcMain, ctx); rii.register(ipcMain, ctx); zemax.register(ipcMain, ctx); + codev.register(ipcMain, ctx); spectrum.register(ipcMain, ctx); paths.register(ipcMain, ctx); preferences.register(ipcMain, ctx); diff --git a/src/preload.js b/src/preload.js index 75f4d1fd..b7bdfc1a 100644 --- a/src/preload.js +++ b/src/preload.js @@ -96,6 +96,9 @@ contextBridge.exposeInMainWorld('electronAPI', { // Zemax COATING.DAT import/export zemaxPickCoatingFile: () => ipcRenderer.invoke('zemax:pick-coating-file'), zemaxSaveCoatingFile: (text, suggestedName) => ipcRenderer.invoke('zemax:save-coating-file', text, suggestedName), + // CODE V MUL coating import (.seq, .mul) and export (.seq) + codevPickCoatingFile: () => ipcRenderer.invoke('codev:pick-coating-file'), + codevSaveCoatingFile: (text, suggestedName) => ipcRenderer.invoke('codev:save-coating-file', text, suggestedName), // Measured-spectrum text import/export spectrumPickFile: () => ipcRenderer.invoke('spectrum:pick-file'), spectrumSaveFile: (text, suggestedName) => ipcRenderer.invoke('spectrum:save-file', text, suggestedName), diff --git a/src/state/DesignContext.js b/src/state/DesignContext.js index ca1ef719..f761588a 100644 --- a/src/state/DesignContext.js +++ b/src/state/DesignContext.js @@ -123,36 +123,155 @@ function useCatalogFollowing(stored, catalogRevision) { ), [stored, catalogRevision]); } -// ── Provider ─────────────────────────────────────────────────────────────────── -// -// Props: -// activeDesignId — which design is currently active (string) -// designs — { [id]: designObject } external map owned by App -// onDesignChange — (id, newDesign) => void called whenever active design mutates -// -// When activeDesignId changes, the provider switches to that design (creating -// a default one on first access). +// ── Provider state ───────────────────────────────────────────────────────────── -export function DesignProvider({ children, activeDesignId, designs, folders, onDesignChange, onCheckpoint, historyView, onJumpToHistory }) { - // Local fallback: if parent doesn't pass controlled props, manage state internally. - const [localDesigns, setLocalDesigns] = useState(() => { +// The provider's own design, for a parent that passes no designs. +function useLocalDesigns() { + const [designs, setDesigns] = useState(() => { const d = makeDefaultDesign(); return { [d.id]: d }; }); - const [localActiveId, setLocalActiveId] = useState(() => { - const d = Object.values(localDesigns)[0]; - return d.id; - }); + const [activeId] = useState(() => Object.keys(designs)[0]); + return { designs, setDesigns, activeId }; +} + +// Where the provider reads and writes designs, and which one is active: the +// parent's, when it owns them, with `activeDesignId` null while none is open; +// otherwise the provider's own. +function useDesignSource({ activeDesignId, designs, onDesignChange }) { + const local = useLocalDesigns(); + if (designs == null || onDesignChange == null) return { controlled: false, ...local }; + const setDesigns = (updater, opts) => { + const next = typeof updater === 'function' ? updater(designs) : updater; + Object.entries(next).forEach(([id, d]) => { + if (designs[id] !== d) onDesignChange(id, d, opts); + }); + }; + return { controlled: true, designs, setDesigns, activeId: activeDesignId ?? null }; +} - // Active-optimizer counter. Tool windows (Refinement / Needle / GE) call - // beginOptimization() on Run and endOptimization() on stop/finalize/unmount. - // Live-preview consumers (OpticalEvaluation) throttle their main-thread - // TMM + chart redraw while isOptimizing is true so worker progress - // messages don't saturate the UI thread. +// Active-optimizer counter. Tool windows (Refinement / Needle / GE) call +// beginOptimization() on Run and endOptimization() on stop/finalize/unmount. +// Live-preview consumers (OpticalEvaluation) throttle their main-thread +// TMM + chart redraw while isOptimizing is true so worker progress +// messages don't saturate the UI thread. +function useOptimizerCount() { const [optimizerActive, setOptimizerActive] = useState(0); const beginOptimization = useCallback(() => setOptimizerActive(c => c + 1), []); const endOptimization = useCallback(() => setOptimizerActive(c => Math.max(0, c - 1)), []); - const isOptimizing = optimizerActive > 0; + return { isOptimizing: optimizerActive > 0, beginOptimization, endOptimization }; +} + +// Per-design "user edit" revision counter. Bumped ONLY on non-transient +// writes (real user/tool edits), NOT on the transient live-preview stream a +// long-running optimizer emits. Synthesis windows snapshot this at run start +// and re-read the design if it changed, so a manual thickness edit between +// runs is picked up instead of optimizing a stale cached stack (M12). +function useEditRevisions(activeId) { + const seqRef = React.useRef({}); + const getDesignRevision = useCallback( + (id) => seqRef.current[id ?? activeId] || 0, [activeId]); + const countEdit = useCallback((opts) => { + if (opts && opts.transient) return; + seqRef.current[activeId] = (seqRef.current[activeId] || 0) + 1; + }, [activeId]); + return { getDesignRevision, countEdit }; +} + +// What a write stores: the updater applied to `current`, with missing or +// duplicate layer ids backfilled at this single chokepoint so every layer +// producer (imports, wizards) is covered, and in symmetric mode the back stack +// derived from the front. The backfill is a cheap no-op when ids are already +// present and unique (the common path). +function storedDesign(current, updater) { + const next = typeof updater === 'function' ? updater(current) : updater; + if (!next) return next; + const f = ensureLayerIds(next.frontLayers); + const b = ensureLayerIds(next.backLayers); + const withIds = f !== next.frontLayers || b !== next.backLayers ? { ...next, frontLayers: f, backLayers: b } : next; + return withSymmetricBack(current, withIds); +} + +// The undo checkpoint and the History window's jump, for the active design. +// The placeholder shown while no design is active has no history, so with no +// active design both do nothing. +function useHistoryActions({ hasTarget, activeId, onCheckpoint, onJumpToHistory }) { + // Push a single undo checkpoint for the active design (pre-run snapshot). + const checkpoint = useCallback(() => { + if (hasTarget && typeof onCheckpoint === 'function') onCheckpoint(activeId); + }, [hasTarget, onCheckpoint, activeId]); + const jumpToHistory = useCallback((index) => { + if (hasTarget && typeof onJumpToHistory === 'function') onJumpToHistory(index); + }, [hasTarget, onJumpToHistory]); + return { checkpoint, jumpToHistory }; +} + +// ── Layer operations (side = 'front' | 'back') ──────────────────────────────── + +// Apply `edit` to one side's layers. An edit that hands back the same array +// leaves the design as it was. +function editLayers(setDesign, side, edit) { + const key = side === 'back' ? 'backLayers' : 'frontLayers'; + setDesign(prev => { + const layers = edit(prev[key]); + return layers === prev[key] ? prev : { ...prev, [key]: layers }; + }); +} + +// `layers` with one layer swapped with its neighbour above or below, or the +// same array when there is none there. +function movedLayers(layers, layerId, direction) { + const idx = layers.findIndex(l => l.id === layerId); + const target = direction === 'up' ? idx - 1 : idx + 1; + if (idx < 0 || target < 0 || target >= layers.length) return layers; + const out = [...layers]; + [out[idx], out[target]] = [out[target], out[idx]]; + return out; +} + +// `layers` with a copy of one layer inserted below it, or the same array when +// no layer has that id. +function duplicatedLayers(layers, layerId) { + const idx = layers.findIndex(l => l.id === layerId); + if (idx < 0) return layers; + const out = [...layers]; + out.splice(idx + 1, 0, { ...layers[idx], id: newLayerId() }); + return out; +} + +function useLayerOperations(setDesign) { + const removeLayer = useCallback((side, layerId) => + editLayers(setDesign, side, layers => layers.filter(l => l.id !== layerId)), [setDesign]); + const updateLayer = useCallback((side, layerId, patch) => + editLayers(setDesign, side, layers => layers.map(l => (l.id === layerId ? { ...l, ...patch } : l))), [setDesign]); + const moveLayer = useCallback((side, layerId, direction) => + editLayers(setDesign, side, layers => movedLayers(layers, layerId, direction)), [setDesign]); + const duplicateLayer = useCallback((side, layerId) => + editLayers(setDesign, side, layers => duplicatedLayers(layers, layerId)), [setDesign]); + return { removeLayer, updateLayer, moveLayer, duplicateLayer }; +} + +// ── Provider ─────────────────────────────────────────────────────────────────── +// +// Props: +// activeDesignId: which design is currently active (string), or null while +// the explorer has none selected +// designs: { [id]: designObject } external map owned by App +// onDesignChange: (id, newDesign) => void, called whenever active design mutates +// +// When activeDesignId changes, the provider switches to that design (creating +// a default one on first access). +// +// A parent that passes `designs` and `onDesignChange` owns the designs whether +// or not one is active. With none active, windows get a placeholder design that +// nothing keeps: every write to it is ignored and `hasActiveDesign` is false, so +// an import or an edit has no design to land in until the user opens one. A +// provider missing either keeps one design of its own and makes it active. + +export function DesignProvider({ children, activeDesignId, designs, folders, onDesignChange, onCheckpoint, historyView, onJumpToHistory }) { + const { controlled, designs: _designs, setDesigns: _setDesigns, activeId: _activeId } = + useDesignSource({ activeDesignId, designs, onDesignChange }); + const { isOptimizing, beginOptimization, endOptimization } = useOptimizerCount(); // Whether open windows follow a run as it proceeds (see useLiveDesign). // Off, they hold the design as it was when the run started and update once @@ -160,19 +279,6 @@ export function DesignProvider({ children, activeDesignId, designs, folders, onD // every window, so the switch reads the same wherever it is shown. const [liveUpdate, setLiveUpdate] = useState(true); - const controlled = activeDesignId != null && designs != null && onDesignChange != null; - - const _designs = controlled ? designs : localDesigns; - const _activeId = controlled ? activeDesignId : localActiveId; - const _setDesigns = controlled - ? (updater, opts) => { - const next = typeof updater === 'function' ? updater(_designs) : updater; - Object.entries(next).forEach(([id, d]) => { - if (_designs[id] !== d) onDesignChange(id, d, opts); - }); - } - : setLocalDesigns; - // Stable fallback design when no item is selected yet const fallbackRef = React.useRef(null); if (!fallbackRef.current) fallbackRef.current = makeDefaultDesign(); @@ -192,14 +298,7 @@ export function DesignProvider({ children, activeDesignId, designs, folders, onD // It is therefore per-design and persists with the project. const evalMode = resolveEvalMode(design); - // Per-design "user edit" revision counter. Bumped ONLY on non-transient - // writes (real user/tool edits), NOT on the transient live-preview stream a - // long-running optimizer emits. Synthesis windows snapshot this at run start - // and re-read the design if it changed, so a manual thickness edit between - // runs is picked up instead of optimizing a stale cached stack (M12). - const userEditSeqRef = React.useRef({}); - const getDesignRevision = useCallback( - (id) => userEditSeqRef.current[id ?? _activeId] || 0, [_activeId]); + const { getDesignRevision, countEdit } = useEditRevisions(_activeId); const _setDesign = useCallback((updater, opts) => { // No active design (fresh install, nothing selected → the empty fallback @@ -207,24 +306,12 @@ export function DesignProvider({ children, activeDesignId, designs, folders, onD // of creating a stray `designs[null]` entry the explorer never shows. // The user creates/selects a design first (the explorer invites it). if (_activeId == null) return; - if (!opts || !opts.transient) { - userEditSeqRef.current[_activeId] = (userEditSeqRef.current[_activeId] || 0) + 1; - } - _setDesigns(prev => { - const current = prev[_activeId] ?? makeDefaultDesign('New Design', _activeId); - let next = typeof updater === 'function' ? updater(current) : updater; - // Backfill missing/duplicate layer ids at this single chokepoint so - // every layer producer (imports, wizards) is covered. Cheap no-op - // when ids are already present & unique (the common path). - if (next) { - const f = ensureLayerIds(next.frontLayers); - const b = ensureLayerIds(next.backLayers); - if (f !== next.frontLayers || b !== next.backLayers) next = { ...next, frontLayers: f, backLayers: b }; - next = withSymmetricBack(current, next); - } - return { ...prev, [_activeId]: next }; - }, opts); - }, [_activeId, _setDesigns]); + countEdit(opts); + _setDesigns(prev => ({ + ...prev, + [_activeId]: storedDesign(prev[_activeId] ?? makeDefaultDesign('New Design', _activeId), updater), + }), opts); + }, [_activeId, _setDesigns, countEdit]); // ── Design-level updates ────────────────────────────────────────────────── // @@ -237,61 +324,13 @@ export function DesignProvider({ children, activeDesignId, designs, folders, onD _setDesign(prev => ({ ...prev, ...patch }), opts), [_setDesign]); - // Push a single undo checkpoint for the active design (pre-run snapshot). - const checkpoint = useCallback(() => { - if (controlled && typeof onCheckpoint === 'function') onCheckpoint(_activeId); - }, [controlled, onCheckpoint, _activeId]); - + const { checkpoint, jumpToHistory } = useHistoryActions({ + hasTarget: controlled && _activeId != null, activeId: _activeId, onCheckpoint, onJumpToHistory, + }); // Undo/redo timeline for the active design + jump-to-state (History window). const history = historyView || { entries: [], currentIndex: -1 }; - const jumpToHistory = useCallback((index) => { - if (controlled && typeof onJumpToHistory === 'function') onJumpToHistory(index); - }, [controlled, onJumpToHistory]); - // ── Layer operations (side = 'front' | 'back') ──────────────────────────── - - const _layersKey = (side) => side === 'back' ? 'backLayers' : 'frontLayers'; - - const removeLayer = useCallback((side, layerId) => { - const key = _layersKey(side); - _setDesign(prev => ({ - ...prev, - [key]: prev[key].filter(l => l.id !== layerId) - })); - }, [_setDesign]); - - const updateLayer = useCallback((side, layerId, patch) => { - const key = _layersKey(side); - _setDesign(prev => ({ - ...prev, - [key]: prev[key].map(l => l.id === layerId ? { ...l, ...patch } : l) - })); - }, [_setDesign]); - - const moveLayer = useCallback((side, layerId, direction) => { - const key = _layersKey(side); - _setDesign(prev => { - const layers = [...prev[key]]; - const idx = layers.findIndex(l => l.id === layerId); - if (idx < 0) return prev; - const target = direction === 'up' ? idx - 1 : idx + 1; - if (target < 0 || target >= layers.length) return prev; - [layers[idx], layers[target]] = [layers[target], layers[idx]]; - return { ...prev, [key]: layers }; - }); - }, [_setDesign]); - - const duplicateLayer = useCallback((side, layerId) => { - const key = _layersKey(side); - _setDesign(prev => { - const idx = prev[key].findIndex(l => l.id === layerId); - if (idx < 0) return prev; - const copy = { ...prev[key][idx], id: newLayerId() }; - const layers = [...prev[key]]; - layers.splice(idx + 1, 0, copy); - return { ...prev, [key]: layers }; - }); - }, [_setDesign]); + const { removeLayer, updateLayer, moveLayer, duplicateLayer } = useLayerOperations(_setDesign); return React.createElement(DesignContext.Provider, { value: { diff --git a/src/utils/coatingLibrary/entryModel.js b/src/utils/coatingLibrary/entryModel.js index b899dc02..df50daa2 100644 --- a/src/utils/coatingLibrary/entryModel.js +++ b/src/utils/coatingLibrary/entryModel.js @@ -410,3 +410,32 @@ export function entryFromDesign(design, side, meta = {}) { created: meta.created || new Date().toISOString(), }); } + +/** + * An entry built from a coating that is not a design side, such as one read + * from another program's file. The coating is taken as the front of a design + * of its own and goes through entryFromDesign, so its layers are stored in + * deposition order and its materials embedded the same way. + * + * @param {object} coating + * name + * layers [{ material, thickness }], incident side first, thickness in nm + * materials { id: record } for the ids no built-in serves; a catalog + * here that holds an id comes first, as in a design + * incidentMedium, substrate material ids + * referenceWavelength nm, optional + * source where the coating came from, shown under Source + * @param {object} meta as entryFromDesign takes it; its `source` wins over the coating's + */ +export function entryFromCoating(coating, meta = {}) { + const design = { + name: coating.name, + incidentMedium: coating.incidentMedium, + substrate: { material: coating.substrate }, + referenceWavelength: coating.referenceWavelength, + frontLayers: coating.layers, + backLayers: [], + ...(coating.materials ? { materials: coating.materials } : {}), + }; + return entryFromDesign(design, 'front', { ...meta, source: meta.source || coating.source }); +} diff --git a/src/utils/io/codevCoating/format.js b/src/utils/io/codevCoating/format.js new file mode 100644 index 00000000..4f0d023b --- /dev/null +++ b/src/utils/io/codevCoating/format.js @@ -0,0 +1,26 @@ +/** Format a number with at most `decimals` decimals, trailing zeros trimmed. */ +export function fmtNum(x, decimals) { + if (!Number.isFinite(x)) return '0'; + const s = x.toFixed(decimals).replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, ''); + return s === '-0' ? '0' : s; +} + +/** + * Write a command and its values, `perLine` values to a line. A line that + * continues ends with `&`, the CODE V continuation mark. + */ +export function wrapValues(head, values, perLine = 8) { + if (values.length <= perLine) return [`${head} ${values.join(' ')}`]; + const lines = []; + for (let i = 0; i < values.length; i += perLine) { + const chunk = values.slice(i, i + perLine).join(' '); + const last = i + perLine >= values.length; + lines.push(`${i === 0 ? head : ' '.repeat(head.length)} ${chunk}${last ? '' : ' &'}`); + } + return lines; +} + +/** Quote a CODE V string: single quotes, none inside. */ +export function quote(text) { + return `'${String(text).replace(/'/g, '')}'`; +} diff --git a/src/utils/io/codevCoating/labels.js b/src/utils/io/codevCoating/labels.js new file mode 100644 index 00000000..166ccb85 --- /dev/null +++ b/src/utils/io/codevCoating/labels.js @@ -0,0 +1,23 @@ +/** A Multilayer Index Catalog label holds at most 6 characters. */ +export const LABEL_LENGTH = 6; + +/** + * Hand out labels of up to 6 letters and digits made from material names, each + * one once. A part of the name in brackets is left out ("SiO2 (Fused Silica)" + * is SiO2). Labels are compared without case. A clash keeps the start of the + * name and ends in a number: SiO2, SiO22, SiO23. + */ +export function makeLabeler() { + const used = new Set(); + return (name) => { + const plain = String(name || '').replace(/\([^)]*\)/g, ''); + const base = plain.replace(/[^A-Za-z0-9]/g, '').slice(0, LABEL_LENGTH) || 'M'; + let label = base; + for (let k = 2; used.has(label.toUpperCase()); k++) { + const suffix = String(k); + label = base.slice(0, LABEL_LENGTH - suffix.length) + suffix; + } + used.add(label.toUpperCase()); + return label; + }; +} diff --git a/src/utils/io/codevCoating/mdaCommands.js b/src/utils/io/codevCoating/mdaCommands.js new file mode 100644 index 00000000..8dca26c3 --- /dev/null +++ b/src/utils/io/codevCoating/mdaCommands.js @@ -0,0 +1,100 @@ +import { CodevParseError } from './parseError.js'; +import { findLabel, keptValues, numberOf, numbersOf } from './seqValues.js'; + +/** The stack as MDA builds it; thicknesses stay in the units they were entered in. */ +export function newStack() { + return { + title: '', physical: false, wavelengths: [], ref: null, angles: null, + incident: { n: 1, k: 0 }, substrate: null, layers: [], + groups: new Map(), openGroup: null, + micOpen: false, mwl: null, micEntries: new Map(), + }; +} + +// INC, SUB and the index of a COA layer: a quoted MIC label, or n with an +// optional k where the command takes one. +function indexOf(tokens, line, withK) { + const [first, second] = tokens; + if (first?.quoted) return { label: first.text }; + return { n: numberOf(first, line), k: withK && second ? numberOf(second, line) : 0 }; +} + +// A layer goes onto the stack and, while a GRO is open, into that group too. +function addLayer(stack, layer) { + stack.layers.push(layer); + if (stack.openGroup !== null) stack.groups.get(stack.openGroup).push(layer); +} + +function replayGroup(stack, label) { + const key = findLabel(stack.groups, label); + if (key === undefined) throw new CodevParseError('unknownGroup', { label }); + for (const layer of [...stack.groups.get(key)]) addLayer(stack, { ...layer }); +} + +// COA 'g' enters the layers of group g again; COA thickness code index enters one layer. +function coa(stack, args, line) { + if (args.length === 1 && args[0].quoted) return replayGroup(stack, args[0].text); + addLayer(stack, { + thickness: numberOf(args[0], line), + code: numberOf(args[1], line), + index: indexOf(args.slice(2), line, true), + line, + }); +} + +// WL λ...: the first 21 values of one WL command, as CODE V 11.2 reads it. +function wl(stack, args, line, warnings) { + stack.wavelengths.push(...keptValues(args, 'WL', line, warnings)); +} + +// WLG min max step: equally spaced wavelengths, both ends included, built as +// CODE V builds them: min and step as float32 in µm, the step added point +// after point in float32. The .mul files CODE V saved from WLG store exactly +// that running sum, to the 10 digits written: the DWDM sample's (WLG 1547 +// 1557 .2), whose last point sits 1.7e-3 nm above 1557 nm, and one CODE V +// 11.2 saved from WLG 400 800 10, whose last sits 3.5e-4 nm below 800 nm. +// With them the DWDM sample matches the R and T CODE V 11.2 prints on the +// steep edge of its passband to 8.8e-5; with min + i·step only to 2.1e-3. +function wlg(stack, args, line) { + const [min, max, step] = numbersOf(args.slice(0, 3), line); + if (!(step > 0)) throw new CodevParseError('badNumber', { line, text: args[2]?.text ?? '' }); + const count = Math.floor((max - min) / step + 1e-9) + 1; + const stepUm = Math.fround(step / 1000); + let um = Math.fround(min / 1000); + for (let i = 0; i < count; i++) { + stack.wavelengths.push(um * 1000); + um = Math.fround(um + stepUm); + } +} + +function pht(stack, args) { + const answer = args[0]?.text.charAt(0).toUpperCase(); + if (answer === 'Y' || answer === 'N') stack.physical = answer === 'Y'; +} + +function gro(stack, args) { + const label = args[0]?.text ?? ''; + stack.groups.set(label, []); + stack.openGroup = label; +} + +/** + * The MDA commands that define the stack, from the MDA sub-option page of the + * CODE V Multilayer Design Reference Manual. Each takes the stack, the tokens + * after the command, the line number and the warnings list. END here closes a + * GRO; inside MIC it closes the catalog (micCommands.js). + */ +export const MDA_COMMANDS = { + PHT: pht, + TIT: (stack, args) => { stack.title = args.map(token => token.text).join(' '); }, + WL: wl, + WLG: wlg, + REF: (stack, args, line) => { stack.ref = numberOf(args[0], line); }, + ANG: (stack, args, line) => { stack.angles = numbersOf(args, line); }, + INC: (stack, args, line) => { stack.incident = indexOf(args, line, false); }, + SUB: (stack, args, line) => { stack.substrate = indexOf(args, line, false); }, + COA: coa, + GRO: gro, + END: (stack) => { stack.openGroup = null; }, + MIC: (stack) => { stack.micOpen = true; stack.mwl = null; }, +}; diff --git a/src/utils/io/codevCoating/micCommands.js b/src/utils/io/codevCoating/micCommands.js new file mode 100644 index 00000000..fa37ba18 --- /dev/null +++ b/src/utils/io/codevCoating/micCommands.js @@ -0,0 +1,46 @@ +import { CodevParseError } from './parseError.js'; +import { commandName } from './tokens.js'; +import { findLabel, keptValues } from './seqValues.js'; + +/** + * One command inside MIC ... END, the Multilayer Index Catalog. MWL sets the + * wavelengths (nm) for the entries that follow it; 'label' n... adds an entry + * with n at those wavelengths; EXT 'label' k... gives its extinction + * coefficients, 0 when absent; END closes the catalog. Anything else is + * reported as an unknown command. MWL, 'label' and EXT keep their first 21 + * values, as CODE V 11.2 does (keptValues). + */ +export function micCommand(stack, tokens, line, warnings) { + const [head, ...args] = tokens; + if (head.quoted) { + stack.micEntries.set(head.text, { mwl: stack.mwl, n: keptValues(args, head.text, line, warnings), k: null, line }); + return; + } + const name = commandName(head); + if (name === 'MWL') stack.mwl = keptValues(args, 'MWL', line, warnings); + else if (name === 'EXT') setExtinction(stack, args, line, warnings); + else if (name === 'END') stack.micOpen = false; + else warnings.push({ kind: 'unknownCommand', command: head.text, line }); +} + +function setExtinction(stack, args, line, warnings) { + const label = args[0]?.text ?? ''; + const key = findLabel(stack.micEntries, label); + if (key === undefined) throw new CodevParseError('unknownMaterial', { label }); + Object.assign(stack.micEntries.get(key), { k: keptValues(args.slice(1), 'EXT', line, warnings), kLine: line }); +} + +/** + * The catalog as [λ_nm, n, k] rows per label, in MWL order. + * @throws {CodevParseError} micMismatch when an entry's n or k count differs from its MWL + */ +export function micTables(stack) { + const tables = {}; + for (const [label, entry] of stack.micEntries) { + const mwl = entry.mwl || []; + if (!mwl.length || entry.n.length !== mwl.length) throw new CodevParseError('micMismatch', { label, line: entry.line }); + if (entry.k && entry.k.length !== mwl.length) throw new CodevParseError('micMismatch', { label, line: entry.kLine }); + tables[label] = mwl.map((lam, i) => [lam, entry.n[i], entry.k ? entry.k[i] : 0]); + } + return tables; +} diff --git a/src/utils/io/codevCoating/micIndex.js b/src/utils/io/codevCoating/micIndex.js new file mode 100644 index 00000000..e1dbe100 --- /dev/null +++ b/src/utils/io/codevCoating/micIndex.js @@ -0,0 +1,245 @@ +/** + * n and k of a Multilayer Index Catalog (MIC) material at any wavelength, from + * its MWL points, as CODE V 11.2 computes them. + * + * The MUL help does not say how CODE V interpolates a MIC table. The rules + * below are read off the n and k that CODE V 11.2 prints in MPR at the + * analysis wavelengths, and reproduce them to the 6 decimals printed, past + * the ends of a table included (CODE V warns there that the index "is being + * extrapolated"), except the three cases below. The listings: Cr, SiO2 and + * BK7 from 21-point tables at 41 wavelengths; the silver, gold, aluminium and + * SiO tables of CODE V's sample coatings; a 16-point Ta2O5 table with a flat + * run; probe tables of 2 to 5 points with n from 1.15 to 4 falling, rising + * or steepening, and k absent, small or large, rising, falling, constant or + * turning, sampled from 380 to 750 nm; and SiO2, glass and Al tables of 21 + * points from 400 to 460.61 nm, sampled up to 700 nm. + * + * - k: straight lines between MWL points; outside the table the end value. + * - Two points: n too is a straight line, its end value held outside. + * - Where k/n is below 0.1 at every MWL point: the three-term Hartmann + * formula CODE V documents for private-catalog glasses (CODE V Lens System + * Setup Reference Manual, Defining Lens Materials, Using Private Catalog + * Glasses), n = A0 + A1 · |λ − A2|^−1.2, through three neighbouring MWL + * points: the first at or above λ and the two below it, the lowest three + * below the table and the highest three above it. A2 lies below the three + * points for a curve that flattens towards long wavelengths, above them for + * one that steepens. Three points that no such curve passes through take + * the parabola through them. Three points on a straight line, or of one n, + * give that line, and CODE V warns "Index data is linear with wavelength". + * - Otherwise: a cubic spline in λ whose end pieces have the second + * derivative of their neighbour, so each end piece is a parabola, and + * outside the table that parabola continued. + * + * That k/n decides is inferred. Every table whose k/n stayed at 0.080 or + * below took the Hartmann curve (SiO, Ta2O5, probes with no k, probes with k + * up to 0.12 rising, falling or constant, one with k 0.3 at n near 4), and + * every table where it reached 0.103 took the spline (Cr, silver, gold, + * aluminium, a probe with k 0.18 at n 1.75, one with k 0.15 at n near 1.2, + * one with a single k of 0.3 and the rest 0). Probes with the same n (MWL 450 + * 550 650, n 1.9 1.8 1.75) differ only in k and show the switch. The limit + * lies between 0.080 and 0.103 and is put at 0.1. k/n² separates the same + * tables (Hartmann up to 0.039, spline from 0.059), so the listings do not + * tell the two apart; a table with n near 1.4 and k near 0.12 would. + * + * Not reproduced: three points with a flat step and then a fall (n 2.1587, + * 2.1587, 2.151057 at 480, 500, 520 nm) have no Hartmann curve; CODE V prints + * 2.156753 at 510 nm, the parabola gives 2.155834. No listing has three + * points where n turns back while k stays small, nor reaches the pole A2 of a + * Hartmann curve, where the formula is used as it stands. For a table that + * steepens, the curve CODE V prints misses its own MWL points by up to 6e-6 + * (n 1.93 printed as 1.929998) and sits up to 2e-5 from the one computed here. + * Far past the end of a spline table CODE V's n leaves the spline computed + * here: for the Al table, 21 points 3.03 nm apart, CODE V prints n 3.0e-4 + * higher at 700 nm, 240 nm past its end. Inferred: CODE V fits the spline in + * single precision. The same spline fitted to the MWL points in µm and n and + * k rounded to float32 meets CODE V's n there to 4.3e-7, and keeps every + * other listed spline table within 7.5e-7 of CODE V's n, as the fit to the + * values as entered does. The spline here is fitted to the values as entered. + */ + +const HARTMANN_POWER = 1.2; +// Inferred, see above: a table whose k/n stays below this at every MWL point +// takes the Hartmann curve. +const HARTMANN_K_PER_N = 0.1; +// A2 is looked for from 1e-9 to 1e9 spans of the three points away. The +// Hartmann curve tends to the straight line through its points as A2 moves +// away, by about (span / distance) of its rise, so past 1e9 spans it differs +// from that line, and from the parabola through the points, by less than +// 1e-9 of the rise. Nearer than 1e-9 spans, the parabola is taken. +const FAR_POLE_SPANS = 1e9; + +// The parabola through three points, a straight line when they lie on one. +function parabola(x, y) { + const d1 = (y[1] - y[0]) / (x[1] - x[0]); + const d2 = ((y[2] - y[1]) / (x[2] - x[1]) - d1) / (x[2] - x[0]); + return { + n: (t) => y[0] + (t - x[0]) * (d1 + (t - x[1]) * d2), + bend: () => 2 * d2, + pole: NaN, + }; +} + +// The Hartmann curve through three points, or null when the search finds no +// A2: n does not fall or rise at both steps, or the three points lie too near +// a straight line, or A2 would lie too near a point. `pole` is A2. +function hartmannCurve(x, y) { + const p = HARTMANN_POWER; + const ratio = (y[0] - y[1]) / (y[1] - y[2]); + if (!(ratio > 0 && Number.isFinite(ratio))) return null; + const lineRatio = (x[1] - x[0]) / (x[2] - x[1]); + const below = ratio > lineRatio; + const poleAt = (s) => (below ? x[0] - s : x[2] + s); + const g = (a, t) => Math.pow(Math.abs(t - a), -p); + const fitRatio = (s) => { + const a = poleAt(s); + return (g(a, x[0]) - g(a, x[1])) / (g(a, x[1]) - g(a, x[2])); + }; + // `s` is how far A2 lies from the nearest of the three points. The fit's + // ratio runs from +∞ (A2 below) or 0 (A2 above) as s → 0 to the + // straight-line ratio as s → ∞, so `miss` is positive while s is short. + const miss = (s) => (below ? fitRatio(s) - ratio : ratio - fitRatio(s)); + const span = x[2] - x[0]; + let lo = span / FAR_POLE_SPANS, hi = span * FAR_POLE_SPANS; + if (!(miss(lo) > 0 && miss(hi) < 0)) return null; + for (let i = 0; i < 200 && hi / lo > 1 + 1e-15; i++) { + const mid = Math.sqrt(lo * hi); + if (miss(mid) > 0) lo = mid; else hi = mid; + } + const a = poleAt(Math.sqrt(lo * hi)); + const a1 = (y[0] - y[1]) / (g(a, x[0]) - g(a, x[1])); + return { + n: (t) => y[0] + a1 * (g(a, t) - g(a, x[0])), + bend: (t) => a1 * p * (p + 1) * Math.pow(Math.abs(t - a), -p - 2), + pole: a, + }; +} + +// Second derivatives of the spline at the MWL points: the usual interior +// equations, and at each end the second derivative equal to its neighbour's. +// Thomas algorithm; at least three points. +function splineMoments(xs, ys) { + const n = xs.length; + const h = xs.slice(1).map((x, i) => x - xs[i]); + const lower = new Array(n).fill(0), diag = new Array(n).fill(1), upper = new Array(n).fill(0), rhs = new Array(n).fill(0); + upper[0] = -1; + lower[n - 1] = -1; + for (let i = 1; i < n - 1; i++) { + lower[i] = h[i - 1]; + diag[i] = 2 * (h[i - 1] + h[i]); + upper[i] = h[i]; + rhs[i] = 6 * ((ys[i + 1] - ys[i]) / h[i] - (ys[i] - ys[i - 1]) / h[i - 1]); + } + for (let i = 1; i < n; i++) { + const f = lower[i] / diag[i - 1]; + diag[i] -= f * upper[i - 1]; + rhs[i] -= f * rhs[i - 1]; + } + const m = new Array(n); + m[n - 1] = rhs[n - 1] / diag[n - 1]; + for (let i = n - 2; i >= 0; i--) m[i] = (rhs[i] - upper[i] * m[i + 1]) / diag[i]; + return m; +} + +// Piece i of the spline, between MWL points i and i + 1, continued past them. +function splinePiece(xs, ys, m, i) { + const h = xs[i + 1] - xs[i]; + const left = (t) => (xs[i + 1] - t) / h; + return { + n: (t) => { + const u = left(t), v = 1 - u; + return u * ys[i] + v * ys[i + 1] + ((u ** 3 - u) * m[i] + (v ** 3 - v) * m[i + 1]) * h * h / 6; + }, + bend: (t) => left(t) * m[i] + (1 - left(t)) * m[i + 1], + pole: NaN, + }; +} + +// Index of the MWL interval holding λ; outside the table, the end interval. +function intervalOf(xs, t) { + let i = 0; + while (i < xs.length - 2 && t > xs[i + 1]) i++; + return i; +} + +function linearHeld(xs, ys, t) { + if (t <= xs[0]) return ys[0]; + if (t >= xs[xs.length - 1]) return ys[ys.length - 1]; + const i = intervalOf(xs, t); + return ys[i] + (t - xs[i]) / (xs[i + 1] - xs[i]) * (ys[i + 1] - ys[i]); +} + +// The pieces n is made of, as a function giving the piece that holds λ. +function nPieces(xs, ys, ks) { + if (xs.length < 3) { + return () => ({ n: (t) => linearHeld(xs, ys, t), bend: () => 0, pole: NaN }); + } + if (ks.every((k, i) => k < HARTMANN_K_PER_N * ys[i])) { + const curves = xs.slice(2).map((_, i) => { + const x = xs.slice(i, i + 3), y = ys.slice(i, i + 3); + return hartmannCurve(x, y) ?? parabola(x, y); + }); + return (t) => { + const j = xs.findIndex(x => x >= t); + return curves[j < 0 ? curves.length - 1 : Math.max(j - 2, 0)]; + }; + } + const m = splineMoments(xs, ys); + const pieces = xs.slice(1).map((_, i) => splinePiece(xs, ys, m, i)); + return (t) => pieces[intervalOf(xs, t)]; +} + +/** + * CODE V's n and k of a MIC table as functions of λ in nm. + * + * `bendMax(lo, hi)` bounds |d²n/dλ²| over [lo, hi], an interval between two + * neighbouring MWL points or outside the table: n'' of a spline piece is a + * straight line in λ, that of a parabola constant, and that of a Hartmann + * curve keeps one sign and grows towards its pole, so each is largest at an + * end of the interval. It is Infinity when the pole lies inside, where n has + * no bound. + * + * @param {Array<[number, number, number]>} rows [λ_nm, n, k] in MWL order + * @returns {{n: (λ:number)=>number, k: (λ:number)=>number, + * bendMax: (lo:number, hi:number)=>number, mwl: number[]}} mwl ascending + */ +export function micCurve(rows) { + const sorted = rows.slice().sort((p, q) => p[0] - q[0]); + const xs = sorted.map(r => r[0]), ys = sorted.map(r => r[1]), ks = sorted.map(r => r[2]); + const pieceAt = nPieces(xs, ys, ks); + const n = (t) => { + const exact = xs.indexOf(t); + return exact >= 0 ? ys[exact] : pieceAt(t).n(t); + }; + const bendMax = (lo, hi) => { + const piece = pieceAt((lo + hi) / 2); + if (piece.pole >= lo && piece.pole <= hi) return Infinity; + return Math.max(Math.abs(piece.bend(lo)), Math.abs(piece.bend(hi))); + }; + return { n, k: (t) => linearHeld(xs, ks, t), bendMax, mwl: xs }; +} + +/** + * n of a MIC table at `lambdaNm`, as CODE V 11.2 computes it. + * @param {Array<[number, number, number]>} rows [λ_nm, n, k] in MWL order + * @param {number} lambdaNm + * @returns {number} + */ +export function micIndexAt(rows, lambdaNm) { + return micCurve(rows).n(lambdaNm); +} + +/** + * k of a MIC table at `lambdaNm`, as CODE V 11.2 computes it. + * @param {Array<[number, number, number]>} rows [λ_nm, n, k] in MWL order + * @param {number} lambdaNm + * @returns {number} + */ +export function micExtinctionAt(rows, lambdaNm) { + return micCurve(rows).k(lambdaNm); +} + +/** Whether `lambdaNm` lies outside the MWL range of a MIC table. */ +export function outsideMic(rows, lambdaNm) { + const xs = rows.map(r => r[0]); + return lambdaNm < Math.min(...xs) || lambdaNm > Math.max(...xs); +} diff --git a/src/utils/io/codevCoating/micPoints.js b/src/utils/io/codevCoating/micPoints.js new file mode 100644 index 00000000..ffa20239 --- /dev/null +++ b/src/utils/io/codevCoating/micPoints.js @@ -0,0 +1,39 @@ +import { micCurve } from './micIndex.js'; + +// A miss CODE V cannot be told apart from the written table: half a unit in +// the sixth decimal, the precision n and k are written with. +const WRITTEN_MISS = 5e-7; + +// The row CODE V's interpolation of the kept rows misses most, in n or k, +// with that miss. +function worstRow(rows, kept) { + const curve = micCurve(kept.map(i => rows[i])); + let worst = { index: -1, miss: 0 }; + rows.forEach(([lam, n, k], index) => { + const miss = Math.max(Math.abs(curve.n(lam) - n), Math.abs(curve.k(lam) - k)); + if (miss > worst.miss) worst = { index, miss }; + }); + return worst; +} + +/** + * Choose at most `max` of the [λ_nm, n, k] rows (ascending λ) for a MIC table. + * + * A table of `max` rows or fewer is kept whole. A longer one keeps both ends, + * then adds, one at a time, the row that CODE V's interpolation of the rows + * kept so far (micIndex.js) misses by the most in n or k, so the points + * gather where the material bends and thin out where CODE V's curve already + * follows it. It stops early once no row is missed by more than half a unit + * in the sixth decimal. + */ +export function pickMicRows(rows, max) { + if (rows.length <= max) return rows.slice(); + const kept = [0, rows.length - 1]; + while (kept.length < max) { + const worst = worstRow(rows, kept); + if (worst.miss <= WRITTEN_MISS) break; + kept.push(worst.index); + kept.sort((a, b) => a - b); + } + return kept.map(i => rows[i]); +} diff --git a/src/utils/io/codevCoating/mulMic.js b/src/utils/io/codevCoating/mulMic.js new file mode 100644 index 00000000..499d6518 --- /dev/null +++ b/src/utils/io/codevCoating/mulMic.js @@ -0,0 +1,82 @@ +import { CodevParseError } from './parseError.js'; + +/* + * The MIC block of a .mul, positions 1-based as CODE V's own pointers are. + * Each MWL group is its wavelengths in µm, then for each material a label word + * (the 6-character label in the 8 bytes of a REAL*8, printed as a number near + * 6E-154), its n at those wavelengths and its k. After the groups, from the + * position `sampledStart`, each material used in the stack has a label word, + * its n at the analysis wavelengths and its k, as CODE V computed them. A + * final 0 ends the block. + * + * The link integers at the same positions: at the start p of a group, the + * number of MWL points and the position of its first label word; one past + * each label word, the position the next material starts at, 0 after the + * last. That is the next label word of the same group, whose own link is 0, + * or the start of the next group, whose link is its point count; either way + * it follows straight on, and the last material ends at `sampledStart`. A + * group holds the materials entered after one MWL command: the format 5 + * samples have one to a group, and CODE V 11.2 kept the four materials + * TFStudio wrote under one MWL in one group, and a repeated MWL command, with + * the same wavelengths, as a group of its own. + */ + +const notMul = () => new CodevParseError('notMul'); +const at = (list, position) => list[position - 1]; +const span = (list, position, count) => list.slice(position - 1, position - 1 + count); +const nm = (um) => Number((um * 1000).toPrecision(12)); + +// Where the material whose label word is at `word` hands on: the position +// its link names, checked against where the material ends. +function nextMaterial(record, word, points) { + const next = at(record.micLinks, word + 1); + if (word + 2 * points + 1 !== (next || record.sampledStart)) throw notMul(); + return next; +} + +// The materials of the group that starts at `start`, as [λ_nm, n, k] rows +// added to `tables`. Returns the start of the next group, or 0 after the last. +function readGroup(record, start, tables) { + const points = at(record.micLinks, start); + let word = at(record.micLinks, start + 1); + if (!(points > 0) || word !== start + points) throw notMul(); + const mwl = span(record.micBlock, start, points).map(nm); + for (;;) { + const n = span(record.micBlock, word + 1, points); + const k = span(record.micBlock, word + 1 + points, points); + tables.push(mwl.map((lam, i) => [lam, n[i], k[i]])); + word = nextMaterial(record, word, points); + if (word === 0 || at(record.micLinks, word) > 0) return word; + } +} + +/** + * The MIC tables of a .mul as [λ_nm, n, k] rows by label, CODE V's n and k at + * the analysis wavelengths by label, and the label of each sampled block's + * label-word position. + */ +export function readMulMic(record) { + const { sampledStart, micBlock, micLabels, wavelengthsUm } = record; + const tableRows = []; + let start = sampledStart > 1 ? 1 : 0; + while (start > 0) start = readGroup(record, start, tableRows); + const stride = 2 * wavelengthsUm.length + 1; + const blocks = (micBlock.length - sampledStart) / stride; + if (!Number.isInteger(blocks) || micLabels.length !== tableRows.length + blocks) throw notMul(); + + const tables = {}; + tableRows.forEach((rows, i) => { tables[micLabels[i]] = rows; }); + const sampled = {}; + const labelAt = {}; + for (let b = 0; b < blocks; b++) { + const word = sampledStart + b * stride; + const label = micLabels[tableRows.length + b]; + if (!tables[label]) throw notMul(); + labelAt[word] = label; + sampled[label] = { + n: span(micBlock, word + 1, wavelengthsUm.length), + k: span(micBlock, word + 1 + wavelengthsUm.length, wavelengthsUm.length), + }; + } + return { tables, sampled, labelAt }; +} diff --git a/src/utils/io/codevCoating/mulReader.js b/src/utils/io/codevCoating/mulReader.js new file mode 100644 index 00000000..315f018f --- /dev/null +++ b/src/utils/io/codevCoating/mulReader.js @@ -0,0 +1,76 @@ +import { CodevParseError } from './parseError.js'; + +const notMul = () => new CodevParseError('notMul'); + +/** + * A REAL as Fortran writes it. In a G field a three-digit exponent leaves no + * room for the E, so 0.6133314552-153 is 0.6133314552E-153. D reads as E. + */ +export function fortranReal(text) { + const match = /^([+-]?(?:\d+\.?\d*|\.\d+))(?:[EeDd]([+-]?\d+)|([+-]\d+))?$/.exec(text); + if (!match) return NaN; + const exponent = match[2] ?? match[3]; + return Number(exponent === undefined ? match[1] : `${match[1]}e${exponent}`); +} + +/** + * The shortest decimal within `relTol` of x. The .mul holds most entered + * values as float32 (1.519999981 for 1.52); this gives back the value as + * entered whenever it had no more digits than a float32 carries. + */ +export function shortestNear(x, relTol) { + if (!Number.isFinite(x) || x === 0) return x; + for (let digits = 1; digits <= 15; digits++) { + const value = Number(x.toPrecision(digits)); + if (Math.abs(value - x) <= relTol * Math.abs(x)) return value; + } + return x; +} + +// Ten-character label fields after a ten-character count. A list that does +// not fit on one 80-column line goes on over the next lines, each with the +// count field left blank (tfs_agmir.mul, saved by CODE V 11.2: 7 of its 8 +// labels on the first line, the 8th on the next). +function readLabels(nextLine) { + const line = nextLine(); + const count = Number(line.slice(0, 10)); + if (!Number.isInteger(count) || count < 0) throw notMul(); + const labels = []; + let rest = line.slice(10); + while (labels.length < count) { + if (!rest.trim()) rest = nextLine().slice(10); + labels.push(rest.slice(0, 10).trim()); + rest = rest.slice(10); + } + return labels; +} + +/** + * Line reader for a .mul. Each block of numbers starts on a new line and runs + * over as many lines as it needs; a block that ends early, holds something + * other than a number, or a file that ends first, is not a .mul. + */ +export function mulReader(text) { + const lines = String(text ?? '').split(/\r\n|\r|\n/); + let at = 0; + const nextLine = () => { + if (at >= lines.length) throw notMul(); + return lines[at++]; + }; + function block(count, parse) { + const values = []; + while (values.length < count) { + const fields = nextLine().trim().split(/\s+/).filter(Boolean); + if (!fields.length) throw notMul(); + values.push(...fields.map(parse)); + } + if (values.length !== count || !values.every(Number.isFinite)) throw notMul(); + return values; + } + return { + line: nextLine, + ints: (count) => block(count, field => (/^[+-]?\d+$/.test(field) ? Number(field) : NaN)), + reals: (count) => block(count, fortranReal), + labels: () => readLabels(nextLine), + }; +} diff --git a/src/utils/io/codevCoating/mulRecord.js b/src/utils/io/codevCoating/mulRecord.js new file mode 100644 index 00000000..e026303c --- /dev/null +++ b/src/utils/io/codevCoating/mulRecord.js @@ -0,0 +1,111 @@ +import { CodevParseError } from './parseError.js'; +import { mulReader } from './mulReader.js'; + +/* + * Layout of a .mul, the file CODE V's SAV writes. The help does not describe + * it; this is what two sets of files show, each saved from a .seq that is + * also at hand: the 15 sample coatings CODE V ships with (format 5), and + * seven TFStudio exports saved by CODE V 11.2 (format 6, in + * tests/reference/codev). Fixed-width Fortran text: integers I10, eight to a + * line, reals G20.10, four to a line, every block on a new line. + * + * line 1 format number (5 or 6) and a real (0.0) + * line 2 TIT, 80 characters + * line 3 date of the SAV; format 6 adds PLAINTEXT + * 5 ints unknown (0 9600 10233 1010 0 in every format 5 file) + * 45 ints counts, by position (0-based) in HEADER below; the rest unknown. + * Format 5 repeats the wavelength count at [4] and the angle + * count at [42]. Format 6 has 1 at [4] in every file, and at [42] + * a number from 0 to 5 that is not the angle count. + * 12 ints unknown, zero in every file + * M ints thickness code of each medium, incident medium first (COA code; + * the incident medium's is 0 in every sample, the substrate's 100 + * in all but one, where it is 0) + * M ints per medium: −1 for an index entered on the line, else the + * 1-based position in the MIC block of the label word that heads + * that material's n and k at the analysis wavelengths + * C ints links between the MWL groups of the MIC block (see mulMic.js) + * S reals settings; [6] is REF in µm, [20..24] the ANG angles in radians; + * [7], [8] and [14] change with the stack and look like results of + * an analysis (one angle, no absorber: [14] = 1 − [7]); [19] is 1 + * in the samples with a MIC and 0 in the others; the rest unknown + * (ones, zeros, 0.5 and 0.98 in every sample) + * W reals analysis wavelengths in µm, float32 (0.4000000060) + * 4×W per wavelength, two of ones and two of zeros in every sample: + * the MAU targets and weights (TRT, WTW, RST, RPT) by their + * defaults; which is which is not known + * M reals thickness of each medium in waves of REF, n(REF) · d / REF + * M reals k of each medium entered on the line, 0 for a MIC medium + * M reals n of each medium at REF, a MIC medium's as CODE V computes it + * labels count, then 10-character labels: one per MIC table, then one + * per block of n and k at the analysis wavelengths + * C reals the MIC block (see mulMic.js) + * labels the MIC materials once each, then M reals and one real per + * angle: not read here + * + * M media (incident, layers, substrate), W wavelengths, C the MIC block + * length, S the settings count. + */ +const HEADER = { + media: 16, wavelengths: 17, micLength: 18, settings: 22, + interfaces: 26, angles: 28, sampledStart: 35, +}; +// The formats read, each with the counts it repeats elsewhere in the header. +const REPEATS = { 5: { wavelengths: 4, angles: 42 }, 6: {} }; +const SETTING = { refUm: 6, firstAngle: 20 }; +const TARGET_BLOCKS = 4; + +const notMul = () => new CodevParseError('notMul'); + +// The counts by name. Each count the format repeats elsewhere in the header +// has to agree with its repeat, or the text is not a .mul. +function counts(header, repeats) { + const c = Object.fromEntries(Object.entries(HEADER).map(([name, at]) => [name, header[at]])); + const conditions = [ + c.wavelengths >= 1, c.media >= 3, c.micLength >= 1, c.angles >= 1, c.sampledStart >= 1, + c.settings > SETTING.firstAngle, c.interfaces === c.media - 1, + ...Object.entries(repeats).map(([name, at]) => header[at] === c[name]), + ]; + if (!conditions.every(Boolean)) throw notMul(); + return c; +} + +function readHead(reader) { + const format = /^\s*(\d+)\s+\S+\s*$/.exec(reader.line())?.[1]; + if (!Object.hasOwn(REPEATS, format)) throw notMul(); + const title = reader.line().trimEnd(); + reader.line(); + reader.ints(5); + const header = reader.ints(45); + reader.ints(12); + return { title, ...counts(header, REPEATS[format]) }; +} + +/** + * The numbers of a .mul by their place in the layout above. + * @param {string} text + * @returns {object} raw record; lengths in µm, angles in radians + * @throws {CodevParseError} notMul + */ +export function readMulRecord(text) { + const reader = mulReader(text); + const head = readHead(reader); + const codes = reader.ints(head.media); + const pointers = reader.ints(head.media); + const micLinks = reader.ints(head.micLength); + const settings = reader.reals(head.settings); + const wavelengthsUm = reader.reals(head.wavelengths); + for (let i = 0; i < TARGET_BLOCKS; i++) reader.reals(head.wavelengths); + const thicknessWaves = reader.reals(head.media); + const kOnLine = reader.reals(head.media); + const nAtRef = reader.reals(head.media); + const micLabels = reader.labels(); + const micBlock = reader.reals(head.micLength); + return { + title: head.title, + refUm: settings[SETTING.refUm], + anglesRad: settings.slice(SETTING.firstAngle, SETTING.firstAngle + head.angles), + wavelengthsUm, codes, pointers, thicknessWaves, kOnLine, nAtRef, + micLinks, micLabels, micBlock, sampledStart: head.sampledStart, + }; +} diff --git a/src/utils/io/codevCoating/parseError.js b/src/utils/io/codevCoating/parseError.js new file mode 100644 index 00000000..cbfdabbf --- /dev/null +++ b/src/utils/io/codevCoating/parseError.js @@ -0,0 +1,19 @@ +/** + * A CODE V coating file that cannot be read as a stack. `kind` names the + * problem and `detail` carries its values: + * noStack no MDA stack with a COA layer + * missingCommand {command} a command CODE V requires is absent (SUB, or WL/WLG) + * unknownGroup {label} COA names a group no GRO defined + * unknownMaterial {label} a layer or medium names a label the MIC does not hold + * micMismatch {label, line} a MIC entry has more or fewer values than its MWL + * badNumber {line, text} a value that should be a number is not + * notMul the text does not have the layout of a CODE V .mul file + */ +export class CodevParseError extends Error { + constructor(kind, detail = {}) { + super(`CODE V import: ${kind}`); + this.name = 'CodevParseError'; + this.kind = kind; + this.detail = detail; + } +} diff --git a/src/utils/io/codevCoating/parseMul.js b/src/utils/io/codevCoating/parseMul.js new file mode 100644 index 00000000..f98950e0 --- /dev/null +++ b/src/utils/io/codevCoating/parseMul.js @@ -0,0 +1,69 @@ +import { CodevParseError } from './parseError.js'; +import { readMulRecord } from './mulRecord.js'; +import { readMulMic } from './mulMic.js'; +import { shortestNear } from './mulReader.js'; + +// How far a value read back may sit from the one entered, relative. A float32 +// holds 24 significant bits, so a stored value is within 2^-24 of the one +// entered; an angle also passes through a product with π/180, so it gets +// twice the room. A thickness is not rounded this way: one entered in waves +// (PHT N) has no short decimal form in nm. +const STORED = 2 ** -23; +const ANGLE = 2 ** -22; + +function mediumIndex(record, labelAt, i) { + const pointer = record.pointers[i]; + if (pointer < 0) return { n: shortestNear(record.nAtRef[i], STORED), k: shortestNear(record.kOnLine[i], STORED) }; + const label = labelAt[pointer]; + if (label === undefined) throw new CodevParseError('notMul'); + return { label }; +} + +// d = T · REF / n(REF): the thickness CODE V stored in waves of REF, back in +// nm, with the same n(REF) it was stored with. +function layerOf(record, labelAt, refNm, i) { + const index = mediumIndex(record, labelAt, i); + const n = 'label' in index ? record.nAtRef[i] : index.n; + if (!(n > 0)) throw new CodevParseError('notMul'); + return { + thicknessNm: record.thicknessWaves[i] * refNm / n, + code: record.codes[i], + index, + }; +} + +/** + * The stack and CODE V's own n and k of each MIC material at the analysis + * wavelengths (`sampled`, by label), from the text of a .mul. + * @throws {CodevParseError} notMul + */ +export function readCodevMul(text) { + const record = readMulRecord(text); + const { tables, sampled, labelAt } = readMulMic(record); + const refNm = shortestNear(record.refUm * 1000, STORED); + const last = record.codes.length - 1; + const layers = []; + for (let i = 1; i < last; i++) layers.push(layerOf(record, labelAt, refNm, i)); + const stack = { + title: record.title, + refNm, + wavelengthsNm: record.wavelengthsUm.map(um => shortestNear(um * 1000, STORED)), + anglesDeg: record.anglesRad.map(rad => shortestNear(rad * 180 / Math.PI, ANGLE)), + incident: mediumIndex(record, labelAt, 0), + substrate: mediumIndex(record, labelAt, last), + layers, + mic: tables, + warnings: [], + }; + return { stack, sampled }; +} + +/** + * Read a .mul saved by CODE V into a CodevStack. + * @param {string} text + * @returns {object} CodevStack, see codevCoatingFile.js + * @throws {CodevParseError} notMul + */ +export function parseCodevMul(text) { + return readCodevMul(text).stack; +} diff --git a/src/utils/io/codevCoating/parseSeq.js b/src/utils/io/codevCoating/parseSeq.js new file mode 100644 index 00000000..9eed1565 --- /dev/null +++ b/src/utils/io/codevCoating/parseSeq.js @@ -0,0 +1,77 @@ +import { CodevParseError } from './parseError.js'; +import { commandName, splitCommands } from './tokens.js'; +import { MDA_COMMANDS, newStack } from './mdaCommands.js'; +import { micCommand } from './micCommands.js'; +import { finishStack } from './stackFinish.js'; +import { hasDecimalComma } from './seqValues.js'; + +// Sub-options of MUL. Each ends data entry (MDA); MCH alters the stack, which +// this reader does not apply, so it is reported. +const SUB_OPTIONS = new Set(['MAN', 'MAU', 'MTO', 'MPL', 'MPR', 'MLI', 'MCH']); + +// MUL commands that do not define the stack: the immediate commands, and the +// settings of plotting, optimization, tolerancing and file listing. They are +// passed over without a warning wherever they appear. +const NOT_STACK = new Set([ + 'GO', 'SAV', 'SVE', 'RES', 'CSA', 'OUT', + 'TRN', 'RFL', 'AVE', 'SPL', 'PPL', 'ALL', 'RAN', 'SPA', 'STL', + 'RPT', 'RST', 'TRT', 'WTW', 'WTA', 'MNT', 'MXT', 'MNC', 'MXC', 'IMP', + 'DLT', 'DLN', 'DIR', 'DEL', 'COP', 'REN', +]); + +// The first line of the stack where a value has a decimal comma, which +// numberOf reads as a point and CODE V does not read at all; noted once per +// file. TIT is text and is passed over. +function noteDecimalComma(tokens, line, warnings) { + if (commandName(tokens[0]) === 'TIT' || warnings.some(w => w.kind === 'decimalComma')) return; + if (tokens.some(token => !token.quoted && hasDecimalComma(token.text))) warnings.push({ kind: 'decimalComma', line }); +} + +function mdaCommand(stack, tokens, line, warnings) { + noteDecimalComma(tokens, line, warnings); + if (stack.micOpen) return micCommand(stack, tokens, line, warnings); + const name = commandName(tokens[0]); + const handler = MDA_COMMANDS[name]; + if (handler) return handler(stack, tokens.slice(1), line, warnings); + if (!NOT_STACK.has(name)) warnings.push({ kind: 'unknownCommand', command: tokens[0].text, line }); +} + +const MODE_AFTER = { MEX: 'outside', MDA: 'mda' }; + +// Where a command leaves the reader: outside MUL, in MUL between sub-options, +// in MDA, or in another sub-option. Returns null for a command that is data. +function nextMode(mode, name) { + if (name === 'MUL') return 'mul'; + if (mode === 'outside') return 'outside'; + return MODE_AFTER[name] ?? (SUB_OPTIONS.has(name) ? 'other' : null); +} + +/** + * Read a CODE V MUL command file (.seq) into a CodevStack. + * + * Only the stack entered in MDA is read; commands outside MUL, and the + * analysis, plot and optimization sub-options, are passed over. A second MDA + * starts a new stack, as it does in CODE V. + * + * @param {string} text + * @returns {object} CodevStack, see codevCoatingFile.js + * @throws {CodevParseError} + */ +export function parseCodevSeq(text) { + const warnings = []; + let mode = 'outside'; + let stack = null; + for (const { line, tokens } of splitCommands(text)) { + const name = commandName(tokens[0]); + const next = nextMode(mode, name); + if (next === null) { + if (mode === 'mda') mdaCommand(stack, tokens, line, warnings); + continue; + } + if (next === 'mda') stack = newStack(); + if (name === 'MCH' && next === 'other') warnings.push({ kind: 'unknownCommand', command: tokens[0].text, line }); + mode = next; + } + if (!stack) throw new CodevParseError('noStack'); + return finishStack(stack, warnings); +} diff --git a/src/utils/io/codevCoating/seqValues.js b/src/utils/io/codevCoating/seqValues.js new file mode 100644 index 00000000..bac03f2b --- /dev/null +++ b/src/utils/io/codevCoating/seqValues.js @@ -0,0 +1,49 @@ +import { CodevParseError } from './parseError.js'; +import { CODEV_LIMITS } from './serialize.js'; + +// A comma between two digits, read as a decimal point. Essential Macleod's +// CODE V export writes every decimal that way ("403,03") on a machine set to +// a decimal-comma locale, and CODE V 11.2 rejects such a number as "Invalid +// data". Only a comma between digits is read so: "400, 500" or "400," stays +// a bad number. +const DECIMAL_COMMA = /(\d),(?=\d)/g; + +/** Whether `text` holds a comma between two digits. */ +export const hasDecimalComma = (text) => /\d,\d/.test(text); + +/** + * A token read as a number. A Fortran D exponent (1.5D-3) reads as E, and a + * comma between two digits as a decimal point. + * @throws {CodevParseError} badNumber, with the line and the token's text + */ +export function numberOf(token, line) { + const text = token && !token.quoted ? token.text : ''; + const value = text ? Number(text.replace(DECIMAL_COMMA, '$1.').replace(/[dD]/, 'e')) : NaN; + if (!Number.isFinite(value)) throw new CodevParseError('badNumber', { line, text: token?.text ?? '' }); + return value; +} + +export const numbersOf = (tokens, line) => tokens.map(token => numberOf(token, line)); + +/** + * The values one command keeps. CODE V 11.2 reads the first 21 values of one + * WL, MWL, 'label' n or EXT command and ignores the rest with "Extra data ... + * ignored"; they are left out here too and reported as an extraValues + * warning under `command`. + */ +export function keptValues(tokens, command, line, warnings) { + const limit = CODEV_LIMITS.valuesPerCommand; + if (tokens.length > limit) warnings.push({ kind: 'extraValues', command, line, count: tokens.length - limit, limit }); + return numbersOf(tokens.slice(0, limit), line); +} + +/** + * The key of `map` (a Map or an object) that names `label`: the same text, or + * failing that the same text in another case. + */ +export function findLabel(map, label) { + const keys = map instanceof Map ? [...map.keys()] : Object.keys(map); + if (keys.includes(label)) return label; + const upper = String(label).toUpperCase(); + return keys.find(key => key.toUpperCase() === upper); +} diff --git a/src/utils/io/codevCoating/serialize.js b/src/utils/io/codevCoating/serialize.js new file mode 100644 index 00000000..8a24c2ac --- /dev/null +++ b/src/utils/io/codevCoating/serialize.js @@ -0,0 +1,171 @@ +import { fmtNum, quote, wrapValues } from './format.js'; +import { makeLabeler } from './labels.js'; +import { pickMicRows } from './micPoints.js'; + +/** + * Limits of the CODE V MUL option, from the MDA sub-option page of the + * Multilayer Design Reference Manual: COA up to 1000 layers, WL and WLG + * together up to 100 wavelengths, ANG up to 5 angles, MWL up to 21 points, + * TIT up to 80 characters. One WL, MWL, 'label' n or EXT command reads at + * most 21 values: CODE V 11.2 ignores the rest with "Extra data ... + * ignored", so a longer wavelength list is written as several WL commands. + */ +export const CODEV_LIMITS = { layers: 1000, wavelengths: 100, angles: 5, micPoints: 21, title: 80, valuesPerCommand: 21 }; + +/** A stack the MUL option cannot hold. `detail` carries the count and limit, or the material. */ +export class CodevExportError extends Error { + constructor(kind, detail) { + super(`CODE V export: ${kind}`); + this.name = 'CodevExportError'; + this.kind = kind; + this.detail = detail; + } +} + +// Decimals written: wavelength and angle, n and k, thickness in nm. +const WL = 4, NK = 6, THICK = 4; + +function checkLimits(counts) { + for (const [kind, count] of Object.entries(counts)) { + const limit = CODEV_LIMITS[kind]; + if (count > limit || (kind === 'wavelengths' && count === 0)) { + throw new CodevExportError(kind, { count, limit }); + } + } +} + +const written = (x) => fmtNum(x, NK); +const isConstant = (rows) => rows.every(r => written(r[1]) === written(rows[0][1]) && written(r[2]) === written(rows[0][2])); +const absorbs = (rows) => rows.some(r => written(r[2]) !== '0'); + +// n and k of one material at the analysis wavelengths, as [λ_nm, n, k] rows. +// A wavelength with no index (a formula at a pole) is left out. +function sampleRows(getNK, wavelengthsNm) { + return wavelengthsNm + .map((lam) => { const [n, k] = getNK(lam); return [lam, n, Math.abs(k || 0)]; }) + .filter(([, n]) => Number.isFinite(n)); +} + +// The Multilayer Index Catalog of the export: one entry per dispersive +// material, keyed by material id, and the warnings raised while filling it. +function newCatalog(opts) { + return { opts, labelFor: makeLabeler(), entries: new Map(), warnings: [] }; +} + +function addEntry(catalog, key, name, rows) { + const picked = pickMicRows(rows, CODEV_LIMITS.micPoints); + if (picked.length < rows.length) catalog.warnings.push({ kind: 'resampled', material: name, from: rows.length, to: picked.length }); + catalog.entries.set(key, { label: catalog.labelFor(name), rows: picked }); +} + +// The rows of material `id` at the analysis wavelengths. INC and SUB +// (`medium` 'incident' or 'substrate') take n only, so an absorbing medium +// loses its k with a warning. +function mediumRows(catalog, id, name, medium) { + const { getNK, wavelengthsNm } = catalog.opts; + const rows = sampleRows((lam) => getNK(id, lam), wavelengthsNm); + if (!rows.length) throw new CodevExportError('noIndex', { material: name }); + if (!medium || !absorbs(rows)) return { rows, dropK: false }; + catalog.warnings.push({ kind: 'mediumAbsorbs', role: medium, material: name }); + return { rows: rows.map(([lam, n]) => [lam, n, 0]), dropK: true }; +} + +// The index text a COA, INC or SUB line refers to material `id` by: its n +// (and k on a COA line) when constant, else its quoted MIC label. +function refer(catalog, id, medium) { + const name = catalog.opts.materialName(id); + const { rows, dropK } = mediumRows(catalog, id, name, medium); + if (isConstant(rows)) { + const [, n, k] = rows[0]; + return absorbs(rows) ? `${written(n)} ${written(k)}` : written(n); + } + const key = dropK ? `${id}\u0000n` : id; + if (!catalog.entries.has(key)) addEntry(catalog, key, name, rows); + return quote(catalog.entries.get(key).label); +} + +// MIC ... END, with an MWL line wherever the wavelengths change. +function micLines(catalog) { + if (!catalog.entries.size) return []; + const lines = ['MIC']; + let lastMwl = ''; + for (const { label, rows } of catalog.entries.values()) { + const mwl = rows.map(r => fmtNum(r[0], WL)); + if (mwl.join(' ') !== lastMwl) lines.push(...wrapValues(' MWL', mwl)); + lastMwl = mwl.join(' '); + lines.push(...wrapValues(` ${quote(label)}`, rows.map(r => written(r[1])))); + if (absorbs(rows)) lines.push(...wrapValues(` EXT ${quote(label)}`, rows.map(r => written(r[2])))); + } + lines.push('END'); + return lines; +} + +// The analysis wavelengths as WL commands of at most 21 values each. +function wlLines(wavelengthsNm) { + const lines = []; + for (let i = 0; i < wavelengthsNm.length; i += CODEV_LIMITS.valuesPerCommand) { + const chunk = wavelengthsNm.slice(i, i + CODEV_LIMITS.valuesPerCommand).map(w => fmtNum(w, WL)); + lines.push(...wrapValues('WL', chunk)); + } + return lines; +} + +/** + * Write a coating as a CODE V MUL command file (.seq). + * + * Running the file in CODE V enters the stack in MDA and ends with SAV, which + * writes the .mul that MLT attaches to a lens surface. Thicknesses are + * physical, in nm (PHT Y). A material whose n and k do not change over the + * analysis wavelengths is written on its COA line; any other goes into the + * Multilayer Index Catalog (MIC) sampled at those wavelengths, cut to 21 + * points by pickMicRows when there are more. INC and SUB take a real index + * only, so the k of an absorbing medium is dropped with a warning. + * + * @param {object} opts + * @param {string} opts.title TIT, up to 80 characters + * @param {string} opts.saveName .mul file name for SAV + * @param {Array<{material:string, thickness:number, locked?:boolean}>} opts.layers + * incident side first, thickness in nm; a locked layer is frozen (code 100), + * any other varies on its own (code 0) + * @param {string} opts.incident material id of the incident medium + * @param {string} opts.substrate material id of the substrate + * @param {number[]} opts.wavelengthsNm analysis wavelengths, ascending, nm + * @param {number[]} [opts.anglesDeg=[0]] angles of incidence in the incident medium, degrees + * @param {number} opts.refNm REF, nm + * @param {(id:string)=>string} opts.materialName + * @param {(id:string, lamNm:number)=>[number,number]} opts.getNK n and k ≥ 0 + * @returns {{text:string, warnings:Array}} + * @throws {CodevExportError} past a MUL limit, or for a material with no index + */ +export function buildCodevSeq(opts) { + const layers = (opts.layers || []).filter(L => L.thickness > 0); + const anglesDeg = opts.anglesDeg?.length ? opts.anglesDeg : [0]; + checkLimits({ layers: layers.length, wavelengths: opts.wavelengthsNm.length, angles: anglesDeg.length }); + + const catalog = newCatalog(opts); + const inc = refer(catalog, opts.incident, 'incident'); + const coa = layers.map(L => `COA ${fmtNum(L.thickness, THICK)} ${L.locked ? 100 : 0} ${refer(catalog, L.material, null)}`); + const sub = refer(catalog, opts.substrate, 'substrate'); + const title = String(opts.title || '').replace(/'/g, '').slice(0, CODEV_LIMITS.title) || 'TFStudio coating'; + const saveName = String(opts.saveName || '').replace(/[^A-Za-z0-9_-]+/g, '_') || 'coating'; + + const lines = [ + `! ${title}`, + '! Written by TFStudio. Layers run from the incident medium to the substrate,', + '! thicknesses are physical, in nm. SAV at the end writes the .mul file.', + 'MUL', + 'MDA', + 'PHT Y', + `TIT ${quote(title)}`, + ...micLines(catalog), + `INC ${inc}`, + ...coa, + `SUB ${sub}`, + ...wlLines(opts.wavelengthsNm), + `REF ${fmtNum(opts.refNm, WL)}`, + `ANG ${anglesDeg.map(a => fmtNum(a, WL)).join(' ')}`, + `SAV ${saveName}`, + 'MEX', + ]; + return { text: lines.join('\r\n') + '\r\n', warnings: catalog.warnings }; +} diff --git a/src/utils/io/codevCoating/stackFinish.js b/src/utils/io/codevCoating/stackFinish.js new file mode 100644 index 00000000..72f15164 --- /dev/null +++ b/src/utils/io/codevCoating/stackFinish.js @@ -0,0 +1,74 @@ +import { CodevParseError } from './parseError.js'; +import { micTables } from './micCommands.js'; +import { micIndexAt, outsideMic } from './micIndex.js'; +import { findLabel } from './seqValues.js'; + +// The MIC key a label resolves to, so every reference names the table by the +// text it was entered with. +function resolveIndex(index, mic) { + if (!('label' in index)) return { n: index.n, k: index.k }; + const key = findLabel(mic, index.label); + if (key === undefined) throw new CodevParseError('unknownMaterial', { label: index.label }); + return { label: key }; +} + +/** + * REF as the MDA page gives it: the one entered, else the central analysis + * wavelength, the one left of centre for an even count. + */ +function referenceNm(stack) { + if (stack.ref !== null) return stack.ref; + return stack.wavelengths[Math.floor((stack.wavelengths.length - 1) / 2)]; +} + +/** + * Physical thickness in nm of a layer entered under PHT N. Its thickness T is + * an optical thickness in waves of REF, "thickness * index / vacuum + * wavelength" in the PHT entry of the MDA page, so d = T · REF / n(REF), with + * n the real index of the layer at REF. + */ +function physicalNm(layer, index, refNm, mic, noteOutside) { + const rows = 'label' in index ? mic[index.label] : null; + if (rows && outsideMic(rows, refNm)) noteOutside(index.label); + const n = rows ? micIndexAt(rows, refNm) : index.n; + if (!(n > 0)) throw new CodevParseError('badNumber', { line: layer.line, text: String(n) }); + return layer.thickness * refNm / n; +} + +function outsideNoter(warnings) { + const noted = new Set(); + return (label) => { + if (noted.has(label)) return; + noted.add(label); + warnings.push({ kind: 'refOutsideTable', label }); + }; +} + +/** + * The CodevStack of a parsed MDA entry: labels resolved, REF filled in, and + * every thickness physical, in nm. + */ +export function finishStack(stack, warnings) { + if (!stack.layers.length) throw new CodevParseError('noStack'); + if (!stack.substrate) throw new CodevParseError('missingCommand', { command: 'SUB' }); + if (!stack.wavelengths.length) throw new CodevParseError('missingCommand', { command: 'WL' }); + const mic = micTables(stack); + const refNm = referenceNm(stack); + const noteOutside = outsideNoter(warnings); + const layers = stack.layers.map((layer) => { + const index = resolveIndex(layer.index, mic); + const thicknessNm = stack.physical ? layer.thickness : physicalNm(layer, index, refNm, mic, noteOutside); + return { thicknessNm, code: layer.code, index }; + }); + return { + title: stack.title, + refNm, + wavelengthsNm: stack.wavelengths.slice(), + anglesDeg: stack.angles?.length ? stack.angles : [0], + incident: resolveIndex(stack.incident, mic), + substrate: resolveIndex(stack.substrate, mic), + layers, + mic, + warnings, + }; +} diff --git a/src/utils/io/codevCoating/toDesign.js b/src/utils/io/codevCoating/toDesign.js new file mode 100644 index 00000000..d3f5d58f --- /dev/null +++ b/src/utils/io/codevCoating/toDesign.js @@ -0,0 +1,113 @@ +import { createLinearInterpolator, LINEAR_INTERPOLATION, TABULATED_INTERPOLATION } from '../../materials/pchip.js'; +import { micCurve } from './micIndex.js'; + +// A constant index is tabulated from the deep UV to the far infrared, the +// range the other design importers give one (designImport/materialResolution.js). +const CONSTANT_RANGE_NM = [200, 50000]; +const D_LINE_NM = 587.5618; +const LOCKED_CODE = 100; +// How far a straight line between two rows of an imported MIC material may +// stray from CODE V's n: half a unit in the sixth decimal, the precision +// CODE V prints n with. +const LINE_MISS = 5e-7; + +const short = (x) => String(Number(x.toPrecision(6))); +const idOf = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'material'; + +function record(name, tabData, interp, comment) { + const nAt = createLinearInterpolator(tabData.map(row => [row[0], row[1]])); + return { + id: idOf(name), + name, + formulaNum: -1, + interp, + coefficients: [], + kTable: [], + tabData, + lambdaMin: tabData[0][0] / 1000, + lambdaMax: tabData[tabData.length - 1][0] / 1000, + nd: nAt ? nAt(D_LINE_NM) : null, + vd: null, density: null, + comment, + color: null, + group: 'Imported', + }; +} + +function constantRecord(n, k, comment) { + const name = k ? `n = ${short(n)}, k = ${short(k)}` : `n = ${short(n)}`; + return record(name, CONSTANT_RANGE_NM.map(lam => [lam, n, k]), TABULATED_INTERPOLATION, comment); +} + +// The wavelengths a MIC material is tabulated at: its MWL points, where k +// bends, and the wavelengths in `needed`, where CODE V computes it; between +// each two, evenly spaced, as many more as keep a straight line within +// LINE_MISS of n. A line between rows h apart misses a curve by at most +// h² · max|n''| / 8. +function micWavelengths(curve, needed) { + const marks = [...new Set([...curve.mwl, ...needed])].sort((a, b) => a - b); + const out = [marks[0]]; + for (let i = 1; i < marks.length; i++) { + const lo = marks[i - 1], hi = marks[i]; + const bend = curve.bendMax(lo, hi); + const steps = Number.isFinite(bend) ? Math.max(1, Math.ceil((hi - lo) * Math.sqrt(bend / (8 * LINE_MISS)))) : 1; + for (let s = 1; s < steps; s++) out.push(lo + (hi - lo) * s / steps); + out.push(hi); + } + return out; +} + +function micRecord(label, rows, needed, comment) { + const curve = micCurve(rows); + const tabData = micWavelengths(curve, needed).map(lam => [lam, curve.n(lam), curve.k(lam)]); + return record(label, tabData, LINEAR_INTERPOLATION, `${comment}. The rows are CODE V's interpolation of its MIC table.`); +} + +/** + * The TFStudio materials and layers of a CodevStack. + * + * Each MIC table becomes a tabulated material named after its label, read + * with straight lines between rows (TFStudio's n + ik with k ≥ 0 is CODE V's + * MIC k as entered). Its rows are n and k as CODE V computes them from the + * table (micIndex.js), from the shortest to the longest of its MWL points, + * analysis wavelengths and REF, so CODE V's values past the ends of the table + * come in where the file analyses. Each index entered on a line becomes a + * constant material named after its value; equal values share one. The + * warnings are the stack's own, then a `coupledLayers` {count} when layers + * carry a coupling code (1 to 99), which TFStudio has no equivalent of: those + * layers come in free. + * + * @param {object} stack CodevStack + * @param {{sourceName?: string}} [opts] file name for the materials' comment + * @returns {{materials: Array<{key:string, material:object}>, incidentKey:string, + * substrateKey:string, layers: Array<{materialKey:string, thickness:number, locked:boolean}>, + * warnings: Array}} layers incident side first, thickness in nm + */ +export function codevStackToDesign(stack, opts = {}) { + const comment = `Imported from CODE V ${opts.sourceName || 'coating'}`; + const needed = [...stack.wavelengthsNm, stack.refNm].filter(Number.isFinite); + const materials = []; + const keys = new Map(); + const keyOf = (index) => { + const key = 'label' in index ? `mic:${index.label}` : `n:${index.n}:${index.k}`; + if (!keys.has(key)) { + keys.set(key, true); + const material = 'label' in index + ? micRecord(index.label, stack.mic[index.label], needed, comment) + : constantRecord(index.n, index.k, comment); + materials.push({ key, material }); + } + return key; + }; + const incidentKey = keyOf(stack.incident); + const layers = stack.layers.map(layer => ({ + materialKey: keyOf(layer.index), + thickness: layer.thicknessNm, + locked: layer.code === LOCKED_CODE, + })); + const substrateKey = keyOf(stack.substrate); + const coupled = stack.layers.filter(layer => layer.code > 0 && layer.code < LOCKED_CODE).length; + const warnings = [...(stack.warnings || [])]; + if (coupled) warnings.push({ kind: 'coupledLayers', count: coupled }); + return { materials, incidentKey, substrateKey, layers, warnings }; +} diff --git a/src/utils/io/codevCoating/tokens.js b/src/utils/io/codevCoating/tokens.js new file mode 100644 index 00000000..346ba7a9 --- /dev/null +++ b/src/utils/io/codevCoating/tokens.js @@ -0,0 +1,86 @@ +const isBlank = (c) => c === ' ' || c === '\t' || c === '\f' || c === '\v'; +const BREAKS = "!;'\""; + +// A quoted string from `start` (the opening quote) to the matching quote, or +// to the end of the line when it is never closed. +function quotedToken(line, start) { + const end = line.indexOf(line[start], start + 1); + const stop = end < 0 ? line.length : end; + return { token: { text: line.slice(start + 1, stop), quoted: true }, next: stop + 1 }; +} + +function bareToken(line, start) { + let end = start; + while (end < line.length && !isBlank(line[end]) && !BREAKS.includes(line[end])) end++; + return { token: { text: line.slice(start, end), quoted: false }, next: end }; +} + +// The tokens of one line up to its comment; a ";" is kept as null. +function lineTokens(line) { + const tokens = []; + let i = 0; + while (i < line.length) { + const c = line[i]; + if (c === '!') break; + if (isBlank(c)) { i++; continue; } + if (c === ';') { tokens.push(null); i++; continue; } + const { token, next } = c === "'" || c === '"' ? quotedToken(line, i) : bareToken(line, i); + tokens.push(token); + i = next; + } + return tokens; +} + +// Whether the line ends in "&", the mark that the command goes on; the mark is +// taken off the tokens. +function takeContinuation(tokens) { + const last = tokens[tokens.length - 1]; + if (!last || last.quoted || !last.text.endsWith('&')) return false; + last.text = last.text.slice(0, -1); + if (!last.text) tokens.pop(); + return true; +} + +function flush(commands, current) { + if (current && current.tokens.length) commands.push(current); + return null; +} + +/** + * Split CODE V command text into commands. + * + * The command line conventions of CODE V: blanks separate tokens, ";" separates + * commands on one line, "&" at the end of a line continues the command on the + * next, and "!" starts a comment that runs to the end of the line. A string is + * enclosed in single quotes; double quotes are read the same way, as some + * CODE V sample files write TIT with them and Essential Macleod's CODE V + * export writes its SAV file name with them. + * + * @param {string} text + * @returns {Array<{line:number, tokens:Array<{text:string, quoted:boolean}>}>} + * each command with the 1-based line it starts on + */ +export function splitCommands(text) { + const commands = []; + let current = null; + String(text ?? '').split(/\r\n|\r|\n/).forEach((line, index) => { + const tokens = lineTokens(line); + const continues = takeContinuation(tokens); + for (const token of tokens) { + if (token === null) { current = flush(commands, current); continue; } + if (!current) current = { line: index + 1, tokens: [] }; + current.tokens.push(token); + } + if (!continues) current = flush(commands, current); + }); + flush(commands, current); + return commands; +} + +/** + * The command a token names. CODE V commands are 1 to 3 letters and a longer + * word is read by its first three ("Group" is GRO); case does not matter. + */ +export function commandName(token) { + return token && !token.quoted ? token.text.slice(0, 3).toUpperCase() : ''; +} diff --git a/src/utils/io/codevCoatingFile.js b/src/utils/io/codevCoatingFile.js new file mode 100644 index 00000000..944b78c7 --- /dev/null +++ b/src/utils/io/codevCoatingFile.js @@ -0,0 +1,84 @@ +/** + * CODE V multilayer coating (MUL option) reader and writer. + * + * Pure ESM, no app dependencies beyond the tabulated-material interpolation, so + * it is testable on its own (tests/codev_coating_*.mjs). + * + * ── Format (authoritative: CODE V Multilayer Design Reference Manual, chapter + * "Multilayer Coating Design and Analysis (MUL)", MDA Sub-Option) ───────── + * + * MUL enter the option + * MDA enter a stack + * PHT Y | N thickness in nm (Y) or in waves of REF (N, the default) + * TIT 'title' up to 80 characters + * WL λ... / WLG min max step analysis wavelengths, nm, up to 100 in all + * REF λ reference wavelength, nm; default the central WL + * ANG θ... angles of incidence, degrees, up to 5; default 0 + * INC n | 'label' incident medium, no k; default 1.00 + * COA d code n [k] one layer, incident side first, up to 1000; + * COA d code 'label' code 100 frozen, 0 free, 1..99 coupled + * COA 'g' the layers of group g again + * GRO 'g' ... END a group; its layers are also entered where defined + * SUB n | 'label' substrate, no k; required + * MIC Multilayer Index Catalog: + * MWL λ... wavelengths, nm, up to 21 + * 'label' n... a material, label up to 6 characters + * EXT 'label' k... its extinction coefficients, default 0 + * END + * SAV name write name.mul + * MEX leave the option + * + * Command syntax: blanks separate tokens, ";" separates commands on a line, + * "&" ending a line continues the command, "!" starts a comment, commands of + * more than three letters are read by their first three, case does not matter + * outside strings. + * + * ── Conventions ───────────────────────────────────────────────────────────── + * + * • Wavelength and physical thickness: nm in both programs. + * • PHT N thickness T is an optical thickness in waves of REF; the physical + * thickness is d = T · REF / n(REF), n the real index of the layer at REF. + * • Extinction: CODE V takes k as a positive number; TFStudio's n + ik with + * k ≥ 0 uses it as it is. + * • The .mul is not described in the help; codevCoating/mulRecord.js gives + * the layout read from CODE V's sample coatings (format 5) and from files + * CODE V 11.2 saved (format 6). It stores every thickness in waves of REF, + * so it is read back with the n(REF) stored beside it. + * • WLG builds its wavelengths as CODE V does, a float32 running sum in µm + * (codevCoating/mdaCommands.js), so they are not round numbers in nm. + * • n and k of a MIC table between and past its MWL points follow the rules + * in codevCoating/micIndex.js, read off CODE V 11.2 listings. A PHT N + * layer of a MIC material takes n(REF) from them, and an imported MIC + * material is tabulated from them, densely enough that TFStudio's straight + * lines between its rows stay within half a unit in the sixth decimal of + * CODE V's n. + * + * ── What the reader returns ───────────────────────────────────────────────── + * + * CodevStack = { + * title, refNm, wavelengthsNm, anglesDeg, + * incident: IndexRef, substrate: IndexRef, + * layers: [{ thicknessNm, code, index: IndexRef }], incident side first + * mic: { label: [[λ_nm, n, k], ...] }, MIC tables as entered + * warnings: [{ kind, ...params }], + * } + * IndexRef = { n, k } | { label } + * + * Warnings: unknownCommand {command, line} for a command the reader does not + * know inside MDA, or MCH (stack changes it does not apply); extraValues + * {command, line, count, limit} for the values past the 21st of one WL, MWL, + * 'label' n or EXT command (command 'WL', 'MWL', 'EXT' or the label), which + * CODE V ignores and so does the reader; decimalComma {line}, once, at the + * first line of the stack with a comma between two digits, which the reader + * takes for a decimal point and CODE V does not read at all; refOutsideTable + * {label} when a PHT N layer's MIC table does not reach REF, where CODE V too + * warns that the index "is being extrapolated"; coupledLayers {count} from + * codevStackToDesign. Errors are + * CodevParseError, kinds listed in codevCoating/parseError.js. + */ + +export { buildCodevSeq, CodevExportError, CODEV_LIMITS } from './codevCoating/serialize.js'; +export { CodevParseError } from './codevCoating/parseError.js'; +export { parseCodevSeq } from './codevCoating/parseSeq.js'; +export { parseCodevMul } from './codevCoating/parseMul.js'; +export { codevStackToDesign } from './codevCoating/toDesign.js'; diff --git a/tests/codev_coating_examples.mjs b/tests/codev_coating_examples.mjs new file mode 100644 index 00000000..09e10e1b --- /dev/null +++ b/tests/codev_coating_examples.mjs @@ -0,0 +1,183 @@ +/** + * The three worked examples of CODE V Multilayer Design Reference Manual, + * Description of Output: each input read with parseCodevSeq, turned into + * TFStudio materials and layers with codevStackToDesign, and computed with the + * TFStudio transfer-matrix kernel, against the MAN table CODE V printed. + * + * Example 1: Al mirror from a MIC table under ZnS/MgF2 quarter waves. + * Example 2: 7-layer quarter-wave stack from a GRO group, at 0° and 30°. + * Example 3: Al mirror under a half wave of SiO2, with the phase table. + * + * R and T agree to one unit in the sixth decimal CODE V prints; CODE V's + * numbers are single precision (its .mul files hold float32 values). Phase: + * CODE V's reflection phase of s is the phase in Macleod's convention, which + * TFStudio reports; its p phase is 180° from that, the p sign convention in + * which r_p = −r_s at normal incidence (Macleod has r_p = r_s there). + * Example 3 prints phases at normal incidence only, so the 180° is confirmed + * there. Its transmission phases are not compared: the transmitted amplitude + * through 10 waves of Al is near 1e-220, CODE V prints its phase at three + * wavelengths and "----" at two, and the printed values differ from + * TFStudio's. + * + * Run: node tests/codev_coating_examples.mjs + */ +import assert from 'node:assert/strict'; +import { parseCodevSeq, codevStackToDesign } from '../src/utils/io/codevCoatingFile.js'; +import { makeGetNK } from '../src/utils/materials/catalogManager/dispersion.js'; +import { tmm, tmmPhaseDispersion, jetConstant } from '../src/tmmcore.js'; + +const RT_TOLERANCE = 1.5e-6; +const PHASE_TOLERANCE_DEG = 1.5e-4; + +let checks = 0; +const near = (actual, expected, tolerance, message) => { + checks++; + assert.ok(Math.abs(actual - expected) <= tolerance, `${message}: ${actual} vs ${expected}`); +}; +const wrapDeg = (deg) => ((deg % 360) + 540) % 360 - 180; + +// The design's stack at one wavelength, as tmm takes it. +function stackAt(design, lam) { + const getNK = Object.fromEntries(design.materials.map(({ key, material }) => [key, makeGetNK(material)])); + const index = (key) => getNK[key](lam); + return { + n0: index(design.incidentKey), + ns: index(design.substrateKey), + layers: design.layers.map(layer => ({ n: index(layer.materialKey), d: layer.thickness })), + }; +} + +function reflectionPhaseDeg(stack, lam, angle, pol) { + const layers = stack.layers.map(layer => ({ nJet: jetConstant(...layer.n), d: layer.d })); + return tmmPhaseDispersion(lam, angle, pol, jetConstant(...stack.n0), jetConstant(...stack.ns), layers).r.phaseDeg; +} + +// Rows: [angle, λ, Rs, Rp, Ts, Tp], and for Example 3 the phases [Rs°, Rp°]. +function checkTable(name, text, rows, phases = null) { + const stack = parseCodevSeq(text); + const design = codevStackToDesign(stack, { sourceName: name }); + rows.forEach(([angle, lam, Rs, Rp, Ts, Tp], i) => { + const at = stackAt(design, lam); + const s = tmm(lam, angle, 's', at.n0, at.ns, at.layers); + const p = tmm(lam, angle, 'p', at.n0, at.ns, at.layers); + const where = `${name}, ${angle}°, ${lam} nm`; + near(s.R, Rs, RT_TOLERANCE, `${where}: Rs`); + near(p.R, Rp, RT_TOLERANCE, `${where}: Rp`); + near(s.T, Ts, RT_TOLERANCE, `${where}: Ts`); + near(p.T, Tp, RT_TOLERANCE, `${where}: Tp`); + if (!phases) return; + const [phaseS, phaseP] = phases[i]; + near(wrapDeg(reflectionPhaseDeg(at, lam, angle, 's') - phaseS), 0, PHASE_TOLERANCE_DEG, `${where}: s reflection phase`); + near(wrapDeg(reflectionPhaseDeg(at, lam, angle, 'p') + 180 - phaseP), 0, PHASE_TOLERANCE_DEG, `${where}: p reflection phase`); + }); +} + +// CODE V Multilayer Design Reference Manual, Description of Output, Example 1 +checkTable('Example 1', `MUL ! Enter an Al mirror overcoated +MDA ! with an (HLHL) stack + MIC ! Define the material properties of Al + MWL 450 500 550 600 650 700 750 + 'Al' 0.49 0.62 0.76 0.97 1.24 1.55 1.80 + EXT 'Al' 5.47 6.08 6.90 7.26 7.79 8.31 8.62 + END + TIT 'Overcoated Al mirror' + INC 1.00 + COA 0.25 100 2.35 ! ZnS + COA 0.25 100 1.38 ! MgF2 + COA 0.25 100 2.35 + COA 0.25 100 1.38 + COA 5.00 100 'Al' + SUB 1.62 ! A substrate is needed + WL 450 500 550 600 650 700 750 + REF 600 +MAN + MPL + RFL + AVE + SPA 450 750 + GO`, [ + [0, 450, 0.752988, 0.752988, 0, 0], + [0, 500, 0.971256, 0.971256, 0, 0], + [0, 550, 0.989925, 0.989925, 0, 0], + [0, 600, 0.991439, 0.991439, 0, 0], + [0, 650, 0.990193, 0.990193, 0, 0], + [0, 700, 0.986307, 0.986307, 0, 0], + [0, 750, 0.978234, 0.978234, 0, 0], +]); + +// CODE V Multilayer Design Reference Manual, Description of Output, Example 2 +checkTable('Example 2', `MUL + MDA ! Choose the Data entry sub-option +TIT '7 Layer Quarter-wave Stack' +COA 0.25 0 2.3 ! First layer +Group 'A' ! Define a 2-layer grouping AND + COA 0.25 0 1.38 ! enter this group + COA 0.25 0 2.3 +END ! End of Group definition +COA 'A' ! Repeat the group +COA 'A' ! 2 more times. +SUB 1.52 ! Define the index of the substrate +WLG 400 750 50 +REF 550 +ANG 0 30 ! 2 angles of incidence for analysis +STL 1 2 ! Specify line styles + MAN ! List the performance data for the stack +GO + MPL ! Plot the reflectance of the stack +RFL +GO`, [ + [0, 400, 0.253488, 0.253488, 0.746511, 0.746511], + [0, 450, 0.528278, 0.528278, 0.471722, 0.471722], + [0, 500, 0.920484, 0.920484, 0.079516, 0.079516], + [0, 550, 0.947786, 0.947786, 0.052214, 0.052214], + [0, 600, 0.930235, 0.930235, 0.069765, 0.069765], + [0, 650, 0.853855, 0.853855, 0.146145, 0.146145], + [0, 700, 0.593479, 0.593479, 0.406521, 0.406521], + [0, 750, 0.093620, 0.093620, 0.906380, 0.906380], + [30, 400, 0.058521, 0.067864, 0.941479, 0.932136], + [30, 450, 0.881649, 0.736083, 0.118351, 0.263917], + [30, 500, 0.961738, 0.911747, 0.038263, 0.088253], + [30, 550, 0.962736, 0.913986, 0.037264, 0.086014], + [30, 600, 0.933938, 0.847887, 0.066062, 0.152113], + [30, 650, 0.820570, 0.616881, 0.179430, 0.383119], + [30, 700, 0.402628, 0.135968, 0.597373, 0.864032], + [30, 750, 0.025979, 0.035899, 0.974021, 0.964101], +]); + +// CODE V Multilayer Design Reference Manual, Description of Output, Example 3 +checkTable('Example 3', `MUL + MDA + TIT 'SiO2 Overcoated Al Mirror For Visible' + COA .5 100 1.45 ! SiO2 + COA 10 100 'AL' ! Al - 10 waves thick + INC 1.0 ! Incident medium - Air + SUB 1.517 ! Substrate (index doesn't matter) + MIC + MWL 350 400 450 550 650 700 750 + 'AL' .31 .40 .51 .83 1.3 1.55 1.8 + EXT 'AL' 4.24 4.86 5.47 6.69 7.79 8.31 8.62 + END + WL 350 450 550 650 750 + REF 550 + ANG 0 + MAN ! List performance + MPL ! Plot performance + RFL + AVE + SPA 350 750 + SAV SIO2_Overcoated_Al ! Save to a .MUL file + MEX`, [ + [0, 350, 0.905576, 0.905576, 0, 0], + [0, 450, 0.907364, 0.907364, 0, 0], + [0, 550, 0.930984, 0.930984, 0, 0], + [0, 650, 0.918779, 0.918779, 0, 0], + [0, 750, 0.892240, 0.892240, 0, 0], +], [ + [-83.6113, 96.3887], + [91.4632, -88.5368], + [163.2458, -16.7542], + [-155.5930, 24.4070], + [-121.9598, 58.0402], +]); + +console.log(`codev_coating_examples: ${checks} checks passed`); diff --git a/tests/codev_coating_listings.mjs b/tests/codev_coating_listings.mjs new file mode 100644 index 00000000..3ff0148d --- /dev/null +++ b/tests/codev_coating_listings.mjs @@ -0,0 +1,122 @@ +/** + * CODE V 11.2 against TFStudio, from the numbers CODE V printed. + * + * tests/reference/codev/ holds .seq files TFStudio wrote and CODE V 11.2 ran, + * the .mul files CODE V saved from them (read in codev_coating_samples.mjs), + * and codev_11_2.json the tables CODE V printed for them, frozen from the + * listings of the owner's runs by tests/reference/gen_codev_coating.mjs. + * + * 1. Each .seq read with parseCodevSeq, turned into TFStudio materials and + * layers with codevStackToDesign, and computed with the TFStudio + * transfer-matrix kernel, against MAN: Rs, Rp, Ts, Tp at every angle and + * wavelength. mic_narrow analyses past both ends of its MIC table. + * 2. micIndexAt and micExtinctionAt against the n and k MPR printed for + * every MIC material: three 21-point tables at 41 wavelengths, the + * 3-point table of mic_narrow, probe tables of 2 to 5 points that differ + * in n and k, some sampled past both ends, and the Ta2O5 table of tfs_ar4 + * between its points (TA). + * 3. The rows codevStackToDesign writes for a MIC material follow CODE V's n + * between them to half a unit in the sixth decimal. + * + * Run: node tests/codev_coating_listings.mjs + */ +import fs from 'node:fs'; +import path from 'node:path'; +import assert from 'node:assert/strict'; +import { fileURLToPath } from 'node:url'; +import { parseCodevSeq, codevStackToDesign } from '../src/utils/io/codevCoatingFile.js'; +import { micCurve, micExtinctionAt, micIndexAt } from '../src/utils/io/codevCoating/micIndex.js'; +import { makeGetNK } from '../src/utils/materials/catalogManager/dispersion.js'; +import { tmm } from '../src/tmmcore.js'; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const FIXTURES = path.join(HERE, 'reference', 'codev'); +const { data } = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'codev_11_2.json'), 'utf8')); + +let checks = 0; +const worst = new Map(); +const near = (actual, expected, tolerance, group, message) => { + checks++; + const miss = Math.abs(actual - expected); + if (!(miss <= (worst.get(group) ?? 0))) worst.set(group, miss); + assert.ok(miss <= tolerance, `${message}: ${actual} vs ${expected}`); +}; + +// ── 1. R and T against MAN ─────────────────────────────────────────────────── +// Half a unit in the sixth decimal CODE V prints, and as much again for the +// single precision its numbers are held in (its .mul files hold float32 +// values). tfs_hr45 misses most on the edge of its reflection band at +// 510 nm and 45°, where Rs changes by 0.12 per nm: its worst, 4.6e-6, is that +// edge moved by 4e-5 nm, less than one single-precision step of the +// wavelength there (510 nm in µm, 24 bits: 6e-5 nm). +const RT_TOLERANCE = 1e-6; +const RT_EDGE_TOLERANCE = { tfs_hr45: 5e-6 }; + +function stackAt(design, getNK, lam) { + const index = (key) => getNK[key](lam); + return { + n0: index(design.incidentKey), + ns: index(design.substrateKey), + layers: design.layers.map(layer => ({ n: index(layer.materialKey), d: layer.thickness })), + }; +} + +for (const [name, rows] of Object.entries(data.man)) { + const stack = parseCodevSeq(fs.readFileSync(path.join(FIXTURES, `${name}.seq`), 'utf8')); + const design = codevStackToDesign(stack, { sourceName: `${name}.seq` }); + const getNK = Object.fromEntries(design.materials.map(({ key, material }) => [key, makeGetNK(material)])); + for (const [angle, lam, Rs, Rp, Ts, Tp] of rows) { + const at = stackAt(design, getNK, lam); + const s = tmm(lam, angle, 's', at.n0, at.ns, at.layers); + const p = tmm(lam, angle, 'p', at.n0, at.ns, at.layers); + const where = `${name}, ${angle}°, ${lam} nm`; + const tolerance = RT_EDGE_TOLERANCE[name] ?? RT_TOLERANCE; + near(s.R, Rs, tolerance, name, `${where}: Rs`); + near(p.R, Rp, tolerance, name, `${where}: Rp`); + near(s.T, Ts, tolerance, name, `${where}: Ts`); + near(p.T, Tp, tolerance, name, `${where}: Tp`); + } +} + +// ── 2. n and k against MPR ─────────────────────────────────────────────────── +// Half a unit in the sixth decimal printed, and one single-precision step of +// an n near 3 (2.4e-7). Two n are not reproduced, see micIndex.js. F5A falls +// ever faster towards long wavelengths, and CODE V's fit of it misses its own +// MWL points (n 1.93 at 600 nm printed as 1.929998, 1.75 at 700 nm as +// 1.750006); at 750 nm, nearer the pole of the curve, the two curves are +// 1.85e-5 apart. TA at 510 nm lies between three points with a flat step, +// which have no Hartmann curve: CODE V prints 2.156753, the parabola through +// them gives 2.155834. +const NK_TOLERANCE = 7.5e-7; +const N_NOT_REPRODUCED = { F5A: 2e-5, 'TA 510': 1e-3 }; +for (const { run, label, table, wavelengthsNm, n, k } of data.mic) { + wavelengthsNm.forEach((lam, i) => { + const tolerance = N_NOT_REPRODUCED[`${run} ${lam}`] ?? N_NOT_REPRODUCED[run] ?? NK_TOLERANCE; + near(micIndexAt(table, lam), n[i], tolerance, `n ${run}`, `${run} '${label}' n at ${lam} nm`); + near(micExtinctionAt(table, lam), k[i], NK_TOLERANCE, `k ${run}`, `${run} '${label}' k at ${lam} nm`); + }); +} + +// ── 3. The rows of an imported MIC material ────────────────────────────────── +// TFStudio reads them with straight lines; checked at seven points between +// each two rows. +for (const name of Object.keys(data.man)) { + const stack = parseCodevSeq(fs.readFileSync(path.join(FIXTURES, `${name}.seq`), 'utf8')); + const design = codevStackToDesign(stack, { sourceName: `${name}.seq` }); + for (const { key, material } of design.materials.filter(({ key: k }) => k.startsWith('mic:'))) { + const curve = micCurve(stack.mic[key.slice(4)]); + const getNK = makeGetNK(material); + const rows = material.tabData; + let miss = 0; + for (let i = 1; i < rows.length; i++) { + for (let s = 1; s < 8; s++) { + const lam = rows[i - 1][0] + (rows[i][0] - rows[i - 1][0]) * s / 8; + miss = Math.max(miss, Math.abs(getNK(lam)[0] - curve.n(lam)), Math.abs(getNK(lam)[1] - curve.k(lam))); + } + } + near(miss, 0, 5e-7 + 1e-12, 'rows', `${name} '${material.name}' between its ${rows.length} rows`); + } +} + +console.log(` worst miss: ${[...worst].map(([group, miss]) => `${group} ${miss.toExponential(1)}`).join(', ')}`); +console.log(`codev_coating_listings: ${checks} checks passed`); diff --git a/tests/codev_coating_parse.mjs b/tests/codev_coating_parse.mjs new file mode 100644 index 00000000..7ceb051a --- /dev/null +++ b/tests/codev_coating_parse.mjs @@ -0,0 +1,327 @@ +/** + * CODE V MUL coating reader: command syntax, the MDA commands, the .mul + * layout, and the conversion to TFStudio materials and layers. + * + * 1. Syntax: comments, ";", "&" continuation, double-quoted strings, command + * words read by their first three letters, any case. + * 2. MDA: PHT Y and N, REF and ANG and INC defaults, WLG as CODE V builds + * it, GRO replay, MIC with MWL changing between materials and EXT + * defaulting to 0, a label used before its MIC, commands of other + * sub-options passed over. + * 3. Warnings and errors, each with its values: among them the 21 values + * one WL, MWL, 'label' or EXT command keeps, and Essential Macleod's + * export with decimal commas. + * 4. n between MIC points as CODE V computes it, against the n CODE V stored + * in the .mul files of its sample coatings. + * 5. A .mul written in the layout the samples show, read back; a format + * number the reader does not know. + * 6. codevStackToDesign: one material per MIC label and per distinct index. + * + * Run: node tests/codev_coating_parse.mjs + */ +import assert from 'node:assert/strict'; +import { + parseCodevSeq, parseCodevMul, codevStackToDesign, CodevParseError, +} from '../src/utils/io/codevCoatingFile.js'; +import { micIndexAt } from '../src/utils/io/codevCoating/micIndex.js'; +import { fortranReal } from '../src/utils/io/codevCoating/mulReader.js'; + +let checks = 0; +const near = (actual, expected, tolerance, message) => { + checks++; + assert.ok(Math.abs(actual - expected) <= tolerance, `${message}: ${actual} vs ${expected}`); +}; +const equal = (actual, expected, message) => { checks++; assert.deepEqual(actual, expected, message); }; +const fails = (text, kind, detail, message) => { + checks++; + assert.throws(() => parseCodevSeq(text), (err) => { + assert.ok(err instanceof CodevParseError, `${message}: a CodevParseError`); + assert.equal(err.kind, kind, message); + if (detail) assert.deepEqual(err.detail, detail, message); + return true; + }); +}; +const seq = (...lines) => lines.join('\n'); + +// ── 1. Syntax ──────────────────────────────────────────────────────────────── +{ + const stack = parseCodevSeq(seq( + 'mul ! comment after a command', + 'Mda', + ' pht y; tit "Double quoted; with a semicolon"', + ' WL 400 450 &', + ' 500 550 & ! a comment after the mark', + ' 600', + " inc 1.0; coa 100 100 1.45; coa 50 0 2.1 0.01; sub 1.52", + 'Mex', + )); + equal(stack.title, 'Double quoted; with a semicolon', 'a quoted string keeps its ";" and its blanks'); + equal(stack.wavelengthsNm, [400, 450, 500, 550, 600], '"&" continues WL over three lines'); + equal(stack.layers.map(layer => layer.thicknessNm), [100, 50], 'PHT Y takes nm as entered'); + equal(stack.layers[1].index, { n: 2.1, k: 0.01 }, 'COA with n and k'); + equal(stack.layers.map(layer => layer.code), [100, 0], 'thickness codes as entered'); + equal(stack.warnings, [], 'no warning for a clean file'); +} + +// ── 2. MDA commands ────────────────────────────────────────────────────────── +// CODE V Multilayer Design Reference Manual, Description of Output, Example 2: +// a group defined with GRO enters its layers there and again at each COA 'A'. +{ + const stack = parseCodevSeq(seq( + 'MUL', 'MDA', "TIT '7 Layer Quarter-wave Stack'", + 'COA 0.25 0 2.3', "Group 'A'", 'COA 0.25 0 1.38', 'COA 0.25 0 2.3', 'END', + "COA 'A'", "COA 'a'", 'SUB 1.52', 'WLG 400 750 50', 'REF 550', 'ANG 0 30', 'STL 1 2', + 'MAN', 'GO', 'MPL', 'RFL', 'GO', + )); + equal(stack.layers.map(layer => layer.index.n), [2.3, 1.38, 2.3, 1.38, 2.3, 1.38, 2.3], 'seven layers, the group three times'); + near(stack.layers[0].thicknessNm, 0.25 * 550 / 2.3, 1e-12, 'PHT N by default: d = T·REF/n'); + near(stack.layers[1].thicknessNm, 0.25 * 550 / 1.38, 1e-12, 'PHT N for a group layer'); + stack.wavelengthsNm.forEach((lam, i) => near(lam, 400 + 50 * i, 1e-4, `WLG point ${i + 1} of 8, both ends included`)); + equal(stack.wavelengthsNm.length, 8, 'WLG 400 750 50 gives 8 wavelengths'); + equal(stack.anglesDeg, [0, 30], 'ANG'); + equal(stack.incident, { n: 1, k: 0 }, 'INC defaults to 1.00'); + equal(stack.warnings, [], 'STL, MAN, MPL, RFL and GO pass without a warning'); +} +{ + // CODE V's DWDM sample coating, WLG 1547 1557 .2: its .mul stores these + // wavelengths in µm, to 10 digits, a float32 running sum that ends + // 1.7e-3 nm above 1557 nm. + const stack = parseCodevSeq(seq('MUL', 'MDA', 'WLG 1547 1557 .2', 'COA .25 100 2.05', 'SUB 1.52')); + equal(stack.wavelengthsNm.length, 51, 'WLG 1547 1557 0.2 gives 51 wavelengths'); + const stored = { 0: 1.547000051, 1: 1.547200084, 25: 1.552000880, 49: 1.556801677, 50: 1.557001710 }; + for (const [i, um] of Object.entries(stored)) { + near(stack.wavelengthsNm[i] / 1000, um, 5e-10, `WLG point ${+i + 1} as CODE V builds it`); + } + equal(stack.refNm, stack.wavelengthsNm[25], 'REF defaults to the central wavelength'); + equal(stack.anglesDeg, [0], 'ANG defaults to 0'); +} +{ + const even = parseCodevSeq(seq('MUL', 'MDA', 'WL 400 500 600 700', 'COA 0.25 100 1.38', 'SUB 1.52')); + equal(even.refNm, 500, 'with an even WL count REF is the one left of centre'); +} +{ + // A label used before its MIC; MWL changes between materials; EXT absent is 0. + const stack = parseCodevSeq(seq( + 'MUL', 'MDA', 'PHT N', "COA 0.5 100 'H'", "COA 0.25 100 'L'", "SUB 'L'", + 'MIC', 'MWL 400 500 600', "'H' 2.40 2.30 2.25", "EXT 'H' 0.01 0.005 0.002", + 'MWL 450 550', "'L' 1.47 1.46", 'END', + 'WL 450 500 550', 'REF 500', + )); + equal(stack.mic.H, [[400, 2.4, 0.01], [500, 2.3, 0.005], [600, 2.25, 0.002]], 'MIC table of H as entered'); + equal(stack.mic.L, [[450, 1.47, 0], [550, 1.46, 0]], 'a new MWL for L; its k is 0 without EXT'); + near(stack.layers[0].thicknessNm, 0.5 * 500 / 2.3, 1e-12, 'PHT N of a MIC layer at an MWL point uses its n'); + near(stack.layers[1].thicknessNm, 0.25 * 500 / 1.465, 1e-12, 'between two MWL points n is the straight line'); + equal(stack.substrate, { label: 'L' }, 'SUB can name a MIC material'); +} + +// ── 3. Warnings and errors ─────────────────────────────────────────────────── +{ + const stack = parseCodevSeq(seq( + 'MUL', 'MDA', 'WL 550', 'XYZ 1 2', 'COA 0.25 100 1.38', 'SUB 1.52', 'MAN', 'GO', 'MCH', 'THI S1 0.3', 'MEX', + )); + equal(stack.warnings, [ + { kind: 'unknownCommand', command: 'XYZ', line: 4 }, + { kind: 'unknownCommand', command: 'MCH', line: 9 }, + ], 'an unknown MDA command and MCH, whose changes are not applied, are reported with their lines'); + equal(stack.layers.length, 1, 'MCH changes nothing'); +} +{ + const stack = parseCodevSeq(seq( + 'MUL', 'MDA', 'MIC', 'MWL 400 500', "'M' 2.0 1.9", 'END', "COA 0.25 100 'M'", "COA 0.5 100 'M'", 'SUB 1.5', 'WL 600', + )); + equal(stack.warnings, [{ kind: 'refOutsideTable', label: 'M' }], 'REF past a MIC table is reported once per material'); + // CODE V 11.2 held the end values of a 2-point table (MWL 450 650) from + // 380 to 425 nm and from 675 to 750 nm. + near(stack.layers[0].thicknessNm, 0.25 * 600 / 1.9, 1e-12, 'a 2-point table holds its end value there'); +} +{ + // CODE V 11.2 printed n 1.743750 at 700 nm for this table, with "Index for + // material X is being extrapolated": the parabola through its three points. + const stack = parseCodevSeq(seq( + 'MUL', 'MDA', 'MIC', 'MWL 450 550 650', "'X' 1.9 1.8 1.75", "EXT 'X' 0.1 0.2 0.25", 'END', + "COA 0.25 100 'X'", 'SUB 1.52', 'WL 700', + )); + equal(stack.warnings, [{ kind: 'refOutsideTable', label: 'X' }], 'REF past a 3-point table is reported'); + near(stack.layers[0].thicknessNm, 0.25 * 700 / 1.74375, 1e-9, 'and n there is extrapolated as CODE V does'); +} +{ + // CODE V 11.2 listing of a 41-value WL command: it kept 400 to 600 nm and + // printed "WARNING - Extra data ... ignored" for the rest. + const values = Array.from({ length: 41 }, (_, i) => 400 + 10 * i); + const stack = parseCodevSeq(seq( + 'MUL', 'MDA', `WL ${values.slice(0, 24).join(' ')} &`, ` ${values.slice(24).join(' ')} x`, + 'COA 0.25 100 1.38', 'SUB 1.52', + )); + equal(stack.wavelengthsNm, values.slice(0, 21), 'one WL command gives its first 21 values'); + equal(stack.refNm, 500, 'REF defaults to the centre of the 21 kept'); + equal(stack.warnings, [{ kind: 'extraValues', command: 'WL', line: 3, count: 21, limit: 21 }], + 'the values past the 21st are reported, a non-number among them included'); + const two = parseCodevSeq(seq('MUL', 'MDA', `WL ${values.slice(0, 21).join(' ')}`, `WL ${values.slice(21).join(' ')}`, + 'COA 0.25 100 1.38', 'SUB 1.52')); + equal(two.wavelengthsNm, values, 'two WL commands of 21 and 20 give all 41'); + equal(two.warnings, [], 'with no warning'); +} +{ + // Essential Macleod's CODE V export as it writes it on a machine set to a + // decimal-comma locale: lower-case commands, "pht yes", WLG, GO and SAVE + // with a double-quoted name inside MDA, every decimal a comma, MIC lines + // longer than the 21 values CODE V 11.2 reads of each, and SUB naming a + // MIC entry whose EXT is near 1e-8, which CODE V 11.2 took without a word. + const mwl = Array.from({ length: 23 }, (_, i) => (400 + 3.03 * i).toFixed(2)); + const line = (head, values) => `${head} ${values.join(' ').replace(/\./g, ',')}`; + const text = seq( + '! Exported from Essential Macleod v11.9.605, 07.10.2026 12:32:36', '', 'mul', 'mda', + "tit 'Opaque Al mirror'", 'wlg 400 460 20', 'ref 550', 'pht yes', '', 'mic', + line('mwl', mwl), + line("'Al'", mwl.map(lam => (0.4 + 0.002 * (lam - 400)).toFixed(6))), + line("EXT 'Al'", mwl.map(lam => (4.45 + 0.011 * (lam - 400)).toFixed(6))), + line("'Glass'", mwl.map(() => '1.53085008566268')), + line("EXT 'Glass'", mwl.map(() => '1.15110099230264E-08')), + 'end', '', "coa 1000 100 'Al'", "sub 'Glass'", 'inc 1.0', '', 'go', 'save "al_mirror.mul"', 'mex', + ); + const stack = parseCodevSeq(text); + const extra = (command, at) => ({ kind: 'extraValues', command, line: at, count: 2, limit: 21 }); + equal(stack.warnings, [{ kind: 'decimalComma', line: 11 }, extra('MWL', 11), extra('Al', 12), extra('EXT', 13), + extra('Glass', 14), extra('EXT', 15)], 'decimal commas noted once, at their first line; each MIC line keeps 21 values'); + equal(stack.mic.Al.length, 21, 'the Al table holds the 21 points CODE V reads'); + equal([stack.mic.Al[1], stack.mic.Al[20]], [[403.03, 0.40606, 4.48333], [460.6, 0.5212, 5.1166]], + 'a comma between digits is a decimal point'); + equal(stack.mic.Glass[0], [400, 1.53085008566268, 1.15110099230264e-8], 'in a number with an exponent too'); + equal([stack.title, stack.wavelengthsNm.length, stack.refNm, stack.layers[0].thicknessNm, stack.substrate], + ['Opaque Al mirror', 4, 550, 1000, { label: 'Glass' }], 'lower-case commands, "pht yes", GO and SAVE'); + const dots = parseCodevSeq(text.replace(/(\d),(\d)/g, '$1.$2')); + equal(dots.warnings, stack.warnings.slice(1), 'the same file with decimal points has no decimalComma'); + equal({ ...dots, warnings: [] }, { ...stack, warnings: [] }, 'and the same stack'); + + const titled = parseCodevSeq(seq('MUL', 'MDA', 'TIT Mirror 1,5', 'WL 550', 'COA 0,25 100 1,38', 'SUB 1,52')); + equal(titled.title, 'Mirror 1,5', 'a title keeps its commas'); + equal(titled.warnings, [{ kind: 'decimalComma', line: 5 }], 'and is not a value with a decimal comma'); + near(titled.layers[0].thicknessNm, 0.25 * 550 / 1.38, 1e-12, 'PHT N with decimal commas'); +} +fails('MUL\nMDA\nWL 400,500,600\nCOA 1 100 1.38\nSUB 1.5', 'badNumber', { line: 3, text: '400,500,600' }, + 'two commas in one value are not a decimal'); +fails('MUL\nMDA\nWL 400, 500\nCOA 1 100 1.38\nSUB 1.5', 'badNumber', { line: 3, text: '400,' }, + 'a comma after a value is not read as a list'); +fails('COA 0.25 100 1.38\nSUB 1.5', 'noStack', null, 'commands outside MUL are not a stack'); +fails('MUL\nMDA\nWL 550\nSUB 1.5', 'noStack', null, 'MDA without COA'); +fails("MUL\nMDA\nWL 550\nCOA 'B'\nSUB 1.5", 'unknownGroup', { label: 'B' }, 'COA names an undefined group'); +fails("MUL\nMDA\nWL 550\nCOA 1 100 'Q'\nSUB 1.5", 'unknownMaterial', { label: 'Q' }, 'COA names a label the MIC lacks'); +fails("MUL\nMDA\nMIC\nMWL 400\nEXT 'Q' 1\nEND", 'unknownMaterial', { label: 'Q' }, 'EXT before its material'); +fails('MUL\nMDA\nWL 550\nCOA 1x5 100 1.38\nSUB 1.5', 'badNumber', { line: 4, text: '1x5' }, 'a thickness that is not a number'); +fails('MUL\nMDA\nWL 550\nCOA 1 100\nSUB 1.5', 'badNumber', { line: 4, text: '' }, 'COA without an index'); +fails("MUL\nMDA\nMIC\nMWL 400 500\n'M' 2.0\nEND\nWL 550\nCOA 1 100 'M'\nSUB 1.5", 'micMismatch', { label: 'M', line: 5 }, 'fewer n than MWL points'); +fails('MUL\nMDA\nWL 550\nCOA 1 100 1.38', 'missingCommand', { command: 'SUB' }, 'no SUB'); +fails('MUL\nMDA\nCOA 1 100 1.38\nSUB 1.5', 'missingCommand', { command: 'WL' }, 'no WL or WLG'); + +// ── 4. n between MIC points, as CODE V computes it ─────────────────────────── +// The MIC tables of four CODE V sample coatings (REFL_SILVER_400nm_1000nm, +// REFL_GOLD_550nm_1000nm, REFL_AL_450nm_700nm, REFL_ALSIO_450nm_700nm; data +// from Palik) and the n CODE V stored for them at the analysis wavelengths. +// The k of each table is part of it: the metals, whose k is many times n, +// take the spline, and SiO, whose k/n stays below 0.1, the Hartmann curve. +// The stored n is a float32, and at an MWL point it is 3 float32 steps off +// the table (Al at 700 nm), so the tolerance is 4e-7, relative. +{ + const table = (mwl, n, k) => mwl.map((lam, i) => [lam, n[i], k[i]]); + const cases = [ + [table([400, 459.2, 495.9, 563.6, 652.6, 774.9, 885.6, 1033], [0.173, 0.144, 0.130, 0.120, 0.140, 0.143, 0.163, 0.226], + [1.950, 2.560, 2.880, 3.45, 4.150, 5.090, 5.950, 6.99]), + { 750: 0.1428141594, 1000: 0.2086311132 }, 'silver'], + [table([495.9, 563.6, 652.6, 774.9, 885.6, 1033], [0.916, 0.306, 0.166, 0.174, 0.210, 0.272], + [1.840, 2.88, 3.15, 4.86, 5.88, 7.07]), + { 550: 0.3852337301, 750: 0.1707715690, 1000: 0.2577517629 }, 'gold'], + [table([400, 500, 550, 600, 700, 750], [0.490, 0.769, 0.958, 1.200, 1.830, 2.40], + [4.860, 6.080, 6.690, 7.260, 8.310, 8.620]), + { 450: 0.6156912446, 700: 1.830000639 }, 'aluminium'], + [table([387.5, 442.8, 563.6, 619.9, 774.9], [2.144, 2.085, 1.994, 1.969, 1.929], + [0.171, 0.084, 0.022, 0.012, 0.002]), + { 450: 2.078301290, 550: 2.002352376, 700: 1.944684386 }, 'SiO'], + ]; + for (const [rows, stored, name] of cases) { + for (const [lam, n] of Object.entries(stored)) { + near(micIndexAt(rows, +lam), n, 4e-7 * n, `${name} n at ${lam} nm`); + } + } +} + +// ── 5. A .mul in the layout of CODE V's samples ────────────────────────────── +// Two layers on glass in air at 500 and 600 nm, one angle (10°), REF 500: an +// inline layer 0.25 waves of n 1.38 and 50 nm of MIC material 'Ti' (MWL 450, +// 650). Reals in G20.10, integers in I10, as CODE V writes them: a G field +// with an exponent fills 20 places, one without fills 16 and four blanks +// follow. The label words are 'Ti' in a REAL*8, printed without their E. +{ + const i10 = (...v) => v.map(x => String(x).padStart(10)).join(''); + const gField = (s) => (/E|\d[+-]\d/.test(s) ? s.padStart(20) : s.padStart(16).padEnd(20)); + const g20 = (...v) => v.map(x => gField(String(x))).join(''); + const word = '0.6011350410-153'; + const header45 = [2, 29, 3, 0, 2, 2, 0, 0, 0, 0, 7, 1, 0, 0, 1, 0, 4, 2, 13, 0, 0, 0, 25, 0, + 0, 0, 3, 0, 1, 0, 5, 3, 0, 1, 2, 8, 1, 0, 0, 0, 0, 0, 1, 3, 3]; + const settings = [1, 1, 1, 1, 1, 0, 0.5, 0, 0, 1, 1, 1, 1, 1, 0, 0.5, 0, 0.98, 0, 1, 0.1745329279, 0, 0, 0, 0]; + const rows = [ + i10(5) + g20('0.0000000000E+00'), + 'Two layers'.padEnd(80), + '07-Oct-26'.padStart(19).padEnd(80), + i10(0, 9600, 10233, 1010, 0), + ...[0, 8, 16, 24, 32, 40].map(at => i10(...header45.slice(at, at + 8))), + i10(0, 0, 0, 0, 0, 0, 0, 0), i10(0, 0, 0, 0), + i10(0, 100, 0, 100), + i10(-1, -1, 8, -1), + i10(2, 3, 0, 0, 0, 0, 0, 0) + '\n' + i10(0, 0, 0, 0, 0), + ...[0, 4, 8, 12, 16, 20, 24].map(at => g20(...settings.slice(at, at + 4))), + g20('0.5000000000', '0.6000000238'), + g20(1, 1), g20(1, 1), g20(0, 0), g20(0, 0), + g20(0, '0.2500000000', '0.2300000042', 0), + g20(0, 0, 0, 0), + g20(1, '1.379999995', '2.300000000', '1.519999981'), + i10(2) + 'Ti'.padEnd(10) + 'Ti'.padEnd(10), + g20('0.4500000000', '0.6500000000', word, '2.400000000'), + g20('2.200000000', '0.2000000000E-01', '0.1000000000E-01', word), + g20('2.300000000', '2.250000000', '0.1500000000E-01', '0.1250000000E-01'), + g20('0.0000000000E+00'), + i10(1) + 'Ti'.padEnd(10), + g20(1, word, '1.519999981', 1), + g20(1), + ]; + const stack = parseCodevMul(rows.join('\r\n')); + equal(stack.title, 'Two layers', 'title'); + equal([stack.refNm, stack.wavelengthsNm, stack.anglesDeg], [500, [500, 600], [10]], + 'REF, WL and ANG back as entered from float32 µm and radians'); + equal(stack.incident, { n: 1, k: 0 }, 'incident medium'); + equal(stack.substrate, { n: 1.52, k: 0 }, 'substrate n from its float32'); + equal(stack.layers.map(layer => [layer.code, layer.index]), [[100, { n: 1.38, k: 0 }], [0, { label: 'Ti' }]], 'codes and indices'); + near(stack.layers[0].thicknessNm, 0.25 * 500 / 1.38, 1e-9, 'waves back to nm with the inline n'); + near(stack.layers[1].thicknessNm, 50, 1e-5, 'waves back to nm with the n(REF) the file stores'); + equal(stack.mic, { Ti: [[450, 2.4, 0.02], [650, 2.2, 0.01]] }, 'MIC table in nm'); + checks++; + assert.throws(() => parseCodevMul('MUL\nMDA\n'), (err) => err.kind === 'notMul', 'command text is not a .mul'); + checks++; + assert.throws(() => parseCodevMul([i10(7) + g20('0.0000000000E+00'), ...rows.slice(1)].join('\r\n')), + (err) => err.kind === 'notMul', 'a format number other than 5 and 6, whose layout is not known'); + equal([fortranReal('0.6133314552-153'), fortranReal('1.5D+02'), fortranReal('-0.25E-01')], [0.6133314552e-153, 150, -0.025], + 'a Fortran real without its E, with D, and with E'); +} + +// ── 6. To TFStudio materials and layers ────────────────────────────────────── +{ + const stack = parseCodevSeq(seq( + 'MUL', 'MDA', 'PHT Y', 'WL 400 500', 'XYZ', + 'MIC', 'MWL 500 400', "'Ag' 0.05 0.17", "EXT 'Ag' 3.1 1.9", 'END', + 'COA 90 100 1.45', 'COA 60 7 2.2 0.001', 'COA 90 7 1.45', "COA 120 0 'Ag'", 'SUB 1.45', + )); + const design = codevStackToDesign(stack, { sourceName: 'mirror.seq' }); + const byKey = Object.fromEntries(design.materials.map(({ key, material }) => [key, material])); + equal(design.materials.length, 4, 'air, 1.45 (shared by two layers and SUB), 2.2 with k, Ag'); + equal(design.layers.map(layer => byKey[layer.materialKey].name), ['n = 1.45', 'n = 2.2, k = 0.001', 'n = 1.45', 'Ag'], + 'constants named after their value, MIC materials after their label, incident side first'); + equal(byKey[design.substrateKey].name, 'n = 1.45', 'the substrate shares the layer material of equal index'); + equal(byKey[design.incidentKey].name, 'n = 1', 'the incident medium'); + equal(design.layers.map(layer => [layer.thickness, layer.locked]), [[90, true], [60, false], [90, false], [120, false]], + 'thickness in nm; code 100 locks a layer'); + const ag = design.materials.find(({ key }) => key === design.layers[3].materialKey).material; + equal([ag.formulaNum, ag.tabData], [-1, [[400, 0.17, 1.9], [500, 0.05, 3.1]]], 'a MIC table becomes a table, sorted by wavelength'); + equal(design.warnings, [{ kind: 'unknownCommand', command: 'XYZ', line: 5 }, { kind: 'coupledLayers', count: 2 }], + "the stack's warnings, then the layers whose coupling code is dropped"); +} + +console.log(`codev_coating_parse: ${checks} checks passed`); diff --git a/tests/codev_coating_roundtrip.mjs b/tests/codev_coating_roundtrip.mjs new file mode 100644 index 00000000..241fea3f --- /dev/null +++ b/tests/codev_coating_roundtrip.mjs @@ -0,0 +1,159 @@ +/** + * CODE V coating writer, read back by the reader. + * + * 1. buildCodevSeq → parseCodevSeq → codevStackToDesign gives back the + * layers in order, their thicknesses and locked codes, the media, REF, + * the wavelengths and angles, and n and k of every material at the + * analysis wavelengths to the six decimals written. + * 2. MIC labels are unique, at most 6 characters; a constant material is + * written on its COA line; long lists go over lines ending in "&". + * 3. A material sampled at more than 21 wavelengths is cut to 21 MIC points, + * with a `resampled` warning. + * 4. An absorbing incident medium or substrate loses its k, with a + * `mediumAbsorbs` warning. + * 5. Each CodevExportError kind, with its values. + * + * Run: node tests/codev_coating_roundtrip.mjs + */ +import assert from 'node:assert/strict'; +import { + buildCodevSeq, CodevExportError, CODEV_LIMITS, parseCodevSeq, codevStackToDesign, +} from '../src/utils/io/codevCoatingFile.js'; +import { makeGetNK } from '../src/utils/materials/catalogManager/dispersion.js'; + +// n and k are written with six decimals: half a unit there, plus the binary +// rounding of a value that sits exactly on the half. +const WRITTEN = 5e-7 + 1e-12; + +let checks = 0; +const near = (actual, expected, tolerance, message) => { + checks++; + assert.ok(Math.abs(actual - expected) <= tolerance, `${message}: ${actual} vs ${expected}`); +}; +const equal = (actual, expected, message) => { checks++; assert.deepEqual(actual, expected, message); }; + +// n and k as functions of λ in nm. Cauchy-type dielectrics, a metal whose n +// and k both change, and two constants. +const MATERIALS = { + air: { name: 'Air', nk: () => [1, 0] }, + glass: { name: 'BK7 glass', nk: (l) => [1.5046 + 4200 / l ** 2, 0] }, + ta2o5: { name: 'Ta2O5', nk: (l) => [2.05 + 30000 / l ** 2, 2e-4 * (500 / l) ** 4] }, + silicaA: { name: 'SiO2 (Fused Silica)', nk: (l) => [1.45 + 3500 / l ** 2, 0] }, + silicaB: { name: 'SiO2 (sputtered)', nk: (l) => [1.46 + 3900 / l ** 2, 0] }, + silver: { name: 'Ag', nk: (l) => [0.05 + 1e-4 * l, 0.0065 * l] }, + mgf2: { name: 'MgF2 film', nk: () => [1.38, 0] }, + absorber: { name: 'Dark film', nk: () => [2, 0.5] }, +}; +const options = (overrides = {}) => ({ + title: 'Round trip', + saveName: 'round_trip', + incident: 'air', + substrate: 'glass', + wavelengthsNm: [400, 450, 500, 550, 600, 650, 700, 750, 800], + anglesDeg: [0, 45], + refNm: 550, + materialName: (id) => MATERIALS[id].name, + getNK: (id, lam) => MATERIALS[id].nk(lam), + layers: [ + { material: 'ta2o5', thickness: 61.25, locked: true }, + { material: 'silicaA', thickness: 94.8765 }, + { material: 'mgf2', thickness: 12.5 }, + { material: 'silicaB', thickness: 100 }, + { material: 'absorber', thickness: 3.1 }, + { material: 'silver', thickness: 120 }, + { material: 'ta2o5', thickness: 20, locked: true }, + ], + ...overrides, +}); +const exportError = (opts, kind, detail, message) => { + checks++; + assert.throws(() => buildCodevSeq(opts), (err) => { + assert.ok(err instanceof CodevExportError, `${message}: a CodevExportError`); + assert.equal(err.kind, kind, message); + assert.deepEqual(err.detail, detail, message); + return true; + }); +}; + +// ── 1 and 2. Round trip ────────────────────────────────────────────────────── +{ + const opts = options(); + const { text, warnings } = buildCodevSeq(opts); + equal(warnings, [], 'nothing to warn about'); + const stack = parseCodevSeq(text); + equal([stack.title, stack.refNm, stack.wavelengthsNm, stack.anglesDeg], [opts.title, 550, opts.wavelengthsNm, [0, 45]], + 'title, REF, WL over two lines joined by "&", ANG'); + equal(stack.warnings, [], 'the written file reads without a warning'); + const design = codevStackToDesign(stack, { sourceName: 'round_trip.seq' }); + equal(design.layers.length, opts.layers.length, 'every layer back'); + equal(design.layers.map(layer => layer.locked), opts.layers.map(layer => !!layer.locked), 'locked layers stay locked'); + opts.layers.forEach((layer, i) => near(design.layers[i].thickness, layer.thickness, 5e-5, `layer ${i + 1} thickness`)); + + const getNK = Object.fromEntries(design.materials.map(({ key, material }) => [key, makeGetNK(material)])); + const sameIndex = (key, id, what) => { + for (const lam of opts.wavelengthsNm) { + const [n, k] = getNK[key](lam), [n0, k0] = opts.getNK(id, lam); + near(n, n0, WRITTEN, `${what} n at ${lam} nm`); + near(k, k0, WRITTEN, `${what} k at ${lam} nm`); + } + }; + opts.layers.forEach((layer, i) => sameIndex(design.layers[i].materialKey, layer.material, `layer ${i + 1}`)); + sameIndex(design.incidentKey, 'air', 'incident medium'); + sameIndex(design.substrateKey, 'glass', 'substrate'); + + const labels = Object.keys(stack.mic); + equal(labels, ['Ta2O5', 'SiO2', 'SiO22', 'Ag', 'BK7gla'], 'one MIC entry per dispersive material, in the order the stack names them'); + checks++; + assert.ok(labels.every(label => label.length <= 6), 'labels hold at most 6 characters'); + equal(new Set(labels.map(label => label.toUpperCase())).size, labels.length, 'labels are unique'); + equal([stack.layers[2].index, stack.layers[4].index], [{ n: 1.38, k: 0 }, { n: 2, k: 0.5 }], + 'a constant material is written on its COA line, with its k when it absorbs'); + checks++; + assert.ok(text.split('\r\n').some(line => line.trimEnd().endsWith('&')), 'the file uses "&" continuation'); +} + +// ── 3. More than 21 points ─────────────────────────────────────────────────── +{ + const wavelengthsNm = Array.from({ length: 40 }, (_, i) => 400 + 10 * i); + const opts = options({ wavelengthsNm, layers: [{ material: 'ta2o5', thickness: 50 }], substrate: 'mgf2' }); + const { text, warnings } = buildCodevSeq(opts); + equal(warnings, [{ kind: 'resampled', material: 'Ta2O5', from: 40, to: CODEV_LIMITS.micPoints }], 'resampled warning'); + const rows = parseCodevSeq(text).mic.Ta2O5; + equal(rows.length, CODEV_LIMITS.micPoints, 'the MIC table holds 21 points'); + equal([rows[0][0], rows[rows.length - 1][0]], [400, 790], 'both ends are kept'); + for (const [lam, n, k] of rows) { + const [n0, k0] = opts.getNK('ta2o5', lam); + near(n, n0, WRITTEN, `kept point ${lam} nm n`); + near(k, k0, WRITTEN, `kept point ${lam} nm k`); + } +} + +// ── 4. Absorbing media ─────────────────────────────────────────────────────── +{ + const { text, warnings } = buildCodevSeq(options({ incident: 'absorber', substrate: 'silver' })); + equal(warnings, [ + { kind: 'mediumAbsorbs', role: 'incident', material: 'Dark film' }, + { kind: 'mediumAbsorbs', role: 'substrate', material: 'Ag' }, + ], 'both media reported'); + const stack = parseCodevSeq(text); + equal(stack.incident, { n: 2, k: 0 }, 'INC is written with n only'); + checks++; + assert.ok(stack.mic[stack.substrate.label].every(row => row[2] === 0), 'SUB names a MIC entry with no k'); + checks++; + assert.ok(stack.layers.some(layer => layer.index.label && stack.mic[layer.index.label].some(row => row[2] > 0)), + 'the layer of the same metal keeps its k in its own MIC entry'); +} + +// ── 5. Export errors ───────────────────────────────────────────────────────── +{ + const many = Array.from({ length: 1001 }, () => ({ material: 'mgf2', thickness: 10 })); + exportError(options({ layers: many }), 'layers', { count: 1001, limit: 1000 }, 'more than 1000 layers'); + const wavelengths = Array.from({ length: 101 }, (_, i) => 400 + i); + exportError(options({ wavelengthsNm: wavelengths }), 'wavelengths', { count: 101, limit: 100 }, 'more than 100 wavelengths'); + exportError(options({ wavelengthsNm: [] }), 'wavelengths', { count: 0, limit: 100 }, 'no wavelength'); + exportError(options({ anglesDeg: [0, 10, 20, 30, 40, 50] }), 'angles', { count: 6, limit: 5 }, 'more than 5 angles'); + const noIndex = options({ getNK: (id, lam) => (id === 'ta2o5' ? [NaN, 0] : MATERIALS[id].nk(lam)) }); + exportError(noIndex, 'noIndex', { material: 'Ta2O5' }, 'a material with no index at any analysis wavelength'); +} + +console.log(`codev_coating_roundtrip: ${checks} checks passed`); diff --git a/tests/codev_coating_samples.mjs b/tests/codev_coating_samples.mjs new file mode 100644 index 00000000..34d98c62 --- /dev/null +++ b/tests/codev_coating_samples.mjs @@ -0,0 +1,246 @@ +/** + * Files CODE V saved and the numbers CODE V printed, against the reader. + * + * 1. The seven .mul files CODE V 11.2 saved (format 6) from TFStudio + * exports, kept in tests/reference/codev beside their .seq. Each .mul + * read with parseCodevMul gives the stack parseCodevSeq reads from its + * .seq: title, REF, the wavelengths as the same float32 in µm, angles, + * media, every layer's code and index, its thickness in nm to float32 + * precision (the .mul stores it as a float32 in waves of REF), and the + * MIC tables. The n and k CODE V stored for each MIC material at the + * analysis wavelengths are those micIndexAt and micExtinctionAt give; + * the worst miss is printed. + * 2. Maintainer-only: the 15 sample coatings CODE V ships (format 5), each a + * .seq and the .mul CODE V saved from it, with the checks of part 1 but + * for the stored k (see storedIndexMatches), and + * the listing of a CODE V 11.2 run that entered each .seq afresh and + * printed MPR and MAN (codev_checks3.lis): each .seq read with + * parseCodevSeq, turned into TFStudio materials and layers with + * codevStackToDesign and computed with the TFStudio transfer-matrix + * kernel, against Rs, Rp, Ts, Tp at every angle and wavelength, and its + * MIC materials' n and k against MPR. The worst miss of each is printed. + * 3. Maintainer-only: Essential Macleod's CODE V export of an Al mirror, + * written with decimal commas, read as the same file with decimal points + * reads; that file against the MPR and MAN of the CODE V 11.2 run that + * entered it (codev_checks7.lis), as in part 2. + * + * The CODE V samples, Macleod's export and the listings are not in the + * repository; without them parts 2 and 3 print SKIP. + * + * Run: node tests/codev_coating_samples.mjs + */ +import fs from 'node:fs'; +import path from 'node:path'; +import assert from 'node:assert/strict'; +import { fileURLToPath } from 'node:url'; +import { parseCodevSeq, codevStackToDesign } from '../src/utils/io/codevCoatingFile.js'; +import { readCodevMul } from '../src/utils/io/codevCoating/parseMul.js'; +import { readMulRecord } from '../src/utils/io/codevCoating/mulRecord.js'; +import { micExtinctionAt, micIndexAt } from '../src/utils/io/codevCoating/micIndex.js'; +import { makeGetNK } from '../src/utils/materials/catalogManager/dispersion.js'; +import { tmm } from '../src/tmmcore.js'; +import { listingRun, manRows, mprSection } from './reference/codevListing.mjs'; + +const SAVED = path.join(path.dirname(fileURLToPath(import.meta.url)), 'reference', 'codev'); +const SAMPLES = 'X:\\TFStudio Dev\\reference\\code v\\coating'; +const LISTINGS = 'X:\\TFStudio Dev\\reference\\code v\\listings'; +const MACLEOD = 'X:\\TFStudio Dev\\reference\\code v\\macleod\\al_mirror.seq'; + +// Thickness: the .mul holds T = n·d/REF as a float32, so the thickness read +// back from it is up to 2^-24 off from storage alone; the samples' worst is +// 1.3 times that (silver, 8e-8). 2^-21 leaves room. +const THICKNESS = 2 ** -21; +// CODE V's own n and k at an MWL point are up to 3 float32 steps off the table. +const STORED_NK = 4e-7; +// Wavelengths, as float32 in µm. CODE V 11.2 stores a WL value λ as the +// float32 of λ/1000 (450 nm as 0.4499999881), and a WLG point as the reader +// builds it, so part 1 asks for the same float32. The format 5 samples store +// the float32 product of λ and the float32 of 0.001 (450 nm as +// 0.4500000179), one float32 step at most from it. +const FORMAT5_WAVELENGTH = 2 ** -23; +// Parts 2 and 3. R, T, n and k: half a unit in the sixth decimal printed, and +// as much again for the single precision CODE V's numbers are held in (one +// step of an n near 3 is 2.4e-7). The DWDM sample misses most on the edge of +// its passband at 1552.8 nm, where Rs changes by 2 per nm: 8.8e-5 there is +// that edge moved by 4.5e-5 nm, less than one single-precision step of the +// wavelength (1.2e-4 nm). +const RT = 1e-6; +const NK = 7.5e-7; +const RT_EDGE = { WDM_200Ghz_1547nm_1557nm: 1e-4 }; + +let checks = 0; +const near = (actual, expected, relative, message) => { + checks++; + assert.ok(Math.abs(actual - expected) <= relative * Math.abs(expected), `${message}: ${actual} vs ${expected}`); + return Math.abs(actual - expected); +}; +const equal = (actual, expected, message) => { checks++; assert.deepEqual(actual, expected, message); }; +const within = (actual, expected, tolerance, message) => { + checks++; + assert.ok(Math.abs(actual - expected) <= tolerance, `${message}: ${actual} vs ${expected}`); + return Math.abs(actual - expected); +}; +const readText = (folder, file) => fs.readFileSync(path.join(folder, file), 'utf8'); + +function sameStack(fromSeq, fromMul, { wavelengthsUm, wavelengthTolerance }, name) { + for (const field of ['title', 'refNm', 'anglesDeg', 'incident', 'substrate']) { + equal(fromMul[field], fromSeq[field], `${name}: ${field}`); + } + equal(fromSeq.wavelengthsNm.length, wavelengthsUm.length, `${name}: wavelength count`); + fromSeq.wavelengthsNm.forEach((lam, i) => near(Math.fround(lam / 1000), Math.fround(wavelengthsUm[i]), + wavelengthTolerance, `${name}: wavelength ${i + 1}, as float32 in µm`)); + equal(fromMul.layers.length, fromSeq.layers.length, `${name}: layer count`); + fromSeq.layers.forEach((layer, i) => { + const other = fromMul.layers[i]; + equal([other.code, other.index], [layer.code, layer.index], `${name}: layer ${i + 1} code and index`); + near(other.thicknessNm, layer.thicknessNm, THICKNESS, `${name}: layer ${i + 1} thickness`); + }); + equal(Object.keys(fromMul.mic), Object.keys(fromSeq.mic), `${name}: MIC labels`); + for (const [label, rows] of Object.entries(fromSeq.mic)) { + equal(fromMul.mic[label].length, rows.length, `${name}: MIC ${label} row count`); + rows.forEach((row, r) => row.forEach((value, c) => { + near(fromMul.mic[label][r][c], value, 1e-12, `${name}: MIC ${label} row ${r + 1}`); + })); + } +} + +// The n and k CODE V stored at the analysis wavelengths, which it computed at +// the float32 wavelengths it stores. The k of the format 5 samples follows +// another rule between MWL points than the k CODE V 11.2 prints for the same +// tables (SiO at 450 nm: 0.0729 stored, 0.080305 printed by CODE V 11.2, which +// micExtinctionAt gives), so there only n is checked. Returns the worst miss. +function storedIndexMatches(stack, sampled, { wavelengthsUm, withK }, name) { + let worst = 0; + for (const [label, { n, k }] of Object.entries(sampled)) { + wavelengthsUm.forEach((um, i) => { + const lam = um * 1000; + worst = Math.max(worst, near(micIndexAt(stack.mic[label], lam), n[i], STORED_NK, `${name}: ${label} n at ${lam} nm`)); + if (withK) worst = Math.max(worst, near(micExtinctionAt(stack.mic[label], lam), k[i], STORED_NK, `${name}: ${label} k at ${lam} nm`)); + }); + } + return worst; +} + +// A .seq and the .mul CODE V saved from it: the same stack, and the n and k +// CODE V stored. Returns the stack read from the .seq and the worst n, k miss. +function checkSaved(seqText, mulText, format5, name) { + const fromSeq = parseCodevSeq(seqText); + const { stack: fromMul, sampled } = readCodevMul(mulText); + const { wavelengthsUm } = readMulRecord(mulText); + sameStack(fromSeq, fromMul, { wavelengthsUm, wavelengthTolerance: format5 ? FORMAT5_WAVELENGTH : 0 }, name); + const worst = storedIndexMatches(fromMul, sampled, { wavelengthsUm, withK: !format5 }, name); + const design = codevStackToDesign(fromMul, { sourceName: name }); + equal(design.layers.length, fromMul.layers.length, `${name}: converts`); + return { fromSeq, worst }; +} + +// R and T against MAN, and n and k against MPR, of one CODE V 11.2 run of +// `stack`. `nTolerance` gives a material's n tolerance where it is not NK. +// Returns the worst misses as text, a material with its own tolerance apart. +function againstRun(stack, lines, name, { rt = RT, nTolerance = {} } = {}) { + const design = codevStackToDesign(stack, { sourceName: name }); + const getNK = Object.fromEntries(design.materials.map(({ key, material }) => [key, makeGetNK(material)])); + const wavelengths = stack.wavelengthsNm; + let worstRT = 0; + manRows(lines).forEach(([angle, printedNm, Rs, Rp, Ts, Tp], row) => { + const lam = wavelengths[row % wavelengths.length]; + equal(lam.toFixed(1), printedNm.toFixed(1), `${name}: MAN row ${row + 1} wavelength`); + const index = (key) => getNK[key](lam); + const layers = design.layers.map(layer => ({ n: index(layer.materialKey), d: layer.thickness })); + const s = tmm(lam, angle, 's', index(design.incidentKey), index(design.substrateKey), layers); + const p = tmm(lam, angle, 'p', index(design.incidentKey), index(design.substrateKey), layers); + const where = `${name}, ${angle}°, ${printedNm} nm`; + worstRT = Math.max(worstRT, within(s.R, Rs, rt, `${where}: Rs`), within(p.R, Rp, rt, `${where}: Rp`), + within(s.T, Ts, rt, `${where}: Ts`), within(p.T, Tp, rt, `${where}: Tp`)); + }); + const n = mprSection(lines, 'REFRACTIVE INDICES - DISPERSIVE MATERIALS'); + const k = mprSection(lines, 'EXTINCTION COEFFICIENTS - DISPERSIVE MATERIALS'); + equal(Object.keys(n), Object.keys(stack.mic), `${name}: MPR prints every MIC material`); + let text = `R, T ${worstRT.toExponential(1)}`; + let worstNK = null; + for (const [label, { wl, values }] of Object.entries(n)) { + const misses = wl.map((printedNm, i) => { + const lam = wavelengths[i]; + equal(lam.toFixed(2), printedNm.toFixed(2), `${name}: MPR wavelength ${i + 1}`); + return Math.max( + within(micIndexAt(stack.mic[label], lam), values[i], nTolerance[label] ?? NK, `${name}: ${label} n at ${lam} nm`), + within(micExtinctionAt(stack.mic[label], lam), k[label]?.values[i] ?? 0, NK, `${name}: ${label} k at ${lam} nm`)); + }); + if (label in nTolerance) text += `, ${label} n, k ${Math.max(...misses).toExponential(1)}`; + else worstNK = Math.max(worstNK ?? 0, ...misses); + } + return worstNK === null ? text : `${text}, n, k ${worstNK.toExponential(1)}`; +} + +// ── 1. TFStudio exports saved by CODE V 11.2 ───────────────────────────────── +{ + const names = fs.readdirSync(SAVED).filter(file => /\.mul$/.test(file)).map(file => file.slice(0, -4)); + equal(names.length, 7, 'seven .mul files saved by CODE V 11.2'); + for (const name of names) { + const { fromSeq, worst } = checkSaved(readText(SAVED, `${name}.seq`), readText(SAVED, `${name}.mul`), false, name); + // tfs_cr41 writes its 41 wavelengths in one WL command, of which + // CODE V 11.2 kept 21 ("Extra data ... ignored"), and so does the reader. + equal(fromSeq.warnings.map(w => w.kind), name === 'tfs_cr41' ? ['extraValues'] : [], `${name}: warnings`); + console.log(` ${name}: stored n, k ${worst.toExponential(1)}`); + } +} + +// ── 2. CODE V's sample coatings ────────────────────────────────────────────── +if (fs.existsSync(SAMPLES)) { + const listingPath = path.join(LISTINGS, 'codev_checks3.lis'); + const listing = fs.existsSync(listingPath) ? fs.readFileSync(listingPath, 'utf8') : null; + if (!listing) console.log(`SKIP part 2 against CODE V 11.2: listing not found at ${listingPath}`); + const files = fs.readdirSync(SAMPLES); + const seqFiles = files.filter(file => /\.seq$/i.test(file)); + for (const seqFile of seqFiles) { + const base = seqFile.replace(/\.seq$/i, ''); + const mulFile = files.find(file => file.toLowerCase() === `${base}.mul`.toLowerCase()); + assert.ok(mulFile, `${base}: its .mul is there`); + const { fromSeq, worst } = checkSaved(readText(SAMPLES, seqFile), readText(SAMPLES, mulFile), true, base); + equal(fromSeq.warnings, [], `${base}: read without a warning`); + const run = listing ? `, ${againstRun(fromSeq, listingRun(listing, `CODE V> IN s_${base}`), base, + { rt: RT_EDGE[base] ?? RT })}` : ''; + console.log(` ${base}: stored n ${worst.toExponential(1)}${run}`); + } + equal(seqFiles.length, 15, 'all 15 samples were read'); +} else { + console.log(`SKIP part 2: CODE V sample coatings not found at ${SAMPLES}`); +} + +// ── 3. Essential Macleod's CODE V export ───────────────────────────────────── +// Its tables run from 400 to 700 nm in 100 points, of which CODE V reads the +// first 21, to 460.61 nm; the analysis runs to 700 nm. SiO2 and the glass end +// on three points in a straight line, which CODE V continues ("Index data is +// linear with wavelength"); Al takes the spline, whose end parabola runs on +// for 240 nm, about 80 times its 3.03 nm spacing, and its k is held. There +// the n of Al computed here falls short of CODE V's, by 3.0e-4 at 700 nm, and +// R misses by 5.0e-5; micIndex.js gives what is known of the cause. +const MACLEOD_AL_N = 3.1e-4; +const MACLEOD_RT = 5.1e-5; + +// The 21-value limit on each MIC line of Macleod's export, from line `from`. +const macleodExtraValues = (from) => ['MWL', 'SiO2', 'EXT', 'Al', 'EXT', 'Glass', 'EXT'] + .map((command, i) => ({ kind: 'extraValues', command, line: from + i, count: 79, limit: 21 })); + +function checkMacleod() { + const dotsPath = path.join(LISTINGS, 'macleod_dots.seq'); + const listingPath = path.join(LISTINGS, 'codev_checks7.lis'); + if (!fs.existsSync(MACLEOD) || !fs.existsSync(dotsPath)) { + console.log(`SKIP part 3: Essential Macleod's export not found at ${MACLEOD} or ${dotsPath}`); + return; + } + const comma = parseCodevSeq(fs.readFileSync(MACLEOD, 'utf8')); + const dots = parseCodevSeq(fs.readFileSync(dotsPath, 'utf8')); + equal(dots.warnings, macleodExtraValues(11), "Macleod: CODE V's 21 values kept on each MWL, n and EXT line, as in its run"); + equal(comma.warnings, [{ kind: 'decimalComma', line: 11 }, ...macleodExtraValues(11)], 'Macleod: decimal commas noted once'); + equal({ ...comma, warnings: [] }, { ...dots, warnings: [] }, 'Macleod: decimal commas read as points'); + if (!fs.existsSync(listingPath)) { + console.log(`SKIP part 3 against CODE V 11.2: listing not found at ${listingPath}`); + return; + } + const lines = listingRun(fs.readFileSync(listingPath, 'utf8'), 'CODE V> IN macleod_dots'); + console.log(` Macleod: ${againstRun(dots, lines, 'Macleod', { rt: MACLEOD_RT, nTolerance: { Al: MACLEOD_AL_N } })}`); +} +checkMacleod(); + +console.log(`codev_coating_samples: ${checks} checks passed`); diff --git a/tests/codev_coatings_window.mjs b/tests/codev_coatings_window.mjs new file mode 100644 index 00000000..d356666f --- /dev/null +++ b/tests/codev_coatings_window.mjs @@ -0,0 +1,307 @@ +/** + * CODE V Coatings window, Export tab: the .seq it writes for each side of a + * design, and the messages it shows. + * + * CODE V enters a stack from the incident medium to the substrate, and on a + * glass/air surface wants air as the incident medium and the glass as the + * substrate whichever face of the element it is. The front of a design is + * therefore written as stored (frontLayers run incident side first) and the + * back reversed (backLayers run substrate side first), with the exit medium as + * INC. Every material is resolved through the design, a locked layer is frozen + * (code 100), and what the writer reports comes out as the locale's sentence. + * An angle of incidence is taken anywhere from 0 up to, not including, 90 + * degrees, decimals included; any other entry leaves the angles as they were. + * + * Run: node tests/codev_coatings_window.mjs + */ +import assert from 'node:assert/strict'; +import { createRequire } from 'node:module'; +import { makeHookRuntime, importWithHookRuntime } from './_hookHarness.mjs'; + +const require = createRequire(import.meta.url); +globalThis.React = require('react'); +globalThis.window = globalThis; +globalThis.electronAPI = { saveCatalog: async () => ({ success: true }), deleteCatalog: async () => ({ success: true }) }; + +const cm = await import('../src/utils/materials/catalogManager.js'); +const { getLocale } = await import('../src/constants/locales/index.js'); +const { CodevExportError, CodevParseError, CODEV_LIMITS } = await import('../src/utils/io/codevCoatingFile.js'); +const { buildGrid } = await import('../src/utils/io/zemaxCoatingFile.js'); +const { analysisWavelengths, sideStack } = await import('../src/components/windows/dataExchange/codevCoatings/exportModel.js'); +const messages = await import('../src/components/windows/dataExchange/codevCoatings/messages.js'); +const runtime = makeHookRuntime(); +const { useGenerateAction, useSaveAction } = await importWithHookRuntime( + '../src/components/windows/dataExchange/codevCoatings/useExportActions.js', runtime); +const z = getLocale('en').codevCoatings; + +cm.initCatalogs({}); +cm.addCatalog({ + id: 'user_lab_00000001', name: 'Lab', source: 'user', + materials: { Water: { id: 'Water', name: 'Water', formulaNum: -1, tabData: [[300, 1.33, 0], [1000, 1.33, 0]] } }, +}); + +const design = { + id: 'd1', name: 'Two sides', + incidentMedium: 'user_lab_00000001:Water', exitMedium: 'builtin:Air', + substrate: { material: 'builtin:BK7', thickness: 1 }, + referenceWavelength: 550, + // Incident side first. + frontLayers: [ + { id: 'f1', material: 'builtin:TiO2', thickness: 100, locked: true }, + { id: 'f2', material: 'builtin:SiO2', thickness: 90, locked: false }, + ], + // Substrate side first. + backLayers: [ + { id: 'b1', material: 'builtin:MgF2', thickness: 50, locked: false }, + { id: 'b2', material: 'builtin:SiO2', thickness: 30, locked: true }, + ], +}; + +const BASE = { + side: 'front', title: 'Two sides', saveName: 'two_sides', + gStart: 400, gEnd: 700, gStep: 50, anglesDeg: [0, 30], refNm: 550, +}; + +function generate(d, overrides = {}) { + const out = { preview: null, warnings: null, flashes: [] }; + runtime.reset(); + const run = runtime.render(() => useGenerateAction({ + ...BASE, ...overrides, z, design: d, + flash: (type, message) => out.flashes.push([type, message]), + setExport: (preview, warnings) => { out.preview = preview; out.warnings = warnings; }, + })); + run(); + out.last = out.flashes.at(-1); + return out; +} + +const lines = (text) => text.split('\r\n'); +const command = (text, name) => lines(text).filter(line => line.startsWith(`${name} `)); +// The COA lines as [thickness, code, index text]. +const coa = (text) => command(text, 'COA').map(line => { + const [, thickness, code, index] = /^COA (\S+) (\S+) (.+)$/.exec(line); + return [Number(thickness), Number(code), index]; +}); + +// ── Front: as stored, the design's incident medium, the substrate ───────────── +{ + const { preview, flashes, last } = generate(design); + assert.equal(last[0], 'success', `front export goes through: ${last[1]}`); + assert.equal(last[1], z.generated(2, 0)); + assert.equal(flashes.length, 1); + assert.deepEqual(command(preview, 'INC'), ['INC 1.33'], 'INC is the incident medium, a constant n'); + const layers = coa(preview); + assert.deepEqual(layers.map(([d, code]) => [d, code]), [[100, 100], [90, 0]], + 'frontLayers in stored order, the locked one frozen'); + assert.match(layers[0][2], /^'TiO2/, 'the first COA is the incident-side layer'); + assert.match(layers[1][2], /^'SiO2/); + assert.match(command(preview, 'SUB')[0], /^SUB 'BK7/, 'SUB is the substrate, sampled into MIC'); + assert.deepEqual(command(preview, 'ANG'), ['ANG 0 30']); + assert.deepEqual(command(preview, 'REF'), ['REF 550']); + assert.deepEqual(command(preview, 'SAV'), ['SAV two_sides']); + assert.deepEqual(command(preview, 'TIT'), ["TIT 'Two sides'"]); + assert.equal(command(preview, 'WL')[0], 'WL 400 450 500 550 600 650 700'); + assert.equal(lines(preview).filter(line => line === 'PHT Y').length, 1, 'thicknesses are physical nm'); +} + +// ── Back: the exit medium as INC, backLayers reversed, the same substrate ───── +{ + const { preview, last } = generate(design, { side: 'back' }); + assert.equal(last[0], 'success', `back export goes through: ${last[1]}`); + assert.deepEqual(command(preview, 'INC'), ['INC 1'], 'INC is the exit medium (air)'); + const layers = coa(preview); + assert.deepEqual(layers.map(([d, code]) => [d, code]), [[30, 100], [50, 0]], + 'the exit-side layer first, the substrate-side layer last'); + assert.match(layers[0][2], /^'SiO2/); + assert.match(layers[1][2], /^'MgF2/); + assert.match(command(preview, 'SUB')[0], /^SUB 'BK7/); + assert.deepEqual(sideStack(design, 'back').layers.map(layer => layer.material), + ['builtin:SiO2', 'builtin:MgF2']); +} + +// A built-in stored bare, as synthesis inserts it, is the same MIC entry as +// its prefixed id: one label for both layers. +{ + const bare = { ...design, frontLayers: [ + { id: 'a', material: 'SiO2', thickness: 20 }, { id: 'b', material: 'builtin:SiO2', thickness: 40 }, + ] }; + const { preview, last } = generate(bare); + assert.equal(last[0], 'success', last[1]); + const [first, second] = coa(preview); + assert.equal(first[2], second[2], 'SiO2 bare and prefixed share one label'); +} + +// ── Warnings come out as the locale's sentences ─────────────────────────────── +{ + // An absorbing substrate: CODE V takes no k for SUB. + const metal = { ...design, substrate: { material: 'builtin:Ag', thickness: 1 } }; + const { warnings, last } = generate(metal); + const absorbs = warnings.find(w => w.kind === 'mediumAbsorbs'); + assert.ok(absorbs, `the k of the substrate is reported: ${JSON.stringify(warnings)}`); + assert.equal(absorbs.role, 'substrate'); + assert.equal(messages.warningText(z, absorbs), z.warnSubstrateAbsorbs(absorbs.material)); + assert.equal(last[1], z.generated(2, warnings.length)); +} +{ + // 31 wavelengths of a dispersive material: its MIC table is cut to 21. + const { warnings, last } = generate(design, { gStep: 10 }); + assert.equal(last[0], 'success', last[1]); + const resampled = warnings.find(w => w.kind === 'resampled'); + assert.ok(resampled, 'a dispersive material over 31 wavelengths is resampled'); + assert.equal(resampled.from, 31); + assert.equal(resampled.to, CODEV_LIMITS.micPoints); + assert.equal(messages.warningText(z, resampled), z.warnResampled(resampled.material, 31, 21)); + assert.equal(messages.warningText(z, { kind: 'unknownCommand', command: 'MAN', line: 12 }), z.warnUnknownCommand('MAN', 12)); + for (const command of ['WL', 'MWL', 'EXT']) { + assert.equal(messages.warningText(z, { kind: 'extraValues', command, line: 3, count: 20, limit: 21 }), + z.warnExtraValues(command, 3, 20, 21), `extra values on one ${command} command`); + } + assert.equal(messages.warningText(z, { kind: 'extraValues', command: 'SiO2', line: 9, count: 2, limit: 21 }), + z.warnExtraMicValues('SiO2', 9, 2, 21), 'extra values on a MIC material line name the material'); + assert.equal(messages.warningText(z, { kind: 'decimalComma', line: 5 }), z.warnDecimalComma(5)); + assert.equal(messages.warningText(z, { kind: 'refOutsideTable', label: 'Ag' }), z.warnRefOutsideTable('Ag')); + assert.equal(messages.warningText(z, { kind: 'coupledLayers', count: 4 }), z.warnCoupledLayers(4)); + assert.equal(messages.warningText(z, { kind: 'somethingNew' }), z.warnOther('somethingNew')); +} + +// ── Refusals: each limit of the MUL option, with its numbers ────────────────── +{ + const { preview, last } = generate(design, { gStep: 1 }); + assert.deepEqual(last, ['error', z.errTooManyWavelengths(301, CODEV_LIMITS.wavelengths)]); + assert.equal(preview, '', 'no preview is left to save'); +} +{ + const many = { ...design, frontLayers: Array.from({ length: 1001 }, (_, i) => ({ id: `m${i}`, material: 'builtin:SiO2', thickness: 10 })) }; + const { last } = generate(many); + assert.deepEqual(last, ['error', z.errTooManyLayers(1001, CODEV_LIMITS.layers)]); +} +{ + const { last } = generate(design, { anglesDeg: [0, 10, 20, 30, 40, 50] }); + assert.deepEqual(last, ['error', z.errTooManyAngles(6, CODEV_LIMITS.angles)]); +} +{ + const { last } = generate({ ...design, backLayers: [] }, { side: 'back' }); + assert.deepEqual(last, ['error', z.nothingToExportBack]); + const front = generate({ ...design, frontLayers: [{ id: 'z', material: 'builtin:SiO2', thickness: 0 }] }); + assert.deepEqual(front.last, ['error', z.nothingToExportFront], 'a zero-thickness layer is no layer'); +} + +// An id that resolves nowhere is refused with its name, never written as air. +{ + const broken = { ...design, frontLayers: [...design.frontLayers, { id: 'x', material: 'user_gone_12345678:X', thickness: 10 }] }; + const { preview, last } = generate(broken); + assert.deepEqual(last, ['error', z.exportUnresolved('user_gone_12345678:X')]); + assert.equal(preview, ''); +} + +// ── The wavelength count matches the grid that would be built ───────────────── +for (const [a, b, step] of [[400, 700, 50], [400, 800, 10], [400, 403, 0.03], [700, 400, 25], [300, 2500, 3], [400, 800, 4.04]]) { + const { count, wavelengthsNm } = analysisWavelengths(a, b, step); + assert.equal(count, buildGrid(a, b, step).length, `count for ${a} to ${b} step ${step}`); + if (wavelengthsNm) assert.deepEqual(wavelengthsNm, buildGrid(a, b, step)); + else assert.ok(count > CODEV_LIMITS.wavelengths, 'only a count past the limit leaves the list unbuilt'); +} + +// ── Error texts for every kind the reader and the writer raise ──────────────── +{ + const parse = (kind, detail) => messages.parseErrorText(z, new CodevParseError(kind, detail)); + assert.equal(parse('noStack'), z.errNoStack); + assert.equal(parse('unknownGroup', { label: 'a' }), z.errUnknownGroup('a')); + assert.equal(parse('unknownMaterial', { label: 'SILVER' }), z.errUnknownMaterial('SILVER')); + assert.equal(parse('badNumber', { line: 7, text: '1.2.3' }), z.errBadNumber(7, '1.2.3')); + assert.equal(parse('notMul'), z.errNotMul); + assert.equal(parse('missingCommand', { command: 'SUB' }), z.errMissingCommand('SUB')); + assert.equal(parse('micMismatch', { label: 'Ag', line: 9 }), z.errMicMismatch('Ag', 9)); + assert.equal(parse('brandNew'), z.errParse('brandNew')); + + const write = (kind, detail) => messages.exportErrorText(z, new CodevExportError(kind, detail)); + assert.equal(write('layers', { count: 1200, limit: 1000 }), z.errTooManyLayers(1200, 1000)); + assert.equal(write('wavelengths', { count: 0, limit: 100 }), z.errNoWavelengths); + assert.equal(write('angles', { count: 7, limit: 5 }), z.errTooManyAngles(7, 5)); + assert.equal(write('noIndex', { material: 'Ge' }), z.errNoIndex('Ge')); +} + +// ── Save offers the .seq under the name SAV gives the .mul ──────────────────── +{ + const flashes = []; + const calls = []; + runtime.reset(); + const save = runtime.render(() => useSaveAction({ + z, flash: (type, message) => flashes.push([type, message]), + preview: 'MUL\r\nMDA\r\nSAV two_sides\r\nMEX\r\n', + })); + window.electronAPI = { + codevSaveCoatingFile: async (text, name) => { calls.push([text, name]); return { success: true, filePath: 'C:\\out\\two_sides.seq' }; }, + }; + await save(); + assert.equal(calls[0][1], 'two_sides.seq'); + assert.deepEqual(flashes.at(-1), ['success', z.savedFile('C:\\out\\two_sides.seq')]); + + window.electronAPI = { codevSaveCoatingFile: async () => ({ success: false, canceled: true }) }; + await save(); + assert.equal(flashes.length, 1, 'a cancelled dialog says nothing'); + + window.electronAPI = { codevSaveCoatingFile: async () => ({ success: false, error: 'disk full' }) }; + await save(); + assert.deepEqual(flashes.at(-1), ['error', z.errSave('disk full')]); +} + +// ── The angle field: 0 up to, not including, 90 degrees ────────────────────── +// Its hooks run on a harness of their own, drawn with React's createElement. +const angleRuntime = makeHookRuntime(); +const realReact = globalThis.React; +globalThis.React = { ...realReact, ...angleRuntime.React }; +const { AnglesField, angleAccepted } = await import('../src/components/windows/dataExchange/codevCoatings/AnglesField.js'); +const { NumInput } = await import('../src/components/windows/analysis/chrome/controls.js'); +globalThis.React = realReact; +const theme = { field: '#111', text: '#eee', textDim: '#999', border: '#333', bg: '#000', panel: '#111', accent: '#39f' }; + +// The field drawn as React would draw it: each angle's input, and the key its +// NumInput is mounted under. +function drawAngles(anglesDeg, setAnglesDeg) { + const fields = []; + const expand = (node) => { + if (Array.isArray(node)) return node.map(expand); + if (!node || typeof node !== 'object') return node; + if (node.type === NumInput) fields.push({ key: node.key }); + if (typeof node.type === 'function') return expand(node.type(node.props)); + if (node.type === 'input') fields.at(-1).input = node; + return { ...node, props: { ...node.props, children: expand(node.props.children) } }; + }; + angleRuntime.render(() => expand(realReact.createElement(AnglesField, { c: theme, z, anglesDeg, setAnglesDeg }))); + return fields; +} + +// Type `text` into the first of the angles 10 and 30 and leave the field. +function typeAngle(text) { + const calls = []; + const set = (next) => calls.push(next); + angleRuntime.reset(); + drawAngles([10, 30], set)[0].input.props.onChange({ target: { value: text } }); + const before = drawAngles([10, 30], set)[0]; + before.input.props.onBlur(); + return { calls, keyBefore: before.key, keyAfter: drawAngles([10, 30], set)[0].key }; +} + +for (const [text, angle] of [['89.5', 89.5], ['89.999', 89.999], ['45.25', 45.25], ['0', 0], ['22,5', 22.5]]) { + const { calls, keyBefore, keyAfter } = typeAngle(text); + assert.deepEqual(calls, [[angle, 30]], `${text} is taken as typed`); + assert.equal(keyAfter, keyBefore, `${text}: the field stays as it is`); +} +for (const text of ['90', '90.5', '120', '-1']) { + const { calls, keyBefore, keyAfter } = typeAngle(text); + assert.deepEqual(calls, [], `${text} is refused`); + assert.notEqual(keyAfter, keyBefore, `${text}: the field is mounted afresh, showing the angle it holds`); +} +{ + const calls = []; + angleRuntime.reset(); + const [field] = drawAngles([89.5], (next) => calls.push(next)); + field.input.props.onKeyDown({ key: 'ArrowUp', preventDefault() {} }); + assert.deepEqual(calls, [], 'the arrow key does not step past 90 either'); +} +for (const [deg, taken] of [[0, true], [89.9999, true], [90, false], [-0.5, false], [NaN, false]]) { + assert.equal(angleAccepted(deg), taken, `an angle of ${deg} is ${taken ? 'taken' : 'refused'}`); +} + +console.log('PASS: codev_coatings_window'); diff --git a/tests/codev_coatings_window_import.mjs b/tests/codev_coatings_window_import.mjs new file mode 100644 index 00000000..0bc1b602 --- /dev/null +++ b/tests/codev_coatings_window_import.mjs @@ -0,0 +1,226 @@ +/** + * CODE V Coatings window, Import tab: opening a .seq or .mul file and putting + * its layers on the front or the back of the active design. + * + * The layers replace one coating, as one undo step, and nothing else of the + * design changes: its incident medium, exit medium and substrate stay, whatever + * INC and SUB the file has. The front takes the layers as the file lists them, + * incident side first; the back takes them reversed, since backLayers are kept + * substrate side first. The materials the layers use, MIC entries or constant + * indices, go into a user catalog named after the file, the way a Zemax + * COATING.DAT import registers its materials: the same file goes back into its + * own catalog, a material already there is used as it is (edits included), and + * another file of the same name gets a catalog of its own. With no design + * selected the import is refused and writes nothing. A reader error comes out + * as the locale's sentence. + * + * Run: node tests/codev_coatings_window_import.mjs + */ +import assert from 'node:assert/strict'; +import { createRequire } from 'node:module'; +import { makeHookRuntime, importWithHookRuntime } from './_hookHarness.mjs'; + +const require = createRequire(import.meta.url); +globalThis.React = require('react'); +globalThis.window = globalThis; +const persisted = { saveCatalog: async () => ({ success: true }), deleteCatalog: async () => ({ success: true }) }; +globalThis.electronAPI = persisted; + +const cm = await import('../src/utils/materials/catalogManager.js'); +const { getLocale } = await import('../src/constants/locales/index.js'); +const { parseCodevSeq } = await import('../src/utils/io/codevCoatingFile.js'); +const runtime = makeHookRuntime(); +const { useImportAction, useLoadAction } = await importWithHookRuntime( + '../src/components/windows/dataExchange/codevCoatings/useImportActions.js', runtime); +const z = getLocale('en').codevCoatings; + +cm.initCatalogs({}); + +const SEQ = [ + 'MUL', + 'MDA', + 'PHT Y', + "TIT 'AR on glass'", + 'WL 450 550 650', + 'REF 550', + 'ANG 0', + 'MIC', + ' MWL 450 550 650', + " 'H' 2.40 2.35 2.31", + " 'L' 1.47 1.46 1.455", + " 'G' 1.525 1.52 1.515", + 'END', + 'INC 1.0', + "COA 60 100 'H'", + "COA 95 0 'L'", + 'COA 12 0 2.0 0.01', + "SUB 'G'", + 'MEX', +].join('\r\n'); + +const FILE = 'C:\\coatings\\ar.seq'; + +function importOnce(stack, { filePath = FILE, fileName = 'ar.seq', side = 'front', hasActiveDesign = true } = {}) { + const events = []; + runtime.reset(); + const run = runtime.render(() => useImportAction({ + z, flash: (type, message) => events.push(['flash', type, message]), + stack, fileName, filePath, hasActiveDesign, + checkpoint: () => events.push(['checkpoint']), + updateDesign: (patch) => events.push(['update', patch]), + })); + run(side); + return events; +} + +const nAt = (id, nm) => cm.getMaterialById(id).getNK(nm); +const catalogOf = (id) => id.slice(0, id.indexOf(':')); +const fields = (layers, key) => layers.map(layer => layer[key]); + +// ── Front: the layers as listed, in one undo step, the media untouched ─────── +const stack = parseCodevSeq(SEQ); +assert.equal(stack.layers.length, 3); +const first = importOnce(stack); +assert.deepEqual(first[0], ['checkpoint'], 'one undo step, taken before the change'); +assert.equal(first[1][0], 'update'); +const patch = first[1][1]; +{ + assert.deepEqual(Object.keys(patch), ['frontLayers'], + 'only the front coating changes: incident medium, exit medium, substrate and back stay'); + assert.deepEqual(fields(patch.frontLayers, 'thickness'), fields(stack.layers, 'thicknessNm'), + 'thickness in nm, incident side first'); + assert.deepEqual(fields(patch.frontLayers, 'locked'), [true, false, false], 'code 100 is a locked layer'); + const [h, l, metal] = fields(patch.frontLayers, 'material'); + assert.ok(Math.abs(nAt(h, 550)[0] - 2.35) < 1e-9, 'H is the MIC entry, n at 550 nm'); + assert.ok(Math.abs(nAt(l, 450)[0] - 1.47) < 1e-9, 'L is the MIC entry, n at 450 nm'); + assert.ok(Math.abs(nAt(metal, 550)[0] - 2.0) < 1e-9 && Math.abs(nAt(metal, 550)[1] - 0.01) < 1e-9, + 'a constant n, k on the COA line is a material of its own'); + + const catId = catalogOf(h); + assert.match(catId, /^codev_ar_[0-9a-f]{8}$/, 'a catalog of the file, with an id no other computer has'); + const cat = cm.getCatalog(catId); + assert.equal(cat.name, 'CODE V ar'); + assert.equal(cat.source, 'user'); + assert.equal(cat.sourceFile, FILE); + for (const layer of patch.frontLayers) assert.equal(catalogOf(layer.material), catId, 'every layer material is in it'); + assert.deepEqual(Object.keys(cat.materials).map(id => `${catId}:${id}`).sort(), [h, l, metal].sort(), + 'and nothing else: neither INC 1.0 nor the MIC entry SUB names'); + const message = first.at(-1); + assert.equal(message[1], 'success'); + assert.ok(message[2].startsWith(z.importedFront(3, 'CODE V ar')), message[2]); +} + +// ── Back: the same layers reversed, since backLayers start at the substrate ── +{ + const events = importOnce(parseCodevSeq(SEQ), { side: 'back' }); + assert.deepEqual(events[0], ['checkpoint'], 'one undo step, taken before the change'); + const backPatch = events[1][1]; + assert.deepEqual(Object.keys(backPatch), ['backLayers'], 'only the back coating changes'); + assert.deepEqual(fields(backPatch.backLayers, 'thickness'), fields(stack.layers, 'thicknessNm').reverse(), + 'the layer the file lists last, at its substrate, is the first stored'); + assert.deepEqual(fields(backPatch.backLayers, 'locked'), [false, false, true]); + assert.deepEqual(fields(backPatch.backLayers, 'material'), fields(patch.frontLayers, 'material').reverse(), + 'the same materials, from the same catalog'); + assert.ok(events.at(-1)[2].startsWith(z.importedBack(3, 'CODE V ar')), events.at(-1)[2]); +} + +// A layer of the same index as INC brings that material in, as a layer material. +{ + const text = SEQ.replace('COA 12 0 2.0 0.01', 'COA 12 0 1.0'); + const events = importOnce(parseCodevSeq(text), { filePath: 'C:\\coatings\\gap.seq', fileName: 'gap.seq' }); + const gap = events[1][1].frontLayers[2].material; + assert.ok(Math.abs(nAt(gap, 550)[0] - 1.0) < 1e-9, 'n 1.0 on the COA line'); + assert.equal(Object.keys(cm.getCatalog(catalogOf(gap)).materials).length, 3, 'H, L and the n 1.0 layer'); +} + +// ── The same file again: its own catalog, the materials it holds, edits kept ── +{ + const [h] = patch.frontLayers.map(layer => layer.material); + const catId = catalogOf(h); + const before = Object.keys(cm.getCatalog(catId).materials).length; + const heldId = h.slice(catId.length + 1); + // The Material Editor saves an edit this way. + cm.saveUserMaterial(catId, { ...cm.getCatalog(catId).materials[heldId], tabData: [[450, 2.5, 0], [550, 2.45, 0], [650, 2.4, 0]] }); + + const again = importOnce(parseCodevSeq(SEQ))[1][1]; + assert.deepEqual(again.frontLayers.map(layer => layer.material), patch.frontLayers.map(layer => layer.material), + 'the layers use the materials the catalog already holds'); + assert.equal(Object.keys(cm.getCatalog(catId).materials).length, before, 'nothing is added twice'); + assert.ok(Math.abs(nAt(h, 550)[0] - 2.45) < 1e-9, 'an edit made in the Material Editor stays'); +} + +// The report adds what the conversion noted, not what the reader noted: that +// is in the window's notices already. A report with a note is a warning, which +// stays up rather than clearing itself. +{ + const text = SEQ.replace("COA 95 0 'L'", "COA 95 5 'L'").replace('ANG 0', 'ANG 0\r\nFOO 1'); + const coupled = parseCodevSeq(text); + assert.ok(coupled.warnings.some(w => w.kind === 'unknownCommand'), 'the reader noted FOO'); + const events = importOnce(coupled, { filePath: 'C:\\coatings\\coupled.seq', fileName: 'coupled.seq' }); + const [, type, message] = events.at(-1); + assert.equal(type, 'warning'); + assert.equal(message, `${z.importedFront(3, 'CODE V coupled')} ${z.warnCoupledLayers(1)}`); + assert.equal(events[1][1].frontLayers[1].locked, false, 'a coupled layer comes in free'); +} + +// Another file of the same name is another catalog. +{ + const other = importOnce(parseCodevSeq(SEQ), { filePath: 'D:\\vendor\\ar.seq' })[1][1]; + assert.notEqual(catalogOf(other.frontLayers[0].material), catalogOf(patch.frontLayers[0].material)); +} + +// ── With no design selected there is nothing to import into ────────────────── +// The window then has a placeholder design that nothing keeps. The import says +// so and writes nothing: no catalog, no undo step, no layers. +{ + const catalogs = () => cm.getCatalogs().map(cat => [cat.id, Object.keys(cat.materials).sort()]); + const before = catalogs(); + for (const side of ['front', 'back']) { + const events = importOnce(parseCodevSeq(SEQ), + { filePath: 'C:\\coatings\\never.seq', fileName: 'never.seq', side, hasActiveDesign: false }); + assert.deepEqual(events, [['flash', 'error', z.importNoDesign]], `${side}: refused, and only that`); + } + assert.deepEqual(catalogs(), before, 'no catalog is made or changed'); +} + +// ── Opening a file: by extension, with reader errors in the locale ──────────── +async function load(result) { + const events = []; + window.electronAPI = { ...persisted, codevPickCoatingFile: async () => result }; + runtime.reset(); + const run = runtime.render(() => useLoadAction({ + z, flash: (type, message) => events.push(['flash', type, message]), + clear: () => events.push(['clear']), + setLoading: (value) => events.push(['loading', value]), + setFile: (file) => events.push(['file', file]), + })); + await run(); + return events; +} +{ + const events = await load({ success: true, text: SEQ, fileName: 'ar.seq', filePath: FILE }); + assert.deepEqual(events.slice(0, 2), [['loading', true], ['clear']], 'opening a file takes the last report down'); + const file = events.find(e => e[0] === 'file')[1]; + assert.equal(file.fileName, 'ar.seq'); + assert.equal(file.filePath, FILE); + assert.equal(file.stack.layers.length, 3); + assert.deepEqual(events.find(e => e[0] === 'flash'), ['flash', 'success', z.loadedFile('ar.seq', 3)]); + assert.deepEqual(events.at(-1), ['loading', false]); +} +{ + const events = await load({ success: true, text: SEQ, fileName: 'ar.mul', filePath: 'C:\\ar.mul' }); + assert.deepEqual(events.find(e => e[0] === 'flash'), ['flash', 'error', z.errNotMul], 'a .mul is read as one'); + assert.ok(!events.some(e => e[0] === 'file'), 'and a file that fails leaves the one shown before'); +} +{ + const text = SEQ.replace("COA 95 0 'L'", "COA 'a'"); + const events = await load({ success: true, text, fileName: 'bad.seq', filePath: 'C:\\bad.seq' }); + assert.deepEqual(events.find(e => e[0] === 'flash'), ['flash', 'error', z.errUnknownGroup('a')]); +} +{ + const events = await load({ success: false, canceled: true }); + assert.ok(!events.some(e => e[0] === 'flash'), 'a cancelled dialog says nothing'); + assert.deepEqual(events.at(-1), ['loading', false]); +} + +console.log('PASS: codev_coatings_window_import'); diff --git a/tests/codev_coatings_window_library.mjs b/tests/codev_coatings_window_library.mjs new file mode 100644 index 00000000..d97b8359 --- /dev/null +++ b/tests/codev_coatings_window_library.mjs @@ -0,0 +1,256 @@ +/** + * Save to Coating Library from the CODE V Coatings window. + * + * The stack read from a file goes through the library's own Save Coating + * dialog and comes out as an entry with its layers in deposition order, + * substrate first, each material under the id the window's import gives it + * and carrying its record. The incident medium and the substrate are the + * file's INC and SUB: an index of n 1 with no k is built-in Air, any other + * index or MIC table the material the import makes of it, embedded. Saving + * writes no catalog. The dialog opens with the band of the analysis + * wavelengths and the first angle of ANG. The entry applies back to a design + * as the stack the import makes, on either side: on a computer that never + * imported the file it computes with the records it carries, and once the + * file has been imported it names the very materials the import put in the + * catalog. + * + * Run: node tests/codev_coatings_window_library.mjs + */ +import assert from 'node:assert/strict'; +import { loadApp, makeLocale, makeTheme, shimBrowserGlobals } from './_uiShim.mjs'; +import { makeHookRuntime } from './_hookHarness.mjs'; + +shimBrowserGlobals(); +await loadApp(); + +const saved = []; +window.electronAPI = new Proxy({ + saveCoating: async (record) => { saved.push(JSON.parse(JSON.stringify(record))); return { success: true }; }, +}, { get: (target, key) => target[key] || (() => Promise.resolve({ success: true })) }); + +// Each module runs its hooks on a harness of its own, and draws with React's +// own createElement. +const realReact = globalThis.React; +async function importOnHarness(specifier) { + const runtime = makeHookRuntime(); + globalThis.React = { ...realReact, ...runtime.React }; + try { + return { runtime, module: await import(specifier) }; + } finally { + globalThis.React = realReact; + } +} +const actions = await importOnHarness('../src/components/windows/dataExchange/codevCoatings/useImportActions.js'); +const dialog = await importOnHarness('../src/components/windows/design/coatingLibrary/SaveCoatingDialog.js'); +const { useImportAction, useLibraryAction } = actions.module; +const { SaveCoatingDialog } = dialog.module; + +const cm = await import('../src/utils/materials/catalogManager.js'); +const { parseCodevSeq } = await import('../src/utils/io/codevCoatingFile.js'); +const { makeCoatingEntry } = await import('../src/utils/coatingLibrary/entryModel.js'); +const { validateEntry } = await import('../src/utils/coatingLibrary/validateEntry.js'); +const { applyCoatingPatch } = await import('../src/utils/coatingLibrary/applyCoating.js'); +const { designMaterialLookup, resolveDesignMaterial } = await import('../src/utils/materials/designMaterials.js'); + +const c = makeTheme(); +const t = makeLocale('en'); +const z = t.codevCoatings; +const sd = t.coatingLibrary.saveDialog; + +const seq = (lines) => ['MUL', 'MDA', 'PHT Y', ...lines, 'MEX'].join('\r\n'); +const MIC = ['MIC', " MWL 450 550 650", " 'H' 2.40 2.35 2.31", " 'L' 1.47 1.46 1.455", " 'G' 1.525 1.52 1.515", 'END']; +const LAYERS = ["COA 60 100 'H'", "COA 95 0 'L'", 'COA 12 0 2.0 0.01']; +const SEQ = seq(["TIT 'AR on glass'", 'WL 450 550 650', 'REF 550', 'ANG 12 30', ...MIC, 'INC 1.0', ...LAYERS, "SUB 'G'"]); +const FILE = { fileName: 'ar.seq', filePath: 'C:\\coatings\\ar.seq' }; + +// The active design the coating is applied to: air on BK7, both sides empty. +const design = { + id: 'lens', name: 'Lens', incidentMedium: 'builtin:Air', exitMedium: 'builtin:Air', + substrate: { material: 'builtin:BK7', thickness: 1 }, referenceWavelength: 550, + surfaceMode: 'both_independent', frontLayers: [], backLayers: [], +}; + +// What the window hands the dialog, and what it reported. +function openLibrary(stack, file = FILE) { + let opened = null; + const flashes = []; + actions.runtime.reset(); + const saveToLibrary = actions.runtime.render(() => useLibraryAction({ + z, stack, ...file, flash: (type, message) => flashes.push([type, message]), + setLibrary: (value) => { opened = value; }, + })); + saveToLibrary(); + return { coating: opened, flashes }; +} + +// The layers the window's import puts on one side of `design`. +function importLayers(stack, side) { + const updates = []; + actions.runtime.reset(); + actions.runtime.render(() => useImportAction({ + z, stack, ...FILE, flash: () => {}, checkpoint: () => {}, updateDesign: (patch) => updates.push(patch), + }))(side); + return updates[0][side === 'back' ? 'backLayers' : 'frontLayers']; +} + +// Every function component of the dialog is drawn without hooks, so the tree +// can be expanded by calling them. +function expand(node) { + if (Array.isArray(node)) return node.map(expand); + if (!node || typeof node !== 'object') return node; + if (typeof node.type === 'function') return expand(node.type(node.props)); + return { ...node, props: { ...node.props, children: expand(node.props.children) } }; +} +function walk(node, visit) { + if (Array.isArray(node)) { node.forEach(child => walk(child, visit)); return; } + if (!node || typeof node !== 'object') return; + visit(node); + walk(node.props.children, visit); +} + +// Press Save on the dialog opened on `coating`; returns what was written and what the dialog showed. +async function saveThroughDialog(coating) { + let savedName = null; + let closed = false; + dialog.runtime.reset(); + const tree = expand(dialog.runtime.render(() => SaveCoatingDialog({ + coating, c, t, onClose: () => { closed = true; }, onSaved: (name) => { savedName = name; }, + }))); + const strings = []; + const inputs = []; + let saveButton = null; + walk(tree, (node) => { + const { children } = node.props; + for (const child of [].concat(children)) if (typeof child === 'string') strings.push(child); + if (node.type === 'button' && children === sd.save) saveButton = node; + if (node.type === 'input') inputs.push(node.props.value); + }); + const before = saved.length; + await saveButton.props.onClick(); + assert.equal(saved.length, before + 1, 'Save writes one coating'); + assert.ok(closed, 'and closes the dialog'); + return { entry: makeCoatingEntry(saved.at(-1)), savedName, strings, inputs }; +} + +const pairs = (layers) => layers.map(({ material, thickness }) => [material, thickness]); +const nkAt = (d, layers) => { + const lookup = designMaterialLookup(d); + return layers.map(layer => [450, 550, 650].map(lambda => lookup(layer.material).getNK(lambda))); +}; + +// ── A file never imported: nothing is written, the entry carries its records ─ +cm.initCatalogs({}); +const stack = parseCodevSeq(SEQ); +let neverImported; +{ + const { coating, flashes } = openLibrary(stack); + assert.deepEqual(flashes, [], 'the dialog opens without a complaint'); + assert.ok(!cm.getCatalogs().some(cat => cat.id.startsWith('codev_')), 'opening the dialog writes no catalog'); + assert.deepEqual(coating.band, [450, 650], 'the band opens at the analysis wavelengths'); + assert.equal(coating.aoi, 12, 'and the angle at the first of ANG'); + + const { entry, savedName, strings, inputs } = await saveThroughDialog(coating); + neverImported = entry; + assert.equal(inputs[0], 'AR on glass', 'the dialog proposes the title of the file'); + assert.ok(inputs.includes('450') && inputs.includes('650') && inputs.includes('12'), `with its band and angle: ${inputs}`); + assert.ok(strings.includes(t.coatingLibrary.layersShort(3)), 'it says how many layers it saves'); + assert.ok(!strings.includes(sd.front) && !strings.includes(sd.back), 'and has no side to pick'); + assert.equal(savedName, 'AR on glass'); + + assert.deepEqual(pairs(entry.layers), pairs([...coating.layers].reverse()), 'the entry holds the layers substrate first'); + assert.deepEqual(entry.layers.map(layer => layer.thickness), [12, 95, 60], 'deposition order, in nm'); + assert.match(entry.layers[2].material, /^codev_ar_[0-9a-f]{8}:h$/, 'under the id the import gives them'); + assert.equal(entry.incidentMedium, 'builtin:Air', 'INC 1.0 is built-in Air'); + assert.match(entry.substrate, /^codev_ar_[0-9a-f]{8}:g$/, 'SUB is the MIC material G, in the catalog of the file'); + assert.equal(entry.materials[entry.substrate].name, 'G'); + assert.deepEqual(Object.keys(entry.materials).sort(), [...new Set([...entry.layers.map(layer => layer.material), entry.substrate])].sort(), + 'every material is embedded, the substrate included, and Air is not'); + assert.deepEqual(entry.bands, [[450, 650]]); + assert.equal(entry.aoi, 12); + assert.equal(entry.referenceWavelength, 550); + assert.equal(entry.source, z.librarySource('ar.seq')); + assert.deepEqual(validateEntry(entry), [], 'the entry is sound'); + + const glass = designMaterialLookup({ ...design, materials: entry.materials })(entry.substrate); + assert.ok(Math.abs(glass.getNK(550)[0] - 1.52) < 1e-9, 'the substrate has the index of the MIC table'); +} + +// It applies to a design here, where no catalog holds its materials, as the +// stack the import makes, computed with the data of the file. +let importedFront; +{ + const { patch, clashes } = applyCoatingPatch(design, neverImported, { side: 'front' }); + assert.deepEqual(clashes, []); + const applied = { ...design, ...patch }; + for (const layer of applied.frontLayers) { + assert.equal(resolveDesignMaterial(applied, layer.material).status, 'embedded', 'from the records the entry carries'); + } + importedFront = importLayers(stack, 'front'); + const imported = { ...design, frontLayers: importedFront }; + assert.deepEqual(applied.frontLayers.map(layer => layer.thickness), importedFront.map(layer => layer.thickness), + 'the same thicknesses, incident side first'); + assert.deepEqual(nkAt(applied, applied.frontLayers), nkAt(imported, importedFront), 'and the same n,k layer by layer'); +} + +// ── The file imported before: the entry names the import's own materials ──── +{ + const catId = importedFront[0].material.split(':')[0]; + const { coating } = openLibrary(stack); + const { entry } = await saveThroughDialog(coating); + assert.deepEqual(pairs(entry.layers), pairs([...importedFront].reverse()), + 'layer for layer the materials and thicknesses the import put on the design'); + assert.equal(entry.materials[entry.layers[0].material].catalogUid, cm.getCatalog(catId).uid, + 'each record is the catalog\'s, stamped with it'); + assert.ok(entry.substrate.startsWith(`${catId}:`), 'SUB takes an id in the same catalog'); + assert.equal(resolveDesignMaterial({ ...design, materials: entry.materials }, entry.substrate).status, 'embedded', + 'which the import did not register, so the entry carries it'); + assert.deepEqual(validateEntry(entry), []); + + const front = applyCoatingPatch(design, entry, { side: 'front' }); + assert.deepEqual(front.clashes, []); + assert.deepEqual(pairs(front.patch.frontLayers), pairs(importedFront), 'it applies to the front as the front import'); + const back = applyCoatingPatch(design, entry, { side: 'back' }); + assert.deepEqual(pairs(back.patch.backLayers), pairs(importLayers(stack, 'back')), 'and to the back as the back import'); + assert.equal(cm.getCatalog(catId).materials[entry.substrate.split(':')[1]], undefined, 'saving registered nothing'); +} + +// ── Inline media other than n 1 are materials of their own, embedded ───────── +{ + const water = parseCodevSeq(seq(['WL 450 550 650', ...MIC, 'INC 1.33', ...LAYERS, 'SUB 1.52'])); + const { coating } = openLibrary(water, { fileName: 'water.seq', filePath: 'C:\\coatings\\water.seq' }); + const { entry } = await saveThroughDialog(coating); + const lookup = designMaterialLookup({ ...design, materials: entry.materials }); + assert.notEqual(entry.incidentMedium, 'builtin:Air', 'INC 1.33 is not air'); + assert.ok(Math.abs(lookup(entry.incidentMedium).getNK(550)[0] - 1.33) < 1e-12, 'it is n 1.33'); + assert.ok(Math.abs(lookup(entry.substrate).getNK(550)[0] - 1.52) < 1e-12, 'SUB 1.52 is n 1.52'); + assert.ok(entry.materials[entry.incidentMedium] && entry.materials[entry.substrate], 'both embedded'); + assert.equal(entry.name, 'water', 'a file with no TIT is named after the file'); + assert.equal(entry.aoi, 0, 'and with no ANG opens at normal incidence'); + assert.deepEqual(validateEntry(entry), []); +} + +// ── WLG: the band opens at the wavelengths as shown, and the entry is sound ── +{ + const wlg = parseCodevSeq(seq(['WLG 400 700 10', ...MIC, ...LAYERS, "SUB 'G'"])); + assert.notEqual(wlg.wavelengthsNm[0], 400, 'WLG wavelengths are float32 sums, not round numbers'); + const { coating } = openLibrary(wlg, { fileName: 'wlg.seq', filePath: 'C:\\coatings\\wlg.seq' }); + assert.equal(coating.band[0], 400, `the band starts at 400 nm, not ${wlg.wavelengthsNm[0]}`); + assert.ok(Math.abs(coating.band[1] - wlg.wavelengthsNm.at(-1)) < 1e-4, `and ends at the last of them: ${coating.band[1]}`); + const { entry } = await saveThroughDialog(coating); + assert.deepEqual(validateEntry(entry), [], 'the materials cover the band the dialog opened with'); +} + +// One analysis wavelength is no band: the dialog opens with its own. +{ + const { coating } = openLibrary(parseCodevSeq(seq(['WL 550', ...MIC, ...LAYERS, "SUB 'G'"]))); + assert.equal(coating.band, undefined); +} + +// With no file read there is nothing to save, and nothing is said. +{ + const { coating, flashes } = openLibrary(null); + assert.equal(coating, null); + assert.deepEqual(flashes, []); +} + +console.log('PASS: codev_coatings_window_library'); diff --git a/tests/codev_coatings_window_render.mjs b/tests/codev_coatings_window_render.mjs new file mode 100644 index 00000000..4ba89909 --- /dev/null +++ b/tests/codev_coatings_window_render.mjs @@ -0,0 +1,220 @@ +/** + * CODE V Coatings window: where the ribbon and the help reach it, and what each + * tab draws. + * + * The window is reached from the Data Exchange group beside Zemax Coatings and + * its help button opens a page that exists in both documentation languages. + * It is built from the shared Data Exchange chrome: the Import and Export tabs + * and the report of the last action in one control row, the reader's or the + * writer's notes in the notice badge there. The Import tab opens the file in + * its panel, sums up what the file sets there, and beside it shows the stack, + * incident side first, with the layers CODE V holds fixed locked; it offers the + * stack to the front and the back coating, the back off in Symmetric mode and + * both off with no design selected, and to the Coating Library. Wavelengths + * are shown as short as the float32 CODE V holds them in allows. The Export + * tab holds its options in a panel section + * above the preview, and is blocked while the design has a material this + * computer cannot resolve, as the Zemax window's is. With no design selected it + * asks for one in place of all of that. + * + * Run: node tests/codev_coatings_window_render.mjs + */ +import assert from 'node:assert/strict'; +import { existsSync, readFileSync } from 'node:fs'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { loadApp, makeDesignCtx, makeLocale, makeSampleDesign, makeTheme, shimBrowserGlobals, withDesign } from './_uiShim.mjs'; + +shimBrowserGlobals(); +const { DesignContext } = await loadApp(); + +const [ + { CodevCoatings }, { tabNotices }, { ImportTab }, { ExportTab }, { codevCoatingsSession }, + { WINDOW_REGISTRY }, { makeTabs, ICONS }, { warningText }, { parseCodevSeq }, +] = await Promise.all([ + import('../src/components/windows/dataExchange/codevCoatings/CodevCoatings.js'), + import('../src/components/windows/dataExchange/codevCoatings/CodevLayout.js'), + import('../src/components/windows/dataExchange/codevCoatings/ImportTab.js'), + import('../src/components/windows/dataExchange/codevCoatings/ExportTab.js'), + import('../src/components/windows/dataExchange/codevCoatings/sessionState.js'), + import('../src/components/docking/windowRegistry.js'), + import('../src/components/Toolbar.js'), + import('../src/components/windows/dataExchange/codevCoatings/messages.js'), + import('../src/utils/io/codevCoatingFile.js'), +]); + +const c = makeTheme(); +const t = makeLocale('en'); +const z = t.codevCoatings; +const noop = () => {}; +const text = (html) => html.replace(/<[^>]+>/g, ' ').replace(/'/g, "'").replace(/"/g, '"').replace(/&/g, '&'); +const attr = (value) => value.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, '''); +const renderWindow = (tl) => renderToStaticMarkup(withDesign(React.createElement(CodevCoatings, { c, t: tl }))); + +// ── Registered, on the ribbon beside Zemax Coatings, with a help page ───────── +{ + const entry = WINDOW_REGISTRY['codev-coatings']; + assert.equal(entry.component, CodevCoatings); + assert.equal(entry.help, 'data-exchange/codev-coatings'); + const exchange = makeTabs(t).flatMap(tab => tab.groups).find(group => group.key === 'exchange'); + const ids = exchange.items.map(item => item.id); + assert.equal(ids.indexOf('codev-coatings'), ids.indexOf('zemax-coatings') + 1, `beside Zemax Coatings: ${ids}`); + assert.ok(ICONS['codev-coatings'], 'the ribbon button has an icon'); + for (const slug of [`data-exchange/codev-coatings.md`, `zh/data-exchange/codev-coatings.md`]) { + const page = new URL(`../docs-site/src/content/docs/${slug}`, import.meta.url); + assert.ok(existsSync(page), `help page ${slug} exists`); + assert.match(readFileSync(page, 'utf8'), /^ribbonIcon: codev-coatings$/m, `${slug} shows the ribbon icon`); + } +} + +// ── Every language, no file: the control row and the file panel ────────────── +for (const code of ['en', 'ru', 'zh', 'it']) { + codevCoatingsSession.reset(); + const tl = makeLocale(code); + const html = renderWindow(tl); + for (const key of ['tabImport', 'tabExport', 'fileTitle', 'openBtn', 'fileHint', 'noFile']) { + assert.ok(text(html).includes(tl.codevCoatings[key]), `${code}: ${key} is shown`); + } + assert.ok(html.includes('tfs-spectrum-import-layout'), `${code}: the Import tab is an import layout, panel and stack`); + assert.equal((html.match(/aria-pressed="true"/g) || []).length, 1, `${code}: one tab is active`); + assert.ok(!html.includes(`title="${tl.analysisChrome.notices}"`), `${code}: no file, no notices`); +} + +// ── Import tab: what was read, incident side first ──────────────────────────── +const stack = { + title: 'Silver Reflector', refNm: 550, wavelengthsNm: [400, 750, 1000], anglesDeg: [0, 20], + incident: { n: 1, k: 0 }, substrate: { n: 1.5, k: 0 }, + layers: [ + { thicknessNm: 95, code: 0, index: { label: 'SiO2' } }, + { thicknessNm: 400, code: 100, index: { label: 'Silver' } }, + { thicknessNm: 12.5, code: 0, index: { n: 2.75, k: 4.46 } }, + ], + mic: {}, + warnings: [{ kind: 'unknownCommand', command: 'MAN', line: 26 }], +}; +const renderImport = (design, shown = stack) => renderToStaticMarkup(React.createElement(ImportTab, { + c, z, stack: shown, design, hasActiveDesign: true, fileName: 'silver.seq', loading: false, onLoad: noop, + importCoating: noop, saveToLibrary: noop, panelWidth: null, setPanelWidth: noop, +})); +// The buttons with these labels, each as [label, off]. +const buttons = (markup, labels = [z.importToFront, z.importToBack]) => [...markup.matchAll(/]*)>([^<]*)<\/button>/g)] + .filter(([, , label]) => labels.includes(label)) + .map(([, attrs, label]) => [label, /\sdisabled/.test(attrs)]); +{ + const html = renderImport(makeSampleDesign()); + const shown = text(html); + for (const part of ['Silver Reflector', 'silver.seq', z.fileTitle, z.openBtn, z.wavelengthsValue(3, 400, 1000), '0°, 20°', '550 nm', + z.summaryIncident, z.summarySubstrate, z.layersHeader(3), z.importToFront, z.importToBack, z.saveToLibrary, + 'n 2.75, k 4.46', 'n 1.5']) { + assert.ok(shown.includes(part), `the Import tab shows ${part}`); + } + assert.ok(html.includes(`title="${attr(z.saveToLibraryTip)}"`), 'the library button says where the media come from'); + const rows = [...html.matchAll(/(.*?)<\/tr>/g)].map(m => m[1]).filter(row => row.includes('95.00') || row.includes('400.00') || row.includes('12.50')); + assert.deepEqual(rows.map(row => (row.match(/>(\d+\.\d\d) row.includes(' ({ + activeDesignId: app.activeDesignId, + designs: app.designs, + folders: [], + onDesignChange: (id, design) => { + app.changes.push(id); + app.designs = { ...app.designs, [id]: design }; + }, + onCheckpoint: id => app.checkpoints.push(id), + historyView: { entries: [], currentIndex: -1 }, + onJumpToHistory: index => app.jumps.push(index), +}); + +// One render of the provider, as the app would draw it after the last change, +// returning what windows read from it. The slots persist between calls, so this +// is one mounted provider rendered again and again. The provider reads some +// hooks off React at call time, so the harness stands in for React meanwhile. +function render() { + const real = globalThis.React; + globalThis.React = runtime.React; + try { return runtime.render(() => DesignProvider(appProps())).props.value; } finally { globalThis.React = real; } +} + +// Every write a window can make, each on a fresh render as a user's clicks are. +function writeEverything(layerId) { + render().updateDesign({ + frontLayers: [ + { id: 'imp1', material: 'TiO2', thickness: 60, locked: false }, + { id: 'imp2', material: 'SiO2', thickness: 95, locked: false }, + ], + }); + render().updateLayer('front', layerId, { thickness: 123 }); + render().duplicateLayer('front', layerId); + render().moveLayer('front', layerId, 'down'); + render().removeLayer('front', layerId); + render().checkpoint(); + render().jumpToHistory(0); +} + +// ── Startup: nothing selected ─────────────────────────────────────────────── +const atStartup = render(); +assert.equal(atStartup.hasActiveDesign, false, 'with nothing selected there is no active design'); +assert.equal(atStartup.activeDesignId, null, 'and no active id'); +assert.ok(atStartup.design && Array.isArray(atStartup.design.frontLayers) && Array.isArray(atStartup.design.backLayers), + 'windows are still given a design to draw'); +const placeholderId = atStartup.design.id; + +writeEverything('imp1'); +const afterWrites = render(); +assert.deepEqual(app.changes, [], 'no write reaches the store'); +assert.deepEqual(app.checkpoints, [], 'no undo checkpoint is pushed, not even for a null id'); +assert.deepEqual(app.jumps, [], 'no history jump is made'); +assert.deepEqual(app.designs, {}, 'the store holds no design'); +assert.equal(afterWrites.design.frontLayers.length, 0, 'the windows go on showing a bare placeholder: the import landed nowhere'); +assert.equal(afterWrites.hasActiveDesign, false, 'and still report no active design'); +assert.equal(afterWrites.getDesignRevision(), 0, 'no edit is counted'); +assert.equal(afterWrites.design.id, placeholderId, 'the placeholder keeps one id, so window state keyed on it stays put'); + +// ── The user opens a design ───────────────────────────────────────────────── +const opened = { + id: 'd1', name: 'Opened', surfaceMode: 'front_only', mfEvalMode: 'side', + incidentMedium: 'Air', exitMedium: 'Air', + substrate: { material: 'BK7', thickness: 1 }, + frontLayers: [ + { id: 'f1', material: 'Ta2O5', thickness: 40, locked: false }, + { id: 'f2', material: 'MgF2', thickness: 88, locked: false }, + ], + backLayers: [], + referenceWavelength: 550, +}; +app.designs = { d1: opened }; +app.activeDesignId = 'd1'; + +const open = render(); +assert.equal(open.hasActiveDesign, true, 'an open design is an active one'); +assert.equal(open.activeDesignId, 'd1', 'with its id'); +assert.equal(open.design, opened, 'windows get the stored design'); +assert.notEqual(open.design.id, placeholderId, 'under an id of its own'); + +open.updateLayer('front', 'f1', { thickness: 41 }); +assert.deepEqual(app.changes, ['d1'], 'a layer edit reaches the store for that design'); +assert.equal(app.designs.d1.frontLayers[0].thickness, 41, 'with the edit in it'); + +render().updateDesign({ notes: 'checked' }); +render().duplicateLayer('front', 'f1'); +render().moveLayer('front', 'f2', 'up'); +render().removeLayer('front', 'f1'); +assert.equal(app.changes.length, 5, 'every kind of write reaches the store'); +assert.ok(app.changes.every(id => id === 'd1'), 'and only for the open design'); +assert.equal(app.designs.d1.notes, 'checked', 'a design-level edit lands'); +assert.deepEqual(app.designs.d1.frontLayers.map(l => l.material), ['MgF2', 'Ta2O5'], + 'duplicate, move and remove land in order'); + +render().checkpoint(); +render().jumpToHistory(0); +assert.deepEqual(app.checkpoints, ['d1'], 'a checkpoint is pushed for the open design'); +assert.deepEqual(app.jumps, [0], 'and a history jump goes through'); +assert.ok(render().getDesignRevision() > 0, 'edits to it are counted'); + +// ── The design is deleted: nothing selected again ─────────────────────────── +app.designs = {}; +app.activeDesignId = null; +const closed = render(); +assert.equal(closed.hasActiveDesign, false, 'with the open design gone there is no active design again'); +assert.equal(closed.design.id, placeholderId, 'the windows are back on the same placeholder'); +assert.equal(closed.design.frontLayers.length, 0, 'still bare'); +const changesBefore = app.changes.length; +writeEverything('imp2'); +assert.equal(app.changes.length, changesBefore, 'and writes are ignored again'); +assert.deepEqual(app.checkpoints, ['d1'], 'with no checkpoint pushed'); + +console.log('PASS: design_no_selection'); diff --git a/tests/no_design_apply_refused.mjs b/tests/no_design_apply_refused.mjs new file mode 100644 index 00000000..147a0de1 --- /dev/null +++ b/tests/no_design_apply_refused.mjs @@ -0,0 +1,126 @@ +/** + * With no design selected there is nothing to put a coating into: Apply in the + * Coating Library is off and says a design is needed. The action behind the + * button, called anyway, writes nothing, pushes no undo step and reports why. + * With a design open the same button is on and the same action goes through. + * Nor is there a coating of the design to save, so Save current coating is off + * the same way. + * + * The Coating Library stays mounted with no design open, because browsing the + * coatings needs none; the windows that only work on a design are not mounted + * at all then (tests/no_design_window_gate.mjs). + * + * The window runs here as its hooks run in the app, through the hook harness, + * so an action can be taken and the window drawn again afterwards. + * Run: node tests/no_design_apply_refused.mjs + */ +import assert from 'node:assert/strict'; +import RealReact from 'react'; +import { makeHookRuntime } from './_hookHarness.mjs'; +import { loadApp, makeDesignCtx, makeLocale, makeSampleDesign, makeTheme, shimBrowserGlobals } from './_uiShim.mjs'; + +shimBrowserGlobals(); + +// Every window module reads its hooks off the global React as it loads, so the +// global delegates to whichever harness is drawing. +let drawing = null; +let designCtx = null; +let DesignContext = null; +const delegate = name => (...args) => drawing.React[name](...args); +globalThis.React = { + ...RealReact, + useState: delegate('useState'), useRef: delegate('useRef'), useMemo: delegate('useMemo'), + useCallback: delegate('useCallback'), useEffect: delegate('useEffect'), useLayoutEffect: delegate('useLayoutEffect'), + useContext: context => (context === DesignContext ? designCtx : context._currentValue), +}; +({ DesignContext } = await loadApp()); + +const { CoatingLibrary } = await import('../src/components/windows/design/coatingLibrary/CoatingLibrary.js'); +const { coatingLibrarySession } = await import('../src/components/windows/design/coatingLibrary/sessionState.js'); +const { BUILTIN_COATINGS } = await import('../src/utils/coatingLibrary/builtin/index.js'); + +const c = makeTheme(); +const t = makeLocale(); +const OPEN_STATES = [false, true]; + +/** The active design as the window sees it, and every write and undo step that reaches it. */ +function openDesign(hasActiveDesign) { + const writes = []; + designCtx = { + ...makeDesignCtx(makeSampleDesign()), + hasActiveDesign, + updateDesign: patch => writes.push(['update', patch]), + checkpoint: () => writes.push(['checkpoint']), + }; + return writes; +} + +// Every element of a window's tree, the components named in `open` drawn by +// calling them. +const elements = (node, open, found = []) => { + if (Array.isArray(node)) node.forEach(child => elements(child, open, found)); + else if (node && typeof node === 'object' && node.props) { + found.push(node); + if (open.includes(node.type?.name)) elements(node.type(node.props), open, found); + else elements(node.props.children, open, found); + } + return found; +}; + +function draw(runtime, Window, open) { + drawing = runtime; + return elements(runtime.render(() => Window({ c, t, setInputDialog: () => {} })), open); +} + +const component = (found, name) => found.find(node => node.type?.name === name); +const button = (found, label) => found.find(node => node.type === 'button' && node.props.children === label); +// Whether a button is off, and what it says when hovered. +const state = node => [!!node.props.disabled, node.props.title]; + +// ── Coating Library: Apply ─────────────────────────────────────────────────── +{ + const ts = t.coatingLibrary; + const entry = BUILTIN_COATINGS[0]; + const show = runtime => { + const found = draw(runtime, CoatingLibrary, ['ApplyBar']); + return { bar: component(found, 'ApplyBar'), apply: button(found, ts.apply) }; + }; + for (const hasActiveDesign of OPEN_STATES) { + coatingLibrarySession.reset(); + coatingLibrarySession.write(null, { selectedId: entry.id }); + const writes = openDesign(hasActiveDesign); + const runtime = makeHookRuntime(); + const shown = show(runtime); + shown.bar.props.onApply(); + const message = show(runtime).bar.props.message; + if (!hasActiveDesign) { + assert.deepEqual(state(shown.apply), [true, ts.applyNoDesign], 'no design: Apply is off and says why'); + assert.deepEqual(writes, [], 'no design: applying writes nothing and pushes no undo step'); + assert.equal(message, ts.applyNoDesign, 'no design: the bar says a design is needed'); + } else { + assert.deepEqual(state(shown.apply), [false, undefined], 'a design open: Apply is on'); + assert.deepEqual(writes.map(([kind]) => kind), ['checkpoint', 'update'], 'one undo step, then the stack'); + assert.equal(writes[1][1].frontLayers.length, entry.layers.length, 'the coating goes on the front'); + assert.ok(message.startsWith(ts.applied(entry.layers.length, ts.sideFront.toLowerCase())), + 'and the bar reports it'); + } + } + coatingLibrarySession.reset(); +} + +// ── Coating Library: Save current coating ──────────────────────────────────── +// It saves a side of the open design. With none open the only design is the +// placeholder, so the button is off and says a design is needed. +{ + const ts = t.coatingLibrary; + for (const hasActiveDesign of OPEN_STATES) { + coatingLibrarySession.reset(); + openDesign(hasActiveDesign); + const save = button(draw(makeHookRuntime(), CoatingLibrary, ['FilterBar']), ts.saveCurrent); + assert.deepEqual(state(save), hasActiveDesign ? [false, ts.saveCurrentTip] : [true, t.windowChrome.noDesign], + `design open: ${hasActiveDesign}`); + } + coatingLibrarySession.reset(); +} + +console.log('no_design_apply_refused: passed'); diff --git a/tests/no_design_material_lists.mjs b/tests/no_design_material_lists.mjs new file mode 100644 index 00000000..e732a61f --- /dev/null +++ b/tests/no_design_material_lists.mjs @@ -0,0 +1,72 @@ +/** + * With no design open, the windows that stay open list no materials of the + * placeholder design the provider hands out meanwhile. + * + * The material picker and the Material Editor each offer the open design's own + * materials as a list of their own ("This design"), and a phase monitor in the + * status bar names the side the design is evaluated on. The placeholder is + * nobody's design, so with no design open there is no such list and no side. + * + * Run: node tests/no_design_material_lists.mjs + */ +import assert from 'node:assert/strict'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { loadApp, makeDesignCtx, makeLocale, makeSampleDesign, makeTheme, shimBrowserGlobals } from './_uiShim.mjs'; + +shimBrowserGlobals(); +const { DesignContext } = await loadApp(); +const [{ MaterialPicker }, { useMaterialEditor }, { SpectralMonitor }, { initCatalogs }, { constantIndexRecord }] = await Promise.all([ + import('../src/components/ui/MaterialPicker.js'), + import('../src/components/windows/design/materialEditor/useMaterialEditor.js'), + import('../src/components/SpectralMonitor.js'), + import('../src/utils/materials/catalogManager.js'), + import('../src/utils/io/designImport/materialResolution.js'), +]); + +const c = makeTheme(); +const t = makeLocale(); +const h = React.createElement; +const OPEN_STATES = [false, true]; + +// A design whose one layer is a material only the design carries, so its name +// can come from nowhere but the design. +const QUARTZ = { ...constantIndexRecord(1.46), name: 'Quartz glass' }; +const design = { + ...makeSampleDesign(), + frontLayers: [{ id: 'l1', material: QUARTZ.id, thickness: 100, locked: false }], + materials: { [QUARTZ.id]: QUARTZ }, +}; +initCatalogs({}); + +const inContext = (element, hasActiveDesign) => + h(DesignContext.Provider, { value: { ...makeDesignCtx(design), hasActiveDesign } }, element); + +// ── Material picker ────────────────────────────────────────────────────────── +for (const hasActiveDesign of OPEN_STATES) { + const html = renderToStaticMarkup(inContext(h(MaterialPicker, { value: QUARTZ.id, onChange: () => {}, c, t }), hasActiveDesign)); + assert.equal(html.includes(QUARTZ.name), hasActiveDesign, + `the picker reads the design's own materials only with a design open (open: ${hasActiveDesign})`); +} + +// ── Material Editor ────────────────────────────────────────────────────────── +for (const hasActiveDesign of OPEN_STATES) { + let editor = null; + function Probe() { + editor = useMaterialEditor({ c: {}, t, setInputDialog: () => {} }); + return null; + } + renderToStaticMarkup(h(DesignContext.Provider, { value: { design, designs: {}, hasActiveDesign } }, h(Probe))); + const listed = editor.browseCatalogs.some(catalog => catalog.name === t.materialEditor.designCatalog); + assert.equal(listed, hasActiveDesign, + `the editor lists the design's materials only with a design open (open: ${hasActiveDesign})`); +} + +// ── Phase monitor ──────────────────────────────────────────────────────────── +localStorage.setItem('tfstudio-monitors-v1', JSON.stringify([{ id: 'gd', type: 'GD', lambda: 550, aoi: 0, pol: 'avg' }])); +for (const hasActiveDesign of OPEN_STATES) { + const html = renderToStaticMarkup(inContext(h(SpectralMonitor, { c, t }), hasActiveDesign)); + assert.equal(html.includes(`>${t.statusMonitors.scopeFront}<`), hasActiveDesign, + `a phase monitor names the side only with a design open (open: ${hasActiveDesign})`); +} + +console.log('no_design_material_lists: passed'); diff --git a/tests/no_design_monitors_and_wizards.mjs b/tests/no_design_monitors_and_wizards.mjs new file mode 100644 index 00000000..83eaa552 --- /dev/null +++ b/tests/no_design_monitors_and_wizards.mjs @@ -0,0 +1,77 @@ +/** + * With no design open, the status monitors and the two monitoring wizards show + * nothing of the placeholder design the provider hands out meanwhile. + * + * The monitors under the workspace read nothing, and the strip leaves out the + * side a design is evaluated on. The Broadband and Monochromatic wizards are + * modals, so the window gate in ToolContent never sees them: each asks for a + * design rather than reporting that the placeholder, a bare substrate, has no + * layers to simulate, and its header carries no evaluation badge. + * + * Run: node tests/no_design_monitors_and_wizards.mjs + */ +import assert from 'node:assert/strict'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { loadApp, makeDesignCtx, makeLocale, makeSampleDesign, makeTheme, shimBrowserGlobals } from './_uiShim.mjs'; + +shimBrowserGlobals(); +const { DesignContext } = await loadApp(); + +const [{ SpectralMonitor, monitorValues }, { BBMWizard }, { MonoWizard }, { EvalModeBadge }, { makeDefaultDesign }] = await Promise.all([ + import('../src/components/SpectralMonitor.js'), + import('../src/components/windows/simulation/bbmWizard/BBMWizard.js'), + import('../src/components/windows/simulation/monoWizard/MonoWizard.js'), + import('../src/components/SurfaceModeBar.js'), + import('../src/state/DesignContext.js'), +]); + +const c = makeTheme(); +const t = makeLocale(); +const h = React.createElement; +const OPEN_STATES = [false, true]; +const placeholder = makeDefaultDesign(); + +const inContext = (element, design, hasActiveDesign) => + h(DesignContext.Provider, { value: { ...makeDesignCtx(design), hasActiveDesign } }, element); + +// ── Status monitors ────────────────────────────────────────────────────────── +{ + const design = makeSampleDesign(); + const monitors = [ + { id: 'count', type: 'fact', fact: 'layerCount' }, + { id: 'reflectance', qty: 'R', type: 'avg', lambdaStart: 400, lambdaEnd: 700, aoi: 0, pol: 'avg' }, + ]; + const read = hasActiveDesign => monitorValues({ + monitors, design, hasActiveDesign, missingMaterialIds: [], coneActive: false, + }); + const [count, reflectance] = read(true); + assert.equal(count, design.frontLayers.length, 'a design open: the monitors read it'); + assert.ok(Number.isFinite(reflectance), `and its band-average R (${reflectance})`); + assert.deepEqual(read(false), [null, null], 'no design open: they read nothing'); + + for (const hasActiveDesign of OPEN_STATES) { + const html = renderToStaticMarkup(inContext(h(SpectralMonitor, { c, t }), placeholder, hasActiveDesign)); + assert.equal(html.includes(`>${t.statusMonitors.evalFront}<`), hasActiveDesign, + `the side evaluated on is shown only with a design open (design open: ${hasActiveDesign})`); + } +} + +// ── Monitoring wizards ─────────────────────────────────────────────────────── +const badge = renderToStaticMarkup(h(EvalModeBadge, { design: placeholder, c, t })); +for (const [name, Wizard, B] of [['BBMWizard', BBMWizard, t.bbmSim], ['MonoWizard', MonoWizard, t.monoSim]]) { + const draw = (design, hasActiveDesign) => renderToStaticMarkup( + inContext(h(Wizard, { c, t, onClose: () => {} }), design, hasActiveDesign)); + + const closed = draw(placeholder, false); + assert.ok(closed.includes(B.noDesign), `${name}: no design open, the wizard asks for one`); + assert.ok(!closed.includes(B.noLayers), `${name}: rather than saying the placeholder has no layers`); + assert.ok(!closed.includes(badge), `${name}: and its header has no evaluation badge`); + + const bare = draw(placeholder, true); + assert.ok(bare.includes(B.noLayers) && bare.includes(badge), `${name}: the same bare substrate, opened, has no layers`); + + const open = draw(makeSampleDesign(), true); + assert.ok(open.includes(B.p1Title) && !open.includes(B.noDesign), `${name}: a design with layers opens on page 1`); +} + +console.log('no_design_monitors_and_wizards: passed'); diff --git a/tests/no_design_window_gate.mjs b/tests/no_design_window_gate.mjs new file mode 100644 index 00000000..dcfc9c78 --- /dev/null +++ b/tests/no_design_window_gate.mjs @@ -0,0 +1,142 @@ +/** + * With no design open, a window that works on a design is not mounted. + * + * The provider hands every window a placeholder design while none is open, so a + * window mounted then draws a bare BK7 substrate, takes edits that go nowhere + * and reports tools applied to nothing. `ToolContent`, where every window is + * mounted, docked or torn off, draws a request to open or create a design in + * place of each window flagged `requiresDesign`, and mounts the window once a + * design is open, on the controls its copy held. A window with work of its own + * without a design is not flagged and mounts either way. + * + * Run: node tests/no_design_window_gate.mjs + */ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { loadApp, makeDesignCtx, makeLocale, makeTheme, shimBrowserGlobals } from './_uiShim.mjs'; + +shimBrowserGlobals(); +const { DesignContext } = await loadApp(); + +const [ + { ToolContent, FloatToolHost }, + { WINDOW_REGISTRY }, + { ErrorBoundary }, + { createWindowSession, useWindowSession }, + { useDesign }, +] = await Promise.all([ + import('../src/components/docking/DockingLayout.js'), + import('../src/components/docking/windowRegistry.js'), + import('../src/components/ui/ErrorBoundary.js'), + import('../src/components/windows/windowSession.js'), + import('../src/state/DesignContext.js'), +]); + +const c = makeTheme(); +const t = makeLocale(); +const h = React.createElement; +const OPEN_STATES = [false, true]; + +// Every element type in a tree as ToolContent builds it, without drawing any +// component: what is not in the tree is never mounted. +function typesIn(node, found = []) { + if (Array.isArray(node)) node.forEach(child => typesIn(child, found)); + else if (node && typeof node === 'object' && node.type) { + found.push(node.type); + typesIn(node.props?.children, found); + } + return found; +} + +const withContext = (element, hasActiveDesign) => + h(DesignContext.Provider, { value: { ...makeDesignCtx(), hasActiveDesign } }, element); + +// ── The windows that work without a design ─────────────────────────────────── +// Every window is either flagged or on this list, so a new window is decided on +// rather than left to compute from the placeholder by default. +const WORKS_WITHOUT_DESIGN = [ + 'material-editor', 'coating-library', 'material-dispersion', 'zemax-coatings', + 'codev-coatings', 'report-gen', 'optimizer-benchmark', 'games', +]; +const windows = Object.entries(WINDOW_REGISTRY).filter(([, entry]) => entry.component); +assert.deepEqual( + windows.filter(([, entry]) => !entry.requiresDesign).map(([id]) => id).sort(), + [...WORKS_WITHOUT_DESIGN].sort(), + 'every window that works on a design waits for one'); + +// ── Every registered window, with and without a design ─────────────────────── +const gatedMarkup = renderToStaticMarkup(ToolContent({ toolId: 'optical-eval', c, t, hasActiveDesign: false })); +assert.ok(gatedMarkup.includes('No design selected. Open or create a design first.'), + 'the pane asks for a design'); + +for (const [toolId, entry] of windows) { + for (const hasActiveDesign of OPEN_STATES) { + const element = ToolContent({ toolId, copyId: `tab-${toolId}`, c, t, hasActiveDesign }); + const types = typesIn(element); + if (entry.requiresDesign && !hasActiveDesign) { + assert.ok(!types.includes(entry.component), `${toolId}: not mounted with no design open`); + assert.ok(!types.includes(ErrorBoundary), `${toolId}: nothing of the window is built`); + assert.equal(renderToStaticMarkup(element), gatedMarkup, `${toolId}: the shared pane is drawn instead`); + } else { + const boundary = element.props.children; + assert.equal(boundary.type, ErrorBoundary, `${toolId}: mounted (design open: ${hasActiveDesign})`); + assert.equal(boundary.props.children.type, entry.component, `${toolId}: its own component`); + } + } +} + +// A window flagged both ways, with no design open, asks for the design: the +// placeholder's materials are not the user's to repair. +assert.equal(renderToStaticMarkup(ToolContent({ + toolId: 'optical-eval', c, t, hasActiveDesign: false, missingMaterialIds: ['user:gone'], +})), gatedMarkup, 'no design open comes before unavailable materials'); + +for (const code of ['en', 'ru', 'zh', 'it']) { + const local = makeLocale(code); + assert.ok(local.windowChrome.noDesign, `${code}: the pane has its text`); + assert.ok(renderToStaticMarkup(ToolContent({ toolId: 'design-editor', c, t: local, hasActiveDesign: false })) + .includes(local.windowChrome.noDesign), `${code}: and draws it`); +} + +// ── Torn off, and back on its controls once a design opens ─────────────────── +// A probe window that shows one value of its session store. The user set it +// while a design was open; the gate in between must neither drop nor reset it. +const probeSession = createWindowSession({ value: 'default' }); +let probeDrawn = 0; +function GateProbe() { + probeDrawn += 1; + const { design } = useDesign(); + const [session] = useWindowSession(probeSession, design); + return h('div', null, `probe ${session.value}`); +} +WINDOW_REGISTRY['gate-probe'] = { component: GateProbe, title: 'probe', label: 'probe', requiresDesign: true }; +const copyId = 'tab-probe'; +probeSession.write(null, { value: 'set by the user' }, copyId); + +// A docked pane is told by the layout whether a design is open; a torn-off one +// reads it from the provider itself. +const drawProbe = (host, hasActiveDesign) => renderToStaticMarkup(withContext(h(host, { + toolId: 'gate-probe', copyId, c, t, ...(host === ToolContent ? { hasActiveDesign } : {}), +}), hasActiveDesign)); + +for (const host of [ToolContent, FloatToolHost]) { + probeDrawn = 0; + const closed = drawProbe(host, false); + assert.ok(closed.includes(t.windowChrome.noDesign), `${host.name}: no design open, the pane is drawn`); + assert.equal(probeDrawn, 0, `${host.name}: and the window is never called`); + + const opened = drawProbe(host, true); + assert.ok(opened.includes('probe set by the user'), `${host.name}: a design open, the window mounts on its controls`); + assert.ok(!opened.includes(t.windowChrome.noDesign), `${host.name}: with no pane`); +} +delete WINDOW_REGISTRY['gate-probe']; + +// The docked tree hands each pane the provider's state. Its panes are drawn by +// an effect a static render does not run, so the wiring is read from the source. +const layoutSrc = readFileSync(new URL('../src/components/docking/DockingLayout.js', import.meta.url), 'utf8'); +assert.match(layoutSrc, /const \{[^}]*\bhasActiveDesign\b[^}]*\} = useDesign\(\);/, 'the layout reads it from the provider'); +assert.match(layoutSrc, /const nodeCtx = \{[^}]*\bhasActiveDesign\b/, 'puts it where the tree renderer reads'); +assert.match(layoutSrc, /h\(ToolContent, \{[^}]*hasActiveDesign: ctx\.hasActiveDesign/, 'and hands it to every pane'); + +console.log('no_design_window_gate: passed'); diff --git a/tests/reference/codev/codev_11_2.json b/tests/reference/codev/codev_11_2.json new file mode 100644 index 00000000..f2ccc215 --- /dev/null +++ b/tests/reference/codev/codev_11_2.json @@ -0,0 +1,4453 @@ +{ + "source": "CODE V PC 11.2, listings of 07-Oct-26 (codev_checks.lis, codev_checks2.lis, codev_checks4.lis to codev_checks6.lis)", + "man": "MAN transmission table of codev/.seq: [angle deg, wavelength nm, Rs, Rp, Ts, Tp], 6 decimals as printed", + "mic": "MIC material: table [wavelength nm, n, k] as entered, and the n and k MPR printed at the analysis wavelengths", + "data": { + "man": { + "tfs_ar4": [ + [ + 0, + 400, + 0.025689, + 0.025689, + 0.971147, + 0.971147 + ], + [ + 0, + 420, + 0.025177, + 0.025177, + 0.971841, + 0.971841 + ], + [ + 0, + 440, + 0.020812, + 0.020812, + 0.976349, + 0.976349 + ], + [ + 0, + 460, + 0.015142, + 0.015142, + 0.982139, + 0.982139 + ], + [ + 0, + 480, + 0.010168, + 0.010168, + 0.987222, + 0.987222 + ], + [ + 0, + 500, + 0.00665, + 0.00665, + 0.990842, + 0.990842 + ], + [ + 0, + 520, + 0.004654, + 0.004654, + 0.992831, + 0.992831 + ], + [ + 0, + 540, + 0.003394, + 0.003394, + 0.994085, + 0.994085 + ], + [ + 0, + 560, + 0.002405, + 0.002405, + 0.995073, + 0.995073 + ], + [ + 0, + 580, + 0.00148, + 0.00148, + 0.995997, + 0.995997 + ], + [ + 0, + 600, + 0.000656, + 0.000656, + 0.996819, + 0.996819 + ], + [ + 0, + 620, + 0.000118, + 0.000118, + 0.997359, + 0.997359 + ], + [ + 0, + 640, + 0.000112, + 0.000112, + 0.997366, + 0.997366 + ], + [ + 0, + 660, + 0.000869, + 0.000869, + 0.99661, + 0.99661 + ], + [ + 0, + 680, + 0.002565, + 0.002565, + 0.994916, + 0.994916 + ], + [ + 0, + 700, + 0.005299, + 0.005299, + 0.992187, + 0.992187 + ], + [ + 30, + 400, + 0.031753, + 0.013295, + 0.965024, + 0.983482 + ], + [ + 30, + 420, + 0.026203, + 0.010601, + 0.970737, + 0.986341 + ], + [ + 30, + 440, + 0.018766, + 0.006989, + 0.978307, + 0.990091 + ], + [ + 30, + 460, + 0.012519, + 0.003956, + 0.984673, + 0.993249 + ], + [ + 30, + 480, + 0.008543, + 0.002026, + 0.988763, + 0.995297 + ], + [ + 30, + 500, + 0.006466, + 0.001028, + 0.99095, + 0.996405 + ], + [ + 30, + 520, + 0.005611, + 0.000635, + 0.991801, + 0.996793 + ], + [ + 30, + 540, + 0.004815, + 0.00038, + 0.992593, + 0.997043 + ], + [ + 30, + 560, + 0.003775, + 0.000144, + 0.993633, + 0.997279 + ], + [ + 30, + 580, + 0.002616, + 0.000002, + 0.994792, + 0.99742 + ], + [ + 30, + 600, + 0.001708, + 0.000139, + 0.995699, + 0.997282 + ], + [ + 30, + 620, + 0.0015, + 0.000764, + 0.99591, + 0.996661 + ], + [ + 30, + 640, + 0.002397, + 0.002053, + 0.995015, + 0.995372 + ], + [ + 30, + 660, + 0.004689, + 0.004125, + 0.992727, + 0.993301 + ], + [ + 30, + 680, + 0.008522, + 0.007027, + 0.988901, + 0.990403 + ], + [ + 30, + 700, + 0.013908, + 0.010742, + 0.983525, + 0.986692 + ] + ], + "tfs_agmir": [ + [ + 0, + 400, + 0.968165, + 0.968165, + 0.000062, + 0.000062 + ], + [ + 0, + 450, + 0.977889, + 0.977889, + 0.000021, + 0.000021 + ], + [ + 0, + 500, + 0.97428, + 0.97428, + 0.000011, + 0.000011 + ], + [ + 0, + 550, + 0.971455, + 0.971455, + 0.000007, + 0.000007 + ], + [ + 0, + 600, + 0.975901, + 0.975901, + 0.000006, + 0.000006 + ], + [ + 0, + 650, + 0.979684, + 0.979684, + 0.000004, + 0.000004 + ], + [ + 0, + 700, + 0.986335, + 0.986335, + 0.000004, + 0.000004 + ], + [ + 0, + 750, + 0.991431, + 0.991431, + 0.000003, + 0.000003 + ], + [ + 0, + 800, + 0.990988, + 0.990988, + 0.000002, + 0.000002 + ], + [ + 0, + 850, + 0.991919, + 0.991919, + 0.000002, + 0.000002 + ], + [ + 0, + 900, + 0.993192, + 0.993192, + 0.000001, + 0.000001 + ], + [ + 0, + 950, + 0.994145, + 0.994145, + 0.000001, + 0.000001 + ], + [ + 0, + 1000, + 0.994932, + 0.994932, + 0.000001, + 0.000001 + ], + [ + 45, + 400, + 0.971902, + 0.954196, + 0.000034, + 0.000048 + ], + [ + 45, + 450, + 0.97668, + 0.970574, + 0.000015, + 0.000018 + ], + [ + 45, + 500, + 0.970118, + 0.969533, + 0.000009, + 0.00001 + ], + [ + 45, + 550, + 0.966313, + 0.969731, + 0.000006, + 0.000006 + ], + [ + 45, + 600, + 0.972893, + 0.976394, + 0.000005, + 0.000005 + ], + [ + 45, + 650, + 0.978871, + 0.980966, + 0.000004, + 0.000004 + ], + [ + 45, + 700, + 0.986892, + 0.987387, + 0.000003, + 0.000003 + ], + [ + 45, + 750, + 0.992336, + 0.992046, + 0.000002, + 0.000002 + ], + [ + 45, + 800, + 0.992366, + 0.991482, + 0.000002, + 0.000002 + ], + [ + 45, + 850, + 0.993437, + 0.992174, + 0.000001, + 0.000002 + ], + [ + 45, + 900, + 0.994642, + 0.993228, + 0.000001, + 0.000001 + ], + [ + 45, + 950, + 0.995498, + 0.994021, + 0.000001, + 0.000001 + ], + [ + 45, + 1000, + 0.996171, + 0.994693, + 0.000001, + 0.000001 + ] + ], + "tfs_hr45": [ + [ + 0, + 450, + 0.142671, + 0.142671, + 0.857329, + 0.857329 + ], + [ + 0, + 465, + 0.16391, + 0.16391, + 0.83609, + 0.83609 + ], + [ + 0, + 480, + 0.20881, + 0.20881, + 0.791191, + 0.791191 + ], + [ + 0, + 495, + 0.105478, + 0.105478, + 0.894522, + 0.894522 + ], + [ + 0, + 510, + 0.367772, + 0.367772, + 0.632228, + 0.632228 + ], + [ + 0, + 525, + 0.024924, + 0.024924, + 0.975076, + 0.975076 + ], + [ + 0, + 540, + 0.486564, + 0.486564, + 0.513435, + 0.513435 + ], + [ + 0, + 555, + 0.403829, + 0.403829, + 0.596171, + 0.596171 + ], + [ + 0, + 570, + 0.778394, + 0.778394, + 0.221606, + 0.221606 + ], + [ + 0, + 585, + 0.982073, + 0.982073, + 0.017927, + 0.017927 + ], + [ + 0, + 600, + 0.99557, + 0.99557, + 0.00443, + 0.00443 + ], + [ + 0, + 615, + 0.998147, + 0.998147, + 0.001853, + 0.001853 + ], + [ + 0, + 630, + 0.99893, + 0.99893, + 0.00107, + 0.00107 + ], + [ + 0, + 645, + 0.999222, + 0.999222, + 0.000778, + 0.000778 + ], + [ + 0, + 660, + 0.999325, + 0.999325, + 0.000675, + 0.000675 + ], + [ + 0, + 675, + 0.999323, + 0.999323, + 0.000677, + 0.000677 + ], + [ + 0, + 690, + 0.99923, + 0.99923, + 0.00077, + 0.00077 + ], + [ + 0, + 705, + 0.999021, + 0.999021, + 0.000979, + 0.000979 + ], + [ + 0, + 720, + 0.998615, + 0.998615, + 0.001385, + 0.001385 + ], + [ + 0, + 735, + 0.997824, + 0.997824, + 0.002176, + 0.002176 + ], + [ + 0, + 750, + 0.996181, + 0.996181, + 0.003819, + 0.003819 + ], + [ + 20, + 450, + 0.035742, + 0.025699, + 0.964257, + 0.974301 + ], + [ + 20, + 465, + 0.293321, + 0.246237, + 0.706679, + 0.753764 + ], + [ + 20, + 480, + 0.033904, + 0.037831, + 0.966096, + 0.962168 + ], + [ + 20, + 495, + 0.355902, + 0.281021, + 0.644098, + 0.71898 + ], + [ + 20, + 510, + 0.17987, + 0.21181, + 0.820129, + 0.78819 + ], + [ + 20, + 525, + 0.356495, + 0.196546, + 0.643505, + 0.803454 + ], + [ + 20, + 540, + 0.54922, + 0.539509, + 0.45078, + 0.460491 + ], + [ + 20, + 555, + 0.598262, + 0.05617, + 0.401738, + 0.94383 + ], + [ + 20, + 570, + 0.980047, + 0.945678, + 0.019953, + 0.054322 + ], + [ + 20, + 585, + 0.995941, + 0.990806, + 0.004059, + 0.009194 + ], + [ + 20, + 600, + 0.998472, + 0.996804, + 0.001528, + 0.003196 + ], + [ + 20, + 615, + 0.999175, + 0.998344, + 0.000825, + 0.001656 + ], + [ + 20, + 630, + 0.999428, + 0.998877, + 0.000572, + 0.001123 + ], + [ + 20, + 645, + 0.99952, + 0.99907, + 0.00048, + 0.00093 + ], + [ + 20, + 660, + 0.999531, + 0.999094, + 0.000469, + 0.000906 + ], + [ + 20, + 675, + 0.999478, + 0.998988, + 0.000522, + 0.001012 + ], + [ + 20, + 690, + 0.999347, + 0.998723, + 0.000653, + 0.001277 + ], + [ + 20, + 705, + 0.99909, + 0.998194, + 0.00091, + 0.001806 + ], + [ + 20, + 720, + 0.99859, + 0.997138, + 0.00141, + 0.002862 + ], + [ + 20, + 735, + 0.997564, + 0.994886, + 0.002436, + 0.005114 + ], + [ + 20, + 750, + 0.995249, + 0.989526, + 0.004751, + 0.010474 + ], + [ + 45, + 450, + 0.088099, + 0.081453, + 0.9119, + 0.918547 + ], + [ + 45, + 465, + 0.452394, + 0.093616, + 0.547606, + 0.906384 + ], + [ + 45, + 480, + 0.079221, + 0.187838, + 0.920778, + 0.812162 + ], + [ + 45, + 495, + 0.646636, + 0.038892, + 0.353364, + 0.961107 + ], + [ + 45, + 510, + 0.075722, + 0.409128, + 0.924278, + 0.590872 + ], + [ + 45, + 525, + 0.98646, + 0.03751, + 0.01354, + 0.962491 + ], + [ + 45, + 540, + 0.998284, + 0.839989, + 0.001716, + 0.160011 + ], + [ + 45, + 555, + 0.999491, + 0.975106, + 0.000509, + 0.024894 + ], + [ + 45, + 570, + 0.999762, + 0.991368, + 0.000238, + 0.008632 + ], + [ + 45, + 585, + 0.99985, + 0.995317, + 0.00015, + 0.004683 + ], + [ + 45, + 600, + 0.999881, + 0.996576, + 0.000119, + 0.003424 + ], + [ + 45, + 615, + 0.999888, + 0.99687, + 0.000112, + 0.00313 + ], + [ + 45, + 630, + 0.999878, + 0.996566, + 0.000122, + 0.003434 + ], + [ + 45, + 645, + 0.99985, + 0.995581, + 0.00015, + 0.004419 + ], + [ + 45, + 660, + 0.999792, + 0.993397, + 0.000208, + 0.006603 + ], + [ + 45, + 675, + 0.99968, + 0.988517, + 0.00032, + 0.011483 + ], + [ + 45, + 690, + 0.999449, + 0.976387, + 0.000551, + 0.023613 + ], + [ + 45, + 705, + 0.998937, + 0.940529, + 0.001063, + 0.059471 + ], + [ + 45, + 720, + 0.997658, + 0.807152, + 0.002342, + 0.192848 + ], + [ + 45, + 735, + 0.993887, + 0.306621, + 0.006113, + 0.693379 + ], + [ + 45, + 750, + 0.979661, + 0.044147, + 0.020339, + 0.955853 + ], + [ + 60, + 450, + 0.524265, + 0.096891, + 0.475735, + 0.90311 + ], + [ + 60, + 465, + 0.528422, + 0.052129, + 0.471578, + 0.94787 + ], + [ + 60, + 480, + 0.320022, + 0.122633, + 0.679978, + 0.877367 + ], + [ + 60, + 495, + 0.991368, + 0.287484, + 0.008632, + 0.712516 + ], + [ + 60, + 510, + 0.999272, + 0.049604, + 0.000728, + 0.950395 + ], + [ + 60, + 525, + 0.999821, + 0.840334, + 0.000179, + 0.159666 + ], + [ + 60, + 540, + 0.999925, + 0.961471, + 0.000075, + 0.038529 + ], + [ + 60, + 555, + 0.999956, + 0.982695, + 0.000044, + 0.017305 + ], + [ + 60, + 570, + 0.999966, + 0.988505, + 0.000034, + 0.011495 + ], + [ + 60, + 585, + 0.999969, + 0.989915, + 0.000031, + 0.010085 + ], + [ + 60, + 600, + 0.999966, + 0.98898, + 0.000034, + 0.01102 + ], + [ + 60, + 615, + 0.999959, + 0.985451, + 0.000041, + 0.014549 + ], + [ + 60, + 630, + 0.999943, + 0.977059, + 0.000057, + 0.022941 + ], + [ + 60, + 645, + 0.999911, + 0.956644, + 0.000089, + 0.043356 + ], + [ + 60, + 660, + 0.999846, + 0.900046, + 0.000154, + 0.099954 + ], + [ + 60, + 675, + 0.999701, + 0.718274, + 0.000299, + 0.281726 + ], + [ + 60, + 690, + 0.999344, + 0.23484, + 0.000656, + 0.765159 + ], + [ + 60, + 705, + 0.998318, + 0.016366, + 0.001682, + 0.983634 + ], + [ + 60, + 720, + 0.994671, + 0.198522, + 0.005329, + 0.801478 + ], + [ + 60, + 735, + 0.976078, + 0.279269, + 0.023922, + 0.72073 + ], + [ + 60, + 750, + 0.779922, + 0.250321, + 0.220079, + 0.749679 + ], + [ + 75, + 450, + 0.808036, + 0.044608, + 0.191965, + 0.955393 + ], + [ + 75, + 465, + 0.901659, + 0.245004, + 0.098341, + 0.754996 + ], + [ + 75, + 480, + 0.99917, + 0.260114, + 0.00083, + 0.739886 + ], + [ + 75, + 495, + 0.999898, + 0.132488, + 0.000102, + 0.867511 + ], + [ + 75, + 510, + 0.99997, + 0.78171, + 0.00003, + 0.21829 + ], + [ + 75, + 525, + 0.999986, + 0.919861, + 0.000014, + 0.080139 + ], + [ + 75, + 540, + 0.999991, + 0.951662, + 0.000009, + 0.048338 + ], + [ + 75, + 555, + 0.999993, + 0.960004, + 0.000007, + 0.039996 + ], + [ + 75, + 570, + 0.999993, + 0.958975, + 0.000007, + 0.041025 + ], + [ + 75, + 585, + 0.999992, + 0.950511, + 0.000008, + 0.049489 + ], + [ + 75, + 600, + 0.999989, + 0.931377, + 0.000011, + 0.068623 + ], + [ + 75, + 615, + 0.999984, + 0.891015, + 0.000016, + 0.108985 + ], + [ + 75, + 630, + 0.999973, + 0.801192, + 0.000027, + 0.198808 + ], + [ + 75, + 645, + 0.99995, + 0.594396, + 0.00005, + 0.405605 + ], + [ + 75, + 660, + 0.999896, + 0.220219, + 0.000104, + 0.77978 + ], + [ + 75, + 675, + 0.999754, + 0.004166, + 0.000246, + 0.995834 + ], + [ + 75, + 690, + 0.999316, + 0.114965, + 0.000684, + 0.885035 + ], + [ + 75, + 705, + 0.9976, + 0.232731, + 0.0024, + 0.767269 + ], + [ + 75, + 720, + 0.987331, + 0.266069, + 0.012669, + 0.733931 + ], + [ + 75, + 735, + 0.816626, + 0.22969, + 0.183374, + 0.77031 + ], + [ + 75, + 750, + 0.697659, + 0.149581, + 0.30234, + 0.850419 + ] + ], + "tfs_const": [ + [ + 0, + 400, + 0.452646, + 0.452646, + 0.103821, + 0.103821 + ], + [ + 0, + 425, + 0.604386, + 0.604386, + 0.079087, + 0.079087 + ], + [ + 0, + 450, + 0.687324, + 0.687324, + 0.065687, + 0.065687 + ], + [ + 0, + 475, + 0.732261, + 0.732261, + 0.058978, + 0.058978 + ], + [ + 0, + 500, + 0.756288, + 0.756288, + 0.056183, + 0.056183 + ], + [ + 0, + 525, + 0.767906, + 0.767906, + 0.055893, + 0.055893 + ], + [ + 0, + 550, + 0.771485, + 0.771485, + 0.057384, + 0.057384 + ], + [ + 0, + 575, + 0.769401, + 0.769401, + 0.060275, + 0.060275 + ], + [ + 0, + 600, + 0.763015, + 0.763015, + 0.064365, + 0.064365 + ], + [ + 0, + 625, + 0.753144, + 0.753144, + 0.069546, + 0.069546 + ], + [ + 0, + 650, + 0.74031, + 0.74031, + 0.075762, + 0.075762 + ], + [ + 0, + 675, + 0.724861, + 0.724861, + 0.082987, + 0.082987 + ], + [ + 0, + 700, + 0.70705, + 0.70705, + 0.091208, + 0.091208 + ], + [ + 60, + 400, + 0.901895, + 0.517047, + 0.013652, + 0.105397 + ], + [ + 60, + 425, + 0.914624, + 0.53793, + 0.012745, + 0.108208 + ], + [ + 60, + 450, + 0.919686, + 0.546805, + 0.012818, + 0.113088 + ], + [ + 60, + 475, + 0.920369, + 0.546547, + 0.013542, + 0.119859 + ], + [ + 60, + 500, + 0.918101, + 0.53944, + 0.01479, + 0.128299 + ], + [ + 60, + 525, + 0.913547, + 0.527198, + 0.016523, + 0.138199 + ], + [ + 60, + 550, + 0.907015, + 0.511115, + 0.018748, + 0.149365 + ], + [ + 60, + 575, + 0.898628, + 0.492195, + 0.021497, + 0.161615 + ], + [ + 60, + 600, + 0.888406, + 0.471245, + 0.024816, + 0.174774 + ], + [ + 60, + 625, + 0.876307, + 0.448926, + 0.028766, + 0.188673 + ], + [ + 60, + 650, + 0.862254, + 0.425789, + 0.033416, + 0.203147 + ], + [ + 60, + 675, + 0.846154, + 0.402296, + 0.038839, + 0.218037 + ], + [ + 60, + 700, + 0.827904, + 0.378832, + 0.045112, + 0.233193 + ] + ], + "tfs_cr41w": [ + [ + 0, + 400, + 0.111322, + 0.111322, + 0.448738, + 0.448738 + ], + [ + 0, + 410, + 0.101589, + 0.101589, + 0.44852, + 0.44852 + ], + [ + 0, + 420, + 0.092891, + 0.092891, + 0.447416, + 0.447416 + ], + [ + 0, + 430, + 0.084752, + 0.084752, + 0.446258, + 0.446258 + ], + [ + 0, + 440, + 0.077814, + 0.077814, + 0.444032, + 0.444032 + ], + [ + 0, + 450, + 0.071601, + 0.071601, + 0.4414, + 0.4414 + ], + [ + 0, + 460, + 0.066896, + 0.066896, + 0.436925, + 0.436925 + ], + [ + 0, + 470, + 0.063643, + 0.063643, + 0.431118, + 0.431118 + ], + [ + 0, + 480, + 0.060383, + 0.060383, + 0.427016, + 0.427016 + ], + [ + 0, + 490, + 0.058577, + 0.058577, + 0.42267, + 0.42267 + ], + [ + 0, + 500, + 0.057428, + 0.057428, + 0.419693, + 0.419693 + ], + [ + 0, + 510, + 0.057091, + 0.057091, + 0.417552, + 0.417552 + ], + [ + 0, + 520, + 0.057415, + 0.057415, + 0.415733, + 0.415733 + ], + [ + 0, + 530, + 0.05921, + 0.05921, + 0.411991, + 0.411991 + ], + [ + 0, + 540, + 0.061873, + 0.061873, + 0.408432, + 0.408432 + ], + [ + 0, + 550, + 0.063747, + 0.063747, + 0.408192, + 0.408192 + ], + [ + 0, + 560, + 0.064467, + 0.064467, + 0.411299, + 0.411299 + ], + [ + 0, + 570, + 0.064988, + 0.064988, + 0.415362, + 0.415362 + ], + [ + 0, + 580, + 0.065726, + 0.065726, + 0.419345, + 0.419345 + ], + [ + 0, + 590, + 0.066528, + 0.066528, + 0.42329, + 0.42329 + ], + [ + 0, + 600, + 0.067179, + 0.067179, + 0.42764, + 0.42764 + ], + [ + 0, + 610, + 0.067699, + 0.067699, + 0.432345, + 0.432345 + ], + [ + 0, + 620, + 0.068151, + 0.068151, + 0.437243, + 0.437243 + ], + [ + 0, + 630, + 0.068608, + 0.068608, + 0.442163, + 0.442163 + ], + [ + 0, + 640, + 0.069432, + 0.069432, + 0.446068, + 0.446068 + ], + [ + 0, + 650, + 0.070336, + 0.070336, + 0.449781, + 0.449781 + ], + [ + 0, + 660, + 0.071331, + 0.071331, + 0.453262, + 0.453262 + ], + [ + 0, + 670, + 0.072426, + 0.072426, + 0.456481, + 0.456481 + ], + [ + 0, + 680, + 0.073646, + 0.073646, + 0.459373, + 0.459373 + ], + [ + 0, + 690, + 0.075013, + 0.075013, + 0.461883, + 0.461883 + ], + [ + 0, + 700, + 0.076549, + 0.076549, + 0.463956, + 0.463956 + ], + [ + 0, + 710, + 0.078121, + 0.078121, + 0.46601, + 0.46601 + ], + [ + 0, + 720, + 0.079821, + 0.079821, + 0.467708, + 0.467708 + ], + [ + 0, + 730, + 0.081579, + 0.081579, + 0.46919, + 0.46919 + ], + [ + 0, + 740, + 0.083335, + 0.083335, + 0.470583, + 0.470583 + ], + [ + 0, + 750, + 0.085139, + 0.085139, + 0.47178, + 0.47178 + ], + [ + 0, + 760, + 0.087051, + 0.087051, + 0.472661, + 0.472661 + ], + [ + 0, + 770, + 0.089203, + 0.089203, + 0.47287, + 0.47287 + ], + [ + 0, + 780, + 0.09148, + 0.09148, + 0.472744, + 0.472744 + ], + [ + 0, + 790, + 0.093869, + 0.093869, + 0.47228, + 0.47228 + ], + [ + 0, + 800, + 0.096336, + 0.096336, + 0.471583, + 0.471583 + ] + ], + "mic_narrow": [ + [ + 0, + 400, + 0.18052, + 0.18052, + 0.69998, + 0.69998 + ], + [ + 0, + 450, + 0.156459, + 0.156459, + 0.731257, + 0.731257 + ], + [ + 0, + 500, + 0.136122, + 0.136122, + 0.713314, + 0.713314 + ], + [ + 0, + 550, + 0.124379, + 0.124379, + 0.696154, + 0.696154 + ], + [ + 0, + 600, + 0.116055, + 0.116055, + 0.698528, + 0.698528 + ], + [ + 0, + 650, + 0.111715, + 0.111715, + 0.698364, + 0.698364 + ], + [ + 0, + 700, + 0.106971, + 0.106971, + 0.713172, + 0.713172 + ] + ] + }, + "mic": [ + { + "run": "tfs_cr41w", + "label": "SiO2", + "table": [ + [ + 400, + 1.470116, + 0 + ], + [ + 410, + 1.469066, + 0 + ], + [ + 420, + 1.468094, + 0 + ], + [ + 430, + 1.46719, + 0 + ], + [ + 440, + 1.46635, + 0 + ], + [ + 450, + 1.465566, + 0 + ], + [ + 460, + 1.464833, + 0 + ], + [ + 470, + 1.464146, + 0 + ], + [ + 480, + 1.463502, + 0 + ], + [ + 490, + 1.462897, + 0 + ], + [ + 510, + 1.461789, + 0 + ], + [ + 530, + 1.460799, + 0 + ], + [ + 550, + 1.459911, + 0 + ], + [ + 570, + 1.459108, + 0 + ], + [ + 600, + 1.458038, + 0 + ], + [ + 630, + 1.4571, + 0 + ], + [ + 660, + 1.456268, + 0 + ], + [ + 690, + 1.455524, + 0 + ], + [ + 720, + 1.454851, + 0 + ], + [ + 760, + 1.454043, + 0 + ], + [ + 800, + 1.453317, + 0 + ] + ], + "wavelengthsNm": [ + 400, + 410, + 420, + 430, + 440, + 450, + 460, + 470, + 480, + 490, + 500, + 510, + 520, + 530, + 540, + 550, + 560, + 570, + 580, + 590, + 600, + 610, + 620, + 630, + 640, + 650, + 660, + 670, + 680, + 690, + 700, + 710, + 720, + 730, + 740, + 750, + 760, + 770, + 780, + 790, + 800 + ], + "n": [ + 1.470116, + 1.469066, + 1.468094, + 1.46719, + 1.46635, + 1.465566, + 1.464833, + 1.464146, + 1.463502, + 1.462897, + 1.462327, + 1.461789, + 1.46128, + 1.460799, + 1.460343, + 1.459911, + 1.4595, + 1.459108, + 1.458735, + 1.458378, + 1.458038, + 1.457712, + 1.457399, + 1.4571, + 1.456812, + 1.456535, + 1.456268, + 1.456011, + 1.455763, + 1.455524, + 1.455292, + 1.455068, + 1.454851, + 1.45464, + 1.454435, + 1.454236, + 1.454043, + 1.453854, + 1.453671, + 1.453491, + 1.453317 + ], + "k": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "run": "tfs_cr41w", + "label": "Cr", + "table": [ + [ + 400, + 2.015033, + 2.846766 + ], + [ + 420, + 2.123557, + 2.973717 + ], + [ + 440, + 2.249079, + 3.086297 + ], + [ + 450, + 2.322176, + 3.135023 + ], + [ + 470, + 2.500695, + 3.236547 + ], + [ + 490, + 2.695918, + 3.289342 + ], + [ + 500, + 2.782654, + 3.306574 + ], + [ + 510, + 2.857252, + 3.322024 + ], + [ + 520, + 2.932022, + 3.329921 + ], + [ + 540, + 3.128363, + 3.33 + ], + [ + 550, + 3.182147, + 3.329919 + ], + [ + 570, + 3.214, + 3.309016 + ], + [ + 580, + 3.219824, + 3.300317 + ], + [ + 590, + 3.215651, + 3.3 + ], + [ + 630, + 3.145058, + 3.306277 + ], + [ + 660, + 3.088778, + 3.341036 + ], + [ + 700, + 3.050498, + 3.386676 + ], + [ + 730, + 3.059394, + 3.405398 + ], + [ + 760, + 3.083976, + 3.422972 + ], + [ + 780, + 3.116284, + 3.441297 + ], + [ + 800, + 3.159071, + 3.461735 + ] + ], + "wavelengthsNm": [ + 400, + 410, + 420, + 430, + 440, + 450, + 460, + 470, + 480, + 490, + 500, + 510, + 520, + 530, + 540, + 550, + 560, + 570, + 580, + 590, + 600, + 610, + 620, + 630, + 640, + 650, + 660, + 670, + 680, + 690, + 700, + 710, + 720, + 730, + 740, + 750, + 760, + 770, + 780, + 790, + 800 + ], + "n": [ + 2.015033, + 2.067436, + 2.123557, + 2.183662, + 2.249079, + 2.322176, + 2.406752, + 2.500695, + 2.599516, + 2.695918, + 2.782654, + 2.857252, + 2.932022, + 3.032987, + 3.128363, + 3.182147, + 3.204962, + 3.214, + 3.219824, + 3.215651, + 3.203588, + 3.186439, + 3.166247, + 3.145058, + 3.12465, + 3.105739, + 3.088778, + 3.074234, + 3.06264, + 3.054545, + 3.050498, + 3.050724, + 3.054153, + 3.059394, + 3.065494, + 3.073269, + 3.083976, + 3.098506, + 3.116284, + 3.136525, + 3.159071 + ], + "k": [ + 2.846766, + 2.910241, + 2.973717, + 3.030007, + 3.086297, + 3.135023, + 3.185785, + 3.236547, + 3.262944, + 3.289342, + 3.306574, + 3.322024, + 3.329921, + 3.32996, + 3.33, + 3.329919, + 3.319467, + 3.309016, + 3.300317, + 3.3, + 3.301569, + 3.303139, + 3.304708, + 3.306277, + 3.317863, + 3.32945, + 3.341036, + 3.352446, + 3.363856, + 3.375266, + 3.386676, + 3.392917, + 3.399157, + 3.405398, + 3.411256, + 3.417114, + 3.422972, + 3.432134, + 3.441297, + 3.451516, + 3.461735 + ] + }, + { + "run": "tfs_cr41w", + "label": "BK7", + "table": [ + [ + 400, + 1.530849, + 0 + ], + [ + 410, + 1.529569, + 0 + ], + [ + 420, + 1.528385, + 0 + ], + [ + 430, + 1.527288, + 0 + ], + [ + 440, + 1.526269, + 0 + ], + [ + 450, + 1.52532, + 0 + ], + [ + 460, + 1.524433, + 0 + ], + [ + 470, + 1.523605, + 0 + ], + [ + 480, + 1.522829, + 0 + ], + [ + 490, + 1.5221, + 0 + ], + [ + 510, + 1.520769, + 0 + ], + [ + 530, + 1.519584, + 0 + ], + [ + 550, + 1.518522, + 0 + ], + [ + 570, + 1.517566, + 0 + ], + [ + 600, + 1.516295, + 0 + ], + [ + 630, + 1.515186, + 0 + ], + [ + 660, + 1.514207, + 0 + ], + [ + 690, + 1.513335, + 0 + ], + [ + 720, + 1.512549, + 0 + ], + [ + 760, + 1.511613, + 0 + ], + [ + 800, + 1.510776, + 0 + ] + ], + "wavelengthsNm": [ + 400, + 410, + 420, + 430, + 440, + 450, + 460, + 470, + 480, + 490, + 500, + 510, + 520, + 530, + 540, + 550, + 560, + 570, + 580, + 590, + 600, + 610, + 620, + 630, + 640, + 650, + 660, + 670, + 680, + 690, + 700, + 710, + 720, + 730, + 740, + 750, + 760, + 770, + 780, + 790, + 800 + ], + "n": [ + 1.530849, + 1.529569, + 1.528385, + 1.527288, + 1.526269, + 1.52532, + 1.524433, + 1.523605, + 1.522829, + 1.5221, + 1.521415, + 1.520769, + 1.52016, + 1.519584, + 1.519039, + 1.518522, + 1.518032, + 1.517566, + 1.517122, + 1.516699, + 1.516295, + 1.515909, + 1.51554, + 1.515186, + 1.514846, + 1.51452, + 1.514207, + 1.513905, + 1.513615, + 1.513335, + 1.513064, + 1.512802, + 1.512549, + 1.512304, + 1.512066, + 1.511836, + 1.511613, + 1.511395, + 1.511183, + 1.510977, + 1.510776 + ], + "k": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "run": "mic_narrow", + "label": "X", + "table": [ + [ + 450, + 1.9, + 0.1 + ], + [ + 550, + 1.8, + 0.2 + ], + [ + 650, + 1.75, + 0.25 + ] + ], + "wavelengthsNm": [ + 400, + 450, + 500, + 550, + 600, + 650, + 700 + ], + "n": [ + 1.96875, + 1.9, + 1.84375, + 1.8, + 1.76875, + 1.75, + 1.74375 + ], + "k": [ + 0.1, + 0.1, + 0.15, + 0.2, + 0.225, + 0.25, + 0.25 + ] + }, + { + "run": "F3", + "label": "F3", + "table": [ + [ + 450, + 1.9, + 0 + ], + [ + 550, + 1.8, + 0 + ], + [ + 650, + 1.75, + 0 + ] + ], + "wavelengthsNm": [ + 380, + 400, + 425, + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650, + 675, + 700, + 725, + 750 + ], + "n": [ + 2.056152, + 1.997726, + 1.94226, + 1.9, + 1.866821, + 1.840137, + 1.81825, + 1.8, + 1.784571, + 1.771369, + 1.759956, + 1.75, + 1.741245, + 1.733491, + 1.726581, + 1.720386 + ], + "k": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "run": "F3K", + "label": "F3K", + "table": [ + [ + 450, + 1.9, + 0.1 + ], + [ + 550, + 1.8, + 0.2 + ], + [ + 650, + 1.75, + 0.25 + ] + ], + "wavelengthsNm": [ + 380, + 400, + 425, + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650, + 675, + 700, + 725, + 750 + ], + "n": [ + 1.99975, + 1.96875, + 1.932812, + 1.9, + 1.870312, + 1.84375, + 1.820312, + 1.8, + 1.782812, + 1.76875, + 1.757812, + 1.75, + 1.745312, + 1.74375, + 1.745313, + 1.75 + ], + "k": [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.125, + 0.15, + 0.175, + 0.2, + 0.2125, + 0.225, + 0.2375, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25 + ] + }, + { + "run": "F4", + "label": "F4", + "table": [ + [ + 400, + 2, + 0 + ], + [ + 500, + 1.9, + 0 + ], + [ + 600, + 1.85, + 0 + ], + [ + 700, + 1.82, + 0 + ] + ], + "wavelengthsNm": [ + 380, + 400, + 425, + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650, + 675, + 700, + 725, + 750 + ], + "n": [ + 2.032913, + 2, + 1.966821, + 1.940137, + 1.918249, + 1.9, + 1.884571, + 1.871369, + 1.859956, + 1.85, + 1.841162, + 1.833313, + 1.8263, + 1.82, + 1.814312, + 1.809152 + ], + "k": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "run": "F4K", + "label": "F4K", + "table": [ + [ + 400, + 2, + 0.05 + ], + [ + 500, + 1.9, + 0.03 + ], + [ + 600, + 1.85, + 0.02 + ], + [ + 700, + 1.82, + 0.01 + ] + ], + "wavelengthsNm": [ + 380, + 400, + 425, + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650, + 675, + 700, + 725, + 750 + ], + "n": [ + 2.032913, + 2, + 1.966821, + 1.940137, + 1.918249, + 1.9, + 1.884571, + 1.871369, + 1.859956, + 1.85, + 1.841162, + 1.833313, + 1.8263, + 1.82, + 1.814312, + 1.809152 + ], + "k": [ + 0.05, + 0.05, + 0.045, + 0.04, + 0.035, + 0.03, + 0.0275, + 0.025, + 0.0225, + 0.02, + 0.0175, + 0.015, + 0.0125, + 0.01, + 0.01, + 0.01 + ] + }, + { + "run": "F5", + "label": "F5", + "table": [ + [ + 387.5, + 2.144, + 0 + ], + [ + 442.8, + 2.085, + 0 + ], + [ + 563.6, + 1.994, + 0 + ], + [ + 619.9, + 1.969, + 0 + ], + [ + 774.9, + 1.929, + 0 + ] + ], + "wavelengthsNm": [ + 380, + 400, + 425, + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650, + 675, + 700, + 725, + 750 + ], + "n": [ + 2.153197, + 2.129366, + 2.102456, + 2.078301, + 2.056509, + 2.036757, + 2.018779, + 2.002352, + 1.988342, + 1.977044, + 1.967124, + 1.958654, + 1.951234, + 1.944684, + 1.938864, + 1.93366 + ], + "k": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "run": "F5A", + "label": "F5A", + "table": [ + [ + 400, + 2, + 0 + ], + [ + 500, + 1.99, + 0 + ], + [ + 550, + 1.97, + 0 + ], + [ + 600, + 1.93, + 0 + ], + [ + 700, + 1.75, + 0 + ] + ], + "wavelengthsNm": [ + 380, + 400, + 425, + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650, + 675, + 700, + 725, + 750 + ], + "n": [ + 2.000878, + 2, + 1.998603, + 1.996717, + 1.994045, + 1.99, + 1.983246, + 1.97, + 1.953946, + 1.929998, + 1.902168, + 1.866127, + 1.817806, + 1.750006, + 1.648757, + 1.483157 + ], + "k": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "run": "R5", + "label": "R5", + "table": [ + [ + 400, + 1.5, + 0 + ], + [ + 500, + 1.55, + 0 + ], + [ + 550, + 1.58, + 0 + ], + [ + 600, + 1.62, + 0 + ], + [ + 700, + 1.7, + 0 + ] + ], + "wavelengthsNm": [ + 380, + 400, + 425, + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650, + 675, + 700, + 725, + 750 + ], + "n": [ + 1.491312, + 1.5, + 1.511424, + 1.523524, + 1.53636, + 1.55, + 1.564518, + 1.58, + 1.598471, + 1.62, + 1.64, + 1.66, + 1.68, + 1.7, + 1.72, + 1.74 + ], + "k": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "run": "T2", + "label": "T2", + "table": [ + [ + 450, + 1.9, + 0.1 + ], + [ + 650, + 1.75, + 0.25 + ] + ], + "wavelengthsNm": [ + 380, + 400, + 425, + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650, + 675, + 700, + 725, + 750 + ], + "n": [ + 1.9, + 1.9, + 1.9, + 1.9, + 1.88125, + 1.8625, + 1.84375, + 1.825, + 1.80625, + 1.7875, + 1.76875, + 1.75, + 1.75, + 1.75, + 1.75, + 1.75 + ], + "k": [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.11875, + 0.1375, + 0.15625, + 0.175, + 0.19375, + 0.2125, + 0.23125, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25 + ] + }, + { + "run": "K1", + "label": "K1", + "table": [ + [ + 450, + 1.9, + 0.5 + ], + [ + 550, + 1.8, + 0.4 + ], + [ + 650, + 1.75, + 0.3 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.9, + 1.870312, + 1.84375, + 1.820312, + 1.8, + 1.782812, + 1.76875, + 1.757812, + 1.75 + ], + "k": [ + 0.5, + 0.475, + 0.45, + 0.425, + 0.4, + 0.375, + 0.35, + 0.325, + 0.3 + ] + }, + { + "run": "K2", + "label": "K2", + "table": [ + [ + 450, + 1.9, + 0.5 + ], + [ + 550, + 1.8, + 0.5 + ], + [ + 650, + 1.75, + 0.5 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.9, + 1.870312, + 1.84375, + 1.820312, + 1.8, + 1.782812, + 1.76875, + 1.757812, + 1.75 + ], + "k": [ + 0.5, + 0.5, + 0.5, + 0.5, + 0.5, + 0.5, + 0.5, + 0.5, + 0.5 + ] + }, + { + "run": "K3", + "label": "K3", + "table": [ + [ + 450, + 1.9, + 0.001 + ], + [ + 550, + 1.8, + 0.002 + ], + [ + 650, + 1.75, + 0.003 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.9, + 1.866821, + 1.840137, + 1.81825, + 1.8, + 1.784571, + 1.771369, + 1.759956, + 1.75 + ], + "k": [ + 0.001, + 0.00125, + 0.0015, + 0.00175, + 0.002, + 0.00225, + 0.0025, + 0.00275, + 0.003 + ] + }, + { + "run": "K4", + "label": "K4", + "table": [ + [ + 450, + 1.9, + 0.01 + ], + [ + 550, + 1.8, + 0.02 + ], + [ + 650, + 1.75, + 0.03 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.9, + 1.866821, + 1.840137, + 1.81825, + 1.8, + 1.784571, + 1.771369, + 1.759956, + 1.75 + ], + "k": [ + 0.01, + 0.0125, + 0.015, + 0.0175, + 0.02, + 0.0225, + 0.025, + 0.0275, + 0.03 + ] + }, + { + "run": "K5", + "label": "K5", + "table": [ + [ + 450, + 1.9, + 0.05 + ], + [ + 550, + 1.8, + 0.06 + ], + [ + 650, + 1.75, + 0.07 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.9, + 1.866821, + 1.840137, + 1.81825, + 1.8, + 1.784571, + 1.771369, + 1.759956, + 1.75 + ], + "k": [ + 0.05, + 0.0525, + 0.055, + 0.0575, + 0.06, + 0.0625, + 0.065, + 0.0675, + 0.07 + ] + }, + { + "run": "K6", + "label": "K6", + "table": [ + [ + 450, + 1.9, + 0.1 + ], + [ + 550, + 1.8, + 0.11 + ], + [ + 650, + 1.75, + 0.12 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.9, + 1.866821, + 1.840137, + 1.81825, + 1.8, + 1.784571, + 1.771369, + 1.759956, + 1.75 + ], + "k": [ + 0.1, + 0.1025, + 0.105, + 0.1075, + 0.11, + 0.1125, + 0.115, + 0.1175, + 0.12 + ] + }, + { + "run": "K7", + "label": "K7", + "table": [ + [ + 450, + 1.9, + 0.25 + ], + [ + 550, + 1.8, + 0.2 + ], + [ + 650, + 1.75, + 0.1 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.9, + 1.870312, + 1.84375, + 1.820312, + 1.8, + 1.782812, + 1.76875, + 1.757812, + 1.75 + ], + "k": [ + 0.25, + 0.2375, + 0.225, + 0.2125, + 0.2, + 0.175, + 0.15, + 0.125, + 0.1 + ] + }, + { + "run": "K8", + "label": "K8", + "table": [ + [ + 450, + 1.9, + 0.2 + ], + [ + 550, + 1.8, + 0.21 + ], + [ + 650, + 1.75, + 0.22 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.9, + 1.870312, + 1.84375, + 1.820312, + 1.8, + 1.782812, + 1.76875, + 1.757812, + 1.75 + ], + "k": [ + 0.2, + 0.2025, + 0.205, + 0.2075, + 0.21, + 0.2125, + 0.215, + 0.2175, + 0.22 + ] + }, + { + "run": "K9", + "label": "K9", + "table": [ + [ + 450, + 1.9, + 0.001 + ], + [ + 550, + 1.8, + 0.001 + ], + [ + 650, + 1.75, + 0.001 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.9, + 1.866821, + 1.840137, + 1.81825, + 1.8, + 1.784571, + 1.771369, + 1.759956, + 1.75 + ], + "k": [ + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001, + 0.001 + ] + }, + { + "run": "K10", + "label": "K10", + "table": [ + [ + 450, + 1.9, + 0.1 + ], + [ + 550, + 1.8, + 0.25 + ], + [ + 650, + 1.75, + 0.2 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.9, + 1.870312, + 1.84375, + 1.820312, + 1.8, + 1.782812, + 1.76875, + 1.757812, + 1.75 + ], + "k": [ + 0.1, + 0.1375, + 0.175, + 0.2125, + 0.25, + 0.2375, + 0.225, + 0.2125, + 0.2 + ] + }, + { + "run": "K11", + "label": "K11", + "table": [ + [ + 450, + 1.9, + 0.25 + ], + [ + 550, + 1.8, + 0.1 + ], + [ + 650, + 1.75, + 0.2 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.9, + 1.870312, + 1.84375, + 1.820312, + 1.8, + 1.782812, + 1.76875, + 1.757812, + 1.75 + ], + "k": [ + 0.25, + 0.2125, + 0.175, + 0.1375, + 0.1, + 0.125, + 0.15, + 0.175, + 0.2 + ] + }, + { + "run": "K12", + "label": "K12", + "table": [ + [ + 400, + 1.5, + 0.1 + ], + [ + 500, + 1.55, + 0.2 + ], + [ + 550, + 1.58, + 0.3 + ], + [ + 600, + 1.62, + 0.4 + ], + [ + 700, + 1.7, + 0.5 + ] + ], + "wavelengthsNm": [ + 400, + 425, + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650, + 675, + 700 + ], + "n": [ + 1.5, + 1.511797, + 1.524062, + 1.536797, + 1.55, + 1.563945, + 1.58, + 1.59918, + 1.62, + 1.640703, + 1.660937, + 1.680703, + 1.7 + ], + "k": [ + 0.1, + 0.125, + 0.15, + 0.175, + 0.2, + 0.25, + 0.3, + 0.35, + 0.4, + 0.425, + 0.45, + 0.475, + 0.5 + ] + }, + { + "run": "TA", + "label": "TA", + "table": [ + [ + 400, + 2.1587, + 0.000761 + ], + [ + 420, + 2.1587, + 0.000761 + ], + [ + 440, + 2.1587, + 0.000761 + ], + [ + 460, + 2.1587, + 0.000761 + ], + [ + 480, + 2.1587, + 0.000761 + ], + [ + 500, + 2.1587, + 0.000761 + ], + [ + 520, + 2.151057, + 0.000795 + ], + [ + 540, + 2.144219, + 0.00083 + ], + [ + 560, + 2.138067, + 0.000864 + ], + [ + 580, + 2.132591, + 0.000898 + ], + [ + 600, + 2.127567, + 0.000933 + ], + [ + 620, + 2.123051, + 0.000967 + ], + [ + 640, + 2.118899, + 0.001002 + ], + [ + 660, + 2.115084, + 0.001037 + ], + [ + 680, + 2.111668, + 0.001072 + ], + [ + 700, + 2.108457, + 0.001107 + ] + ], + "wavelengthsNm": [ + 410, + 430, + 450, + 470, + 490, + 510, + 530, + 550, + 570, + 590, + 610, + 630, + 650, + 670, + 690 + ], + "n": [ + 2.1587, + 2.1587, + 2.1587, + 2.1587, + 2.1587, + 2.156753, + 2.147545, + 2.141064, + 2.135251, + 2.130026, + 2.12525, + 2.120932, + 2.116952, + 2.11333, + 2.110038 + ], + "k": [ + 0.000761, + 0.000761, + 0.000761, + 0.000761, + 0.000761, + 0.000778, + 0.000812, + 0.000847, + 0.000881, + 0.000915, + 0.00095, + 0.000984, + 0.001019, + 0.001055, + 0.001089 + ] + }, + { + "run": "A", + "label": "A", + "table": [ + [ + 450, + 1.9, + 0.3 + ], + [ + 550, + 1.8, + 0 + ], + [ + 650, + 1.75, + 0 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.9, + 1.870312, + 1.84375, + 1.820312, + 1.8, + 1.782812, + 1.76875, + 1.757812, + 1.75 + ], + "k": [ + 0.3, + 0.225, + 0.15, + 0.075, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "run": "B", + "label": "B", + "table": [ + [ + 450, + 4, + 0.3 + ], + [ + 550, + 3.9, + 0.3 + ], + [ + 650, + 3.85, + 0.3 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 4, + 3.966821, + 3.940137, + 3.91825, + 3.9, + 3.884571, + 3.871369, + 3.859956, + 3.85 + ], + "k": [ + 0.3, + 0.3, + 0.3, + 0.3, + 0.3, + 0.3, + 0.3, + 0.3, + 0.3 + ] + }, + { + "run": "C", + "label": "C", + "table": [ + [ + 450, + 1.3, + 0.15 + ], + [ + 550, + 1.2, + 0.15 + ], + [ + 650, + 1.15, + 0.15 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.3, + 1.270313, + 1.24375, + 1.220313, + 1.2, + 1.182813, + 1.16875, + 1.157812, + 1.15 + ], + "k": [ + 0.15, + 0.15, + 0.15, + 0.15, + 0.15, + 0.15, + 0.15, + 0.15, + 0.15 + ] + }, + { + "run": "D", + "label": "D", + "table": [ + [ + 450, + 1.9, + 0.18 + ], + [ + 550, + 1.8, + 0.18 + ], + [ + 650, + 1.75, + 0.18 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.9, + 1.870312, + 1.84375, + 1.820312, + 1.8, + 1.782812, + 1.76875, + 1.757812, + 1.75 + ], + "k": [ + 0.18, + 0.18, + 0.18, + 0.18, + 0.18, + 0.18, + 0.18, + 0.18, + 0.18 + ] + }, + { + "run": "E", + "label": "E", + "table": [ + [ + 450, + 1.9, + 0.19 + ], + [ + 550, + 1.8, + 0.19 + ], + [ + 650, + 1.75, + 0.19 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.9, + 1.870312, + 1.84375, + 1.820312, + 1.8, + 1.782812, + 1.76875, + 1.757812, + 1.75 + ], + "k": [ + 0.19, + 0.19, + 0.19, + 0.19, + 0.19, + 0.19, + 0.19, + 0.19, + 0.19 + ] + }, + { + "run": "F", + "label": "F", + "table": [ + [ + 450, + 1.9, + 0.2 + ], + [ + 550, + 1.8, + 0.2 + ], + [ + 650, + 1.75, + 0.2 + ] + ], + "wavelengthsNm": [ + 450, + 475, + 500, + 525, + 550, + 575, + 600, + 625, + 650 + ], + "n": [ + 1.9, + 1.870312, + 1.84375, + 1.820312, + 1.8, + 1.782812, + 1.76875, + 1.757812, + 1.75 + ], + "k": [ + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2 + ] + } + ] + } +} diff --git a/tests/reference/codev/mic_narrow.seq b/tests/reference/codev/mic_narrow.seq new file mode 100644 index 00000000..66d84945 --- /dev/null +++ b/tests/reference/codev/mic_narrow.seq @@ -0,0 +1,17 @@ +MUL +MDA +PHT Y +TIT 'MIC narrower than WL' +MIC + MWL 450 550 650 + 'X' 1.9 1.8 1.75 + EXT 'X' 0.1 0.2 0.25 +END +INC 1 +COA 50 0 'X' +SUB 1.52 +WL 400 450 500 550 600 650 700 +REF 550 +MPR +MAN +MEX diff --git a/tests/reference/codev/tfs_agmir.mul b/tests/reference/codev/tfs_agmir.mul new file mode 100644 index 00000000..8571f7b9 --- /dev/null +++ b/tests/reference/codev/tfs_agmir.mul @@ -0,0 +1,140 @@ + 6 0.000000000 +TFStudio protected silver mirror + 07-Oct-26 PLAINTEXT + 0 1102 38685 667 0 + 1 4 1 0 1 0 0 0 + 0 0 2 2 0 0 1 0 + 5 13 230 0 0 0 25 0 + 0 0 4 0 2 0 5 4 + 0 1 5 122 1 0 0 0 + 0 0 2 95 3 + 0 0 0 0 0 0 0 0 + 0 0 0 0 + 0 0 0 0 0 + -1 122 149 176 203 + 13 14 0 0 0 0 0 0 + 0 0 0 0 0 0 41 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 68 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 95 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 0.000000000 0.6000000000 0.1390791929E-01 + 0.1074183328E-01 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 0.9851084505 0.5000000000 + 0.000000000 0.9800000191 0.000000000 1.000000000 + 0.000000000 0.7853981853 0.000000000 0.000000000 + 0.000000000 + 0.4000000060 0.4499999881 0.5000000000 0.5500000119 + 0.6000000238 0.6499999762 0.6999999881 0.7500000000 + 0.8000000119 0.8500000238 0.8999999762 0.9499999881 + 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 0.000000000 0.2308560165 0.1380950026E-01 0.2667698264E-01 + 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 1.000000000 1.458037999 0.5523800105E-01 3.201237917 + 1.516294999 + 8SiO2 Ag Cr BK7 SiO2 Ag Cr + BK7 + 0.4000000000 0.4500000000 0.5000000000 0.5500000000 + 0.6000000000 0.6500000000 0.7000000000 0.7500000000 + 0.8000000000 0.8500000000 0.9000000000 0.9500000000 + 1.000000000 0.6013470421-153 1.470116000 1.465566000 + 1.462326000 1.459911000 1.458038000 1.456535000 + 1.455292000 1.454237000 1.453317000 1.452498000 + 1.451754000 1.451065000 1.450417000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.6013470017-153 0.5000000000E-01 0.4000000000E-01 0.5000000000E-01 + 0.5994900000E-01 0.5523800000E-01 0.5257500000E-01 0.4094400000E-01 + 0.3024700000E-01 0.3752900000E-01 0.4000000000E-01 0.4000000000E-01 + 0.4000000000E-01 0.4000000000E-01 2.103964000 2.648112000 + 3.130684000 3.598180000 4.010526000 4.409558000 + 4.802551000 5.195858000 5.567119000 5.964165000 + 6.373863000 6.741926000 7.113226000 0.6013470017-153 + 2.015033000 2.322176000 2.782654000 3.182147000 + 3.201238000 3.103746000 3.050498000 3.074845000 + 3.159071000 3.243574000 3.310132000 3.367711000 + 3.434528000 2.846766000 3.135023000 3.306574000 + 3.329919000 3.300000000 3.329583000 3.386676000 + 3.416057000 3.461735000 3.497815000 3.524656000 + 3.556707000 3.572290000 0.6013470019-153 1.530849000 + 1.525320000 1.521414000 1.518522000 1.516295000 + 1.514520000 1.513064000 1.511836000 1.510776000 + 1.509840000 1.508997000 1.508223000 1.507502000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.6013470421-153 1.470115999 1.465566001 + 1.462326000 1.459910999 1.458037999 1.456535001 + 1.455292000 1.454237000 1.453317000 1.452498000 + 1.451754000 1.451065000 1.450417000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.6013470017-153 0.5000000075E-01 0.3999999911E-01 0.5000000075E-01 + 0.5994899943E-01 0.5523800105E-01 0.5257499963E-01 0.4094399884E-01 + 0.3024699911E-01 0.3752899915E-01 0.3999999911E-01 0.3999999911E-01 + 0.3999999911E-01 0.3999999911E-01 2.103964065 2.648111870 + 3.130684000 3.598180098 4.010526190 4.409557810 + 4.802550906 5.195858000 5.567119095 5.964165195 + 6.373862805 6.741925912 7.113226000 0.6013470017-153 + 2.015033007 2.322175980 2.782654047 3.182147026 + 3.201237917 3.103745937 3.050498009 3.074845076 + 3.159070969 3.243573904 3.310132027 3.367711067 + 3.434528112 2.846766034 3.135022931 3.306574000 + 3.329918993 3.300000014 3.329582986 3.386675986 + 3.416057000 3.461735009 3.497815013 3.524655987 + 3.556706992 3.572290000 0.6013470019-153 1.530848999 + 1.525320001 1.521414000 1.518521999 1.516294999 + 1.514520001 1.513064000 1.511836000 1.510776000 + 1.509840000 1.508997000 1.508223000 1.507502000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 + 4SiO2 Ag Cr BK7 + 1.000000000 0.6013470421-153 0.6013470017-153 0.6013470017-153 + 0.6013470019-153 + 1.000000000 1.000000000 diff --git a/tests/reference/codev/tfs_agmir.seq b/tests/reference/codev/tfs_agmir.seq new file mode 100644 index 00000000..b6efff97 --- /dev/null +++ b/tests/reference/codev/tfs_agmir.seq @@ -0,0 +1,34 @@ +! TFStudio protected silver mirror +! Written by TFStudio. Layers run from the incident medium to the substrate, +! thicknesses are physical, in nm. SAV at the end writes the .mul file. +MUL +MDA +PHT Y +TIT 'TFStudio protected silver mirror' +MIC + MWL 400 450 500 550 600 650 700 750 & + 800 850 900 950 1000 + 'SiO2' 1.470116 1.465566 1.462326 1.459911 1.458038 1.456535 1.455292 1.454237 & + 1.453317 1.452498 1.451754 1.451065 1.450417 + 'Ag' 0.05 0.04 0.05 0.059949 0.055238 0.052575 0.040944 0.030247 & + 0.037529 0.04 0.04 0.04 0.04 + EXT 'Ag' 2.103964 2.648112 3.130684 3.59818 4.010526 4.409558 4.802551 5.195858 & + 5.567119 5.964165 6.373863 6.741926 7.113226 + 'Cr' 2.015033 2.322176 2.782654 3.182147 3.201238 3.103746 3.050498 3.074845 & + 3.159071 3.243574 3.310132 3.367711 3.434528 + EXT 'Cr' 2.846766 3.135023 3.306574 3.329919 3.3 3.329583 3.386676 3.416057 & + 3.461735 3.497815 3.524656 3.556707 3.57229 + 'BK7' 1.530849 1.52532 1.521414 1.518522 1.516295 1.51452 1.513064 1.511836 & + 1.510776 1.50984 1.508997 1.508223 1.507502 +END +INC 1 +COA 95 0 'SiO2' +COA 150 0 'Ag' +COA 5 0 'Cr' +SUB 'BK7' +WL 400 450 500 550 600 650 700 750 & + 800 850 900 950 1000 +REF 600 +ANG 0 45 +SAV tfs_agmir +MEX diff --git a/tests/reference/codev/tfs_ar4.mul b/tests/reference/codev/tfs_ar4.mul new file mode 100644 index 00000000..9305098a --- /dev/null +++ b/tests/reference/codev/tfs_ar4.mul @@ -0,0 +1,133 @@ + 6 0.000000000 +TFStudio 4-layer AR Ta2O5 SiO2 on BK7 + 07-Oct-26 PLAINTEXT + 0 1102 38685 667 0 + 1 4 1 0 1 0 0 0 + 0 0 2 2 0 0 1 0 + 6 16 215 0 0 0 25 0 + 0 0 5 0 2 0 5 5 + 0 1 4 116 1 0 0 0 + 0 0 0 83 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 + 0 0 100 0 0 0 + -1 116 149 116 149 182 + 16 17 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 50 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 83 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 0.000000000 0.5500000000 0.000000000 + 0.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 0.000000000 0.5000000000 + 0.000000000 0.9800000191 0.000000000 1.000000000 + 0.000000000 0.5235987902 0.000000000 0.000000000 + 0.000000000 + 0.4000000060 0.4199999869 0.4399999976 0.4600000083 + 0.4799999893 0.5000000000 0.5199999809 0.5400000215 + 0.5600000024 0.5799999833 0.6000000238 0.6200000048 + 0.6399999857 0.6600000262 0.6800000072 0.6999999881 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.2455305441 0.4359984204 0.8122415938E-01 + 0.5605693828E-01 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 + 1.000000000 1.459911343 2.141063672 1.459911343 + 2.141063672 1.518522332 + 6SiO2 Ta2O5 BK7 SiO2 Ta2O5 BK7 + 0.4000000000 0.4200000000 0.4400000000 0.4600000000 + 0.4800000000 0.5000000000 0.5200000000 0.5400000000 + 0.5600000000 0.5800000000 0.6000000000 0.6200000000 + 0.6400000000 0.6600000000 0.6800000000 0.7000000000 + 0.6013470421-153 1.470116000 1.468094000 1.466350000 + 1.464833000 1.463502000 1.462326000 1.461280000 + 1.460344000 1.459500000 1.458735000 1.458038000 + 1.457399000 1.456812000 1.456268000 1.455764000 + 1.455292000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.6013590559-153 2.158700000 2.158700000 + 2.158700000 2.158700000 2.158700000 2.158700000 + 2.151057000 2.144219000 2.138067000 2.132591000 + 2.127567000 2.123051000 2.118899000 2.115084000 + 2.111668000 2.108457000 0.7610000000E-03 0.7610000000E-03 + 0.7610000000E-03 0.7610000000E-03 0.7610000000E-03 0.7610000000E-03 + 0.7950000000E-03 0.8300000000E-03 0.8640000000E-03 0.8980000000E-03 + 0.9330000000E-03 0.9670000000E-03 0.1002000000E-02 0.1037000000E-02 + 0.1072000000E-02 0.1107000000E-02 0.6013470019-153 1.530849000 + 1.528385000 1.526269000 1.524433000 1.522829000 + 1.521414000 1.520160000 1.519039000 1.518032000 + 1.517122000 1.516295000 1.515539000 1.514846000 + 1.514207000 1.513615000 1.513064000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.6013470421-153 + 1.470115999 1.468094001 1.466350000 1.464832999 + 1.463502001 1.462326000 1.461280001 1.460343999 + 1.459500000 1.458735001 1.458037999 1.457399000 + 1.456812000 1.456267999 1.455764000 1.455292000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.6013590559-153 2.158700000 2.158700000 2.158700000 + 2.158700000 2.158700000 2.158700000 2.151057243 + 2.144218993 2.138066999 2.132591004 2.127566994 + 2.123050999 2.118899003 2.115083995 2.111667999 + 2.108457002 0.7610000000E-03 0.7610000000E-03 0.7610000000E-03 + 0.7610000000E-03 0.7610000000E-03 0.7610000000E-03 0.7949999676E-03 + 0.8300000365E-03 0.8640000041E-03 0.8979999716E-03 0.9330000405E-03 + 0.9670000083E-03 0.1001999975E-02 0.1037000046E-02 0.1072000013E-02 + 0.1106999979E-02 0.6013470019-153 1.530848999 1.528385001 + 1.526269000 1.524432999 1.522829001 1.521414000 + 1.520160001 1.519038999 1.518032000 1.517122001 + 1.516294999 1.515539000 1.514846000 1.514206999 + 1.513615000 1.513064000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 + 5SiO2 Ta2O5 SiO2 Ta2O5 BK7 + 1.000000000 0.6013470421-153 0.6013590559-153 0.6013470421-153 + 0.6013590559-153 0.6013470019-153 + 1.000000000 1.000000000 diff --git a/tests/reference/codev/tfs_ar4.seq b/tests/reference/codev/tfs_ar4.seq new file mode 100644 index 00000000..a08135bb --- /dev/null +++ b/tests/reference/codev/tfs_ar4.seq @@ -0,0 +1,31 @@ +! TFStudio 4-layer AR Ta2O5 SiO2 on BK7 +! Written by TFStudio. Layers run from the incident medium to the substrate, +! thicknesses are physical, in nm. SAV at the end writes the .mul file. +MUL +MDA +PHT Y +TIT 'TFStudio 4-layer AR Ta2O5 SiO2 on BK7' +MIC + MWL 400 420 440 460 480 500 520 540 & + 560 580 600 620 640 660 680 700 + 'SiO2' 1.470116 1.468094 1.46635 1.464833 1.463502 1.462326 1.46128 1.460344 & + 1.4595 1.458735 1.458038 1.457399 1.456812 1.456268 1.455764 1.455292 + 'Ta2O5' 2.1587 2.1587 2.1587 2.1587 2.1587 2.1587 2.151057 2.144219 & + 2.138067 2.132591 2.127567 2.123051 2.118899 2.115084 2.111668 2.108457 + EXT 'Ta2O5' 0.000761 0.000761 0.000761 0.000761 0.000761 0.000761 0.000795 0.00083 & + 0.000864 0.000898 0.000933 0.000967 0.001002 0.001037 0.001072 0.001107 + 'BK7' 1.530849 1.528385 1.526269 1.524433 1.522829 1.521414 1.52016 1.519039 & + 1.518032 1.517122 1.516295 1.515539 1.514846 1.514207 1.513615 1.513064 +END +INC 1 +COA 92.5 0 'SiO2' +COA 112 100 'Ta2O5' +COA 30.6 0 'SiO2' +COA 14.4 0 'Ta2O5' +SUB 'BK7' +WL 400 420 440 460 480 500 520 540 & + 560 580 600 620 640 660 680 700 +REF 550 +ANG 0 30 +SAV tfs_ar4 +MEX diff --git a/tests/reference/codev/tfs_const.mul b/tests/reference/codev/tfs_const.mul new file mode 100644 index 00000000..848af360 --- /dev/null +++ b/tests/reference/codev/tfs_const.mul @@ -0,0 +1,78 @@ + 6 0.000000000 +TFStudio constant indices, metal film at 60 deg + 07-Oct-26 PLAINTEXT + 0 1102 38685 667 0 + 1 4 1 0 1 0 0 0 + 0 0 2 2 0 0 1 0 + 6 13 68 0 0 0 25 0 + 0 0 5 0 2 0 5 5 + 0 1 2 41 1 0 0 0 + 0 0 1 14 3 + 0 0 0 0 0 0 0 0 + 0 0 0 0 + 0 0 0 100 0 0 + -1 -1 -1 -1 -1 41 + 13 14 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 0.000000000 0.5500000000 0.6717900773E-01 + 0.6717900773E-01 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 0.4276399899 0.5000000000 + 0.000000000 0.9800000191 0.000000000 0.000000000 + 0.000000000 1.047197580 0.000000000 0.000000000 + 0.000000000 + 0.4000000060 0.4250000119 0.4499999881 0.4749999940 + 0.5000000000 0.5249999762 0.5500000119 0.5749999881 + 0.6000000238 0.6250000000 0.6499999762 0.6750000119 + 0.6999999881 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 0.000000000 0.2499545353 0.2499054499 0.6000000000E-01 + 0.2499054499 0.000000000 + 0.000000000 0.000000000 0.000000000 4.460000038 + 0.000000000 0.000000000 + 1.000000000 2.349999905 1.379999995 2.750000000 + 1.379999995 1.518521999 + 2BK7 BK7 + 0.4000000000 0.4250000000 0.4500000000 0.4750000000 + 0.5000000000 0.5250000000 0.5500000000 0.5750000000 + 0.6000000000 0.6250000000 0.6500000000 0.6750000000 + 0.7000000000 0.6013470019-153 1.530849000 1.527827000 + 1.525320000 1.523211000 1.521414000 1.519868000 + 1.518522000 1.517341000 1.516295000 1.515361000 + 1.514520000 1.513759000 1.513064000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.6013470019-153 1.530848999 1.527826999 1.525320001 + 1.523211000 1.521414000 1.519868001 1.518521999 + 1.517341001 1.516294999 1.515361000 1.514520001 + 1.513759000 1.513064000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 1BK7 + 1.000000000 2.349999905 1.379999995 2.750000000 + 1.379999995 0.6013470019-153 + 1.000000000 1.000000000 diff --git a/tests/reference/codev/tfs_const.seq b/tests/reference/codev/tfs_const.seq new file mode 100644 index 00000000..c04d854f --- /dev/null +++ b/tests/reference/codev/tfs_const.seq @@ -0,0 +1,25 @@ +! TFStudio constant indices, metal film at 60 deg +! Written by TFStudio. Layers run from the incident medium to the substrate, +! thicknesses are physical, in nm. SAV at the end writes the .mul file. +MUL +MDA +PHT Y +TIT 'TFStudio constant indices, metal film at 60 deg' +MIC + MWL 400 425 450 475 500 525 550 575 & + 600 625 650 675 700 + 'BK7' 1.530849 1.527827 1.52532 1.523211 1.521414 1.519868 1.518522 1.517341 & + 1.516295 1.515361 1.51452 1.513759 1.513064 +END +INC 1 +COA 58.5 0 2.35 +COA 99.6 0 1.38 +COA 12 100 2.75 4.46 +COA 99.6 0 1.38 +SUB 'BK7' +WL 400 425 450 475 500 525 550 575 & + 600 625 650 675 700 +REF 550 +ANG 0 60 +SAV tfs_const +MEX diff --git a/tests/reference/codev/tfs_cr41.mul b/tests/reference/codev/tfs_cr41.mul new file mode 100644 index 00000000..98ba6bad --- /dev/null +++ b/tests/reference/codev/tfs_cr41.mul @@ -0,0 +1,180 @@ + 6 0.000000000 +TFStudio Cr SiO2 on BK7, 41 wavelengths + 07-Oct-26 PLAINTEXT + 0 1102 38685 667 0 + 1 4 1 0 1 0 0 0 + 0 0 2 2 0 0 1 0 + 4 21 322 0 0 0 25 0 + 0 0 3 0 1 0 5 3 + 0 1 6 193 1 0 0 0 + 0 0 5 150 3 + 0 0 0 0 0 0 0 0 + 0 0 0 0 + 0 0 0 0 + -1 193 236 279 + 21 22 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 65 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 21 86 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 129 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 21 150 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 0.000000000 0.5500000000 0.6976594876 + 0.1495805912 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 0.5763797493 0.5000000000 + 0.000000000 0.9800000191 0.000000000 1.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 0.4000000060 0.4099999964 0.4199999869 0.4300000072 + 0.4399999976 0.4499999881 0.4600000083 0.4699999988 + 0.4799999893 0.4900000095 0.5000000000 0.5099999905 + 0.5199999809 0.5299999714 0.5400000215 0.5500000119 + 0.5600000024 0.5699999928 0.5799999833 0.5899999738 + 0.6000000238 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 0.000000000 0.2123506908 0.4628577492E-01 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 1.000000000 1.459910999 3.182147026 1.518521999 + 6SiO2 Cr BK7 SiO2 Cr BK7 + 0.4000000000 0.4100000000 0.4200000000 0.4300000000 + 0.4400000000 0.4500000000 0.4600000000 0.4700000000 + 0.4800000000 0.4900000000 0.5100000000 0.5300000000 + 0.5500000000 0.5700000000 0.6000000000 0.6300000000 + 0.6600000000 0.6900000000 0.7200000000 0.7600000000 + 0.8000000000 0.6013470421-153 1.470116000 1.469066000 + 1.468094000 1.467190000 1.466350000 1.465566000 + 1.464833000 1.464146000 1.463502000 1.462897000 + 1.461789000 1.460799000 1.459911000 1.459108000 + 1.458038000 1.457100000 1.456268000 1.455524000 + 1.454851000 1.454043000 1.453317000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.4000000000 0.4200000000 0.4400000000 0.4500000000 + 0.4700000000 0.4900000000 0.5000000000 0.5100000000 + 0.5200000000 0.5400000000 0.5500000000 0.5700000000 + 0.5800000000 0.5900000000 0.6300000000 0.6600000000 + 0.7000000000 0.7300000000 0.7600000000 0.7800000000 + 0.8000000000 0.6013470017-153 2.015033000 2.123557000 + 2.249079000 2.322176000 2.500695000 2.695918000 + 2.782654000 2.857252000 2.932022000 3.128363000 + 3.182147000 3.214000000 3.219824000 3.215651000 + 3.145058000 3.088778000 3.050498000 3.059394000 + 3.083976000 3.116284000 3.159071000 2.846766000 + 2.973717000 3.086297000 3.135023000 3.236547000 + 3.289342000 3.306574000 3.322024000 3.329921000 + 3.330000000 3.329919000 3.309016000 3.300317000 + 3.300000000 3.306277000 3.341036000 3.386676000 + 3.405398000 3.422972000 3.441297000 3.461735000 + 0.4000000000 0.4100000000 0.4200000000 0.4300000000 + 0.4400000000 0.4500000000 0.4600000000 0.4700000000 + 0.4800000000 0.4900000000 0.5100000000 0.5300000000 + 0.5500000000 0.5700000000 0.6000000000 0.6300000000 + 0.6600000000 0.6900000000 0.7200000000 0.7600000000 + 0.8000000000 0.6013470019-153 1.530849000 1.529569000 + 1.528385000 1.527288000 1.526269000 1.525320000 + 1.524433000 1.523605000 1.522829000 1.522100000 + 1.520769000 1.519584000 1.518522000 1.517566000 + 1.516295000 1.515186000 1.514207000 1.513335000 + 1.512549000 1.511613000 1.510776000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.6013470421-153 1.470115999 1.469066000 1.468094001 + 1.467189999 1.466350000 1.465566001 1.464832999 + 1.464146000 1.463502001 1.462896999 1.462326954 + 1.461789001 1.461280426 1.460799001 1.460343231 + 1.459910999 1.459499634 1.459108000 1.458734621 + 1.458378326 1.458037999 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.6013470017-153 + 2.015033007 2.067435980 2.123557091 2.183662415 + 2.249078989 2.322175980 2.406752348 2.500694990 + 2.599516392 2.695918083 2.782654047 2.857251883 + 2.932022095 3.032986879 3.128362894 3.182147026 + 3.204962492 3.213999987 3.219824076 3.215651035 + 3.203588247 2.846766038 2.910241477 2.973716917 + 3.030007040 3.086296987 3.135022942 3.185785042 + 3.236546994 3.262944472 3.289342016 3.306574000 + 3.322023985 3.329920985 3.329960500 3.330000000 + 3.329918988 3.319467498 3.309016007 3.300317015 + 3.300000001 3.301569254 0.6013470019-153 1.530848999 + 1.529569000 1.528385002 1.527287999 1.526269000 + 1.525320001 1.524432999 1.523605000 1.522829001 + 1.522099999 1.521414562 1.520769001 1.520159750 + 1.519584002 1.519038817 1.518521999 1.518031738 + 1.517566000 1.517122108 1.516698899 1.516294999 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 + 3SiO2 Cr BK7 + 1.000000000 0.6013470421-153 0.6013470017-153 0.6013470019-153 + 1.000000000 1.000000000 diff --git a/tests/reference/codev/tfs_cr41.seq b/tests/reference/codev/tfs_cr41.seq new file mode 100644 index 00000000..4dce96a5 --- /dev/null +++ b/tests/reference/codev/tfs_cr41.seq @@ -0,0 +1,44 @@ +! TFStudio Cr SiO2 on BK7, 41 wavelengths +! Written by TFStudio. Layers run from the incident medium to the substrate, +! thicknesses are physical, in nm. SAV at the end writes the .mul file. +MUL +MDA +PHT Y +TIT 'TFStudio Cr SiO2 on BK7, 41 wavelengths' +MIC + MWL 400 410 420 430 440 450 460 470 & + 480 490 510 530 550 570 600 630 & + 660 690 720 760 800 + 'SiO2' 1.470116 1.469066 1.468094 1.46719 1.46635 1.465566 1.464833 1.464146 & + 1.463502 1.462897 1.461789 1.460799 1.459911 1.459108 1.458038 1.4571 & + 1.456268 1.455524 1.454851 1.454043 1.453317 + MWL 400 420 440 450 470 490 500 510 & + 520 540 550 570 580 590 630 660 & + 700 730 760 780 800 + 'Cr' 2.015033 2.123557 2.249079 2.322176 2.500695 2.695918 2.782654 2.857252 & + 2.932022 3.128363 3.182147 3.214 3.219824 3.215651 3.145058 3.088778 & + 3.050498 3.059394 3.083976 3.116284 3.159071 + EXT 'Cr' 2.846766 2.973717 3.086297 3.135023 3.236547 3.289342 3.306574 3.322024 & + 3.329921 3.33 3.329919 3.309016 3.300317 3.3 3.306277 3.341036 & + 3.386676 3.405398 3.422972 3.441297 3.461735 + MWL 400 410 420 430 440 450 460 470 & + 480 490 510 530 550 570 600 630 & + 660 690 720 760 800 + 'BK7' 1.530849 1.529569 1.528385 1.527288 1.526269 1.52532 1.524433 1.523605 & + 1.522829 1.5221 1.520769 1.519584 1.518522 1.517566 1.516295 1.515186 & + 1.514207 1.513335 1.512549 1.511613 1.510776 +END +INC 1 +COA 80 0 'SiO2' +COA 8 0 'Cr' +SUB 'BK7' +WL 400 410 420 430 440 450 460 470 & + 480 490 500 510 520 530 540 550 & + 560 570 580 590 600 610 620 630 & + 640 650 660 670 680 690 700 710 & + 720 730 740 750 760 770 780 790 & + 800 +REF 550 +ANG 0 +SAV tfs_cr41 +MEX diff --git a/tests/reference/codev/tfs_cr41g.mul b/tests/reference/codev/tfs_cr41g.mul new file mode 100644 index 00000000..2f552b1a --- /dev/null +++ b/tests/reference/codev/tfs_cr41g.mul @@ -0,0 +1,250 @@ + 6 0.000000000 +Cr SiO2 on BK7, 41 wavelengths from WLG + 07-Oct-26 PLAINTEXT + 0 1102 38685 679 0 + 1 4 1 0 1 0 0 0 + 0 0 2 2 0 0 1 0 + 4 41 442 0 0 0 25 0 + 0 0 3 0 1 0 5 3 + 0 1 6 193 1 0 0 0 + 0 0 1 150 3 + 0 0 0 0 0 0 0 0 + 0 0 0 0 + 0 0 0 100 + -1 193 276 359 + 21 22 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 65 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 21 86 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 129 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 21 150 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 0.000000000 0.5500000000 0.9633602217E-01 + 0.9633602217E-01 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 0.4715828852 0.5000000000 + 0.000000000 0.9800000191 0.000000000 1.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 0.4000000060 0.4099999964 0.4199999869 0.4299999774 + 0.4399999678 0.4499999583 0.4599999487 0.4699999392 + 0.4799999297 0.4899999201 0.4999999106 0.5099999309 + 0.5199999213 0.5299999118 0.5399999022 0.5499998927 + 0.5599998832 0.5699998736 0.5799998641 0.5899998546 + 0.5999998450 0.6099998355 0.6199998260 0.6299998164 + 0.6399998069 0.6499997973 0.6599997878 0.6699997783 + 0.6799997687 0.6899997592 0.6999997497 0.7099997401 + 0.7199997306 0.7299997211 0.7399997115 0.7499997020 + 0.7599996924 0.7699996829 0.7799996734 0.7899996638 + 0.7999996543 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 0.000000000 0.2123506908 0.4628577492E-01 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 1.000000000 1.459910999 3.182147026 1.518521999 + 6SiO2 Cr BK7 SiO2 Cr BK7 + 0.4000000000 0.4100000000 0.4200000000 0.4300000000 + 0.4400000000 0.4500000000 0.4600000000 0.4700000000 + 0.4800000000 0.4900000000 0.5100000000 0.5300000000 + 0.5500000000 0.5700000000 0.6000000000 0.6300000000 + 0.6600000000 0.6900000000 0.7200000000 0.7600000000 + 0.8000000000 0.6013470421-153 1.470116000 1.469066000 + 1.468094000 1.467190000 1.466350000 1.465566000 + 1.464833000 1.464146000 1.463502000 1.462897000 + 1.461789000 1.460799000 1.459911000 1.459108000 + 1.458038000 1.457100000 1.456268000 1.455524000 + 1.454851000 1.454043000 1.453317000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.4000000000 0.4200000000 0.4400000000 0.4500000000 + 0.4700000000 0.4900000000 0.5000000000 0.5100000000 + 0.5200000000 0.5400000000 0.5500000000 0.5700000000 + 0.5800000000 0.5900000000 0.6300000000 0.6600000000 + 0.7000000000 0.7300000000 0.7600000000 0.7800000000 + 0.8000000000 0.6013470017-153 2.015033000 2.123557000 + 2.249079000 2.322176000 2.500695000 2.695918000 + 2.782654000 2.857252000 2.932022000 3.128363000 + 3.182147000 3.214000000 3.219824000 3.215651000 + 3.145058000 3.088778000 3.050498000 3.059394000 + 3.083976000 3.116284000 3.159071000 2.846766000 + 2.973717000 3.086297000 3.135023000 3.236547000 + 3.289342000 3.306574000 3.322024000 3.329921000 + 3.330000000 3.329919000 3.309016000 3.300317000 + 3.300000000 3.306277000 3.341036000 3.386676000 + 3.405398000 3.422972000 3.441297000 3.461735000 + 0.4000000000 0.4100000000 0.4200000000 0.4300000000 + 0.4400000000 0.4500000000 0.4600000000 0.4700000000 + 0.4800000000 0.4900000000 0.5100000000 0.5300000000 + 0.5500000000 0.5700000000 0.6000000000 0.6300000000 + 0.6600000000 0.6900000000 0.7200000000 0.7600000000 + 0.8000000000 0.6013470019-153 1.530849000 1.529569000 + 1.528385000 1.527288000 1.526269000 1.525320000 + 1.524433000 1.523605000 1.522829000 1.522100000 + 1.520769000 1.519584000 1.518522000 1.517566000 + 1.516295000 1.515186000 1.514207000 1.513335000 + 1.512549000 1.511613000 1.510776000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.6013470421-153 1.470115999 1.469066000 1.468094001 + 1.467190002 1.466350003 1.465566003 1.464833004 + 1.464146004 1.463502004 1.462897005 1.462326959 + 1.461789004 1.461280429 1.460799004 1.460343236 + 1.459911005 1.459499638 1.459108005 1.458734625 + 1.458378330 1.458038005 1.457711893 1.457399501 + 1.457100005 1.456811789 1.456534659 1.456268006 + 1.456010921 1.455763080 1.455524006 1.455292287 + 1.455068069 1.454851006 1.454639986 1.454435151 + 1.454236239 1.454043006 1.453854310 1.453670533 + 1.453491490 1.453317006 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.6013470017-153 + 2.015033007 2.067435980 2.123557091 2.183662176 + 2.249078751 2.322175741 2.406751871 2.500694513 + 2.599515915 2.695917368 2.782653332 2.857251406 + 2.932021618 3.032986403 3.128361940 3.182146549 + 3.204962254 3.213999987 3.219824076 3.215651035 + 3.203588486 3.186439276 3.166247845 3.145058393 + 3.124649763 3.105739355 3.088778496 3.074234486 + 3.062640667 3.054545403 3.050498009 3.050723791 + 3.054153442 3.059393644 3.065493584 3.073268652 + 3.083975554 3.098505735 3.116283417 3.136523724 + 3.159070015 2.846766038 2.910241477 2.973716917 + 3.030006873 3.086296819 3.135022797 3.185784740 + 3.236546691 3.262944314 3.289341789 3.306573846 + 3.322023893 3.329920938 3.329960500 3.330000000 + 3.329919001 3.319467622 3.309016132 3.300317118 + 3.300000005 3.301569226 3.303138474 3.304707723 + 3.306276971 3.317863110 3.329449432 3.341035754 + 3.352445747 3.363855736 3.375265725 3.386675714 + 3.392916504 3.399157165 3.405397826 3.411255831 + 3.417113825 3.422971820 3.432134209 3.441296701 + 3.451515656 3.461734647 0.6013470019-153 1.530848999 + 1.529569000 1.528385002 1.527288002 1.526269003 + 1.525320004 1.524433004 1.523605005 1.522829005 + 1.522100006 1.521414568 1.520769004 1.520159754 + 1.519584005 1.519038823 1.518522005 1.518031743 + 1.517566006 1.517122113 1.516698904 1.516295006 + 1.515908890 1.515539571 1.515186006 1.514846369 + 1.514520294 1.514207007 1.513905324 1.513614856 + 1.513335007 1.513064082 1.512802227 1.512549007 + 1.512303752 1.512066230 1.511836091 1.511613007 + 1.511395096 1.511183111 1.510976821 1.510776007 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 + 3SiO2 Cr BK7 + 1.000000000 0.6013470421-153 0.6013470017-153 0.6013470019-153 + 1.000000000 1.000000000 diff --git a/tests/reference/codev/tfs_cr41g.seq b/tests/reference/codev/tfs_cr41g.seq new file mode 100644 index 00000000..40e85109 --- /dev/null +++ b/tests/reference/codev/tfs_cr41g.seq @@ -0,0 +1,39 @@ +! Cr SiO2 on BK7, 41 wavelengths from WLG +! Written by TFStudio. Layers run from the incident medium to the substrate, +! thicknesses are physical, in nm. SAV at the end writes the .mul file. +MUL +MDA +PHT Y +TIT 'Cr SiO2 on BK7, 41 wavelengths from WLG' +MIC + MWL 400 410 420 430 440 450 460 470 & + 480 490 510 530 550 570 600 630 & + 660 690 720 760 800 + 'SiO2' 1.470116 1.469066 1.468094 1.46719 1.46635 1.465566 1.464833 1.464146 & + 1.463502 1.462897 1.461789 1.460799 1.459911 1.459108 1.458038 1.4571 & + 1.456268 1.455524 1.454851 1.454043 1.453317 + MWL 400 420 440 450 470 490 500 510 & + 520 540 550 570 580 590 630 660 & + 700 730 760 780 800 + 'Cr' 2.015033 2.123557 2.249079 2.322176 2.500695 2.695918 2.782654 2.857252 & + 2.932022 3.128363 3.182147 3.214 3.219824 3.215651 3.145058 3.088778 & + 3.050498 3.059394 3.083976 3.116284 3.159071 + EXT 'Cr' 2.846766 2.973717 3.086297 3.135023 3.236547 3.289342 3.306574 3.322024 & + 3.329921 3.33 3.329919 3.309016 3.300317 3.3 3.306277 3.341036 & + 3.386676 3.405398 3.422972 3.441297 3.461735 + MWL 400 410 420 430 440 450 460 470 & + 480 490 510 530 550 570 600 630 & + 660 690 720 760 800 + 'BK7' 1.530849 1.529569 1.528385 1.527288 1.526269 1.52532 1.524433 1.523605 & + 1.522829 1.5221 1.520769 1.519584 1.518522 1.517566 1.516295 1.515186 & + 1.514207 1.513335 1.512549 1.511613 1.510776 +END +INC 1 +COA 80 0 'SiO2' +COA 8 0 'Cr' +SUB 'BK7' +WLG 400 800 10 +REF 550 +ANG 0 +SAV tfs_cr41g +MEX diff --git a/tests/reference/codev/tfs_cr41w.mul b/tests/reference/codev/tfs_cr41w.mul new file mode 100644 index 00000000..c7f2464d --- /dev/null +++ b/tests/reference/codev/tfs_cr41w.mul @@ -0,0 +1,250 @@ + 6 0.000000000 +Cr SiO2 on BK7, 41 wavelengths in two WL commands + 07-Oct-26 PLAINTEXT + 0 1102 38685 679 0 + 1 4 1 0 1 0 0 0 + 0 0 2 2 0 0 1 0 + 4 41 442 0 0 0 25 0 + 0 0 3 0 1 0 5 3 + 0 1 6 193 1 0 0 0 + 0 0 2 150 3 + 0 0 0 0 0 0 0 0 + 0 0 0 0 + 0 0 0 100 + -1 193 276 359 + 21 22 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 65 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 21 86 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 129 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 21 150 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 0.000000000 0.5500000000 0.8279042211 + 0.3788321023 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 0.1391525180 0.5000000000 + 0.000000000 0.9800000191 0.000000000 1.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 0.4000000060 0.4099999964 0.4199999869 0.4300000072 + 0.4399999976 0.4499999881 0.4600000083 0.4699999988 + 0.4799999893 0.4900000095 0.5000000000 0.5099999905 + 0.5199999809 0.5299999714 0.5400000215 0.5500000119 + 0.5600000024 0.5699999928 0.5799999833 0.5899999738 + 0.6000000238 0.6100000143 0.6200000048 0.6299999952 + 0.6399999857 0.6499999762 0.6600000262 0.6700000167 + 0.6800000072 0.6899999976 0.6999999881 0.7099999785 + 0.7200000286 0.7300000191 0.7400000095 0.7500000000 + 0.7599999905 0.7699999809 0.7799999714 0.7900000215 + 0.8000000119 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 0.000000000 0.2123506908 0.4628577492E-01 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 1.000000000 1.459910999 3.182147026 1.518521999 + 6SiO2 Cr BK7 SiO2 Cr BK7 + 0.4000000000 0.4100000000 0.4200000000 0.4300000000 + 0.4400000000 0.4500000000 0.4600000000 0.4700000000 + 0.4800000000 0.4900000000 0.5100000000 0.5300000000 + 0.5500000000 0.5700000000 0.6000000000 0.6300000000 + 0.6600000000 0.6900000000 0.7200000000 0.7600000000 + 0.8000000000 0.6013470421-153 1.470116000 1.469066000 + 1.468094000 1.467190000 1.466350000 1.465566000 + 1.464833000 1.464146000 1.463502000 1.462897000 + 1.461789000 1.460799000 1.459911000 1.459108000 + 1.458038000 1.457100000 1.456268000 1.455524000 + 1.454851000 1.454043000 1.453317000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.4000000000 0.4200000000 0.4400000000 0.4500000000 + 0.4700000000 0.4900000000 0.5000000000 0.5100000000 + 0.5200000000 0.5400000000 0.5500000000 0.5700000000 + 0.5800000000 0.5900000000 0.6300000000 0.6600000000 + 0.7000000000 0.7300000000 0.7600000000 0.7800000000 + 0.8000000000 0.6013470017-153 2.015033000 2.123557000 + 2.249079000 2.322176000 2.500695000 2.695918000 + 2.782654000 2.857252000 2.932022000 3.128363000 + 3.182147000 3.214000000 3.219824000 3.215651000 + 3.145058000 3.088778000 3.050498000 3.059394000 + 3.083976000 3.116284000 3.159071000 2.846766000 + 2.973717000 3.086297000 3.135023000 3.236547000 + 3.289342000 3.306574000 3.322024000 3.329921000 + 3.330000000 3.329919000 3.309016000 3.300317000 + 3.300000000 3.306277000 3.341036000 3.386676000 + 3.405398000 3.422972000 3.441297000 3.461735000 + 0.4000000000 0.4100000000 0.4200000000 0.4300000000 + 0.4400000000 0.4500000000 0.4600000000 0.4700000000 + 0.4800000000 0.4900000000 0.5100000000 0.5300000000 + 0.5500000000 0.5700000000 0.6000000000 0.6300000000 + 0.6600000000 0.6900000000 0.7200000000 0.7600000000 + 0.8000000000 0.6013470019-153 1.530849000 1.529569000 + 1.528385000 1.527288000 1.526269000 1.525320000 + 1.524433000 1.523605000 1.522829000 1.522100000 + 1.520769000 1.519584000 1.518522000 1.517566000 + 1.516295000 1.515186000 1.514207000 1.513335000 + 1.512549000 1.511613000 1.510776000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.6013470421-153 1.470115999 1.469066000 1.468094001 + 1.467189999 1.466350000 1.465566001 1.464832999 + 1.464146000 1.463502001 1.462896999 1.462326954 + 1.461789001 1.461280426 1.460799001 1.460343231 + 1.459910999 1.459499634 1.459108000 1.458734621 + 1.458378326 1.458037999 1.457711887 1.457399496 + 1.457100000 1.456811784 1.456534654 1.456267999 + 1.456010915 1.455763074 1.455524000 1.455292281 + 1.455068064 1.454850999 1.454639980 1.454435145 + 1.454236233 1.454043000 1.453854305 1.453670528 + 1.453491484 1.453317000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.6013470017-153 + 2.015033007 2.067435980 2.123557091 2.183662415 + 2.249078989 2.322175980 2.406752348 2.500694990 + 2.599516392 2.695918083 2.782654047 2.857251883 + 2.932022095 3.032986879 3.128362894 3.182147026 + 3.204962492 3.213999987 3.219824076 3.215651035 + 3.203588247 3.186439037 3.166247368 3.145057917 + 3.124649525 3.105739117 3.088778019 3.074234247 + 3.062640429 3.054545164 3.050498009 3.050723791 + 3.054153442 3.059393883 3.065493822 3.073268890 + 3.083976030 3.098506212 3.116283894 3.136524677 + 3.159070969 2.846766038 2.910241477 2.973716917 + 3.030007040 3.086296987 3.135022942 3.185785042 + 3.236546994 3.262944472 3.289342016 3.306574000 + 3.322023985 3.329920985 3.329960500 3.330000000 + 3.329918988 3.319467498 3.309016007 3.300317015 + 3.300000001 3.301569254 3.303138502 3.304707751 + 3.306276999 3.317863317 3.329449639 3.341036030 + 3.352446019 3.363856008 3.375265997 3.386675986 + 3.392916653 3.399157351 3.405398011 3.411256006 + 3.417114000 3.422971994 3.432134483 3.441296974 + 3.451516022 3.461735000 0.6013470019-153 1.530848999 + 1.529569000 1.528385002 1.527287999 1.526269000 + 1.525320001 1.524432999 1.523605000 1.522829001 + 1.522099999 1.521414562 1.520769001 1.520159750 + 1.519584002 1.519038817 1.518521999 1.518031738 + 1.517566000 1.517122108 1.516698899 1.516294999 + 1.515908883 1.515539565 1.515186000 1.514846363 + 1.514520288 1.514206999 1.513905317 1.513614849 + 1.513335000 1.513064075 1.512802220 1.512548999 + 1.512303745 1.512066223 1.511836085 1.511613000 + 1.511395089 1.511183105 1.510976814 1.510776000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 + 3SiO2 Cr BK7 + 1.000000000 0.6013470421-153 0.6013470017-153 0.6013470019-153 + 1.000000000 1.000000000 diff --git a/tests/reference/codev/tfs_cr41w.seq b/tests/reference/codev/tfs_cr41w.seq new file mode 100644 index 00000000..e39e5582 --- /dev/null +++ b/tests/reference/codev/tfs_cr41w.seq @@ -0,0 +1,44 @@ +! Cr SiO2 on BK7, 41 wavelengths in two WL commands +! Written by TFStudio. Layers run from the incident medium to the substrate, +! thicknesses are physical, in nm. SAV at the end writes the .mul file. +MUL +MDA +PHT Y +TIT 'Cr SiO2 on BK7, 41 wavelengths in two WL commands' +MIC + MWL 400 410 420 430 440 450 460 470 & + 480 490 510 530 550 570 600 630 & + 660 690 720 760 800 + 'SiO2' 1.470116 1.469066 1.468094 1.46719 1.46635 1.465566 1.464833 1.464146 & + 1.463502 1.462897 1.461789 1.460799 1.459911 1.459108 1.458038 1.4571 & + 1.456268 1.455524 1.454851 1.454043 1.453317 + MWL 400 420 440 450 470 490 500 510 & + 520 540 550 570 580 590 630 660 & + 700 730 760 780 800 + 'Cr' 2.015033 2.123557 2.249079 2.322176 2.500695 2.695918 2.782654 2.857252 & + 2.932022 3.128363 3.182147 3.214 3.219824 3.215651 3.145058 3.088778 & + 3.050498 3.059394 3.083976 3.116284 3.159071 + EXT 'Cr' 2.846766 2.973717 3.086297 3.135023 3.236547 3.289342 3.306574 3.322024 & + 3.329921 3.33 3.329919 3.309016 3.300317 3.3 3.306277 3.341036 & + 3.386676 3.405398 3.422972 3.441297 3.461735 + MWL 400 410 420 430 440 450 460 470 & + 480 490 510 530 550 570 600 630 & + 660 690 720 760 800 + 'BK7' 1.530849 1.529569 1.528385 1.527288 1.526269 1.52532 1.524433 1.523605 & + 1.522829 1.5221 1.520769 1.519584 1.518522 1.517566 1.516295 1.515186 & + 1.514207 1.513335 1.512549 1.511613 1.510776 +END +INC 1 +COA 80 0 'SiO2' +COA 8 0 'Cr' +SUB 'BK7' +WL 400 410 420 430 440 450 460 470 & + 480 490 500 510 520 530 540 550 & + 560 570 580 590 600 +WL 610 620 630 640 650 660 670 680 & + 690 700 710 720 730 740 750 760 & + 770 780 790 800 +REF 550 +ANG 0 +SAV tfs_cr41w +MEX diff --git a/tests/reference/codev/tfs_hr45.mul b/tests/reference/codev/tfs_hr45.mul new file mode 100644 index 00000000..d0b4f042 --- /dev/null +++ b/tests/reference/codev/tfs_hr45.mul @@ -0,0 +1,185 @@ + 6 0.000000000 +TFStudio 15-layer TiO2 SiO2 mirror for 45 deg + 07-Oct-26 PLAINTEXT + 0 1102 38685 667 0 + 1 4 1 0 1 0 0 0 + 0 0 2 2 0 0 1 0 + 17 21 280 0 0 0 25 0 + 0 0 16 0 5 0 5 16 + 0 1 4 151 1 0 0 0 + 0 0 2 108 3 + 0 0 0 0 0 0 0 0 + 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 100 + 0 + -1 151 194 151 194 151 194 151 + 194 151 194 151 194 151 194 151 + 237 + 21 22 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 65 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 108 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 0.000000000 0.6000000000 0.9961706886 + 0.9946927813 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 0.8542337347E-06 0.5000000000 + 0.000000000 0.9800000191 0.000000000 0.000000000 + 0.000000000 0.3490658402 0.7853981853 1.047197580 + 1.308996916 + 0.4499999881 0.4650000036 0.4799999893 0.4950000048 + 0.5099999905 0.5249999762 0.5400000215 0.5550000072 + 0.5699999928 0.5849999785 0.6000000238 0.6150000095 + 0.6299999952 0.6449999809 0.6600000262 0.6750000119 + 0.6899999976 0.7049999833 0.7200000286 0.7350000143 + 0.7500000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 1.000000000 1.000000000 1.000000000 + 1.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 0.000000000 0.2772773251 0.2853657306 0.2772773251 + 0.2853657306 0.2772773251 0.2853657306 0.2772773251 + 0.2853657306 0.2772773251 0.2853657306 0.2772773251 + 0.2853657306 0.2772773251 0.2853657306 0.2772773251 + 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 + 1.000000000 2.485617988 1.458037999 2.485617988 + 1.458037999 2.485617988 1.458037999 2.485617988 + 1.458037999 2.485617988 1.458037999 2.485617988 + 1.458037999 2.485617988 1.458037999 2.485617988 + 1.516294999 + 6TiO2 SiO2 BK7 TiO2 SiO2 BK7 + 0.4500000000 0.4650000000 0.4800000000 0.4950000000 + 0.5100000000 0.5250000000 0.5400000000 0.5550000000 + 0.5700000000 0.5850000000 0.6000000000 0.6150000000 + 0.6300000000 0.6450000000 0.6600000000 0.6750000000 + 0.6900000000 0.7050000000 0.7200000000 0.7350000000 + 0.7500000000 0.6013470421-153 2.635504000 2.609262000 + 2.587039000 2.567985000 2.551463000 2.537002000 + 2.524267000 2.512956000 2.502861000 2.493790000 + 2.485618000 2.478210000 2.471465000 2.465318000 + 2.459686000 2.454515000 2.449742000 2.445346000 + 2.441273000 2.437486000 2.433980000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.6013470421-153 1.465566000 1.464484000 1.463502000 + 1.462607000 1.461789000 1.461037000 1.460344000 + 1.459703000 1.459108000 1.458554000 1.458038000 + 1.457554000 1.457100000 1.456672000 1.456268000 + 1.455886000 1.455524000 1.455179000 1.454851000 + 1.454537000 1.454237000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.6013470019-153 + 1.525320000 1.524012000 1.522829000 1.521752000 + 1.520769000 1.519868000 1.519039000 1.518274000 + 1.517566000 1.516908000 1.516295000 1.515722000 + 1.515186000 1.514682000 1.514207000 1.513759000 + 1.513335000 1.512932000 1.512549000 1.512185000 + 1.511836000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.6013470421-153 2.635504023 + 2.609261994 2.587039015 2.567984994 2.551463010 + 2.537002022 2.524266983 2.512955995 2.502861005 + 2.493790012 2.485617988 2.478209996 2.471465002 + 2.465318008 2.459685991 2.454514996 2.449742001 + 2.445346005 2.441272993 2.437485997 2.433980000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.6013470421-153 1.465566001 1.464484000 + 1.463502001 1.462607000 1.461789001 1.461037001 + 1.460343999 1.459703000 1.459108000 1.458554001 + 1.458037999 1.457554000 1.457100000 1.456672001 + 1.456267999 1.455886000 1.455524000 1.455179000 + 1.454850999 1.454537000 1.454237000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.6013470019-153 1.525320001 1.524012000 1.522829001 + 1.521752000 1.520769001 1.519868001 1.519038999 + 1.518274000 1.517566000 1.516908001 1.516294999 + 1.515722000 1.515186000 1.514682001 1.514206999 + 1.513759000 1.513335000 1.512932000 1.512548999 + 1.512185000 1.511836000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 0.000000000 0.000000000 0.000000000 0.000000000 + 16TiO2 SiO2 TiO2 SiO2 TiO2 SiO2 TiO2 + SiO2 TiO2 SiO2 TiO2 SiO2 TiO2 SiO2 + TiO2 BK7 + 1.000000000 0.6013470421-153 0.6013470421-153 0.6013470421-153 + 0.6013470421-153 0.6013470421-153 0.6013470421-153 0.6013470421-153 + 0.6013470421-153 0.6013470421-153 0.6013470421-153 0.6013470421-153 + 0.6013470421-153 0.6013470421-153 0.6013470421-153 0.6013470421-153 + 0.6013470019-153 + 1.000000000 1.000000000 diff --git a/tests/reference/codev/tfs_hr45.seq b/tests/reference/codev/tfs_hr45.seq new file mode 100644 index 00000000..24150059 --- /dev/null +++ b/tests/reference/codev/tfs_hr45.seq @@ -0,0 +1,45 @@ +! TFStudio 15-layer TiO2 SiO2 mirror for 45 deg +! Written by TFStudio. Layers run from the incident medium to the substrate, +! thicknesses are physical, in nm. SAV at the end writes the .mul file. +MUL +MDA +PHT Y +TIT 'TFStudio 15-layer TiO2 SiO2 mirror for 45 deg' +MIC + MWL 450 465 480 495 510 525 540 555 & + 570 585 600 615 630 645 660 675 & + 690 705 720 735 750 + 'TiO2' 2.635504 2.609262 2.587039 2.567985 2.551463 2.537002 2.524267 2.512956 & + 2.502861 2.49379 2.485618 2.47821 2.471465 2.465318 2.459686 2.454515 & + 2.449742 2.445346 2.441273 2.437486 2.43398 + 'SiO2' 1.465566 1.464484 1.463502 1.462607 1.461789 1.461037 1.460344 1.459703 & + 1.459108 1.458554 1.458038 1.457554 1.4571 1.456672 1.456268 1.455886 & + 1.455524 1.455179 1.454851 1.454537 1.454237 + 'BK7' 1.52532 1.524012 1.522829 1.521752 1.520769 1.519868 1.519039 1.518274 & + 1.517566 1.516908 1.516295 1.515722 1.515186 1.514682 1.514207 1.513759 & + 1.513335 1.512932 1.512549 1.512185 1.511836 +END +INC 1 +COA 66.9316 0 'TiO2' +COA 117.4314 0 'SiO2' +COA 66.9316 0 'TiO2' +COA 117.4314 0 'SiO2' +COA 66.9316 0 'TiO2' +COA 117.4314 0 'SiO2' +COA 66.9316 0 'TiO2' +COA 117.4314 0 'SiO2' +COA 66.9316 0 'TiO2' +COA 117.4314 0 'SiO2' +COA 66.9316 0 'TiO2' +COA 117.4314 0 'SiO2' +COA 66.9316 0 'TiO2' +COA 117.4314 0 'SiO2' +COA 66.9316 100 'TiO2' +SUB 'BK7' +WL 450 465 480 495 510 525 540 555 & + 570 585 600 615 630 645 660 675 & + 690 705 720 735 750 +REF 600 +ANG 0 20 45 60 75 +SAV tfs_hr45 +MEX diff --git a/tests/reference/codevListing.mjs b/tests/reference/codevListing.mjs new file mode 100644 index 00000000..ed03f57c --- /dev/null +++ b/tests/reference/codevListing.mjs @@ -0,0 +1,71 @@ +/** + * Read the tables of a CODE V 11.2 text listing (OUT T): the MAN transmission + * table and the MPR index tables of one MUL run. + * + * Used by gen_codev_coating.mjs, which freezes the owner's listings into + * codev/codev_11_2.json, and by tests/codev_coating_samples.mjs, which reads + * the listing of CODE V's own sample coatings where it is present. + */ + +/** + * The lines of one run: from the line that reads `start` (trailing blanks + * aside) up to the next "MUL> MEX". + */ +export function listingRun(text, start) { + const lines = text.split(/\r?\n/); + const from = lines.findIndex(line => line.trimEnd() === start); + if (from < 0) throw new Error(`the listing has no line "${start}"`); + const to = lines.findIndex((line, i) => i > from && line.startsWith('MUL> MEX')); + return lines.slice(from, to < 0 ? lines.length : to); +} + +const words = (line) => line.trim().split(/\s+/).filter(Boolean); + +/** MAN: [angle°, λ_nm, Rs, Rp, Ts, Tp] per printed row. */ +export function manRows(lines) { + const at = lines.findIndex(line => line.includes('T R A N S M I S S I O N')); + if (at < 0) throw new Error('no MAN table in the run'); + const rows = []; + let angle = null; + for (let i = at + 1; i < lines.length && !lines[i].includes('Phase table'); i++) { + const v = words(lines[i]).map(Number); + if (!v.length || !v.every(Number.isFinite)) continue; + if (v.length === 1) angle = v[0]; + else if (v.length === 7) rows.push([angle, v[0], v[1], v[2], v[4], v[5]]); + } + return rows; +} + +const SECTION = /^\s*(REFRACTIVE INDICES|EXTINCTION COEFFICIENTS|MULTILAYER CATALOG|MAN\s)/; + +/** + * One MPR section by its title, as {label: {wl, values}}: "REFRACTIVE INDICES + * - DISPERSIVE MATERIALS" and "EXTINCTION COEFFICIENTS - DISPERSIVE + * MATERIALS" at the analysis wavelengths, "MULTILAYER CATALOG: INDEX DATA" + * and "MULTILAYER CATALOG: EXTINCTION COEFFICIENTS" at each table's MWL. + * MPR leaves out both extinction sections when no MIC entry has an EXT, so a + * section that is not there reads as no entries. + */ +export function mprSection(lines, title) { + const at = lines.findIndex(line => line.trim() === title); + const table = {}; + if (at < 0) return table; + let wl = [], current = null; + for (let i = at + 1; i < lines.length && !SECTION.test(lines[i]); i++) { + const line = lines[i].trim(); + if (!line) continue; + if (line.startsWith('WL ')) { + wl = []; + current = wl; + current.push(...words(line.slice(3)).map(Number)); + } else if (line.startsWith("'")) { + const label = line.slice(1, line.indexOf("'", 1)); + table[label] = { wl, values: [] }; + current = table[label].values; + current.push(...words(line.slice(label.length + 2)).map(Number)); + } else if (current) { + current.push(...words(line).map(Number)); + } + } + return table; +} diff --git a/tests/reference/gen_codev_coating.mjs b/tests/reference/gen_codev_coating.mjs new file mode 100644 index 00000000..62e03eee --- /dev/null +++ b/tests/reference/gen_codev_coating.mjs @@ -0,0 +1,123 @@ +/** + * Freeze CODE V 11.2 results into codev/: the .seq files TFStudio wrote and + * CODE V ran, the .mul files CODE V saved from them, and codev_11_2.json with + * the numbers CODE V printed for them. + * + * The source is runs of CODE V 11.2 (CODE V PC, 07-Oct-26) on the owner's + * work PC, each a command file and the listing it wrote with OUT T: + * + * run_checks.seq codev_checks.lis five TFStudio exports, SAV, RES, MPR, MAN + * run_checks2.seq codev_checks2.lis a TFStudio export with 41 wavelengths in + * two WL commands, MPR and MAN; the same + * stack with WLG, MPR; a 3-point MIC + * table narrower than WL, MPR and MAN + * run_checks4.seq codev_checks4.lis MIC probe tables of 2 to 5 points, MPR + * run_checks5.seq codev_checks5.lis MIC probe tables that differ in k, and + * the Ta2O5 table of tfs_ar4, MPR + * run_checks6.seq codev_checks6.lis MIC probe tables that differ in k and n + * about the switch to the spline, MPR + * + * Written to codev/: + * .seq the MUL ... MEX block of the command file, as CODE V ran it + * .mul the .mul CODE V 11.2 saved from each TFStudio export + * (format 6), copied as it is; tfs_cr41.seq and + * tfs_cr41g.seq are written for their .mul files only + * codev_11_2.json man: MAN per .seq, rows [angle°, λ nm, Rs, Rp, Ts, Tp] + * mic: per MIC material, the table [λ nm, n, k] and the n + * and k MPR printed at the analysis wavelengths + * + * Run (maintainer only; needs the listings, which are not in the repo): + * node tests/reference/gen_codev_coating.mjs + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { listingRun, manRows, mprSection } from './codevListing.mjs'; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const LISTINGS = 'X:/TFStudio Dev/reference/code v/listings'; +const OUT = path.join(HERE, 'codev'); + +const read = (file) => fs.readFileSync(path.join(LISTINGS, file), 'utf8'); + +// Each fixture: the command file, the comment line that heads its block there, +// the listing, and the line that starts its MAN run in the listing. +const FIXTURES = [ + ['tfs_ar4', 'run_checks.seq', '! ---- tfs_ar4', 'codev_checks.lis', 'MUL> RES tfs_ar4'], + ['tfs_agmir', 'run_checks.seq', '! ---- tfs_agmir', 'codev_checks.lis', 'MUL> RES tfs_agmir'], + ['tfs_hr45', 'run_checks.seq', '! ---- tfs_hr45', 'codev_checks.lis', 'MUL> RES tfs_hr45'], + ['tfs_const', 'run_checks.seq', '! ---- tfs_const', 'codev_checks.lis', 'MUL> RES tfs_const'], + ['tfs_cr41w', 'run_checks2.seq', '! ---- 41 wavelengths written as two WL commands of 21 and 20', + 'codev_checks2.lis', 'MUL> RES tfs_cr41w'], + ['mic_narrow', 'run_checks2.seq', '! ---- a MIC table narrower than the analysis wavelengths (may stop the run, so it is last)', + 'codev_checks2.lis', "MDA> TIT 'MIC narrower than WL'"], +]; + +// TFStudio exports whose .mul is checked but whose MAN is not: tfs_cr41 is +// tfs_cr41w with all 41 wavelengths in one WL command, of which CODE V kept +// 21, and tfs_cr41g the same stack with WLG. +const SEQ_ONLY = [ + ['tfs_cr41', 'run_checks.seq', '! ---- tfs_cr41'], + ['tfs_cr41g', 'run_checks2.seq', '! ---- the same stack with WLG'], +]; +// The .mul files CODE V 11.2 saved, in the listings folder. +const SAVED_MUL = ['tfs_ar4', 'tfs_agmir', 'tfs_hr45', 'tfs_const', 'tfs_cr41', 'tfs_cr41w', 'tfs_cr41g']; + +// The probe runs of a command file: the first word of each title names one. +const probeRuns = (seqFile, listing) => read(seqFile).split(/\r?\n/).filter(line => line.startsWith('TIT ')) + .map(line => [line.slice(5, line.indexOf(' ', 5)), listing, `MDA> ${line.trimEnd()}`]); + +// MPR runs whose MIC materials go into `mic`, by listing and the line that starts the run. +const MPR_RUNS = [ + ['tfs_cr41w', 'codev_checks2.lis', 'MUL> RES tfs_cr41w'], + ['mic_narrow', 'codev_checks2.lis', "MDA> TIT 'MIC narrower than WL'"], + ...probeRuns('run_checks4.seq', 'codev_checks4.lis'), + ...probeRuns('run_checks5.seq', 'codev_checks5.lis'), + ...probeRuns('run_checks6.seq', 'codev_checks6.lis'), +]; + +// The lines after `marker` up to and including the first MEX. +function seqBlock(text, marker) { + const lines = text.split(/\r?\n/); + const from = lines.findIndex(line => line.trimEnd() === marker); + if (from < 0) throw new Error(`no "${marker}"`); + const to = lines.findIndex((line, i) => i > from && line.trim() === 'MEX'); + return lines.slice(from + 1, to + 1).join('\r\n') + '\r\n'; +} + +function micEntries(run, lines) { + const n = mprSection(lines, 'REFRACTIVE INDICES - DISPERSIVE MATERIALS'); + const k = mprSection(lines, 'EXTINCTION COEFFICIENTS - DISPERSIVE MATERIALS'); + const tableN = mprSection(lines, 'MULTILAYER CATALOG: INDEX DATA'); + const tableK = mprSection(lines, 'MULTILAYER CATALOG: EXTINCTION COEFFICIENTS'); + return Object.keys(tableN).map(label => ({ + run, + label, + table: tableN[label].wl.map((lam, i) => [lam, tableN[label].values[i], tableK[label]?.values[i] ?? 0]), + wavelengthsNm: n[label].wl, + n: n[label].values, + k: k[label]?.values ?? n[label].wl.map(() => 0), + })); +} + +fs.mkdirSync(OUT, { recursive: true }); +const man = {}; +for (const [name, seqFile, marker, listing, start] of FIXTURES) { + fs.writeFileSync(path.join(OUT, `${name}.seq`), seqBlock(read(seqFile), marker), 'utf8'); + man[name] = manRows(listingRun(read(listing), start)); +} +for (const [name, seqFile, marker] of SEQ_ONLY) { + fs.writeFileSync(path.join(OUT, `${name}.seq`), seqBlock(read(seqFile), marker), 'utf8'); +} +for (const name of SAVED_MUL) fs.copyFileSync(path.join(LISTINGS, `${name}.mul`), path.join(OUT, `${name}.mul`)); +const mic = MPR_RUNS.flatMap(([run, listing, start]) => micEntries(run, listingRun(read(listing), start))); + +fs.writeFileSync(path.join(OUT, 'codev_11_2.json'), JSON.stringify({ + source: 'CODE V PC 11.2, listings of 07-Oct-26 (codev_checks.lis, codev_checks2.lis, codev_checks4.lis to codev_checks6.lis)', + man: 'MAN transmission table of codev/.seq: [angle deg, wavelength nm, Rs, Rp, Ts, Tp], 6 decimals as printed', + mic: 'MIC material: table [wavelength nm, n, k] as entered, and the n and k MPR printed at the analysis wavelengths', + data: { man, mic }, +}, null, 1) + '\n', 'utf8'); + +console.log(`wrote ${OUT}: ${FIXTURES.length + SEQ_ONLY.length} .seq files, ${SAVED_MUL.length} .mul files, ` + + `${Object.values(man).flat().length} MAN rows, ${mic.length} MIC materials`); diff --git a/tests/report_window.mjs b/tests/report_window.mjs index f3720e6f..97407129 100644 --- a/tests/report_window.mjs +++ b/tests/report_window.mjs @@ -1,14 +1,15 @@ /** * Report window: renders on the analysis frame with the rail, the page and the * export strip; a new block copies the settings its source window shows now. + * With no design open it reports only the designs picked for it. */ import assert from 'node:assert/strict'; import { renderToStaticMarkup } from 'react-dom/server'; -import { loadApp, makeLocale, makeSampleDesign, makeTheme, shimBrowserGlobals, withDesign } from './_uiShim.mjs'; +import { loadApp, makeDesignCtx, makeLocale, makeSampleDesign, makeTheme, shimBrowserGlobals, withDesign } from './_uiShim.mjs'; shimBrowserGlobals(); -await loadApp(); +const { DesignContext } = await loadApp(); // The source windows' stores register when their modules load, as they do in // the app through the window registry. @@ -141,4 +142,39 @@ assert.ok(blockSummary(W, newBlock('layers', { columns: 2, groupPeriods: true }) 'and follows a change made in the window'); } +// ── No design open ─────────────────────────────────────────────────────────── +// The provider then hands out a placeholder nobody opened. There is no current +// design, so there is no page and nothing to export; a design picked in the +// Designs control is still reported, and the placeholder never is. +{ + const { reportSession } = await import('../src/components/windows/information/report/sessionState.js'); + const { makeDefaultDesign } = await import('../src/state/DesignContext.js'); + const placeholder = makeDefaultDesign(); + const value = { ...makeDesignCtx(placeholder), hasActiveDesign: false, activeDesignId: null, designs: { [design.id]: design } }; + const render = () => renderToStaticMarkup(React.createElement(DesignContext.Provider, { value }, + React.createElement(ReportWindow, { c, t }))); + // Whether the Export button in the bottom strip is off. + const exportOff = html => { + const start = html.lastIndexOf('${W.export}', start))); + }; + const onByDefault = BUILTIN_TEMPLATES['design-record'].blocks.filter(b => b.on !== false).length; + + reportSession.reset(); + const current = render(); + assert.ok(!current.includes(' { saved.push(JSON.parse(JSON.stringify(record))); return { success: true }; }, +}, { get: (target, key) => target[key] || (() => Promise.resolve({ success: true })) }); + +// Each module runs its hooks on a harness of its own, and draws with React's +// own createElement. +const realReact = globalThis.React; +async function importOnHarness(specifier) { + const runtime = makeHookRuntime(); + globalThis.React = { ...realReact, ...runtime.React }; + try { + return { runtime, module: await import(specifier) }; + } finally { + globalThis.React = realReact; + } +} +const actions = await importOnHarness('../src/components/windows/dataExchange/zemaxCoatings/useImportActions.js'); +const dialog = await importOnHarness('../src/components/windows/design/coatingLibrary/SaveCoatingDialog.js'); +const { useCoatingImportAction, useLibraryAction } = actions.module; +const { SaveCoatingDialog } = dialog.module; + +const cm = await import('../src/utils/materials/catalogManager.js'); +const { parseZemaxCoating } = await import('../src/utils/io/zemaxCoatingFile.js'); +const { makeCoatingEntry } = await import('../src/utils/coatingLibrary/entryModel.js'); +const { validateEntry } = await import('../src/utils/coatingLibrary/validateEntry.js'); +const { applyCoatingPatch } = await import('../src/utils/coatingLibrary/applyCoating.js'); +const { designMaterialLookup, resolveDesignMaterial } = await import('../src/utils/materials/designMaterials.js'); +const { makeDefaultDesign } = await import('../src/state/DesignContext.js'); + +const c = makeTheme(); +const t = makeLocale('en'); +const z = t.zemaxCoatings; +const sd = t.coatingLibrary.saveDialog; + +const doc = parseZemaxCoating([ + 'MATE H', '0.30 2.40 -0.001', '1.00 2.20 0', + 'MATE L', '0.30 1.48 0', '1.00 1.44 0', + 'COAT AR4', 'H 0.25 0', 'L 0.1 1', 'H 0.05 1', 'L 0.25 0', +].join('\n')); +const FILE = { fileName: 'LAB.DAT', filePath: 'C:\\lab\\LAB.DAT' }; +const GLASS = 'user_glass_00000001:G'; +// The active design: its substrate is a material only the design carries. +const design = { + id: 'lens', name: 'Lens', incidentMedium: 'builtin:Air', exitMedium: 'builtin:Air', + substrate: { material: GLASS, thickness: 1 }, referenceWavelength: 550, + frontLayers: [], backLayers: [], + materials: { [GLASS]: { id: 'G', name: 'Lab glass', formulaNum: -1, tabData: [[300, 1.52, 0], [1000, 1.5, 0]] } }, +}; +const windowArgs = { z, doc, selCoating: 0, refNm: 550, ...FILE }; + +// The coating the window hands the dialog, and the warnings it would report. +function libraryCoating(active = design) { + let opened = null; + const flashes = []; + actions.runtime.reset(); + const saveToLibrary = actions.runtime.render(() => useLibraryAction({ + ...windowArgs, design: active, flash: (type, message) => flashes.push([type, message]), + setLibraryCoating: (value) => { opened = value; }, + })); + saveToLibrary(); + assert.deepEqual(flashes, [], 'the dialog opens without a complaint'); + return opened; +} + +// Every function component of the dialog is drawn without hooks, so the tree +// can be expanded by calling them. +function expand(node) { + if (Array.isArray(node)) return node.map(expand); + if (!node || typeof node !== 'object') return node; + if (typeof node.type === 'function') return expand(node.type(node.props)); + return { ...node, props: { ...node.props, children: expand(node.props.children) } }; +} +function walk(node, visit) { + if (Array.isArray(node)) { node.forEach(child => walk(child, visit)); return; } + if (!node || typeof node !== 'object') return; + visit(node); + walk(node.props.children, visit); +} + +// Press Save on the dialog opened on `coating`; returns what was written and the dialog's text. +async function saveThroughDialog(coating) { + let savedName = null; + let closed = false; + dialog.runtime.reset(); + const tree = expand(dialog.runtime.render(() => SaveCoatingDialog({ + coating, c, t, onClose: () => { closed = true; }, onSaved: (name) => { savedName = name; }, + }))); + const strings = []; + let saveButton = null; + let nameField = null; + walk(tree, (node) => { + const { children } = node.props; + for (const child of [].concat(children)) if (typeof child === 'string') strings.push(child); + if (node.type === 'button' && children === sd.save) saveButton = node; + if (node.type === 'input' && node.props.autoFocus) nameField = node; + }); + const before = saved.length; + await saveButton.props.onClick(); + assert.equal(saved.length, before + 1, 'Save writes one coating'); + assert.ok(closed, 'and closes the dialog'); + return { record: saved.at(-1), savedName, strings, nameValue: nameField.props.value }; +} + +const pairs = (layers) => layers.map(({ material, thickness }) => [material, thickness]); +// The n,k of each front layer of a design, at three wavelengths. +const nk = (d) => { + const lookup = designMaterialLookup(d); + return d.frontLayers.map(layer => [400, 550, 900].map(lambda => lookup(layer.material).getNK(lambda))); +}; +const catalogs = () => cm.getCatalogs().map(cat => [cat.id, Object.keys(cat.materials).sort()]); + +cm.initCatalogs({}); + +// ── With no design selected the import writes nothing ──────────────────────── +// It says why, and makes no catalog, no undo step and no layers. +{ + const before = catalogs(); + const events = []; + actions.runtime.reset(); + actions.runtime.render(() => useCoatingImportAction({ + ...windowArgs, hasActiveDesign: false, + flash: (type, message) => events.push(['flash', type, message]), + checkpoint: () => events.push(['checkpoint']), updateDesign: (p) => events.push(['update', p]), + }))(); + assert.deepEqual(events, [['flash', 'error', z.importNoDesign]], 'refused, and only that'); + assert.deepEqual(catalogs(), before, 'no catalog is made or changed'); +} + +// ── A file never imported: nothing is written, the entry carries its records ─ +let firstEntry; +{ + const opened = libraryCoating(); + assert.ok(!cm.getCatalogs().some(cat => cat.id.startsWith('zemax_')), 'opening the dialog writes no catalog'); + const { coating } = opened; + assert.deepEqual(coating.layers.map(layer => layer.material.split(':')[1]), ['h', 'l', 'h', 'l'], 'incident side first, as the file lists them'); + + const { record, savedName, strings, nameValue } = await saveThroughDialog(coating); + assert.equal(nameValue, 'AR4', 'the dialog proposes the COAT name'); + assert.ok(strings.includes(t.coatingLibrary.layersShort(4)), 'and says how many layers it saves'); + assert.ok(!strings.includes(sd.front) && !strings.includes(sd.back), 'with no side to pick'); + assert.equal(savedName, 'AR4'); + + const entry = makeCoatingEntry(record); + firstEntry = entry; + assert.deepEqual(pairs(entry.layers), pairs([...coating.layers].reverse()), 'the entry holds the layers substrate first'); + assert.match(entry.layers[0].material, /^zemax_lab_[0-9a-f]{8}:l$/, 'under the id the import gives them'); + assert.equal(entry.layers[1].thickness, 50, 'an absolute thickness in nm'); + assert.equal(entry.incidentMedium, 'builtin:Air', 'the incident medium is the active design\'s'); + assert.equal(entry.substrate, GLASS, 'and so is the substrate'); + assert.deepEqual(Object.keys(entry.materials).sort(), [...new Set(entry.layers.map(layer => layer.material)), GLASS].sort(), + 'every material that is not built in is embedded, the design\'s substrate included'); + assert.deepEqual(entry.materials[entry.layers[1].material].tabData, [[300, 2.4, 0.001], [1000, 2.2, 0]], 'with the file\'s data'); + assert.equal(entry.source, z.librarySource('AR4', 'LAB.DAT')); + assert.equal(entry.referenceWavelength, 550, 'λ₀ is the one the thicknesses were converted at'); + assert.deepEqual(validateEntry(entry), [], 'the entry is sound'); +} + +// With no design selected the window has the placeholder design. Its media are +// built in, so the entry carries only its layers' records, and it applies as +// the stack the entry saved with a design does. +{ + const { coating } = libraryCoating(makeDefaultDesign()); + const entry = makeCoatingEntry((await saveThroughDialog(coating)).record); + assert.deepEqual([entry.incidentMedium, entry.substrate], ['Air', 'BK7'], 'air and BK7, as the help page says'); + assert.deepEqual(Object.keys(entry.materials).sort(), [...new Set(entry.layers.map(layer => layer.material))].sort(), + 'no record for a built-in medium'); + assert.deepEqual(validateEntry(entry), [], 'the entry is sound'); + const { patch, clashes } = applyCoatingPatch(design, entry, { side: 'front' }); + assert.deepEqual(clashes, []); + const applied = { ...design, ...patch }; + const reference = { ...design, ...applyCoatingPatch(design, firstEntry, { side: 'front' }).patch }; + assert.deepEqual(applied.frontLayers.map(layer => layer.thickness), reference.frontLayers.map(layer => layer.thickness), + 'the same thicknesses, incident side first'); + assert.deepEqual(nk(applied), nk(reference), 'and the same n,k layer by layer'); +} + +// It applies to a design here, where no catalog holds its materials, as the +// stack the import makes, computed with the file's data. +let importedLayers; +{ + const { patch, clashes } = applyCoatingPatch(design, firstEntry, { side: 'front', mode: 'replace' }); + assert.deepEqual(clashes, []); + const applied = { ...design, ...patch }; + const updates = []; + actions.runtime.reset(); + const importCoating = actions.runtime.render(() => useCoatingImportAction({ + ...windowArgs, hasActiveDesign: true, flash: () => {}, checkpoint: () => {}, updateDesign: (p) => updates.push(p), + })); + importCoating(); + importedLayers = updates[0].frontLayers; + const imported = { ...design, frontLayers: importedLayers }; + assert.deepEqual(applied.frontLayers.map(layer => layer.thickness), importedLayers.map(layer => layer.thickness), + 'the same thicknesses, incident side first'); + assert.deepEqual(nk(applied), nk(imported), 'and the same n,k layer by layer'); + for (const layer of applied.frontLayers) { + assert.equal(resolveDesignMaterial(applied, layer.material).status, 'embedded', 'from the records the entry carries'); + } +} + +// ── The file imported before: the entry names the import's own materials ──── +{ + const { coating } = libraryCoating(); + const { record } = await saveThroughDialog(coating); + const entry = makeCoatingEntry(record); + assert.deepEqual(pairs(entry.layers), pairs([...importedLayers].reverse()), + 'layer for layer the materials and thicknesses the import put on the design'); + const catId = importedLayers[0].material.split(':')[0]; + assert.equal(entry.materials[entry.layers[0].material].catalogUid, cm.getCatalog(catId).uid, + 'each record is the catalog\'s, stamped with it'); + const { patch, clashes } = applyCoatingPatch({ ...design, frontLayers: [] }, entry, { side: 'front' }); + assert.deepEqual(clashes, []); + assert.deepEqual(pairs(patch.frontLayers), pairs(importedLayers), 'and applies back as that stack'); +} + +// A COAT the window cannot convert is reported, and no dialog opens. +{ + let opened = null; + const flashes = []; + actions.runtime.reset(); + actions.runtime.render(() => useLibraryAction({ + ...windowArgs, selCoating: 5, design, flash: (type, message) => flashes.push([type, message]), + setLibraryCoating: (value) => { opened = value; }, + }))(); + assert.equal(opened, null); + assert.deepEqual(flashes, [['error', z.importNotStack]]); +} + +console.log('PASS: zemax_coating_to_library'); diff --git a/tests/zemax_coatings_refactor_characterization_test.mjs b/tests/zemax_coatings_refactor_characterization_test.mjs index 5888e259..6de3f6ad 100644 --- a/tests/zemax_coatings_refactor_characterization_test.mjs +++ b/tests/zemax_coatings_refactor_characterization_test.mjs @@ -61,7 +61,7 @@ assert.equal(registration.nameMap['A-B'], `${mixId}:a_b_3`); assert.equal(registration.nameMap['A B'], `${mixId}:a_b_2`); assert.deepEqual(registration.cat.materials.a_b.tabData, [[400, 1.4, 0], [600, 1.6, 0.02]]); -const selected = importer.buildMaterialRegistration(materials, 'mix.dat', new Set(['A B'])); +const selected = importer.buildMaterialRegistration(materials, 'mix.dat', new Set([1])); assert.deepEqual(Object.keys(selected.cat.materials), ['a_b']); assert.equal(selected.nameMap['A B'], `${selected.catId}:a_b`); diff --git a/tests/zemax_coatings_window.mjs b/tests/zemax_coatings_window.mjs new file mode 100644 index 00000000..4e70da2c --- /dev/null +++ b/tests/zemax_coatings_window.mjs @@ -0,0 +1,158 @@ +/** + * Zemax Coatings window: built from the shared Data Exchange chrome, and what + * each tab draws. + * + * The tabs, the reference wavelength and the report of the last action sit in + * one control row, as in Measured Spectra. Both import tabs carry the panel + * that opens the file; the Coatings tab offers the selected stack to the front + * coating, off with no design selected, and to the Coating Library; the + * Materials tab selects records; the Export tab holds its options in a panel + * section above the preview, and with no design selected asks for one instead. + * A file that defines a material name twice raises a notice in the control row. + * + * Run: node tests/zemax_coatings_window.mjs + */ +import assert from 'node:assert/strict'; +import { existsSync } from 'node:fs'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { loadApp, makeDesignCtx, makeLocale, makeTheme, shimBrowserGlobals, withDesign } from './_uiShim.mjs'; + +shimBrowserGlobals(); +const { DesignContext } = await loadApp(); + +const [{ ZemaxCoatings }, { zemaxCoatingsSession }, { WINDOW_REGISTRY }, { parseZemaxCoating }] = await Promise.all([ + import('../src/components/windows/dataExchange/zemaxCoatings/ZemaxCoatings.js'), + import('../src/components/windows/dataExchange/zemaxCoatings/sessionState.js'), + import('../src/components/docking/windowRegistry.js'), + import('../src/utils/io/zemaxCoatingFile.js'), +]); + +const c = makeTheme(); +const text = (html) => html.replace(/<[^>]+>/g, ' ').replace(/'/g, "'").replace(/"/g, '"').replace(/&/g, '&'); +const render = (t) => renderToStaticMarkup(withDesign(React.createElement(ZemaxCoatings, { c, t, setInputDialog: () => {} }))); +// The window as it is with no design selected in the explorer. +const renderNoDesign = (t) => renderToStaticMarkup(React.createElement(DesignContext.Provider, + { value: { ...makeDesignCtx(), hasActiveDesign: false } }, + React.createElement(ZemaxCoatings, { c, t, setInputDialog: () => {} }))); +const attr = (value) => value.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, '''); +// The buttons with these labels, each as [label, off]. +const buttons = (markup, labels) => [...markup.matchAll(/]*)>([^<]*)<\/button>/g)] + .filter(([, , label]) => labels.includes(label)) + .map(([, attrs, label]) => [label, /\sdisabled/.test(attrs)]); + +// ── Registered, with a help page in both documentation languages ───────────── +{ + const entry = WINDOW_REGISTRY['zemax-coatings']; + assert.equal(entry.component, ZemaxCoatings); + assert.equal(entry.help, 'data-exchange/zemax-coatings'); + for (const slug of ['data-exchange/zemax-coatings.md', 'zh/data-exchange/zemax-coatings.md']) { + assert.ok(existsSync(new URL(`../docs-site/src/content/docs/${slug}`, import.meta.url)), `help page ${slug} exists`); + } +} + +// ── Every language, no file: the control row and the file panel ────────────── +for (const code of ['en', 'ru', 'zh', 'it']) { + zemaxCoatingsSession.reset(); + const t = makeLocale(code); + const z = t.zemaxCoatings; + const html = render(t); + const shown = text(html); + for (const key of ['tabCoatings', 'tabMaterials', 'tabExport', 'refWavelength', 'fileTitle', 'loadBtn', 'fileHint', 'noFile']) { + assert.ok(shown.includes(z[key]), `${code}: ${key} is shown`); + } + assert.ok(html.includes(`title="${z.refWaveHint.replace(/"/g, '"').replace(/'/g, ''')}"`), `${code}: λ₀ says what it converts`); + assert.ok(html.includes('tfs-spectrum-import-layout'), `${code}: the Coatings tab is an import layout, panel and preview`); + assert.equal((html.match(/aria-pressed="true"/g) || []).length, 1, `${code}: one tab is active`); +} + +// ── A loaded file ──────────────────────────────────────────────────────────── +const t = makeLocale('en'); +const z = t.zemaxCoatings; +const doc = parseZemaxCoating([ + 'MATE TIO2', '0.40 2.35 0', '0.80 2.25 0', + 'MATE TIO2', '0.40 2.45 0', '0.80 2.35 0', + 'MATE SIO2', '0.40 1.47 0', '0.80 1.45 0', + 'COAT AR', 'TIO2 0.25 0', 'SIO2 0.1 1', + 'COAT I.0.5', +].join('\n')); +const load = (patch) => { + zemaxCoatingsSession.reset(); + zemaxCoatingsSession.write(null, { doc, fileName: 'LAB.DAT', filePath: 'C:\\lab\\LAB.DAT', selCoating: 0, ...patch }); +}; + +// Coatings: the list in the panel, the selected stack beside it with its actions. +{ + load({ tab: 'coatings' }); + const html = render(t); + const shown = text(html); + for (const part of ['LAB.DAT', 'AR', 'I.0.5', z.typeStack, z.typeIdeal, z.layersHeader, z.importToFront, z.saveToLibrary]) { + assert.ok(shown.includes(part), `the Coatings tab shows ${part}`); + } + assert.ok(html.includes(`title="${z.saveToLibraryTip}"`), 'the library button says where the media come from'); + assert.deepEqual(buttons(html, [z.importToFront]), [[z.importToFront, false]], 'a design open, the import is on'); + assert.ok(!html.includes(attr(z.importNoDesign)), 'and nothing asks for one'); + assert.ok(shown.includes('100.00 nm'), 'an absolute thickness is shown in nm'); + // d = T·λ₀/n₀ at 550 nm, n₀ interpolated in each TIO2 table. + const thickness = (n0) => `${(0.25 * 550 / n0).toFixed(2)} nm`; + assert.ok(shown.includes(thickness(2.45 - 0.1 * 0.15 / 0.4)) && !shown.includes(thickness(2.35 - 0.1 * 0.15 / 0.4)), + 'a relative thickness uses the last record of a repeated name, as the import does'); +} + +// Materials: the selection actions in the panel, the records beside them, and +// the repeated name raised in the control row. +{ + load({ tab: 'materials', selRows: new Set([1]) }); + const html = render(t); + const shown = text(html); + for (const part of [z.selectAll, z.clearSel, z.importSelected, z.importAll, z.repeatedRow(1, 2), z.repeatedRow(2, 2)]) { + assert.ok(shown.includes(part), `the Materials tab shows ${part}`); + } + const checked = [...html.matchAll(/]*)>/g)].map(([, attrs]) => /checked/.test(attrs)); + assert.deepEqual(checked, [false, true, false], 'the selection is the second TIO2 row alone'); + assert.ok(html.includes(`title="${t.analysisChrome.notices}"`), 'the repeated name raises a notice'); +} + +// With no design selected there is nothing to import into: the import to the +// front coating is off and says why. The file still opens, the stack can still +// go to the Coating Library, and materials still go into a catalog. +{ + load({ tab: 'coatings' }); + const coatings = renderNoDesign(t); + assert.deepEqual(buttons(coatings, [z.importToFront, z.saveToLibrary, z.loadBtn]), + [[z.loadBtn, false], [z.importToFront, true], [z.saveToLibrary, false]]); + assert.ok(coatings.includes(` [m.name, m])); const at = (name, um) => byName[name].points.find(p => Math.abs(p[0] - um) < 1e-9); diff --git a/tests/zemax_reimport_merge.mjs b/tests/zemax_reimport_merge.mjs index 3bb7585b..2c2e94e9 100644 --- a/tests/zemax_reimport_merge.mjs +++ b/tests/zemax_reimport_merge.mjs @@ -37,6 +37,8 @@ const z = getLocale('en').zemaxCoatings; const mate = (name, n) => ({ name, points: [[0.4, n + 0.02, 0], [0.55, n, 0], [0.8, n - 0.01, 0]] }); const FILE = 'C:\\Zemax\\COATING.DAT'; const all = [mate('TIO2', 2.35), mate('SIO2', 1.46), mate('MGF2', 1.38), mate('TA2O5', 2.1)]; +// The rows of `all` with these names: the Materials tab selects records by row. +const rows = (...names) => new Set(names.map(name => all.findIndex(material => material.name === name))); const n550 = (id) => cm.getMaterialById(id).getNK(550)[0]; const keys = (catId) => Object.keys(cm.getCatalog(catId).materials).sort(); const lastWrite = (catId) => writes.filter(cat => cat.id === catId).at(-1); @@ -61,7 +63,7 @@ const design = { }; // A coating import from the same file that needs only SIO2 and MGF2. -const coat = importer.registerMaterials(all, 'COATING.DAT', new Set(['SIO2', 'MGF2']), FILE); +const coat = importer.registerMaterials(all, 'COATING.DAT', rows('SIO2', 'MGF2'), FILE); assert.equal(coat.catId, catId, 'the same path goes back into its own catalog'); assert.deepEqual(keys(catId), ['mgf2', 'my_ito', 'sio2', 'ta2o5', 'tio2'], 'nothing else of the catalog is dropped'); assert.deepEqual(Object.keys(lastWrite(catId).materials).sort(), ['mgf2', 'my_ito', 'sio2', 'ta2o5', 'tio2'], 'nor from its file'); @@ -73,16 +75,16 @@ for (const layer of design.frontLayers) { } // A coating import of a material the catalog holds in edited form uses the held one. -const coatTio2 = importer.registerMaterials([mate('TIO2', 2.3)], 'COATING.DAT', new Set(['TIO2']), FILE); +const coatTio2 = importer.registerMaterials([mate('TIO2', 2.3)], 'COATING.DAT', new Set([0]), FILE); assert.equal(coatTio2.nameMap.TIO2, `${catId}:tio2`); assert.equal(n550(`${catId}:tio2`), 2.45, 'a coating import does not overwrite a held material'); // A materials import finds the changed one and replaces it only when asked to. -const plan = importer.buildMaterialRegistration(all, 'COATING.DAT', new Set(['TIO2', 'TA2O5']), FILE); +const plan = importer.buildMaterialRegistration(all, 'COATING.DAT', rows('TIO2', 'TA2O5'), FILE); assert.deepEqual(plan.changed, ['TIO2'], 'the edited material is named as changed'); -importer.registerMaterials(all, 'COATING.DAT', new Set(['TIO2', 'TA2O5']), FILE); +importer.registerMaterials(all, 'COATING.DAT', rows('TIO2', 'TA2O5'), FILE); assert.equal(n550(`${catId}:tio2`), 2.45, 'without the go-ahead the held material stays'); -importer.registerMaterials(all, 'COATING.DAT', new Set(['TIO2', 'TA2O5']), FILE, { replaceChanged: true }); +importer.registerMaterials(all, 'COATING.DAT', rows('TIO2', 'TA2O5'), FILE, { replaceChanged: true }); assert.equal(n550(`${catId}:tio2`), 2.35, 'with it the file data replaces the held material'); assert.deepEqual(keys(catId), ['mgf2', 'my_ito', 'sio2', 'ta2o5', 'tio2'], 'under the same id'); @@ -93,7 +95,7 @@ cm.saveUserMaterial(catId, { ...cm.getCatalog(catId).materials.tio2, tabData: [[ let dialog = null; const importMaterials = runtime.render(() => useMaterialImportAction({ z, flash: (type, message) => flashes.push([type, message]), doc: { materials: all, coatings: [] }, - selMats: new Set(['TIO2']), fileName: 'COATING.DAT', filePath: FILE, setInputDialog: (d) => { dialog = d; }, + selRows: rows('TIO2'), fileName: 'COATING.DAT', filePath: FILE, setInputDialog: (d) => { dialog = d; }, })); const before = writes.length; importMaterials(false); @@ -113,8 +115,9 @@ cm.saveUserMaterial(catId, { ...cm.getCatalog(catId).materials.tio2, tabData: [[ { cm.saveUserMaterial(catId, { ...cm.getCatalog(catId).materials.sio2, tabData: [[400, 1.5, 0], [550, 1.49, 0], [800, 1.48, 0]] }); const updates = []; + const flashes = []; const importCoating = runtime.render(() => useCoatingImportAction({ - z, flash: () => {}, doc: { + z, flash: (type, message) => flashes.push([type, message]), doc: { materials: all, coatings: [{ name: 'AR', type: 'layers', layers: [{ material: 'SIO2', thickness: 0.25, isAbsolute: false }] }], }, @@ -127,6 +130,9 @@ cm.saveUserMaterial(catId, { ...cm.getCatalog(catId).materials.tio2, tabData: [[ assert.equal(n550(`${catId}:sio2`), 1.49, 'the held SIO2 is kept'); // The thickness comes from the file's own index, as Zemax computes it. assert.ok(Math.abs(layer.thickness - 0.25 * 550 / 1.46) < 1e-9, `thickness from the file's n (${layer.thickness})`); + const [type, report] = flashes.at(-1); + assert.equal(type, 'success'); + assert.ok(report.includes(`“${cm.getCatalog(catId).name}”`), `the report names the file's catalog: ${report}`); } // Another file of the same name is another catalog, with a name of its own. @@ -141,7 +147,7 @@ cm.addCatalog({ materials: { hfo2: { id: 'hfo2', name: 'HFO2', formulaNum: -1, tabData: [[400, 2.0, 0], [800, 1.95, 0]] } }, }); const oldUid = cm.getCatalog('zemax_coating').uid; -const legacy = importer.registerMaterials(all, 'COATING.DAT', new Set(['SIO2']), 'E:\\old\\COATING.DAT'); +const legacy = importer.registerMaterials(all, 'COATING.DAT', rows('SIO2'), 'E:\\old\\COATING.DAT'); assert.equal(legacy.catId, 'zemax_coating', 'an existing id is kept'); assert.deepEqual(keys('zemax_coating'), ['hfo2', 'sio2']); assert.equal(cm.getCatalog('zemax_coating').uid, oldUid); diff --git a/tests/zemax_same_name_materials.mjs b/tests/zemax_same_name_materials.mjs new file mode 100644 index 00000000..f446d6d8 --- /dev/null +++ b/tests/zemax_same_name_materials.mjs @@ -0,0 +1,149 @@ +/** + * A COATING.DAT that defines one material name twice, and one written in + * lower case. + * + * The Materials tab selects by row. Checking one of two records that share a + * name checks that record only, and each one imports on its own: the later + * one under a name that tells it apart, and importing both never replaces one + * with the other in the catalog. A name in lower case imports when its row is + * checked. A COAT layer naming the repeated material is still built with the + * last record of that name. The tab marks the repeated rows. + * + * Run: node tests/zemax_same_name_materials.mjs + */ +import assert from 'node:assert/strict'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { loadApp, makeLocale, makeTheme, shimBrowserGlobals } from './_uiShim.mjs'; +import { makeHookRuntime, importWithHookRuntime } from './_hookHarness.mjs'; + +shimBrowserGlobals(); +await loadApp(); + +const cm = await import('../src/utils/materials/catalogManager.js'); +const { parseZemaxCoating } = await import('../src/utils/io/zemaxCoatingFile.js'); +const { fileMaterialNames } = await import('../src/components/windows/dataExchange/zemaxCoatings/catalogImport.js'); +const { MaterialsTab } = await import('../src/components/windows/dataExchange/zemaxCoatings/MaterialsTab.js'); +const runtime = makeHookRuntime(); +const { useMaterialImportAction, useCoatingImportAction } = await importWithHookRuntime( + '../src/components/windows/dataExchange/zemaxCoatings/useImportActions.js', runtime); + +const c = makeTheme(); +const z = makeLocale('en').zemaxCoatings; + +// Two TIO2 records with different data, then a material named in lower case. +const doc = parseZemaxCoating([ + 'MATE TIO2', '0.40 2.35 0', '0.80 2.25 0', + 'MATE TIO2', '0.40 2.45 0', '0.80 2.35 0', + 'MATE sio2', '0.40 1.47 0', '0.80 1.45 0', + 'COAT AR', 'TIO2 0.1 1', 'sio2 0.1 1', +].join('\n')); +assert.equal(doc.materials.length, 3, 'the reader keeps both records of the repeated name'); + +cm.initCatalogs({}); +const catalogOf = (catName) => cm.getCatalogs().find(cat => cat.name === catName); +const n400 = (material) => cm.getMaterialById(material).getNK(400)[0]; +const byName = (cat) => Object.fromEntries(Object.entries(cat.materials).map(([id, m]) => [m.name, `${cat.id}:${id}`])); + +// ── How the records are told apart ────────────────────────────────────────── +assert.deepEqual(fileMaterialNames(doc.materials), [ + { name: 'TIO2', repeat: { index: 1, count: 2 } }, + { name: 'TIO2 (2)', repeat: { index: 2, count: 2 } }, + { name: 'sio2', repeat: null }, +]); +assert.deepEqual(fileMaterialNames([{ name: 'MgF2' }, { name: 'MGF2' }, { name: 'MGF2 (2)' }]).map(row => row.name), + ['MgF2', 'MGF2 (3)', 'MGF2 (2)'], 'Zemax names are case-insensitive, and a number the file already uses is skipped'); + +// ── Selection: one row, not every row of the name ─────────────────────────── +function tableRows(selRows, setSelRows = () => {}) { + const tab = MaterialsTab({ c, z, doc, selRows, setSelRows, importMaterials: () => {} }); + const table = tab.props.children.type(tab.props.children.props); + return table.props.children[1].props.children; +} +const checkedOf = rows => rows.map(row => row.props.children[0].props.children.props.checked); +{ + let selected = new Set(); + tableRows(selected, next => { selected = next; })[0].props.onClick(); + assert.deepEqual([...selected], [0], 'clicking the first TIO2 selects that row'); + assert.deepEqual(checkedOf(tableRows(selected)), [true, false, false], 'and checks it alone'); + tableRows(selected, next => { selected = next; })[1].props.onClick(); + tableRows(selected, next => { selected = next; })[0].props.onClick(); + assert.deepEqual(checkedOf(tableRows(selected)), [false, true, false], 'unchecking one leaves the other checked'); +} + +// ── The repeated rows are marked ──────────────────────────────────────────── +{ + const html = renderToStaticMarkup(React.createElement(MaterialsTab, { + c, z, doc, selRows: new Set(), setSelRows: () => {}, importMaterials: () => {}, + loading: false, onLoad: () => {}, fileName: 'SAME.DAT', panelWidth: null, setPanelWidth: () => {}, + })); + assert.ok(html.includes(z.repeatedRow(1, 2)) && html.includes(z.repeatedRow(2, 2)), 'both TIO2 rows are marked'); + assert.ok(html.includes(z.repeatedRowTip('TIO2 (2)')), 'the second says the name it imports under'); + assert.ok(!html.includes(z.repeatedRow(1, 1)), 'a name defined once is not marked'); +} + +function importRows(selRows, filePath, all = false) { + const flashes = []; + let dialog = null; + const importMaterials = runtime.render(() => useMaterialImportAction({ + z, flash: (type, message) => flashes.push([type, message]), doc, selRows, + fileName: 'SAME.DAT', filePath, setInputDialog: (d) => { dialog = d; }, + })); + importMaterials(all); + return { flash: flashes.at(-1), dialog }; +} + +// ── Import of each record, one at a time ──────────────────────────────────── +{ + const FILE = 'C:\\lib\\each\\SAME.DAT'; + const first = importRows(new Set([0]), FILE); + assert.equal(first.flash[1], z.importedMaterials(1, 'Zemax SAME'), 'the first record imports alone'); + const second = importRows(new Set([1]), FILE); + assert.equal(second.dialog, null, 'the second record is not offered as a replacement for the first'); + assert.equal(second.flash[1], z.importedMaterials(1, 'Zemax SAME')); + const names = byName(catalogOf('Zemax SAME')); + assert.deepEqual(Object.keys(names).sort(), ['TIO2', 'TIO2 (2)'], 'both records are in the catalog, told apart'); + assert.equal(n400(names.TIO2), 2.35); + assert.equal(n400(names['TIO2 (2)']), 2.45); +} + +// ── Import of both after one of them ──────────────────────────────────────── +{ + const FILE = 'C:\\lib\\both\\SAME.DAT'; + importRows(new Set([0]), FILE); + const both = importRows(new Set([0, 1]), FILE); + assert.equal(both.dialog, null, 'nothing of the catalog is offered for replacement'); + const names = byName(catalogOf('Zemax SAME 2')); + assert.deepEqual(Object.keys(names).sort(), ['TIO2', 'TIO2 (2)']); + assert.equal(n400(names.TIO2), 2.35, 'the first keeps its data'); + assert.equal(n400(names['TIO2 (2)']), 2.45, 'the second has its own'); + const again = importRows(null, FILE, true); + assert.equal(again.dialog, null, 'importing the whole file again changes nothing'); + assert.equal(Object.keys(catalogOf('Zemax SAME 2').materials).length, 3, 'and adds only sio2'); +} + +// ── A name in lower case imports when its row is checked ──────────────────── +{ + const lower = importRows(new Set([2]), 'C:\\lib\\lower\\SAME.DAT'); + assert.equal(lower.flash[0], 'success'); + assert.equal(lower.flash[1], z.importedMaterials(1, 'Zemax SAME 3'), 'one material, not none'); + const names = byName(catalogOf('Zemax SAME 3')); + assert.deepEqual(Object.keys(names), ['sio2']); + assert.equal(n400(names.sio2), 1.47); +} + +// ── A COAT layer naming the repeated material takes its last record ───────── +{ + const updates = []; + const importCoating = runtime.render(() => useCoatingImportAction({ + z, flash: () => {}, doc, selCoating: 0, fileName: 'SAME.DAT', filePath: 'C:\\lib\\coat\\SAME.DAT', + refNm: 550, checkpoint: () => {}, updateDesign: patch => updates.push(patch), + })); + importCoating(); + const [tio2, sio2] = updates[0].frontLayers; + assert.equal(n400(tio2.material), 2.45, 'the layer is built with the last TIO2 record'); + assert.equal(cm.getMaterialById(tio2.material).name, 'TIO2 (2)', 'which the design names as the tab does'); + assert.equal(n400(sio2.material), 1.47, 'the lower-case name resolves too'); + assert.deepEqual([tio2.thickness, sio2.thickness], [100, 100]); +} + +console.log('PASS: zemax_same_name_materials'); diff --git a/web/demo-shim.js b/web/demo-shim.js index 49cbdc73..671cc0ab 100644 --- a/web/demo-shim.js +++ b/web/demo-shim.js @@ -470,6 +470,8 @@ saveProcessFiles: () => Promise.resolve(fail('directory export is unavailable in the web demo')), zemaxPickCoatingFile: () => pickText('.dat,.DAT'), zemaxSaveCoatingFile: (text, suggestedName) => saveText(text, suggestedName || 'COATING.DAT', 'text/plain;charset=utf-8'), + codevPickCoatingFile: () => pickText('.seq,.mul,.SEQ,.MUL'), + codevSaveCoatingFile: (text, suggestedName) => saveText(text, suggestedName || 'coating.seq', 'text/plain;charset=utf-8'), spectrumPickFile: () => pickText('.csv,.txt,.asc,.prn,.dx,.jdx,.dat,.tsv'), spectrumSaveFile: (text, suggestedName) => saveText(text, suggestedName || 'spectrum.csv', 'text/csv;charset=utf-8'),